diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 5f5a2afc39e..706feac6e35 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; @@ -310,11 +310,29 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh diff.map(d => TestsDiffOp.deserialize(this.uriIdentityService, d))); } + /** + * @inheritdoc + */ public async $runTests(req: ResolvedTestRunRequest, token: CancellationToken): Promise { const result = await this.testService.runResolvedTests(req, token); return result.id; } + /** + * @inheritdoc + */ + public async $getCoverageDetails(resultId: string, taskIndex: number, uri: UriComponents, token: CancellationToken): Promise { + const details = await this.resultService.getResult(resultId) + ?.tasks[taskIndex] + ?.coverage.get() + ?.getUri(URI.from(uri)) + ?.details(token); + + // Return empty if nothing. Some failure is always possible here because + // results might be cleared in the meantime. + return details || []; + } + public override dispose() { super.dispose(); for (const subscription of this.testProviderRegistrations.values()) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7e7e3c5930..04d0de99551 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2815,6 +2815,8 @@ export interface MainThreadTestingShape { $unsubscribeFromDiffs(): void; /** Publishes that new tests were available on the given source. */ $publishDiff(controllerId: string, diff: TestsDiffOp.Serialized[]): void; + /** Gets coverage details from a test result. */ + $getCoverageDetails(resultId: string, taskIndex: number, uri: UriComponents, token: CancellationToken): Promise; // --- test run configurations: diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index d269b34e120..456816da246 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -399,6 +399,12 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { results .map(r => { const o = Convert.TestResults.to(r); + const taskWithCoverage = r.tasks.findIndex(t => t.hasCoverage); + if (taskWithCoverage !== -1) { + o.getDetailedCoverage = (uri, token = CancellationToken.None) => + this.proxy.$getCoverageDetails(r.id, taskWithCoverage, uri, token).then(r => r.map(Convert.TestCoverage.to)); + } + testResultInternalIDs.set(o, r.id); return o; }) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ff62b8cf5b3..ede0cd5a525 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2041,6 +2041,43 @@ export namespace TestCoverage { return 'line' in location ? Position.from(location) : Range.from(location); } + function toLocation(location: IPosition | editorRange.IRange): types.Position | types.Range; + function toLocation(location: IPosition | editorRange.IRange | undefined): types.Position | types.Range | undefined; + function toLocation(location: IPosition | editorRange.IRange | undefined): types.Position | types.Range | undefined { + if (!location) { return undefined; } + return 'endLineNumber' in location ? Range.to(location) : Position.to(location); + } + + export function to(serialized: CoverageDetails.Serialized): vscode.FileCoverageDetail { + if (serialized.type === DetailType.Statement) { + const branches: vscode.BranchCoverage[] = []; + if (serialized.branches) { + for (const branch of serialized.branches) { + branches.push({ + executed: branch.count, + location: toLocation(branch.location), + label: branch.label + }); + } + } + return new types.StatementCoverage( + serialized.count, + toLocation(serialized.location), + serialized.branches?.map(b => new types.BranchCoverage( + b.count, + toLocation(b.location)!, + b.label, + )) + ); + } else { + return new types.DeclarationCoverage( + serialized.name, + serialized.count, + toLocation(serialized.location), + ); + } + } + export function fromDetails(coverage: vscode.FileCoverageDetail): CoverageDetails.Serialized { if (typeof coverage.executed === 'number' && coverage.executed < 0) { throw new Error(`Invalid coverage count ${coverage.executed}`); diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index e9cbaca87f5..6f624060dea 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -607,7 +607,7 @@ export class LiveTestResult extends Disposable implements ITestResult { private readonly doSerialize = new Lazy((): ISerializedTestResults => ({ id: this.id, completedAt: this.completedAt!, - tasks: this.tasks.map(t => ({ id: t.id, name: t.name, ctrlId: t.ctrlId })), + tasks: this.tasks.map(t => ({ id: t.id, name: t.name, ctrlId: t.ctrlId, hasCoverage: !!t.coverage.get() })), name: this.name, request: this.request, items: [...this.testById.values()].map(TestResultItem.serializeWithoutMessages), @@ -616,7 +616,7 @@ export class LiveTestResult extends Disposable implements ITestResult { private readonly doSerializeWithMessages = new Lazy((): ISerializedTestResults => ({ id: this.id, completedAt: this.completedAt!, - tasks: this.tasks.map(t => ({ id: t.id, name: t.name, ctrlId: t.ctrlId })), + tasks: this.tasks.map(t => ({ id: t.id, name: t.name, ctrlId: t.ctrlId, hasCoverage: !!t.coverage.get() })), name: this.name, request: this.request, items: [...this.testById.values()].map(TestResultItem.serialize), diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 380bdc00587..42241e66187 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -585,7 +585,7 @@ export interface ISerializedTestResults { /** Subset of test result items */ items: TestResultItem.Serialized[]; /** Tasks involved in the run. */ - tasks: { id: string; name: string | undefined; ctrlId: string }[]; + tasks: { id: string; name: string | undefined; ctrlId: string; hasCoverage: boolean }[]; /** Human-readable name of the test run. */ name: string; /** Test trigger informaton */ diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index fdc20713b09..49b196b8c8f 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -293,7 +293,7 @@ suite('Workbench - Test Results Service', () => { } as IUriIdentityService, { completedAt, id: 'some-id', - tasks: [{ id: 't', name: undefined, ctrlId: 'ctrl' }], + tasks: [{ id: 't', name: undefined, ctrlId: 'ctrl', hasCoverage: false }], name: 'hello world', request: defaultOpts([]), items: [{ diff --git a/src/vscode-dts/vscode.proposed.testObserver.d.ts b/src/vscode-dts/vscode.proposed.testObserver.d.ts index cb8091c887e..910a61428f7 100644 --- a/src/vscode-dts/vscode.proposed.testObserver.d.ts +++ b/src/vscode-dts/vscode.proposed.testObserver.d.ts @@ -102,6 +102,12 @@ declare module 'vscode' { * were passed in the {@link tests.runTests} method. */ readonly results: ReadonlyArray>; + + /** + * Gets coverage information for a URI. This function is available only + * when a test run reported coverage. + */ + getDetailedCoverage?(uri: Uri, token?: CancellationToken): Thenable; } /**