diff --git a/src/constants.ts b/src/constants.ts
index 2023c384..b23bf197 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -73,6 +73,7 @@ export namespace Context {
export namespace JUnitTestPart {
export const CLASS: string = 'class:';
export const NESTED_CLASS: string = 'nested-class:';
+ export const SUITE: string = 'suite:';
export const METHOD: string = 'method:';
export const TEST_FACTORY: string = 'test-factory:';
// Property id is for jqwik
diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts
index 4dc9f9c3..06f5719c 100644
--- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts
+++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts
@@ -191,7 +191,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
}
protected getTestId(message: string): string {
- if (message.includes('engine:junit5') || message.includes('engine:junit-jupiter') || message.includes('engine:jqwik')) {
+ if (message.includes('engine:junit5') || message.includes('engine:junit-jupiter') || message.includes('engine:jqwik') || message.includes('engine:junit-platform-suite')) {
return this.getTestIdForJunit5Method(message);
} else {
return this.getTestIdForNonJunit5Method(message);
@@ -218,6 +218,11 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
if (part.startsWith(JUnitTestPart.CLASS)) {
className = part.substring(JUnitTestPart.CLASS.length);
+ } else if (part.startsWith(JUnitTestPart.SUITE)) {
+ // Only use suite name as className if no class: part has been seen yet
+ if (!className) {
+ className = part.substring(JUnitTestPart.SUITE.length);
+ }
} else if (part.startsWith(JUnitTestPart.METHOD)) {
const rawMethodName: string = part.substring(JUnitTestPart.METHOD.length);
// If the method name exists then we want to include the '#' qualifier.
diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts
index eab24d78..cae956be 100644
--- a/test/suite/JUnitAnalyzer.test.ts
+++ b/test/suite/JUnitAnalyzer.test.ts
@@ -5,10 +5,11 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
-import { MarkdownString, Range, TestController, TestMessage, TestRunRequest, tests, workspace } from 'vscode';
+import { MarkdownString, Range, TestController, TestMessage, TestRunRequest, tests, Uri, workspace } from 'vscode';
import { JUnitRunnerResultAnalyzer } from '../../src/runners/junitRunner/JUnitRunnerResultAnalyzer';
import { generateTestItem } from './utils';
-import { TestKind, IRunTestContext } from '../../src/java-test-runner.api';
+import { TestKind, TestLevel, IRunTestContext } from '../../src/java-test-runner.api';
+import { dataCache } from '../../src/controller/testItemDataCache';
// tslint:disable: only-arrow-functions
// tslint:disable: no-object-literal-type-assertion
@@ -543,4 +544,73 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
sinon.assert.calledWith(passedSpy, dummy);
});
+ test("test JUnit 5 @Suite class passed result", () => {
+ // Create a class-level test item for the Suite class
+ const suiteItem = testController.createTestItem('junit@junit5.suite.MyTestSuite', 'MyTestSuite', Uri.file('/mock/test/MyTestSuite.java'));
+ suiteItem.range = new Range(0, 0, 5, 0);
+ dataCache.set(suiteItem, {
+ jdtHandler: '',
+ fullName: 'junit5.suite.MyTestSuite',
+ projectName: 'junit',
+ testLevel: TestLevel.Class,
+ testKind: TestKind.JUnit5,
+ });
+
+ // Pre-create child items under the suite so that triggeredTestsMapping
+ // can find them and the global createTestItem is never called.
+ const classItem = testController.createTestItem('junit@junit5.AppTest', 'AppTest', Uri.file('/mock/test/AppTest.java'));
+ classItem.range = new Range(0, 0, 10, 0);
+ dataCache.set(classItem, {
+ jdtHandler: '',
+ fullName: 'junit5.AppTest',
+ projectName: 'junit',
+ testLevel: TestLevel.Class,
+ testKind: TestKind.JUnit5,
+ });
+
+ const methodItem = generateTestItem(testController, 'junit@junit5.AppTest#testGetGreeting()', TestKind.JUnit5);
+ classItem.children.replace([methodItem]);
+ suiteItem.children.replace([classItem]);
+
+ const testRunRequest = new TestRunRequest([suiteItem], []);
+ const testRun = testController.createTestRun(testRunRequest);
+ const startedSpy = sinon.spy(testRun, 'started');
+ const passedSpy = sinon.spy(testRun, 'passed');
+
+ // This is the output format when running a @Suite class with junit-platform-suite engine.
+ // The suite container uses [engine:junit-platform-suite]/[suite:...] format.
+ // Child tests use [engine:junit-platform-suite]/[suite:...]/[engine:junit-jupiter]/[class:...]/[method:...].
+ // The protocol nests %TESTS/%TESTE for suite → class → method.
+ const testRunnerOutput = `%TESTC 2 v2
+%TSTTREE1,junit5.suite.MyTestSuite,true,1,false,-1,MyTestSuite,,[engine:junit-platform-suite]/[suite:junit5.suite.MyTestSuite]
+%TSTTREE2,junit5.AppTest,true,1,false,1,AppTest,,[engine:junit-platform-suite]/[suite:junit5.suite.MyTestSuite]/[engine:junit-jupiter]/[class:junit5.AppTest]
+%TSTTREE3,testGetGreeting(junit5.AppTest),false,1,false,2,testGetGreeting(),,[engine:junit-platform-suite]/[suite:junit5.suite.MyTestSuite]/[engine:junit-jupiter]/[class:junit5.AppTest]/[method:testGetGreeting()]
+%TESTS 1,junit5.suite.MyTestSuite
+%TESTS 2,junit5.AppTest
+%TESTS 3,testGetGreeting(junit5.AppTest)
+%TESTE 3,testGetGreeting(junit5.AppTest)
+%TESTE 2,junit5.AppTest
+%TESTE 1,junit5.suite.MyTestSuite
+%RUNTIME15`;
+
+ const runnerContext: IRunTestContext = {
+ isDebug: false,
+ kind: TestKind.JUnit5,
+ projectName: 'junit',
+ testItems: [suiteItem],
+ testRun: testRun,
+ workspaceFolder: workspace.workspaceFolders?.[0]!,
+ };
+
+ const analyzer = new JUnitRunnerResultAnalyzer(runnerContext);
+ analyzer.analyzeData(testRunnerOutput);
+
+ // Verify the suite item itself started and passed (the core regression in #1828)
+ sinon.assert.calledWith(startedSpy, suiteItem);
+ sinon.assert.calledWith(passedSpy, suiteItem, sinon.match.number);
+ // Verify the method-level child also started and passed
+ sinon.assert.calledWith(startedSpy, methodItem);
+ sinon.assert.calledWith(passedSpy, methodItem, sinon.match.number);
+ });
+
});
diff --git a/test/test-projects/junit/pom.xml b/test/test-projects/junit/pom.xml
index 19ab57cf..b7804d47 100644
--- a/test/test-projects/junit/pom.xml
+++ b/test/test-projects/junit/pom.xml
@@ -35,6 +35,12 @@
5.6.0
test
+
+ org.junit.platform
+ junit-platform-suite
+ 1.6.0
+ test
+
net.jqwik
jqwik
diff --git a/test/test-projects/junit/src/test/java/junit5/suite/MyTestSuite.java b/test/test-projects/junit/src/test/java/junit5/suite/MyTestSuite.java
new file mode 100644
index 00000000..de4a5692
--- /dev/null
+++ b/test/test-projects/junit/src/test/java/junit5/suite/MyTestSuite.java
@@ -0,0 +1,9 @@
+package junit5.suite;
+
+import org.junit.platform.suite.api.SelectPackages;
+import org.junit.platform.suite.api.Suite;
+
+@Suite
+@SelectPackages("junit5")
+public class MyTestSuite {
+}