[jvm] IDEA-207545 Convert TestFailedLine inspection to UAST

GitOrigin-RevId: 921c7fe92dd5b27b9fe0105fffd9f93ffb0a3e98
This commit is contained in:
Bart van Helvert
2021-05-24 17:57:08 +02:00
committed by intellij-monorepo-bot
parent dfc4c07815
commit 32a57be862
21 changed files with 353 additions and 338 deletions

View File

@@ -1,104 +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.testframework;
import com.intellij.codeInspection.*;
import com.intellij.debugger.DebuggerManagerEx;
import com.intellij.execution.*;
import com.intellij.execution.actions.ConfigurationContext;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.execution.stacktrace.StackTraceLine;
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Iconable;
import com.intellij.psi.*;
import com.intellij.testIntegration.TestFailedLineManager;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class TestFailedLineInspection extends LocalInspectionTool {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
PsiElement nameElement = call.getMethodExpression().getReferenceNameElement();
if (nameElement == null) return;
TestStateStorage.Record state = TestFailedLineManager.getInstance(holder.getProject()).getFailedLineState(call);
if (state == null) return;
if (state.magnitude <= TestStateInfo.Magnitude.IGNORED_INDEX.getValue())
return;
LocalQuickFix[] fixes = {new DebugFailedTestFix(call, state.topStacktraceLine),
new RunActionFix(call, DefaultRunExecutor.EXECUTOR_ID)};
ProblemDescriptor descriptor = InspectionManager.getInstance(holder.getProject())
.createProblemDescriptor(nameElement, state.errorMessage, isOnTheFly, fixes,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
descriptor.setTextAttributes(CodeInsightColors.RUNTIME_ERROR);
holder.registerProblem(descriptor);
}
};
}
private static class DebugFailedTestFix extends RunActionFix{
private final String myTopStacktraceLine;
DebugFailedTestFix(PsiElement element, String topStacktraceLine) {
super(element, DefaultDebugExecutor.EXECUTOR_ID);
myTopStacktraceLine = topStacktraceLine;
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
StackTraceLine line = new StackTraceLine(project, myTopStacktraceLine);
Location<PsiMethod> location = line.getMethodLocation(project);
if (location != null) {
Document document = PsiDocumentManager.getInstance(project).getDocument(location.getPsiElement().getContainingFile());
if (document != null) {
DebuggerManagerEx.getInstanceEx(project).getBreakpointManager().addLineBreakpoint(document, line.getLineNumber());
}
}
super.applyFix(project, descriptor);
}
}
private static class RunActionFix implements LocalQuickFix, Iconable {
private final Executor myExecutor;
private final RunnerAndConfigurationSettings myConfiguration;
RunActionFix(PsiElement element, String executorId) {
myExecutor = ExecutorRegistry.getInstance().getExecutorById(executorId);
ConfigurationContext context = new ConfigurationContext(element);
myConfiguration = context.getConfiguration();
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
String text = myExecutor.getStartActionText(ProgramRunnerUtil.shortenName(myConfiguration.getName(), 0));
return UIUtil.removeMnemonic(text);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
ExecutionUtil.runConfiguration(myConfiguration, myExecutor);
}
@Override
public Icon getIcon(int flags) {
return myExecutor.getIcon();
}
}
}

View File

@@ -345,7 +345,6 @@
<projectService serviceImplementation="com.intellij.jarRepository.RemoteRepositoriesConfiguration"/>
<projectService serviceImplementation="com.intellij.jarRepository.services.MavenRepositoryServicesManager"/>
<projectService serviceImplementation="com.intellij.psi.util.ProjectIconsAccessor"/>
<projectService serviceImplementation="com.intellij.testIntegration.TestFailedLineManager"/>
<projectService serviceImplementation="com.intellij.codeInsight.JavaProjectCodeInsightSettings"/>
<projectService serviceImplementation="com.intellij.javadoc.JavadocGenerationManager"/>
<projectService serviceInterface="com.intellij.codeInspection.ex.EntryPointsManager"
@@ -1314,11 +1313,6 @@
<java.error.fix errorCode="lambda.variable.must.be.final" implementationClass="com.intellij.codeInsight.daemon.impl.quickfix.VariableAccessFromInnerClassJava10Fix"/>
<stripTrailingSpacesFilterFactory implementation="com.intellij.codeEditor.JavaStripTrailingSpacesFilterFactory"/>
<localInspection groupPath="Java" language="JAVA" shortName="TestFailedLine"
enabledByDefault="true" level="WARNING"
groupBundle="messages.InspectionsBundle" groupKey="group.names.junit.issues"
implementationClass="com.intellij.execution.testframework.TestFailedLineInspection"
key="inspection.test.failed.line.display.name" bundle="messages.JavaBundle"/>
<localInspection language="JAVA" shortName="MoveFieldAssignmentToInitializer" enabledByDefault="true" level="INFORMATION"
groupPath="Java" groupBundle="messages.InspectionsBundle" groupKey="group.names.code.style.issues"
implementationClass="com.intellij.codeInspection.MoveFieldAssignmentToInitializerInspection"

