Skip to content

Commit ddddc9d

Browse files
authored
fix: show only test cases in Test Results (#1888)
* fix: show only test cases in Test Results * fix: preserve skipped suite outcomes * fix: use plain labels for test hierarchy
1 parent cc6a833 commit ddddc9d

7 files changed

Lines changed: 188 additions & 151 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/controller/utils.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export function updateOrCreateTestItem(parent: TestItem, childData: IJavaTestIte
126126

127127
function updateTestItem(testItem: TestItem, metaInfo: IJavaTestItem): void {
128128
testItem.range = asRange(metaInfo.range);
129-
testItem.label = `${getCodiconLabel(metaInfo.testLevel)} ${metaInfo.label}`;
129+
testItem.label = metaInfo.label;
130130
dataCache.set(testItem, {
131131
jdtHandler: metaInfo.jdtHandler,
132132
fullName: metaInfo.fullName,
@@ -148,7 +148,7 @@ export function createTestItem(metaInfo: IJavaTestItem, parent?: TestItem): Test
148148
}
149149
const item: TestItem = testController.createTestItem(
150150
metaInfo.id,
151-
`${getCodiconLabel(metaInfo.testLevel)} ${metaInfo.label}`.trim(),
151+
metaInfo.label,
152152
metaInfo.uri ? Uri.parse(metaInfo.uri) : undefined,
153153
);
154154
item.range = asRange(metaInfo.range);
@@ -172,24 +172,6 @@ export function createTestItem(metaInfo: IJavaTestItem, parent?: TestItem): Test
172172
return item;
173173
}
174174

175-
/**
176-
* Get codicon label based on the test level.
177-
*/
178-
function getCodiconLabel(testLevel: TestLevel): string {
179-
switch (testLevel) {
180-
case TestLevel.Project:
181-
return '$(project)';
182-
case TestLevel.Package:
183-
return '$(symbol-namespace)';
184-
case TestLevel.Class:
185-
return '$(symbol-class)';
186-
case TestLevel.Method:
187-
return '$(symbol-method)';
188-
default:
189-
return '';
190-
}
191-
}
192-
193175
let updateNodeForDocumentTimeout: NodeJS.Timer;
194176
/**
195177
* Update test item in a document with adaptive debounce enabled.

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: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ 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();
21+
private suiteItems: Set<TestItem> = new Set();
2022

2123
// tests may be run concurrently, so each item's current state needs to be remembered
2224
private currentStates: Map<TestItem, CurrentItemState> = new Map();
@@ -67,27 +69,30 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
6769
if (data.startsWith(MessageId.TestTree)) {
6870
this.enlistToTestMapping(data.substring(MessageId.TestTree.length).trim());
6971
} else if (data.startsWith(MessageId.TestStart)) {
70-
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestStart.length));
71-
if (!item) {
72+
const testInfo: ITestInfo | undefined = this.getTestInfo(data.substr(MessageId.TestStart.length));
73+
if (!testInfo?.testItem) {
7274
return;
7375
}
74-
this.initializeParentState(item, this.triggeredTestsMapping);
76+
const item: TestItem = testInfo.testItem;
7577
this.setCurrentState(item, TestResultState.Running, 0);
7678
this.setDurationAtStart(this.getCurrentState(item));
77-
setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState);
78-
this.updateParentOnChildStart(item);
79+
if (!testInfo.isSuite) {
80+
setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState);
81+
}
7982
} else if (data.startsWith(MessageId.TestEnd)) {
80-
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestEnd.length));
81-
if (!item) {
83+
const testInfo: ITestInfo | undefined = this.getTestInfo(data.substr(MessageId.TestEnd.length));
84+
if (!testInfo?.testItem) {
8285
return;
8386
}
87+
const item: TestItem = testInfo.testItem;
8488
const currentState: CurrentItemState = this.getCurrentState(item);
8589
this.calcDurationAtEnd(currentState);
8690
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);
91+
const shouldReportSuite: boolean = currentState.resultState === TestResultState.Failed ||
92+
currentState.resultState === TestResultState.Errored ||
93+
(currentState.resultState === TestResultState.Skipped && testInfo.testCount === 0);
94+
if (!testInfo.isSuite || shouldReportSuite) {
95+
setTestState(this.testContext.testRun, item, currentState.resultState, undefined, currentState.duration);
9196
}
9297
} else if (data.startsWith(MessageId.TestFailed)) {
9398
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestFailed.length));
@@ -121,14 +126,18 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
121126
return;
122127
}
123128
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);
129+
const isSkippedSuite: boolean = currentResultState === TestResultState.Skipped &&
130+
this.suiteItems.has(this.tracingItem);
131+
if (!isSkippedSuite) {
132+
if (this.assertionFailure) {
133+
this.tryAppendMessage(this.tracingItem, this.assertionFailure, currentResultState);
134+
}
135+
if (this.traces?.value) {
136+
this.tryAppendMessage(this.tracingItem, new TestMessage(this.traces), currentResultState);
137+
}
138+
if (currentResultState === TestResultState.Errored) {
139+
setTestState(this.testContext.testRun, this.tracingItem, currentResultState);
140+
}
132141
}
133142
this.recordingType = RecordingType.None;
134143
} else if (data.startsWith(MessageId.ExpectStart)) {
@@ -192,8 +201,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
192201
}
193202

194203
protected getTestItem(message: string): TestItem | undefined {
204+
return this.getTestInfo(message)?.testItem;
205+
}
206+
207+
private getTestInfo(message: string): ITestInfo | undefined {
195208
const index: string = message.substring(0, message.indexOf(',')).trim();
196-
return this.testOutputMapping.get(index)?.testItem;
209+
return this.testOutputMapping.get(index);
197210
}
198211

199212
protected getTestId(message: string): string {
@@ -406,6 +419,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
406419
testId,
407420
testCount,
408421
testItem,
422+
isSuite,
409423
});
410424
}
411425

@@ -424,7 +438,15 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
424438
testId,
425439
testCount,
426440
testItem,
441+
isSuite,
427442
});
443+
if (isSuite && testItem) {
444+
this.suiteItems.add(testItem);
445+
}
446+
if (!isSuite && testItem && !this.enqueuedTests.has(testItem)) {
447+
this.enqueuedTests.add(testItem);
448+
this.testContext.testRun.enqueued(testItem);
449+
}
428450
}
429451
}
430452

@@ -526,6 +548,7 @@ interface ITestInfo {
526548
testId: string;
527549
testCount: number;
528550
testItem: TestItem | undefined;
551+
isSuite: boolean;
529552
}
530553

531554
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
}

0 commit comments

Comments
 (0)