From 9f223f0e11d8ca0595b8a23c2e1fa0d25aba86a4 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 17 Aug 2026 14:10:51 +0800 Subject: [PATCH 1/2] fix: reuse Jupiter finder for JUnit 6 discovery JUnit 5 and JUnit 6 share the same Jupiter discovery semantics. Reuse the established finder so non-static @Nested classes receive the relaxed accessibility rules, while retaining the JUnit 6 kind for runtime selection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7a1a4d1-b63f-438b-a255-54976b0f5a0b --- .../projects/junit6-nested/.classpath | 12 + .../projects/junit6-nested/.project | 17 ++ .../src/test/java/example/NestedTests.java | 15 + .../searcher/JUnit6TestSearcherTest.java | 52 ++++ .../META-INF/MANIFEST.MF | 1 + .../plugin/searcher/JUnit6TestFinder.java | 287 ------------------ .../plugin/searcher/JUnit6TestSearcher.java | 43 +-- 7 files changed, 101 insertions(+), 326 deletions(-) create mode 100644 java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.classpath create mode 100644 java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.project create mode 100644 java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/src/test/java/example/NestedTests.java create mode 100644 java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java delete mode 100644 java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestFinder.java diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.classpath b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.classpath new file mode 100644 index 00000000..16709364 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.project b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.project new file mode 100644 index 00000000..45f36a83 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/.project @@ -0,0 +1,17 @@ + + + junit6-nested + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/src/test/java/example/NestedTests.java b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/src/test/java/example/NestedTests.java new file mode 100644 index 00000000..cafa12ea --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/junit6-nested/src/test/java/example/NestedTests.java @@ -0,0 +1,15 @@ +package example; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class NestedTests { + + @Nested + class MemberTests { + + @Test + void testNested() { + } + } +} diff --git a/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java new file mode 100644 index 00000000..c12c87b3 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2026 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.test.plugin.searcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.junit.Test; + +import com.microsoft.java.test.plugin.AbstractProjectsManagerBasedTest; +import com.microsoft.java.test.plugin.model.TestKind; + +public class JUnit6TestSearcherTest extends AbstractProjectsManagerBasedTest { + + @Test + public void testFindNonStaticNestedTestClass() throws Exception { + final List projects = importProjects("junit6-nested"); + final IJavaProject javaProject = JavaCore.create(projects.get(0)); + final IType outerType = javaProject.findType("example.NestedTests"); + assertNotNull(outerType); + + final IType nestedType = outerType.getType("MemberTests"); + assertTrue(nestedType.exists()); + + final JUnit6TestSearcher searcher = new JUnit6TestSearcher(); + assertTrue(searcher.isTestClass(nestedType)); + + final Set discoveredTypes = searcher.findTestItemsInContainer( + nestedType, new NullProgressMonitor()); + assertTrue(discoveredTypes.contains(nestedType)); + assertEquals(TestKind.JUnit6, searcher.getTestKind()); + assertEquals("org.eclipse.jdt.junit.loader.junit6", searcher.getJdtTestKind()); + } +} diff --git a/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF b/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF index f9f385b5..c496594d 100644 --- a/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF +++ b/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF @@ -53,6 +53,7 @@ Require-Bundle: org.eclipse.jdt.core, org.jacoco.core;bundle-version="[0.8.15,0.8.16)" Export-Package: com.microsoft.java.test.plugin.launchers;x-friends:="com.microsoft.java.test.plugin.test", com.microsoft.java.test.plugin.model;x-friends:="com.microsoft.java.test.plugin.test", + com.microsoft.java.test.plugin.searcher;x-friends:="com.microsoft.java.test.plugin.test", com.microsoft.java.test.plugin.coverage;x-friends:="com.microsoft.java.test.plugin.test", com.microsoft.java.test.plugin.coverage.model;x-friends:="com.microsoft.java.test.plugin.test" Bundle-ClassPath: . diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestFinder.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestFinder.java deleted file mode 100644 index 2580fa10..00000000 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestFinder.java +++ /dev/null @@ -1,287 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017-2025 Microsoft Corporation and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Microsoft Corporation - initial API and implementation - *******************************************************************************/ - -package com.microsoft.java.test.plugin.searcher; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.SubMonitor; -import org.eclipse.jdt.core.IClassFile; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IRegion; -import org.eclipse.jdt.core.ISourceRange; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.ITypeHierarchy; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.IAnnotationBinding; -import org.eclipse.jdt.core.dom.IBinding; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.Modifier; -import org.eclipse.jdt.core.dom.RecordDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclaration; -import org.eclipse.jdt.internal.junit.launcher.ITestFinder; -import org.eclipse.jdt.internal.junit.util.CoreTestSearchEngine; - -import java.util.HashSet; -import java.util.Set; - -/** - * Test finder for JUnit 6 (Jupiter API 6.x). - * - *

This class is similar to JUnit5TestFinder but uses the JUnit 6 loader - * to properly detect tests in JUnit 6 projects. - * - *

Why this class exists: Eclipse JDT does not yet have built-in support for JUnit 6. - * This is a custom implementation that will be needed until Eclipse JDT adds official JUnit 6 support. - * - *

Key differences from JUnit5TestFinder: - *

    - *
  • Uses custom loader ID "org.eclipse.jdt.junit.loader.junit6"
  • - *
  • Filters out abstract classes explicitly
  • - *
  • Only supports JUnit Jupiter annotations (not JUnit 4 vintage)
  • - *
- * - * @see org.eclipse.jdt.internal.junit.launcher.JUnit5TestFinder - */ -public class JUnit6TestFinder implements ITestFinder { - - /** - * Custom loader ID for JUnit 6 tests. - * This is used by Eclipse JDT's classpath resolution mechanism to identify - * JUnit 6 test runtime dependencies. - */ - private static final String JUNIT6_LOADER = "org.eclipse.jdt.junit.loader.junit6"; - - public JUnit6TestFinder() { - } - - @Override - public void findTestsInContainer(IJavaElement element, Set result, IProgressMonitor pm) - throws CoreException { - if (element == null || result == null) { - throw new IllegalArgumentException(); - } - - if (element instanceof IType) { - final IType type = (IType) element; - if (internalIsTest(type, pm)) { - result.add(type); - } - return; - } - - final SubMonitor subMonitor = SubMonitor.convert(pm, "Searching for JUnit 6 tests...", 4); - - final IRegion region = CoreTestSearchEngine.getRegion(element); - final ITypeHierarchy hierarchy = JavaCore.newTypeHierarchy(region, null, subMonitor.split(1)); - final IType[] allClasses = hierarchy.getAllClasses(); - - for (final IType type : allClasses) { - if (region.contains(type) && internalIsTest(type, pm)) { - addTypeAndSubtypes(type, result, hierarchy); - } - } - - // Also find JUnit 3 style tests that implement junit.framework.Test - final IType testInterface = element.getJavaProject().findType("junit.framework.Test"); - if (testInterface != null) { - CoreTestSearchEngine.findTestImplementorClasses(hierarchy, testInterface, region, result); - } - - CoreTestSearchEngine.findSuiteMethods(element, result, subMonitor.split(1)); - } - - private void addTypeAndSubtypes(IType type, Set result, ITypeHierarchy hierarchy) { - if (result.add(type)) { - final IType[] subclasses = hierarchy.getSubclasses(type); - for (final IType subclass : subclasses) { - addTypeAndSubtypes(subclass, result, hierarchy); - } - } - } - - @Override - public boolean isTest(IType type) throws JavaModelException { - return internalIsTest(type, null); - } - - private boolean internalIsTest(IType type, IProgressMonitor pm) throws JavaModelException { - // Use JUnit 6 loader to check if the class is accessible - if (!CoreTestSearchEngine.isAccessibleClass(type, JUNIT6_LOADER)) { - return false; - } - - if (CoreTestSearchEngine.hasSuiteMethod(type)) { - return true; - } - - final ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); - - if (type.getCompilationUnit() != null) { - parser.setSource(type.getCompilationUnit()); - } else if (!isAvailable(type.getSourceRange())) { - parser.setProject(type.getJavaProject()); - final IBinding[] bindings = parser.createBindings(new IJavaElement[] { type }, pm); - if (bindings.length == 1 && bindings[0] instanceof ITypeBinding) { - final ITypeBinding typeBinding = (ITypeBinding) bindings[0]; - return isTest(typeBinding); - } - return false; - } else { - final IClassFile classFile = type.getClassFile(); - if (classFile != null) { - parser.setSource(classFile); - } else { - return false; - } - } - - parser.setFocalPosition(0); - parser.setResolveBindings(true); - - final CompilationUnit cu = (CompilationUnit) parser.createAST(pm); - final ASTNode node = cu.findDeclaringNode(type.getKey()); - - if (node instanceof TypeDeclaration || node instanceof RecordDeclaration) { - final AbstractTypeDeclaration typeDecl = (AbstractTypeDeclaration) node; - final ITypeBinding binding = typeDecl.resolveBinding(); - if (binding != null) { - return isTest(binding); - } - } - - return false; - } - - private static boolean isAvailable(ISourceRange range) { - return range != null && range.getOffset() != -1; - } - - private boolean isTest(ITypeBinding typeBinding) { - if (typeBinding == null || Modifier.isAbstract(typeBinding.getModifiers())) { - return false; - } - - // Check if the type itself has test methods - if (hasTestMethods(typeBinding)) { - return true; - } - - // Check nested classes with @Nested annotation - for (final ITypeBinding nestedType : typeBinding.getDeclaredTypes()) { - if (isNestedTestClass(nestedType)) { - return true; - } - } - - // Check superclass - final ITypeBinding superclass = typeBinding.getSuperclass(); - if (superclass != null && !superclass.getQualifiedName().equals("java.lang.Object")) { - if (isTest(superclass)) { - return true; - } - } - - return false; - } - - private boolean hasTestMethods(ITypeBinding typeBinding) { - for (final IMethodBinding method : typeBinding.getDeclaredMethods()) { - if (isTestMethod(method)) { - return true; - } - } - return false; - } - - private boolean isTestMethod(IMethodBinding method) { - for (final IAnnotationBinding annotation : method.getAnnotations()) { - if (annotation == null) { - continue; - } - final ITypeBinding annotationType = annotation.getAnnotationType(); - if (annotationType != null) { - // Check for @Testable meta-annotation - if (isTestableAnnotation(annotationType, new HashSet<>())) { - return true; - } - } - } - return false; - } - - /** - * Checks if an annotation or its meta-annotations is a JUnit testable annotation. - * This supports JUnit Platform's meta-annotation model where custom annotations - * can be annotated with @Testable to make them test annotations. - * - * @param annotationType the annotation type to check - * @param visited set of already visited annotations to prevent infinite recursion - * @return true if this is a testable annotation or has @Testable in its hierarchy - */ - private boolean isTestableAnnotation(ITypeBinding annotationType, Set visited) { - if (annotationType == null || !visited.add(annotationType)) { - return false; - } - - final String qualifiedName = annotationType.getQualifiedName(); - - // Direct check for @Testable meta-annotation - if ("org.junit.platform.commons.annotation.Testable".equals(qualifiedName)) { - return true; - } - - // Check for common JUnit Jupiter test annotations - if (qualifiedName.startsWith("org.junit.jupiter.api.")) { - if (qualifiedName.equals("org.junit.jupiter.api.Test") || - qualifiedName.equals("org.junit.jupiter.api.RepeatedTest") || - qualifiedName.equals("org.junit.jupiter.api.ParameterizedTest") || - qualifiedName.equals("org.junit.jupiter.api.TestFactory") || - qualifiedName.equals("org.junit.jupiter.api.TestTemplate")) { - return true; - } - } - - // Check meta-annotations - for (final IAnnotationBinding metaAnnotation : annotationType.getAnnotations()) { - if (metaAnnotation == null) { - continue; - } - final ITypeBinding metaAnnotationType = metaAnnotation.getAnnotationType(); - if (isTestableAnnotation(metaAnnotationType, visited)) { - return true; - } - } - - return false; - } - - private boolean isNestedTestClass(ITypeBinding nestedType) { - for (final IAnnotationBinding annotation : nestedType.getAnnotations()) { - if (annotation == null) { - continue; - } - final ITypeBinding annotationType = annotation.getAnnotationType(); - if (annotationType != null && - "org.junit.jupiter.api.Nested".equals(annotationType.getQualifiedName())) { - return isTest(nestedType); - } - } - return false; - } -} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java index d9e3fc2e..48aed200 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java @@ -13,37 +13,18 @@ import com.microsoft.java.test.plugin.model.TestKind; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - /** * Test searcher for JUnit 6 (Jupiter API 6.x). - * - *

JUnit 6 is an evolutionary release built on top of JUnit 5's Jupiter platform. - * It maintains full backward compatibility with JUnit 5 while adding improvements - * and new features. This class extends JUnit5TestSearcher to inherit all the - * Jupiter test detection logic, only overriding the parts specific to JUnit 6: - *

    - *
  • Test kind identification (JUnit6 vs JUnit5)
  • - *
  • Test finder instance (uses JUnit6TestFinder for proper classpath resolution)
  • - *
- * + * + *

JUnit 5 and JUnit 6 share the same Jupiter test discovery semantics. The + * JUnit version is distinguished when selecting the test kind and runtime. + * * @see JUnit5TestSearcher - * @see JUnit6TestFinder */ public class JUnit6TestSearcher extends JUnit5TestSearcher { - private static final JUnit6TestFinder JUNIT6_TEST_FINDER = new JUnit6TestFinder(); - @Override public TestKind getTestKind() { return TestKind.JUnit6; @@ -53,20 +34,4 @@ public TestKind getTestKind() { public String getJdtTestKind() { return TestKindRegistry.JUNIT6_TEST_KIND_ID; } - - @Override - public boolean isTestClass(IType type) throws JavaModelException { - return JUNIT6_TEST_FINDER.isTest(type); - } - - @Override - public Set findTestItemsInContainer(IJavaElement element, IProgressMonitor monitor) throws CoreException { - final Set types = new HashSet<>(); - try { - JUNIT6_TEST_FINDER.findTestsInContainer(element, types, monitor); - } catch (OperationCanceledException e) { - return Collections.emptySet(); - } - return types; - } } From e75aa36809f1f8902fe0cf0077d51f59c024b8bf Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 17 Aug 2026 14:45:36 +0800 Subject: [PATCH 2/2] test: use JDT constant for JUnit 6 kind Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7a1a4d1-b63f-438b-a255-54976b0f5a0b --- .../java/test/plugin/searcher/JUnit6TestSearcherTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java index c12c87b3..d8897299 100644 --- a/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java +++ b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcherTest.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry; import org.junit.Test; import com.microsoft.java.test.plugin.AbstractProjectsManagerBasedTest; @@ -47,6 +48,6 @@ public void testFindNonStaticNestedTestClass() throws Exception { nestedType, new NullProgressMonitor()); assertTrue(discoveredTypes.contains(nestedType)); assertEquals(TestKind.JUnit6, searcher.getTestKind()); - assertEquals("org.eclipse.jdt.junit.loader.junit6", searcher.getJdtTestKind()); + assertEquals(TestKindRegistry.JUNIT6_TEST_KIND_ID, searcher.getJdtTestKind()); } }