View File

@@ -1,97 +0,0 @@
// Copyright 2000-2021 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.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 project.getService(TestFailedLineManager.class);
}
public TestFailedLineManager(@NotNull Project project) {
myStorage = TestStateStorage.getInstance(project);
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, false)) return null;
String url = "java:test://" + ClassUtil.getJVMClassName(psiClass) + "/" + psiMethod.getName();
TestStateStorage.Record state = myStorage.getState(url);
if (state == null) return new TestInfo(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 || info.myRecord == 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;
}
}
}

View File

@@ -11,6 +11,7 @@ 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;
@@ -24,21 +25,33 @@ public class TestRunLineMarkerProvider extends RunLineMarkerContributor {
if (isIdentifier(e)) {
PsiElement element = e.getParent();
if (element instanceof PsiClass) {
TestFramework framework = TestFrameworks.detectFramework((PsiClass)element);
if (framework != null && framework.isTestClass(element)) {
String url = "java:suite://" + ClassUtil.getJVMClassName((PsiClass)element);
TestStateStorage.Record state = TestStateStorage.getInstance(e.getProject()).getState(url);
return getInfo(state, true);
}
if (!isTestClass((PsiClass)element)) return null;
String url = "java:suite://" + ClassUtil.getJVMClassName((PsiClass)element);
TestStateStorage.Record state = TestStateStorage.getInstance(e.getProject()).getState(url);
return getInfo(state, true);
}
if (element instanceof PsiMethod) {
TestFailedLineManager.TestInfo testInfo = TestFailedLineManager.getInstance(e.getProject()).getTestInfo((PsiMethod)element);
return testInfo == null ? null : getInfo(testInfo.myRecord, false);
PsiClass containingClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (!isTestMethod(containingClass, (PsiMethod)element)) return null;
String url = "java:test://" + ClassUtil.getJVMClassName(containingClass) + "/" + ((PsiMethod)element).getName();
TestStateStorage.Record state = TestStateStorage.getInstance(e.getProject()).getState(url);
return getInfo(state, false);
}
}
return null;
}
private static boolean isTestClass(PsiClass clazz) {
TestFramework framework = TestFrameworks.detectFramework(clazz);
return framework != null && framework.isTestClass(clazz);
}
private static boolean isTestMethod(PsiClass containingClass, PsiMethod method) {
if (containingClass == null) return false;
TestFramework framework = TestFrameworks.detectFramework(containingClass);
return framework != null && framework.isTestMethod(method, false);
}
@NotNull
private static Info getInfo(TestStateStorage.Record state, boolean isClass) {
return new Info(getTestStateIcon(state, isClass), ExecutorAction.getActions(1), element -> ExecutionBundle.message("run.text"));

View File

@@ -1,7 +0,0 @@
<html>
<body>
Reports failed method calls or assertions in tests.
It helps to detect the failed line in code faster and start debugging it immediately if needed.
<!-- tooltip end -->
</body>
</html>

View File

@@ -1,115 +0,0 @@
// Copyright 2000-2020 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.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.execution.TestStateStorage;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.testframework.JavaTestLocator;
import com.intellij.execution.testframework.TestConsoleProperties;
import com.intellij.execution.testframework.TestFailedLineInspection;
import com.intellij.execution.testframework.sm.runner.MockRuntimeConfiguration;
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
import com.intellij.execution.testframework.sm.runner.SMTestProxy;
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo;
import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView;
import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm;
import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.project.DumbServiceImpl;
import com.intellij.openapi.util.Disposer;
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.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testIntegration.TestFailedLineManager;
import com.intellij.util.concurrency.AppExecutorUtil;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class FailedLineTest extends LightJavaCodeInsightFixtureTestCase {
public void testFailedLineManager() {
configure();
myFixture.enableInspections(new TestFailedLineInspection());
myFixture.testHighlighting();
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();
List<HighlightInfo> infos = myFixture.doHighlighting(HighlightSeverity.WARNING);
assertEquals(1, infos.size());
TextAttributes attributes = infos.get(0).forcedTextAttributes;
assertNotNull(attributes);
assertEquals(EffectType.BOLD_DOTTED_LINE, attributes.getEffectType());
}
public void testTopStacktraceLine() {
TestStateStorage.Record record = configure();
assertEquals("\tat MainTest.assertEquals(Assert.java:207)", record.topStacktraceLine);
}
public void testDumbMode() throws InterruptedException {
TestConsoleProperties consoleProperties = new SMTRunnerConsoleProperties(new MockRuntimeConfiguration(getProject()), "SMRunnerTests",
DefaultDebugExecutor.getDebugExecutorInstance());
SMTRunnerConsoleView view = new SMTRunnerConsoleView(consoleProperties);
try {
DumbServiceImpl.getInstance(getProject()).setDumb(true);
String url = "schema://url";
SMTestProxy test = new SMTestProxy("foo", false, url);
test.setLocator(JavaTestLocator.INSTANCE);
view.initUI();
SMTestRunnerResultsForm form = view.getResultsViewer();
form.getTestsRootNode().addChild(test);
test.setTestFailed("oops", "stacktrace", true);
ApplicationManager.getApplication().invokeLater(() -> DumbServiceImpl.getInstance(getProject()).setDumb(false));
form.onTestingFinished(form.getTestsRootNode());
AppExecutorUtil.getAppExecutorService().awaitTermination(1, TimeUnit.SECONDS);
}
finally {
Disposer.dispose(view);
DumbServiceImpl.getInstance(getProject()).setDumb(false);
}
}
private TestStateStorage.Record configure() {
myFixture.addClass("package junit.framework; public class TestCase {}");
myFixture.configureByText("MainTest.java", " public class MainTest extends junit.framework.TestCase {\n" +
" public void testFoo() {\n" +
" <warning descr=\"oops\">assertE<caret>quals</warning>();\n" +
" assertEquals();\n" +
" }\n" +
" public void assertEquals() {}\n" +
" }");
String url = "java:test://MainTest/testFoo";
TestStackTraceParser pair = new TestStackTraceParser(url, "\tat junit.framework.Assert.fail(Assert.java:47)\n" +
"\tat MainTest.assertEquals(Assert.java:207)\n" +
"\tat MainTest.testFoo(MainTest.java:3)", "oops", JavaTestLocator.INSTANCE, getProject());
assertEquals(3, pair.getFailedLine());
assertEquals("assertEquals", pair.getFailedMethodName());
TestStateStorage.Record record = new TestStateStorage.Record(TestStateInfo.Magnitude.FAILED_INDEX.getValue(), new Date(),
0, pair.getFailedLine(), pair.getFailedMethodName(),
pair.getErrorMessage(), pair.getTopLocationLine());
TestStateStorage.getInstance(getProject()).writeState(url, record);
return record;
}
}

View File

@@ -10,6 +10,10 @@
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.jvm.analysis" exported="" />
<orderEntry type="module" module-name="intellij.java" />
<orderEntry type="module" module-name="intellij.java.debugger.impl" />
<orderEntry type="module" module-name="intellij.java.execution.impl" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.debugger" />
<orderEntry type="library" name="StreamEx" level="project" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
</component>

View File

@@ -6,6 +6,7 @@
interface="com.intellij.codeInspection.blockingCallsDetection.NonBlockingContextChecker" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceImplementation="com.intellij.codeInspection.TestFailedLineManager"/>
<localInspection language="UAST" enabledByDefault="true" level="WARNING" shortName="UnstableApiUsage"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspections.unstable.api.usage.display.name"
@@ -48,6 +49,10 @@
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspections.testonly.display.name"
implementationClass="com.intellij.codeInspection.TestOnlyInspection"/>
<localInspection language="UAST" enabledByDefault="false" level="WARNING" shortName="TestFailedLine"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspection.test.failed.line.display.name"
implementationClass="com.intellij.codeInspection.TestFailedLineInspection"/>
<notificationGroup id="UAST" displayType="BALLOON"/>
</extensions>
<extensions defaultExtensionNs="com.intellij.codeInsight">

View File

@@ -0,0 +1,14 @@
<html>
<body>
Reports failed method calls or assertions in tests. It helps to detect the failed line in code faster and start debugging it immediately if
needed.
<p>Example:</p>
<pre>
@Test
fun foo() {
assertEquals(1, 0) // highlighted
}
</pre>
<!-- tooltip end -->
</body>
</html>

View File

@@ -60,6 +60,8 @@ jvm.inspections.testonly.field.reference=Test-only field is referenced in produc
jvm.inspections.testonly.method.call=Test-only method is called in production code
jvm.inspections.testonly.visiblefortesting=@VisibleForTesting makes little sense on @TestOnly code
jvm.inspection.test.failed.line.display.name=Failed line in test
jvm.inspections.string.touppercase.tolowercase.without.locale.description=<code>String.{0}()</code> called without specifying a Locale using internationalized strings #loc
can.t.build.uast.tree.for.file=Can't build UAST tree for file
title.uast=UAST

View File

@@ -0,0 +1,89 @@
// Copyright 2000-2021 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.codeInspection
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.execution.ExecutorRegistry
import com.intellij.execution.ProgramRunnerUtil
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.runners.ExecutionUtil
import com.intellij.execution.stacktrace.StackTraceLine
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.editor.colors.CodeInsightColors
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Iconable
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.uast.UastVisitorAdapter
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.Nls
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.sourcePsiElement
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
import javax.swing.Icon
class TestFailedLineInspection : AbstractBaseUastLocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor =
UastVisitorAdapter(TestFailedVisitor(holder, isOnTheFly), true)
class TestFailedVisitor(private val holder: ProblemsHolder, private val isOnTheFly: Boolean) : AbstractUastNonRecursiveVisitor() {
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression): Boolean {
if (node.lang is JavaLanguage) return false // java doesn't pass in the UCallExpression selector as a standalone element
return true
}
override fun visitCallExpression(node: UCallExpression): Boolean {
val sourcePsi = node.sourcePsi ?: return true
val state = TestFailedLineManager.getInstance(holder.project).getFailedLineState(node) ?: return true
if (state.magnitude <= TestStateInfo.Magnitude.IGNORED_INDEX.value) return true
val fixes = arrayOf<LocalQuickFix>(
DebugActionFix(sourcePsi, state.topStacktraceLine),
RunActionFix(sourcePsi, DefaultRunExecutor.EXECUTOR_ID)
)
val identifier = node.methodIdentifier.sourcePsiElement ?: return true
val descriptor = InspectionManager.getInstance(holder.project).createProblemDescriptor(
identifier, state.errorMessage, isOnTheFly, fixes, ProblemHighlightType.GENERIC_ERROR_OR_WARNING
).apply { setTextAttributes(CodeInsightColors.RUNTIME_ERROR) }
holder.registerProblem(descriptor)
return true
}
}
private class DebugActionFix(
element: PsiElement,
private val myTopStacktraceLine: String
) : RunActionFix(element, DefaultDebugExecutor.EXECUTOR_ID) {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val line = StackTraceLine(project, myTopStacktraceLine)
line.getMethodLocation(project)?.let { location ->
PsiDocumentManager.getInstance(project).getDocument(location.psiElement.containingFile)?.let { document ->
DebuggerManagerEx.getInstanceEx(project).breakpointManager.addLineBreakpoint(document, line.lineNumber)
}
}
super.applyFix(project, descriptor)
}
}
private open class RunActionFix(element: PsiElement, executorId: String) : LocalQuickFix, Iconable {
private val myExecutor = ExecutorRegistry.getInstance().getExecutorById(executorId) ?: throw IllegalStateException(
"Could not create action because executor $executorId was not found.")
private val myConfiguration: RunnerAndConfigurationSettings =
ConfigurationContext(element).configuration ?: throw IllegalStateException(
"Could not create action because configuration context was not found for element $element.")
override fun getFamilyName(): @Nls(capitalization = Nls.Capitalization.Sentence) String =
UIUtil.removeMnemonic(myExecutor.getStartActionText(ProgramRunnerUtil.shortenName(myConfiguration.name, 0)))
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
ExecutionUtil.runConfiguration(myConfiguration, myExecutor)
}
override fun getIcon(flags: Int): Icon = myExecutor.icon
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2000-2021 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.codeInspection
import com.intellij.codeInsight.TestFrameworks
import com.intellij.execution.TestStateStorage
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.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.psi.util.ClassUtil
import com.intellij.util.containers.FactoryMap
import org.jetbrains.uast.*
class TestFailedLineManager(project: Project) : FileEditorManagerListener {
private val testStorage = TestStateStorage.getInstance(project)
private val cache = FactoryMap.create<VirtualFile, MutableMap<String, TestInfo>> { hashMapOf() }
init {
project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this)
}
class TestInfo(var record: TestStateStorage.Record) {
var pointer: SmartPsiElementPointer<PsiElement>? = null
}
private fun getTestInfo(method: UMethod): TestInfo? {
val containingClass = method.getContainingUClass() ?: return null
val javaClazz = containingClass.javaPsi
val framework = TestFrameworks.detectFramework(javaClazz) ?: return null
if (!framework.isTestMethod(method.javaPsi, false)) return null
val url = "java:test://" + ClassUtil.getJVMClassName(javaClazz) + "/" + method.name
val state = testStorage.getState(url) ?: return null
val vFile = method.getContainingUFile()?.sourcePsi?.virtualFile ?: return null
val infoInFile = cache[vFile] ?: return null
var info = infoInFile[url]
if (info == null || state.date != info.record.date) {
info = TestInfo(state)
infoInFile[url] = info
}
return info
}
fun getFailedLineState(call: UCallExpression): TestStateStorage.Record? {
val containingMethod = call.getContainingUMethod() ?: return null
val callSourcePsi = call.sourcePsi ?: return null
val file = call.getContainingUFile()?.sourcePsi ?: return null
val info = getTestInfo(containingMethod) ?: return null
val document = PsiDocumentManager.getInstance(callSourcePsi.project).getDocument(file) ?: return null
info.pointer?.element?.let { pointerElem ->
if (callSourcePsi == pointerElem) {
info.record.failedLine = document.getLineNumber(callSourcePsi.textOffset) + 1
return info.record
}
}
if (info.record.failedLine == -1 || StringUtil.isEmpty(info.record.failedMethod)) return null
if (info.record.failedLine != document.getLineNumber(callSourcePsi.textOffset) + 1) return null
if (info.record.failedMethod != call.methodName) return null
info.pointer = SmartPointerManager.createPointer(callSourcePsi)
return info.record
}
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {
cache.remove(file)?.forEach { (s: String, info: TestInfo) -> testStorage.writeState(s, info.record) }
}
companion object {
fun getInstance(project: Project): TestFailedLineManager = project.getService(TestFailedLineManager::class.java)
}
}

