mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 03:54:24 +01:00
testing: initial api side of test coverage
For https://github.com/microsoft/vscode/issues/123713
This commit is contained in:
@@ -11,7 +11,9 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
|
||||
import { ExtensionRunTestsRequest, ITestItem, ITestMessage, ITestRunTask, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
|
||||
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
@@ -78,6 +80,23 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
||||
this.withLiveRun(runId, r => r.addTestChainToRun(controllerId, tests));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
$signalCoverageAvailable(runId: string, taskId: string): void {
|
||||
this.withLiveRun(runId, run => {
|
||||
const task = run.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
(task.coverage as MutableObservableValue<TestCoverage>).value = new TestCoverage({
|
||||
provideFileCoverage: token => this.proxy.$provideFileCoverage(runId, taskId, token),
|
||||
resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -1272,6 +1272,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
TestMessage: extHostTypes.TestMessage,
|
||||
TextSearchCompleteMessageType: TextSearchCompleteMessageType,
|
||||
TestMessageSeverity: extHostTypes.TestMessageSeverity,
|
||||
CoveredCount: extHostTypes.CoveredCount,
|
||||
FileCoverage: extHostTypes.FileCoverage,
|
||||
StatementCoverage: extHostTypes.StatementCoverage,
|
||||
BranchCoverage: extHostTypes.BranchCoverage,
|
||||
FunctionCoverage: extHostTypes.FunctionCoverage,
|
||||
WorkspaceTrustState: extHostTypes.WorkspaceTrustState
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
|
||||
import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
|
||||
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForControllerRequest, RunTestsRequest, ITestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForControllerRequest, RunTestsRequest, ITestIdWithSrc, TestsDiff, IFileCoverage, CoverageDetails } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { ActivationKind, ExtensionHostKind, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier';
|
||||
@@ -2064,14 +2064,19 @@ export const enum ExtHostTestingResource {
|
||||
export interface ExtHostTestingShape {
|
||||
$runControllerTests(req: RunTestForControllerRequest, token: CancellationToken): Promise<void>;
|
||||
$cancelExtensionTestRun(runId: string | undefined): void;
|
||||
|
||||
/** Handles a diff of tests, as a result of a subscribeToDiffs() call */
|
||||
$acceptDiff(diff: TestsDiff): void;
|
||||
|
||||
/** Publishes that a test run finished. */
|
||||
$publishTestResults(results: ISerializedTestResults[]): void;
|
||||
/** Expands a test item's children, by the given number of levels. */
|
||||
$expandTest(src: ITestIdWithSrc, levels: number): Promise<void>;
|
||||
/** Requests file coverage for a test run. Errors if not available. */
|
||||
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<IFileCoverage[]>;
|
||||
/**
|
||||
* Requests coverage details for the file index in coverage data for the run.
|
||||
* Requires file coverage to have been previously requested via $provideFileCoverage.
|
||||
*/
|
||||
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]>;
|
||||
}
|
||||
|
||||
export interface MainThreadTestingShape {
|
||||
@@ -2101,6 +2106,8 @@ export interface MainThreadTestingShape {
|
||||
$appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void;
|
||||
/** Appends raw output to the test run.. */
|
||||
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer): void;
|
||||
/** Triggered when coverage is added to test results. */
|
||||
$signalCoverageAvailable(runId: string, taskId: string): void;
|
||||
/** Signals a task in a test run started. */
|
||||
$startedTestRunTask(runId: string, task: ITestRunTask): void;
|
||||
/** Signals a task in a test run ended. */
|
||||
|
||||
@@ -21,7 +21,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||
import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { TestItemImpl } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
|
||||
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestIdWithSrc, ITestItem, RunTestForControllerRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestIdWithSrc, ITestItem, RunTestForControllerRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
export class ExtHostTesting implements ExtHostTestingShape {
|
||||
@@ -121,6 +121,20 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
}, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<IFileCoverage[]> {
|
||||
return Iterable.find(this.runTracker.trackers, t => t.id === runId)?.getCoverage(taskId)?.provideFileCoverage(token) ?? Promise.resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]> {
|
||||
return Iterable.find(this.runTracker.trackers, t => t.id === runId)?.getCoverage(taskId)?.resolveFileCoverage(fileIndex, token) ?? Promise.resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates test results shown to extensions.
|
||||
* @override
|
||||
@@ -224,7 +238,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
}
|
||||
|
||||
class TestRunTracker extends Disposable {
|
||||
private readonly task = new Set<TestRunImpl>();
|
||||
private readonly tasks = new Map</* task ID */string, { run: TestRunImpl, coverage: TestRunCoverageBearer }>();
|
||||
private readonly sharedTestIds = new Set<string>();
|
||||
private readonly cts: CancellationTokenSource;
|
||||
private readonly endEmitter = this._register(new Emitter<void>());
|
||||
@@ -239,7 +253,7 @@ class TestRunTracker extends Disposable {
|
||||
* Gets whether there are any tests running.
|
||||
*/
|
||||
public get isRunning() {
|
||||
return this.task.size > 0;
|
||||
return this.tasks.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,21 +267,27 @@ class TestRunTracker extends Disposable {
|
||||
super();
|
||||
this.cts = this._register(new CancellationTokenSource(parentToken));
|
||||
this._register(this.cts.token.onCancellationRequested(() => {
|
||||
for (const task of this.task) {
|
||||
task.end();
|
||||
for (const { run } of this.tasks.values()) {
|
||||
run.end();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public getCoverage(taskId: string) {
|
||||
return this.tasks.get(taskId)?.coverage;
|
||||
}
|
||||
|
||||
public createRun(name: string | undefined) {
|
||||
const run = new TestRunImpl(name, this.cts.token, this.dto, this.sharedTestIds, this.proxy, () => {
|
||||
this.task.delete(run);
|
||||
const taskId = generateUuid();
|
||||
const coverage = new TestRunCoverageBearer(this.proxy, this.dto.id, taskId);
|
||||
const run = new TestRunImpl(name, this.cts.token, taskId, coverage, this.dto, this.sharedTestIds, this.proxy, () => {
|
||||
this.tasks.delete(run.taskId);
|
||||
if (!this.isRunning) {
|
||||
this.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
this.task.add(run);
|
||||
this.tasks.set(run.taskId, { run, coverage });
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -397,17 +417,88 @@ export class TestRunDto {
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunCoverageBearer {
|
||||
private _coverageProvider?: vscode.TestCoverageProvider;
|
||||
private fileCoverage?: Promise<vscode.FileCoverage[] | null | undefined>;
|
||||
|
||||
public set coverageProvider(provider: vscode.TestCoverageProvider | undefined) {
|
||||
if (this._coverageProvider) {
|
||||
throw new Error('The TestCoverageProvider cannot be replaced after being provided');
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._coverageProvider = provider;
|
||||
this.proxy.$signalCoverageAvailable(this.runId, this.taskId);
|
||||
}
|
||||
|
||||
public get coverageProvider() {
|
||||
return this._coverageProvider;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly proxy: MainThreadTestingShape,
|
||||
private readonly runId: string,
|
||||
private readonly taskId: string,
|
||||
) {
|
||||
}
|
||||
|
||||
public async provideFileCoverage(token: CancellationToken): Promise<IFileCoverage[]> {
|
||||
if (!this._coverageProvider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.fileCoverage) {
|
||||
this.fileCoverage = (async () => this._coverageProvider!.provideFileCoverage(token))();
|
||||
}
|
||||
|
||||
try {
|
||||
const coverage = await this.fileCoverage;
|
||||
return coverage?.map(Convert.TestCoverage.fromFile) ?? [];
|
||||
} catch (e) {
|
||||
this.fileCoverage = undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async resolveFileCoverage(index: number, token: CancellationToken): Promise<CoverageDetails[]> {
|
||||
const fileCoverage = await this.fileCoverage;
|
||||
let file = fileCoverage?.[index];
|
||||
if (!this._coverageProvider || !fileCoverage || !file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!file.detailedCoverage) {
|
||||
file = fileCoverage[index] = await this._coverageProvider.resolveFileCoverage?.(file, token) ?? file;
|
||||
}
|
||||
|
||||
return file.detailedCoverage?.map(Convert.TestCoverage.fromDetailed) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunImpl implements vscode.TestRun {
|
||||
readonly #proxy: MainThreadTestingShape;
|
||||
readonly #req: TestRunDto;
|
||||
readonly #sharedIds: Set<string>;
|
||||
readonly #onEnd: () => void;
|
||||
readonly #coverage: TestRunCoverageBearer;
|
||||
#ended = false;
|
||||
public readonly taskId = generateUuid();
|
||||
|
||||
public set coverageProvider(provider: vscode.TestCoverageProvider | undefined) {
|
||||
this.#coverage.coverageProvider = provider;
|
||||
}
|
||||
|
||||
public get coverageProvider() {
|
||||
return this.#coverage.coverageProvider;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly name: string | undefined,
|
||||
public readonly token: CancellationToken,
|
||||
public readonly taskId: string,
|
||||
coverage: TestRunCoverageBearer,
|
||||
dto: TestRunDto,
|
||||
sharedTestIds: Set<string>,
|
||||
proxy: MainThreadTestingShape,
|
||||
@@ -416,6 +507,7 @@ class TestRunImpl implements vscode.TestRun {
|
||||
this.#onEnd = onEnd;
|
||||
this.#proxy = proxy;
|
||||
this.#req = dto;
|
||||
this.#coverage = coverage;
|
||||
this.#sharedIds = sharedTestIds;
|
||||
proxy.$startedTestRunTask(dto.id, { id: this.taskId, name, running: true });
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor';
|
||||
import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
|
||||
import * as search from 'vs/workbench/contrib/search/common/search';
|
||||
import { ISerializedTestResults, ITestItem, ITestItemContext, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestItem, ITestItemContext, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import type * as vscode from 'vscode';
|
||||
import * as types from './extHostTypes';
|
||||
@@ -1728,6 +1728,45 @@ export namespace TestResults {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TestCoverage {
|
||||
function fromCoveredCount(count: vscode.CoveredCount): ICoveredCount {
|
||||
return { covered: count.covered, total: count.covered };
|
||||
}
|
||||
|
||||
function fromLocation(location: vscode.Range | vscode.Position) {
|
||||
return 'line' in location ? Position.from(location) : Range.from(location);
|
||||
}
|
||||
|
||||
export function fromDetailed(coverage: vscode.DetailedCoverage): CoverageDetails {
|
||||
if ('branches' in coverage) {
|
||||
return {
|
||||
count: coverage.executionCount,
|
||||
location: fromLocation(coverage.location),
|
||||
type: DetailType.Statement,
|
||||
branches: coverage.branches.length
|
||||
? coverage.branches.map(b => ({ count: b.executionCount, location: b.location && fromLocation(b.location) }))
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: DetailType.Function,
|
||||
count: coverage.executionCount,
|
||||
location: fromLocation(coverage.location),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function fromFile(coverage: vscode.FileCoverage): IFileCoverage {
|
||||
return {
|
||||
uri: coverage.uri,
|
||||
statement: fromCoveredCount(coverage.statementCoverage),
|
||||
branch: coverage.branchCoverage && fromCoveredCount(coverage.branchCoverage),
|
||||
function: coverage.functionCoverage && fromCoveredCount(coverage.functionCoverage),
|
||||
details: coverage.detailedCoverage?.map(fromDetailed),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CodeActionTriggerKind {
|
||||
|
||||
export function to(value: modes.CodeActionTriggerType): types.CodeActionTriggerKind {
|
||||
|
||||
@@ -3419,7 +3419,7 @@ export class TestItemImpl implements vscode.TestItem {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@es5ClassCompat
|
||||
export class TestMessage implements vscode.TestMessage {
|
||||
public severity = TestMessageSeverity.Error;
|
||||
public expectedOutput?: string;
|
||||
@@ -3437,6 +3437,82 @@ export class TestMessage implements vscode.TestMessage {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Test Coverage
|
||||
@es5ClassCompat
|
||||
export class CoveredCount implements vscode.CoveredCount {
|
||||
constructor(public covered: number, public total: number) { }
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class FileCoverage implements vscode.FileCoverage {
|
||||
public static fromDetails(uri: vscode.Uri, details: vscode.DetailedCoverage[]): vscode.FileCoverage {
|
||||
const statements = new CoveredCount(0, 0);
|
||||
const branches = new CoveredCount(0, 0);
|
||||
const fn = new CoveredCount(0, 0);
|
||||
|
||||
for (const detail of details) {
|
||||
if ('branches' in detail) {
|
||||
statements.total += 1;
|
||||
statements.covered += detail.executionCount > 0 ? 1 : 0;
|
||||
|
||||
for (const branch of detail.branches) {
|
||||
branches.total += 1;
|
||||
branches.covered += branch.executionCount > 0 ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
fn.total += 1;
|
||||
fn.covered += detail.executionCount > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
const coverage = new FileCoverage(
|
||||
uri,
|
||||
statements,
|
||||
branches.total > 0 ? branches : undefined,
|
||||
fn.total > 0 ? fn : undefined,
|
||||
);
|
||||
|
||||
coverage.detailedCoverage = details;
|
||||
|
||||
return coverage;
|
||||
}
|
||||
|
||||
detailedCoverage?: vscode.DetailedCoverage[];
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
public statementCoverage: vscode.CoveredCount,
|
||||
public branchCoverage?: vscode.CoveredCount,
|
||||
public functionCoverage?: vscode.CoveredCount,
|
||||
) { }
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class StatementCoverage implements vscode.StatementCoverage {
|
||||
constructor(
|
||||
public executionCount: number,
|
||||
public location: Position | Range,
|
||||
public branches: vscode.BranchCoverage[] = [],
|
||||
) { }
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class BranchCoverage implements vscode.BranchCoverage {
|
||||
constructor(
|
||||
public executionCount: number,
|
||||
public location: Position | Range,
|
||||
) { }
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class FunctionCoverage implements vscode.FunctionCoverage {
|
||||
constructor(
|
||||
public executionCount: number,
|
||||
public location: Position | Range,
|
||||
) { }
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export enum ExternalUriOpenerPriority {
|
||||
None = 0,
|
||||
Option = 1,
|
||||
|
||||
Reference in New Issue
Block a user