Skip to content

Commit a0a3523

Browse files
committed
Improve JUnit version detection from class annotations and imports
Previously detectJUnitTestKind only inspected the project classpath, which caused JUnit 4 test classes to be launched with the JUnit 5 runner in PDE/mixed-classpath projects where both versions are resolvable. Now the detection first inspects the test class itself: - Checks method and type annotations (@test, @RunWith, @ExtendWith, etc.) - Uses import statements to disambiguate bare annotation names - Checks superclass for junit.framework.TestCase (JUnit 3) - Falls back to project-level classpath scanning Also distinguishes JUnit 6 from JUnit 5 when Jupiter annotations are found, by checking the junit-jupiter-api jar version on the classpath.
1 parent 32483b2 commit a0a3523

1 file changed

Lines changed: 205 additions & 12 deletions

File tree

  • plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/UnitTestService.java

Lines changed: 205 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -328,14 +328,211 @@ public String runTestMethod(String projectName, String className, String methodN
328328
}
329329

330330
/**
331-
* Detects the appropriate JUnit test kind loader based on the project's
332-
* classpath. Checks resolved classpath entries to distinguish JUnit 5 from 6.
331+
* Detects the appropriate JUnit test kind loader. When a specific test class
332+
* is provided, inspects its annotations and superclass to determine the exact
333+
* JUnit version used — this avoids misdetection in PDE/mixed-classpath projects
334+
* where multiple JUnit versions are resolvable. Falls back to project-level
335+
* classpath analysis when no test class is given.
333336
*/
334-
private String detectJUnitTestKind(IJavaProject javaProject) throws JavaModelException {
335-
// JUnit 5 / 6 (Jupiter) - both share org.junit.jupiter.api.Test
337+
private String detectJUnitTestKind(IJavaProject javaProject, IType testClass, IPackageFragment packageFragment) throws JavaModelException {
338+
if (testClass != null) {
339+
String detected = detectJUnitTestKindFromClass(testClass);
340+
if (detected != null) {
341+
detected = refineAndValidate(detected, javaProject);
342+
if (detected != null) {
343+
return detected;
344+
}
345+
}
346+
}
347+
if (packageFragment != null) {
348+
String detected = detectJUnitTestKindFromPackage(packageFragment);
349+
if (detected != null) {
350+
detected = refineAndValidate(detected, javaProject);
351+
if (detected != null) {
352+
return detected;
353+
}
354+
}
355+
}
356+
return detectJUnitTestKindFromProject(javaProject);
357+
}
358+
359+
private String refineAndValidate(String testKind, IJavaProject javaProject) throws JavaModelException {
360+
if (testKind.equals("org.eclipse.jdt.junit.loader.junit5")) {
361+
if (!isJUnit5Runnable(javaProject)) {
362+
return null;
363+
}
364+
String refined = detectJupiterVersion(javaProject);
365+
return refined != null ? refined : testKind;
366+
}
367+
return testKind;
368+
}
369+
370+
private boolean isJUnit5Runnable(IJavaProject javaProject) throws JavaModelException {
371+
return javaProject.findType("org.junit.platform.commons.annotation.Testable") != null;
372+
}
373+
374+
private String detectJUnitTestKindFromPackage(IPackageFragment packageFragment) throws JavaModelException {
375+
int junit4Count = 0;
376+
int junit5Count = 0;
377+
int junit3Count = 0;
378+
379+
for (ICompilationUnit cu : packageFragment.getCompilationUnits()) {
380+
for (IType type : cu.getTypes()) {
381+
String kind = detectJUnitTestKindFromClass(type);
382+
if (kind != null) {
383+
switch (kind) {
384+
case "org.eclipse.jdt.junit.loader.junit3":
385+
junit3Count++;
386+
break;
387+
case "org.eclipse.jdt.junit.loader.junit4":
388+
junit4Count++;
389+
break;
390+
case "org.eclipse.jdt.junit.loader.junit5":
391+
junit5Count++;
392+
break;
393+
}
394+
}
395+
}
396+
}
397+
398+
if (junit5Count > 0 && junit4Count == 0 && junit3Count == 0) {
399+
return "org.eclipse.jdt.junit.loader.junit5";
400+
}
401+
if (junit4Count > 0 && junit5Count == 0 && junit3Count == 0) {
402+
return "org.eclipse.jdt.junit.loader.junit4";
403+
}
404+
if (junit3Count > 0 && junit4Count == 0 && junit5Count == 0) {
405+
return "org.eclipse.jdt.junit.loader.junit3";
406+
}
407+
if (junit4Count > 0 || junit3Count > 0) {
408+
return "org.eclipse.jdt.junit.loader.junit4";
409+
}
410+
return null;
411+
}
412+
413+
private String detectJupiterVersion(IJavaProject javaProject) throws JavaModelException {
414+
for (var entry : javaProject.getResolvedClasspath(true)) {
415+
String entryPath = entry.getPath().toString();
416+
if (entryPath.contains("junit-jupiter-api")) {
417+
if (entryPath.matches(".*junit-jupiter-api[_-]6\\..*")) {
418+
return "org.eclipse.jdt.junit.loader.junit6";
419+
}
420+
return "org.eclipse.jdt.junit.loader.junit5";
421+
}
422+
}
336423
IType jupiterTest = javaProject.findType("org.junit.jupiter.api.Test");
337424
if (jupiterTest != null) {
338-
// Check the resolved classpath for jupiter version to distinguish 5.x from 6.x
425+
String typePath = jupiterTest.getPath().toString();
426+
if (typePath.matches(".*junit-jupiter-api[_-]6\\..*")) {
427+
return "org.eclipse.jdt.junit.loader.junit6";
428+
}
429+
}
430+
return null;
431+
}
432+
433+
private String detectJUnitTestKindFromClass(IType testClass) throws JavaModelException {
434+
String[] imports = getImportsFromCompilationUnit(testClass);
435+
boolean importsJUnit5 = false;
436+
boolean importsJUnit4 = false;
437+
for (String imp : imports) {
438+
if (imp.startsWith("org.junit.jupiter.")) {
439+
importsJUnit5 = true;
440+
} else if (imp.equals("org.junit.Test") || imp.equals("org.junit.runner.RunWith")
441+
|| (imp.startsWith("org.junit.") && !imp.startsWith("org.junit.jupiter."))) {
442+
importsJUnit4 = true;
443+
}
444+
}
445+
446+
boolean hasJUnit4Indicator = false;
447+
boolean hasJUnit5Indicator = false;
448+
449+
for (IMethod method : testClass.getMethods()) {
450+
for (IAnnotation annotation : method.getAnnotations()) {
451+
String name = annotation.getElementName();
452+
if (name.equals("org.junit.Test")) {
453+
hasJUnit4Indicator = true;
454+
} else if (name.equals("org.junit.jupiter.api.Test")
455+
|| name.equals("org.junit.jupiter.params.ParameterizedTest")) {
456+
hasJUnit5Indicator = true;
457+
} else if (name.equals("Test")) {
458+
if (importsJUnit5) {
459+
hasJUnit5Indicator = true;
460+
} else if (importsJUnit4) {
461+
hasJUnit4Indicator = true;
462+
}
463+
} else if (name.equals("ParameterizedTest")) {
464+
if (importsJUnit5) {
465+
hasJUnit5Indicator = true;
466+
}
467+
}
468+
}
469+
}
470+
471+
for (IAnnotation annotation : testClass.getAnnotations()) {
472+
String name = annotation.getElementName();
473+
if (name.equals("RunWith") || name.equals("org.junit.runner.RunWith")) {
474+
hasJUnit4Indicator = true;
475+
}
476+
if (name.equals("ExtendWith") || name.equals("org.junit.jupiter.api.extension.ExtendWith")) {
477+
hasJUnit5Indicator = true;
478+
}
479+
}
480+
481+
if (hasJUnit5Indicator) {
482+
return "org.eclipse.jdt.junit.loader.junit5";
483+
}
484+
if (hasJUnit4Indicator) {
485+
return "org.eclipse.jdt.junit.loader.junit4";
486+
}
487+
488+
IType superType = findSuperType(testClass);
489+
if (superType != null && "junit.framework.TestCase".equals(superType.getFullyQualifiedName())) {
490+
return "org.eclipse.jdt.junit.loader.junit3";
491+
}
492+
493+
if (importsJUnit5) {
494+
return "org.eclipse.jdt.junit.loader.junit5";
495+
}
496+
if (importsJUnit4) {
497+
return "org.eclipse.jdt.junit.loader.junit4";
498+
}
499+
500+
return null;
501+
}
502+
503+
private IType findSuperType(IType type) throws JavaModelException {
504+
String superName = type.getSuperclassName();
505+
if (superName == null) {
506+
return null;
507+
}
508+
String[][] resolved = type.resolveType(superName);
509+
if (resolved != null && resolved.length > 0) {
510+
String fqn = resolved[0][0].isEmpty() ? resolved[0][1] : resolved[0][0] + "." + resolved[0][1];
511+
return type.getJavaProject().findType(fqn);
512+
}
513+
return null;
514+
}
515+
516+
private String[] getImportsFromCompilationUnit(IType type) {
517+
ICompilationUnit cu = type.getCompilationUnit();
518+
if (cu == null) {
519+
return new String[0];
520+
}
521+
try {
522+
var imports = cu.getImports();
523+
String[] result = new String[imports.length];
524+
for (int i = 0; i < imports.length; i++) {
525+
result[i] = imports[i].getElementName();
526+
}
527+
return result;
528+
} catch (JavaModelException e) {
529+
return new String[0];
530+
}
531+
}
532+
533+
private String detectJUnitTestKindFromProject(IJavaProject javaProject) throws JavaModelException {
534+
IType jupiterTest = javaProject.findType("org.junit.jupiter.api.Test");
535+
if (jupiterTest != null && isJUnit5Runnable(javaProject)) {
339536
for (var entry : javaProject.getResolvedClasspath(true)) {
340537
String entryPath = entry.getPath().toString();
341538
if (entryPath.contains("junit-jupiter-api")) {
@@ -345,23 +542,19 @@ private String detectJUnitTestKind(IJavaProject javaProject) throws JavaModelExc
345542
break;
346543
}
347544
}
348-
// Also check the type's own path (covers non-OSGi setups)
349545
String typePath = jupiterTest.getPath().toString();
350546
if (typePath.matches(".*junit-jupiter-api[_-]6\\..*")) {
351547
return "org.eclipse.jdt.junit.loader.junit6";
352548
}
353549
return "org.eclipse.jdt.junit.loader.junit5";
354550
}
355-
// JUnit 4
356551
if (javaProject.findType("org.junit.Test") != null) {
357552
return "org.eclipse.jdt.junit.loader.junit4";
358553
}
359-
// JUnit 3
360554
if (javaProject.findType("junit.framework.TestCase") != null) {
361555
return "org.eclipse.jdt.junit.loader.junit3";
362556
}
363-
// Default fallback
364-
return "org.eclipse.jdt.junit.loader.junit5";
557+
return "org.eclipse.jdt.junit.loader.junit4";
365558
}
366559

367560
private String buildLaunchName(IJavaProject javaProject, IPackageFragment packageFragment,
@@ -544,8 +737,8 @@ public void testCaseFinished(ITestCaseElement testCaseElement) {
544737
javaProject.getHandleIdentifier());
545738
}
546739

547-
// Detect the appropriate JUnit version from the project's classpath
548-
String testKind = detectJUnitTestKind(javaProject);
740+
// Detect the appropriate JUnit version — prefer class-level detection when available
741+
String testKind = detectJUnitTestKind(javaProject, testClass, packageFragment);
549742
workingCopy.setAttribute("org.eclipse.jdt.junit.TEST_KIND", testKind);
550743

551744
// Create the actual configuration

0 commit comments

Comments
 (0)