diff --git a/java/execution/impl/intellij.java.execution.impl.iml b/java/execution/impl/intellij.java.execution.impl.iml index ccea3a44b23f..528e250ee475 100644 --- a/java/execution/impl/intellij.java.execution.impl.iml +++ b/java/execution/impl/intellij.java.execution.impl.iml @@ -25,6 +25,7 @@ + diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsChangeListDecorator.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsChangeListDecorator.java deleted file mode 100644 index d89c3a3707da..000000000000 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsChangeListDecorator.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.execution.testDiscovery; - -import com.intellij.execution.testDiscovery.actions.ShowDiscoveredTestsAction; -import com.intellij.ide.DataManager; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.registry.Registry; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ChangeListDecorator; -import com.intellij.openapi.vcs.changes.LocalChangeList; -import com.intellij.ui.ColoredTreeCellRenderer; -import com.intellij.ui.SimpleTextAttributes; -import com.intellij.util.ArrayUtil; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.NotNull; - -import static com.intellij.ui.SimpleTextAttributes.STYLE_UNDERLINE; - -public class AffectedTestsChangeListDecorator implements ChangeListDecorator { - private final Project myProject; - - public AffectedTestsChangeListDecorator(@NotNull Project project) { - myProject = project; - } - - @Override - public void decorateChangeList(LocalChangeList changeList, - ColoredTreeCellRenderer renderer, - boolean selected, - boolean expanded, - boolean hasFocus) { - if (!Registry.is("show.affected.tests.in.changelists")) return; - if (!ShowDiscoveredTestsAction.isEnabled(myProject)) return; - if (changeList.getChanges().isEmpty()) return; - - renderer.append(", ", SimpleTextAttributes.GRAYED_ATTRIBUTES); - renderer.append("show affected tests", new SimpleTextAttributes(STYLE_UNDERLINE, UIUtil.getInactiveTextColor()), (Runnable)() -> { - DataContext dataContext = DataManager.getInstance().getDataContext(renderer.getTree()); - Change[] objects = ArrayUtil.toObjectArray(changeList.getChanges(), Change.class); - ShowDiscoveredTestsAction.showDiscoveredTestsByChanges(myProject, objects, changeList.getName(), dataContext); - }); - } -} \ No newline at end of file diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsInChangeListPainter.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsInChangeListPainter.java new file mode 100644 index 000000000000..6a57d00d50ac --- /dev/null +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/AffectedTestsInChangeListPainter.java @@ -0,0 +1,125 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.execution.testDiscovery; + +import com.intellij.execution.testDiscovery.actions.ShowDiscoveredTestsAction; +import com.intellij.ide.DataManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.components.ProjectComponent; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vcs.changes.*; +import com.intellij.psi.PsiMethod; +import com.intellij.ui.ColoredTreeCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.Alarm; +import com.intellij.util.ArrayUtil; +import com.intellij.util.io.PowerStatus; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.intellij.ui.SimpleTextAttributes.STYLE_UNDERLINE; + +public class AffectedTestsInChangeListPainter implements ChangeListDecorator, ProjectComponent { + private final Project myProject; + private final ChangeListManager myChangeListManager; + private final ChangeListAdapter myChangeListListener; + private final Alarm myAlarm; + private final Set myCache = new HashSet<>(); + + public AffectedTestsInChangeListPainter(@NotNull Project project, ChangeListManager changeListManager) { + myProject = project; + myChangeListManager = changeListManager; + myChangeListListener = new ChangeListAdapter() { + @Override + public void changeListsChanged() { + scheduleUpdate(); + } + + @Override + public void changeListUpdateDone() { + scheduleUpdate(); + } + + @Override + public void defaultListChanged(ChangeList oldDefaultList, ChangeList newDefaultList, boolean automatic) { + scheduleUpdate(); + } + + @Override + public void unchangedFileStatusChanged() { + scheduleUpdate(); + } + }; + myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, project); + myChangeListManager.addChangeListListener(myChangeListListener); + } + + @Override + public void projectOpened() { + DumbService.getInstance(myProject).runWhenSmart(() -> scheduleUpdate()); + } + + @Override + public void projectClosed() { + myAlarm.cancelAllRequests(); + } + + @Override + public void disposeComponent() { + myAlarm.cancelAllRequests(); + myCache.clear(); + myChangeListManager.removeChangeListListener(myChangeListListener); + } + + private static int updateDelay() { + return PowerStatus.getPowerStatus() == PowerStatus.AC ? 50 : 300; + } + + @Override + public void decorateChangeList(LocalChangeList changeList, + ColoredTreeCellRenderer renderer, + boolean selected, + boolean expanded, + boolean hasFocus) { + if (!Registry.is("show.affected.tests.in.changelists")) return; + if (!ShowDiscoveredTestsAction.isEnabled(myProject)) return; + if (changeList.getChanges().isEmpty()) return; + if (!myCache.contains(changeList.getId())) return; + + renderer.append(", ", SimpleTextAttributes.GRAYED_ATTRIBUTES); + renderer.append("show affected tests", new SimpleTextAttributes(STYLE_UNDERLINE, UIUtil.getInactiveTextColor()), (Runnable)() -> { + DataContext dataContext = DataManager.getInstance().getDataContext(renderer.getTree()); + Change[] changes = ArrayUtil.toObjectArray(changeList.getChanges(), Change.class); + ShowDiscoveredTestsAction.showDiscoveredTestsByChanges(myProject, changes, changeList.getName(), dataContext); + }); + } + + private void scheduleUpdate() { + if (!Registry.is("show.affected.tests.in.changelists")) return; + if (!ShowDiscoveredTestsAction.isEnabled(myProject)) return; + myAlarm.cancelAllRequests(); + myAlarm.addRequest(() -> update(), updateDelay()); + } + + private void update() { + myCache.clear(); + List lists = myChangeListManager.getChangeLists(); + for (LocalChangeList list : lists) { + if (list.getChanges().isEmpty()) continue; + + PsiMethod[] methods = ShowDiscoveredTestsAction.findMethods(myProject, ArrayUtil.toObjectArray(list.getChanges(), Change.class)); + if (methods.length == 0) continue; + ReadAction.run( + () -> ShowDiscoveredTestsAction.processMethods(myProject, methods, (clazz, method, parameter) -> { + myCache.add(list.getId()); + return false; + }, () -> ChangesViewManager.getInstance(myProject).scheduleRefresh())); + } + } +} \ No newline at end of file diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/TestDiscoveryProducer.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/TestDiscoveryProducer.java index 194bd21a7dea..75e8580e8994 100644 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/TestDiscoveryProducer.java +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/TestDiscoveryProducer.java @@ -5,6 +5,8 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Couple; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; import com.intellij.util.containers.MultiMap; import gnu.trove.THashSet; import org.jetbrains.annotations.ApiStatus; @@ -32,7 +34,7 @@ public interface TestDiscoveryProducer { @NotNull String classFQName, @NotNull String methodName, byte frameworkId, - @NotNull TestConsumer consumer) { + @NotNull TestProcessor processor) { MultiMap visitedTests = new MultiMap() { @NotNull @Override @@ -47,7 +49,7 @@ public interface TestDiscoveryProducer { if (!visitedTests.get(classFQName).contains(methodRawName)) { visitedTests.putValue(className, methodRawName); Couple couple = extractParameter(methodRawName); - consumer.accept(className, couple.first, couple.second); + if (!processor.process(className, couple.first, couple.second)) return; } } } @@ -63,7 +65,12 @@ public interface TestDiscoveryProducer { } @FunctionalInterface - interface TestConsumer { - boolean accept(@NotNull String className, @NotNull String methodName, @Nullable String parameter); + interface TestProcessor { + boolean process(@NotNull String className, @NotNull String methodName, @Nullable String parameter); + } + + @FunctionalInterface + interface PsiTestProcessor { + boolean process(@NotNull PsiClass clazz, @NotNull PsiMethod method, @Nullable String parameter); } } diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTree.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTree.java index 3fc8b63f9730..46e19db1d68c 100644 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTree.java +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTree.java @@ -138,6 +138,10 @@ class DiscoveredTestsTree extends Tree implements DataProvider { return myModel.getTestCount(); } + public int getTestClassesCount() { + return myModel.getTestClassesCount(); + } + @Nullable @Override public Object getData(String dataId) { diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTreeModel.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTreeModel.java index 09fc877100ac..5d8500aa1b8a 100644 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTreeModel.java +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/DiscoveredTestsTreeModel.java @@ -98,6 +98,10 @@ class DiscoveredTestsTreeModel extends BaseTreeModel { return myTests.values().stream().mapToInt(ms -> ms.size()).sum(); } + synchronized int getTestClassesCount() { + return myTests.size(); + } + public static abstract class Node { @NotNull private final SmartPsiElementPointer myPointer; diff --git a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/ShowDiscoveredTestsAction.java b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/ShowDiscoveredTestsAction.java index 372e8e5c57af..e7e86c6d9e7e 100644 --- a/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/ShowDiscoveredTestsAction.java +++ b/java/execution/impl/src/com/intellij/execution/testDiscovery/actions/ShowDiscoveredTestsAction.java @@ -39,6 +39,7 @@ import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.changes.Change; import com.intellij.psi.*; @@ -72,7 +73,7 @@ import static com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR; import static com.intellij.openapi.actionSystem.CommonDataKeys.PSI_FILE; public class ShowDiscoveredTestsAction extends AnAction { - private static final String RUN_ALL_ACTION_TEXT = "Run All"; + private static final String RUN_ALL_ACTION_TEXT = "Run All Affected Tests"; @Override public void update(AnActionEvent e) { @@ -102,7 +103,8 @@ public class ShowDiscoveredTestsAction extends AnAction { if (key == null) return; DataContext dataContext = DataManager.getInstance().getDataContext(e.getRequiredData(EDITOR).getContentComponent()); FeatureUsageTracker.getInstance().triggerFeatureUsed("test.discovery"); - String presentableName = PsiFormatUtil.formatMethod(method, PsiSubstitutor.EMPTY, PsiFormatUtilBase.SHOW_CONTAINING_CLASS | PsiFormatUtilBase.SHOW_NAME, 0); + String presentableName = + PsiFormatUtil.formatMethod(method, PsiSubstitutor.EMPTY, PsiFormatUtilBase.SHOW_CONTAINING_CLASS | PsiFormatUtilBase.SHOW_NAME, 0); showDiscoveredTests(project, dataContext, presentableName, method); } @@ -117,6 +119,13 @@ public class ShowDiscoveredTestsAction extends AnAction { @NotNull Change[] changes, @NotNull String title, @NotNull DataContext dataContext) { + PsiMethod[] asJavaMethods = findMethods(project, changes); + FeatureUsageTracker.getInstance().triggerFeatureUsed("test.discovery.selected.changes"); + showDiscoveredTests(project, dataContext, title, asJavaMethods); + } + + @NotNull + public static PsiMethod[] findMethods(@NotNull Project project, @NotNull Change... changes) { UastMetaLanguage jvmLanguage = Language.findInstance(UastMetaLanguage.class); List methods = FormatChangedTextUtil.getInstance().getChangedElements(project, changes, file -> { @@ -142,13 +151,11 @@ public class ShowDiscoveredTestsAction extends AnAction { return physicalMethods; }); - PsiMethod[] asJavaMethods = methods + return methods .stream() .map(m -> ObjectUtils.tryCast(Objects.requireNonNull(UastContextKt.toUElement(m)).getJavaPsi(), PsiMethod.class)) .filter(Objects::nonNull) .toArray(PsiMethod.ARRAY_FACTORY::create); - FeatureUsageTracker.getInstance().triggerFeatureUsed("test.discovery.selected.changes"); - showDiscoveredTests(project, dataContext, title, asJavaMethods); } public static boolean isEnabled(@Nullable Project project) { @@ -157,7 +164,7 @@ public class ShowDiscoveredTestsAction extends AnAction { } @Nullable - private static PsiMethod findMethodAtCaret(AnActionEvent e) { + private static PsiMethod findMethodAtCaret(@NotNull AnActionEvent e) { Editor editor = e.getData(EDITOR); PsiFile file = e.getData(PSI_FILE); if (editor == null || file == null) return null; @@ -178,7 +185,8 @@ public class ShowDiscoveredTestsAction extends AnAction { ConfigurationContext context = ConfigurationContext.getFromContext(dataContext); - ActiveComponent runButton = createButton(RUN_ALL_ACTION_TEXT, AllIcons.Actions.Execute, () -> runAllDiscoveredTests(project, tree, ref, context, initTitle)); + ActiveComponent runButton = + createButton(RUN_ALL_ACTION_TEXT, AllIcons.Actions.Execute, () -> runAllDiscoveredTests(project, tree, ref, context, initTitle)); Runnable pinActionListener = () -> { UsageView view = FindUtil.showInUsageView(null, tree.getTestMethods(), param -> param, initTitle, p -> { @@ -203,17 +211,17 @@ public class ShowDiscoveredTestsAction extends AnAction { }; KeyStroke findUsageKeyStroke = findUsagesKeyStroke(); - String pinTooltip = "Open Find Usages Toolwindow" + (findUsageKeyStroke == null ? "" : " " + KeymapUtil.getKeystrokeText(findUsageKeyStroke)); + String pinTooltip = + "Open Find Usages Toolwindow" + (findUsageKeyStroke == null ? "" : " " + KeymapUtil.getKeystrokeText(findUsageKeyStroke)); ActiveComponent pinButton = createButton(pinTooltip, AllIcons.General.Pin_tab, pinActionListener); - CompositeActiveComponent component = new CompositeActiveComponent(runButton, pinButton); - final PopupChooserBuilder builder = new PopupChooserBuilder(tree) .setTitle(initTitle) .setMovable(true) .setResizable(true) - .setCommandButton(component) + .setCommandButton(new CompositeActiveComponent(pinButton)) + .setSettingButton(new CompositeActiveComponent(runButton).getComponent()) .setItemChoosenCallback(() -> PsiNavigateUtil.navigate(tree.getSelectedElement())) .registerKeyboardAction(findUsageKeyStroke, __ -> pinActionListener.run()) .setMinSize(new JBDimension(500, 300)); @@ -229,49 +237,75 @@ public class ShowDiscoveredTestsAction extends AnAction { model.addTreeModelListener(new TreeModelAdapter() { @Override protected void process(TreeModelEvent event, EventType type) { - popup.setCaption("Found " + tree.getTestCount() + " Tests for " + title); + int testsCount = tree.getTestCount(); + int classesCount = tree.getTestClassesCount(); + popup.setCaption("Found " + testsCount + " " + + StringUtil.pluralize("Test", testsCount) + + " in " + classesCount + " " + + StringUtil.pluralize("Class", classesCount) + + " for " + title); } }); popup.showInBestPositionFor(dataContext); - GlobalSearchScope scope = GlobalSearchScope.projectScope(project); + Runnable whenDone = () -> { + popup.pack(true, true); + tree.setPaintBusy(false); + }; + processMethods(project, methods, (clazz, method, parameter) -> { + tree.addTest(clazz, method, parameter); + return true; + }, whenDone); + } + + public static void processMethods(@NotNull Project project, + @NotNull PsiMethod[] methods, + @NotNull TestDiscoveryProducer.PsiTestProcessor consumer, + @Nullable Runnable doWhenDone) { ApplicationManager.getApplication().executeOnPooledThread(() -> { - for (PsiMethod method : methods) { - Couple methodFqnName = ReadAction.compute(() -> getMethodKey(method)); - if (methodFqnName == null) continue; - String fqn = methodFqnName.first; - String methodName = methodFqnName.second; - - for (TestDiscoveryConfigurationProducer producer : getRunConfigurationProducers(project)) { - byte frameworkId = ((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getTestFrameworkId(); - TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkId, (testClass, testMethod, parameter) -> { - PsiClass[] testClassPsi = {null}; - PsiMethod[] testMethodPsi = {null}; - ReadAction.run(() -> { - testClassPsi[0] = ClassUtil.findPsiClass(PsiManager.getInstance(project), testClass, null, true, scope); - boolean checkBases = parameter != null; // check bases for parameterized tests - if (testClassPsi[0] != null) { - testMethodPsi[0] = ArrayUtil.getFirstElement(testClassPsi[0].findMethodsByName(testMethod, checkBases)); - } - }); - if (testMethodPsi[0] != null) { - tree.addTest(testClassPsi[0], testMethodPsi[0], parameter); - } - return true; - }); - } + processMethodsInner(project, methods, consumer); + if (doWhenDone != null) { + EdtInvocationManager.getInstance().invokeLater(doWhenDone); } - - EdtInvocationManager.getInstance().invokeLater(() -> { - popup.pack(true, true); - tree.setPaintBusy(false); - }); }); } + private static void processMethodsInner(@NotNull Project project, + @NotNull PsiMethod[] methods, + @NotNull TestDiscoveryProducer.PsiTestProcessor processor) { + if (DumbService.isDumb(project)) return; + GlobalSearchScope scope = GlobalSearchScope.projectScope(project); + for (PsiMethod method : methods) { + Couple methodFqnName = ReadAction.compute(() -> getMethodKey(method)); + if (methodFqnName == null) continue; + String fqn = methodFqnName.first; + String methodName = methodFqnName.second; + + for (TestDiscoveryConfigurationProducer producer : getRunConfigurationProducers(project)) { + byte frameworkId = + ((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getTestFrameworkId(); + TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkId, (testClass, testMethod, parameter) -> { + PsiClass[] testClassPsi = {null}; + PsiMethod[] testMethodPsi = {null}; + ReadAction.run(() -> { + testClassPsi[0] = ClassUtil.findPsiClass(PsiManager.getInstance(project), testClass, null, true, scope); + boolean checkBases = parameter != null; // check bases for parameterized tests + if (testClassPsi[0] != null) { + testMethodPsi[0] = ArrayUtil.getFirstElement(testClassPsi[0].findMethodsByName(testMethod, checkBases)); + } + }); + if (testMethodPsi[0] != null) { + if (!processor.process(testClassPsi[0], testMethodPsi[0], parameter)) return false; + } + return true; + }); + } + } + } + private static ActiveComponent createButton(String text, Icon icon, Runnable listener) { - return new ActiveComponent.Adapter() { + return new ActiveComponent.Adapter() { @Override public JComponent getComponent() { Presentation presentation = new Presentation(); @@ -287,10 +321,11 @@ public class ShowDiscoveredTestsAction extends AnAction { } }; } + private static void runAllDiscoveredTests(@NotNull Project project, DiscoveredTestsTree tree, Ref ref, - ConfigurationContext context, + ConfigurationContext context, String title) { Executor executor = DefaultRunExecutor.getRunExecutorInstance(); Module targetModule = TestDiscoveryConfigurationProducer.detectTargetModule(tree.getContainingModules(), project); diff --git a/resources/src/META-INF/JavaIdePlugin.xml b/resources/src/META-INF/JavaIdePlugin.xml index 0304a3338914..44f27d2dcb7c 100644 --- a/resources/src/META-INF/JavaIdePlugin.xml +++ b/resources/src/META-INF/JavaIdePlugin.xml @@ -71,7 +71,7 @@ com.intellij.compiler.backwardRefs.CompilerReferenceServiceImpl - com.intellij.execution.testDiscovery.AffectedTestsChangeListDecorator + com.intellij.execution.testDiscovery.AffectedTestsInChangeListPainter