com.android.tools.idea.testing.AndroidGradleTestCase.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.testing.AndroidGradleTestCase.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.testing;

import com.android.tools.idea.IdeInfo;
import com.android.tools.idea.gradle.project.AndroidGradleProjectComponent;
import com.android.tools.idea.gradle.project.build.invoker.GradleBuildInvoker;
import com.android.tools.idea.gradle.project.build.invoker.GradleInvocationResult;
import com.android.tools.idea.gradle.project.importing.GradleProjectImporter;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncState;
import com.android.tools.idea.gradle.util.GradleWrapper;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.project.AndroidProjectInfo;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.sdk.Jdks;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.intellij.ide.highlighter.ModuleFileType;
import com.intellij.idea.IdeaTestApplication;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.EmptyModuleType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.TestDialog;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.testFramework.PlatformTestCase;
import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
import com.intellij.testFramework.fixtures.TestFixtureBuilder;
import com.intellij.util.Consumer;
import org.jetbrains.android.AndroidTestBase;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.annotation.RegEx;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.android.SdkConstants.*;
import static com.android.testutils.TestUtils.getSdk;
import static com.android.testutils.TestUtils.getWorkspaceFile;
import static com.android.tools.idea.gradle.util.Projects.isLegacyIdeaAndroidProject;
import static com.android.tools.idea.testing.FileSubject.file;
import static com.android.tools.idea.testing.TestProjectPaths.SIMPLE_APPLICATION;
import static com.google.common.io.Files.write;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction;
import static com.intellij.openapi.util.io.FileUtil.*;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static com.intellij.pom.java.LanguageLevel.JDK_1_8;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * Base class for unit tests that operate on Gradle projects
 *
 * TODO: After converting all tests over, check to see if there are any methods we can delete or
 * reduce visibility on.
 */
public abstract class AndroidGradleTestCase extends AndroidTestBase {
    private static final Logger LOG = Logger.getInstance(AndroidGradleTestCase.class);

    protected AndroidFacet myAndroidFacet;
    protected Modules myModules;

    public AndroidGradleTestCase() {
    }

    protected boolean createDefaultProject() {
        return true;
    }

    @NotNull
    protected File getProjectFolderPath() {
        String projectFolderPath = getProject().getBasePath();
        assertNotNull(projectFolderPath);
        return new File(projectFolderPath);
    }

    @NotNull
    protected File getBuildFilePath(@NotNull String moduleName) {
        File buildFilePath = new File(getProjectFolderPath(), join(moduleName, FN_BUILD_GRADLE));
        assertAbout(file()).that(buildFilePath).isFile();
        return buildFilePath;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();

        GradleProjectImporter.ourSkipSetupFromTest = true;

        if (createDefaultProject()) {
            TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder = IdeaTestFixtureFactory.getFixtureFactory()
                    .createFixtureBuilder(getName());
            myFixture = JavaTestFixtureFactory.getFixtureFactory()
                    .createCodeInsightFixture(projectBuilder.getFixture());
            myFixture.setUp();
            myFixture.setTestDataPath(getTestDataPath());
            ensureSdkManagerAvailable();
            setUpSdks();

            Project project = getProject();
            myModules = new Modules(project);
        } else {
            // This is necessary when we don't create a default project,
            // to ensure that the LocalFileSystemHolder is initialized.
            IdeaTestApplication.getInstance();

            ensureSdkManagerAvailable();
        }
    }

    private void setUpSdks() {
        // We seem to have two different locations where the SDK needs to be specified.
        // One is whatever is already defined in the JDK Table, and the other is the global one as defined by IdeSdks.
        // Gradle import will fail if the global one isn't set.
        File androidSdkPath = getSdk();

        IdeSdks ideSdks = IdeSdks.getInstance();
        runWriteCommandAction(getProject(), () -> {
            if (IdeInfo.getInstance().isAndroidStudio()) {
                ideSdks.setUseEmbeddedJdk();
                LOG.info("Set JDK to " + ideSdks.getJdkPath());
            }

            ideSdks.setAndroidSdkPath(androidSdkPath, getProject());
            LOG.info("Set IDE Sdk Path to " + androidSdkPath);
        });

        Sdk currentJdk = ideSdks.getJdk();
        assertNotNull(currentJdk);
        assertTrue("JDK 8 is required. Found: " + currentJdk.getHomePath(),
                Jdks.getInstance().isApplicableJdk(currentJdk, JDK_1_8));
    }

