testing: assorted TPI fixes (#208740)

* testing: bounds check coverage data

Fixes #208663

* testing: fix coverage label of boolean counts

Fixes #208463

* testing: clarify docs on loadDetailedCoverage

Fixes #208724
This commit is contained in:
Connor Peet 2024-03-25 16:14:08 -07:00 committed by GitHub
parent a8a38595ac
commit 53ff5ca19e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 43 additions and 15 deletions

View file

@ -2024,6 +2024,10 @@ export namespace TestCoverage {
}
export function fromDetails(coverage: vscode.FileCoverageDetail): CoverageDetails.Serialized {
if (typeof coverage.executed === 'number' && coverage.executed < 0) {
throw new Error(`Invalid coverage count ${coverage.executed}`);
}
if ('branches' in coverage) {
return {
count: coverage.executed,
@ -2044,6 +2048,10 @@ export namespace TestCoverage {
}
export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized {
types.validateTestCoverageCount(coverage.statementCoverage);
types.validateTestCoverageCount(coverage.branchCoverage);
types.validateTestCoverageCount(coverage.declarationCoverage);
return {
id,
uri: coverage.uri,

View file

@ -4027,14 +4027,23 @@ export class TestTag implements vscode.TestTag {
//#region Test Coverage
export class TestCoverageCount implements vscode.TestCoverageCount {
constructor(public covered: number, public total: number) {
validateTestCoverageCount(this);
}
}
const validateCC = (cc?: vscode.TestCoverageCount) => {
if (cc && cc.covered > cc.total) {
export function validateTestCoverageCount(cc?: vscode.TestCoverageCount) {
if (!cc) {
return;
}
if (cc.covered > cc.total) {
throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`);
}
};
if (cc.total < 0) {
throw new Error(`The number of covered items (${cc.total}) cannot be negative`);
}
}
export class FileCoverage implements vscode.FileCoverage {
public static fromDetails(uri: vscode.Uri, details: vscode.FileCoverageDetail[]): vscode.FileCoverage {
@ -4077,9 +4086,6 @@ export class FileCoverage implements vscode.FileCoverage {
public branchCoverage?: vscode.TestCoverageCount,
public declarationCoverage?: vscode.TestCoverageCount,
) {
validateCC(statementCoverage);
validateCC(branchCoverage);
validateCC(declarationCoverage);
}
}

View file

@ -6,6 +6,7 @@
import * as dom from 'vs/base/browser/dom';
import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget';
import { mapFindFirst } from 'vs/base/common/arraysFind';
import { assertNever } from 'vs/base/common/assert';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
@ -31,7 +32,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons';
import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
import { CoverageDetails, DetailType, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
const MAX_HOVERED_LINES = 30;
@ -452,32 +453,42 @@ export class CoverageDetailsModel {
/** Gets the markdown description for the given detail */
public describe(detail: CoverageDetailsWithBranch, model: ITextModel): IMarkdownString | undefined {
if (detail.type === DetailType.Declaration) {
return new MarkdownString().appendMarkdown(localize('coverage.declExecutedCount', '`{0}` was executed {1} time(s).', detail.name, detail.count));
return namedDetailLabel(detail.name, detail);
} else if (detail.type === DetailType.Statement) {
const text = wrapName(model.getValueInRange(tidyLocation(detail.location)).trim() || `<empty statement>`);
const str = new MarkdownString();
if (detail.branches?.length) {
const covered = detail.branches.filter(b => !!b.count).length;
str.appendMarkdown(localize('coverage.branches', '{0} of {1} of branches in {2} were covered.', covered, detail.branches.length, text));
return new MarkdownString().appendMarkdown(localize('coverage.branches', '{0} of {1} of branches in {2} were covered.', covered, detail.branches.length, text));
} else {
str.appendMarkdown(localize('coverage.codeExecutedCount', '{0} was executed {1} time(s).', text, detail.count));
return namedDetailLabel(text, detail);
}
return str;
} else if (detail.type === DetailType.Branch) {
const text = wrapName(model.getValueInRange(tidyLocation(detail.detail.location)).trim() || `<empty statement>`);
const { count, label } = detail.detail.branches![detail.branch];
const label2 = label ? wrapInBackticks(label) : `#${detail.branch + 1}`;
if (count === 0) {
if (!count) {
return new MarkdownString().appendMarkdown(localize('coverage.branchNotCovered', 'Branch {0} in {1} was not covered.', label2, text));
} else if (count === true) {
return new MarkdownString().appendMarkdown(localize('coverage.branchCoveredYes', 'Branch {0} in {1} was executed.', label2, text));
} else {
return new MarkdownString().appendMarkdown(localize('coverage.branchCovered', 'Branch {0} in {1} was executed {2} time(s).', label2, text, count));
}
}
return undefined;
assertNever(detail);
}
}
function namedDetailLabel(name: string, detail: IStatementCoverage | IDeclarationCoverage) {
return new MarkdownString().appendMarkdown(
!detail.count // 0 or false
? localize('coverage.declExecutedNo', '`{0}` was not executed.', name)
: typeof detail.count === 'number'
? localize('coverage.declExecutedCount', '`{0}` was executed {1} time(s).', name, detail.count)
: localize('coverage.declExecutedYes', '`{0}` was executed.', name)
);
}
// 'tidies' the range by normalizing it into a range and removing leading
// and trailing whitespace.
function tidyLocation(location: Range | Position): Range {

View file

@ -17195,7 +17195,10 @@ declare module 'vscode' {
runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable<void> | void;
/**
* A function that provides detailed statement and function-level coverage for a file.
* An extension-provided function that provides detailed statement and
* function-level coverage for a file. The editor will call this when more
* detail is needed for a file, such as when it's opened in an editor or
* expanded in the **Test Coverage** view.
*
* The {@link FileCoverage} object passed to this function is the same instance
* emitted on {@link TestRun.addCoverage} calls associated with this profile.