Skip to content

Commit b79bf37

Browse files
committed
fix: show only test cases in Test Results
Copilot-Session: 66d94993-0cc9-4a2f-886b-99a1649e7051
1 parent cc6a833 commit b79bf37

6 files changed

Lines changed: 179 additions & 131 deletions

File tree

src/controller/testController.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
184184
return Promise.resolve(coverageProvider!.getCoverageDetails(fileCoverage.uri));
185185
};
186186
}
187+
const testRunner: TestRunner | undefined = testRunnerService.getRunner(request.profile?.label, request.profile?.kind);
188+
if (testRunner) {
189+
enqueueTestMethods(testItems, run);
190+
}
187191

188192
try {
189193
await new Promise<void>(async (resolve: () => void): Promise<void> => {
@@ -195,7 +199,6 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
195199
disposables.forEach((d: Disposable) => d.dispose());
196200
return resolve();
197201
});
198-
enqueueTestMethods(testItems, run);
199202
// TODO: first group by project, then merge test methods.
200203
const queue: TestItem[][] = mergeTestMethods(testItems);
201204
for (const testsInQueue of queue) {
@@ -219,7 +222,6 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in
219222
profile: request.profile,
220223
testConfig: await loadRunConfig(itemsPerProject, workspaceFolder),
221224
};
222-
const testRunner: TestRunner | undefined = testRunnerService.getRunner(request.profile?.label, request.profile?.kind);
223225
if (testRunner) {
224226
await executeWithTestRunner(option, testRunner, testContext, run, disposables);
225227
disposables.forEach((d: Disposable) => d.dispose());

src/runners/baseRunner/RunnerResultAnalyzer.ts

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
// Licensed under the MIT license.
33

44
import { Location, MarkdownString, TestItem } from 'vscode';
5-
import { dataCache, ITestItemData } from '../../controller/testItemDataCache';
6-
import { IRunTestContext, TestLevel, TestResultState } from '../../java-test-runner.api';
5+
import { IRunTestContext } from '../../java-test-runner.api';
76
import { processStackTraceLine } from '../utils';
87

98
export abstract class RunnerResultAnalyzer {
10-
// Track parent test item states to update them when all children complete
11-
protected parentStates: Map<TestItem, ParentItemState> = new Map();
12-
139
constructor(protected testContext: IRunTestContext) { }
1410

1511
public abstract analyzeData(data: string): void;
@@ -40,95 +36,4 @@ export abstract class RunnerResultAnalyzer {
4036
return stacktrace.includes(s);
4137
});
4238
}
43-
44-
/**
45-
* Initialize parent state tracking for a test item.
46-
* Counts how many method-level children are being tested.
47-
*/
48-
protected initializeParentState(item: TestItem, triggeredTestsMapping: Map<string, TestItem>): void {
49-
const parent: TestItem | undefined = item.parent;
50-
if (!parent) {
51-
return;
52-
}
53-
54-
const parentData: ITestItemData | undefined = dataCache.get(parent);
55-
if (!parentData || parentData.testLevel !== TestLevel.Class) {
56-
return;
57-
}
58-
59-
if (!this.parentStates.has(parent)) {
60-
// Count how many method-level children are being tested (only count triggered tests)
61-
let childCount: number = 0;
62-
parent.children.forEach((child: TestItem) => {
63-
const childData: ITestItemData | undefined = dataCache.get(child);
64-
if (childData?.testLevel === TestLevel.Method && triggeredTestsMapping.has(child.id)) {
65-
childCount++;
66-
}
67-
});
68-
69-
this.parentStates.set(parent, {
70-
started: false,
71-
childrenTotal: childCount,
72-
childrenCompleted: 0,
73-
hasFailure: false,
74-
});
75-
}
76-
}
77-
78-
/**
79-
* Update parent test item when a child test starts.
80-
* Marks the parent as "started" when the first child starts.
81-
*/
82-
protected updateParentOnChildStart(item: TestItem): void {
83-
const parent: TestItem | undefined = item.parent;
84-
if (!parent) {
85-
return;
86-
}
87-
88-
const parentState: ParentItemState | undefined = this.parentStates.get(parent);
89-
if (parentState && !parentState.started) {
90-
parentState.started = true;
91-
this.testContext.testRun.started(parent);
92-
}
93-
}
94-
95-
/**
96-
* Update parent test item when a child test completes.
97-
* Marks the parent as "passed" or "failed" when all children complete.
98-
*/
99-
protected updateParentOnChildComplete(item: TestItem, childState: TestResultState): void {
100-
const parent: TestItem | undefined = item.parent;
101-
if (!parent) {
102-
return;
103-
}
104-
105-
const parentState: ParentItemState | undefined = this.parentStates.get(parent);
106-
if (!parentState) {
107-
return;
108-
}
109-
110-
// Consider failed or errored tests as failures for the parent
111-
if (childState === TestResultState.Failed ||
112-
childState === TestResultState.Errored) {
113-
parentState.hasFailure = true;
114-
}
115-
116-
parentState.childrenCompleted++;
117-
118-
// Check if all children have completed
119-
if (parentState.childrenCompleted >= parentState.childrenTotal && parentState.childrenTotal > 0) {
120-
if (parentState.hasFailure) {
121-
this.testContext.testRun.failed(parent, []);
122-
} else {
123-
this.testContext.testRun.passed(parent);
124-
}
125-
}
126-
}
127-
}
128-
129-
interface ParentItemState {
130-
started: boolean;
131-
childrenTotal: number;
132-
childrenCompleted: number;
133-
hasFailure: boolean;
13439
}

