Skip to content

Commit b8d3098

Browse files
authored
fix: add @suite support on junit 5 (#1846)
* fix: support junit suite case support * test: add test case for this * test: update
1 parent 0f1cc47 commit b8d3098

5 files changed

Lines changed: 94 additions & 3 deletions

File tree

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export namespace Context {
7373
export namespace JUnitTestPart {
7474
export const CLASS: string = 'class:';
7575
export const NESTED_CLASS: string = 'nested-class:';
76+
export const SUITE: string = 'suite:';
7677
export const METHOD: string = 'method:';
7778
export const TEST_FACTORY: string = 'test-factory:';
7879
// Property id is for jqwik

src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
191191
}
192192

193193
protected getTestId(message: string): string {
194-
if (message.includes('engine:junit5') || message.includes('engine:junit-jupiter') || message.includes('engine:jqwik')) {
194+
if (message.includes('engine:junit5') || message.includes('engine:junit-jupiter') || message.includes('engine:jqwik') || message.includes('engine:junit-platform-suite')) {
195195
return this.getTestIdForJunit5Method(message);
196196
} else {
197197
return this.getTestIdForNonJunit5Method(message);
@@ -218,6 +218,11 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
218218

219219
if (part.startsWith(JUnitTestPart.CLASS)) {
220220
className = part.substring(JUnitTestPart.CLASS.length);
221+
} else if (part.startsWith(JUnitTestPart.SUITE)) {
222+
// Only use suite name as className if no class: part has been seen yet
223+
if (!className) {
224+
className = part.substring(JUnitTestPart.SUITE.length);
225+
}
221226
} else if (part.startsWith(JUnitTestPart.METHOD)) {
222227
const rawMethodName: string = part.substring(JUnitTestPart.METHOD.length);
223228
// If the method name exists then we want to include the '#' qualifier.

test/suite/JUnitAnalyzer.test.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
import * as assert from 'assert';
77
import * as sinon from 'sinon';
8-
import { MarkdownString, Range, TestController, TestMessage, TestRunRequest, tests, workspace } from 'vscode';
8+
import { MarkdownString, Range, TestController, TestMessage, TestRunRequest, tests, Uri, workspace } from 'vscode';
99
import { JUnitRunnerResultAnalyzer } from '../../src/runners/junitRunner/JUnitRunnerResultAnalyzer';
1010
import { generateTestItem } from './utils';
11-
import { TestKind, IRunTestContext } from '../../src/java-test-runner.api';
11+
import { TestKind, TestLevel, IRunTestContext } from '../../src/java-test-runner.api';
12+
import { dataCache } from '../../src/controller/testItemDataCache';
1213

1314
// tslint:disable: only-arrow-functions
1415
// tslint:disable: no-object-literal-type-assertion
@@ -543,4 +544,73 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
543544
sinon.assert.calledWith(passedSpy, dummy);
544545
});
545546

547+
test("test JUnit 5 @Suite class passed result", () => {
548+
// Create a class-level test item for the Suite class
549+
const suiteItem = testController.createTestItem('junit@junit5.suite.MyTestSuite', 'MyTestSuite', Uri.file('/mock/test/MyTestSuite.java'));
550+
suiteItem.range = new Range(0, 0, 5, 0);
551+
dataCache.set(suiteItem, {
552+
jdtHandler: '',
553+
fullName: 'junit5.suite.MyTestSuite',
554+
projectName: 'junit',
555+
testLevel: TestLevel.Class,
556+
testKind: TestKind.JUnit5,
557+
});
558+
559+
// Pre-create child items under the suite so that triggeredTestsMapping
560+
// can find them and the global createTestItem is never called.
561+
const classItem = testController.createTestItem('junit@junit5.AppTest', 'AppTest', Uri.file('/mock/test/AppTest.java'));
562+
classItem.range = new Range(0, 0, 10, 0);
563+
dataCache.set(classItem, {
564+
jdtHandler: '',
565+
fullName: 'junit5.AppTest',
566+
projectName: 'junit',
567+
testLevel: TestLevel.Class,
568+
testKind: TestKind.JUnit5,
569+
});
570+
571+
const methodItem = generateTestItem(testController, 'junit@junit5.AppTest#testGetGreeting()', TestKind.JUnit5);
572+
classItem.children.replace([methodItem]);
573+
suiteItem.children.replace([classItem]);
574+
575+
const testRunRequest = new TestRunRequest([suiteItem], []);
576+
const testRun = testController.createTestRun(testRunRequest);
577+
const startedSpy = sinon.spy(testRun, 'started');
578+
const passedSpy = sinon.spy(testRun, 'passed');
579+
580+
// This is the output format when running a @Suite class with junit-platform-suite engine.
581+
// The suite container uses [engine:junit-platform-suite]/[suite:...] format.
582+
// Child tests use [engine:junit-platform-suite]/[suite:...]/[engine:junit-jupiter]/[class:...]/[method:...].
583+
// The protocol nests %TESTS/%TESTE for suite → class → method.
584+
const testRunnerOutput = `%TESTC 2 v2
585+
%TSTTREE1,junit5.suite.MyTestSuite,true,1,false,-1,MyTestSuite,,[engine:junit-platform-suite]/[suite:junit5.suite.MyTestSuite]
586+
%TSTTREE2,junit5.AppTest,true,1,false,1,AppTest,,[engine:junit-platform-suite]/[suite:junit5.suite.MyTestSuite]/[engine:junit-jupiter]/[class:junit5.AppTest]
587+
%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()]
588+
%TESTS 1,junit5.suite.MyTestSuite
589+
%TESTS 2,junit5.AppTest
590+
%TESTS 3,testGetGreeting(junit5.AppTest)
591+
%TESTE 3,testGetGreeting(junit5.AppTest)
592+
%TESTE 2,junit5.AppTest
593+
%TESTE 1,junit5.suite.MyTestSuite
594+
%RUNTIME15`;
595+
596+
const runnerContext: IRunTestContext = {
597+
isDebug: false,
598+
kind: TestKind.JUnit5,
599+
projectName: 'junit',
600+
testItems: [suiteItem],
601+
testRun: testRun,
602+
workspaceFolder: workspace.workspaceFolders?.[0]!,
603+
};
604+
605+
const analyzer = new JUnitRunnerResultAnalyzer(runnerContext);
606+
analyzer.analyzeData(testRunnerOutput);
607+
608+
// Verify the suite item itself started and passed (the core regression in #1828)
609+
sinon.assert.calledWith(startedSpy, suiteItem);
610+
sinon.assert.calledWith(passedSpy, suiteItem, sinon.match.number);
611+
// Verify the method-level child also started and passed
612+
sinon.assert.calledWith(startedSpy, methodItem);
613+
sinon.assert.calledWith(passedSpy, methodItem, sinon.match.number);
614+
});
615+
546616
});

test/test-projects/junit/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
<version>5.6.0</version>
3636
<scope>test</scope>
3737
</dependency>
38+
<dependency>
39+
<groupId>org.junit.platform</groupId>
40+
<artifactId>junit-platform-suite</artifactId>
41+
<version>1.6.0</version>
42+
<scope>test</scope>
43+
</dependency>
3844
<dependency>
3945
<groupId>net.jqwik</groupId>
4046
<artifactId>jqwik</artifactId>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package junit5.suite;
2+
3+
import org.junit.platform.suite.api.SelectPackages;
4+
import org.junit.platform.suite.api.Suite;
5+
6+
@Suite
7+
@SelectPackages("junit5")
8+
public class MyTestSuite {
9+
}

0 commit comments

Comments
 (0)