Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
580 changes: 580 additions & 0 deletions .github/instructions/testing-workflow.instructions.md

Large diffs are not rendered by default.

476 changes: 56 additions & 420 deletions src/client/testing/testController/common/resultResolver.ts

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions src/client/testing/testController/common/testCoverageHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode';
import { CoveragePayload, FileCoverageMetrics } from './types';

/**
* Stateless handler for processing coverage payloads and creating coverage objects.
* This handler is shared across all workspaces and contains no instance state.
*/
export class TestCoverageHandler {
/**
* Process coverage payload
* Pure function - returns coverage data without storing it
*/
public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map<string, FileCoverageDetail[]> {
const detailedCoverageMap = new Map<string, FileCoverageDetail[]>();

if (payload.result === undefined) {
return detailedCoverageMap;
}

for (const [key, value] of Object.entries(payload.result)) {
const fileNameStr = key;
const fileCoverageMetrics: FileCoverageMetrics = value;

// Create FileCoverage object and add to run instance
const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics);
runInstance.addCoverage(fileCoverage);

// Create detailed coverage array for this file
const detailedCoverage = this.createDetailedCoverage(
fileCoverageMetrics.lines_covered ?? [],
fileCoverageMetrics.lines_missed ?? [],
);
detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage);
}

return detailedCoverageMap;
}

/**
* Create FileCoverage object from metrics
*/
private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage {
const linesCovered = metrics.lines_covered ?? [];
const linesMissed = metrics.lines_missed ?? [];
const executedBranches = metrics.executed_branches;
const totalBranches = metrics.total_branches;

const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length);

if (totalBranches === -1) {
// branch coverage was not enabled and should not be displayed
return new FileCoverage(uri, lineCoverageCount);
} else {
const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches);
return new FileCoverage(uri, lineCoverageCount, branchCoverageCount);
}
}

/**
* Create detailed coverage array for a file
* Only line coverage on detailed, not branch coverage
*/
private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] {
const detailedCoverageArray: FileCoverageDetail[] = [];

// Add covered lines
for (const line of linesCovered) {
// line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number
// true value means line is covered
const statementCoverage = new StatementCoverage(
true,
new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER),
);
detailedCoverageArray.push(statementCoverage);
}

// Add missed lines
for (const line of linesMissed) {
// line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number
// false value means line is NOT covered
const statementCoverage = new StatementCoverage(
false,
new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER),
);
detailedCoverageArray.push(statementCoverage);
}

return detailedCoverageArray;
}
}
104 changes: 104 additions & 0 deletions src/client/testing/testController/common/testDiscoveryHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode';
import * as util from 'util';
import { DiscoveredTestPayload } from './types';
import { TestProvider } from '../../types';
import { traceError } from '../../../logging';
import { Testing } from '../../../common/utils/localize';
import { createErrorTestItem } from './testItemUtilities';
import { buildErrorNodeOptions, populateTestTree } from './utils';
import { TestItemIndex } from './testItemIndex';

/**
* Stateless handler for processing discovery payloads and building/updating the TestItem tree.
* This handler is shared across all workspaces and contains no instance state.
*/
export class TestDiscoveryHandler {
/**
* Process discovery payload and update test tree
* Pure function - no instance state used
*/
public processDiscovery(
payload: DiscoveredTestPayload,
testController: TestController,
testItemIndex: TestItemIndex,
workspaceUri: Uri,
testProvider: TestProvider,
token?: CancellationToken,
): void {
if (!payload) {
// No test data is available
return;
}

const workspacePath = workspaceUri.fsPath;
const rawTestData = payload as DiscoveredTestPayload;

// Check if there were any errors in the discovery process.
if (rawTestData.status === 'error') {
this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider);
} else {
// remove error node only if no errors exist.
testController.items.delete(`DiscoveryError:${workspacePath}`);
}

if (rawTestData.tests || rawTestData.tests === null) {
// if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not.
// parse and insert test data.

// Clear existing mappings before rebuilding test tree
testItemIndex.clear();

// If the test root for this folder exists: Workspace refresh, update its children.
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree.
// Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test
populateTestTree(
testController,
rawTestData.tests,
undefined,
{
runIdToTestItem: testItemIndex.runIdToTestItemMap,
runIdToVSid: testItemIndex.runIdToVSidMap,
vsIdToRunId: testItemIndex.vsIdToRunIdMap,
} as any,
token,
);
}
}

/**
* Create an error node for discovery failures
*/
public createErrorNode(
testController: TestController,
workspaceUri: Uri,
error: string[] | undefined,
testProvider: TestProvider,
): void {
const workspacePath = workspaceUri.fsPath;
const testingErrorConst =
testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery;

traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? '');

let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`);
const message = util.format(
`${testingErrorConst} ${Testing.seePythonOutput}\r\n`,
error?.join('\r\n\r\n') ?? '',
);

if (errorNode === undefined) {
const options = buildErrorNodeOptions(workspaceUri, message, testProvider);
errorNode = createErrorTestItem(testController, options);
testController.items.add(errorNode);
}

const errorNodeLabel: MarkdownString = new MarkdownString(
`[Show output](command:python.viewOutput) to view error logs`,
);
errorNodeLabel.isTrusted = true;
errorNode.error = errorNodeLabel;
}
}
Loading