src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
1717
private triggeredTestsMapping: Map<string, TestItem> = new Map();
1818
private projectName: string;
1919
private incompleteTestSuite: ITestInfo[] = [];
20+
private enqueuedTests: Set<TestItem> = new Set();
2021

2122
// tests may be run concurrently, so each item's current state needs to be remembered
2223
private currentStates: Map<TestItem, CurrentItemState> = new Map();
@@ -67,27 +68,29 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
6768
if (data.startsWith(MessageId.TestTree)) {
6869
this.enlistToTestMapping(data.substring(MessageId.TestTree.length).trim());
6970
} else if (data.startsWith(MessageId.TestStart)) {
70-
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestStart.length));
71-
if (!item) {
71+
const testInfo: ITestInfo | undefined = this.getTestInfo(data.substr(MessageId.TestStart.length));
72+
if (!testInfo?.testItem) {
7273
return;
7374
}
74-
this.initializeParentState(item, this.triggeredTestsMapping);
75+
const item: TestItem = testInfo.testItem;
7576
this.setCurrentState(item, TestResultState.Running, 0);
7677
this.setDurationAtStart(this.getCurrentState(item));
77-
setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState);
78-
this.updateParentOnChildStart(item);
78+
if (!testInfo.isSuite) {
79+
setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState);
80+
}
7981
} else if (data.startsWith(MessageId.TestEnd)) {
80-
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestEnd.length));
81-
if (!item) {
82+
const testInfo: ITestInfo | undefined = this.getTestInfo(data.substr(MessageId.TestEnd.length));
83+
if (!testInfo?.testItem) {
8284
return;
8385
}
86+
const item: TestItem = testInfo.testItem;
8487
const currentState: CurrentItemState = this.getCurrentState(item);
8588
this.calcDurationAtEnd(currentState);
8689
this.determineResultStateAtEnd(data, currentState);
87-
setTestState(this.testContext.testRun, item, currentState.resultState, undefined, currentState.duration);
88-
const itemData: ITestItemData | undefined = dataCache.get(item);
89-
if (itemData?.testLevel === TestLevel.Method) {
90-
this.updateParentOnChildComplete(item, currentState.resultState);
90+
if (!testInfo.isSuite ||
91+
currentState.resultState === TestResultState.Failed ||
92+
currentState.resultState === TestResultState.Errored) {
93+
setTestState(this.testContext.testRun, item, currentState.resultState, undefined, currentState.duration);
9194
}
9295
} else if (data.startsWith(MessageId.TestFailed)) {
9396
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestFailed.length));
@@ -121,14 +124,16 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
121124
return;
122125
}
123126
const currentResultState: TestResultState = this.getCurrentState(this.tracingItem).resultState;
124-
if (this.assertionFailure) {
125-
this.tryAppendMessage(this.tracingItem, this.assertionFailure, currentResultState);
126-
}
127-
if (this.traces?.value) {
128-
this.tryAppendMessage(this.tracingItem, new TestMessage(this.traces), currentResultState);
129-
}
130-
if (currentResultState === TestResultState.Errored) {
131-
setTestState(this.testContext.testRun, this.tracingItem, currentResultState);
127+
if (currentResultState !== TestResultState.Skipped) {
128+
if (this.assertionFailure) {
129+
this.tryAppendMessage(this.tracingItem, this.assertionFailure, currentResultState);
130+
}
131+
if (this.traces?.value) {
132+
this.tryAppendMessage(this.tracingItem, new TestMessage(this.traces), currentResultState);
133+
}
134+
if (currentResultState === TestResultState.Errored) {
135+
setTestState(this.testContext.testRun, this.tracingItem, currentResultState);
136+
}
132137
}
133138
this.recordingType = RecordingType.None;
134139
} else if (data.startsWith(MessageId.ExpectStart)) {
@@ -192,8 +197,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
192197
}
193198

