diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml
index 9923eada804c..7e2178656c4c 100644
--- a/java/java-impl/src/META-INF/JavaPlugin.xml
+++ b/java/java-impl/src/META-INF/JavaPlugin.xml
@@ -199,6 +199,12 @@
+
+
diff --git a/java/java-impl/src/com/intellij/testIntegration/TestFailedLineInspection.java b/java/java-impl/src/com/intellij/testIntegration/TestFailedLineInspection.java
new file mode 100644
index 000000000000..718d999daa24
--- /dev/null
+++ b/java/java-impl/src/com/intellij/testIntegration/TestFailedLineInspection.java
@@ -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);
+ }
+ };
+ }
+}
diff --git a/java/java-impl/src/com/intellij/testIntegration/TestFailedLineManager.java b/java/java-impl/src/com/intellij/testIntegration/TestFailedLineManager.java
new file mode 100644
index 000000000000..d89626d5d0a0
--- /dev/null
+++ b/java/java-impl/src/com/intellij/testIntegration/TestFailedLineManager.java
@@ -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> 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 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 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 myPointer;
+
+ public TestInfo(TestStateStorage.Record record) {
+ myRecord = record;
+ }
+ }
+}
diff --git a/java/java-impl/src/com/intellij/testIntegration/TestRunLineMarkerProvider.java b/java/java-impl/src/com/intellij/testIntegration/TestRunLineMarkerProvider.java
index 06d160c09092..7a4c5be9c4cd 100644
--- a/java/java-impl/src/com/intellij/testIntegration/TestRunLineMarkerProvider.java
+++ b/java/java-impl/src/com/intellij/testIntegration/TestRunLineMarkerProvider.java
@@ -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);
}
diff --git a/java/java-impl/src/inspectionDescriptions/TestFailedLine.html b/java/java-impl/src/inspectionDescriptions/TestFailedLine.html
new file mode 100644
index 000000000000..6d780f208245
--- /dev/null
+++ b/java/java-impl/src/inspectionDescriptions/TestFailedLine.html
@@ -0,0 +1,5 @@
+
+
+This inspection highlights failed method calls or assertions in tests.
+
+
\ No newline at end of file
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/FailedLineTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/FailedLineTest.java
new file mode 100644
index 000000000000..f6efe1f7687e
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/FailedLineTest.java
@@ -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 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" +
+ " assertEquals();\n" +
+ " assertEquals();\n" +
+ " }\n" +
+ " public void assertEquals() {}\n" +
+ " }");
+ }
+}
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/RunLineMarkerTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/RunLineMarkerTest.java
index 9489d2465899..ecce8e3d8382 100644
--- a/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/RunLineMarkerTest.java
+++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/navigation/RunLineMarkerTest.java
@@ -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 MainTest extends junit.framework.TestCase {\n" +
diff --git a/platform/lang-impl/src/com/intellij/execution/TestStateStorage.java b/platform/lang-impl/src/com/intellij/execution/TestStateStorage.java
index b4add3f5c8fe..393bfe179653 100644
--- a/platform/lang-impl/src/com/intellij/execution/TestStateStorage.java
+++ b/platform/lang-impl/src/com/intellij/execution/TestStateStorage.java
@@ -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);
}
diff --git a/platform/lang-impl/src/com/intellij/execution/lineMarker/RunLineMarkerContributor.java b/platform/lang-impl/src/com/intellij/execution/lineMarker/RunLineMarkerContributor.java
index ba804cfbe6bd..8fd56488f94f 100644
--- a/platform/lang-impl/src/com/intellij/execution/lineMarker/RunLineMarkerContributor.java
+++ b/platform/lang-impl/src/com/intellij/execution/lineMarker/RunLineMarkerContributor.java
@@ -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) {
diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java
index 2124daa18a11..c3bfeeacf248 100644
--- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java
+++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/SMTestProxy.java
@@ -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);
diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsForm.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsForm.java
index 6563ce025195..c7742464708c 100644
--- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsForm.java
+++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/SMTestRunnerResultsForm.java
@@ -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 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()) {
diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestStackTraceParser.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestStackTraceParser.java
new file mode 100644
index 000000000000..e6e253fafcbc
--- /dev/null
+++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestStackTraceParser.java
@@ -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 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;
+ }
+ }
+}