Skip to content

Commit 0467c6b

Browse files
Allow disabling and enabling tests via a quick assist (#2738)
* Add JUnit Quick Assist Processor for disabling/enabling tests Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent cfc342e commit 0467c6b

File tree

9 files changed

+910
-0
lines changed

9 files changed

+910
-0
lines changed

org.eclipse.jdt.junit/plugin.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ providerName=Eclipse.org
1717

1818
testRunListeners.name= Test Run Listeners
1919
junitQuickFixProcessor= JUnit Quick Fix Processor
20+
junitQuickAssistProcessor= JUnit Quick Assist Processor
2021
junitClasspathFixProcessor= JUnit Classpath Fix Processor
2122

2223
junitLaunchConfigs.name= JUnit Launch Configurations

org.eclipse.jdt.junit/plugin.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@
272272
</quickFixProcessor>
273273
</extension>
274274

275+
<extension
276+
point="org.eclipse.jdt.ui.quickAssistProcessors">
277+
<quickAssistProcessor
278+
name="%junitQuickAssistProcessor"
279+
class="org.eclipse.jdt.internal.junit.ui.JUnitQuickAssistProcessor"
280+
id="org.eclipse.jdt.junit.JUnitQuickAssistProcessor">
281+
</quickAssistProcessor>
282+
</extension>
283+
275284
<extension
276285
point="org.eclipse.jdt.ui.classpathFixProcessors">
277286
<classpathFixProcessor
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Carsten Hammer and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Carsten Hammer using github copilot - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.internal.junit.ui;
15+
16+
import org.eclipse.swt.graphics.Image;
17+
import org.eclipse.swt.graphics.Point;
18+
19+
import org.eclipse.core.runtime.CoreException;
20+
21+
import org.eclipse.text.edits.MultiTextEdit;
22+
import org.eclipse.text.edits.TextEdit;
23+
24+
import org.eclipse.jface.text.BadLocationException;
25+
import org.eclipse.jface.text.IDocument;
26+
import org.eclipse.jface.text.contentassist.IContextInformation;
27+
28+
import org.eclipse.jdt.core.ICompilationUnit;
29+
import org.eclipse.jdt.core.dom.AST;
30+
import org.eclipse.jdt.core.dom.CompilationUnit;
31+
import org.eclipse.jdt.core.dom.MethodDeclaration;
32+
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
33+
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
34+
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
35+
36+
import org.eclipse.jdt.ui.CodeStyleConfiguration;
37+
import org.eclipse.jdt.ui.ISharedImages;
38+
import org.eclipse.jdt.ui.JavaUI;
39+
import org.eclipse.jdt.ui.text.java.IInvocationContext;
40+
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
41+
42+
public class AddAnnotationProposal implements IJavaCompletionProposal {
43+
44+
private final IInvocationContext fContext;
45+
private final MethodDeclaration fMethodDecl;
46+
private final String fAnnotationQualifiedName;
47+
private final String fAnnotationSimpleName;
48+
49+
public AddAnnotationProposal(IInvocationContext context, MethodDeclaration methodDecl, String annotationQualifiedName, String annotationSimpleName) {
50+
fContext = context;
51+
fMethodDecl = methodDecl;
52+
fAnnotationQualifiedName = annotationQualifiedName;
53+
fAnnotationSimpleName = annotationSimpleName;
54+
}
55+
56+
@Override
57+
public void apply(IDocument document) {
58+
try {
59+
CompilationUnit astRoot = fContext.getASTRoot();
60+
ICompilationUnit cu = fContext.getCompilationUnit();
61+
62+
AST ast = astRoot.getAST();
63+
ASTRewrite rewrite = ASTRewrite.create(ast);
64+
65+
// Add the annotation
66+
org.eclipse.jdt.core.dom.MarkerAnnotation annotation = ast.newMarkerAnnotation();
67+
annotation.setTypeName(ast.newName(fAnnotationSimpleName));
68+
69+
ListRewrite listRewrite = rewrite.getListRewrite(fMethodDecl, MethodDeclaration.MODIFIERS2_PROPERTY);
70+
listRewrite.insertFirst(annotation, null);
71+
72+
// Add import
73+
ImportRewrite importRewrite = CodeStyleConfiguration.createImportRewrite(astRoot, true);
74+
importRewrite.addImport(fAnnotationQualifiedName);
75+
76+
// Combine both edits using MultiTextEdit to avoid conflicts
77+
MultiTextEdit multiEdit = new MultiTextEdit();
78+
79+
TextEdit importEdit = importRewrite.rewriteImports(null);
80+
if (importEdit.hasChildren() || importEdit.getLength() != 0) {
81+
multiEdit.addChild(importEdit);
82+
}
83+
84+
TextEdit rewriteEdit = rewrite.rewriteAST(document, cu.getOptions(true));
85+
multiEdit.addChild(rewriteEdit);
86+
87+
// Apply the combined edit
88+
multiEdit.apply(document);
89+
90+
} catch (CoreException | BadLocationException e) {
91+
JUnitPlugin.log(e);
92+
}
93+
}
94+
95+
@Override
96+
public String getAdditionalProposalInfo() {
97+
return java.text.MessageFormat.format(JUnitMessages.JUnitQuickAssistProcessor_add_annotation_info, fAnnotationSimpleName);
98+
}
99+
100+
@Override
101+
public IContextInformation getContextInformation() {
102+
return null;
103+
}
104+
105+
@Override
106+
public String getDisplayString() {
107+
return java.text.MessageFormat.format(JUnitMessages.JUnitQuickAssistProcessor_add_annotation_description, fAnnotationSimpleName);
108+
}
109+
110+
@Override
111+
public Image getImage() {
112+
return JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_ANNOTATION);
113+
}
114+
115+
@Override
116+
public Point getSelection(IDocument document) {
117+
return null;
118+
}
119+
120+
@Override
121+
public int getRelevance() {
122+
return 10;
123+
}
124+
}

