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; + } + } +}