/* * 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.framework.fixture; import com.android.tools.idea.gradle.GradleSyncState; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.android.tools.idea.gradle.compiler.AndroidGradleBuildConfiguration; import com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor; import com.android.tools.idea.gradle.invoker.GradleInvocationResult; import com.android.tools.idea.gradle.util.BuildMode; import com.android.tools.idea.gradle.util.GradleUtil; import com.android.tools.idea.gradle.util.ProjectBuilder; import com.android.tools.idea.tests.gui.framework.fixture.avdmanager.AvdManagerDialogFixture; import com.google.common.collect.Lists; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.compiler.CompilationStatusListener; import com.intellij.openapi.compiler.CompileContext; import com.intellij.openapi.compiler.CompilerManager; import com.intellij.openapi.externalSystem.model.ExternalSystemException; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.options.ex.IdeConfigurablesGroup; import com.intellij.openapi.options.ex.ProjectConfigurablesGroup; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.impl.IdeFrameImpl; import com.intellij.ui.EditorNotificationPanel; import com.intellij.ui.HyperlinkLabel; import com.intellij.util.ThreeState; import com.intellij.util.messages.MessageBusConnection; import org.fest.reflect.core.Reflection; import org.fest.swing.core.GenericTypeMatcher; import org.fest.swing.core.Robot; import org.fest.swing.core.matcher.JLabelMatcher; import org.fest.swing.driver.ComponentDriver; import org.fest.swing.edt.GuiActionRunner; import org.fest.swing.edt.GuiQuery; import org.fest.swing.fixture.ComponentFixture; import org.fest.swing.timing.Condition; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.gradle.settings.GradleProjectSettings; import javax.swing.*; import java.awt.*; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import static com.android.SdkConstants.FD_GRADLE; import static com.android.tools.idea.gradle.GradleSyncState.GRADLE_SYNC_TOPIC; import static com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor.GRADLE_BUILD_TOPIC; import static com.android.tools.idea.gradle.util.BuildMode.COMPILE_JAVA; import static com.android.tools.idea.gradle.util.BuildMode.SOURCE_GEN; import static com.android.tools.idea.tests.gui.framework.GuiTests.LONG_TIMEOUT; import static com.android.tools.idea.tests.gui.framework.GuiTests.SHORT_TIMEOUT; import static com.intellij.openapi.util.io.FileUtil.delete; import static com.intellij.openapi.util.text.StringUtil.isNotEmpty; import static junit.framework.Assert.assertNotNull; import static org.fest.assertions.Assertions.assertThat; import static org.fest.swing.timing.Pause.pause; import static org.fest.util.Strings.quote; import static org.jetbrains.android.AndroidPlugin.EXECUTE_BEFORE_PROJECT_BUILD_IN_GUI_TEST_KEY; import static org.jetbrains.android.AndroidPlugin.EXECUTE_BEFORE_PROJECT_SYNC_TASK_IN_GUI_TEST_KEY; import static org.jetbrains.plugins.gradle.settings.DistributionType.DEFAULT_WRAPPED; import static org.jetbrains.plugins.gradle.settings.DistributionType.LOCAL; import static org.junit.Assert.*; public class IdeFrameFixture extends ComponentFixture<IdeFrameImpl> { private EditorFixture myEditor; @NotNull private final File myProjectPath; @NotNull private final GradleProjectEventListener myGradleProjectEventListener; @NotNull public static IdeFrameFixture find(@NotNull final Robot robot, @NotNull final File projectPath, @Nullable final String projectName) { final GenericTypeMatcher<IdeFrameImpl> matcher = new GenericTypeMatcher<IdeFrameImpl>(IdeFrameImpl.class) { @Override protected boolean isMatching(IdeFrameImpl frame) { Project project = frame.getProject(); if (project != null && projectPath.getPath().equals(project.getBasePath())) { return projectName == null || projectName.equals(project.getName()); } return false; } }; pause(new Condition("IdeFrame " + quote(projectPath.getPath()) + " to show up") { @Override public boolean test() { Collection<IdeFrameImpl> frames = robot.finder().findAll(matcher); return !frames.isEmpty(); } }, LONG_TIMEOUT); IdeFrameImpl ideFrame = robot.finder().find(matcher); return new IdeFrameFixture(robot, ideFrame, projectPath); } public IdeFrameFixture(@NotNull Robot robot, @NotNull IdeFrameImpl target, @NotNull File projectPath) { super(robot, target); myProjectPath = projectPath; final Project project = getProject(); Disposable disposable = new NoOpDisposable(); Disposer.register(project, disposable); myGradleProjectEventListener = new GradleProjectEventListener(); MessageBusConnection connection = project.getMessageBus().connect(disposable); connection.subscribe(GRADLE_SYNC_TOPIC, myGradleProjectEventListener); connection.subscribe(GRADLE_BUILD_TOPIC, myGradleProjectEventListener); } @NotNull public File getProjectPath() { return myProjectPath; } @NotNull public IdeFrameFixture requireModuleCount(int expected) { Module[] modules = getModuleManager().getModules(); assertThat(modules).as("Module count in project " + quote(getProject().getName())).hasSize(expected); return this; } @NotNull public IdeaAndroidProject getAndroidProjectForModule(@NotNull String name) { Module module = getModule(name); AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null && facet.isGradleProject()) { IdeaAndroidProject androidProject = facet.getIdeaAndroidProject(); if (androidProject != null) { return androidProject; } } throw new AssertionError("Unable to find IdeaAndroidProject for module " + quote(name)); } @NotNull public Module getModule(@NotNull String name) { for (Module module : getModuleManager().getModules()) { if (name.equals(module.getName())) { return module; } } throw new AssertionError("Unable to find module with name " + quote(name)); } @NotNull private ModuleManager getModuleManager() { return ModuleManager.getInstance(getProject()); } @NotNull public Project getProject() { Project project = target.getProject(); assertNotNull(project); return project; } @NotNull public EditorFixture getEditor() { if (myEditor == null) { myEditor = new EditorFixture(robot, this); } return myEditor; } @NotNull public GradleInvocationResult invokeProjectMake() { myGradleProjectEventListener.reset(); final AtomicReference<GradleInvocationResult> resultRef = new AtomicReference<GradleInvocationResult>(); ProjectBuilder.getInstance(getProject()).addAfterProjectBuildTask(new ProjectBuilder.AfterProjectBuildTask() { @Override public void execute(@NotNull GradleInvocationResult result) { resultRef.set(result); } @Override public boolean execute(CompileContext context) { return false; } }); selectProjectMakeAction(); waitForBuildToFinish(COMPILE_JAVA); GradleInvocationResult result = resultRef.get(); assertNotNull(result); return result; } @NotNull public IdeFrameFixture invokeProjectMakeAndSimulateFailure(@NotNull final String failure) { Runnable failTask = new Runnable() { @Override public void run() { throw new ExternalSystemException(failure); } }; ApplicationManager.getApplication().putUserData(EXECUTE_BEFORE_PROJECT_BUILD_IN_GUI_TEST_KEY, failTask); selectProjectMakeAction(); return this; } @NotNull public CompileContext invokeProjectMakeUsingJps() { final Project project = getProject(); AndroidGradleBuildConfiguration buildConfiguration = AndroidGradleBuildConfiguration.getInstance(project); buildConfiguration.USE_EXPERIMENTAL_FASTER_BUILD = false; final AtomicReference<CompileContext> contextRef = new AtomicReference<CompileContext>(); CompilerManager compilerManager = CompilerManager.getInstance(project); Disposable disposable = new NoOpDisposable(); compilerManager.addCompilationStatusListener(new CompilationStatusListener() { @Override public void compilationFinished(boolean aborted, int errors, int warnings, CompileContext compileContext) { contextRef.set(compileContext); } @Override public void fileGenerated(String outputRoot, String relativePath) { } }, disposable); try { selectProjectMakeAction(); pause(new Condition("Build (" + COMPILE_JAVA + ") for project " + quote(project.getName()) + " to finish'") { @Override public boolean test() { CompileContext context = contextRef.get(); return context != null; } }, LONG_TIMEOUT); CompileContext context = contextRef.get(); assertNotNull(context); return context; } finally { Disposer.dispose(disposable); } } protected void selectProjectMakeAction() { JMenuItem makeProjectMenuItem = findActionMenuItem("Build", "Make Project"); robot.click(makeProjectMenuItem); } @NotNull private JMenuItem findActionMenuItem(@NotNull String... path) { assertThat(path).isNotEmpty(); int segmentCount = path.length; Container root = target; for (int i = 0; i < segmentCount; i++) { final String segment = path[i]; JMenuItem found = robot.finder().find(root, new GenericTypeMatcher<JMenuItem>(JMenuItem.class) { @Override protected boolean isMatching(JMenuItem menuItem) { return segment.equals(menuItem.getText()); } }); if (i < segmentCount - 1) { robot.click(found); root = robot.findActivePopupMenu(); continue; } return found; } throw new AssertionError("Menu item with path " + Arrays.toString(path) + " should have been found already"); } private void waitForBuildToFinish(@NotNull final BuildMode buildMode) { final Project project = getProject(); pause(new Condition("Build (" + buildMode + ") for project " + quote(project.getName()) + " to finish'") { @Override public boolean test() { if (buildMode == SOURCE_GEN) { PostProjectBuildTasksExecutor tasksExecutor = PostProjectBuildTasksExecutor.getInstance(project); if (tasksExecutor.getLastBuildTimestamp() > -1) { // This will happen when creating a new project. Source generation happens before the IDE frame is found and build listeners // are created. It is fairly safe to assume that source generation happened if we have a timestamp for a "last performed build". return true; } } return myGradleProjectEventListener.isBuildFinished(buildMode); } }, LONG_TIMEOUT); waitForBackgroundTasksToFinish(); robot.waitForIdle(); } @NotNull public FileFixture findExistingFileByRelativePath(@NotNull String relativePath) { VirtualFile file = findFileByRelativePath(relativePath, true); return new FileFixture(getProject(), file); } @Nullable @Contract("_, true -> !null") public VirtualFile findFileByRelativePath(@NotNull String relativePath, boolean requireExists) { //noinspection Contract assertFalse("Should use '/' in test relative paths, not File.separator", relativePath.contains("\\")); Project project = getProject(); VirtualFile file = project.getBaseDir().findFileByRelativePath(relativePath); if (requireExists) { //noinspection Contract assertNotNull("Unable to find file with relative path " + quote(relativePath), file); } return file; } @NotNull public IdeFrameFixture requestProjectSyncAndExpectFailure() { requestProjectSync(); return waitForGradleProjectSyncToFail(); } @NotNull public IdeFrameFixture requestProjectSyncAndSimulateFailure(@NotNull final String failure) { Runnable failTask = new Runnable() { @Override public void run() { throw new RuntimeException(failure); } }; ApplicationManager.getApplication().putUserData(EXECUTE_BEFORE_PROJECT_SYNC_TASK_IN_GUI_TEST_KEY, failTask); // When simulating the error, we don't have to wait for sync to happen. Sync never happens because the error is thrown before it (sync) // is started. return requestProjectSync(); } @NotNull public IdeFrameFixture requestProjectSync() { myGradleProjectEventListener.reset(); // We wait until all "Run Configurations" are populated in the toolbar combo-box. Until then the "Project Sync" button is not in its // final position, and FEST will click the wrong button. pause(new Condition("Waiting for 'Run Configurations' to be populated") { @Override public boolean test() { RunConfigurationComboBoxFixture runConfigurationComboBox = RunConfigurationComboBoxFixture.find(IdeFrameFixture.this); return isNotEmpty(runConfigurationComboBox.getText()); } }, SHORT_TIMEOUT); findActionButtonByActionId("Android.SyncProject").click(); return this; } @NotNull public IdeFrameFixture waitForGradleProjectSyncToFail() { try { waitForGradleProjectSyncToFinish(true); fail("Expecting project sync to fail"); } catch (RuntimeException expected) { // expected failure. } return waitForBackgroundTasksToFinish(); } @NotNull public IdeFrameFixture waitForGradleProjectSyncToFinish() { waitForGradleProjectSyncToFinish(false); return this; } private void waitForGradleProjectSyncToFinish(final boolean expectSyncFailure) { final Project project = getProject(); // ensure GradleInvoker (in-process build) is always enabled. AndroidGradleBuildConfiguration buildConfiguration = AndroidGradleBuildConfiguration.getInstance(project); buildConfiguration.USE_EXPERIMENTAL_FASTER_BUILD = true; pause(new Condition("Syncing project " + quote(project.getName()) + " to finish") { @Override public boolean test() { GradleSyncState syncState = GradleSyncState.getInstance(project); boolean syncFinished = (myGradleProjectEventListener.isSyncFinished() || syncState.isSyncNeeded() != ThreeState.YES) && !syncState.isSyncInProgress(); if (expectSyncFailure) { syncFinished = syncFinished && myGradleProjectEventListener.hasSyncError(); } return syncFinished; } }, LONG_TIMEOUT); if (myGradleProjectEventListener.hasSyncError()) { RuntimeException syncError = myGradleProjectEventListener.getSyncError(); myGradleProjectEventListener.reset(); throw syncError; } if (!myGradleProjectEventListener.isSyncSkipped()) { waitForBuildToFinish(SOURCE_GEN); } waitForBackgroundTasksToFinish(); } @NotNull public IdeFrameFixture waitForBackgroundTasksToFinish() { pause(new Condition("Background tasks to finish") { @Override public boolean test() { ProgressManager progressManager = ProgressManager.getInstance(); return !progressManager.hasModalProgressIndicator() && !progressManager.hasProgressIndicator() && !progressManager.hasUnsafeProgressIndicator(); } }, LONG_TIMEOUT); robot.waitForIdle(); return this; } @NotNull private ActionButtonFixture findActionButtonByActionId(String actionId) { return ActionButtonFixture.findByActionId(actionId, robot, target); } @NotNull public MessagesToolWindowFixture getMessagesToolWindow() { return new MessagesToolWindowFixture(getProject(), robot); } /** Checks that the given error message is showing in the editor (or no messages are showing, if the parameter is null */ @Nullable public EditorNotificationPanelFixture requireEditorNotification(@Nullable String message) { EditorNotificationPanel panel = findPanel(message); // fails test if not found (or if null and notifications were found) return new EditorNotificationPanelFixture(robot, panel); } /** Locates an editor notification with the given main message (unless the message is null, in which case we assert * that there are no visible editor notifications. Will fail if the given notification is not found. */ @Nullable private EditorNotificationPanel findPanel(@Nullable String message) { Collection<EditorNotificationPanel> panels = robot.finder().findAll(target, new GenericTypeMatcher<EditorNotificationPanel>( EditorNotificationPanel.class, true) { @Override protected boolean isMatching(EditorNotificationPanel component) { return true; } }); if (message == null) { if (!panels.isEmpty()) { List<String> labels = Lists.newArrayList(); for (EditorNotificationPanel panel : panels) { labels.add(getEditorNotificationLabel(panel)); } fail("Found editor notifications when none were expected: " + labels); } } else { List<String> labels = Lists.newArrayList(); for (EditorNotificationPanel panel : panels) { String label = getEditorNotificationLabel(panel); labels.add(label); if (label.contains(message)) { return panel; } } fail("Did not find message " + message + "; available notifications are " + labels); } return null; } /** Looks up the main label for a given editor notification panel */ private String getEditorNotificationLabel(@NotNull EditorNotificationPanel panel) { final JLabel label = robot.finder().find(panel, JLabelMatcher.any()); return GuiActionRunner.execute(new GuiQuery<String>() { @Override @Nullable protected String executeInEDT() throws Throwable { return label.getText(); } }); } /** Clicks the given link in the editor notification with the given message */ public void clickEditorNotification(@NotNull String message, @NotNull final String linkText) { final EditorNotificationPanel panel = findPanel(message); assertNotNull(panel); HyperlinkLabel label = robot.finder().find(panel, new GenericTypeMatcher<HyperlinkLabel>(HyperlinkLabel.class, true) { @Override protected boolean isMatching(HyperlinkLabel component) { String text = Reflection.method("getText").withReturnType(String.class).in(component).invoke(); return text.contains(linkText); } }); ComponentDriver driver = new ComponentDriver(robot); driver.click(label); } @NotNull public IdeSettingsDialogFixture openIdeSettings() { // Using invokeLater because we are going to show a *modal* dialog via API (instead of clicking a button, for example.) If we use // GuiActionRunner the test will hang until the modal dialog is closed. ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { Project project = getProject(); ShowSettingsUtil.getInstance().showSettingsDialog(project, new ProjectConfigurablesGroup(project), new IdeConfigurablesGroup()); } }); return IdeSettingsDialogFixture.find(robot); } @NotNull public IdeFrameFixture deleteGradleWrapper() { deleteWrapper(getProjectPath()); return this; } @NotNull public IdeFrameFixture requireGradleWrapperSet() { File wrapperDirPath = getGradleWrapperDirPath(getProjectPath()); assertThat(wrapperDirPath).as("Gradle wrapper").isDirectory(); GradleProjectSettings settings = getGradleSettings(); assertEquals("Gradle distribution type", DEFAULT_WRAPPED, settings.getDistributionType()); return this; } public static void deleteWrapper(@NotNull File projectDirPath) { File wrapperDirPath = getGradleWrapperDirPath(projectDirPath); delete(wrapperDirPath); assertThat(wrapperDirPath).as("Gradle wrapper").doesNotExist(); } @NotNull private static File getGradleWrapperDirPath(@NotNull File projectDirPath) { return new File(projectDirPath, FD_GRADLE); } @NotNull public IdeFrameFixture useLocalGradleDistribution(@NotNull String gradleHome) { GradleProjectSettings settings = getGradleSettings(); settings.setDistributionType(LOCAL); settings.setGradleHome(gradleHome); return this; } @NotNull public GradleProjectSettings getGradleSettings() { GradleProjectSettings settings = GradleUtil.getGradleProjectSettings(getProject()); assertNotNull(settings); return settings; } @NotNull public AvdManagerDialogFixture invokeAvdManager() { ActionButtonFixture button = findActionButtonByActionId("Android.RunAndroidAvdManager"); button.click(); return AvdManagerDialogFixture.find(robot); } private static class NoOpDisposable implements Disposable { @Override public void dispose() { } } }