org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ public final class JUnitMessages extends NLS {
237237

238238
public static String JUnitQuickFixProcessor_add_assert_info;
239239

240+
public static String JUnitQuickAssistProcessor_add_annotation_description;
241+
242+
public static String JUnitQuickAssistProcessor_add_annotation_info;
243+
244+
public static String JUnitQuickAssistProcessor_remove_annotation_description;
245+
246+
public static String JUnitQuickAssistProcessor_remove_annotation_info;
247+
240248
public static String JUnitViewEditorLauncher_dialog_title;
241249

242250
public static String JUnitViewEditorLauncher_error_occurred;

org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,8 @@ ClasspathVariableMarkerResolutionGenerator_use_JUnit3=Use the JUnit 3 library
279279
ClasspathVariableMarkerResolutionGenerator_use_JUnit3_desc=Changes the classpath variable entry to use the JUnit 3 library
280280

281281
TestSearchEngine_search_message_progress_monitor=Searching for test methods in ''{0}''
282+
283+
JUnitQuickAssistProcessor_add_annotation_description=Disable test with @{0}
284+
JUnitQuickAssistProcessor_add_annotation_info=Adds @{0} annotation to disable this test
285+
JUnitQuickAssistProcessor_remove_annotation_description=Enable test (remove @{0})
286+
JUnitQuickAssistProcessor_remove_annotation_info=Removes @{0} annotation to enable this test
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Carsten Hammer and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Carsten Hammer using github copilot - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.internal.junit.ui;
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import org.eclipse.core.runtime.CoreException;
20+
21+
import org.eclipse.jdt.core.dom.ASTNode;
22+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
23+
import org.eclipse.jdt.core.dom.IMethodBinding;
24+
import org.eclipse.jdt.core.dom.ITypeBinding;
25+
import org.eclipse.jdt.core.dom.MethodDeclaration;
26+
27+
import org.eclipse.jdt.internal.junit.JUnitCorePlugin;
28+
29+
import org.eclipse.jdt.ui.text.java.IInvocationContext;
30+
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
31+
import org.eclipse.jdt.ui.text.java.IProblemLocation;
32+
import org.eclipse.jdt.ui.text.java.IQuickAssistProcessor;
33+
34+
public class JUnitQuickAssistProcessor implements IQuickAssistProcessor {
35+
36+
private static final String JUNIT4_IGNORE_ANNOTATION = "org.junit.Ignore"; //$NON-NLS-1$
37+
private static final String JUNIT5_DISABLED_ANNOTATION = "org.junit.jupiter.api.Disabled"; //$NON-NLS-1$
38+
private static final String JUNIT5_TEST_ANNOTATION = "org.junit.jupiter.api.Test"; //$NON-NLS-1$
39+
private static final String JUNIT5_PARAMETERIZED_TEST_ANNOTATION = "org.junit.jupiter.params.ParameterizedTest"; //$NON-NLS-1$
40+
private static final String JUNIT5_REPEATED_TEST_ANNOTATION = "org.junit.jupiter.api.RepeatedTest"; //$NON-NLS-1$
41+
private static final String JUNIT5_TEST_FACTORY_ANNOTATION = "org.junit.jupiter.api.TestFactory"; //$NON-NLS-1$
42+
private static final String JUNIT5_TEST_TEMPLATE_ANNOTATION = "org.junit.jupiter.api.TestTemplate"; //$NON-NLS-1$
43+
44+
@Override
45+
public boolean hasAssists(IInvocationContext context) throws CoreException {
46+
ASTNode coveringNode = context.getCoveringNode();
47+
if (coveringNode == null) {
48+
return false;
49+
}
50+
51+
MethodDeclaration methodDecl = getMethodDeclaration(coveringNode);
52+
if (methodDecl == null) {
53+
return false;
54+
}
55+
56+
return isJUnitTestMethod(methodDecl);
57+
}
58+
59+
@Override
60+
public IJavaCompletionProposal[] getAssists(IInvocationContext context, IProblemLocation[] locations) throws CoreException {
61+
ASTNode coveringNode = context.getCoveringNode();
62+
if (coveringNode == null) {
63+
return null;
64+
}
65+
66+
MethodDeclaration methodDecl = getMethodDeclaration(coveringNode);
67+
if (methodDecl == null) {
68+
return null;
69+
}
70+
71+
if (!isJUnitTestMethod(methodDecl)) {
72+
return null;
73+
}
74+
75+
List<IJavaCompletionProposal> proposals = new ArrayList<>();
76+
77+
boolean hasDisabledAnnotation = hasAnnotation(methodDecl, JUNIT5_DISABLED_ANNOTATION);
78+
boolean hasIgnoreAnnotation = hasAnnotation(methodDecl, JUNIT4_IGNORE_ANNOTATION);
79+
80+
if (hasDisabledAnnotation || hasIgnoreAnnotation) {
81+
// Offer to remove the annotation
82+
String annotationToRemove = hasDisabledAnnotation ? JUNIT5_DISABLED_ANNOTATION : JUNIT4_IGNORE_ANNOTATION;
83+
proposals.add(new RemoveAnnotationProposal(context, methodDecl, annotationToRemove));
84+
} else {
85+
// Offer to add the appropriate annotation based on JUnit version
86+
if (isJUnit5TestMethod(methodDecl)) {
87+
// JUnit 5 test
88+
proposals.add(new AddAnnotationProposal(context, methodDecl, JUNIT5_DISABLED_ANNOTATION, "Disabled")); //$NON-NLS-1$
89+
} else if (hasAnnotation(methodDecl, JUnitCorePlugin.JUNIT4_ANNOTATION_NAME)) {
90+
// JUnit 4 test
91+
proposals.add(new AddAnnotationProposal(context, methodDecl, JUNIT4_IGNORE_ANNOTATION, "Ignore")); //$NON-NLS-1$
92+
}
93+
}
94+
95+
if (proposals.isEmpty()) {
96+
return null;
97+
}
98+
99+
return proposals.toArray(new IJavaCompletionProposal[proposals.size()]);
100+
}
101+
102+
private MethodDeclaration getMethodDeclaration(ASTNode node) {
103+
while (node != null && !(node instanceof MethodDeclaration)) {
104+
node = node.getParent();
105+
}
106+
return (MethodDeclaration) node;
107+
}
108+
109+
private boolean isJUnitTestMethod(MethodDeclaration methodDecl) {
110+
IMethodBinding binding = methodDecl.resolveBinding();
111+
if (binding == null) {
112+
return false;
113+
}
114+
115+
IAnnotationBinding[] annotations = binding.getAnnotations();
116+
for (IAnnotationBinding annotation : annotations) {
117+
ITypeBinding annotationType = annotation.getAnnotationType();
118+
if (annotationType != null) {
119+
String qualifiedName = annotationType.getQualifiedName();
120+
if (JUnitCorePlugin.JUNIT4_ANNOTATION_NAME.equals(qualifiedName) ||
121+
isJUnit5TestAnnotation(qualifiedName)) {
122+
return true;
123+
}
124+
}
125+
}
126+
127+
return false;
128+
}
129+
130+
private boolean isJUnit5TestMethod(MethodDeclaration methodDecl) {
131+
IMethodBinding binding = methodDecl.resolveBinding();
132+
if (binding == null) {
133+
return false;
134+
}
135+
136+
IAnnotationBinding[] annotations = binding.getAnnotations();
137+
for (IAnnotationBinding annotation : annotations) {
138+
ITypeBinding annotationType = annotation.getAnnotationType();
139+
if (annotationType != null) {
140+
String qualifiedName = annotationType.getQualifiedName();
141+
if (isJUnit5TestAnnotation(qualifiedName)) {
142+
return true;
143+
}
144+
}
145+
}
146+
147+
return false;
148+
}
149+
150+
private boolean isJUnit5TestAnnotation(String qualifiedName) {
151+
return JUNIT5_TEST_ANNOTATION.equals(qualifiedName) ||
152+
JUNIT5_PARAMETERIZED_TEST_ANNOTATION.equals(qualifiedName) ||
153+
JUNIT5_REPEATED_TEST_ANNOTATION.equals(qualifiedName) ||
154+
JUNIT5_TEST_FACTORY_ANNOTATION.equals(qualifiedName) ||
155+
JUNIT5_TEST_TEMPLATE_ANNOTATION.equals(qualifiedName);
156+
}
157+
158+
private boolean hasAnnotation(MethodDeclaration methodDecl, String annotationQualifiedName) {
159+
IMethodBinding binding = methodDecl.resolveBinding();
160+
if (binding == null) {
161+
return false;
162+
}
163+
164+
IAnnotationBinding[] annotations = binding.getAnnotations();
165+
for (IAnnotationBinding annotation : annotations) {
166+
ITypeBinding annotationType = annotation.getAnnotationType();
167+
if (annotationType != null && annotationQualifiedName.equals(annotationType.getQualifiedName())) {
168+
return true;
169+
}
170+
}
171+
172+
return false;
173+
}
174+
}

0 commit comments

Comments
 (0)