194199
protected getTestItem(message: string): TestItem | undefined {
200+
return this.getTestInfo(message)?.testItem;
201+
}
202+
203+
private getTestInfo(message: string): ITestInfo | undefined {
195204
const index: string = message.substring(0, message.indexOf(',')).trim();
196-
return this.testOutputMapping.get(index)?.testItem;
205+
return this.testOutputMapping.get(index);
197206
}
198207

199208
protected getTestId(message: string): string {
@@ -406,6 +415,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
406415
testId,
407416
testCount,
408417
testItem,
418+
isSuite,
409419
});
410420
}
411421

@@ -424,7 +434,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
424434
testId,
425435
testCount,
426436
testItem,
437+
isSuite,
427438
});
439+
if (!isSuite && testItem && !this.enqueuedTests.has(testItem)) {
440+
this.enqueuedTests.add(testItem);
441+
this.testContext.testRun.enqueued(testItem);
442+
}
428443
}
429444
}
430445

@@ -526,6 +541,7 @@ interface ITestInfo {
526541
testId: string;
527542
testCount: number;
528543
testItem: TestItem | undefined;
544+
isSuite: boolean;
529545
}
530546

531547
enum RecordingType {

src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { Location, MarkdownString, TestItem, TestMessage } from 'vscode';
5-
import { dataCache, ITestItemData } from '../../controller/testItemDataCache';
5+
import { dataCache } from '../../controller/testItemDataCache';
66
import { RunnerResultAnalyzer } from '../baseRunner/RunnerResultAnalyzer';
77
import { setTestState } from '../utils';
88
import { IRunTestContext, TestLevel, TestResultState } from '../../java-test-runner.api';
@@ -33,6 +33,7 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
3333
}
3434
if (testLevel === TestLevel.Method) {
3535
this.triggeredTestsMapping.set(item.id, item);
36+
this.testContext.testRun.enqueued(item);
3637
} else {
3738
item.children.forEach((child: TestItem) => {
3839
queue.push(child);
@@ -73,10 +74,8 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
7374
if (!item) {
7475
return;
7576
}
76-
this.initializeParentState(item, this.triggeredTestsMapping);
7777
this.currentTestState = TestResultState.Running;
7878
this.testContext.testRun.started(item);
79-
this.updateParentOnChildStart(item);
8079
} else if (outputData.name === TEST_FAIL) {
8180
const item: TestItem | undefined = this.getTestItem(id);
8281
if (!item) {
@@ -114,10 +113,6 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
114113
}
115114
const duration: number | undefined = this.parseDuration(attributes.duration);
116115
setTestState(this.testContext.testRun, item, this.currentTestState, undefined, duration);
117-
const itemData: ITestItemData | undefined = dataCache.get(item);
118-
if (itemData?.testLevel === TestLevel.Method) {
119-
this.updateParentOnChildComplete(item, this.currentTestState);
120-
}
121116
}
122117
}
123118

@@ -141,7 +136,9 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
141136
message += `\n${attributes.trace}`;
142137
}
143138
const testMessage: TestMessage = new TestMessage(message);
144-
for (const item of this.testContext.testItems) {
139+
const testCases: Set<TestItem> = new Set(this.triggeredTestsMapping.values());
140+
const items: Iterable<TestItem> = testCases.size > 0 ? testCases : this.testContext.testItems;
141+
for (const item of items) {
145142
this.testContext.testRun.errored(item, testMessage);
146143
}
147144
}

