Skip to content

Commit 538b845

Browse files
Solve issue #207: Ask for concrete subclass when running tests in abstract class
- Implemented `SearchTools.findConcreteSubclasses(IType)` to retrieve non-abstract implementations of a type. - Enhanced `RunTestsActionExecutor` to automatically resolve or prompt for concrete subclasses when test execution is triggered from an abstract class or its CUT. - Added "All concrete subclasses" option to the selection dialog. - Updated `JUnitTestSelectionLaunchConfigurationDelegate` to support granular `IMember` execution (types or methods). - Added JUnit tests for subclass resolution logic. - Enhanced test infrastructure to support interface definitions in `@Project` annotations. Co-authored-by: RoiSoleil <3462260+RoiSoleil@users.noreply.github.com>
1 parent 39ae626 commit 538b845

9 files changed

Lines changed: 307 additions & 16 deletions

File tree

org.moreunit.plugin/src/org/moreunit/handler/RunTestsActionExecutor.java

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@
1313

1414
import static org.moreunit.elements.CorrespondingMemberRequest.newCorrespondingMemberRequest;
1515

16+
import java.util.ArrayList;
1617
import java.util.Collection;
18+
import java.util.Collections;
1719
import java.util.LinkedHashSet;
20+
import java.util.List;
21+
import java.util.Set;
1822

1923
import org.eclipse.core.resources.IFile;
2024
import org.eclipse.core.runtime.NullProgressMonitor;
25+
import org.eclipse.jdt.core.Flags;
2126
import org.eclipse.jdt.core.ICompilationUnit;
2227
import org.eclipse.jdt.core.IJavaElement;
2328
import org.eclipse.jdt.core.IJavaProject;
2429
import org.eclipse.jdt.core.IMember;
2530
import org.eclipse.jdt.core.IMethod;
2631
import org.eclipse.jdt.core.IType;
2732
import org.eclipse.jdt.core.JavaCore;
33+
import org.eclipse.jdt.core.JavaModelException;
2834
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
35+
import org.eclipse.swt.graphics.Image;
36+
import org.eclipse.swt.widgets.Display;
2937
import org.eclipse.ui.IEditorPart;
3038
import org.moreunit.actions.RunTestAction;
3139
import org.moreunit.actions.RunTestFromCompilationUnitAction;
@@ -42,7 +50,12 @@
4250
import org.moreunit.launch.TestLauncher;
4351
import org.moreunit.preferences.Preferences;
4452
import org.moreunit.preferences.Preferences.MethodSearchMode;
53+
import org.moreunit.ui.ChooseDialog;
54+
import org.moreunit.ui.MemberContentProvider;
55+
import org.moreunit.ui.TreeActionElement;
4556
import org.moreunit.util.FeatureDetector;
57+
import org.moreunit.util.MemberJumpHistory;
58+
import org.moreunit.util.SearchTools;
4659

