show a link with affected tests near a change list if needed

This commit is contained in:
Sergey Ignatov
2018-06-10 18:58:26 -07:00
parent 2a6b5be74b
commit 7509def084
8 changed files with 225 additions and 93 deletions
@@ -25,6 +25,7 @@
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="miglayout-swing" level="project" />
<orderEntry type="module" module-name="intellij.platform.vcs.impl" />
</component>
<component name="copyright">
<Base>
@@ -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);
});
}
}
@@ -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<String> 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<LocalChangeList> 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()));
}
}
}
@@ -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<String, String> visitedTests = new MultiMap<String, String>() {
@NotNull
@Override
@@ -47,7 +49,7 @@ public interface TestDiscoveryProducer {
if (!visitedTests.get(classFQName).contains(methodRawName)) {
visitedTests.putValue(className, methodRawName);
Couple<String> 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);
}
}
@@ -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) {
@@ -98,6 +98,10 @@ class DiscoveredTestsTreeModel extends BaseTreeModel<Object> {
return myTests.values().stream().mapToInt(ms -> ms.size()).sum();
}
synchronized int getTestClassesCount() {
return myTests.size();
}
public static abstract class Node<Psi extends PsiMember> {
@NotNull
private final SmartPsiElementPointer<Psi> myPointer;
@@ -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<PsiElement> 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<String> 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<String> 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<JBPopup> ref,
ConfigurationContext context,
ConfigurationContext context,
String title) {
Executor executor = DefaultRunExecutor.getRunExecutorInstance();
Module targetModule = TestDiscoveryConfigurationProducer.detectTargetModule(tree.getContainingModules(), project);
+1 -1
View File
@@ -71,7 +71,7 @@
<implementation-class>com.intellij.compiler.backwardRefs.CompilerReferenceServiceImpl</implementation-class>
</component>
<component>
<implementation-class>com.intellij.execution.testDiscovery.AffectedTestsChangeListDecorator</implementation-class>
<implementation-class>com.intellij.execution.testDiscovery.AffectedTestsInChangeListPainter</implementation-class>
</component>
</project-components>