View File

@@ -0,0 +1,8 @@
public class MainTest extends junit.framework.TestCase {
public void testFoo() {
<warning descr="junit.framework.AssertionFailedError:">assertEquals</warning>();
assertEquals();
}
public void assertEquals() {}
}

View File

@@ -0,0 +1,7 @@
public class QualifiedTest extends junit.framework.TestCase {
public void testFoo() {
QualifiedTest.<warning descr="junit.framework.AssertionFailedError:">assertEquals</warning>();
}
public static void assertEquals() {}
}

View File

@@ -0,0 +1,28 @@
package com.intellij.codeInspection.tests.java
import com.intellij.codeInspection.tests.TestFailedLineInspectionTestBase
import com.intellij.jvm.analysis.JvmAnalysisTestsUtil
class JavaTestFailedLineInspectionTest : TestFailedLineInspectionTestBase() {
override fun getTestDataPath() = JvmAnalysisTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/testfailedline"
fun testMainTest() {
doTest(
fileName = "MainTest",
fileExt = "java",
methodName = "testFoo",
errorLn = 3,
errorMessage = "junit.framework.AssertionFailedError:"
)
}
fun testQualifiedTest() {
doTest(
fileName = "QualifiedTest",
fileExt = "java",
methodName = "testFoo",
errorLn = 3,
errorMessage = "junit.framework.AssertionFailedError:"
)
}
}