    @Override
    protected void tearDown() throws Exception {
        try {
            Messages.setTestDialog(TestDialog.DEFAULT);
            if (myFixture != null) {
                try {
                    Project project = myFixture.getProject();
                    // Since we don't really open the project, but we manually register listeners in the gradle importer
                    // by explicitly calling AndroidGradleProjectComponent#configureGradleProject, we need to counteract
                    // that here, otherwise the testsuite will leak
                    if (AndroidProjectInfo.getInstance(project).requiresAndroidModel()) {
                        AndroidGradleProjectComponent projectComponent = AndroidGradleProjectComponent
                                .getInstance(project);
                        projectComponent.projectClosed();
                    }
                } finally {
                    try {
                        myFixture.tearDown();
                    } catch (Exception e) {
                        LOG.warn("Failed to tear down " + myFixture.getClass().getSimpleName(), e);
                    }
                    myFixture = null;
                }
            }

            GradleProjectImporter.ourSkipSetupFromTest = false;

            ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
            Project[] openProjects = projectManager.getOpenProjects();
            if (openProjects.length > 0) {
                PlatformTestCase.closeAndDisposeProjectAndCheckThatNoOpenProjects(openProjects[0]);
            }
        } finally {
            try {
                assertEquals(0, ProjectManager.getInstance().getOpenProjects().length);
            } finally {
                //noinspection ThrowFromFinallyBlock
                super.tearDown();
            }
        }
    }

    @NotNull
    protected String loadProjectAndExpectSyncError(@NotNull String relativePath) throws Exception {
        prepareProjectForImport(relativePath);
        return requestSyncAndGetExpectedFailure();
    }

    protected void loadSimpleApplication() throws Exception {
        loadProject(SIMPLE_APPLICATION);
    }

    protected void loadProject(@NotNull String relativePath) throws Exception {
        loadProject(relativePath, null, null);
    }

    protected void loadProject(@NotNull String relativePath, @NotNull String chosenModuleName) throws Exception {

        loadProject(relativePath, null, chosenModuleName);
    }

    protected void loadProject(@NotNull String relativePath, @Nullable GradleSyncListener listener)
            throws Exception {
        loadProject(relativePath, listener, null);
    }

    protected void loadProject(@NotNull String relativePath, @Nullable GradleSyncListener listener,
            @Nullable String chosenModuleName) throws Exception {
        prepareProjectForImport(relativePath);
        Project project = getProject();
        File projectRoot = virtualToIoFile(project.getBaseDir());

        importProject(project.getName(), projectRoot, listener);

        assertTrue(AndroidProjectInfo.getInstance(project).requiresAndroidModel());
        assertFalse(isLegacyIdeaAndroidProject(project));

        ModuleManager moduleManager = ModuleManager.getInstance(project);
        Module[] modules = moduleManager.getModules();

        // if module name is specified, find it
        if (chosenModuleName != null) {
            for (Module module : modules) {
                if (chosenModuleName.equals(module.getName())) {
                    myAndroidFacet = AndroidFacet.getInstance(module);
                    break;
                }
            }
        }

        if (myAndroidFacet == null) {
            // then try and find a non-lib facet
            for (Module module : modules) {
                AndroidFacet androidFacet = AndroidFacet.getInstance(module);
                if (androidFacet != null && androidFacet.isAppProject()) {
                    myAndroidFacet = androidFacet;
                    break;
                }
            }
        }

        // then try and find ANY android facet
        if (myAndroidFacet == null) {
            for (Module module : modules) {
                myAndroidFacet = AndroidFacet.getInstance(module);
                if (myAndroidFacet != null) {
                    break;
                }
            }
        }
        refreshProjectFiles();
    }

