@@ -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