View File

@@ -0,0 +1,8 @@
class MainTest : junit.framework.TestCase() {
fun testFoo() {
<warning descr="junit.framework.AssertionFailedError:">assertEquals</warning>()
assertEquals()
}
fun assertEquals() {}
}

View File

@@ -0,0 +1,9 @@
class QualifiedTest : junit.framework.TestCase() {
fun testFoo() {
Assertions.<warning descr="junit.framework.AssertionFailedError:">assertEquals</warning>()
}
object Assertions {
fun assertEquals() {}
}
}

View File

@@ -0,0 +1,28 @@
package com.intellij.codeInspection
import com.intellij.codeInspection.tests.TestFailedLineInspectionTestBase
import com.intellij.jvm.analysis.JvmAnalysisKtTestsUtil
class KotlinTestFailedLineInspectionTest : TestFailedLineInspectionTestBase() {
override fun getBasePath() = JvmAnalysisKtTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/testfailedline"
fun testMainTest() {
doTest(
fileName = "MainTest",
fileExt = "kt",
methodName = "testFoo",
errorLn = 3,
errorMessage = "junit.framework.AssertionFailedError:"
)
}
fun testQualifiedTest() {
doTest(
fileName = "QualifiedTest",
fileExt = "kt",
methodName = "testFoo",
errorLn = 3,
errorMessage = "junit.framework.AssertionFailedError:"
)
}
}

