failed test line highlighting

This commit is contained in:
Dmitry Avdeev
2018-04-25 15:40:51 +03:00
parent 4261a85d1a
commit eacf0b939a
12 changed files with 294 additions and 19 deletions
@@ -199,6 +199,12 @@
<callHierarchyProvider language="JAVA" implementationClass="com.intellij.ide.hierarchy.call.JavaCallHierarchyProvider"/>
<runLineMarkerContributor language="JAVA" order="after applicationRunLineMarkerProvider"
implementationClass="com.intellij.testIntegration.TestRunLineMarkerProvider"/>
<projectService serviceImplementation="com.intellij.testIntegration.TestFailedLineManager"/>
<localInspection groupPath="Java" language="JAVA" shortName="TestFailedLine"
enabledByDefault="true" level="ERROR"
groupBundle="messages.InspectionsBundle" groupKey="group.names.junit.issues"
displayName="Highlight problem line in test"
implementationClass="com.intellij.testIntegration.TestFailedLineInspection"/>
<testGenerator language="JAVA" implementationClass="com.intellij.testIntegration.createTest.JavaTestGenerator" />
<testCreator language="JAVA" implementationClass="com.intellij.testIntegration.JavaTestCreator"/>
<testFinder implementation="com.intellij.testIntegration.JavaTestFinder"/>
@@ -0,0 +1,27 @@
// 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.testIntegration;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.execution.TestStateStorage;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiMethodCallExpression;
import org.jetbrains.annotations.NotNull;
public class TestFailedLineInspection extends LocalInspectionTool {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
TestStateStorage.Record state = TestFailedLineManager.getInstance(call.getProject()).getFailedLineState(call);
if (state == null) return;
holder.registerProblem(call, state.errorMessage);
}
};
}
}
@@ -0,0 +1,98 @@
// 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.testIntegration;
import com.intellij.codeInsight.TestFrameworks;
import com.intellij.execution.TestStateStorage;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.FactoryMap;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
public class TestFailedLineManager implements FileEditorManagerListener {
private final TestStateStorage myStorage;
private final Map<VirtualFile, Map<String, TestInfo>> myMap;
public static TestFailedLineManager getInstance(@NotNull Project project) {
return ServiceManager.getService(project, TestFailedLineManager.class);
}
public TestFailedLineManager(Project project, TestStateStorage storage) {
myStorage = storage;
myMap = FactoryMap.create(o -> new HashMap<>());
project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this);
}
public TestInfo getTestInfo(@NotNull PsiMethod psiMethod) {
PsiClass psiClass = PsiTreeUtil.getParentOfType(psiMethod, PsiClass.class);
if (psiClass == null) return null;
TestFramework framework = TestFrameworks.detectFramework(psiClass);
if (framework == null || !framework.isTestMethod(psiMethod)) return null;
String url = "java:test://" + ClassUtil.getJVMClassName(psiClass) + "." + psiMethod.getName();
TestStateStorage.Record state = myStorage.getState(url);
if (state == null) return null;
VirtualFile file = psiMethod.getContainingFile().getVirtualFile();
Map<String, TestInfo> map = myMap.get(file);
TestInfo info = map.get(url);
if (info == null || !state.date.equals(info.myRecord.date)) {
info = new TestInfo(state);
map.put(url, info);
}
return info;
}
public TestStateStorage.Record getFailedLineState(PsiMethodCallExpression call) {
PsiMethod psiMethod = PsiTreeUtil.getParentOfType(call, PsiMethod.class);
if (psiMethod == null) return null;
TestInfo info = getTestInfo(psiMethod);
if (info == null) return null;
Document document = PsiDocumentManager.getInstance(call.getProject()).getDocument(call.getContainingFile());
if (document == null) return null;
if (info.myPointer != null) {
PsiElement element = info.myPointer.getElement();
if (element != null) {
if (call == element) {
info.myRecord.failedLine = document.getLineNumber(call.getTextOffset()) + 1;
return info.myRecord;
}
return null;
}
}
TestStateStorage.Record state = info.myRecord;
if (state.failedLine == -1 || StringUtil.isEmpty(state.failedMethod)) return null;
if (!state.failedMethod.equals(call.getMethodExpression().getText())) return null;
if (state.failedLine != document.getLineNumber(call.getTextOffset()) + 1) return null;
info.myPointer = SmartPointerManager.createPointer(call);
return info.myRecord;
}
@Override
public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
Map<String, TestInfo> map = myMap.remove(file);
if (map != null) {
map.forEach((s, info) -> myStorage.writeState(s, info.myRecord));
}
}
public static class TestInfo {
public TestStateStorage.Record myRecord;
public SmartPsiElementPointer<PsiElement> myPointer;
public TestInfo(TestStateStorage.Record record) {
myRecord = record;
}
}
}
@@ -16,15 +16,14 @@
package com.intellij.testIntegration;
import com.intellij.codeInsight.TestFrameworks;
import com.intellij.execution.TestStateStorage;
import com.intellij.execution.lineMarker.ExecutorAction;
import com.intellij.execution.lineMarker.RunLineMarkerContributor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiIdentifier;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -43,26 +42,21 @@ public class TestRunLineMarkerProvider extends RunLineMarkerContributor {
TestFramework framework = TestFrameworks.detectFramework((PsiClass)element);
if (framework != null && framework.isTestClass(element)) {
String url = "java:suite://" + ClassUtil.getJVMClassName((PsiClass)element);
return getInfo(url, e.getProject(), true);
TestStateStorage.Record state = TestStateStorage.getInstance(e.getProject()).getState(url);
return getInfo(state, true);
}
}
if (element instanceof PsiMethod) {
PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (psiClass != null) {
TestFramework framework = TestFrameworks.detectFramework(psiClass);
if (framework != null && framework.isTestMethod(element)) {
String url = "java:test://" + ClassUtil.getJVMClassName(psiClass) + "." + ((PsiMethod)element).getName();
return getInfo(url, e.getProject(), false);
}
}
TestFailedLineManager.TestInfo testInfo = TestFailedLineManager.getInstance(e.getProject()).getTestInfo((PsiMethod)element);
return testInfo == null ? null : getInfo(testInfo.myRecord, false);
}
}
return null;
}
@NotNull
private static Info getInfo(String url, Project project, boolean isClass) {
Icon icon = getTestStateIcon(url, project, isClass);
private static Info getInfo(TestStateStorage.Record state, boolean isClass) {
Icon icon = getTestStateIcon(state, isClass);
return new Info(icon, ExecutorAction.getActions(1), RunLineMarkerContributor.RUN_TEST_TOOLTIP_PROVIDER);
}
@@ -0,0 +1,5 @@
<html>
<body>
This inspection highlights failed method calls or assertions in tests.
</body>
</html>
@@ -0,0 +1,60 @@
// 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.java.codeInsight.navigation;
import com.intellij.execution.TestStateStorage;
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo;
import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import com.intellij.testIntegration.TestFailedLineInspection;
import com.intellij.testIntegration.TestFailedLineManager;
import java.util.Date;
public class FailedLineTest extends LightCodeInsightFixtureTestCase {
public void testFailedLineManager() {
configure();
PsiElement element = PsiUtilBase.getElementAtCaret(getEditor());
PsiMethodCallExpression callExpression = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class);
PsiMethod psiMethod = PsiTreeUtil.getParentOfType(callExpression, PsiMethod.class);
TestFailedLineManager manager = TestFailedLineManager.getInstance(getProject());
assertNotNull(manager.getTestInfo(psiMethod));
TestStateStorage.Record record = manager.getFailedLineState(callExpression);
assertNotNull(record);
}
public void testFailedLineInspection() {
configure();
myFixture.enableInspections(new TestFailedLineInspection());
myFixture.testHighlighting();
}
private void configure() {
String url = "java:test://MainTest.testFoo";
Pair<Integer, String> pair = TestStackTraceParser.findFailLine("\tat junit.framework.Assert.fail(Assert.java:47)\n" +
"\tat MainTest.assertEquals(Assert.java:207)\n" +
"\tat MainTest.testFoo(MainTest.java:3)", url);
assertEquals(3, pair.first.intValue());
assertEquals("assertEquals", pair.second);
TestStateStorage.getInstance(getProject())
.writeState(url, new TestStateStorage.Record(TestStateInfo.Magnitude.FAILED_INDEX.getValue(), new Date(),
0, pair.first, pair.second, "oops"));
myFixture.addClass("package junit.framework; public class TestCase {}");
myFixture.configureByText("MainTest.java", " public class MainTest extends junit.framework.TestCase {\n" +
" public void testFoo() {\n" +
" <error descr=\"oops\">assertE<caret>quals()</error>;\n" +
" assertEquals();\n" +
" }\n" +
" public void assertEquals() {}\n" +
" }");
}
}
@@ -82,7 +82,8 @@ public class RunLineMarkerTest extends LightCodeInsightFixtureTestCase {
TestStateStorage stateStorage = TestStateStorage.getInstance(getProject());
String testUrl = "java:suite://Main$MainTest";
try {
stateStorage.writeState(testUrl, new TestStateStorage.Record(TestStateInfo.Magnitude.FAILED_INDEX.getValue(), new Date(), 0));
stateStorage.writeState(testUrl, new TestStateStorage.Record(TestStateInfo.Magnitude.FAILED_INDEX.getValue(), new Date(), 0, 0, "",
""));
myFixture.addClass("package junit.framework; public class TestCase {}");
PsiFile file = myFixture.configureByText("MainTest.java", "public class Main {\n" +
" public class Main<caret>Test extends junit.framework.TestCase {\n" +
@@ -22,6 +22,7 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.DataExternalizer;
@@ -45,7 +46,7 @@ public class TestStateStorage implements Disposable {
private static final File TEST_HISTORY_PATH = new File(PathManager.getSystemPath(), "testHistory");
private static final int CURRENT_VERSION = 1;
private static final int CURRENT_VERSION = 4;
private final File myFile;
@@ -57,11 +58,17 @@ public class TestStateStorage implements Disposable {
public final int magnitude;
public final long configurationHash;
public final Date date;
public int failedLine;
public final String failedMethod;
public final String errorMessage;
public Record(int magnitude, Date date, long configurationHash) {
public Record(int magnitude, Date date, long configurationHash, int failLine, String method, String errorMessage) {
this.magnitude = magnitude;
this.date = date;
this.configurationHash = configurationHash;
this.failedLine = failLine;
failedMethod = method;
this.errorMessage = errorMessage;
}
}
@@ -105,11 +112,14 @@ public class TestStateStorage implements Disposable {
out.writeInt(value.magnitude);
out.writeLong(value.date.getTime());
out.writeLong(value.configurationHash);
out.writeInt(value.failedLine);
out.writeUTF(StringUtil.notNullize(value.failedMethod));
out.writeUTF(StringUtil.notNullize(value.errorMessage));
}
@Override
public Record read(@NotNull DataInput in) throws IOException {
return new Record(in.readInt(), new Date(in.readLong()), in.readLong());
return new Record(in.readInt(), new Date(in.readLong()), in.readLong(), in.readInt(), in.readUTF(), in.readUTF());
}
}, 4096, CURRENT_VERSION);
}
@@ -81,6 +81,10 @@ public abstract class RunLineMarkerContributor {
protected static Icon getTestStateIcon(String url, Project project, boolean isClass) {
TestStateStorage.Record state = TestStateStorage.getInstance(project).getState(url);
return getTestStateIcon(state, isClass);
}
protected static Icon getTestStateIcon(TestStateStorage.Record state, boolean isClass) {
if (state != null) {
TestStateInfo.Magnitude magnitude = TestIconMapper.getMagnitude(state.magnitude);
if (magnitude != null) {
@@ -59,6 +59,7 @@ public class SMTestProxy extends AbstractTestProxy {
private boolean myHasPassedTestsCached = false;
private String myStacktrace;
private String myErrorMessage;
private boolean myIsEmptyIsCached = false; // is used for separating unknown and unset values
private boolean myIsEmpty = true;
@@ -138,6 +139,15 @@ public class SMTestProxy extends AbstractTestProxy {
if (myStacktrace == null) myStacktrace = stacktrace;
}
@Nullable
public String getStacktrace() {
return myStacktrace;
}
public String getErrorMessage() {
return myErrorMessage;
}
public boolean isLeaf() {
return myChildren == null || myChildren.isEmpty();
}
@@ -412,6 +422,7 @@ public class SMTestProxy extends AbstractTestProxy {
public void setTestFailed(@NotNull String localizedMessage, @Nullable String stackTrace, boolean testError) {
setStacktraceIfNotSet(stackTrace);
myErrorMessage = localizedMessage;
TestFailedState failedState = new TestFailedState(localizedMessage, stackTrace);
if (myState instanceof TestComparisionFailedState) {
CompoundTestFailedState states = new CompoundTestFailedState(localizedMessage, stackTrace);
@@ -458,6 +469,7 @@ public class SMTestProxy extends AbstractTestProxy {
@Nullable final String expectedFilePath,
@Nullable final String actualFilePath) {
setStacktraceIfNotSet(stackTrace);
myErrorMessage = localizedMessage;
final TestComparisionFailedState comparisionFailedState = new TestComparisionFailedState(localizedMessage, stackTrace, actualText, expectedText, expectedFilePath, actualFilePath);
if (myState instanceof CompoundTestFailedState) {
((CompoundTestFailedState)myState).addFailure(comparisionFailedState);
@@ -51,6 +51,7 @@ import com.intellij.openapi.progress.util.ColorProgressBar;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Pass;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
@@ -842,12 +843,16 @@ public class SMTestRunnerResultsForm extends TestResultsPanel
String url = proxy instanceof SMTestProxy.SMRootTestProxy ? ((SMTestProxy.SMRootTestProxy)proxy).getRootLocation() : proxy.getLocationUrl();
if (url != null) {
String configurationName = myConfiguration != null ? myConfiguration.getName() : null;
storage.writeState(url, new TestStateStorage.Record(proxy.getMagnitude(), new Date(),
configurationName == null ? 0 : configurationName.hashCode()));
Pair<Integer, String> pair = TestStackTraceParser.findFailLine(proxy.getStacktrace(), proxy.getLocationUrl());
storage.writeState(url, new TestStateStorage.Record(proxy.getMagnitude(), new Date(),
configurationName == null ? 0 : configurationName.hashCode(),
pair == null ? -1 : pair.first, pair == null ? "" : pair.second,
proxy.getErrorMessage()));
}
}
});
}
@Override
public void onSuccess() {
if (myOutputFile != null && myOutputFile.exists()) {
@@ -0,0 +1,53 @@
// 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.testframework.sm.runner.ui;
import com.intellij.openapi.util.Pair;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestStackTraceParser {
private final static Pattern outerPattern = Pattern.compile("\tat (.*)\\.([^.]*)\\((.*)\\)");
private final static Pattern innerPattern = Pattern.compile("(.*):(\\d*)");
/**
* Return line number and called method name.
*/
@Nullable
public static Pair<Integer, String> findFailLine(@Nullable String stacktrace, @Nullable String url) {
if (stacktrace == null || url == null) return null;
int i = url.indexOf("//");
if (i == -1) return null;
String path = "\tat " + url.substring(i + 2);
try (BufferedReader reader = new BufferedReader(new StringReader(stacktrace))) {
String line, previous = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith(path)) {
Matcher matcher = outerPattern.matcher(line);
if (!matcher.matches()) return null;
Matcher matcher1 = innerPattern.matcher(matcher.group(3));
if (!matcher1.matches()) return null;
int lineNumber = Integer.parseInt(matcher1.group(2));
if (previous == null) return null;
Matcher matcher2 = outerPattern.matcher(previous);
if (!matcher2.matches()) return null;
return Pair.create(lineNumber, matcher2.group(2));
}
previous = line;
}
return null;
}
catch (IOException | NumberFormatException e) {
return null;
}
}
}