4760
/**
4861
* Executes the actions "Run test(s)" launched from the handlers:<br>
@@ -134,10 +147,56 @@ private void executeRunAllTestsAction(ICompilationUnit compilationUnit, String l
134147
{
135148
testCases.add(selectedJavaType);
136149
}
137-
return testCases;
150+
return resolveAbstractTestCases(testCases);
138151
}, testCases -> runTests(testCases, launchMode));
139152
}
140153

154+
private Collection<IType> resolveAbstractTestCases(Collection<IType> testCases)
155+
{
156+
Collection<IType> resolvedTestCases = new LinkedHashSet<>();
157+
for (IType testCase : testCases)
158+
{
159+
resolvedTestCases.addAll(resolveAbstractTestCase(testCase));
160+
}
161+
return resolvedTestCases;
162+
}
163+
164+
private Collection<IType> resolveAbstractTestCase(IType testCase)
165+
{
166+
try
167+
{
168+
if(! Flags.isAbstract(testCase.getFlags()))
169+
{
170+
return Collections.singleton(testCase);
171+
}
172+
173+
Collection<IType> concreteSubclasses = SearchTools.findConcreteSubclasses(testCase);
174+
if(concreteSubclasses.isEmpty())
175+
{
176+
return Collections.singleton(testCase);
177+
}
178+
if(concreteSubclasses.size() == 1)
179+
{
180+
return Collections.singleton(concreteSubclasses.iterator().next());
181+
}
182+
183+
Collection<IType> choice = chooseSubclasses(testCase, concreteSubclasses);
184+
if(choice != null && ! choice.isEmpty())
185+
{
186+
for (IType type : choice)
187+
{
188+
MemberJumpHistory.getInstance().registerJump(testCase, type);
189+
}
190+
return choice;
191+
}
192+
}
193+
catch (JavaModelException e)
194+
{
195+
// ignore and return original
196+
}
197+
return Collections.singleton(testCase);
198+
}
199+
141200
private void saveIfNeeded(ICompilationUnit compilationUnit)
142201
{
143202
IEditorPart editorPart = EditorUtility.isOpenInEditor(compilationUnit);
@@ -194,10 +253,139 @@ private void executeRunTestsOfSelectedMemberAction(IMethod methodFromEditor, ICo
194253
{
195254
testElements.add(getTestElementFromTestCase(methodFromEditor, selectedJavaType));
196255
}
197-
return testElements;
256+
return resolveAbstractTestElements(testElements);
198257
}, testElements -> runTests(testElements, launchMode));
199258
}
200259

260+
private Collection<IMember> resolveAbstractTestElements(Collection<IMember> testElements)
261+
{
262+
Collection<IMember> resolvedTestElements = new LinkedHashSet<>();
263+
for (IMember testElement : testElements)
264+
{
265+
resolvedTestElements.addAll(resolveAbstractTestElement(testElement));
266+
}
267+
return resolvedTestElements;
268+
}
269+
270+
private Collection<IMember> resolveAbstractTestElement(IMember testElement)
271+
{
272+
IType type = testElement instanceof IType ? (IType) testElement : testElement.getDeclaringType();
273+
try
274+
{
275+
if(! Flags.isAbstract(type.getFlags()))
276+
{
277+
return Collections.singleton(testElement);
278+
}
279+
280+
Collection<IType> concreteSubclasses = SearchTools.findConcreteSubclasses(type);
281+
if(concreteSubclasses.isEmpty())
282+
{
283+
return Collections.singleton(testElement);
284+
}
285+
if(concreteSubclasses.size() == 1)
286+
{
287+
return Collections.singleton(substituteType(testElement, concreteSubclasses.iterator().next()));
288+
}
289+
290+
Collection<IType> chosenSubclasses = chooseSubclasses(type, concreteSubclasses);
291+
if(chosenSubclasses != null && ! chosenSubclasses.isEmpty())
292+
{
293+
Collection<IMember> resolvedElements = new ArrayList<>();
294+
for (IType chosenSubclass : chosenSubclasses)
295+
{
296+
MemberJumpHistory.getInstance().registerJump(testElement, chosenSubclass);
297+
resolvedElements.add(substituteType(testElement, chosenSubclass));
298+
}
299+
return resolvedElements;
300+
}
301+
}
302+
catch (JavaModelException e)
303+
{
304+
// ignore and return original
305+
}
306+
return Collections.singleton(testElement);
307+
}
308+
309+
private IMember substituteType(IMember testElement, IType newType)
310+
{
311+
if(testElement instanceof IType)
312+
{
313+
return newType;
314+
}
315+
// It's a method. We return the method with the same name from the new type if it exists.
316+
// If not, we still return the method from the abstract class, but the launcher should handle it.
317+
// Actually, JDT's JUnit launcher handles methods from superclasses correctly if the target type is the subclass.
318+
// But for clarity, let's see if we can find the method in the subclass.
319+
IMethod method = (IMethod) testElement;
320+
IMethod subclassMethod = newType.getMethod(method.getElementName(), method.getParameterTypes());
321+
if(subclassMethod.exists())
322+
{
323+
return subclassMethod;
324+
}
325+
return method;
326+
}
327+
328+
private Collection<IType> chooseSubclasses(IType abstractType, Collection<IType> concreteSubclasses)
329+
{
330+
IMember defaultSelection = MemberJumpHistory.getInstance().getLastCorrespondingJumpMember(abstractType);
331+
if(! concreteSubclasses.contains(defaultSelection))
332+
{
333+
defaultSelection = null;
334+
}
335+
336+
MemberContentProvider contentProvider = new MemberContentProvider(concreteSubclasses, (IType) defaultSelection);
337+
contentProvider.withAction(new AllSubclassesAction(concreteSubclasses));
338+
339+
String promptText = "Choose concrete subclass for " + abstractType.getElementName();
340+
return Display.getDefault().syncCall(() -> {
341+
ChooseDialog<Object> dialog = new ChooseDialog<>(promptText, contentProvider);
342+
Object choice = dialog.getChoice();
343+
if(choice instanceof Collection)
344+
{
345+
return (Collection<IType>) choice;
346+
}
347+
if(choice instanceof IType)
348+
{
349+
return Collections.singleton((IType) choice);
350+
}
351+
return null;
352+
});
353+
}
354+
355+
private static class AllSubclassesAction implements TreeActionElement<Collection<IType>>
356+
{
357+
private final Collection<IType> concreteSubclasses;
358+
359+
public AllSubclassesAction(Collection<IType> concreteSubclasses)
360+
{
361+
this.concreteSubclasses = concreteSubclasses;
362+
}
363+
364+
@Override
365+
public boolean provideElement()
366+
{
367+
return true;
368+
}
369+
370+
@Override
371+
public Collection<IType> execute()
372+
{
373+
return concreteSubclasses;
374+
}
375+
376+
@Override
377+
public String getText()
378+
{
379+
return "All concrete subclasses";
380+
}
381+
382+
@Override
383+
public Image getImage()
384+
{
385+
return null;
386+
}
387+
}
388+
201389
/**
202390
* Returns the test method that is selected in editor if any, otherwise
203391
* returns the test case.

org.moreunit.plugin/src/org/moreunit/launch/JUnitTestSelectionLaunchConfigurationDelegate.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,16 @@
1818
*/
1919
class JUnitTestSelectionLaunchConfigurationDelegate extends JUnitLaunchConfigurationDelegate
2020
{
21-
private final Collection<IType> testCasesToRun;
21+
private final Collection<IMember> testMembersToRun;
2222

2323
JUnitTestSelectionLaunchConfigurationDelegate(Collection< ? extends IMember> testsToRun)
2424
{
25-
testCasesToRun = getTestCases(testsToRun);
26-
}
27-
28-
private Collection<IType> getTestCases(Collection< ? extends IMember> testsToRun)
29-
{
30-
Collection<IType> testCases = new LinkedHashSet<IType>();
31-
for (IMember testMember : testsToRun)
32-
{
33-
testCases.add(testMember instanceof IType ? (IType) testMember : testMember.getDeclaringType());
34-
}
35-
return testCases;
25+
testMembersToRun = new LinkedHashSet<>(testsToRun);
3626
}
3727

3828
@Override
3929
protected IMember[] evaluateTests(ILaunchConfiguration configuration, IProgressMonitor monitor) throws CoreException
4030
{
41-
return testCasesToRun.toArray(new IMember[0]);
31+
return testMembersToRun.toArray(new IMember[0]);
4232
}
4333
}