View File

@@ -9,5 +9,6 @@
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.jvm.analysis.impl" exported="" />
<orderEntry type="module" module-name="intellij.java.testFramework" exported="" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
</component>
</module>

View File

@@ -0,0 +1,53 @@
package com.intellij.codeInspection.tests
import com.intellij.codeInspection.TestFailedLineInspection
import com.intellij.execution.TestStateStorage
import com.intellij.execution.testframework.JavaTestLocator
import com.intellij.execution.testframework.sm.runner.states.TestStateInfo
import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase
import java.util.*
abstract class TestFailedLineInspectionTestBase : JavaCodeInsightFixtureTestCase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(inspection)
myFixture.addClass("package junit.framework; public class TestCase {}")
}
override fun tearDown() {
try {
myFixture.disableInspections(inspection)
}
finally {
super.tearDown()
}
}
protected fun doTest(fileName: String, fileExt: String, methodName: String, errorLn: Int, errorMessage: String) {
val url = "java:test://$fileName/$methodName"
val pair = TestStackTraceParser(url, """
|${'\t'}at junit.framework.Assert.fail(Assert.java:47)
|${'\t'}at $fileName.assertEquals(Assert.java:207)
|${'\t'}at $fileName.$methodName($fileName.$fileExt:$errorLn)""".trimMargin(), errorMessage, JavaTestLocator.INSTANCE, project)
assertEquals(errorLn, pair.failedLine)
assertEquals("assertEquals", pair.failedMethodName)
val record = TestStateStorage.Record(
TestStateInfo.Magnitude.FAILED_INDEX.value, Date(), 0,
pair.failedLine, pair.failedMethodName, pair.errorMessage, pair.topLocationLine
)
TestStateStorage.getInstance(project).writeState(url, record)
myFixture.testHighlighting("$fileName.$fileExt")
val infos = myFixture.doHighlighting(HighlightSeverity.WARNING)
assertEquals(1, infos.size)
val attributes = infos[0].forcedTextAttributes
assertNotNull(attributes)
assertEquals(EffectType.BOLD_DOTTED_LINE, attributes.effectType)
}
companion object {
private val inspection = TestFailedLineInspection()
}
}

View File

@@ -44,7 +44,7 @@ public class TestStateStorage implements Disposable {
public final long configurationHash;
public final Date date;
public int failedLine;
public final String failedMethod;
public String failedMethod;
public final @NlsSafe String errorMessage;
public final String topStacktraceLine;