Skip to content

Commit 7e11d06

Browse files
authored
refactor test resultResolver (#25619)
1 parent 6ec13c7 commit 7e11d06

File tree

13 files changed

+3720
-504
lines changed

13 files changed

+3720
-504
lines changed

.github/instructions/testing-workflow.instructions.md

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.

src/client/testing/testController/common/resultResolver.ts

Lines changed: 56 additions & 420 deletions
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode';
5+
import { CoveragePayload, FileCoverageMetrics } from './types';
6+
7+
/**
8+
* Stateless handler for processing coverage payloads and creating coverage objects.
9+
* This handler is shared across all workspaces and contains no instance state.
10+
*/
11+
export class TestCoverageHandler {
12+
/**
13+
* Process coverage payload
14+
* Pure function - returns coverage data without storing it
15+
*/
16+
public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map<string, FileCoverageDetail[]> {
17+
const detailedCoverageMap = new Map<string, FileCoverageDetail[]>();
18+
19+
if (payload.result === undefined) {
20+
return detailedCoverageMap;
21+
}
22+
23+
for (const [key, value] of Object.entries(payload.result)) {
24+
const fileNameStr = key;
25+
const fileCoverageMetrics: FileCoverageMetrics = value;
26+
27+
// Create FileCoverage object and add to run instance
28+
const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics);
29+
runInstance.addCoverage(fileCoverage);
30+
31+
// Create detailed coverage array for this file
32+
const detailedCoverage = this.createDetailedCoverage(
33+
fileCoverageMetrics.lines_covered ?? [],
34+
fileCoverageMetrics.lines_missed ?? [],
35+
);
36+
detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage);
37+
}
38+
39+
return detailedCoverageMap;
40+
}
41+
42+
/**
43+
* Create FileCoverage object from metrics
44+
*/
45+
private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage {
46+
const linesCovered = metrics.lines_covered ?? [];
47+
const linesMissed = metrics.lines_missed ?? [];
48+
const executedBranches = metrics.executed_branches;
49+
const totalBranches = metrics.total_branches;
50+
51+
const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length);
52+
53+
if (totalBranches === -1) {
54+
// branch coverage was not enabled and should not be displayed
55+
return new FileCoverage(uri, lineCoverageCount);
56+
} else {
57+
const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches);
58+
return new FileCoverage(uri, lineCoverageCount, branchCoverageCount);
59+
}
60+
}
61+
62+
/**
63+
* Create detailed coverage array for a file
64+
* Only line coverage on detailed, not branch coverage
65+
*/
66+
private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] {
67+
const detailedCoverageArray: FileCoverageDetail[] = [];
68+
69+
// Add covered lines
70+
for (const line of linesCovered) {
71+
// line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number
72+
// true value means line is covered
73+
const statementCoverage = new StatementCoverage(
74+
true,
75+
new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER),
76+
);
77+
detailedCoverageArray.push(statementCoverage);
78+
}
79+
80+
// Add missed lines
81+
for (const line of linesMissed) {
82+
// line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number
83+
// false value means line is NOT covered
84+
const statementCoverage = new StatementCoverage(
85+
false,
86+
new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER),
87+
);
88+
detailedCoverageArray.push(statementCoverage);
89+
}
90+
91+
return detailedCoverageArray;
92+
}
93+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode';
5+
import * as util from 'util';
6+
import { DiscoveredTestPayload } from './types';
7+
import { TestProvider } from '../../types';
8+
import { traceError } from '../../../logging';
9+
import { Testing } from '../../../common/utils/localize';
10+
import { createErrorTestItem } from './testItemUtilities';
11+
import { buildErrorNodeOptions, populateTestTree } from './utils';
12+
import { TestItemIndex } from './testItemIndex';
13+
14+
/**
15+
* Stateless handler for processing discovery payloads and building/updating the TestItem tree.
16+
* This handler is shared across all workspaces and contains no instance state.
17+
*/
18+
export class TestDiscoveryHandler {
19+
/**
20+
* Process discovery payload and update test tree
21+
* Pure function - no instance state used
22+
*/
23+
public processDiscovery(
24+
payload: DiscoveredTestPayload,
25+
testController: TestController,
26+
testItemIndex: TestItemIndex,
27+
workspaceUri: Uri,
28+
testProvider: TestProvider,
29+
token?: CancellationToken,
30+
): void {
31+
if (!payload) {
32+
// No test data is available
33+
return;
34+
}
35+
36+
const workspacePath = workspaceUri.fsPath;
37+
const rawTestData = payload as DiscoveredTestPayload;
38+
39+
// Check if there were any errors in the discovery process.
40+
if (rawTestData.status === 'error') {
41+
this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider);
42+
} else {
43+
// remove error node only if no errors exist.
44+
testController.items.delete(`DiscoveryError:${workspacePath}`);
45+
}
46+
47+
if (rawTestData.tests || rawTestData.tests === null) {
48+
// if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not.
49+
// parse and insert test data.
50+
51+
// Clear existing mappings before rebuilding test tree
52+
testItemIndex.clear();
53+
54+
// If the test root for this folder exists: Workspace refresh, update its children.
55+
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree.
56+
// Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test
57+
populateTestTree(
58+
testController,
59+
rawTestData.tests,
60+
undefined,
61+
{
62+
runIdToTestItem: testItemIndex.runIdToTestItemMap,
63+
runIdToVSid: testItemIndex.runIdToVSidMap,
64+
vsIdToRunId: testItemIndex.vsIdToRunIdMap,
65+
} as any,
66+
token,
67+
);
68+
}
69+
}
70+
71+
/**
72+
* Create an error node for discovery failures
73+
*/
74+
public createErrorNode(
75+
testController: TestController,
76+
workspaceUri: Uri,
77+
error: string[] | undefined,
78+
testProvider: TestProvider,
79+
): void {
80+
const workspacePath = workspaceUri.fsPath;
81+
const testingErrorConst =
82+
testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery;
83+
84+
traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? '');
85+
86+
let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`);
87+
const message = util.format(
88+
`${testingErrorConst} ${Testing.seePythonOutput}\r\n`,
89+
error?.join('\r\n\r\n') ?? '',
90+
);
91+
92+
if (errorNode === undefined) {
93+
const options = buildErrorNodeOptions(workspaceUri, message, testProvider);
94+
errorNode = createErrorTestItem(testController, options);
95+
testController.items.add(errorNode);
96+
}
97+
98+
const errorNodeLabel: MarkdownString = new MarkdownString(
99+
`[Show output](command:python.viewOutput) to view error logs`,
100+
);
101+
errorNodeLabel.isTrusted = true;
102+
errorNode.error = errorNodeLabel;
103+
}
104+
}

0 commit comments

Comments
 (0)