test/suite/JUnitAnalyzer.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
407407
const testItem = generateTestItem(testController, 'junit@junit5.ParameterizedAnnotationTest#testMultiArguments(String, String, String)', TestKind.JUnit5, new Range(10, 0, 16, 0));
408408
const testRunRequest = new TestRunRequest([testItem], []);
409409
const testRun = testController.createTestRun(testRunRequest);
410+
const enqueuedSpy = sinon.spy(testRun, 'enqueued');
410411
const startedSpy = sinon.spy(testRun, 'started');
411412
const passedSpy = sinon.spy(testRun, 'passed');
412413
const testRunnerOutput = `%TESTC 0 v2
@@ -434,6 +435,8 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
434435
stub.returns(dummy);
435436
analyzer.analyzeData(testRunnerOutput);
436437

438+
assert.strictEqual(enqueuedSpy.calledWith(testItem), false);
439+
sinon.assert.calledWith(enqueuedSpy, dummy);
437440
sinon.assert.calledWith(startedSpy, dummy);
438441
sinon.assert.calledWith(passedSpy, dummy);
439442
});
@@ -574,6 +577,7 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
574577

575578
const testRunRequest = new TestRunRequest([suiteItem], []);
576579
const testRun = testController.createTestRun(testRunRequest);
580+
const enqueuedSpy = sinon.spy(testRun, 'enqueued');
577581
const startedSpy = sinon.spy(testRun, 'started');
578582
const passedSpy = sinon.spy(testRun, 'passed');
579583

@@ -605,12 +609,59 @@ org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
605609
const analyzer = new JUnitRunnerResultAnalyzer(runnerContext);
606610
analyzer.analyzeData(testRunnerOutput);
607611

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+
assert.strictEqual(enqueuedSpy.calledWith(suiteItem), false);
613+
assert.strictEqual(enqueuedSpy.calledWith(classItem), false);
614+
sinon.assert.calledWith(enqueuedSpy, methodItem);
615+
assert.strictEqual(startedSpy.calledWith(suiteItem), false);
616+
assert.strictEqual(passedSpy.calledWith(suiteItem), false);
617+
assert.strictEqual(startedSpy.calledWith(classItem), false);
618+
assert.strictEqual(passedSpy.calledWith(classItem), false);
612619
sinon.assert.calledWith(startedSpy, methodItem);
613620
sinon.assert.calledWith(passedSpy, methodItem, sinon.match.number);
614621
});
615622

623+
test("does not report an assumption-aborted suite as a test case", () => {
624+
const suiteItem = testController.createTestItem(
625+
'junit@junit5.AbortedSuite',
626+
'AbortedSuite',
627+
Uri.file('/mock/test/AbortedSuite.java'),
628+
);
629+
dataCache.set(suiteItem, {
630+
jdtHandler: '',
631+
fullName: 'junit5.AbortedSuite',
632+
projectName: 'junit',
633+
testLevel: TestLevel.Class,
634+
testKind: TestKind.JUnit5,
635+
});
636+
637+
const testRun = testController.createTestRun(new TestRunRequest([suiteItem], []));
638+
const enqueuedSpy = sinon.spy(testRun, 'enqueued');
639+
const startedSpy = sinon.spy(testRun, 'started');
640+
const skippedSpy = sinon.spy(testRun, 'skipped');
641+
const testRunnerOutput = `%TESTC 0 v2
642+
%TSTTREE1,junit5.AbortedSuite,true,0,false,-1,AbortedSuite,,[engine:junit-jupiter]/[class:junit5.AbortedSuite]
643+
%TESTS 1,junit5.AbortedSuite
644+
%FAILED 1,@AssumptionFailure: junit5.AbortedSuite
645+
%TRACES
646+
org.opentest4j.TestAbortedException: aborted
647+
%TRACEE
648+
%TESTE 1,@AssumptionFailure: junit5.AbortedSuite
649+
%RUNTIME10`;
650+
const runnerContext: IRunTestContext = {
651+
isDebug: false,
652+
kind: TestKind.JUnit5,
653+
projectName: 'junit',
654+
testItems: [suiteItem],
655+
testRun,
656+
workspaceFolder: workspace.workspaceFolders?.[0]!,
657+
};
658+
659+
const analyzer = new JUnitRunnerResultAnalyzer(runnerContext);
660+
analyzer.analyzeData(testRunnerOutput);
661+
662+
assert.strictEqual(enqueuedSpy.calledWith(suiteItem), false);
663+
assert.strictEqual(startedSpy.calledWith(suiteItem), false);
664+
assert.strictEqual(skippedSpy.calledWith(suiteItem), false);
665+
});
666+
616667
});

0 commit comments

Comments
 (0)