org.moreunit.plugin/src/org/moreunit/util/SearchTools.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import java.util.TreeSet;
1010

1111
import org.eclipse.core.runtime.CoreException;
12+
import org.eclipse.core.runtime.NullProgressMonitor;
13+
import org.eclipse.jdt.core.Flags;
1214
import org.eclipse.jdt.core.IType;
15+
import org.eclipse.jdt.core.ITypeHierarchy;
16+
import org.eclipse.jdt.core.JavaModelException;
1317
import org.eclipse.jdt.core.search.IJavaSearchScope;
1418
import org.eclipse.jdt.core.search.SearchEngine;
1519
import org.eclipse.jdt.core.search.SearchMatch;
@@ -31,6 +35,21 @@ public static Set<IType> searchFor(Collection<String> typeNamePatterns, IJavaSea
3135
return search(createSearchPattern(typeNamePatterns, TYPE, DECLARATIONS, R_PATTERN_MATCH), scope);
3236
}
3337

38+
public static Set<IType> findConcreteSubclasses(IType type) throws JavaModelException
39+
{
40+
Set<IType> concreteSubclasses = new LinkedHashSet<>();
41+
ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor());
42+
IType[] subtypes = hierarchy.getAllSubtypes(type);
43+
for (IType subtype : subtypes)
44+
{
45+
if(! Flags.isAbstract(subtype.getFlags()) && ! Flags.isInterface(subtype.getFlags()))
46+
{
47+
concreteSubclasses.add(subtype);
48+
}
49+
}
50+
return concreteSubclasses;
51+
}
52+
3453
private static Set<IType> search(SearchPattern pattern, IJavaSearchScope scope) throws CoreException
3554
{
3655
SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };

org.moreunit.test.dependencies/src/org/moreunit/test/context/AnnotationConfigExtractor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ private Collection<JavaType> splitTypes(String classes)
265265
{
266266
types.add(JavaType.newEnum(packageName, typeDef.substring(5)));
267267
}
268+
else if(typeDef.startsWith("interface "))
269+
{
270+
types.add(JavaType.newInterface(packageName, typeDef.substring(10)));
271+
}
268272
else if(typeDef.startsWith("class "))
269273
{
270274
types.add(JavaType.newClass(packageName, typeDef.substring(6)));

org.moreunit.test.dependencies/src/org/moreunit/test/workspace/JavaType.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public static JavaType newClass(String packageName, String typeName)
2727
return new JavaType(JavaTypeKind.CLASS, packageName, typeName);
2828
}
2929

30+
public static JavaType newInterface(String packageName, String typeName)
31+
{
32+
return new JavaType(JavaTypeKind.INTERFACE, packageName, typeName);
33+
}
34+
3035
@Override
3136
public int hashCode()
3237
{

org.moreunit.test.dependencies/src/org/moreunit/test/workspace/JavaTypeKind.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
public enum JavaTypeKind
44
{
5-
CLASS, ENUM;
5+
CLASS, ENUM, INTERFACE;
66

77
String toJavaCode()
88
{

org.moreunit.test.dependencies/src/org/moreunit/test/workspace/SourceFolderHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ public TypeHandler createType(JavaType javaType)
6767
{
6868
type = WorkspaceHelper.createJavaClass(packageFragment, javaType.typeName);
6969
}
70+
else if(javaType.typeKind == JavaTypeKind.INTERFACE)
71+
{
72+
type = WorkspaceHelper.createJavaInterface(packageFragment, javaType.typeName);
73+
}
7074
else
7175
{
7276
type = WorkspaceHelper.createJavaEnum(packageFragment, javaType.typeName);
@@ -90,6 +94,11 @@ public TypeHandler createEnum(String fullyQualifiedTypeName)
9094
return createType(JavaTypeKind.ENUM, fullyQualifiedTypeName);
9195
}
9296

97+
public TypeHandler createInterface(String fullyQualifiedTypeName)
98+
{
99+
return createType(JavaTypeKind.INTERFACE, fullyQualifiedTypeName);
100+
}
101+
93102
public CompilationUnitHandler createCompilationUnit(String fullyQualifiedTypeName, String contents)
94103
{
95104
try
@@ -113,6 +122,10 @@ private TypeHandler createType(JavaTypeKind typeKind, String fullyQualifiedTypeN
113122
{
114123
return createType(JavaType.newEnum(name.packageName, name.typeName));
115124
}
125+
if(typeKind == JavaTypeKind.INTERFACE)
126+
{
127+
return createType(JavaType.newInterface(name.packageName, name.typeName));
128+
}
116129
return createType(JavaType.newClass(name.packageName, name.typeName));
117130
}
118131

org.moreunit.test.dependencies/src/org/moreunit/test/workspace/WorkspaceHelper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ public static IType createJavaEnum(IPackageFragment packageFragment, String java
189189
return createJavaType(packageFragment, javaClassName, JavaTypeKind.ENUM);
190190
}
191191

192+
public static IType createJavaInterface(IPackageFragment packageFragment, String javaClassName) throws JavaModelException
193+
{
194+
return createJavaType(packageFragment, javaClassName, JavaTypeKind.INTERFACE);
195+
}
196+
192197
private static IType createJavaType(IPackageFragment packageFragment, String javaClassName, JavaTypeKind type) throws JavaModelException
193198
{
194199
String sourceCode = String.format("%s%s%s", getPackageDeclarationString(packageFragment), NEW_LINE, getTypeDeclarationString(type, javaClassName));

0 commit comments

Comments
 (0)