test discovery: add action to find all tests for a changelist

(cherry picked from commit a6303e3fd4a6e4fec9ed48863ae2a24f0da43153)
This commit is contained in:
Aleksey Pivovarov
2018-02-20 00:55:51 +03:00
committed by Sergey Ignatov
parent d52ef307ba
commit ec2e9357ad
4 changed files with 243 additions and 61 deletions
@@ -1,6 +1,7 @@
// 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.codeInsight.actions.FormatChangedTextUtil;
import com.intellij.codeInsight.navigation.ListBackgroundUpdaterTask;
import com.intellij.execution.Executor;
import com.intellij.execution.JavaTestConfigurationBase;
@@ -12,11 +13,14 @@ import com.intellij.find.FindUtil;
import com.intellij.find.actions.CompositeActiveComponent;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.ide.util.DefaultPsiElementCellRenderer;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
@@ -24,10 +28,14 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.IconButton;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.PopupChooserBuilder;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.CollectionListModel;
import com.intellij.ui.InplaceButton;
import com.intellij.ui.components.JBList;
@@ -37,12 +45,13 @@ import com.intellij.util.ArrayUtil;
import com.intellij.util.PsiNavigateUtil;
import com.intellij.util.ui.EdtInvocationManager;
import com.intellij.util.ui.JBDimension;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
import static com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR;
@@ -57,43 +66,89 @@ public class ShowDiscoveredTestsAction extends AnAction {
PsiElement at = file == null || editor == null ? null : file.findElementAt(editor.getCaretModel().getOffset());
PsiMethod method = PsiTreeUtil.getParentOfType(at, PsiMethod.class);
if (method == null || project == null) {
e.getPresentation().setEnabledAndVisible(false);
}
e.getPresentation().setEnabledAndVisible(method != null && project != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
Editor editor = e.getData(EDITOR);
PsiFile file = e.getData(PSI_FILE);
Editor editor = e.getRequiredData(EDITOR);
PsiFile file = e.getRequiredData(PSI_FILE);
Project project = e.getProject();
assert project != null;
PsiElement at = file == null || editor == null ? null : file.findElementAt(editor.getCaretModel().getOffset());
PsiElement at = file.findElementAt(editor.getCaretModel().getOffset());
PsiMethod method = PsiTreeUtil.getParentOfType(at, PsiMethod.class);
assert method != null;
PsiClass c = method.getContainingClass();
String fqn = c != null ? c.getQualifiedName() : null;
if (fqn == null) return;
String methodName = method.getName();
String methodPresentationName = c.getName() + "." + methodName;
DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent());
showDiscoveredTests(project, dataContext, methodPresentationName, Collections.singletonList(method));
}
public static class FromChangeList extends AnAction {
@Override
public void update(AnActionEvent e) {
Project project = e.getProject();
Change[] changes = e.getData(VcsDataKeys.CHANGES);
e.getPresentation().setEnabledAndVisible(project != null && changes != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
Change[] changes = e.getRequiredData(VcsDataKeys.CHANGES);
Project project = e.getProject();
assert project != null;
List<PsiMethod> methods = FormatChangedTextUtil.getInstance().getChangedElements(project, Arrays.asList(changes), file -> {
if (file.getFileType() != JavaFileType.INSTANCE) return null;
PsiFile psiFile = PsiUtilCore.getPsiFile(project, file);
Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) return null;
PsiDocumentManager.getInstance(project).commitDocument(document);
List<PsiMethod> fileMethods = new ArrayList<>();
psiFile.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
if (psiFile.equals(method.getContainingFile())) {
fileMethods.add(method);
}
}
});
return fileMethods;
});
showDiscoveredTests(project, e.getDataContext(), "Selected Changes", methods);
}
}
private static void showDiscoveredTests(@NotNull Project project,
@NotNull DataContext dataContext,
@NotNull String title,
@NotNull List<PsiMethod> methods) {
CollectionListModel<PsiElement> model = new CollectionListModel<>();
final JBList<PsiElement> list = new JBList<>(model);
//list.setFixedCellHeight();
HintUpdateSupply.installSimpleHintUpdateSupply(list);
list.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
String initTitle = "Tests for " + methodPresentationName;
String initTitle = "Tests for " + title;
DefaultPsiElementCellRenderer renderer = new DefaultPsiElementCellRenderer();
Ref<JBPopup> ref = new Ref<>();
ConfigurationContext context = ConfigurationContext.getFromContext(dataContext);
InplaceButton runButton = new InplaceButton(new IconButton("Run All", AllIcons.Actions.Execute), __ -> {
Executor executor = DefaultRunExecutor.getRunExecutorInstance();
DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent());
ConfigurationContext context = ConfigurationContext.getFromContext(dataContext);
List<Module> containingModules =
model.getItems().stream()
.map(element -> ModuleUtilCore.findModuleForPsiElement(element))
@@ -101,17 +156,17 @@ public class ShowDiscoveredTestsAction extends AnAction {
.collect(Collectors.toList());
Module targetModule = TestDiscoveryConfigurationProducer.detectTargetModule(containingModules, project);
//first producer with results will be picked
getProducers(project).stream()
.map(producer -> producer.createDelegate(method, targetModule).findOrCreateConfigurationFromContext(context))
.filter(Objects::nonNull)
.findFirst()
.ifPresent(configuration -> {
ExecutionUtil.runConfiguration(configuration.getConfigurationSettings(), executor);
JBPopup popup = ref.get();
if (popup != null) {
popup.cancel();
}
});
StreamEx.of(getProducers(project)).cross(methods)
.mapKeyValue((producer, method) -> producer.createDelegate(method, targetModule).findOrCreateConfigurationFromContext(context))
.filter(Objects::nonNull)
.findFirst()
.ifPresent(configuration -> {
ExecutionUtil.runConfiguration(configuration.getConfigurationSettings(), executor);
JBPopup popup = ref.get();
if (popup != null) {
popup.cancel();
}
});
});
ActionListener pinActionListener = __ -> {
@@ -146,10 +201,10 @@ public class ShowDiscoveredTestsAction extends AnAction {
JBPopup popup = builder.createPopup();
ref.set(popup);
list.setEmptyText("No tests captured for " + methodPresentationName);
list.setEmptyText("No tests captured for " + title);
list.setPaintBusy(true);
popup.showInBestPositionFor(editor);
popup.showInBestPositionFor(dataContext);
JavaPsiFacade javaFacade = JavaPsiFacade.getInstance(project);
GlobalSearchScope scope = GlobalSearchScope.projectScope(project);
@@ -158,25 +213,32 @@ public class ShowDiscoveredTestsAction extends AnAction {
ListBackgroundUpdaterTask loadTestsTask = new ListBackgroundUpdaterTask(project, "Load tests", renderer.getComparator()) {
@Override
public String getCaption(int size) {
return "Found " + size + " Tests for " + methodPresentationName;
return "Found " + size + " Tests for " + title;
}
};
loadTestsTask.init((AbstractPopup)popup, list, new Ref<>());
ApplicationManager.getApplication().executeOnPooledThread(() -> {
for (TestDiscoveryConfigurationProducer producer : getProducers(project)) {
String frameworkPrefix =
((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getFrameworkPrefix();
TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkPrefix, (testClass, testMethod) -> {
PsiMethod psiMethod = ReadAction.compute(() -> {
PsiClass cc = testClass == null ? null : javaFacade.findClass(testClass, scope);
return cc == null ? null : ArrayUtil.getFirstElement(cc.findMethodsByName(testMethod, false));
for (PsiMethod method : methods) {
Couple<String> methodFqnName = ReadAction.compute(() -> getMethodQualifiedName(method));
if (methodFqnName == null) continue;
String fqn = methodFqnName.first;
String methodName = methodFqnName.second;
for (TestDiscoveryConfigurationProducer producer : getProducers(project)) {
String frameworkPrefix =
((JavaTestConfigurationBase)producer.getConfigurationFactory().createTemplateConfiguration(project)).getFrameworkPrefix();
TestDiscoveryProducer.consumeDiscoveredTests(project, fqn, methodName, frameworkPrefix, (testClass, testMethod) -> {
PsiMethod psiMethod = ReadAction.compute(() -> {
PsiClass cc = testClass == null ? null : javaFacade.findClass(testClass, scope);
return cc == null ? null : ArrayUtil.getFirstElement(cc.findMethodsByName(testMethod, false));
});
if (psiMethod != null) {
loadTestsTask.updateComponent(psiMethod);
}
});
if (psiMethod != null) {
loadTestsTask.updateComponent(psiMethod);
}
});
}
}
EdtInvocationManager.getInstance().invokeLater(() -> {
@@ -186,6 +248,15 @@ public class ShowDiscoveredTestsAction extends AnAction {
});
}
@Nullable
private static Couple<String> getMethodQualifiedName(@NotNull PsiMethod method) {
PsiClass c = method.getContainingClass();
String fqn = c != null ? c.getQualifiedName() : null;
if (fqn == null) return null;
String methodName = method.getName();
return Couple.of(fqn, methodName);
}
@Nullable
protected static KeyStroke findUsagesKeyStroke() {
AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_USAGES);
@@ -30,15 +30,18 @@ import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.codeStyle.ChangedRangesInfo;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.diff.FilesTooBigForDiffException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -157,6 +160,23 @@ public class FormatChangedTextUtil {
return null;
}
@NotNull
public <T extends PsiElement> List<T> getChangedElements(@NotNull Project project,
@NotNull List<Change> changes,
@NotNull Convertor<VirtualFile, List<T>> elementsConvertor) {
List<T> result = new ArrayList<>();
for (Change change : changes) {
VirtualFile file = change.getVirtualFile();
if (file != null) {
List<T> elements = elementsConvertor.convert(file);
if (elements != null) {
result.addAll(elements);
}
}
}
return result;
}
/**
* Allows to temporally suppress document modification tracking.
*
@@ -17,42 +17,44 @@ package com.intellij.codeInsight.actions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ChangeListManagerImpl;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.ex.LineStatusTracker;
import com.intellij.openapi.vcs.ex.PartialLocalLineStatusTracker;
import com.intellij.openapi.vcs.ex.Range;
import com.intellij.openapi.vcs.ex.RangesBuilder;
import com.intellij.openapi.vcs.impl.LineStatusTrackerManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.ChangedRangesInfo;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.diff.FilesTooBigForDiffException;
import com.intellij.util.containers.Convertor;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil {
@Override
@NotNull
public List<TextRange> getChangedTextRanges(@NotNull Project project, @NotNull PsiFile file) throws FilesTooBigForDiffException {
public List<TextRange> getChangedTextRanges(@NotNull Project project, @NotNull PsiFile file) {
ChangedRangesInfo helper = getChangedRangesInfo(file);
return helper != null ? helper.allChangedRanges : ContainerUtil.newArrayList();
}
@Override
@Nullable
public ChangedRangesInfo getChangedRangesInfo(@NotNull PsiFile file) throws FilesTooBigForDiffException {
public ChangedRangesInfo getChangedRangesInfo(@NotNull PsiFile file) {
Project project = file.getProject();
Document document = PsiDocumentManager.getInstance(project).getDocument(file);
if (document == null) return null;
@@ -82,6 +84,98 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil {
return contentFromVcs != null ? calculateChangedRangesInfo(document, contentFromVcs) : null;
}
@NotNull
@Override
public <T extends PsiElement> List<T> getChangedElements(@NotNull Project project,
@NotNull List<Change> changes,
@NotNull Convertor<VirtualFile, List<T>> elementsConvertor) {
List<T> result = new ArrayList<>();
for (Change change : changes) {
if (change.getType() == Change.Type.DELETED) continue;
if (!(change.getAfterRevision() instanceof CurrentContentRevision)) continue;
VirtualFile file = ((CurrentContentRevision)change.getAfterRevision()).getVirtualFile();
if (file == null) continue;
Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) continue;
List<T> elements = elementsConvertor.convert(file);
if (ContainerUtil.isEmpty(elements)) continue;
BitSet changedLines = getChangedLines(project, document, change);
if (changedLines != null) {
for (T element : elements) {
if (isElementChanged(element, document, changedLines)) {
result.add(element);
}
}
}
else {
result.addAll(elements);
}
}
return result;
}
@Nullable
private static BitSet getChangedLines(@NotNull Project project, @NotNull Document document, @NotNull Change change) {
if (change.getType() == Change.Type.NEW) return null;
List<? extends Range> ranges = getChangedRanges(project, document, change);
if (ranges == null) return null;
BitSet changedLines = new BitSet();
for (Range range : ranges) {
if (range.getType() == Range.DELETED) {
changedLines.set(range.getLine1() - 1, range.getLine1() + 1);
}
else {
changedLines.set(range.getLine1(), range.getLine2());
}
}
return changedLines;
}
@Nullable
private static List<? extends Range> getChangedRanges(@NotNull Project project, @NotNull Document document, @NotNull Change change) {
LineStatusTracker<?> tracker = LineStatusTrackerManager.getInstance(project).getLineStatusTracker(document);
if (tracker != null) {
if (change instanceof ChangeListChange && tracker instanceof PartialLocalLineStatusTracker) {
String changeListId = ((ChangeListChange)change).getChangeListId();
List<PartialLocalLineStatusTracker.LocalRange> ranges = ((PartialLocalLineStatusTracker)tracker).getRanges();
if (ranges != null) {
return ContainerUtil.filter(ranges, range -> range.getChangelistId().equals(changeListId));
}
else {
return null;
}
}
else {
return tracker.getRanges();
}
}
else {
String contentFromVcs = getRevisionedContentFrom(change);
if (contentFromVcs != null) {
return getRanges(document, contentFromVcs);
}
else {
return null;
}
}
}
private static boolean isElementChanged(@NotNull PsiElement element, @NotNull Document document, @NotNull BitSet changedLines) {
TextRange textRange = element.getTextRange();
int startLine = document.getLineNumber(textRange.getStartOffset());
int endLine = textRange.isEmpty()
? startLine + 1
: document.getLineNumber(textRange.getEndOffset() - 1) + 1;
int nextSetBit = changedLines.nextSetBit(startLine);
return nextSetBit != -1 && nextSetBit < endLine;
}
@Nullable
private static String getRevisionedContentFrom(@NotNull Change change) {
ContentRevision revision = change.getBeforeRevision();
@@ -111,39 +205,33 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil {
}
@NotNull
private static ChangedRangesInfo calculateChangedRangesInfo(@NotNull Document document,
@NotNull CharSequence contentFromVcs) throws FilesTooBigForDiffException {
private static ChangedRangesInfo calculateChangedRangesInfo(@NotNull Document document, @NotNull CharSequence contentFromVcs) {
return getChangedTextRanges(document, getRanges(document, contentFromVcs));
}
@NotNull
private static List<Range> getRanges(@NotNull Document document,
@NotNull CharSequence contentFromVcs) throws FilesTooBigForDiffException {
@NotNull CharSequence contentFromVcs) {
return RangesBuilder.createRanges(document.getImmutableCharSequence(), StringUtilRt.convertLineSeparators(contentFromVcs, "\n"));
}
@Override
public int calculateChangedLinesNumber(@NotNull Document document, @NotNull CharSequence contentFromVcs) {
try {
List<Range> changedRanges = getRanges(document, contentFromVcs);
int linesChanges = 0;
for (Range range : changedRanges) {
int inserted = range.getLine2() - range.getLine1();
int deleted = range.getVcsLine2() - range.getVcsLine1();
linesChanges += Math.max(inserted, deleted);
}
return linesChanges;
} catch (FilesTooBigForDiffException e) {
LOG.info("File too big, can not calculate changed lines number");
return -1;
List<Range> changedRanges = getRanges(document, contentFromVcs);
int linesChanges = 0;
for (Range range : changedRanges) {
int inserted = range.getLine2() - range.getLine1();
int deleted = range.getVcsLine2() - range.getVcsLine1();
linesChanges += Math.max(inserted, deleted);
}
return linesChanges;
}
@NotNull
private static ChangedRangesInfo getChangedTextRanges(@NotNull Document document, @NotNull List<? extends Range> changedRanges) {
final List<TextRange> ranges = ContainerUtil.newArrayList();
final List<TextRange> insertedRanges = ContainerUtil.newArrayList();
for (Range range : changedRanges) {
if (range.getType() != Range.DELETED) {
int changeStartLine = range.getLine1();
@@ -159,7 +247,7 @@ class VcsAwareFormatChangedTextUtil extends FormatChangedTextUtil {
}
}
}
return new ChangedRangesInfo(ranges, insertedRanges);
}
+3
View File
@@ -402,6 +402,9 @@
<action class="com.intellij.execution.testDiscovery.ShowDiscoveredTestsAction" internal="true" id="ShowDiscoveredTests" text="Show Discovered Tests">
<add-to-group group-id="Internal" anchor="last"/>
</action>
<action class="com.intellij.execution.testDiscovery.ShowDiscoveredTestsAction$FromChangeList" internal="true" id="ShowDiscoveredTestsFromChangeList" text="Show Discovered Tests">
<add-to-group group-id="ChangesViewPopupMenu" anchor="last"/>
</action>
<action id="JShell.Console"
class="com.intellij.execution.jshell.LaunchJShellConsoleAction"