testing: finalize test coverage (#208115)

Closes #123713
This commit is contained in:
Connor Peet 2024-03-19 11:46:17 -07:00 committed by GitHub
parent a4eeea64a7
commit 2aa0f1c40b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 216 additions and 238 deletions

View file

@ -45,7 +45,6 @@
"terminalDataWriteEvent",
"terminalDimensions",
"tunnels",
"testCoverage",
"testObserver",
"textSearchProvider",
"timeline",

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.88.0",
"distro": "7ca938298e57ad434ea8807e132707055458a749",
"distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd",
"author": {
"name": "Microsoft Corporation"
},

View file

@ -28,7 +28,6 @@ import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants';
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection';
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';
interface ControllerInfo {
@ -155,7 +154,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
return new TestItemImpl(controllerId, id, label, uri);
},
createTestRun: (request, name, persist = true) => {
return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist);
return this.runTracker.createTestRun(controllerId, collection, request, name, persist);
},
invalidateTestResults: items => {
if (items === undefined) {
@ -354,7 +353,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
return {};
}
const { collection, profiles, extension } = lookup;
const { collection, profiles } = lookup;
const profile = profiles.get(req.profileId);
if (!profile) {
return {};
@ -385,7 +384,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape {
const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun(
publicReq,
TestRunDto.fromInternal(req, lookup.collection),
extension,
profile,
token,
);
@ -463,7 +461,6 @@ class TestRunTracker extends Disposable {
constructor(
private readonly dto: TestRunDto,
private readonly proxy: MainThreadTestingShape,
private readonly extension: IRelaxedExtensionDescription,
private readonly logService: ILogService,
private readonly profile: vscode.TestRunProfile | undefined,
parentToken?: CancellationToken,
@ -517,7 +514,6 @@ class TestRunTracker extends Disposable {
const runId = this.dto.id;
const ctrlId = this.dto.controllerId;
const taskId = generateUuid();
const extension = this.extension;
const guardTestMutation = <Args extends unknown[]>(fn: (test: vscode.TestItem, ...args: Args) => void) =>
(test: vscode.TestItem, ...args: Args) => {
@ -574,7 +570,6 @@ class TestRunTracker extends Disposable {
},
// todo@connor4312: back compat
set coverageProvider(provider: ICoverageProvider | undefined) {
checkProposedApiEnabled(extension, 'testCoverage');
coverageProvider = provider;
if (provider) {
Promise.resolve(provider.provideFileCoverage(CancellationToken.None)).then(coverage => {
@ -585,10 +580,7 @@ class TestRunTracker extends Disposable {
});
}
},
addCoverage: coverage => {
checkProposedApiEnabled(extension, 'testCoverage');
addCoverage(coverage);
},
addCoverage,
//#region state mutation
enqueued: guardTestMutation(test => {
this.proxy.$updateTestStateInRun(runId, taskId, TestId.fromExtHostTestItem(test, ctrlId).toString(), TestResultState.Queued);
@ -745,8 +737,8 @@ export class TestRunCoordinator {
* `$startedExtensionTestRun` is not invoked. The run must eventually
* be cancelled manually.
*/
public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, extension: Readonly<IRelaxedExtensionDescription>, profile: vscode.TestRunProfile, token: CancellationToken) {
return this.getTracker(req, dto, extension, profile, token);
public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) {
return this.getTracker(req, dto, profile, token);
}
/**
@ -768,7 +760,7 @@ export class TestRunCoordinator {
/**
* Implements the public `createTestRun` API.
*/
public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
const existing = this.tracked.get(request);
if (existing) {
return existing.createRun(name);
@ -788,7 +780,7 @@ export class TestRunCoordinator {
persist
});
const tracker = this.getTracker(request, dto, extension, request.profile);
const tracker = this.getTracker(request, dto, request.profile);
Event.once(tracker.onEnd)(() => {
this.proxy.$finishedExtensionTestRun(dto.id);
});
@ -796,8 +788,8 @@ export class TestRunCoordinator {
return tracker.createRun(name);
}
private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, extension: IRelaxedExtensionDescription, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) {
const tracker = new TestRunTracker(dto, this.proxy, extension, this.logService, profile, token);
private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) {
const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, token);
this.tracked.set(req, tracker);
this.trackedById.set(tracker.id, tracker);
return tracker;

View file

@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri';
import { mock, mockObject, MockObject } from 'vs/base/test/common/mock';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import * as editorRange from 'vs/editor/common/core/range';
import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { NullLogService } from 'vs/platform/log/common/log';
import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
@ -603,7 +603,6 @@ suite('ExtHost Testing', () => {
let req: TestRunRequest;
let dto: TestRunDto;
const ext: IRelaxedExtensionDescription = {} as any;
teardown(() => {
for (const { id } of c.trackers) {
@ -637,11 +636,11 @@ suite('ExtHost Testing', () => {
});
test('tracks a run started from a main thread request', () => {
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token));
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token));
assert.strictEqual(tracker.hasRunningTasks, false);
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
const task1 = c.createTestRun('ctrl', single, req, 'run1', true);
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
assert.strictEqual(proxy.$startedExtensionTestRun.called, false);
assert.strictEqual(tracker.hasRunningTasks, true);
@ -662,8 +661,8 @@ suite('ExtHost Testing', () => {
test('run cancel force ends after a timeout', () => {
const clock = sinon.useFakeTimers();
try {
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token));
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token));
const task = c.createTestRun('ctrl', single, req, 'run1', true);
const onEnded = sinon.stub();
ds.add(tracker.onEnd(onEnded));
@ -687,8 +686,8 @@ suite('ExtHost Testing', () => {
});
test('run cancel force ends on second cancellation request', () => {
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, ext, configuration, cts.token));
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token));
const task = c.createTestRun('ctrl', single, req, 'run1', true);
const onEnded = sinon.stub();
ds.add(tracker.onEnd(onEnded));
@ -706,7 +705,7 @@ suite('ExtHost Testing', () => {
});
test('tracks a run started from an extension request', () => {
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
const task1 = c.createTestRun('ctrl', single, req, 'hello world', false);
const tracker = Iterable.first(c.trackers)!;
assert.strictEqual(tracker.hasRunningTasks, true);
@ -722,8 +721,8 @@ suite('ExtHost Testing', () => {
}]
]);
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true);
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true);
task1.end();
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
@ -737,7 +736,7 @@ suite('ExtHost Testing', () => {
});
test('adds tests to run smartly', () => {
const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false);
const tracker = Iterable.first(c.trackers)!;
const expectedArgs: unknown[][] = [];
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
@ -776,7 +775,7 @@ suite('ExtHost Testing', () => {
const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt'));
test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0));
single.root.children.replace([test1, test2]);
const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
const task = c.createTestRun('ctrlId', single, req, 'hello world', false);
const message1 = new TestMessage('some message');
message1.location = new Location(URI.file('/a.txt'), new Position(0, 0));
@ -817,7 +816,7 @@ suite('ExtHost Testing', () => {
});
test('guards calls after runs are ended', () => {
const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
const task = c.createTestRun('ctrl', single, req, 'hello world', false);
task.end();
task.failed(single.root, new TestMessage('some message'));
@ -829,7 +828,7 @@ suite('ExtHost Testing', () => {
});
test('excludes tests outside tree or explicitly excluded', () => {
const task = c.createTestRun(ext, 'ctrlId', single, {
const task = c.createTestRun('ctrlId', single, {
profile: configuration,
include: [single.root.children.get('id-a')!],
exclude: [single.root.children.get('id-a')!.children.get('id-aa')!],
@ -858,7 +857,7 @@ suite('ExtHost Testing', () => {
const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
testB!.children.replace([childB]);
const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false);
const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false);
const tracker = Iterable.first(c.trackers)!;
task1.passed(childA);

View file

@ -113,7 +113,6 @@ export const allApiProposals = Object.freeze({
terminalExecuteCommandEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts',
terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts',
terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts',
testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts',
testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts',
textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts',
timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts',

View file

@ -17194,6 +17194,14 @@ declare module 'vscode' {
*/
runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable<void> | void;
/**
* A function that provides detailed statement and function-level coverage for a file.
*
* The {@link FileCoverage} object passed to this function is the same instance
* emitted on {@link TestRun.addCoverage} calls associated with this profile.
*/
loadDetailedCoverage?: (testRun: TestRun, fileCoverage: FileCoverage, token: CancellationToken) => Thenable<FileCoverageDetail[]>;
/**
* Deletes the run profile.
*/
@ -17473,11 +17481,22 @@ declare module 'vscode' {
*/
appendOutput(output: string, location?: Location, test?: TestItem): void;
/**
* Adds coverage for a file in the run.
*/
addCoverage(fileCoverage: FileCoverage): void;
/**
* Signals the end of the test run. Any tests included in the run whose
* states have not been updated will have their state reset.
*/
end(): void;
/**
* An event fired when the editor is no longer interested in data
* associated with the test run.
*/
onDidDispose: Event<void>;
}
/**
@ -17687,6 +17706,177 @@ declare module 'vscode' {
constructor(message: string | MarkdownString);
}
/**
* A class that contains information about a covered resource. A count can
* be give for lines, branches, and declarations in a file.
*/
export class TestCoverageCount {
/**
* Number of items covered in the file.
*/
covered: number;
/**
* Total number of covered items in the file.
*/
total: number;
/**
* @param covered Value for {@link TestCoverageCount.covered}
* @param total Value for {@link TestCoverageCount.total}
*/
constructor(covered: number, total: number);
}
/**
* Contains coverage metadata for a file.
*/
export class FileCoverage {
/**
* File URI.
*/
readonly uri: Uri;
/**
* Statement coverage information. If the reporter does not provide statement
* coverage information, this can instead be used to represent line coverage.
*/
statementCoverage: TestCoverageCount;
/**
* Branch coverage information.
*/
branchCoverage?: TestCoverageCount;
/**
* Declaration coverage information. Depending on the reporter and
* language, this may be types such as functions, methods, or namespaces.
*/
declarationCoverage?: TestCoverageCount;
/**
* Creates a {@link FileCoverage} instance with counts filled in from
* the coverage details.
* @param uri Covered file URI
* @param detailed Detailed coverage information
*/
static fromDetails(uri: Uri, details: readonly FileCoverageDetail[]): FileCoverage;
/**
* @param uri Covered file URI
* @param statementCoverage Statement coverage information. If the reporter
* does not provide statement coverage information, this can instead be
* used to represent line coverage.
* @param branchCoverage Branch coverage information
* @param declarationCoverage Declaration coverage information
*/
constructor(
uri: Uri,
statementCoverage: TestCoverageCount,
branchCoverage?: TestCoverageCount,
declarationCoverage?: TestCoverageCount,
);
}
/**
* Contains coverage information for a single statement or line.
*/
export class StatementCoverage {
/**
* The number of times this statement was executed, or a boolean indicating
* whether it was executed if the exact count is unknown. If zero or false,
* the statement will be marked as un-covered.
*/
executed: number | boolean;
/**
* Statement location.
*/
location: Position | Range;
/**
* Coverage from branches of this line or statement. If it's not a
* conditional, this will be empty.
*/
branches: BranchCoverage[];
/**
* @param location The statement position.
* @param executed The number of times this statement was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the statement will be marked as un-covered.
* @param branches Coverage from branches of this line. If it's not a
* conditional, this should be omitted.
*/
constructor(executed: number | boolean, location: Position | Range, branches?: BranchCoverage[]);
}
/**
* Contains coverage information for a branch of a {@link StatementCoverage}.
*/
export class BranchCoverage {
/**
* The number of times this branch was executed, or a boolean indicating
* whether it was executed if the exact count is unknown. If zero or false,
* the branch will be marked as un-covered.
*/
executed: number | boolean;
/**
* Branch location.
*/
location?: Position | Range;
/**
* Label for the branch, used in the context of "the ${label} branch was
* not taken," for example.
*/
label?: string;
/**
* @param executed The number of times this branch was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the branch will be marked as un-covered.
* @param location The branch position.
*/
constructor(executed: number | boolean, location?: Position | Range, label?: string);
}
/**
* Contains coverage information for a declaration. Depending on the reporter
* and language, this may be types such as functions, methods, or namespaces.
*/
export class DeclarationCoverage {
/**
* Name of the declaration.
*/
name: string;
/**
* The number of times this declaration was executed, or a boolean
* indicating whether it was executed if the exact count is unknown. If
* zero or false, the declaration will be marked as un-covered.
*/
executed: number | boolean;
/**
* Declaration location.
*/
location: Position | Range;
/**
* @param executed The number of times this declaration was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the declaration will be marked as un-covered.
* @param location The declaration position.
*/
constructor(name: string, executed: number | boolean, location: Position | Range);
}
/**
* Coverage details returned from {@link TestRunProfile.loadDetailedCoverage}.
*/
export type FileCoverageDetail = StatementCoverage | DeclarationCoverage;
/**
* The tab represents a single text based resource.
*/

View file

@ -1,201 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/123713
export interface TestRun {
/**
* Adds coverage for a file in the run.
*/
addCoverage(fileCoverage: FileCoverage): void;
/**
* An event fired when the editor is no longer interested in data
* associated with the test run.
*/
onDidDispose: Event<void>;
}
export interface TestRunProfile {
/**
* A function that provides detailed statement and function-level coverage for a file.
*
* The {@link FileCoverage} object passed to this function is the same instance
* emitted on {@link TestRun.addCoverage} calls associated with this profile.
*/
loadDetailedCoverage?: (testRun: TestRun, fileCoverage: FileCoverage, token: CancellationToken) => Thenable<FileCoverageDetail[]>;
}
/**
* A class that contains information about a covered resource. A count can
* be give for lines, branches, and declarations in a file.
*/
export class TestCoverageCount {
/**
* Number of items covered in the file.
*/
covered: number;
/**
* Total number of covered items in the file.
*/
total: number;
/**
* @param covered Value for {@link TestCoverageCount.covered}
* @param total Value for {@link TestCoverageCount.total}
*/
constructor(covered: number, total: number);
}
/**
* Contains coverage metadata for a file.
*/
export class FileCoverage {
/**
* File URI.
*/
readonly uri: Uri;
/**
* Statement coverage information. If the reporter does not provide statement
* coverage information, this can instead be used to represent line coverage.
*/
statementCoverage: TestCoverageCount;
/**
* Branch coverage information.
*/
branchCoverage?: TestCoverageCount;
/**
* Declaration coverage information. Depending on the reporter and
* language, this may be types such as functions, methods, or namespaces.
*/
declarationCoverage?: TestCoverageCount;
/**
* Creates a {@link FileCoverage} instance with counts filled in from
* the coverage details.
* @param uri Covered file URI
* @param detailed Detailed coverage information
*/
static fromDetails(uri: Uri, details: readonly FileCoverageDetail[]): FileCoverage;
/**
* @param uri Covered file URI
* @param statementCoverage Statement coverage information. If the reporter
* does not provide statement coverage information, this can instead be
* used to represent line coverage.
* @param branchCoverage Branch coverage information
* @param declarationCoverage Declaration coverage information
*/
constructor(
uri: Uri,
statementCoverage: TestCoverageCount,
branchCoverage?: TestCoverageCount,
declarationCoverage?: TestCoverageCount,
);
}
/**
* Contains coverage information for a single statement or line.
*/
export class StatementCoverage {
/**
* The number of times this statement was executed, or a boolean indicating
* whether it was executed if the exact count is unknown. If zero or false,
* the statement will be marked as un-covered.
*/
executed: number | boolean;
/**
* Statement location.
*/
location: Position | Range;
/**
* Coverage from branches of this line or statement. If it's not a
* conditional, this will be empty.
*/
branches: BranchCoverage[];
/**
* @param location The statement position.
* @param executed The number of times this statement was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the statement will be marked as un-covered.
* @param branches Coverage from branches of this line. If it's not a
* conditional, this should be omitted.
*/
constructor(executed: number | boolean, location: Position | Range, branches?: BranchCoverage[]);
}
/**
* Contains coverage information for a branch of a {@link StatementCoverage}.
*/
export class BranchCoverage {
/**
* The number of times this branch was executed, or a boolean indicating
* whether it was executed if the exact count is unknown. If zero or false,
* the branch will be marked as un-covered.
*/
executed: number | boolean;
/**
* Branch location.
*/
location?: Position | Range;
/**
* Label for the branch, used in the context of "the ${label} branch was
* not taken," for example.
*/
label?: string;
/**
* @param executed The number of times this branch was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the branch will be marked as un-covered.
* @param location The branch position.
*/
constructor(executed: number | boolean, location?: Position | Range, label?: string);
}
/**
* Contains coverage information for a declaration. Depending on the reporter
* and language, this may be types such as functions, methods, or namespaces.
*/
export class DeclarationCoverage {
/**
* Name of the declaration.
*/
name: string;
/**
* The number of times this declaration was executed, or a boolean
* indicating whether it was executed if the exact count is unknown. If
* zero or false, the declaration will be marked as un-covered.
*/
executed: number | boolean;
/**
* Declaration location.
*/
location: Position | Range;
/**
* @param executed The number of times this declaration was executed, or a
* boolean indicating whether it was executed if the exact count is
* unknown. If zero or false, the declaration will be marked as un-covered.
* @param location The declaration position.
*/
constructor(name: string, executed: number | boolean, location: Position | Range);
}
export type FileCoverageDetail = StatementCoverage | DeclarationCoverage;
}