diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 66fb28ae318..78151f0b8e2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2074,13 +2074,31 @@ declare module 'vscode' { * Returns an observer that retrieves tests in the given text document. */ export function createDocumentTestObserver(document: TextDocument): TestObserver; + + /** + * The last or selected test run. Cleared when a new test run starts. + */ + export const testResults: TestResults | undefined; + + /** + * Event that fires when the testResults are updated. + */ + export const onDidChangeTestResults: Event; + } + + export interface TestResults { + /** + * The results from the latest test run. The array contains a snapshot of + * all tests involved in the run at the moment when it completed. + */ + readonly tests: ReadonlyArray | undefined; } export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. */ - readonly tests: ReadonlyArray; + readonly tests: ReadonlyArray; /** * An event that fires when an existing test in the collection changes, or @@ -2110,23 +2128,23 @@ declare module 'vscode' { /** * List of all tests that are newly added. */ - readonly added: ReadonlyArray; + readonly added: ReadonlyArray; /** * List of existing tests that have updated. */ - readonly updated: ReadonlyArray; + readonly updated: ReadonlyArray; /** * List of existing tests that have been removed. */ - readonly removed: ReadonlyArray; + readonly removed: ReadonlyArray; /** * Highest node in the test tree under which changes were made. This can * be easily plugged into events like the TreeDataProvider update event. */ - readonly commonChangeAncestor: TestItem | null; + readonly commonChangeAncestor: RequiredTestItem | null; } /** @@ -2232,6 +2250,16 @@ declare module 'vscode' { */ label: string; + /** + * Optional unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of a test item. + * + * If the ID is not provided, it defaults to the concatenation of the + * item's label and its parent's ID, if any. + */ + readonly id?: string; + /** * Optional description that appears next to the label. */ @@ -2268,6 +2296,15 @@ declare module 'vscode' { state: TestState; } + /** + * A {@link TestItem} with its defaults filled in. + */ + export type RequiredTestItem = { + [K in keyof Required]: K extends 'children' + ? RequiredTestItem[] + : (K extends 'description' | 'location' ? TestItem[K] : Required[K]) + }; + export enum TestRunState { // Initial state Unset = 0, diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 95fc3087629..370f08df900 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -36,13 +37,25 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh constructor( extHostContext: IExtHostContext, @ITestService private readonly testService: ITestService, + @ITestResultService resultService: ITestResultService, ) { super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); + + const testCompleteListener = this._register(new MutableDisposable()); + this._register(resultService.onNewTestResult(results => { + testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests })); + })); + testService.updateRootProviderCount(1); + const lastCompleted = resultService.results.find(r => !r.isComplete); + if (lastCompleted) { + this.proxy.$publishTestResults({ tests: lastCompleted.tests }); + } + for (const { resource, uri } of this.testService.subscriptions) { this.proxy.$subscribeToTests(resource, uri); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2e246091426..1907f2993ad 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -340,6 +340,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, + get onDidChangeTestResults() { + checkProposedApiEnabled(extension); + return extHostTesting.onLastResultsChanged; + }, + get testResults() { + checkProposedApiEnabled(extension); + return extHostTesting.lastResults; + }, }; // namespace: extensions diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 16e194fc063..0f2d3d55700 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -58,7 +58,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface IEnvironment { @@ -1843,6 +1843,7 @@ export interface ExtHostTestingShape { $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; $lookupTest(test: TestIdWithProvider): Promise; $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; + $publishTestResults(results: InternalTestResults): void; } export interface MainThreadTestingShape { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 6e51482cf5c..fb30845668e 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -17,15 +17,16 @@ import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; -import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { + private readonly resultsChangedEmitter = new Emitter(); private readonly providers = new Map(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); @@ -38,6 +39,9 @@ export class ExtHostTesting implements ExtHostTestingShape { private workspaceObservers: WorkspaceFolderTestObserverFactory; private textDocumentObservers: TextDocumentTestObserverFactory; + public onLastResultsChanged = this.resultsChangedEmitter.event; + public lastResults?: vscode.TestResults; + constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); @@ -98,6 +102,19 @@ export class ExtHostTesting implements ExtHostTestingShape { }, token); } + + /** + * Updates test results shown to extensions. + * @override + */ + public $publishTestResults(results: InternalTestResults): void { + const convert = (item: InternalTestItemWithChildren): vscode.RequiredTestItem => + ({ ...TestItem.toShallow(item.item), children: item.children.map(convert) }); + + this.lastResults = { tests: results.tests.map(convert) }; + this.resultsChangedEmitter.fire(); + } + /** * Handles a request to read tests for a file, or workspace. * @override @@ -398,7 +415,7 @@ export class TestItemFilteredWrapper implements vscode.TestItem { interface MirroredCollectionTestItem extends IncrementalTestCollectionItem { revived: vscode.TestItem; depth: number; - wrapped?: vscode.TestItem; + wrapped?: vscode.RequiredTestItem; } class MirroredChangeCollector extends IncrementalChangeCollector { @@ -427,7 +444,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector): vscode.TestItem[] { - let output: vscode.TestItem[] = []; + public getAllAsTestItem(itemIds: Iterable): vscode.RequiredTestItem[] { + let output: vscode.RequiredTestItem[] = []; for (const itemId of itemIds) { const item = this.items.get(itemId); if (item) { @@ -593,7 +610,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection { const MirroredItemId = Symbol('MirroredItemId'); -class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { +class TestItemFromMirror implements vscode.RequiredTestItem { readonly #internal: MirroredCollectionTestItem; readonly #collection: MirroredTestCollection; + public get id() { return this.#internal.revived.id!; } public get label() { return this.#internal.revived.label; } public get description() { return this.#internal.revived.description; } public get state() { return this.#internal.revived.state; } @@ -643,14 +661,15 @@ class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { } public toJSON() { - const serialized: RequiredTestItem & TestIdWithProvider = { + const serialized: vscode.RequiredTestItem & TestIdWithProvider = { + id: this.id, label: this.label, description: this.description, state: this.state, location: this.location, runnable: this.runnable, debuggable: this.debuggable, - children: this.children.map(c => (c as ExtHostTestItem).toJSON()), + children: this.children.map(c => (c as TestItemFromMirror).toJSON()), providerId: this.#internal.providerId, testId: this.#internal.id, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 526193c85d1..d1512e59b87 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1412,8 +1412,9 @@ export namespace TestState { export namespace TestItem { - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem, parentExtId?: string): ITestItem { return { + extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label), label: item.label, location: item.location ? location.from(item.location) : undefined, debuggable: item.debuggable ?? false, @@ -1423,8 +1424,9 @@ export namespace TestItem { }; } - export function to(item: ITestItem): vscode.TestItem { + export function toShallow(item: ITestItem): Omit { return { + id: item.extId, label: item.label, location: item.location && location.to({ range: item.location.range, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 3e754de6dbe..d07624eac74 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2978,15 +2978,7 @@ export class TestState { } } -type AllowedUndefined = 'description' | 'location'; - -/** - * Test item without any optional properties. Only some properties are - * permitted to be undefined, but they must still exist. - */ -export type RequiredTestItem = { - [K in keyof Required]: K extends AllowedUndefined ? vscode.TestItem[K] : Required[K] -}; +export type RequiredTestItem = vscode.RequiredTestItem; export type TestItem = vscode.TestItem; diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index c989796a67c..41f4c891fc0 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -126,12 +126,13 @@ export class SingleUseTestCollection implements IDisposable { private addItem(actual: ApiTestItem, providerId: string, parent: string | null) { let internal = this.testItemToInternal.get(actual); + const parentItem = parent ? this.testIdToInternal.get(parent) : null; if (!internal) { internal = { actual, id: this.getId(), parent, - item: TestItem.from(actual), + item: TestItem.from(actual, parentItem?.item.extId), providerId, previousChildren: new Set(), previousEquals: itemEqualityComparator(actual), @@ -141,7 +142,7 @@ export class SingleUseTestCollection implements IDisposable { this.testIdToInternal.set(internal.id, internal); this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); } else if (!internal.previousEquals(actual)) { - internal.item = TestItem.from(actual); + internal.item = TestItem.from(actual, parentItem?.item.extId); internal.previousEquals = itemEqualityComparator(actual); this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); } @@ -200,6 +201,7 @@ export class SingleUseTestCollection implements IDisposable { } const keyMap: { [K in keyof Omit]: null } = { + id: null, label: null, location: null, state: null, diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index cee17ec7668..566146dc027 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -62,6 +62,8 @@ export interface ITestState { * The TestItem from .d.ts, as a plain object without children. */ export interface ITestItem { + /** ID of the test given by the test provider */ + extId: string; label: string; children?: never; location: ModeLocation | undefined; @@ -81,6 +83,14 @@ export interface InternalTestItem { item: ITestItem; } +export interface InternalTestItemWithChildren extends InternalTestItem { + children: InternalTestItemWithChildren[]; +} + +export interface InternalTestResults { + tests: InternalTestItemWithChildren[]; +} + export const enum TestDiffOpType { /** Adds a new test (with children) */ Add, diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index d2235b4bcfc..992081d3a77 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; -import { IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IncrementalTestCollectionItem, InternalTestItemWithChildren, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; @@ -50,9 +50,7 @@ const makeNode = ( return mapped; }; -export interface TestResultItem extends InternalTestItem { - children: TestResultItem[] -} +export interface TestResultItem extends InternalTestItemWithChildren { } /** * Results of a test. These are created when the test initially started running diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 11838dab2a2..1be5d1bc114 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -17,6 +17,7 @@ import { Range } from 'vs/editor/common/core/range'; const simplify = (item: TestItem) => { if ('toJSON' in item) { item = (item as any).toJSON(); + delete (item as any).id; delete (item as any).providerId; delete (item as any).testId; } @@ -70,10 +71,10 @@ suite('ExtHost Testing', () => { single.addRoot(tests, 'pid'); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { id: '0', providerId: 'pid', parent: null, item: convert.TestItem.from(stubTest('root')) }], - [TestDiffOpType.Add, { id: '1', providerId: 'pid', parent: '0', item: convert.TestItem.from(stubTest('a')) }], - [TestDiffOpType.Add, { id: '2', providerId: 'pid', parent: '1', item: convert.TestItem.from(stubTest('aa')) }], - [TestDiffOpType.Add, { id: '3', providerId: 'pid', parent: '1', item: convert.TestItem.from(stubTest('ab')) }], - [TestDiffOpType.Add, { id: '4', providerId: 'pid', parent: '0', item: convert.TestItem.from(stubTest('b')) }], + [TestDiffOpType.Add, { id: '1', providerId: 'pid', parent: '0', item: convert.TestItem.from(stubTest('a'), 'root') }], + [TestDiffOpType.Add, { id: '2', providerId: 'pid', parent: '1', item: convert.TestItem.from(stubTest('aa'), 'root\0a') }], + [TestDiffOpType.Add, { id: '3', providerId: 'pid', parent: '1', item: convert.TestItem.from(stubTest('ab'), 'root\0a') }], + [TestDiffOpType.Add, { id: '4', providerId: 'pid', parent: '0', item: convert.TestItem.from(stubTest('b'), 'root') }], ]); }); @@ -91,7 +92,7 @@ suite('ExtHost Testing', () => { tests.children![0].description = 'Hello world'; /* item a */ single.onItemChange(tests, 'pid'); assert.deepStrictEqual(single.collectDiff(), [ - [TestDiffOpType.Update, { id: '1', parent: '0', providerId: 'pid', item: convert.TestItem.from({ ...stubTest('a'), description: 'Hello world' }) }], + [TestDiffOpType.Update, { id: '1', parent: '0', providerId: 'pid', item: convert.TestItem.from({ ...stubTest('a'), description: 'Hello world' }, 'root') }], ]); single.onItemChange(tests, 'pid'); @@ -121,7 +122,7 @@ suite('ExtHost Testing', () => { single.onItemChange(tests, 'pid'); assert.deepStrictEqual(single.collectDiff(), [ - [TestDiffOpType.Add, { id: '5', providerId: 'pid', parent: '1', item: convert.TestItem.from(child) }], + [TestDiffOpType.Add, { id: '5', providerId: 'pid', parent: '1', item: convert.TestItem.from(child, 'root\0a') }], ]); assert.deepStrictEqual([...owned.idToInternal.keys()].sort(), ['0', '1', '2', '3', '4', '5']); assert.strictEqual(single.itemToInternal.size, 6);