    protected void prepareProjectForImport(@NotNull String relativePath) throws IOException {
        File root = new File(getTestDataPath(), toSystemDependentName(relativePath));
        assertTrue(root.getPath(), root.exists());

        File build = new File(root, FN_BUILD_GRADLE);
        File settings = new File(root, FN_SETTINGS_GRADLE);
        assertTrue("Couldn't find build.gradle or settings.gradle in " + root.getPath(),
                build.exists() || settings.exists());

        // Sync the model
        Project project = myFixture.getProject();
        File projectRoot = virtualToIoFile(project.getBaseDir());
        copyDir(root, projectRoot);

        // We need the wrapper for import to succeed
        createGradleWrapper(projectRoot);

        // Override settings just for tests (e.g. sdk.dir)
        updateLocalProperties();

        // Update dependencies to latest, and possibly repository URL too if android.mavenRepoUrl is set
        updateGradleVersions(projectRoot);
    }

    @NotNull
    protected GradleInvocationResult generateSources() throws InterruptedException {
        return invokeGradle(getProject(), gradleInvoker -> gradleInvoker.generateSources(false));
    }

    protected static GradleInvocationResult invokeGradleTasks(@NotNull Project project, @NotNull String... tasks)
            throws InterruptedException {
        assertThat(tasks).named("Gradle tasks").isNotEmpty();
        return invokeGradle(project, gradleInvoker -> gradleInvoker.executeTasks(Lists.newArrayList(tasks)));
    }

    @NotNull
    private static GradleInvocationResult invokeGradle(@NotNull Project project,
            @NotNull Consumer<GradleBuildInvoker> gradleInvocationTask) throws InterruptedException {
        Ref<GradleInvocationResult> resultRef = new Ref<>();
        CountDownLatch latch = new CountDownLatch(1);
        GradleBuildInvoker gradleBuildInvoker = GradleBuildInvoker.getInstance(project);

        GradleBuildInvoker.AfterGradleInvocationTask task = result -> {
            resultRef.set(result);
            latch.countDown();
        };

        gradleBuildInvoker.add(task);

        try {
            gradleInvocationTask.consume(gradleBuildInvoker);
        } finally {
            gradleBuildInvoker.remove(task);
        }

        latch.await();
        GradleInvocationResult result = resultRef.get();
        assert result != null;
        return result;
    }

    public static void updateGradleVersions(@NotNull File file) throws IOException {
        updateGradleVersions(file, getLocalRepositories());
    }

