com.android.tools.idea.tests.gui.gradle.GradleSyncTest.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.tests.gui.gradle.GradleSyncTest.java

Source

/*
 * Copyright (C) 2014 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.tests.gui.gradle;

import com.android.sdklib.IAndroidTarget;
import com.android.testutils.TestUtils;
import com.android.tools.idea.gradle.dsl.model.GradleBuildModel;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.android.tools.idea.gradle.project.GradleExperimentalSettings;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncState;
import com.android.tools.idea.gradle.projectView.AndroidTreeStructureProvider;
import com.android.tools.idea.gradle.util.GradleProperties;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.tests.gui.framework.GuiTestRule;
import com.android.tools.idea.tests.gui.framework.GuiTestRunner;
import com.android.tools.idea.tests.gui.framework.RunIn;
import com.android.tools.idea.tests.gui.framework.TestGroup;
import com.android.tools.idea.tests.gui.framework.fixture.*;
import com.android.tools.idea.tests.gui.framework.fixture.EditorFixture.Tab;
import com.android.tools.idea.tests.gui.framework.fixture.MessagesToolWindowFixture.ContentFixture;
import com.android.tools.idea.tests.gui.framework.fixture.MessagesToolWindowFixture.HyperlinkFixture;
import com.android.tools.idea.tests.gui.framework.fixture.MessagesToolWindowFixture.MessageFixture;
import com.android.tools.idea.tests.gui.framework.fixture.gradle.ChooseGradleHomeDialogFixture;
import com.google.common.collect.Lists;
import com.intellij.ide.projectView.TreeStructureProvider;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.util.net.HttpConfigurable;
import org.fest.swing.edt.GuiQuery;
import org.fest.swing.edt.GuiTask;
import org.fest.swing.timing.Wait;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.settings.GradleSettings;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.tools.idea.gradle.dsl.model.dependencies.CommonConfigurationNames.ANDROID_TEST_COMPILE;
import static com.android.tools.idea.gradle.dsl.model.dependencies.CommonConfigurationNames.COMPILE;
import static com.android.tools.idea.gradle.util.FilePaths.findParentContentEntry;
import static com.android.tools.idea.gradle.util.FilePaths.pathToUrl;
import static com.android.tools.idea.gradle.util.GradleUtil.getGradleBuildFile;
import static com.android.tools.idea.gradle.util.PropertiesFiles.getProperties;
import static com.android.tools.idea.testing.FileSubject.file;
import static com.android.tools.idea.tests.gui.framework.GuiTests.*;
import static com.android.tools.idea.tests.gui.framework.fixture.MessagesToolWindowFixture.MessageMatcher.firstLineStartingWith;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.intellij.ide.errorTreeView.ErrorTreeElementKind.ERROR;
import static com.intellij.ide.errorTreeView.ErrorTreeElementKind.WARNING;
import static com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction;
import static com.intellij.openapi.roots.OrderRootType.CLASSES;
import static com.intellij.openapi.roots.OrderRootType.SOURCES;
import static com.intellij.openapi.util.io.FileUtil.*;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile;
import static com.intellij.openapi.vfs.VfsUtilCore.isAncestor;
import static com.intellij.openapi.vfs.VfsUtilCore.urlToPath;
import static com.intellij.pom.java.LanguageLevel.JDK_1_8;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.fest.swing.edt.GuiActionRunner.execute;
import static org.jetbrains.android.AndroidPlugin.getGuiTestSuiteState;
import static org.junit.Assert.*;

@RunIn(TestGroup.PROJECT_SUPPORT)
@RunWith(GuiTestRunner.class)
public class GradleSyncTest {

    @Rule
    public final GuiTestRule guiTest = new GuiTestRule();

    private static final String ANDROID_SDK_MANAGER_DIALOG_TITLE = "Android SDK Manager";
    private static final String GRADLE_SETTINGS_DIALOG_TITLE = "Gradle Settings";
    private static final String GRADLE_SYNC_DIALOG_TITLE = "Gradle Sync";

    @Before
    public void skipSourceGenerationOnSync() {
        GradleExperimentalSettings.getInstance().SKIP_SOURCE_GEN_ON_PROJECT_SYNC = true;
    }

    @Test
    // See https://code.google.com/p/android/issues/detail?id=183368
    public void withTestOnlyInterModuleDependencies() throws IOException {
        guiTest.importMultiModule();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Module appModule = guiTest.ideFrame().getModule("app");

        // Set a dependency on a module that does not exist.
        execute(new GuiTask() {
            @Override
            protected void executeInEDT() throws Throwable {
                runWriteCommandAction(ideFrame.getProject(), () -> {
                    GradleBuildModel buildModel = GradleBuildModel.get(appModule);
                    buildModel.dependencies().addModule(ANDROID_TEST_COMPILE, ":library3");
                    buildModel.applyChanges();
                });
            }
        });

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        for (OrderEntry entry : ModuleRootManager.getInstance(appModule).getOrderEntries()) {
            if (entry instanceof ModuleOrderEntry) {
                ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry) entry;
                if ("library3".equals(moduleOrderEntry.getModuleName())) {
                    assertEquals(DependencyScope.TEST, moduleOrderEntry.getScope());
                    return;
                }
            }
        }
        fail("No dependency for library3 found");
    }

    @Test
    public void jdkNodeModificationInProjectView() throws IOException {
        guiTest.importSimpleApplication();

        Project project = guiTest.ideFrame().getProject();
        AndroidTreeStructureProvider treeStructureProvider = null;
        TreeStructureProvider[] treeStructureProviders = Extensions.getExtensions(TreeStructureProvider.EP_NAME,
                project);
        for (TreeStructureProvider current : treeStructureProviders) {
            if (current instanceof AndroidTreeStructureProvider) {
                treeStructureProvider = (AndroidTreeStructureProvider) current;
            }
        }

        List<AbstractTreeNode> changedNodes = Lists.newArrayList();
        treeStructureProvider.addChangeListener((parent, newChildren) -> changedNodes.add(parent));

        ProjectViewFixture projectView = guiTest.ideFrame().getProjectView();
        ProjectViewFixture.PaneFixture projectPane = projectView.selectProjectPane();
        ProjectViewFixture.NodeFixture externalLibrariesNode = projectPane.findExternalLibrariesNode();
        projectPane.expand();

        // 2 nodes should be changed: JDK (remove all children except rt.jar) and rt.jar (remove all children except packages 'java' and
        // 'javax'.
        Wait.seconds(1).expecting("'Project View' to be customized").until(() -> changedNodes.size() == 2);

        List<ProjectViewFixture.NodeFixture> libraryNodes = externalLibrariesNode.getChildren();

        ProjectViewFixture.NodeFixture jdkNode = null;
        // Find JDK node.
        for (ProjectViewFixture.NodeFixture node : libraryNodes) {
            if (node.isJdk()) {
                jdkNode = node;
                break;
            }
        }

        ProjectViewFixture.NodeFixture finalJdkNode = jdkNode;
        Wait.seconds(1).expecting("JDK node to be customized").until(() -> finalJdkNode.getChildren().size() == 1);

        // Now we verify that the JDK node has only these children:
        // - jdk
        //   - rt.jar
        //     - java
        //     - javax
        List<ProjectViewFixture.NodeFixture> jdkChildren = jdkNode.getChildren();
        assertThat(jdkChildren).hasSize(1);

        ProjectViewFixture.NodeFixture rtJarNode = jdkChildren.get(0);
        rtJarNode.requireDirectory("rt.jar");

        List<ProjectViewFixture.NodeFixture> rtJarChildren = rtJarNode.getChildren();
        assertThat(rtJarChildren).hasSize(2);

        rtJarChildren.get(0).requireDirectory("java");
        rtJarChildren.get(1).requireDirectory("javax");
    }

    @Test
    public void updatingGradleVersionWithLocalDistribution() throws IOException {
        File unsupportedGradleHome = getUnsupportedGradleHomeOrSkipTest();
        File gradleHomePath = getGradleHomePathOrSkipTest();

        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        ideFrame.deleteGradleWrapper().useLocalGradleDistribution(unsupportedGradleHome).requestProjectSync();

        // Expect message suggesting to use Gradle wrapper. Click "Cancel" to use local distribution.
        ideFrame.findMessageDialog(GRADLE_SYNC_DIALOG_TITLE).clickCancel();

        ChooseGradleHomeDialogFixture chooseGradleHomeDialog = ChooseGradleHomeDialogFixture.find(guiTest.robot());
        chooseGradleHomeDialog.chooseGradleHome(gradleHomePath).clickOk().requireNotShowing();

        ideFrame.waitForGradleProjectSyncToFinish();
    }

    @Test
    public void userFriendlyErrorWhenUsingUnsupportedVersionOfGradle() throws IOException {
        File unsupportedGradleHome = getUnsupportedGradleHomeOrSkipTest();

        guiTest.importMultiModule();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        ideFrame.deleteGradleWrapper().useLocalGradleDistribution(unsupportedGradleHome).requestProjectSync();

        // Expect message suggesting to use Gradle wrapper. Click "OK" to use wrapper.
        ideFrame.findMessageDialog(GRADLE_SYNC_DIALOG_TITLE).clickOk();

        ideFrame.waitForGradleProjectSyncToStart().waitForGradleProjectSyncToFinish().requireGradleWrapperSet();
    }

    // See https://code.google.com/p/android/issues/detail?id=74259
    @Test
    public void withCentralBuildDirectoryInRootModule() throws IOException {
        // In issue 74259, project sync fails because the "app" build directory is set to "CentralBuildDirectory/central/build", which is
        // outside the content root of the "app" module.
        String projectDirName = "CentralBuildDirectory";
        File projectPath = new File(getProjectCreationDirPath(), projectDirName);

        // The bug appears only when the central build folder does not exist.
        File centralBuildDirPath = new File(projectPath, join("central", "build"));
        File centralBuildParentDirPath = centralBuildDirPath.getParentFile();
        delete(centralBuildParentDirPath);

        guiTest.importProjectAndWaitForProjectSyncToFinish(projectDirName);
        Module app = guiTest.ideFrame().getModule("app");

        // Now we have to make sure that if project import was successful, the build folder (with custom path) is excluded in the IDE (to
        // prevent unnecessary file indexing, which decreases performance.)
        File[] excludeFolderPaths = GuiQuery.getNonNull(() -> {
            ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(app);
            ModifiableRootModel rootModel = moduleRootManager.getModifiableModel();
            try {
                ContentEntry[] contentEntries = rootModel.getContentEntries();
                ContentEntry parent = findParentContentEntry(centralBuildDirPath, contentEntries);

                List<File> paths = Lists.newArrayList();

                for (ExcludeFolder excluded : parent.getExcludeFolders()) {
                    String path = urlToPath(excluded.getUrl());
                    if (isNotEmpty(path)) {
                        paths.add(new File(toSystemDependentName(path)));
                    }
                }
                return paths.toArray(new File[paths.size()]);
            } finally {
                rootModel.dispose();
            }
        });

        assertThat(excludeFolderPaths).isNotEmpty();

        boolean isExcluded = false;
        for (File path : notNullize(excludeFolderPaths)) {
            if (isAncestor(centralBuildParentDirPath, path, true)) {
                isExcluded = true;
                break;
            }
        }

        assertTrue(String.format("Folder '%1$s' should be excluded", centralBuildDirPath.getPath()), isExcluded);
    }

    // See https://code.google.com/p/android/issues/detail?id=74341
    @Test
    public void editorShouldFindAppCompatStyle() throws IOException {
        guiTest.importProjectAndWaitForProjectSyncToFinish("AarDependency");
        EditorFixture editor = guiTest.ideFrame().getEditor();

        editor.open("app/src/main/res/values/strings.xml", Tab.EDITOR);
        editor.waitForCodeAnalysisHighlightCount(HighlightSeverity.ERROR, 0);
    }

    @Ignore("http://b.android.com/222226")
    @Test
    public void moduleSelectionOnImport() throws IOException {
        GradleExperimentalSettings.getInstance().SELECT_MODULES_ON_PROJECT_IMPORT = true;
        guiTest.importProject("Flavoredlib");

        ModulesToImportDialogFixture projectSubsetDialog = ModulesToImportDialogFixture.find(guiTest.robot());
        projectSubsetDialog.setSelected("lib", false).clickOk();

        IdeFrameFixture ideFrame = guiTest.ideFrame();
        ideFrame.waitForGradleProjectSyncToFinish();

        // Verify that "lib" (which was unchecked in the "Select Modules to Include" dialog) is not a module.
        assertThat(ideFrame.getModuleNames()).containsExactly("Flavoredlib", "app");

        // subsequent project syncs should respect module selection
        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();
        assertThat(ideFrame.getModuleNames()).containsExactly("Flavoredlib", "app");
    }

    // See https://code.google.com/p/android/issues/detail?id=165576
    @Test
    public void javaModelSerialization() throws IOException {
        guiTest.importProjectAndWaitForProjectSyncToFinish("MultipleModuleTypes");

        guiTest.ideFrame().requestProjectSync().waitForGradleProjectSyncToFinish().closeProject();

        guiTest.importProjectAndWaitForProjectSyncToFinish("MultipleModuleTypes");

        LibraryTable libraryTable = ProjectLibraryTable.getInstance(guiTest.ideFrame().getProject());
        // When serialization of Java model fails, libraries are not set up.
        // Here we confirm that serialization works, because the Java module has the dependency declared in its build.gradle file.
        assertThat(libraryTable.getLibraries()).asList().hasSize(1);
    }

    // See https://code.google.com/p/android/issues/detail?id=167378
    @Test
    public void interJavaModuleDependencies() throws IOException {
        guiTest.importMultiModule();

        Module library = guiTest.ideFrame().getModule("library");
        ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(library);

        // Verify that the module "library" depends on module "library2"
        ModuleOrderEntry moduleDependency = null;
        for (OrderEntry orderEntry : moduleRootManager.getOrderEntries()) {
            if (orderEntry instanceof ModuleOrderEntry) {
                moduleDependency = (ModuleOrderEntry) orderEntry;
                break;
            }
        }

        assertThat(moduleDependency.getModuleName()).isEqualTo("library2");
    }

    // See https://code.google.com/p/android/issues/detail?id=169778
    @Test
    public void javaToAndroidModuleDependencies() throws IOException {
        guiTest.importMultiModule();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Module library3 = ideFrame.getModule("library3");
        assertNull(AndroidFacet.getInstance(library3));

        File library3BuildFile = new File(ideFrame.getProjectPath(), join("library3", FN_BUILD_GRADLE));
        assertAbout(file()).that(library3BuildFile).isFile();
        appendToFile(library3BuildFile, "dependencies { compile project(':app') }");

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(library3);
        // Verify that the module "library3" doesn't depend on module "app"
        ModuleOrderEntry moduleDependency = null;
        for (OrderEntry orderEntry : moduleRootManager.getOrderEntries()) {
            if (orderEntry instanceof ModuleOrderEntry) {
                moduleDependency = (ModuleOrderEntry) orderEntry;
                break;
            }
        }

        assertNull(moduleDependency);

        ContentFixture syncMessages = ideFrame.getMessagesToolWindow().getGradleSyncContent();
        MessageFixture message = syncMessages.findMessage(WARNING,
                firstLineStartingWith("Ignoring dependency of module 'app' on module 'library3'."));

        // Verify if the error message's link goes to the build file.
        VirtualFile buildFile = getGradleBuildFile(library3);
        message.requireLocation(new File(buildFile.getPath()), 0);
    }

    // See https://code.google.com/p/android/issues/detail?id=73087
    @Test
    public void withUserDefinedLibraryAttachments() throws IOException {
        guiTest.importProjectAndWaitForProjectSyncToFinish("MultipleModuleTypes");

        File javadocJarPath = new File(guiTest.getProjectPath(), "fake-javadoc.jar");
        try (ZipOutputStream zos = new ZipOutputStream(
                new BufferedOutputStream(new FileOutputStream(javadocJarPath)))) {
            zos.putNextEntry(new ZipEntry("allclasses-frame.html"));
            zos.putNextEntry(new ZipEntry("allclasses-noframe.html"));
        }
        refreshFiles();

        IdeFrameFixture ideFrame = guiTest.ideFrame();

        LibraryPropertiesDialogFixture propertiesDialog = ideFrame.showPropertiesForLibrary("guava-18.0");
        propertiesDialog.addAttachment(javadocJarPath).clickOk();

        guiTest.waitForBackgroundTasks();

        String javadocJarUrl = pathToUrl(javadocJarPath.getPath());

        // Verify that the library has the Javadoc attachment we just added.
        LibraryFixture library = propertiesDialog.getLibrary();
        library.requireJavadocUrls(javadocJarUrl);

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        // Verify that the library still has the Javadoc attachment after sync.
        library = propertiesDialog.getLibrary();
        library.requireJavadocUrls(javadocJarUrl);
    }

    // See https://code.google.com/p/android/issues/detail?id=169743
    // JVM settings for Gradle should be cleared before any invocation to Gradle.
    @Test
    public void shouldClearJvmArgsOnSyncAndBuild() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Project project = ideFrame.getProject();

        GradleProperties gradleProperties = new GradleProperties(project);
        gradleProperties.clear();
        gradleProperties.save();

        VirtualFile gradlePropertiesFile = findFileByIoFile(gradleProperties.getPath(), true);
        ideFrame.getEditor().open(gradlePropertiesFile, Tab.DEFAULT);

        String jvmArgs = "-Xmx2048m";
        ideFrame.setGradleJvmArgs(jvmArgs);

        ideFrame.requestProjectSync();

        // Copy JVM args to gradle.properties file.
        ideFrame.findMessageDialog(GRADLE_SETTINGS_DIALOG_TITLE).clickYes();

        // Verify JVM args were removed from IDE's Gradle settings.
        ideFrame.waitForGradleProjectSyncToFinish();
        assertNull(GradleSettings.getInstance(project).getGradleVmOptions());

        // Verify JVM args were copied to gradle.properties file
        refreshFiles();

        gradleProperties = new GradleProperties(project);
        assertEquals(jvmArgs, gradleProperties.getJvmArgs());
    }

    // Verifies that the IDE, during sync, asks the user to copy IDE proxy settings to gradle.properties, if applicable.
    // See https://code.google.com/p/android/issues/detail?id=65325
    @Test
    public void withIdeProxySettings() throws IOException {
        System.getProperties().setProperty("show.do.not.copy.http.proxy.settings.to.gradle", "true");

        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        File gradlePropertiesPath = new File(ideFrame.getProjectPath(), "gradle.properties");
        createIfNotExists(gradlePropertiesPath);

        String host = "myproxy.test.com";
        int port = 443;

        HttpConfigurable ideSettings = HttpConfigurable.getInstance();
        ideSettings.USE_HTTP_PROXY = true;
        ideSettings.PROXY_HOST = host;
        ideSettings.PROXY_PORT = port;

        ideFrame.requestProjectSync();

        // Expect IDE to ask user to copy proxy settings.
        ProxySettingsDialogFixture proxyDialog = ProxySettingsDialogFixture.find(guiTest.robot());
        proxyDialog.setDoNotShowThisDialog(true);
        proxyDialog.clickOk();

        ideFrame.waitForGradleProjectSyncToStart().waitForGradleProjectSyncToFinish();

        // Verify gradle.properties has proxy settings.
        assertAbout(file()).that(gradlePropertiesPath).isFile();

        Properties gradleProperties = getProperties(gradlePropertiesPath);
        assertEquals(host, gradleProperties.getProperty("systemProp.http.proxyHost"));
        assertEquals(String.valueOf(port), gradleProperties.getProperty("systemProp.http.proxyPort"));

        // Verifies that the "Do not show this dialog in the future" does not show up. If it does show up the test will timeout and fail.
        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();
    }

    // Verifies that the IDE switches SDKs if the IDE and project SDKs are not the same.
    @Test
    public void sdkSwitch() throws IOException {
        File secondSdkPath = getFilePathPropertyOrSkipTest("second.android.sdk.path",
                "the path of a secondary Android SDK", true);

        getGuiTestSuiteState().setSkipSdkMerge(true);

        IdeSdks ideSdks = IdeSdks.getInstance();
        File originalSdkPath = ideSdks.getAndroidSdkPath();

        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        // Change the SDK in the project. We expect the IDE to have the same SDK as the project.
        LocalProperties localProperties = new LocalProperties(ideFrame.getProject());
        localProperties.setAndroidSdkPath(secondSdkPath);
        localProperties.save();

        ideFrame.requestProjectSync();

        MessagesFixture messages = ideFrame.findMessageDialog(ANDROID_SDK_MANAGER_DIALOG_TITLE);
        messages.click("Use Project's SDK");

        ideFrame.waitForGradleProjectSyncToFinish();

        assertThat(ideSdks.getAndroidSdkPath()).isEqualTo(secondSdkPath);

        // Set the project's SDK to be the original one. Now we will choose the IDE's SDK.
        localProperties = new LocalProperties(ideFrame.getProject());
        localProperties.setAndroidSdkPath(originalSdkPath);
        localProperties.save();

        ideFrame.requestProjectSync();

        messages = ideFrame.findMessageDialog(ANDROID_SDK_MANAGER_DIALOG_TITLE);
        messages.click("Use Android Studio's SDK");

        ideFrame.waitForGradleProjectSyncToFinish();

        localProperties = new LocalProperties(ideFrame.getProject());
        assertThat(localProperties.getAndroidSdkPath()).isEqualTo(secondSdkPath);
    }

    // Verifies that after making a change in a build.gradle file, the editor notification saying that sync is needed shows up. This wasn't
    // the case after a project import.
    // See https://code.google.com/p/android/issues/detail?id=171370
    @Test
    public void editorNotificationsWhenSyncNeededAfterProjectImport() throws IOException {
        IdeFrameFixture ideFrame = guiTest.importSimpleApplication();
        // @formatter:off
        ideFrame.getEditor().open("app/build.gradle").waitUntilErrorAnalysisFinishes().enterText("Hello World")
                .awaitNotification(
                        "Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly.");
        // @formatter:on
    }

    // Verifies that sync does not fail and user is warned when a project contains an Android module without variants.
    // See https://code.google.com/p/android/issues/detail?id=170722
    @Test
    public void withAndroidProjectWithoutVariants() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        assertNotNull(AndroidFacet.getInstance(ideFrame.getModule("app")));

        File appBuildFile = new File(ideFrame.getProjectPath(), join("app", FN_BUILD_GRADLE));
        assertAbout(file()).that(appBuildFile).isFile();

        // Remove all variants.
        appendToFile(appBuildFile, "android.variantFilter { variant -> variant.ignore = true }");

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        // Verify user was warned.
        ContentFixture syncMessages = ideFrame.getMessagesToolWindow().getGradleSyncContent();
        syncMessages.findMessage(ERROR,
                firstLineStartingWith("The module 'app' is an Android project without build variants"));

        // Verify AndroidFacet was removed.
        assertNull(AndroidFacet.getInstance(ideFrame.getModule("app")));
    }

    @Test
    public void withModuleLanguageLevelEqualTo8() throws IOException {
        Sdk jdk = IdeSdks.getInstance().getJdk();
        if (jdk == null) {
            skipTest("JDK is null");
        }

        assume().that(JavaSdk.getInstance().getVersion(jdk)).isAtLeast(JavaSdkVersion.JDK_1_8);

        guiTest.importProjectAndWaitForProjectSyncToFinish("MultipleModuleTypes");
        Module javaLib = guiTest.ideFrame().getModule("javaLib");
        assertEquals(JDK_1_8, getJavaLanguageLevel(javaLib));
    }

    @Test
    public void withPreReleasePlugin() throws IOException {
        guiTest.importMultiModule();
        guiTest.ideFrame().updateAndroidGradlePluginVersion("1.2.0-beta1").requestProjectSync()
                .waitForGradleProjectSyncToFail();

        ContentFixture syncMessages = guiTest.ideFrame().getMessagesToolWindow().getGradleSyncContent();
        MessageFixture message = syncMessages.findMessage(ERROR,
                firstLineStartingWith("Plugin is too old, please update to a more recent version"));
        // Verify that the "quick fix" is added.
        message.findHyperlink("Fix plugin version and sync project");
    }

    @Test
    public void syncDuringOfflineMode() throws IOException {
        String hyperlinkText = "Disable offline mode and sync project";

        guiTest.importSimpleApplication();

        IdeFrameFixture ideFrame = guiTest.ideFrame();
        File buildFile = new File(ideFrame.getProjectPath(), join("app", FN_BUILD_GRADLE));
        assertAbout(file()).that(buildFile).isFile();
        appendToFile(buildFile, "dependencies { compile 'something:not:exists' }");

        GradleSettings gradleSettings = GradleSettings.getInstance(ideFrame.getProject());
        gradleSettings.setOfflineWork(true);

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();
        MessagesToolWindowFixture messagesToolWindow = ideFrame.getMessagesToolWindow();
        MessageFixture message = messagesToolWindow.getGradleSyncContent().findMessage(ERROR,
                firstLineStartingWith("Failed to resolve:"));

        HyperlinkFixture hyperlink = message.findHyperlink(hyperlinkText);
        hyperlink.click();

        assertFalse(gradleSettings.isOfflineWork());
        ideFrame.waitForGradleProjectSyncToFinish();
        messagesToolWindow = ideFrame.getMessagesToolWindow();
        message = messagesToolWindow.getGradleSyncContent().findMessage(ERROR,
                firstLineStartingWith("Failed to resolve:"));

        try {
            message.findHyperlink(hyperlinkText);
            fail(hyperlinkText + " link still present");
        } catch (AssertionError e) {
            // After offline mode is disable, the previous hyperlink will disappear after next sync
            assertThat(e.getMessage()).contains("Failed to find URL");
            assertThat(e.getMessage()).contains(hyperlinkText);
        }
    }

    @Nullable
    private static LanguageLevel getJavaLanguageLevel(@NotNull Module module) {
        return LanguageLevelModuleExtensionImpl.getInstance(module).getLanguageLevel();
    }

    @Test
    public void shouldUseLibrary() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Project project = ideFrame.getProject();

        // Make sure the library was added.
        LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
        String libraryName = "org.apache.http.legacy-" + TestUtils.getLatestAndroidPlatform();
        Library library = libraryTable.getLibraryByName(libraryName);

        // Verify that the library has the right j
        VirtualFile[] jarFiles = library.getFiles(CLASSES);
        assertThat(jarFiles).asList().hasSize(1);
        VirtualFile jarFile = jarFiles[0];
        assertEquals("org.apache.http.legacy.jar", jarFile.getName());

        // Verify that the module depends on the library
        Module appModule = ideFrame.getModule("app");
        AtomicBoolean dependencyFound = new AtomicBoolean();
        new ReadAction() {
            @Override
            protected void run(@NotNull Result result) throws Throwable {
                ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(appModule).getModifiableModel();
                try {
                    for (OrderEntry orderEntry : modifiableModel.getOrderEntries()) {
                        if (orderEntry instanceof LibraryOrderEntry) {
                            LibraryOrderEntry libraryDependency = (LibraryOrderEntry) orderEntry;
                            if (libraryDependency.getLibrary() == library) {
                                dependencyFound.set(true);
                            }
                        }
                    }
                } finally {
                    modifiableModel.dispose();
                }
            }
        }.execute();
        assertTrue("Module app should depend on library '" + library.getName() + "'", dependencyFound.get());
    }

    @Test
    public void aarSourceAttachments() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Project project = ideFrame.getProject();

        Module appModule = ideFrame.getModule("app");

        execute(new GuiTask() {
            @Override
            protected void executeInEDT() throws Throwable {
                runWriteCommandAction(project, () -> {
                    GradleBuildModel buildModel = GradleBuildModel.get(appModule);

                    String newDependency = "com.mapbox.mapboxsdk:mapbox-android-sdk:0.7.4@aar";
                    buildModel.dependencies().addArtifact(COMPILE, newDependency);
                    buildModel.applyChanges();
                });
            }
        });

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        // Verify that the library has sources.
        LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
        String libraryName = "mapbox-android-sdk-0.7.4";
        Library library = libraryTable.getLibraryByName(libraryName);
        VirtualFile[] files = library.getFiles(SOURCES);
        assertThat(files).asList().hasSize(1);
    }

    // https://code.google.com/p/android/issues/detail?id=185313
    @Test
    public void sdkCreationForAddons() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrame = guiTest.ideFrame();

        Project project = ideFrame.getProject();

        Module appModule = ideFrame.getModule("app");
        GradleBuildFile buildFile = GradleBuildFile.get(appModule);

        execute(new GuiTask() {
            @Override
            protected void executeInEDT() throws Throwable {
                runWriteCommandAction(project,
                        () -> buildFile.setValue(BuildFileKey.COMPILE_SDK_VERSION, "Google Inc.:Google APIs:24"));
            }
        });

        ideFrame.requestProjectSync().waitForGradleProjectSyncToFinish();

        Sdk sdk = ModuleRootManager.getInstance(appModule).getSdk();

        AndroidSdkData sdkData = AndroidSdkData.getSdkData(sdk);

        SdkAdditionalData data = sdk.getSdkAdditionalData();
        assertThat(data).isInstanceOf(AndroidSdkAdditionalData.class);

        AndroidSdkAdditionalData androidSdkData = (AndroidSdkAdditionalData) data;
        IAndroidTarget buildTarget = androidSdkData.getBuildTarget(sdkData);

        // By checking that there are no additional libraries in the SDK, we are verifying that an additional SDK was not created for add-ons.
        assertThat(buildTarget.getAdditionalLibraries()).hasSize(0);
    }

    @Test
    public void gradleModelCache() throws IOException {
        guiTest.importSimpleApplication();
        IdeFrameFixture ideFrameFixture = guiTest.ideFrame();

        File projectPath = ideFrameFixture.getProjectPath();
        ideFrameFixture.closeProject();

        AtomicBoolean syncSkipped = new AtomicBoolean(false);

        // Reopen project and verify that sync was skipped (i.e. model loaded from cache)
        execute(new GuiTask() {
            @Override
            protected void executeInEDT() throws Throwable {
                ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
                Project project = projectManager.convertAndLoadProject(projectPath.getPath());
                GradleSyncState.subscribe(project, new GradleSyncListener.Adapter() {
                    @Override
                    public void syncSkipped(@NotNull Project project) {
                        syncSkipped.set(true);
                    }
                });
                projectManager.openProject(project);
            }
        });

        Wait.seconds(5).expecting("sync to be skipped").until(syncSkipped::get);
    }

    /**
     * Verify that the project syncs and gradle file updates after changing the minSdkVersion in the build.gradle file.
     * <p>
     * This is run to qualify releases. Please involve the test team in substantial changes.
     * <p>
     * <pre>
     *   Steps:
     *   1. Import a project.
     *   2. Open build.gradle file for the project and update the min sdk value to 23.
     *   3. Sync the project.
     *   Verify:
     *   Project syncs and minSdk version is updated.
     *   </pre>
     */
    @RunIn(TestGroup.QA)
    @Test
    public void modifyMinSdkAndSync() throws Exception {
        IdeFrameFixture ideFrame = guiTest.importSimpleApplication();
        // @formatter:off
        ideFrame.getEditor().open("app/build.gradle").select("minSdkVersion (19)").enterText("23")
                .awaitNotification(
                        "Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly.")
                .performAction("Sync Now").waitForGradleProjectSyncToFinish();
        // @formatter:on
    }
}