From 07d8ca43fefcf9be29907dea0d1e71bacd29adf0 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 24 Nov 2025 16:03:57 +0800 Subject: [PATCH 1/6] feat: support junit 6 --- .../java/test/plugin/model/TestKind.java | 7 ++ .../plugin/provider/TestKindProvider.java | 70 ++++++++++++++++++- package-lock.json | 2 +- package.json | 1 + scripts/buildJdtlsExt.js | 1 + src/controller/testController.ts | 2 + src/java-test-runner.api.ts | 1 + .../junitRunner/JUnitRunnerResultAnalyzer.ts | 4 +- src/utils/launchUtils.ts | 2 +- 9 files changed, 85 insertions(+), 5 deletions(-) diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/model/TestKind.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/model/TestKind.java index 5bdd5087..5fc33a6f 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/model/TestKind.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/model/TestKind.java @@ -24,6 +24,9 @@ public enum TestKind { @SerializedName("2") TestNG(2), + @SerializedName("3") + JUnit6(3), + @SerializedName("100") None(100); @@ -37,6 +40,8 @@ public static TestKind fromString(String s) { return JUnit; case "JUnit 5": return JUnit5; + case "JUnit 6": + return JUnit6; case "TestNG": return TestNG; default: @@ -53,6 +58,8 @@ public String toString() { return "JUnit 4"; case JUnit5: return "JUnit 5"; + case JUnit6: + return "JUnit 6"; case TestNG: return "TestNG"; default: diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/provider/TestKindProvider.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/provider/TestKindProvider.java index af89d11f..96020ec8 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/provider/TestKindProvider.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/provider/TestKindProvider.java @@ -14,13 +14,20 @@ import com.microsoft.java.test.plugin.model.TestKind; import com.microsoft.java.test.plugin.util.JUnitPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.IVMInstall2; +import org.eclipse.jdt.launching.JavaRuntime; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TestKindProvider { private static Map> map = new HashMap<>(); @@ -45,7 +52,11 @@ private static List getTestKinds(IJavaProject javaProject) { final List result = new LinkedList<>(); try { if (javaProject.findType(JUNIT5_TEST) != null) { - result.add(TestKind.JUnit5); + if (isJUnit6(javaProject)) { + result.add(TestKind.JUnit6); + } else { + result.add(TestKind.JUnit5); + } } if (javaProject.findType(JUNIT4_TEST) != null) { @@ -60,4 +71,61 @@ private static List getTestKinds(IJavaProject javaProject) { } return result; } + + private static boolean isJUnit6(IJavaProject project) { + if (!isJava17OrHigher(project)) { + return false; + } + return isJUnitJupiterApiVersion6OrHigher(project); + } + + private static boolean isJava17OrHigher(IJavaProject project) { + try { + final IVMInstall vm = JavaRuntime.getVMInstall(project); + if (vm instanceof IVMInstall2) { + final String javaVersion = ((IVMInstall2) vm).getJavaVersion(); + if (javaVersion != null) { + return getMajorVersion(javaVersion) >= 17; + } + } + } catch (CoreException e) { + // ignore + } + return false; + } + + private static int getMajorVersion(String version) { + if (version.startsWith("1.")) { + return Integer.parseInt(version.substring(2, 3)); + } + final int dot = version.indexOf('.'); + if (dot != -1) { + return Integer.parseInt(version.substring(0, dot)); + } + try { + return Integer.parseInt(version); + } catch (NumberFormatException e) { + return 0; + } + } + + private static boolean isJUnitJupiterApiVersion6OrHigher(IJavaProject project) { + try { + for (final IClasspathEntry entry : project.getResolvedClasspath(true)) { + if (entry.getPath().lastSegment().contains("junit-jupiter-api")) { + final String fileName = entry.getPath().lastSegment(); + final Matcher m = Pattern.compile("junit-jupiter-api-(\\d+)\\.").matcher(fileName); + if (m.find()) { + final int major = Integer.parseInt(m.group(1)); + if (major >= 6) { + return true; + } + } + } + } + } catch (JavaModelException e) { + // ignore + } + return false; + } } diff --git a/package-lock.json b/package-lock.json index 2e065a15..30187568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "vscode-java-test", - "version": "0.43.1", + "version": "0.43.2", "dependencies": { "fs-extra": "^10.1.0", "get-port": "^4.2.0", diff --git a/package.json b/package.json index e10303a6..2987bade 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "./server/org.apiguardian.api_1.1.2.jar", "./server/org.eclipse.jdt.junit4.runtime_1.3.100.v20231214-1952.jar", "./server/org.eclipse.jdt.junit5.runtime_1.1.300.v20231214-1952.jar", + "./server/undefined", "./server/org.opentest4j_1.3.0.jar", "./server/org.jacoco.core_0.8.14.202510111229.jar", "./server/com.microsoft.java.test.plugin-0.43.1.jar" diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index 87c29f2d..54c78b8a 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -12,6 +12,7 @@ const serverDir = path.resolve('java-extension'); const bundleList = [ 'org.eclipse.jdt.junit4.runtime_', 'org.eclipse.jdt.junit5.runtime_', + 'org.eclipse.jdt.junit6.runtime_', 'junit-jupiter-api', 'junit-jupiter-engine', 'junit-jupiter-migrationsupport', diff --git a/src/controller/testController.ts b/src/controller/testController.ts index 4242f7ae..4b31d2fe 100644 --- a/src/controller/testController.ts +++ b/src/controller/testController.ts @@ -696,6 +696,7 @@ function getRunnerByContext(testContext: IRunTestContext): BaseRunner | undefine switch (testContext.kind) { case TestKind.JUnit: case TestKind.JUnit5: + case TestKind.JUnit6: return new JUnitRunner(testContext); case TestKind.TestNG: return new TestNGRunner(testContext); @@ -711,6 +712,7 @@ function trackTestFrameworkVersion(testKind: TestKind, classpaths: string[], mod artifactPattern = /junit-(\d+\.\d+\.\d+(-[a-zA-Z\d]+)?).jar/; break; case TestKind.JUnit5: + case TestKind.JUnit6: artifactPattern = /junit-jupiter-api-(\d+\.\d+\.\d+(-[a-zA-Z\d]+)?).jar/; break; case TestKind.TestNG: diff --git a/src/java-test-runner.api.ts b/src/java-test-runner.api.ts index df35283b..7688e5d6 100644 --- a/src/java-test-runner.api.ts +++ b/src/java-test-runner.api.ts @@ -220,6 +220,7 @@ export enum TestKind { JUnit5 = 0, JUnit = 1, TestNG = 2, + JUnit6 = 3, None = 100, } diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 5a376a2a..e93e6655 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -349,7 +349,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { const isDynamic: boolean = result[4] === 'true'; const parentIndex: string = result[5]; const displayName: string = result[6].replace(/\\,/g, ','); - const uniqueId: string | undefined = this.testContext.kind === TestKind.JUnit5 ? + const uniqueId: string | undefined = (this.testContext.kind === TestKind.JUnit5 || this.testContext.kind === TestKind.JUnit6) ? result[8]?.replace(/\\,/g, ',') : undefined; let testItem: TestItem | undefined; @@ -398,7 +398,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { } if (testItem) { - if (dataCache.get(testItem)?.testKind === TestKind.JUnit5 && + if ((dataCache.get(testItem)?.testKind === TestKind.JUnit5 || dataCache.get(testItem)?.testKind === TestKind.JUnit6) && this.getLabelWithoutCodicon(testItem.label) !== displayName) { testItem.description = displayName; } else { diff --git a/src/utils/launchUtils.ts b/src/utils/launchUtils.ts index 53500e20..433b8486 100644 --- a/src/utils/launchUtils.ts +++ b/src/utils/launchUtils.ts @@ -60,7 +60,7 @@ export async function resolveLaunchConfigurationForRunner(runner: BaseRunner, te ], args: [ ...launchArguments.programArguments, - ...(testContext.kind === TestKind.JUnit5 ? parseTags(config) : []) + ...(testContext.kind === TestKind.JUnit5 || testContext.kind === TestKind.JUnit6 ? parseTags(config) : []) ], }); } From 89a04b71551038d070772fd6237f23966a3fb119 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 24 Nov 2025 17:06:42 +0800 Subject: [PATCH 2/6] fix: update --- .../JUnitLaunchConfigurationDelegate.java | 3 +- .../plugin/launchers/JUnitLaunchUtils.java | 1 + .../plugin/searcher/JUnit6TestSearcher.java | 165 ++++++++++++++++++ .../test/plugin/util/TestFrameworkUtils.java | 4 + .../test/plugin/util/TestGenerationUtils.java | 1 + 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java index d0910410..ddbfb262 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java @@ -147,7 +147,8 @@ private void addTestItemArgs(List arguments) throws CoreException { arguments.add("-test"); final IMethod method = (IMethod) JavaCore.create(this.args.testNames[0]); String testName = method.getElementName(); - if (this.args.testKind == TestKind.JUnit5 && method.getParameters().length > 0) { + if ((this.args.testKind == TestKind.JUnit5 || this.args.testKind == TestKind.JUnit6) && + method.getParameters().length > 0) { final ICompilationUnit unit = method.getCompilationUnit(); if (unit == null) { throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR, diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java index b2aa18f1..5a958833 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java @@ -204,6 +204,7 @@ private static String getEclipseTestKind(TestKind testKind) { case JUnit: return JUNIT4_LOADER; case JUnit5: + case JUnit6: return JUNIT5_LOADER; case TestNG: return TESTNG_LOADER; 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 new file mode 100644 index 00000000..1bffc56a --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit6TestSearcher.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * 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 com.microsoft.java.test.plugin.model.TestKind; +import com.microsoft.java.test.plugin.util.TestFrameworkUtils; + +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.core.dom.IAnnotationBinding; +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.internal.junit.launcher.JUnit5TestFinder; +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). + * + * Key differences from JUnit 5: + * - Uses standard @Test annotation directly (org.junit.jupiter.api.Test) + * - Does not rely on @Testable meta-annotation + * - Still supports @Nested for nested test classes + */ +public class JUnit6TestSearcher extends BaseFrameworkSearcher { + + private static final JUnit5TestFinder JUNIT5_TEST_FINDER = new JUnit5TestFinder(); + + // Standard JUnit Jupiter annotations + public static final String JUPITER_TEST = "org.junit.jupiter.api.Test"; + public static final String JUPITER_PARAMETERIZED_TEST = "org.junit.jupiter.api.params.ParameterizedTest"; + public static final String JUPITER_REPEATED_TEST = "org.junit.jupiter.api.RepeatedTest"; + public static final String JUPITER_TEST_FACTORY = "org.junit.jupiter.api.TestFactory"; + public static final String JUPITER_TEST_TEMPLATE = "org.junit.jupiter.api.TestTemplate"; + public static final String JUPITER_NESTED = "org.junit.jupiter.api.Nested"; + + protected static final String DISPLAY_NAME_ANNOTATION_JUNIT6 = "org.junit.jupiter.api.DisplayName"; + + public JUnit6TestSearcher() { + super(); + // JUnit 6 uses standard test annotations, not @Testable + this.testMethodAnnotations = new String[] { + JUPITER_TEST, + JUPITER_PARAMETERIZED_TEST, + JUPITER_REPEATED_TEST, + JUPITER_TEST_FACTORY, + JUPITER_TEST_TEMPLATE + }; + } + + @Override + public TestKind getTestKind() { + return TestKind.JUnit6; + } + + @Override + public String getJdtTestKind() { + // JUnit 6 uses the same JDT test kind as JUnit 5 + return TestKindRegistry.JUNIT5_TEST_KIND_ID; + } + + @Override + public boolean isTestMethod(IMethodBinding methodBinding) { + final int modifiers = methodBinding.getModifiers(); + if (Modifier.isAbstract(modifiers) || Modifier.isStatic(modifiers) || Modifier.isPrivate(modifiers)) { + return false; + } + + if (methodBinding.isConstructor()) { + return false; + } + + // Check for standard JUnit test annotations directly + return this.findAnnotation(methodBinding.getAnnotations(), this.getTestMethodAnnotations()); + } + + @Override + public boolean findAnnotation(IAnnotationBinding[] annotations, String[] annotationNames) { + for (final IAnnotationBinding annotation : annotations) { + if (annotation == null) { + continue; + } + for (final String annotationName : annotationNames) { + if (matchesName(annotation.getAnnotationType(), annotationName)) { + return true; + } + + // Special handling for @Nested to check annotation hierarchy + if (JUPITER_NESTED.equals(annotationName)) { + final Set hierarchy = new HashSet<>(); + if (matchesNameInAnnotationHierarchy(annotation, annotationName, hierarchy)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean isTestClass(IType type) throws JavaModelException { + // Reuse JUnit 5 test finder as it works for JUnit 6 as well + return JUNIT5_TEST_FINDER.isTest(type); + } + + private boolean matchesName(ITypeBinding annotationType, String annotationName) { + return TestFrameworkUtils.isEquivalentAnnotationType(annotationType, annotationName); + } + + /** + * Recursively checks the annotation hierarchy to find matching annotations. + * This is useful for meta-annotations like @Nested. + */ + private boolean matchesNameInAnnotationHierarchy(IAnnotationBinding annotation, String annotationName, + Set hierarchy) { + final ITypeBinding type = annotation.getAnnotationType(); + if (type == null) { + return false; + } + + for (final IAnnotationBinding annotationBinding : type.getAnnotations()) { + if (annotationBinding == null) { + continue; + } + final ITypeBinding annotationType = annotationBinding.getAnnotationType(); + if (annotationType != null && hierarchy.add(annotationType)) { + if (matchesName(annotationType, annotationName) || + matchesNameInAnnotationHierarchy(annotationBinding, annotationName, hierarchy)) { + return true; + } + } + } + + return false; + } + + @Override + public Set findTestItemsInContainer(IJavaElement element, IProgressMonitor monitor) throws CoreException { + final Set types = new HashSet<>(); + try { + // Reuse JUnit 5 test finder as the test discovery mechanism is compatible + JUNIT5_TEST_FINDER.findTestsInContainer(element, types, monitor); + } catch (OperationCanceledException e) { + return Collections.emptySet(); + } + return types; + } +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestFrameworkUtils.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestFrameworkUtils.java index 00cc1223..b3e8677e 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestFrameworkUtils.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestFrameworkUtils.java @@ -14,6 +14,7 @@ import com.microsoft.java.test.plugin.model.TestKind; import com.microsoft.java.test.plugin.searcher.JUnit4TestSearcher; import com.microsoft.java.test.plugin.searcher.JUnit5TestSearcher; +import com.microsoft.java.test.plugin.searcher.JUnit6TestSearcher; import com.microsoft.java.test.plugin.searcher.TestFrameworkSearcher; import com.microsoft.java.test.plugin.searcher.TestNGTestSearcher; @@ -25,6 +26,7 @@ public class TestFrameworkUtils { public static final TestFrameworkSearcher JUNIT4_TEST_SEARCHER = new JUnit4TestSearcher(); public static final TestFrameworkSearcher JUNIT5_TEST_SEARCHER = new JUnit5TestSearcher(); + public static final TestFrameworkSearcher JUNIT6_TEST_SEARCHER = new JUnit6TestSearcher(); public static final TestFrameworkSearcher TESTNG_TEST_SEARCHER = new TestNGTestSearcher(); public static boolean isEquivalentAnnotationType(ITypeBinding annotationType, String annotationName) { @@ -37,6 +39,8 @@ public static TestFrameworkSearcher getSearcherByTestKind(TestKind kind) { return JUNIT4_TEST_SEARCHER; case JUnit5: return JUNIT5_TEST_SEARCHER; + case JUnit6: + return JUNIT6_TEST_SEARCHER; case TestNG: return TESTNG_TEST_SEARCHER; default: diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestGenerationUtils.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestGenerationUtils.java index f75cab3d..c3509134 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestGenerationUtils.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestGenerationUtils.java @@ -764,6 +764,7 @@ private static List getLifecycleAnnotations(TestKind testKind) { list.add(JUNIT4_AFTER_CLASS_ANNOTATION); break; case JUnit5: + case JUnit6: list.add(JUNIT5_BEFORE_CLASS_ANNOTATION); list.add(JUNIT5_SET_UP_ANNOTATION); list.add(JUNIT5_TEAR_DOWN_ANNOTATION); From b10581c8f802dcde081d348ccbf4b448d0d2f316 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 25 Nov 2025 14:03:04 +0800 Subject: [PATCH 3/6] feat: update --- .../META-INF/MANIFEST.MF | 26 ++++++++--------- .../plugin/searcher/JUnit6TestSearcher.java | 2 +- .../com.microsoft.java.test.tp.target | 6 ++-- package.json | 28 +++++++++---------- scripts/buildJdtlsExt.js | 1 - 5 files changed, 30 insertions(+), 33 deletions(-) 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 14a3ed0d..94ac5eae 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 @@ -24,19 +24,19 @@ Require-Bundle: org.eclipse.jdt.core, org.eclipse.jdt.junit.runtime, org.eclipse.jdt.junit4.runtime, org.eclipse.jdt.junit5.runtime, - junit-jupiter-api;bundle-version="5.4.0", - junit-jupiter-engine;bundle-version="5.4.0", - junit-jupiter-migrationsupport;bundle-version="5.4.0", - junit-jupiter-params;bundle-version="5.4.0", - junit-vintage-engine;bundle-version="5.4.0", - org.opentest4j;bundle-version="1.1.1", - junit-platform-commons;bundle-version="1.4.0", - junit-platform-engine;bundle-version="1.4.0", - junit-platform-launcher;bundle-version="1.4.0", - junit-platform-runner;bundle-version="1.4.0", - junit-platform-suite-api;bundle-version="1.4.0", - junit-platform-suite-commons;bundle-version="1.8.1", - junit-platform-suite-engine;bundle-version="1.8.1", + junit-jupiter-api;bundle-version="[5.4.0,7.0.0)", + junit-jupiter-engine;bundle-version="[5.4.0,7.0.0)", + junit-jupiter-migrationsupport;bundle-version="[5.4.0,7.0.0)", + junit-jupiter-params;bundle-version="[5.4.0,7.0.0)", + junit-vintage-engine;bundle-version="[5.4.0,7.0.0)", + org.opentest4j;bundle-version="[1.1.1,2.0.0)", + junit-platform-commons;bundle-version="[1.4.0,2.0.0)", + junit-platform-engine;bundle-version="[1.4.0,2.0.0)", + junit-platform-launcher;bundle-version="[1.4.0,2.0.0)", + junit-platform-runner;bundle-version="[1.4.0,2.0.0)", + junit-platform-suite-api;bundle-version="[1.4.0,2.0.0)", + junit-platform-suite-commons;bundle-version="[1.4.0,2.0.0)";resolution:=optional, + junit-platform-suite-engine;bundle-version="[1.4.0,2.0.0)";resolution:=optional, org.apiguardian.api;bundle-version="1.0.0", org.apache.commons.lang3;bundle-version="3.1.0", com.google.gson;bundle-version="2.7.0", 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 1bffc56a..ae3f0c62 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 @@ -73,7 +73,7 @@ public TestKind getTestKind() { @Override public String getJdtTestKind() { // JUnit 6 uses the same JDT test kind as JUnit 5 - return TestKindRegistry.JUNIT5_TEST_KIND_ID; + return TestKindRegistry.JUNIT6_TEST_KIND_ID; } @Override diff --git a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target index 5d387936..aa39c2f6 100644 --- a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target +++ b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target @@ -4,17 +4,15 @@ - - - + - + diff --git a/package.json b/package.json index 2987bade..53ab0831 100644 --- a/package.json +++ b/package.json @@ -55,21 +55,21 @@ "main": "./main.js", "contributes": { "javaExtensions": [ - "./server/junit-jupiter-api_5.11.0.jar", - "./server/junit-jupiter-engine_5.11.0.jar", - "./server/junit-jupiter-migrationsupport_5.11.0.jar", - "./server/junit-jupiter-params_5.11.0.jar", - "./server/junit-platform-commons_1.11.0.jar", - "./server/junit-platform-engine_1.11.0.jar", - "./server/junit-platform-launcher_1.11.0.jar", - "./server/junit-platform-runner_1.11.0.jar", - "./server/junit-platform-suite-api_1.11.0.jar", - "./server/junit-platform-suite-commons_1.11.0.jar", - "./server/junit-platform-suite-engine_1.11.0.jar", - "./server/junit-vintage-engine_5.11.0.jar", + "./server/junit-jupiter-api_5.14.1.jar", + "./server/junit-jupiter-engine_5.14.1.jar", + "./server/junit-jupiter-migrationsupport_5.14.1.jar", + "./server/junit-jupiter-params_5.14.1.jar", + "./server/junit-platform-commons_1.14.1.jar", + "./server/junit-platform-engine_1.14.1.jar", + "./server/junit-platform-launcher_1.14.1.jar", + "./server/junit-platform-runner_1.14.1.jar", + "./server/junit-platform-suite-api_1.14.1.jar", + "./server/junit-platform-suite-commons_1.14.1.jar", + "./server/junit-platform-suite-engine_1.14.1.jar", + "./server/junit-vintage-engine_5.14.1.jar", "./server/org.apiguardian.api_1.1.2.jar", - "./server/org.eclipse.jdt.junit4.runtime_1.3.100.v20231214-1952.jar", - "./server/org.eclipse.jdt.junit5.runtime_1.1.300.v20231214-1952.jar", + "./server/org.eclipse.jdt.junit4.runtime_1.4.0.v20251113-1434.jar", + "./server/org.eclipse.jdt.junit5.runtime_1.2.0.v20251113-1434.jar", "./server/undefined", "./server/org.opentest4j_1.3.0.jar", "./server/org.jacoco.core_0.8.14.202510111229.jar", diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index 54c78b8a..87c29f2d 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -12,7 +12,6 @@ const serverDir = path.resolve('java-extension'); const bundleList = [ 'org.eclipse.jdt.junit4.runtime_', 'org.eclipse.jdt.junit5.runtime_', - 'org.eclipse.jdt.junit6.runtime_', 'junit-jupiter-api', 'junit-jupiter-engine', 'junit-jupiter-migrationsupport', From 0c895af3ea24737386e30fb279153780d4956518 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 25 Nov 2025 15:58:26 +0800 Subject: [PATCH 4/6] feat: update --- .../META-INF/MANIFEST.MF | 1 + .../plugin/launchers/JUnitLaunchUtils.java | 4 +- .../com.microsoft.java.test.tp.target | 1 + package.json | 18 +++--- scripts/buildJdtlsExt.js | 61 ++++++++++++++++++- 5 files changed, 72 insertions(+), 13 deletions(-) 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 94ac5eae..e66fb9d3 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 @@ -24,6 +24,7 @@ Require-Bundle: org.eclipse.jdt.core, org.eclipse.jdt.junit.runtime, org.eclipse.jdt.junit4.runtime, org.eclipse.jdt.junit5.runtime, + org.eclipse.jdt.junit6.runtime, junit-jupiter-api;bundle-version="[5.4.0,7.0.0)", junit-jupiter-engine;bundle-version="[5.4.0,7.0.0)", junit-jupiter-migrationsupport;bundle-version="[5.4.0,7.0.0)", diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java index 5a958833..a22a2b35 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchUtils.java @@ -48,6 +48,7 @@ public class JUnitLaunchUtils { private static final String TESTNG_LOADER = "com.microsoft.java.test.loader.testng"; private static final String JUNIT5_LOADER = "org.eclipse.jdt.junit.loader.junit5"; + private static final String JUNIT6_LOADER = "org.eclipse.jdt.junit.loader.junit6"; private static final String JUNIT4_LOADER = "org.eclipse.jdt.junit.loader.junit4"; private JUnitLaunchUtils() {} @@ -204,8 +205,9 @@ private static String getEclipseTestKind(TestKind testKind) { case JUnit: return JUNIT4_LOADER; case JUnit5: - case JUnit6: return JUNIT5_LOADER; + case JUnit6: + return JUNIT6_LOADER; case TestNG: return TESTNG_LOADER; default: diff --git a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target index aa39c2f6..c190d8bf 100644 --- a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target +++ b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target @@ -8,6 +8,7 @@ + diff --git a/package.json b/package.json index 53ab0831..a62c94a5 100644 --- a/package.json +++ b/package.json @@ -55,22 +55,22 @@ "main": "./main.js", "contributes": { "javaExtensions": [ - "./server/junit-jupiter-api_5.14.1.jar", - "./server/junit-jupiter-engine_5.14.1.jar", + "./server/junit-jupiter-api_6.0.1.jar", + "./server/junit-jupiter-engine_6.0.1.jar", "./server/junit-jupiter-migrationsupport_5.14.1.jar", - "./server/junit-jupiter-params_5.14.1.jar", - "./server/junit-platform-commons_1.14.1.jar", - "./server/junit-platform-engine_1.14.1.jar", - "./server/junit-platform-launcher_1.14.1.jar", + "./server/junit-jupiter-params_6.0.1.jar", + "./server/junit-platform-commons_6.0.1.jar", + "./server/junit-platform-engine_6.0.1.jar", + "./server/junit-platform-launcher_6.0.1.jar", "./server/junit-platform-runner_1.14.1.jar", - "./server/junit-platform-suite-api_1.14.1.jar", + "./server/junit-platform-suite-api_6.0.1.jar", "./server/junit-platform-suite-commons_1.14.1.jar", - "./server/junit-platform-suite-engine_1.14.1.jar", + "./server/junit-platform-suite-engine_6.0.1.jar", "./server/junit-vintage-engine_5.14.1.jar", "./server/org.apiguardian.api_1.1.2.jar", "./server/org.eclipse.jdt.junit4.runtime_1.4.0.v20251113-1434.jar", "./server/org.eclipse.jdt.junit5.runtime_1.2.0.v20251113-1434.jar", - "./server/undefined", + "./server/org.eclipse.jdt.junit6.runtime_1.0.0.v20251112-1701.jar", "./server/org.opentest4j_1.3.0.jar", "./server/org.jacoco.core_0.8.14.202510111229.jar", "./server/com.microsoft.java.test.plugin-0.43.1.jar" diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index 87c29f2d..4538832c 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -12,6 +12,10 @@ const serverDir = path.resolve('java-extension'); const bundleList = [ 'org.eclipse.jdt.junit4.runtime_', 'org.eclipse.jdt.junit5.runtime_', + 'org.eclipse.jdt.junit6.runtime_', + 'org.eclipse.jdt.junit.core_', + 'org.eclipse.jdt.junit.runtime_', + 'org.eclipse.core.resources_', 'junit-jupiter-api', 'junit-jupiter-engine', 'junit-jupiter-migrationsupport', @@ -45,16 +49,67 @@ function copy(sourceFolder, targetFolder, fileFilter) { } } +// Helper function to select the correct JAR version when multiple versions exist +function selectJarVersion(files, baseName) { + const matchingFiles = files.filter(file => file.startsWith(baseName + '_')); + if (matchingFiles.length === 0) { + return null; + } + + // For JUnit Platform and Jupiter, prefer version 6.x over 5.x + const junit6Pattern = new RegExp(`^${baseName}_(6\\.\\d+\\.\\d+)\\.jar$`); + const junit5Pattern = new RegExp(`^${baseName}_(5\\.\\d+\\.\\d+)\\.jar$`); + const platformPattern = new RegExp(`^${baseName}_(\\d+\\.\\d+\\.\\d+)\\.jar$`); + + // Check if this is a JUnit component that has both 5.x and 6.x versions + const isJunitComponent = baseName.startsWith('junit-jupiter') || + baseName.startsWith('junit-platform'); + + if (isJunitComponent) { + // Prefer 6.x version for JUnit components + const version6Files = matchingFiles.filter(file => junit6Pattern.test(file)); + if (version6Files.length > 0) { + // Sort by version and return the highest + return version6Files.sort().reverse()[0]; + } + } + + // For junit-vintage-engine and other packages, use 5.x + if (baseName === 'junit-vintage-engine') { + const version5Files = matchingFiles.filter(file => junit5Pattern.test(file)); + if (version5Files.length > 0) { + return version5Files.sort().reverse()[0]; + } + } + + // For platform-runner and suite-commons, prefer 1.x version + if (baseName === 'junit-platform-runner' || baseName === 'junit-platform-suite-commons') { + const platform1Files = matchingFiles.filter(file => platformPattern.test(file) && file.includes('_1.')); + if (platform1Files.length > 0) { + return platform1Files.sort().reverse()[0]; + } + } + + // Default: return the highest version + return matchingFiles.sort().reverse()[0]; +} + function updateVersion() { // Update the version const packageJsonData = require('../package.json'); const javaExtensions = packageJsonData.contributes.javaExtensions; + const destFolder = path.resolve('./server'); + const files = fs.readdirSync(destFolder); + if (Array.isArray(javaExtensions)) { - packageJsonData.contributes.javaExtensions = javaExtensions.map((extensionString) => { + packageJsonData.contributes.javaExtensions = javaExtensions.map((extensionString) => { const ind = extensionString.indexOf('_'); - const fileName = findNewRequiredJar(extensionString.substring(extensionString.lastIndexOf('/') + 1, ind)); if (ind >= 0) { - return extensionString.substring(0, extensionString.lastIndexOf('/') + 1) + fileName; + const baseName = extensionString.substring(extensionString.lastIndexOf('/') + 1, ind); + const fileName = selectJarVersion(files, baseName) || findNewRequiredJar(baseName); + if (fileName) { + return extensionString.substring(0, extensionString.lastIndexOf('/') + 1) + fileName; + } } return extensionString; }); From 051cdc1fd877dec287a8e48ee97c1282f7f0b445 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Mon, 1 Dec 2025 23:51:07 +0800 Subject: [PATCH 5/6] feat: support junit 6 test --- .../META-INF/MANIFEST.MF | 14 - .../JUnitLaunchConfigurationDelegate.java | 275 +++++++++++++++++- .../java/test/plugin/util/JUnitPlugin.java | 45 +++ package.json | 10 +- scripts/buildJdtlsExt.js | 111 +++++-- 5 files changed, 413 insertions(+), 42 deletions(-) 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 e66fb9d3..a873ff2a 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 @@ -25,20 +25,6 @@ Require-Bundle: org.eclipse.jdt.core, org.eclipse.jdt.junit4.runtime, org.eclipse.jdt.junit5.runtime, org.eclipse.jdt.junit6.runtime, - junit-jupiter-api;bundle-version="[5.4.0,7.0.0)", - junit-jupiter-engine;bundle-version="[5.4.0,7.0.0)", - junit-jupiter-migrationsupport;bundle-version="[5.4.0,7.0.0)", - junit-jupiter-params;bundle-version="[5.4.0,7.0.0)", - junit-vintage-engine;bundle-version="[5.4.0,7.0.0)", - org.opentest4j;bundle-version="[1.1.1,2.0.0)", - junit-platform-commons;bundle-version="[1.4.0,2.0.0)", - junit-platform-engine;bundle-version="[1.4.0,2.0.0)", - junit-platform-launcher;bundle-version="[1.4.0,2.0.0)", - junit-platform-runner;bundle-version="[1.4.0,2.0.0)", - junit-platform-suite-api;bundle-version="[1.4.0,2.0.0)", - junit-platform-suite-commons;bundle-version="[1.4.0,2.0.0)";resolution:=optional, - junit-platform-suite-engine;bundle-version="[1.4.0,2.0.0)";resolution:=optional, - org.apiguardian.api;bundle-version="1.0.0", org.apache.commons.lang3;bundle-version="3.1.0", com.google.gson;bundle-version="2.7.0", org.objectweb.asm;bundle-version="[9.9.0,9.10.0)", diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java index ddbfb262..6c658d09 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java @@ -20,9 +20,11 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; @@ -37,6 +39,7 @@ import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.internal.corext.refactoring.structure.ASTNodeSearchUtil; import org.eclipse.jdt.launching.VMRunnerConfiguration; +import org.osgi.framework.Bundle; import java.io.BufferedWriter; import java.io.File; @@ -45,13 +48,17 @@ import java.io.OutputStreamWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class JUnitLaunchConfigurationDelegate extends org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate { @@ -82,8 +89,24 @@ public Response getJUnitLaunchArguments(ILaunchConfigurati launchArguments.workingDirectory = config.getWorkingDirectory(); launchArguments.mainClass = config.getClassToLaunch(); launchArguments.projectName = javaProject.getProject().getName(); - launchArguments.classpath = config.getClassPath(); - launchArguments.modulepath = config.getModulepath(); + + // Debug: Log original classpath + JUnitPlugin.logInfo("[JUnit Launch] TestKind: " + this.args.testKind); + JUnitPlugin.logInfo("[JUnit Launch] Original classpath entries: " + + (config.getClassPath() != null ? config.getClassPath().length : 0)); + if (config.getClassPath() != null) { + for (final String cp : config.getClassPath()) { + JUnitPlugin.logInfo("[JUnit Launch] Classpath: " + cp); + } + } + + launchArguments.classpath = filterClasspathByTestKind(config.getClassPath(), this.args.testKind, true); + launchArguments.modulepath = filterClasspathByTestKind(config.getModulepath(), this.args.testKind, false); + + // Debug: Log filtered classpath + JUnitPlugin.logInfo("[JUnit Launch] Filtered classpath entries: " + + (launchArguments.classpath != null ? launchArguments.classpath.length : 0)); + launchArguments.vmArguments = getVmArguments(config); launchArguments.programArguments = parseParameters(config.getProgramArguments()); @@ -207,4 +230,252 @@ private String createTestNamesFile(String[] testNames) throws CoreException { IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR, "", e)); //$NON-NLS-1$ } } + + /** + * Filter the classpath/modulepath based on the test kind to avoid version conflicts. + * For JUnit 5: use junit-platform-* version 1.x and junit-jupiter-* version 5.x + * For JUnit 6: use junit-platform-* version 6.x and junit-jupiter-* version 6.x + * + * @param paths the original classpath or modulepath array + * @param testKind the test framework kind + * @return filtered paths array + */ + private String[] filterClasspathByTestKind(String[] paths, TestKind testKind) { + return filterClasspathByTestKind(paths, testKind, false); + } + + /** + * Filter the classpath/modulepath based on the test kind to avoid version conflicts. + * For JUnit 5: use junit-platform-* version 1.x and junit-jupiter-* version 5.x + * For JUnit 6: use junit-platform-* version 6.x and junit-jupiter-* version 6.x + * + * Also optionally injects the required JUnit bundles from OSGi into the classpath since + * Eclipse's ClasspathLocalizer only adds the junit*runtime bundle but not its Require-Bundle deps. + * + * @param paths the original classpath or modulepath array + * @param testKind the test framework kind + * @param injectBundles whether to inject missing JUnit bundles (should only be true for classpath) + * @return filtered paths array with JUnit bundles optionally injected + */ + private String[] filterClasspathByTestKind(String[] paths, TestKind testKind, boolean injectBundles) { + if (paths == null || testKind == null) { + return paths; + } + + // Pattern to match junit-platform-* and junit-jupiter-* jars with version numbers + // Supports both Maven format (junit-jupiter-api-6.0.0.jar) and + // OSGi bundle format (junit-jupiter-api_6.0.0.jar) + // Capture full version string for version matching + final Pattern junitVersionPattern = Pattern.compile( + "(junit-platform-[a-z-]+|junit-jupiter-[a-z-]+)[-_](\\d+)\\.(\\d+)\\.(\\d+)\\.jar$"); + + final List filteredPaths = new ArrayList<>(); + final Set foundJUnitArtifacts = new HashSet<>(); + int includedCount = 0; + int excludedCount = 0; + + // First pass: check if project has incomplete JUnit dependencies + // If so, we'll use all OSGi bundles to ensure version consistency + boolean needsFullBundleInjection = false; + if (injectBundles && (testKind == TestKind.JUnit5 || testKind == TestKind.JUnit6)) { + final Set projectArtifacts = new HashSet<>(); + for (final String path : paths) { + final String fileName = new File(path).getName(); + final Matcher m = junitVersionPattern.matcher(fileName); + if (m.find()) { + projectArtifacts.add(m.group(1)); + } + } + // Check if essential runtime bundles are missing + final boolean hasApi = projectArtifacts.contains("junit-jupiter-api"); + final boolean hasEngine = projectArtifacts.contains("junit-jupiter-engine"); + final boolean hasLauncher = projectArtifacts.contains("junit-platform-launcher"); + needsFullBundleInjection = hasApi && (!hasEngine || !hasLauncher); + + if (needsFullBundleInjection) { + JUnitPlugin.logInfo("[Classpath Filter] Project has incomplete JUnit dependencies, " + + "will use OSGi bundles for all JUnit components to ensure version consistency"); + } + } + + for (final String path : paths) { + final String fileName = new File(path).getName(); + final Matcher matcher = junitVersionPattern.matcher(fileName); + + if (matcher.find()) { + final String artifactName = matcher.group(1); + final int majorVersion = Integer.parseInt(matcher.group(2)); + + // If we need full bundle injection, exclude ALL project JUnit jars + if (needsFullBundleInjection) { + excludedCount++; + JUnitPlugin.logInfo("[Classpath Filter] Excluding project jar " + + "(will use OSGi bundle): " + fileName); + continue; + } + + if (testKind == TestKind.JUnit5) { + // For JUnit 5: only include version 1.x for platform, 5.x for jupiter + if (majorVersion < 6) { + filteredPaths.add(path); + foundJUnitArtifacts.add(artifactName); + includedCount++; + } else { + excludedCount++; + JUnitPlugin.logInfo("[Classpath Filter] JUnit5 - Excluding: " + fileName); + } + } else if (testKind == TestKind.JUnit6) { + // For JUnit 6: only include version 6.x for both platform and jupiter + if (majorVersion >= 6) { + filteredPaths.add(path); + foundJUnitArtifacts.add(artifactName); + includedCount++; + } else { + excludedCount++; + JUnitPlugin.logInfo("[Classpath Filter] JUnit6 - Excluding: " + fileName); + } + } else { + // For other test kinds, include all + filteredPaths.add(path); + foundJUnitArtifacts.add(artifactName); + includedCount++; + } + } else { + // Not a junit-platform/jupiter jar, include it + filteredPaths.add(path); + } + } + + // Inject JUnit bundles from OSGi if needed + // If needsFullBundleInjection, foundJUnitArtifacts is empty so all bundles will be injected + if (injectBundles && (testKind == TestKind.JUnit5 || testKind == TestKind.JUnit6)) { + final List injectedPaths = injectMissingJUnitBundles(testKind, foundJUnitArtifacts); + if (!injectedPaths.isEmpty()) { + filteredPaths.addAll(injectedPaths); + JUnitPlugin.logInfo("[Classpath Inject] Injected " + injectedPaths.size() + + " JUnit bundles for " + testKind); + } + } + + JUnitPlugin.logInfo("[Classpath Filter] TestKind=" + testKind + ", Included=" + + includedCount + " JUnit jars, Excluded=" + excludedCount + + " JUnit jars, Found artifacts=" + foundJUnitArtifacts + + ", Total paths=" + filteredPaths.size()); + + return filteredPaths.toArray(new String[0]); + } + + /** + * Inject missing JUnit bundles from OSGi runtime into the classpath. + * User's project typically only declares junit-jupiter-api as dependency, + * but test execution needs engine, launcher, and other runtime bundles. + * + * @param testKind the test framework kind + * @param foundArtifacts set of JUnit artifact names already in classpath + * @return list of bundle paths to add to classpath + */ + private List injectMissingJUnitBundles(TestKind testKind, Set foundArtifacts) { + final List bundlePaths = new ArrayList<>(); + + // Define required bundles based on test kind + final String[][] bundleSpecs; + if (testKind == TestKind.JUnit5) { + bundleSpecs = new String[][] { + {"junit-jupiter-api", "[5.0.0,6.0.0)"}, + {"junit-jupiter-engine", "[5.0.0,6.0.0)"}, + {"junit-jupiter-params", "[5.0.0,6.0.0)"}, + {"junit-platform-commons", "[1.0.0,2.0.0)"}, + {"junit-platform-engine", "[1.0.0,2.0.0)"}, + {"junit-platform-launcher", "[1.0.0,2.0.0)"}, + {"junit-platform-suite-api", "[1.0.0,2.0.0)"}, + {"junit-platform-suite-engine", "[1.0.0,2.0.0)"} + }; + } else if (testKind == TestKind.JUnit6) { + bundleSpecs = new String[][] { + {"junit-jupiter-api", "[6.0.0,7.0.0)"}, + {"junit-jupiter-engine", "[6.0.0,7.0.0)"}, + {"junit-jupiter-params", "[6.0.0,7.0.0)"}, + {"junit-platform-commons", "[6.0.0,7.0.0)"}, + {"junit-platform-engine", "[6.0.0,7.0.0)"}, + {"junit-platform-launcher", "[6.0.0,7.0.0)"}, + {"junit-platform-suite-api", "[6.0.0,7.0.0)"}, + {"junit-platform-suite-engine", "[6.0.0,7.0.0)"} + }; + } else { + return bundlePaths; + } + + // Common dependencies + final String[][] commonBundles = { + {"org.opentest4j", "[1.0.0,3.0.0)"}, + {"org.apiguardian.api", "[1.0.0,2.0.0)"} + }; + + // Inject bundles that are not already in classpath + for (final String[] spec : bundleSpecs) { + final String bundleName = spec[0]; + final String versionRange = spec[1]; + + if (!foundArtifacts.contains(bundleName)) { + final String bundlePath = getBundlePath(bundleName, versionRange); + if (bundlePath != null) { + bundlePaths.add(bundlePath); + JUnitPlugin.logInfo("[Classpath Inject] Added bundle: " + + bundleName + " -> " + bundlePath); + } else { + JUnitPlugin.logInfo("[Classpath Inject] Bundle not found: " + + bundleName + " " + versionRange); + } + } + } + + // Inject common dependencies if not present + for (final String[] spec : commonBundles) { + final String bundleName = spec[0]; + final String versionRange = spec[1]; + + final boolean found = foundArtifacts.stream() + .anyMatch(a -> a.contains("opentest4j") || a.contains("apiguardian")); + + if (!found) { + final String bundlePath = getBundlePath(bundleName, versionRange); + if (bundlePath != null) { + bundlePaths.add(bundlePath); + JUnitPlugin.logInfo("[Classpath Inject] Added common bundle: " + + bundleName + " -> " + bundlePath); + } + } + } + + return bundlePaths; + } + + /** + * Get the file path for an OSGi bundle by its symbolic name and version range. + * + * @param bundleName the bundle symbolic name + * @param versionRange the OSGi version range string + * @return the absolute file path to the bundle, or null if not found + */ + private String getBundlePath(String bundleName, String versionRange) { + final Bundle[] bundles = Platform.getBundles(bundleName, versionRange); + if (bundles != null && bundles.length > 0) { + final Bundle bundle = bundles[0]; + try { + final Optional bundleFile = FileLocator.getBundleFileLocation(bundle); + if (bundleFile.isPresent()) { + return bundleFile.get().getAbsolutePath(); + } + // Fallback: try to resolve via bundle entry + final URL bundleUrl = bundle.getEntry("/"); + if (bundleUrl != null) { + final URL fileUrl = FileLocator.toFileURL(bundleUrl); + return new File(fileUrl.getPath()).getAbsolutePath(); + } + } catch (IOException e) { + JUnitPlugin.logException("Failed to get bundle path for " + bundleName, e); + } + } + return null; + } } diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/JUnitPlugin.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/JUnitPlugin.java index 1f3231a2..3d72f867 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/JUnitPlugin.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/JUnitPlugin.java @@ -19,6 +19,7 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; @@ -53,6 +54,8 @@ public class JUnitPlugin implements BundleActivator { public void start(BundleContext context) throws Exception { handler.addElementChangeListener(); JUnitPlugin.context = context; + // Debug: Log JUnit bundle status on startup + logJUnitBundleStatus(); } /* @@ -114,4 +117,46 @@ public static Object askClientForInput(String prompt, String defaultValue) { return JavaLanguageServerPlugin.getInstance().getClientConnection() .executeClientCommand(JAVA_TEST_ASK_CLIENT_FOR_INPUT, prompt, defaultValue); } + + /** + * Debug method to log all installed OSGi bundles related to JUnit. + * Call this method to diagnose bundle resolution issues. + */ + public static void logJUnitBundleStatus() { + if (context == null) { + return; + } + + logInfo("=== OSGi Bundle Status for JUnit ==="); + final Bundle[] bundles = context.getBundles(); + + for (final Bundle bundle : bundles) { + final String symbolicName = bundle.getSymbolicName(); + if (symbolicName != null && ( + symbolicName.contains("junit") || + symbolicName.contains("jupiter") || + symbolicName.contains("opentest4j") || + symbolicName.contains("apiguardian"))) { + + final String stateStr = getBundleStateString(bundle.getState()); + logInfo(String.format("Bundle: %s, Version: %s, State: %s", + symbolicName, + bundle.getVersion().toString(), + stateStr)); + } + } + logInfo("=== End Bundle Status ==="); + } + + private static String getBundleStateString(int state) { + switch (state) { + case Bundle.UNINSTALLED: return "UNINSTALLED"; + case Bundle.INSTALLED: return "INSTALLED"; + case Bundle.RESOLVED: return "RESOLVED"; + case Bundle.STARTING: return "STARTING"; + case Bundle.STOPPING: return "STOPPING"; + case Bundle.ACTIVE: return "ACTIVE"; + default: return "UNKNOWN(" + state + ")"; + } + } } diff --git a/package.json b/package.json index a62c94a5..eef210cc 100644 --- a/package.json +++ b/package.json @@ -55,23 +55,31 @@ "main": "./main.js", "contributes": { "javaExtensions": [ + "./server/junit-jupiter-api_5.14.1.jar", "./server/junit-jupiter-api_6.0.1.jar", + "./server/junit-jupiter-engine_5.14.1.jar", "./server/junit-jupiter-engine_6.0.1.jar", "./server/junit-jupiter-migrationsupport_5.14.1.jar", + "./server/junit-jupiter-params_5.14.1.jar", "./server/junit-jupiter-params_6.0.1.jar", + "./server/junit-platform-commons_1.14.1.jar", "./server/junit-platform-commons_6.0.1.jar", + "./server/junit-platform-engine_1.14.1.jar", "./server/junit-platform-engine_6.0.1.jar", + "./server/junit-platform-launcher_1.14.1.jar", "./server/junit-platform-launcher_6.0.1.jar", "./server/junit-platform-runner_1.14.1.jar", + "./server/junit-platform-suite-api_1.14.1.jar", "./server/junit-platform-suite-api_6.0.1.jar", "./server/junit-platform-suite-commons_1.14.1.jar", + "./server/junit-platform-suite-engine_1.14.1.jar", "./server/junit-platform-suite-engine_6.0.1.jar", "./server/junit-vintage-engine_5.14.1.jar", "./server/org.apiguardian.api_1.1.2.jar", + "./server/org.opentest4j_1.3.0.jar", "./server/org.eclipse.jdt.junit4.runtime_1.4.0.v20251113-1434.jar", "./server/org.eclipse.jdt.junit5.runtime_1.2.0.v20251113-1434.jar", "./server/org.eclipse.jdt.junit6.runtime_1.0.0.v20251112-1701.jar", - "./server/org.opentest4j_1.3.0.jar", "./server/org.jacoco.core_0.8.14.202510111229.jar", "./server/com.microsoft.java.test.plugin-0.43.1.jar" ], diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index 4538832c..fe4d32b2 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -49,49 +49,86 @@ function copy(sourceFolder, targetFolder, fileFilter) { } } -// Helper function to select the correct JAR version when multiple versions exist -function selectJarVersion(files, baseName) { +// Helper function to select the JAR version(s) for javaExtensions (OSGi bundles) +// Returns both JUnit 5 (5.x/1.x) and JUnit 6 (6.x) versions for components that need both +// This is required because org.eclipse.jdt.junit5.runtime requires 5.x and +// org.eclipse.jdt.junit6.runtime requires 6.x +function selectJarVersions(files, baseName) { const matchingFiles = files.filter(file => file.startsWith(baseName + '_')); if (matchingFiles.length === 0) { - return null; + return []; } - // For JUnit Platform and Jupiter, prefer version 6.x over 5.x - const junit6Pattern = new RegExp(`^${baseName}_(6\\.\\d+\\.\\d+)\\.jar$`); + // Pattern to match version numbers const junit5Pattern = new RegExp(`^${baseName}_(5\\.\\d+\\.\\d+)\\.jar$`); - const platformPattern = new RegExp(`^${baseName}_(\\d+\\.\\d+\\.\\d+)\\.jar$`); + const junit6Pattern = new RegExp(`^${baseName}_(6\\.\\d+\\.\\d+)\\.jar$`); + const platform1Pattern = new RegExp(`^${baseName}_(1\\.\\d+\\.\\d+)\\.jar$`); - // Check if this is a JUnit component that has both 5.x and 6.x versions - const isJunitComponent = baseName.startsWith('junit-jupiter') || - baseName.startsWith('junit-platform'); + // Components that need both JUnit 5 and JUnit 6 versions + const needsBothVersions = [ + 'junit-jupiter-api', + 'junit-jupiter-engine', + 'junit-jupiter-params', + 'junit-platform-commons', + 'junit-platform-engine', + 'junit-platform-launcher', + 'junit-platform-suite-api', + 'junit-platform-suite-engine' + ]; - if (isJunitComponent) { - // Prefer 6.x version for JUnit components + if (needsBothVersions.includes(baseName)) { + const result = []; + // Add 5.x version if exists (for junit-jupiter-*) + const version5Files = matchingFiles.filter(file => junit5Pattern.test(file)); + if (version5Files.length > 0) { + result.push(version5Files.sort().reverse()[0]); + } + // Add 1.x version if exists (for junit-platform-*) + const version1Files = matchingFiles.filter(file => platform1Pattern.test(file)); + if (version1Files.length > 0) { + result.push(version1Files.sort().reverse()[0]); + } + // Add 6.x version if exists const version6Files = matchingFiles.filter(file => junit6Pattern.test(file)); if (version6Files.length > 0) { - // Sort by version and return the highest - return version6Files.sort().reverse()[0]; + result.push(version6Files.sort().reverse()[0]); } + // Remove duplicates and return + return [...new Set(result)]; } - // For junit-vintage-engine and other packages, use 5.x + // For junit-vintage-engine, only use 5.x if (baseName === 'junit-vintage-engine') { const version5Files = matchingFiles.filter(file => junit5Pattern.test(file)); if (version5Files.length > 0) { - return version5Files.sort().reverse()[0]; + return [version5Files.sort().reverse()[0]]; } } - // For platform-runner and suite-commons, prefer 1.x version + // For junit-jupiter-migrationsupport, only use 5.x (no 6.x version exists) + if (baseName === 'junit-jupiter-migrationsupport') { + const version5Files = matchingFiles.filter(file => junit5Pattern.test(file)); + if (version5Files.length > 0) { + return [version5Files.sort().reverse()[0]]; + } + } + + // For platform-runner and suite-commons, use 1.x version only if (baseName === 'junit-platform-runner' || baseName === 'junit-platform-suite-commons') { - const platform1Files = matchingFiles.filter(file => platformPattern.test(file) && file.includes('_1.')); + const platform1Files = matchingFiles.filter(file => platform1Pattern.test(file)); if (platform1Files.length > 0) { - return platform1Files.sort().reverse()[0]; + return [platform1Files.sort().reverse()[0]]; } } // Default: return the highest version - return matchingFiles.sort().reverse()[0]; + return [matchingFiles.sort().reverse()[0]]; +} + +// Legacy function for backward compatibility +function selectJarVersion(files, baseName) { + const versions = selectJarVersions(files, baseName); + return versions.length > 0 ? versions[0] : null; } function updateVersion() { @@ -102,17 +139,41 @@ function updateVersion() { const files = fs.readdirSync(destFolder); if (Array.isArray(javaExtensions)) { - packageJsonData.contributes.javaExtensions = javaExtensions.map((extensionString) => { + // Track which base names we've already processed to avoid duplicates + const processedBaseNames = new Set(); + const newJavaExtensions = []; + + for (const extensionString of javaExtensions) { const ind = extensionString.indexOf('_'); if (ind >= 0) { const baseName = extensionString.substring(extensionString.lastIndexOf('/') + 1, ind); - const fileName = selectJarVersion(files, baseName) || findNewRequiredJar(baseName); - if (fileName) { - return extensionString.substring(0, extensionString.lastIndexOf('/') + 1) + fileName; + + // Skip if we've already processed this base name + if (processedBaseNames.has(baseName)) { + continue; } + processedBaseNames.add(baseName); + + // Get all versions for this component + const fileNames = selectJarVersions(files, baseName); + if (fileNames.length > 0) { + for (const fileName of fileNames) { + newJavaExtensions.push('./server/' + fileName); + } + } else { + // Fallback to finding any matching jar + const fallbackFileName = findNewRequiredJar(baseName); + if (fallbackFileName) { + newJavaExtensions.push('./server/' + fallbackFileName); + } + } + } else { + // Non-versioned extension (like the plugin jar) + newJavaExtensions.push(extensionString); } - return extensionString; - }); + } + + packageJsonData.contributes.javaExtensions = newJavaExtensions; fs.writeFileSync(path.resolve('package.json'), JSON.stringify(packageJsonData, null, 4)); fs.appendFileSync(path.resolve('package.json'), os.EOL); From a52c3f84805844f2e7fef759d682da5a02393bb8 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 2 Dec 2025 10:15:43 +0800 Subject: [PATCH 6/6] feat: update code --- .../JUnitLaunchConfigurationDelegate.java | 297 +++++++++--------- 1 file changed, 146 insertions(+), 151 deletions(-) diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java index 6c658d09..2016ad38 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/launchers/JUnitLaunchConfigurationDelegate.java @@ -67,6 +67,30 @@ public class JUnitLaunchConfigurationDelegate extends org.eclipse.jdt.junit.laun private static final Set testNameArgs = new HashSet<>( Arrays.asList("-test", "-classNames", "-packageNameFile", "-testNameFile")); + // Pattern to match junit-platform-* and junit-jupiter-* jars with version numbers + // Supports both Maven format (junit-jupiter-api-6.0.0.jar) and + // OSGi bundle format (junit-jupiter-api_6.0.0.jar) + private static final Pattern JUNIT_VERSION_PATTERN = Pattern.compile( + "(junit-platform-[a-z-]+|junit-jupiter-[a-z-]+)[-_](\\d+)\\.(\\d+)\\.(\\d+)\\.jar$"); + + // JUnit bundle names that need to be injected for test execution + private static final String[] JUNIT_BUNDLE_NAMES = { + "junit-jupiter-api", + "junit-jupiter-engine", + "junit-jupiter-params", + "junit-platform-commons", + "junit-platform-engine", + "junit-platform-launcher", + "junit-platform-suite-api", + "junit-platform-suite-engine" + }; + + // Common dependency bundle names + private static final String[] COMMON_BUNDLE_NAMES = { + "org.opentest4j", + "org.apiguardian.api" + }; + public JUnitLaunchConfigurationDelegate(Argument args) { super(); this.args = args; @@ -232,24 +256,31 @@ private String createTestNamesFile(String[] testNames) throws CoreException { } /** - * Filter the classpath/modulepath based on the test kind to avoid version conflicts. - * For JUnit 5: use junit-platform-* version 1.x and junit-jupiter-* version 5.x - * For JUnit 6: use junit-platform-* version 6.x and junit-jupiter-* version 6.x + * Check if the given major version is compatible with the test kind. + * For JUnit 5: version < 6 (1.x for platform, 5.x for jupiter) + * For JUnit 6: version >= 6 * - * @param paths the original classpath or modulepath array + * @param majorVersion the major version number * @param testKind the test framework kind - * @return filtered paths array + * @return true if compatible, false otherwise */ - private String[] filterClasspathByTestKind(String[] paths, TestKind testKind) { - return filterClasspathByTestKind(paths, testKind, false); + private boolean isVersionCompatible(int majorVersion, TestKind testKind) { + switch (testKind) { + case JUnit5: + return majorVersion < 6; + case JUnit6: + return majorVersion >= 6; + default: + return true; + } } - + /** * Filter the classpath/modulepath based on the test kind to avoid version conflicts. * For JUnit 5: use junit-platform-* version 1.x and junit-jupiter-* version 5.x * For JUnit 6: use junit-platform-* version 6.x and junit-jupiter-* version 6.x * - * Also optionally injects the required JUnit bundles from OSGi into the classpath since + * Also optionally injects the required JUnit bundles from OSGi into the classpath since * Eclipse's ClasspathLocalizer only adds the junit*runtime bundle but not its Require-Bundle deps. * * @param paths the original classpath or modulepath array @@ -262,93 +293,70 @@ private String[] filterClasspathByTestKind(String[] paths, TestKind testKind, bo return paths; } - // Pattern to match junit-platform-* and junit-jupiter-* jars with version numbers - // Supports both Maven format (junit-jupiter-api-6.0.0.jar) and - // OSGi bundle format (junit-jupiter-api_6.0.0.jar) - // Capture full version string for version matching - final Pattern junitVersionPattern = Pattern.compile( - "(junit-platform-[a-z-]+|junit-jupiter-[a-z-]+)[-_](\\d+)\\.(\\d+)\\.(\\d+)\\.jar$"); + // For non-JUnit5/6 test kinds, no filtering needed + if (testKind != TestKind.JUnit5 && testKind != TestKind.JUnit6) { + return paths; + } final List filteredPaths = new ArrayList<>(); final Set foundJUnitArtifacts = new HashSet<>(); + // Track essential artifacts for incomplete dependency detection + boolean hasApi = false; + boolean hasEngine = false; + boolean hasLauncher = false; int includedCount = 0; int excludedCount = 0; - - // First pass: check if project has incomplete JUnit dependencies - // If so, we'll use all OSGi bundles to ensure version consistency - boolean needsFullBundleInjection = false; - if (injectBundles && (testKind == TestKind.JUnit5 || testKind == TestKind.JUnit6)) { - final Set projectArtifacts = new HashSet<>(); - for (final String path : paths) { - final String fileName = new File(path).getName(); - final Matcher m = junitVersionPattern.matcher(fileName); - if (m.find()) { - projectArtifacts.add(m.group(1)); - } - } - // Check if essential runtime bundles are missing - final boolean hasApi = projectArtifacts.contains("junit-jupiter-api"); - final boolean hasEngine = projectArtifacts.contains("junit-jupiter-engine"); - final boolean hasLauncher = projectArtifacts.contains("junit-platform-launcher"); - needsFullBundleInjection = hasApi && (!hasEngine || !hasLauncher); - - if (needsFullBundleInjection) { - JUnitPlugin.logInfo("[Classpath Filter] Project has incomplete JUnit dependencies, " + - "will use OSGi bundles for all JUnit components to ensure version consistency"); - } - } - + + // Single pass: filter paths and detect incomplete dependencies simultaneously for (final String path : paths) { final String fileName = new File(path).getName(); - final Matcher matcher = junitVersionPattern.matcher(fileName); - - if (matcher.find()) { - final String artifactName = matcher.group(1); - final int majorVersion = Integer.parseInt(matcher.group(2)); - - // If we need full bundle injection, exclude ALL project JUnit jars - if (needsFullBundleInjection) { - excludedCount++; - JUnitPlugin.logInfo("[Classpath Filter] Excluding project jar " + - "(will use OSGi bundle): " + fileName); - continue; - } - - if (testKind == TestKind.JUnit5) { - // For JUnit 5: only include version 1.x for platform, 5.x for jupiter - if (majorVersion < 6) { - filteredPaths.add(path); - foundJUnitArtifacts.add(artifactName); - includedCount++; - } else { - excludedCount++; - JUnitPlugin.logInfo("[Classpath Filter] JUnit5 - Excluding: " + fileName); - } - } else if (testKind == TestKind.JUnit6) { - // For JUnit 6: only include version 6.x for both platform and jupiter - if (majorVersion >= 6) { - filteredPaths.add(path); - foundJUnitArtifacts.add(artifactName); - includedCount++; - } else { - excludedCount++; - JUnitPlugin.logInfo("[Classpath Filter] JUnit6 - Excluding: " + fileName); - } - } else { - // For other test kinds, include all - filteredPaths.add(path); - foundJUnitArtifacts.add(artifactName); - includedCount++; + final Matcher matcher = JUNIT_VERSION_PATTERN.matcher(fileName); + + if (!matcher.find()) { + // Not a junit-platform/jupiter jar, include it directly + filteredPaths.add(path); + continue; + } + + final String artifactName = matcher.group(1); + final int majorVersion = Integer.parseInt(matcher.group(2)); + + // Track essential artifacts for bundle injection decision + if (injectBundles) { + if ("junit-jupiter-api".equals(artifactName)) { + hasApi = true; + } else if ("junit-jupiter-engine".equals(artifactName)) { + hasEngine = true; + } else if ("junit-platform-launcher".equals(artifactName)) { + hasLauncher = true; } - } else { - // Not a junit-platform/jupiter jar, include it + } + + if (isVersionCompatible(majorVersion, testKind)) { filteredPaths.add(path); + foundJUnitArtifacts.add(artifactName); + includedCount++; + } else { + excludedCount++; + JUnitPlugin.logInfo("[Classpath Filter] " + testKind + " - Excluding: " + fileName); } } - + + // Handle incomplete JUnit dependencies: if project has API but missing engine/launcher, + // remove all project JUnit jars and use OSGi bundles for version consistency + final boolean needsFullBundleInjection = injectBundles && hasApi && (!hasEngine || !hasLauncher); + if (needsFullBundleInjection) { + JUnitPlugin.logInfo("[Classpath Filter] Project has incomplete JUnit dependencies, " + + "will use OSGi bundles for all JUnit components to ensure version consistency"); + // Remove all JUnit jars that were added + filteredPaths.removeIf(p -> JUNIT_VERSION_PATTERN.matcher(new File(p).getName()).find()); + foundJUnitArtifacts.clear(); + excludedCount += includedCount; + includedCount = 0; + } + // Inject JUnit bundles from OSGi if needed - // If needsFullBundleInjection, foundJUnitArtifacts is empty so all bundles will be injected - if (injectBundles && (testKind == TestKind.JUnit5 || testKind == TestKind.JUnit6)) { + if (injectBundles) { final List injectedPaths = injectMissingJUnitBundles(testKind, foundJUnitArtifacts); if (!injectedPaths.isEmpty()) { filteredPaths.addAll(injectedPaths); @@ -356,7 +364,7 @@ private String[] filterClasspathByTestKind(String[] paths, TestKind testKind, bo " JUnit bundles for " + testKind); } } - + JUnitPlugin.logInfo("[Classpath Filter] TestKind=" + testKind + ", Included=" + includedCount + " JUnit jars, Excluded=" + excludedCount + " JUnit jars, Found artifacts=" + foundJUnitArtifacts + @@ -376,79 +384,66 @@ private String[] filterClasspathByTestKind(String[] paths, TestKind testKind, bo */ private List injectMissingJUnitBundles(TestKind testKind, Set foundArtifacts) { final List bundlePaths = new ArrayList<>(); - - // Define required bundles based on test kind - final String[][] bundleSpecs; - if (testKind == TestKind.JUnit5) { - bundleSpecs = new String[][] { - {"junit-jupiter-api", "[5.0.0,6.0.0)"}, - {"junit-jupiter-engine", "[5.0.0,6.0.0)"}, - {"junit-jupiter-params", "[5.0.0,6.0.0)"}, - {"junit-platform-commons", "[1.0.0,2.0.0)"}, - {"junit-platform-engine", "[1.0.0,2.0.0)"}, - {"junit-platform-launcher", "[1.0.0,2.0.0)"}, - {"junit-platform-suite-api", "[1.0.0,2.0.0)"}, - {"junit-platform-suite-engine", "[1.0.0,2.0.0)"} - }; - } else if (testKind == TestKind.JUnit6) { - bundleSpecs = new String[][] { - {"junit-jupiter-api", "[6.0.0,7.0.0)"}, - {"junit-jupiter-engine", "[6.0.0,7.0.0)"}, - {"junit-jupiter-params", "[6.0.0,7.0.0)"}, - {"junit-platform-commons", "[6.0.0,7.0.0)"}, - {"junit-platform-engine", "[6.0.0,7.0.0)"}, - {"junit-platform-launcher", "[6.0.0,7.0.0)"}, - {"junit-platform-suite-api", "[6.0.0,7.0.0)"}, - {"junit-platform-suite-engine", "[6.0.0,7.0.0)"} - }; - } else { - return bundlePaths; - } - - // Common dependencies - final String[][] commonBundles = { - {"org.opentest4j", "[1.0.0,3.0.0)"}, - {"org.apiguardian.api", "[1.0.0,2.0.0)"} - }; - - // Inject bundles that are not already in classpath - for (final String[] spec : bundleSpecs) { - final String bundleName = spec[0]; - final String versionRange = spec[1]; - + + // Inject JUnit bundles that are not already in classpath + for (final String bundleName : JUNIT_BUNDLE_NAMES) { if (!foundArtifacts.contains(bundleName)) { - final String bundlePath = getBundlePath(bundleName, versionRange); - if (bundlePath != null) { - bundlePaths.add(bundlePath); - JUnitPlugin.logInfo("[Classpath Inject] Added bundle: " + - bundleName + " -> " + bundlePath); - } else { - JUnitPlugin.logInfo("[Classpath Inject] Bundle not found: " + - bundleName + " " + versionRange); - } + injectBundle(bundleName, getVersionRange(bundleName, testKind), bundlePaths); } } - - // Inject common dependencies if not present - for (final String[] spec : commonBundles) { - final String bundleName = spec[0]; - final String versionRange = spec[1]; - - final boolean found = foundArtifacts.stream() - .anyMatch(a -> a.contains("opentest4j") || a.contains("apiguardian")); - - if (!found) { - final String bundlePath = getBundlePath(bundleName, versionRange); - if (bundlePath != null) { - bundlePaths.add(bundlePath); - JUnitPlugin.logInfo("[Classpath Inject] Added common bundle: " + - bundleName + " -> " + bundlePath); - } + + // Inject common dependencies if not present (check once, not per bundle) + final boolean hasCommonDeps = foundArtifacts.stream() + .anyMatch(a -> a.contains("opentest4j") || a.contains("apiguardian")); + + if (!hasCommonDeps) { + for (final String bundleName : COMMON_BUNDLE_NAMES) { + injectBundle(bundleName, getVersionRange(bundleName, testKind), bundlePaths); } } - + return bundlePaths; } + + /** + * Get the OSGi version range for a bundle based on test kind. + * + * @param bundleName the bundle symbolic name + * @param testKind the test framework kind + * @return the OSGi version range string + */ + private String getVersionRange(String bundleName, TestKind testKind) { + if (bundleName.startsWith("junit-jupiter")) { + return testKind == TestKind.JUnit5 ? "[5.0.0,6.0.0)" : "[6.0.0,7.0.0)"; + } else if (bundleName.startsWith("junit-platform")) { + return testKind == TestKind.JUnit5 ? "[1.0.0,2.0.0)" : "[6.0.0,7.0.0)"; + } else if ("org.opentest4j".equals(bundleName)) { + return "[1.0.0,3.0.0)"; + } else if ("org.apiguardian.api".equals(bundleName)) { + return "[1.0.0,2.0.0)"; + } + return null; + } + + /** + * Try to inject a bundle into the classpath. + * + * @param bundleName the bundle symbolic name + * @param versionRange the OSGi version range + * @param bundlePaths the list to add the bundle path to + */ + private void injectBundle(String bundleName, String versionRange, List bundlePaths) { + if (versionRange == null) { + return; + } + final String bundlePath = getBundlePath(bundleName, versionRange); + if (bundlePath != null) { + bundlePaths.add(bundlePath); + JUnitPlugin.logInfo("[Classpath Inject] Added bundle: " + bundleName + " -> " + bundlePath); + } else { + JUnitPlugin.logInfo("[Classpath Inject] Bundle not found: " + bundleName + " " + versionRange); + } + } /** * Get the file path for an OSGi bundle by its symbolic name and version range.