    private static void updateGradleVersions(@NotNull File file, @NotNull String localRepositories)
            throws IOException {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File child : files) {
                    updateGradleVersions(child, localRepositories);
                }
            }
        } else if (file.getPath().endsWith(DOT_GRADLE) && file.isFile()) {
            String contentsOrig = Files.toString(file, Charsets.UTF_8);
            String contents = contentsOrig;

            BuildEnvironment buildEnvironment = BuildEnvironment.getInstance();

            contents = replaceRegexGroup(contents, "classpath ['\"]com.android.tools.build:gradle:(.+)['\"]",
                    buildEnvironment.getGradlePluginVersion());
            contents = replaceRegexGroup(contents,
                    "classpath ['\"]com.android.tools.build:gradle-experimental:(.+)['\"]",
                    buildEnvironment.getExperimentalPluginVersion());

            contents = replaceRegexGroup(contents, "buildToolsVersion ['\"](.+)['\"]",
                    buildEnvironment.getBuildToolsVersion());
            contents = replaceRegexGroup(contents, "compileSdkVersion ([0-9]+)",
                    buildEnvironment.getCompileSdkVersion());
            contents = replaceRegexGroup(contents, "targetSdkVersion ([0-9]+)",
                    buildEnvironment.getTargetSdkVersion());
            contents = contents.replaceAll("repositories[ ]+\\{", "repositories {\n" + localRepositories);

            if (!contents.equals(contentsOrig)) {
                write(contents, file, Charsets.UTF_8);
            }
        }
    }

    @NotNull
    protected static String getLocalRepositories() {
        return getLocalRepository("prebuilts/tools/common/m2/repository")
                + getLocalRepository("prebuilts/tools/common/offline-m2");
    }

    @NotNull
    protected static String getLocalRepository(@NotNull String dir) {
        String uri = getWorkspaceFile(dir).toURI().toString();
        return "maven { url \"" + uri + "\" }\n";
    }

    /**
     * Take a regex pattern with a single group in it and replace the contents of that group with a
     * new value.
     *
     * For example, the pattern "Version: (.+)" with value "Test" would take the input string
     * "Version: Production" and change it to "Version: Test"
     *
     * The reason such a special-case pattern substitution utility method exists is this class is
     * responsible for loading read-only gradle test files and copying them over into a mutable
     * version for tests to load. When doing so, it updates obsolete values (like old android
     * platforms) to more current versions. This lets tests continue to run whenever we update our
     * tools to the latest versions, without having to go back and change a bunch of broken tests
     * each time.
     *
     * If a regex is passed in with more than one group, later groups will be ignored; and if no
     * groups are present, this will throw an exception. It is up to the caller to ensure that the
     * regex is well formed and only includes a single group.
     *
     * @return The {@code contents} string, modified by the replacement {@code value}, (unless no
     * {@code regex} match was found).
     */
    @NotNull
    private static String replaceRegexGroup(String contents, @RegEx String regex, String value) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(contents);
        if (matcher.find()) {
            contents = contents.substring(0, matcher.start(1)) + value + contents.substring(matcher.end(1));
        }
        return contents;
    }

    private void updateLocalProperties() throws IOException {
        LocalProperties localProperties = new LocalProperties(getProject());
        File sdkPath = getSdk();
        assertAbout(file()).that(sdkPath).named("Android SDK path").isDirectory();
        localProperties.setAndroidSdkPath(sdkPath.getPath());
        localProperties.save();
    }

    protected static void createGradleWrapper(File projectRoot) throws IOException {
        GradleWrapper wrapper = GradleWrapper.create(projectRoot);
        File path = getWorkspaceFile("tools/external/gradle/gradle-" + GRADLE_LATEST_VERSION + "-bin.zip");
        assertAbout(file()).that(path).named("Gradle distribution path").isFile();
        wrapper.updateDistributionUrl(path);
    }

    protected void importProject(@NotNull String projectName, @NotNull File projectRoot,
            @Nullable GradleSyncListener listener) throws Exception {
        Ref<Throwable> throwableRef = new Ref<>();
        SyncListener syncListener = new SyncListener();

        Project project = getProject();
        GradleSyncState.subscribe(project, syncListener);

        runWriteCommandAction(project, () -> {
            try {
                // When importing project for tests we do not generate the sources as that triggers a compilation which finishes asynchronously.
                // This causes race conditions and intermittent errors. If a test needs source generation this should be handled separately.
                GradleProjectImporter.Request request = new GradleProjectImporter.Request();
                request.setProject(project).setGenerateSourcesOnSuccess(false);
                GradleProjectImporter.getInstance().importProject(projectName, projectRoot, request, listener);
            } catch (Throwable e) {
                throwableRef.set(e);
            }
        });

        Throwable throwable = throwableRef.get();
        if (throwable != null) {
            if (throwable instanceof IOException) {
                throw (IOException) throwable;
            } else if (throwable instanceof ConfigurationException) {
                throw (ConfigurationException) throwable;
            } else {
                throw new RuntimeException(throwable);
            }
        }

        syncListener.await();
        if (syncListener.failureMessage != null && listener == null) {
            fail(syncListener.failureMessage);
        }
    }

    @NotNull
    protected AndroidModuleModel getModel() {
        AndroidModuleModel model = AndroidModuleModel.get(myAndroidFacet);
        assert model != null;
        return model;
    }

    @NotNull
    protected String getTextForFile(@NotNull String relativePath) {
        Project project = getProject();
        VirtualFile file = project.getBaseDir().findFileByRelativePath(relativePath);
        if (file != null) {
            PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
            if (psiFile != null) {
                return psiFile.getText();
            }
        }

        return "";
    }

    protected void requestSyncAndWait(@NotNull GradleSyncInvoker.Request request) throws Exception {
        SyncListener syncListener = requestSync(request);
        checkStatus(syncListener);
    }

    protected void requestSyncAndWait() throws Exception {
        SyncListener syncListener = requestSync();
        checkStatus(syncListener);
    }

    private static void checkStatus(@NotNull SyncListener syncListener) {
        if (!syncListener.success) {
            String cause = syncListener.failureMessage;
            if (isEmpty(cause)) {
                cause = "<Unknown>";
            }
            fail(cause);
        }
    }

    @NotNull
    protected String requestSyncAndGetExpectedFailure() throws Exception {
        SyncListener syncListener = requestSync();
        assertFalse(syncListener.success);
        String message = syncListener.failureMessage;
        assertNotNull(message);
        return message;
    }

    @NotNull
    private SyncListener requestSync() throws Exception {
        GradleSyncInvoker.Request request = new GradleSyncInvoker.Request().setGenerateSourcesOnSuccess(false);
        return requestSync(request);
    }

    @NotNull
    private SyncListener requestSync(@NotNull GradleSyncInvoker.Request request) throws Exception {
        SyncListener syncListener = new SyncListener();
        refreshProjectFiles();

        GradleSyncInvoker.getInstance().requestProjectSync(getProject(), request, syncListener);

        syncListener.await();
        return syncListener;
    }

    private static void refreshProjectFiles() {
        // With IJ14 code base, we run tests with NO_FS_ROOTS_ACCESS_CHECK turned on. I'm not sure if that
        // is the cause of the issue, but not all files inside a project are seen while running unit tests.
        // This explicit refresh of the entire project fix such issues (e.g. AndroidProjectViewTest).
        // This refresh must be synchronous and recursive so it is completed before continuing the test and clean everything so indexes are
        // properly updated. Apparently this solves outdated indexes and stubs problems
        LocalFileSystem.getInstance().refresh(false /* synchronous */);
    }

    @NotNull
    protected Module createModule(@NotNull String name) {
        return createModule(name, EmptyModuleType.getInstance());
    }

    @NotNull
    protected Module createModule(@NotNull String name, @NotNull ModuleType type) {
        VirtualFile projectRootFolder = getProject().getBaseDir();
        assertNotNull(projectRootFolder);
        final File moduleFile = new File(virtualToIoFile(projectRootFolder),
                name + ModuleFileType.DOT_DEFAULT_EXTENSION);
        createIfDoesntExist(moduleFile);
        return new WriteAction<Module>() {
            @Override
            protected void run(@NotNull Result<Module> result) throws Throwable {
                VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(moduleFile);
                assertNotNull(virtualFile);
                Module module = ModuleManager.getInstance(getProject()).newModule(virtualFile.getPath(),
                        type.getId());
                module.getModuleFile();
                result.setResult(module);
            }
        }.execute().getResultObject();
    }

    private static class SyncListener extends GradleSyncListener.Adapter {
        @NotNull
        private final CountDownLatch myLatch;

        boolean success;
        String failureMessage;

        SyncListener() {
            myLatch = new CountDownLatch(1);
        }

        @Override
        public void syncSucceeded(@NotNull Project project) {
            success = true;
            myLatch.countDown();
        }

        @Override
        public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
            failureMessage = errorMessage;
            myLatch.countDown();
        }

        void await() throws InterruptedException {
            myLatch.await(5, MINUTES);
        }
    }
}