From 186e565ec0eaee72c980ffe546ec3e4cb3ed1635 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 17 Jun 2021 12:17:55 -0700 Subject: [PATCH] refactor: update to new testing API Previously in the testing API, you called `registerTestProvider` with your own instance of a TestController, and VS Code would request workspace or document tests. This has been changed: now, you call `createTestController`, which returns an object, and call `createTestItem` to insert test nodes under the `controller.root`. Extensions should generally decide themselves when to publish tests. For example, when a file is opened in an editor, test extensions will want to make sure tests for that file are available so that inline decorations can be shown. This is pretty similar to what the editor API does in diagnostics. There is still a `resolveChildrenHandler` on the controller (rather than the TestItem directly), which you should _set_ if the test extension supports lazy discovery. Additionally, if you support running tests, you'll also want a `runHandler` (migrating from the old `runTests` method). Some of the existing test providers have been updated, you can check them out here: - https://github.com/microsoft/vscode-extension-samples/tree/main/test-provider-sample - https://github.com/microsoft/vscode-selfhost-test-provider In summary, to update to the new API: - Call `vscode.test.createTestController` instead of `registerTestController` - Move the contents of your `runTests` method to `controller.runHandler` - Move your `TestItem.resolveHandler` to `controller.resolveChildrenHandler`, which may involve adding some `instanceof` checks. - If you lazily discovered tests in `createDocumentTestRoot`, you'll want to trigger that logic based on `vscode.workspace.onDidOpenTextDocument`. - If your test runner can deal with showing locations of unsaved changes, listen for `vscode.workspace.onDidChangeTextDocument` to trigger those changes in the tree. --- src/vs/vscode.proposed.d.ts | 228 +++--- .../api/browser/mainThreadTesting.ts | 50 +- .../workbench/api/common/extHost.api.impl.ts | 27 +- .../workbench/api/common/extHost.protocol.ts | 33 +- src/vs/workbench/api/common/extHostTesting.ts | 714 +++--------------- .../api/common/extHostTestingPrivateApi.ts | 1 - .../api/common/extHostTypeConverters.ts | 3 +- src/vs/workbench/api/common/extHostTypes.ts | 60 +- .../hierarchalByLocation.ts | 204 ++--- .../explorerProjections/hierarchalByName.ts | 29 +- .../explorerProjections/hierarchalNodes.ts | 13 +- .../browser/explorerProjections/index.ts | 87 +-- .../browser/explorerProjections/nodeHelper.ts | 4 +- .../testing/browser/testExplorerActions.ts | 150 ++-- .../testing/browser/testing.contribution.ts | 16 +- .../testing/browser/testingDecorations.ts | 88 +-- .../testing/browser/testingExplorerView.ts | 130 +--- .../testing/browser/testingOutputPeek.ts | 67 +- .../common/mainThreadTestCollection.ts | 158 ++++ .../testing/common/ownedTestCollection.ts | 150 ++-- .../contrib/testing/common/testCollection.ts | 26 +- .../contrib/testing/common/testResult.ts | 13 +- .../testing/common/testResultService.ts | 4 +- .../contrib/testing/common/testService.ts | 110 ++- .../contrib/testing/common/testServiceImpl.ts | 414 +--------- .../contrib/testing/common/testStubs.ts | 80 +- .../contrib/testing/common/testingAutoRun.ts | 57 +- .../common/workspaceTestCollectionService.ts | 314 -------- .../hierarchalByLocation.test.ts | 97 +-- .../hierarchalByName.test.ts | 111 +-- .../testing/test/browser/testObjectTree.ts | 55 +- .../test/common/ownedTestCollection.ts | 28 +- .../test/common/testResultService.test.ts | 35 +- .../test/common/testResultStorage.test.ts | 9 +- .../test/browser/api/extHostTesting.test.ts | 219 ++---- 35 files changed, 1092 insertions(+), 2692 deletions(-) create mode 100644 src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts delete mode 100644 src/vs/workbench/contrib/testing/common/workspaceTestCollectionService.ts diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 017f4df470a..93629a1a599 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1829,10 +1829,11 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/107467 export namespace test { /** - * Registers a controller that can discover and - * run tests in workspaces and documents. + * Creates a new test controller. + * + * @param id Identifier for the controller, must be globally unique. */ - export function registerTestController(testController: TestController): Disposable; + export function createTestController(id: string): TestController; /** * Requests that tests be run by their controller. @@ -1842,47 +1843,10 @@ declare module 'vscode' { export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; /** - * Returns an observer that retrieves tests in the given workspace folder. + * Returns an observer that watches and can request tests. * @stability experimental */ - export function createWorkspaceTestObserver(workspaceFolder: WorkspaceFolder): TestObserver; - - /** - * Returns an observer that retrieves tests in the given text document. - * @stability experimental - */ - export function createDocumentTestObserver(document: TextDocument): TestObserver; - - /** - * Creates a {@link TestRun}. This should be called by the - * {@link TestRunner} when a request is made to execute tests, and may also - * be called if a test run is detected externally. Once created, tests - * that are included in the results will be moved into the - * {@link TestResultState.Pending} state. - * - * @param request Test run request. Only tests inside the `include` may be - * modified, and tests in its `exclude` are ignored. - * @param name The human-readable name of the run. This can be used to - * disambiguate multiple sets of results in a test run. It is useful if - * tests are run across multiple platforms, for example. - * @param persist Whether the results created by the run should be - * persisted in the editor. This may be false if the results are coming from - * a file already saved externally, such as a coverage information file. - */ - export function createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; - - /** - * Creates a new managed {@link TestItem} instance. - * @param options Initial/required options for the item - * @param data Custom data to be stored in {@link TestItem.data} - */ - export function createTestItem(options: TestItemOptions, data: T): TestItem; - - /** - * Creates a new managed {@link TestItem} instance. - * @param options Initial/required options for the item - */ - export function createTestItem(options: TestItemOptions): TestItem; + export function createTestObserver(): TestObserver; /** * List of test results stored by the editor, sorted in descending @@ -1914,16 +1878,6 @@ declare module 'vscode' { */ readonly onDidChangeTest: Event; - /** - * An event that fires when all test providers have signalled that the tests - * the observer references have been discovered. Providers may continue to - * watch for changes and cause {@link onDidChangeTest} to fire as files - * change, until the observer is disposed. - * - * @todo as below - */ - readonly onDidDiscoverInitialTests: Event; - /** * Dispose of the observer, allowing the editor to eventually tell test * providers that they no longer need to update tests. @@ -1954,43 +1908,68 @@ declare module 'vscode' { /** * Interface to discover and execute tests. */ - export interface TestController { + export interface TestController { /** - * Requests that tests be provided for the given workspace. This will - * be called when tests need to be enumerated for the workspace, such as - * when the user opens the test explorer. + * Root test item. Tests in the workspace should be added as children of + * the root. The extension controls when to add these, although the + * editor may request children using the {@link resolveChildrenHandler}, + * and the extension should add tests for a file when + * {@link vscode.workspace.onDidOpenTextDocument} fires in order for + * decorations for tests within the file to be visible. * - * It's guaranteed that this method will not be called again while - * there is a previous uncancelled call for the given workspace folder. - * - * @param workspace The workspace in which to observe tests - * @param cancellationToken Token that signals the used asked to abort the test run. - * @returns the root test item for the workspace + * Tests in this collection should be watched and updated by the extension + * as files change. See {@link resolveChildrenHandler} for details around + * for the lifecycle of watches. */ - createWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult>; + readonly root: TestItem; /** - * Requests that tests be provided for the given document. This will be - * called when tests need to be enumerated for a single open file, for - * instance by code lens UI. - * - * It's suggested that the provider listen to change events for the text - * document to provide information for tests that might not yet be - * saved. - * - * If the test system is not able to provide or estimate for tests on a - * per-file basis, this method may not be implemented. In that case, the - * editor will request and use the information from the workspace tree. - * - * @param document The document in which to observe tests - * @param cancellationToken Token that signals the used asked to abort the test run. - * @returns the root test item for the document + * Creates a new managed {@link TestItem} instance as a child of this + * one. + * @param id Unique identifier for the TestItem. + * @param label Human-readable label of the test item. + * @param parent Parent of the item. This is required; top-level items + * should be created as children of the {@link root}. + * @param uri URI this TestItem is associated with. May be a file or directory. + * @param data Custom data to be stored in {@link TestItem.data} */ - createDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult>; + createTestItem( + id: string, + label: string, + parent: TestItem, + uri?: Uri, + data?: TChild, + ): TestItem; + + + /** + * A function provided by the extension that the editor may call to request + * children of a test item, if the {@link TestItem.status} is `Pending`. + * + * When called, the item should discover tests and call {@link TestItem.addChild}. + * The items should set its {@link TestItem.status} to `Resolved` when + * discovery is finished. + * + * The item should continue watching for changes to the children and + * firing updates until the `token` is cancelled. The process of watching + * the tests may involve creating a file watcher, for example. After the + * token is cancelled and watching stops, the TestItem should set its + * {@link TestItem.status} back to `Pending`. + * + * The editor will only call this method when it's interested in refreshing + * the children of the item, and will not call it again while there's an + * existing, uncancelled discovery for an item. + * + * @param item An unresolved test item for which + * children are being requested + * @param token Cancellation for the request. Cancellation will be + * requested if the test changes before the previous call completes. + */ + resolveChildrenHandler?: (item: TestItem, token: CancellationToken) => void; /** * Starts a test run. When called, the controller should call - * {@link vscode.test.createTestRun}. All tasks associated with the + * {@link TestController.createTestRun}. All tasks associated with the * run should be created before the function returns or the reutrned * promise is resolved. * @@ -2000,13 +1979,36 @@ declare module 'vscode' { * instances associated with the request will be * automatically cancelled as well. */ - runTests(request: TestRunRequest, token: CancellationToken): Thenable | void; + runHandler?: (request: TestRunRequest, token: CancellationToken) => Thenable | void; + /** + * Creates a {@link TestRun}. This should be called by the + * {@link TestRunner} when a request is made to execute tests, and may also + * be called if a test run is detected externally. Once created, tests + * that are included in the results will be moved into the + * {@link TestResultState.Pending} state. + * + * @param request Test run request. Only tests inside the `include` may be + * modified, and tests in its `exclude` are ignored. + * @param name The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @param persist Whether the results created by the run should be + * persisted in the editor. This may be false if the results are coming from + * a file already saved externally, such as a coverage information file. + */ + createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; + + /** + * Unregisters the test controller, disposing of its associated tests + * and unpersisted results. + */ + dispose(): void; } /** * Options given to {@link test.runTests}. */ - export interface TestRunRequest { + export class TestRunRequest { /** * Array of specific tests to run. The controllers should run all of the * given tests and all children of the given tests, excluding any tests @@ -2025,6 +2027,13 @@ declare module 'vscode' { * Whether tests in this run should be debugged. */ debug: boolean; + + /** + * @param tests Array of specific tests to run. + * @param exclude Tests to exclude from the run + * @param debug Whether tests in this run should be debugged. + */ + constructor(tests: readonly TestItem[], exclude?: readonly TestItem[], debug?: boolean); } /** @@ -2098,33 +2107,11 @@ declare module 'vscode' { Pending = 0, } - /** - * Options initially passed into `vscode.test.createTestItem` - */ - export interface TestItemOptions { - /** - * 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 cannot change for the lifetime of the TestItem. - */ - id: string; - - /** - * URI this TestItem is associated with. May be a file or directory. - */ - uri?: Uri; - - /** - * Display name describing the test item. - */ - label: string; - } - /** * A test item is an item shown in the "test explorer" view. It encompasses * both a suite and a test, since they have almost or identical capabilities. */ - export interface TestItem { + export interface TestItem { /** * Unique identifier for the TestItem. This is used to correlate * test results and tests in the document with those in the workspace @@ -2140,7 +2127,7 @@ declare module 'vscode' { /** * A mapping of children by ID to the associated TestItem instances. */ - readonly children: ReadonlyMap>; + readonly children: ReadonlyMap; /** * The parent of this item, if any. Assigned automatically when calling @@ -2212,35 +2199,6 @@ declare module 'vscode' { */ invalidate(): void; - /** - * A function provided by the extension that the editor may call to request - * children of the item, if the {@link TestItem.status} is `Pending`. - * - * When called, the item should discover tests and call {@link TestItem.addChild}. - * The items should set its {@link TestItem.status} to `Resolved` when - * discovery is finished. - * - * The item should continue watching for changes to the children and - * firing updates until the token is cancelled. The process of watching - * the tests may involve creating a file watcher, for example. After the - * token is cancelled and watching stops, the TestItem should set its - * {@link TestItem.status} back to `Pending`. - * - * The editor will only call this method when it's interested in refreshing - * the children of the item, and will not call it again while there's an - * existing, uncancelled discovery for an item. - * - * @param token Cancellation for the request. Cancellation will be - * requested if the test changes before the previous call completes. - */ - resolveHandler?: (token: CancellationToken) => void; - - /** - * Attaches a child, created from the {@link test.createTestItem} function, - * to this item. A `TestItem` may be a child of at most one other item. - */ - addChild(child: TestItem): void; - /** * Removes the test and its children from the tree. Any tokens passed to * child `resolveHandler` methods will be cancelled. diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a595d628d9a..94735126579 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -5,17 +5,17 @@ import { VSBuffer } from 'vs/base/common/buffer'; 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 { isDefined } from 'vs/base/common/types'; -import { URI, UriComponents } from 'vs/base/common/uri'; +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 { ExtensionRunTestsRequest, getTestSubscriptionKey, ITestItem, ITestMessage, ITestRunTask, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, ITestItem, ITestMessage, ITestRunTask, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; 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'; -import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; const reviveDiff = (diff: TestsDiff) => { for (const entry of diff) { @@ -34,6 +34,7 @@ const reviveDiff = (diff: TestsDiff) => { @extHostNamedCustomer(MainContext.MainThreadTesting) export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider { private readonly proxy: ExtHostTestingShape; + private readonly diffListener = this._register(new MutableDisposable()); private readonly testSubscriptions = new Map(); private readonly testProviderRegistrations = new Map(); @@ -44,15 +45,13 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh ) { 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 prevResults = resultService.results.map(r => r.toJSON()).filter(isDefined); if (prevResults.length) { this.proxy.$publishTestResults(prevResults); } - this._register(this.testService.onCancelTestRun(({ runId }) => { + this._register(this.testService.onDidCancelTestRun(({ runId }) => { this.proxy.$cancelExtensionTestRun(runId); })); @@ -63,18 +62,12 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this.proxy.$publishTestResults([serialized]); } })); - - this._register(testService.registerRootProvider(this)); - - for (const { resource, uri } of this.testService.subscriptions) { - this.proxy.$subscribeToTests(resource, uri); - } } /** * @inheritdoc */ - $addTestsToRun(runId: string, tests: ITestItem[]): void { + $addTestsToRun(controllerId: string, runId: string, tests: ITestItem[]): void { for (const test of tests) { test.uri = URI.revive(test.uri); if (test.range) { @@ -82,7 +75,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh } } - this.withLiveRun(runId, r => r.addTestChainToRun(tests)); + this.withLiveRun(runId, r => r.addTestChainToRun(controllerId, tests)); } /** @@ -146,14 +139,13 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $registerTestController(id: string) { - const disposable = this.testService.registerTestController(id, { - runTests: (req, token) => this.proxy.$runTestsForProvider(req, token), - lookupTest: test => this.proxy.$lookupTest(test), + public $registerTestController(controllerId: string) { + const disposable = this.testService.registerTestController(controllerId, { + runTests: (req, token) => this.proxy.$runControllerTests(req, token), expandTest: (src, levels) => this.proxy.$expandTest(src, isFinite(levels) ? levels : -1), }); - this.testProviderRegistrations.set(id, disposable); + this.testProviderRegistrations.set(controllerId, disposable); } /** @@ -167,28 +159,24 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $subscribeToDiffs(resource: ExtHostTestingResource, uriComponents: UriComponents): void { - const uri = URI.revive(uriComponents); - const disposable = this.testService.subscribeToDiffs(resource, uri, - diff => this.proxy.$acceptDiff(resource, uriComponents, diff)); - this.testSubscriptions.set(getTestSubscriptionKey(resource, uri), disposable); + public $subscribeToDiffs(): void { + this.proxy.$acceptDiff(this.testService.collection.getReviverDiff()); + this.diffListener.value = this.testService.onDidProcessDiff(this.proxy.$acceptDiff, this.proxy); } /** * @inheritdoc */ - public $unsubscribeFromDiffs(resource: ExtHostTestingResource, uriComponents: UriComponents): void { - const key = getTestSubscriptionKey(resource, URI.revive(uriComponents)); - this.testSubscriptions.get(key)?.dispose(); - this.testSubscriptions.delete(key); + public $unsubscribeFromDiffs(): void { + this.diffListener.clear(); } /** * @inheritdoc */ - public $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void { + public $publishDiff(controllerId: string, diff: TestsDiff): void { reviveDiff(diff); - this.testService.publishDiff(resource, URI.revive(uri), diff); + this.testService.publishDiff(controllerId, diff); } public async $runTests(req: RunTestsRequest, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0cbc9207ce9..b9aaff50911 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -55,7 +55,7 @@ import { values } from 'vs/base/common/collections'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; @@ -171,7 +171,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); - const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, accessor.get(IInstantiationService), extHostDocumentsAndEditors)); + const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); // Check that no named customers are missing @@ -358,29 +358,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I : extHostTypes.ExtensionKind.UI; const test: typeof vscode.test = { - registerTestController(provider) { + createTestController(provider) { checkProposedApiEnabled(extension); - return extHostTesting.registerTestController(extension.identifier.value, provider); + return extHostTesting.createTestController(provider); }, - createDocumentTestObserver(document) { + createTestObserver() { checkProposedApiEnabled(extension); - return extHostTesting.createTextDocumentTestObserver(document); - }, - createWorkspaceTestObserver(workspaceFolder) { - checkProposedApiEnabled(extension); - return extHostTesting.createWorkspaceTestObserver(workspaceFolder); + return extHostTesting.createTestObserver(); }, runTests(provider) { checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, - createTestItem(options: vscode.TestItemOptions, data?: T) { - return new extHostTypes.TestItemImpl(options.id, options.label, options.uri, data); - }, - createTestRun(request, name, persist) { - checkProposedApiEnabled(extension); - return extHostTesting.createTestRun(request, name, persist); - }, get onDidChangeTestResults() { checkProposedApiEnabled(extension); return extHostTesting.onResultsChanged; @@ -391,9 +380,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, }; - // todo@connor4312: backwards compatibility for a short period - (test as any).createTestRunTask = test.createTestRun; - // namespace: extensions const extensions: typeof vscode.extensions = { getExtension(extensionId: string): vscode.Extension | undefined { @@ -1289,6 +1275,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestItemStatus: extHostTypes.TestItemStatus, TestResultState: extHostTypes.TestResultState, + TestRunRequest: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, TextSearchCompleteMessageType: TextSearchCompleteMessageType, TestMessageSeverity: extHostTypes.TestMessageSeverity, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c52ecaf5b26..e76c084ae0c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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, InternalTestItem, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, RunTestForControllerRequest, RunTestsRequest, ITestIdWithSrc, TestsDiff } 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'; @@ -2066,36 +2066,39 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; + $runControllerTests(req: RunTestForControllerRequest, token: CancellationToken): Promise; $cancelExtensionTestRun(runId: string | undefined): void; - $subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void; - $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; - $lookupTest(test: TestIdWithSrc): Promise; - $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): 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; - $expandTest(src: TestIdWithSrc, levels: number): Promise; + /** Expands a test item's children, by the given number of levels. */ + $expandTest(src: ITestIdWithSrc, levels: number): Promise; } export interface MainThreadTestingShape { /** Registeres that there's a test controller with the given ID */ - $registerTestController(id: string): void; + $registerTestController(controllerId: string): void; /** Diposes of the test controller with the given ID */ - $unregisterTestController(id: string): void; - /** Requests tests from the given resource/uri, from the observer API. */ - $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; - /** Stops requesting tests from the given resource/uri, from the observer API. */ - $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; + $unregisterTestController(controllerId: string): void; + /** Requests tests published to VS Code. */ + $subscribeToDiffs(): void; + /** Stops requesting tests published to VS Code. */ + $unsubscribeFromDiffs(): void; /** Publishes that new tests were available on the given source. */ - $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; + $publishDiff(controllerId: string, diff: TestsDiff): void; /** Request by an extension to run tests. */ $runTests(req: RunTestsRequest, token: CancellationToken): Promise; // --- test run handling: + /** * Adds tests to the run. The tests are given in descending depth. The first * item will be a previously-known test, or a test root. */ - $addTestsToRun(runId: string, tests: ITestItem[]): void; + $addTestsToRun(controllerId: string, runId: string, tests: ITestItem[]): void; /** Updates the state of a test run in the given run. */ $updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void; /** Appends a message to a test in the run. */ diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index ba9c914b463..2066a1eff6b 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -4,86 +4,95 @@ *--------------------------------------------------------------------------------------------*/ import { mapFind } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; -import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; -import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { ExtHostTestItemEventType, getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestItemImpl } from 'vs/workbench/api/common/extHostTypes'; -import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { OwnedTestCollection, SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +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 type * as vscode from 'vscode'; - export class ExtHostTesting implements ExtHostTestingShape { private readonly resultsChangedEmitter = new Emitter(); - private readonly controllers = new TestControllers(); + private readonly controllers = new Map, + collection: SingleUseTestCollection, + }>(); private readonly proxy: MainThreadTestingShape; - private readonly ownedTests = new OwnedTestCollection(); private readonly runTracker: TestRunCoordinator; - private readonly subscriptions: TestSubscriptions; - private readonly mainThreadSubscriptions = new Map(); - - private workspaceObservers: WorkspaceFolderTestObserverFactory; - private textDocumentObservers: TextDocumentTestObserverFactory; + private readonly observer: TestObservers; public onResultsChanged = this.resultsChangedEmitter.event; public results: ReadonlyArray = []; - constructor( - @IExtHostRpcService rpc: IExtHostRpcService, - @IInstantiationService instantionService: IInstantiationService, - @IExtHostDocumentsAndEditors documents: IExtHostDocumentsAndEditors, - ) { + constructor(@IExtHostRpcService rpc: IExtHostRpcService) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); - - this.subscriptions = instantionService.createInstance(TestSubscriptions, this.ownedTests, this.controllers); + this.observer = new TestObservers(this.proxy); this.runTracker = new TestRunCoordinator(this.proxy); - this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); - this.textDocumentObservers = new TextDocumentTestObserverFactory(this.proxy, documents); } /** * Implements vscode.test.registerTestProvider */ - public registerTestController(extensionId: string, controller: vscode.TestController): vscode.Disposable { - const controllerId = generateUuid(); - const registration = this.controllers.register(controllerId, controller); + public createTestController(controllerId: string): vscode.TestController { + const disposable = new DisposableStore(); + const collection = disposable.add(new SingleUseTestCollection(controllerId)); + const initialExpand = disposable.add(new RunOnceScheduler(() => collection.expand(collection.root.id, 0), 0)); + + const controller: vscode.TestController = { + root: collection.root, + createTestRun: (request, name, persist = true) => { + return this.runTracker.createTestRun(controllerId, request, name, persist); + }, + createTestItem(id: string, label: string, parent: vscode.TestItem, uri: vscode.Uri, data?: TChild) { + if (!(parent instanceof TestItemImpl)) { + throw new Error(`The "parent" passed in for TestItem ${id} is invalid`); + } + + return new TestItemImpl(id, label, uri, data as TChild, parent); + }, + set resolveChildrenHandler(fn) { + collection.resolveHandler = fn; + if (fn) { + initialExpand.schedule(); + } + }, + get resolveChildrenHandler() { + return collection.resolveHandler; + }, + dispose: () => { + disposable.dispose(); + }, + }; + this.proxy.$registerTestController(controllerId); + disposable.add(toDisposable(() => this.proxy.$unregisterTestController(controllerId))); - return toDisposable(() => { - registration.dispose(); - this.proxy.$unregisterTestController(controllerId); - }); + this.controllers.set(controllerId, { controller, collection }); + disposable.add(toDisposable(() => this.controllers.delete(controllerId))); + + disposable.add(collection.onDidGenerateDiff(diff => this.proxy.$publishDiff(controllerId, diff))); + + return controller; } /** - * Implements vscode.test.createTextDocumentTestObserver + * Implements vscode.test.createTestObserver */ - public createTextDocumentTestObserver(document: vscode.TextDocument) { - return this.textDocumentObservers.checkout(document.uri); + public createTestObserver() { + return this.observer.checkout(); } - /** - * Implements vscode.test.createWorkspaceTestObserver - */ - public createWorkspaceTestObserver(workspaceFolder: vscode.WorkspaceFolder) { - return this.workspaceObservers.checkout(workspaceFolder.uri); - } /** * Implements vscode.test.runTests @@ -93,7 +102,7 @@ export class ExtHostTesting implements ExtHostTestingShape { tests .map(this.getInternalTestForReference, this) .filter(isDefined) - .map(t => ({ src: t.src, testId: t.item.extId })); + .map(t => ({ controllerId: t.controllerId, testId: t.item.extId })); await this.proxy.$runTests({ exclude: req.exclude ? testListToProviders(req.exclude).map(t => t.testId) : undefined, @@ -102,13 +111,6 @@ export class ExtHostTesting implements ExtHostTestingShape { }, token); } - /** - * Implements vscode.test.createTestRun - */ - public createTestRun(request: vscode.TestRunRequest, name: string | undefined, persist = true): vscode.TestRun { - return this.runTracker.createTestRun(request, name, persist); - } - /** * Updates test results shown to extensions. * @override @@ -125,61 +127,24 @@ export class ExtHostTesting implements ExtHostTestingShape { this.resultsChangedEmitter.fire(); } - /** - * Handles a request to read tests for a file, or workspace. - * @override - */ - public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { - const uri = URI.revive(uriComponents); - const { disposable, info } = await this.subscriptions.subscribeToTests(resource, uri); - this.mainThreadSubscriptions.set(getTestSubscriptionKey(resource, uri), disposable); - - if (info) { - this.proxy.$publishDiff(resource, uri, info.collection.reviveDiff()); - info.collection.onDidGenerateDiff(diff => this.proxy.$publishDiff(resource, uri, diff)); - - // note: we don't increment the count initially -- this is done by the - // main thread, incrementing once per extension host. We just push the - // diff to signal that roots have been discovered. - info.initialSubscribeP.then(() => info.collection.pushDiff([TestDiffOpType.IncrementPendingExtHosts, -1])); - } - } - /** * Expands the nodes in the test tree. If levels is less than zero, it will * be treated as infinite. - * @override */ - public async $expandTest(test: TestIdWithSrc, levels: number) { - const collection = this.subscriptions.getCollectionById(test.src.tree); + public async $expandTest({ controllerId, testId }: ITestIdWithSrc, levels: number) { + const collection = this.controllers.get(controllerId)?.collection; if (collection) { - await collection.expand(test.testId, levels < 0 ? Infinity : levels); + await collection.expand(testId, levels < 0 ? Infinity : levels); collection.flushDiff(); } } - /** - * Disposes of a previous subscription to tests. - * @override - */ - public $unsubscribeFromTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { - const uri = URI.revive(uriComponents); - const subscriptionKey = getTestSubscriptionKey(resource, uri); - this.mainThreadSubscriptions.get(subscriptionKey)?.dispose(); - this.mainThreadSubscriptions.delete(subscriptionKey); - } - /** * Receives a test update from the main thread. Called (eventually) whenever * tests change. - * @override */ - public $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void { - if (resource === ExtHostTestingResource.TextDocument) { - this.textDocumentObservers.acceptDiff(URI.revive(uri), diff); - } else { - this.workspaceObservers.acceptDiff(URI.revive(uri), diff); - } + public $acceptDiff(diff: TestsDiff): void { + this.observer.applyDiff(diff); } /** @@ -187,38 +152,38 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise { - const controller = this.controllers.get(req.tests[0].src.controller); - if (!controller) { + public async $runControllerTests(req: RunTestForControllerRequest, token: CancellationToken): Promise { + const lookup = this.controllers.get(req.controllerId); + if (!lookup) { return; } - const includeTests = req.tests - .map(({ testId, src }) => this.ownedTests.getTestById(testId, src?.tree)) - .filter(isDefined) - .map(([_tree, test]) => test); + const { controller, collection } = lookup; + const includeTests = req.testIds + .map((testId) => collection.tree.get(testId)) + .filter(isDefined); const excludeTests = req.excludeExtIds - .map(id => this.ownedTests.getTestById(id)) + .map(id => lookup.collection.tree.get(id)) .filter(isDefined) - .filter(([tree, exclude]) => - includeTests.some(include => tree.comparePositions(include, exclude) === TestPosition.IsChild), - ); + .filter(exclude => includeTests.some( + include => collection.tree.comparePositions(include, exclude) === TestPosition.IsChild, + )); if (!includeTests.length) { return; } const publicReq: vscode.TestRunRequest = { - tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)), - exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)), + tests: includeTests.map(t => t.actual), + exclude: excludeTests.map(t => t.actual), debug: req.debug, }; const tracker = this.runTracker.prepareForMainThreadTestRun(publicReq, TestRunDto.fromInternal(req), token); try { - await controller.runTests(publicReq, token); + await controller.runHandler?.(publicReq, token); } finally { if (tracker.isRunning && !token.isCancellationRequested) { await Event.toPromise(tracker.onEnd); @@ -239,26 +204,12 @@ export class ExtHostTesting implements ExtHostTestingShape { } } - public $lookupTest(req: TestIdWithSrc): Promise { - const owned = this.ownedTests.getTestById(req.testId); - if (!owned) { - return Promise.resolve(undefined); - } - - const { actual, discoverCts, expandLevels, ...item } = owned[1]; - return Promise.resolve(item); - } - /** * Gets the internal test item associated with the reference from the extension. */ private getInternalTestForReference(test: vscode.TestItem) { - // Find workspace items first, then owned tests, then document tests. - // If a test instance exists in both the workspace and document, prefer - // the workspace because it's less ephemeral. - return this.workspaceObservers.getMirroredTestDataByReference(test) - ?? this.subscriptions.getCollectionTestByReference(test) - ?? this.textDocumentObservers.getMirroredTestDataByReference(test); + return mapFind(this.controllers.values(), ({ collection }) => collection.getTestByReference(test)) + ?? this.observer.getMirroredTestDataByReference(test); } } @@ -367,7 +318,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(controllerId: string, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -375,7 +326,7 @@ export class TestRunCoordinator { // If there is not an existing tracked extension for the request, start // a new, detached session. - const dto = TestRunDto.fromPublic(request); + const dto = TestRunDto.fromPublic(controllerId, request); this.proxy.$startedExtensionTestRun({ debug: request.debug, exclude: request.exclude?.map(t => t.id) ?? [], @@ -398,23 +349,26 @@ export class TestRunCoordinator { } export class TestRunDto { - public static fromPublic(request: vscode.TestRunRequest) { + public static fromPublic(controllerId: string, request: vscode.TestRunRequest) { return new TestRunDto( + controllerId, generateUuid(), new Set(request.tests.map(t => t.id)), new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()), ); } - public static fromInternal(request: RunTestForProviderRequest) { + public static fromInternal(request: RunTestForControllerRequest) { return new TestRunDto( + request.controllerId, request.runId, - new Set(request.tests.map(t => t.testId)), + new Set(request.testIds), new Set(request.excludeExtIds), ); } constructor( + public readonly controllerId: string, public readonly id: string, private readonly include: ReadonlySet, private readonly exclude: ReadonlySet, @@ -506,160 +460,7 @@ class TestRunImpl implements vscode.TestRun { test = test.parent; } - this.#proxy.$addTestsToRun(this.#req.id, chain); - } -} - -/* - * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children - * to only the children that are located in a certain vscode.Uri. - */ -export class TestItemFilteredWrapper extends TestItemImpl { - private static wrapperMap = new WeakMap, TestItemFilteredWrapper>>(); - - public static removeFilter(document: vscode.TextDocument): void { - this.wrapperMap.delete(document); - } - - // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. - public static getWrapperForTestItem( - item: vscode.TestItem, - filterDocument: vscode.TextDocument, - parent?: TestItemFilteredWrapper, - ): TestItemFilteredWrapper { - let innerMap = this.wrapperMap.get(filterDocument); - if (innerMap?.has(item)) { - return innerMap.get(item) as TestItemFilteredWrapper; - } - - if (!innerMap) { - innerMap = new WeakMap(); - this.wrapperMap.set(filterDocument, innerMap); - } - - const w = new TestItemFilteredWrapper(item, filterDocument, parent); - innerMap.set(item, w); - return w; - } - - /** - * If the TestItem is wrapped, returns the unwrapped item provided - * by the extension. - */ - public static unwrap(item: vscode.TestItem | TestItemFilteredWrapper) { - return item instanceof TestItemFilteredWrapper ? item.actual as vscode.TestItem : item; - } - - private _cachedMatchesFilter: boolean | undefined; - private disposed?: boolean; - private readonly disposable = new DisposableStore(); - - /** - * Gets whether this node, or any of its children, match the document filter. - */ - public get hasNodeMatchingFilter(): boolean { - if (this._cachedMatchesFilter === undefined) { - return this.refreshMatch(); - } else { - return this._cachedMatchesFilter; - } - } - - private constructor( - public readonly actual: vscode.TestItem, - private filterDocument: vscode.TextDocument, - public readonly wrappedParent?: TestItemFilteredWrapper, - ) { - super(actual.id, actual.label, actual.uri, undefined); - if (!(actual instanceof TestItemImpl)) { - throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); - } - - // the resolveHandler is intentionally omitted here. When creating the test - // root, we ask the collection to expand infinitely. We must not duplicate - // call the resolveHandler again from the wrapper.. - this.debuggable = actual.debuggable; - this.runnable = actual.runnable; - this.description = actual.description; - this.error = actual.error; - this.status = actual.status; - this.range = actual.range; - - const wrapperApi = getPrivateApiFor(this); - const actualApi = getPrivateApiFor(actual); - this.disposable.add(actualApi.bus.event(evt => { - switch (evt[0]) { - case ExtHostTestItemEventType.SetProp: - (this as Record)[evt[1]] = evt[2]; - break; - case ExtHostTestItemEventType.NewChild: - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this); - getPrivateApiFor(wrapper).parent = actual; - wrapper.refreshMatch(); - break; - case ExtHostTestItemEventType.Disposed: - this.dispose(); - break; - default: - wrapperApi.bus.fire(evt); - } - })); - } - - /** - * Refreshes the `hasNodeMatchingFilter` state for this item. It matches - * if the test itself has a location that matches, or if any of its - * children do. - */ - public refreshMatch() { - if (this.disposed) { - return false; - } - - const didMatch = this._cachedMatchesFilter; - - // The `children` of the wrapper only include the children who match the - // filter. Synchronize them. - for (const rawChild of this.actual.children.values()) { - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(rawChild, this.filterDocument, this); - if (!wrapper.hasNodeMatchingFilter) { - wrapper.hide(); - } else if (!this.children.has(wrapper.id)) { - this.addChild(wrapper); - } - } - - const nowMatches = this.children.size > 0 || this.actual.uri?.toString() === this.filterDocument.uri.toString(); - this._cachedMatchesFilter = nowMatches; - - if (nowMatches !== didMatch && this.wrappedParent?._cachedMatchesFilter !== undefined) { - this.wrappedParent.refreshMatch(); - } - - return this._cachedMatchesFilter; - } - - public hide() { - if (this.wrappedParent) { - getPrivateApiFor(this.wrappedParent).children.delete(this.id); - } - - getPrivateApiFor(this).bus.fire([ExtHostTestItemEventType.Disposed]); - } - - public override dispose() { - if (this.disposed) { - return; - } - - this.disposed = true; - this.disposable.dispose(); - this.hide(); - this.wrappedParent?.refreshMatch(); - - for (const child of this.children.values()) { - child.dispose(); - } + this.#proxy.$addTestsToRun(this.#req.controllerId, this.#req.id, chain); } } @@ -756,23 +557,8 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection) { - let output: vscode.TestItem[] = []; - for (const itemId of itemIds) { - const item = this.items.get(itemId); - if (item) { - output.push(item.revived); - } - } - - return output; + public get rootTests() { + return super.roots; } /** @@ -811,328 +597,52 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection(); + public checkout(): vscode.TestObserver { + if (!this.current) { + this.current = this.createObserverData(); + } - public checkout(resourceUri: URI): vscode.TestObserver { - const resourceKey = resourceUri.toString(); - const resource = this.resources.get(resourceKey) ?? this.createObserverData(resourceUri); - - resource.pendingDeletion?.dispose(); - resource.observers++; + const current = this.current; + current.observers++; return { - onDidChangeTest: resource.tests.onDidChangeTests, - onDidDiscoverInitialTests: new Emitter().event, // todo@connor4312 - get tests() { - return resource.tests.rootTestItems; - }, + onDidChangeTest: current.tests.onDidChangeTests, + get tests() { return [...current.tests.rootTests].map(t => t.revived); }, dispose: once(() => { - if (!--resource.observers) { - resource.pendingDeletion = this.eventuallyDispose(resourceUri); + if (--current.observers === 0) { + this.proxy.$unsubscribeFromDiffs(); + this.current = undefined; } }), }; } /** - * Gets the internal test data by its reference, in any observer. + * Gets the internal test data by its reference. */ public getMirroredTestDataByReference(ref: vscode.TestItem) { - for (const { tests } of this.resources.values()) { - const v = tests.getMirroredTestDataByReference(ref); - if (v) { - return v; - } - } - - return undefined; + return this.current?.tests.getMirroredTestDataByReference(ref); } /** - * Called when no observers are listening for the resource any more. Should - * defer unlistening on the resource, and return a disposiable - * to halt the process in case new listeners come in. + * Applies test diffs to the current set of observed tests. */ - protected eventuallyDispose(resourceUri: URI) { - return disposableTimeout(() => this.unlisten(resourceUri), 10 * 1000); + public applyDiff(diff: TestsDiff) { + this.current?.tests.apply(diff); } - /** - * Starts listening to test information for the given resource. - */ - protected abstract listen(resourceUri: URI, onDiff: (diff: TestsDiff) => void): IDisposable; - - private createObserverData(resourceUri: URI): IObserverData { + private createObserverData() { const tests = new MirroredTestCollection(); - const listener = this.listen(resourceUri, diff => tests.apply(diff)); - const data: IObserverData = { observers: 0, tests, listener }; - this.resources.set(resourceUri.toString(), data); - return data; - } - - /** - * Called when a resource is no longer in use. - */ - protected unlisten(resourceUri: URI) { - const key = resourceUri.toString(); - const resource = this.resources.get(key); - if (resource) { - resource.observers = -1; - resource.pendingDeletion?.dispose(); - resource.listener.dispose(); - this.resources.delete(key); - } - } -} - -class WorkspaceFolderTestObserverFactory extends AbstractTestObserverFactory { - private diffListeners = new Map void>(); - - constructor(private readonly proxy: MainThreadTestingShape) { - super(); - } - - /** - * Publishees the diff for the workspace folder with the given uri. - */ - public acceptDiff(resourceUri: URI, diff: TestsDiff) { - this.diffListeners.get(resourceUri.toString())?.(diff); - } - - /** - * @override - */ - public listen(resourceUri: URI, onDiff: (diff: TestsDiff) => void) { - this.proxy.$subscribeToDiffs(ExtHostTestingResource.Workspace, resourceUri); - - const uriString = resourceUri.toString(); - this.diffListeners.set(uriString, onDiff); - - return toDisposable(() => { - this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.Workspace, resourceUri); - this.diffListeners.delete(uriString); - }); - } -} - -class TextDocumentTestObserverFactory extends AbstractTestObserverFactory { - private diffListeners = new Map void>(); - - constructor(private readonly proxy: MainThreadTestingShape, private documents: IExtHostDocumentsAndEditors) { - super(); - } - - /** - * Publishees the diff for the document with the given uri. - */ - public acceptDiff(resourceUri: URI, diff: TestsDiff) { - this.diffListeners.get(resourceUri.toString())?.(diff); - } - - /** - * @override - */ - public listen(resourceUri: URI, onDiff: (diff: TestsDiff) => void) { - const document = this.documents.getDocument(resourceUri); - if (!document) { - return toDisposable(() => undefined); - } - - const uriString = resourceUri.toString(); - this.diffListeners.set(uriString, onDiff); - - this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri); - return toDisposable(() => { - this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri); - this.diffListeners.delete(uriString); - }); - } -} - -const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; - -type SubscribeFunction = (controllerId: string, controller: vscode.TestController) => void; - -class TestControllers extends Disposable { - private readonly registeredEmitter = this._register(new Emitter<{ controllerId: string, controller: vscode.TestController }>()); - private readonly unregisteredEmitter = this._register(new Emitter<{ controllerId: string, controller: vscode.TestController }>()); - private readonly value = new Map>(); - - public readonly onRegistered = this.registeredEmitter.event; - - public register(controllerId: string, controller: vscode.TestController): IDisposable { - this.value.set(controllerId, controller); - this.registeredEmitter.fire({ controller, controllerId }); - return toDisposable(() => { - this.value.delete(controllerId); - this.unregisteredEmitter.fire({ controller, controllerId }); - }); - } - - public get(controllerId: string) { - return this.value.get(controllerId); - } - - [Symbol.iterator]() { - return this.value[Symbol.iterator](); - } -} - -class TestSubscriptions extends Disposable { - private readonly subs = new Map, - subscribeFn: SubscribeFunction; - }>(); - - constructor( - private readonly ownedTests: OwnedTestCollection, - private readonly controllers: TestControllers, - @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, - @IExtHostWorkspace private readonly workspace: IExtHostWorkspace, - ) { - super(); - - this._register(controllers.onRegistered(({ controller, controllerId }) => { - const subs = [...this.subs.values()]; - // give the ext a moment to register things rather than synchronously invoking within activate() - setTimeout(() => { - for (const { subscribeFn } of subs) { - subscribeFn(controllerId, controller); - } - }, 0); - })); - } - - - /** - * Gets the test collection for a controller by ID, if one exists. - */ - public getCollectionById(treeId: number) { - return mapFind(this.subs.values(), s => s.collection.treeId === treeId ? s.collection : undefined); - } - - /** - * Gets an internal test from the collection by reference, if it exists. - */ - public getCollectionTestByReference(test: vscode.TestItem) { - return mapFind(this.subs.values(), c => c.collection.getTestByReference(test)); - } - - private async createDefaultDocumentTestRoot(folder: vscode.WorkspaceFolder | undefined, document: vscode.TextDocument, controllerId: string, token: CancellationToken) { - if (!folder) { - return; - } - - const { disposable, info } = await this.subscribeToTests(ExtHostTestingResource.Workspace, folder.uri); - if (!info) { - return; - } - - const root = Iterable.find(info.collection.roots, r => r.src.controller === controllerId); - if (!root) { - return; - } - - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(root.actual, document); - info.collection.expand(root.actual.id, Infinity); - wrapper.refreshMatch(); - - token.onCancellationRequested(() => { - TestItemFilteredWrapper.removeFilter(document); - wrapper.dispose(); - disposable.dispose(); - }); - - return wrapper; - } - - public async subscribeToTests(resource: ExtHostTestingResource, uri: URI) { - const subscriptionKey = getTestSubscriptionKey(resource, uri); - const existing = this.subs.get(subscriptionKey); - if (existing) { - existing.listeners++; - return { disposable: this.getUnsubscriber(subscriptionKey), info: existing }; - } - - const cancellation = new CancellationTokenSource(); - let method: undefined | ((p: vscode.TestController, controllerId: string) => vscode.ProviderResult>); - if (resource === ExtHostTestingResource.TextDocument) { - let document = this.documents.getDocument(uri); - - // we can ask to subscribe to tests before the documents are populated in - // the extension host. Try to wait. - if (!document) { - const store = new DisposableStore(); - document = await new Promise(resolve => { - store.add(disposableTimeout(() => resolve(undefined), 5000)); - store.add(this.documents.onDidAddDocuments(e => { - const data = e.find(data => data.document.uri.toString() === uri.toString()); - if (data) { resolve(data); } - })); - }).finally(() => store.dispose()); - } - - if (document) { - const folder = await this.workspace.getWorkspaceFolder2(uri, false); - method = (p, id) => p.createDocumentTestRoot - ? p.createDocumentTestRoot(document!.document, cancellation.token) - : this.createDefaultDocumentTestRoot(folder, document!.document, id, cancellation.token); - } - } else { - const folder = await this.workspace.getWorkspaceFolder2(uri, false); - if (folder) { - method = p => p.createWorkspaceTestRoot(folder, cancellation.token); - } - } - - if (!method) { - return { disposable: toDisposable(() => { }), info: undefined }; - } - - const subscribeFn = async (id: string, provider: vscode.TestController) => { - try { - const root = await method!(provider, id); - if (root) { - collection.addRoot(root, id); - } - } catch (e) { - console.error(e); - } - }; - - const disposable = new DisposableStore(); - const collection = disposable.add(this.ownedTests.createForHierarchy()); - disposable.add(toDisposable(() => cancellation.dispose(true))); - const subscribes: Promise[] = []; - for (const [id, controller] of this.controllers) { - subscribes.push(subscribeFn(id, controller)); - } - - const initialSubscribeP = Promise.all(subscribes).then(() => undefined); - const info = { store: disposable, listeners: 1, initialSubscribeP, collection, subscribeFn }; - this.subs.set(subscriptionKey, info); - return { disposable: this.getUnsubscriber(subscriptionKey), info }; - } - - private getUnsubscriber(key: string) { - return toDisposable(() => { - const sub = this.subs.get(key); - if (sub && --sub.listeners === 0) { - sub.store.dispose(); - this.subs.delete(key); - } - }); + this.proxy.$subscribeToDiffs(); + return { observers: 0, tests, }; } } diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index 66394266b4b..60d9bbb45d1 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -22,7 +22,6 @@ export type ExtHostTestItemEvent = export interface IExtHostTestItemApi { children: Map; - parent?: TestItemImpl; bus: Emitter; } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1ab59702f9a..a80dd14a3f3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1672,7 +1672,6 @@ export namespace TestItem { label: item.label, uri: URI.revive(item.uri), range: Range.to(item.range || undefined), - addChild: () => undefined, dispose: () => undefined, status: types.TestItemStatus.Pending, data: undefined as never, @@ -1683,7 +1682,7 @@ export namespace TestItem { } export function to(item: ITestItem): types.TestItemImpl { - const testItem = new types.TestItemImpl(item.extId, item.label, URI.revive(item.uri), undefined); + const testItem = new types.TestItemImpl(item.extId, item.label, URI.revive(item.uri), undefined, undefined); testItem.range = Range.to(item.range || undefined); testItem.debuggable = item.debuggable; testItem.description = item.description || undefined; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b40c6495560..692b105a9db 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3335,23 +3335,29 @@ const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefine return a.isEqual(b); }; -export class TestItemImpl implements vscode.TestItem { +export class TestRunRequest implements vscode.TestRunRequest { + constructor( + public readonly tests: vscode.TestItem[], + public readonly exclude?: vscode.TestItem[] | undefined, + public readonly debug = false, + ) { } +} + +export class TestItemImpl implements vscode.TestItem { public readonly id!: string; public readonly uri!: vscode.Uri | undefined; - public readonly children!: ReadonlyMap; - public readonly parent!: TestItemImpl | undefined; + public readonly children!: ReadonlyMap>; + public readonly parent!: TestItemImpl | undefined; public range!: vscode.Range | undefined; public description!: string | undefined; public runnable!: boolean; public debuggable!: boolean; + public label!: string; public error!: string | vscode.MarkdownString; public status!: vscode.TestItemStatus; - /** Extension-owned resolve handler */ - public resolveHandler?: (token: vscode.CancellationToken) => void; - - constructor(id: string, public label: string, uri: vscode.Uri | undefined, public data: unknown) { + constructor(id: string, label: string, uri: vscode.Uri | undefined, public data: T, parent: vscode.TestItem | undefined) { const api = getPrivateApiFor(this); Object.defineProperties(this, { @@ -3367,7 +3373,8 @@ export class TestItemImpl implements vscode.TestItem { }, parent: { enumerable: false, - get: () => api.parent, + value: parent, + writable: false, }, children: { value: new ReadonlyMapView(api.children), @@ -3375,12 +3382,27 @@ export class TestItemImpl implements vscode.TestItem { writable: false, }, range: testItemPropAccessor(api, 'range', undefined, rangeComparator), + label: testItemPropAccessor(api, 'label', label, strictEqualComparator), description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator), runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator), debuggable: testItemPropAccessor(api, 'debuggable', false, strictEqualComparator), status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator), error: testItemPropAccessor(api, 'error', undefined, strictEqualComparator), }); + + if (parent) { + if (!(parent instanceof TestItemImpl)) { + throw new Error(`The "parent" passed in for TestItem ${id} is invalid`); + } + + const parentApi = getPrivateApiFor(parent); + if (parentApi.children.has(id)) { + throw new Error(`Attempted to insert a duplicate test item ID ${id}`); + } + + parentApi.children.set(id, this); + parentApi.bus.fire([ExtHostTestItemEventType.NewChild, this]); + } } public invalidate() { @@ -3388,27 +3410,11 @@ export class TestItemImpl implements vscode.TestItem { } public dispose() { - const api = getPrivateApiFor(this); - if (api.parent) { - getPrivateApiFor(api.parent).children.delete(this.id); + if (this.parent) { + getPrivateApiFor(this.parent).children.delete(this.id); } - api.bus.fire([ExtHostTestItemEventType.Disposed]); - } - - public addChild(child: vscode.TestItem) { - if (!(child instanceof TestItemImpl)) { - throw new Error('Test child must be created through vscode.test.createTestItem()'); - } - - const api = getPrivateApiFor(this); - if (api.children.has(child.id)) { - throw new Error(`Attempted to insert a duplicate test item ID ${child.id}`); - } - - api.children.set(child.id, child); - getPrivateApiFor(child).parent = this; - api.bus.fire([ExtHostTestItemEventType.NewChild, child]); + getPrivateApiFor(this).bus.fire([ExtHostTestItemEventType.Disposed]); } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index c1d1844d028..b9bcddb1715 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -4,20 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { mapFind } from 'vs/base/common/arrays'; import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { isDefined } from 'vs/base/common/types'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { ByLocationFolderElement, ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; -import { IActionableTestTreeElement, isActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; +import { IActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { IComputedStateAndDurationAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; const computedStateAccessor: IComputedStateAndDurationAccessor = { getOwnState: i => i instanceof TestItemTreeElement ? i.ownState : TestResultState.Unset, @@ -28,7 +27,10 @@ const computedStateAccessor: IComputedStateAndDurationAccessor i instanceof TestItemTreeElement ? i.ownDuration : undefined, setComputedDuration: (i, d) => i.duration = d, - getChildren: i => Iterable.filter(i.children.values(), isActionableTestTreeElement), + getChildren: i => Iterable.filter( + i.children.values(), + (t): t is TestItemTreeElement => t instanceof TestItemTreeElement, + ), *getParents(i) { for (let parent = i.parent; parent; parent = parent.parent) { yield parent; @@ -41,21 +43,15 @@ const computedStateAccessor: IComputedStateAndDurationAccessor(); - protected readonly changes = new NodeChangeList(); - - /** - * Root folders and contained items. - */ - protected readonly folders = new Map, - }>(); + protected readonly changes = new NodeChangeList(); + protected readonly items = new Map(); /** * Gets root elements of the tree. */ - protected get roots() { - return Iterable.map(this.folders.values(), f => f.root); + protected get roots(): Iterable { + const rootsIt = Iterable.map(this.testService.collection.rootItems, r => this.items.get(r.item.extId)); + return Iterable.filter(rootsIt, isDefined); } /** @@ -63,10 +59,12 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes */ public readonly onUpdate = this.updateEmitter.event; - constructor(protected readonly listener: TestSubscriptionListener, @ITestResultService private readonly results: ITestResultService) { + constructor( + @ITestService private readonly testService: ITestService, + @ITestResultService private readonly results: ITestResultService, + ) { super(); - this._register(listener.onDiff(({ folder, diff }) => this.applyDiff(folder.folder, diff))); - this._register(listener.onFolderChange(this.applyFolderChange, this)); + this._register(testService.onDidProcessDiff((diff) => this.applyDiff(diff))); // when test results are cleared, recalculate all state this._register(results.onResultsChanged((evt) => { @@ -74,34 +72,32 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return; } - for (const { items } of this.folders.values()) { - for (const inTree of [...items.values()].sort((a, b) => b.depth - a.depth)) { - const lookup = this.results.getStateById(inTree.test.item.extId)?.[1]; - let computed = TestResultState.Unset; - let ownDuration: number | undefined; - let updated = false; - if (lookup) { - computed = lookup.computedState; - ownDuration = lookup.ownDuration; - } + for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) { + const lookup = this.results.getStateById(inTree.test.item.extId)?.[1]; + let computed = TestResultState.Unset; + let ownDuration: number | undefined; + let updated = false; + if (lookup) { + computed = lookup.computedState; + ownDuration = lookup.ownDuration; + } - if (lookup) { - inTree.ownState = lookup.ownComputedState; - } + if (lookup) { + inTree.ownState = lookup.ownComputedState; + } - if (computed !== inTree.state) { - inTree.state = computed; - updated = true; - } + if (computed !== inTree.state) { + inTree.state = computed; + updated = true; + } - if (ownDuration !== inTree.ownDuration) { - inTree.ownDuration = ownDuration; - updated = true; - } + if (ownDuration !== inTree.ownDuration) { + inTree.ownDuration = ownDuration; + updated = true; + } - if (updated) { - this.addUpdated(inTree); - } + if (updated) { + this.addUpdated(inTree); } } @@ -118,32 +114,25 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes } } - for (const { items } of this.folders.values()) { - let item = items.get(result.item.extId); - if (item) { - item.retired = result.retired; - item.ownState = result.ownComputedState; - item.ownDuration = result.ownDuration; - // For items without children, always use the computed state. They are - // either leaves (for which it's fine) or nodes where we haven't expanded - // children and should trust whatever the result service gives us. - const explicitComputed = item.children.size ? undefined : result.computedState; - refreshComputedState(computedStateAccessor, item, explicitComputed).forEach(this.addUpdated); - this.addUpdated(item); - this.updateEmitter.fire(); - } + const item = this.items.get(result.item.extId); + if (!item) { + return; } + + item.retired = result.retired; + item.ownState = result.ownComputedState; + item.ownDuration = result.ownDuration; + // For items without children, always use the computed state. They are + // either leaves (for which it's fine) or nodes where we haven't expanded + // children and should trust whatever the result service gives us. + const explicitComputed = item.children.size ? undefined : result.computedState; + refreshComputedState(computedStateAccessor, item, explicitComputed).forEach(this.addUpdated); + this.addUpdated(item); + this.updateEmitter.fire(); })); - for (const [folder, collection] of listener.workspaceFolderCollections) { - const { items } = this.getOrCreateFolderElement(folder.folder); - for (const node of collection.all) { - this.storeItem(items, this.createItem(node, folder.folder)); - } - } - - for (const folder of this.folders.values()) { - this.changes.addedOrRemoved(folder.root); + for (const test of testService.collection.all) { + this.storeItem(this.createItem(test)); } } @@ -151,45 +140,31 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes * Gets the depth of children to expanded automatically for the node, */ protected getRevealDepth(element: ByLocationTestItemElement): number | undefined { - return element.depth === 1 ? 0 : undefined; + return element.depth === 0 ? 0 : undefined; } /** * @inheritdoc */ public getElementByTestId(testId: string): TestItemTreeElement | undefined { - return mapFind(this.folders.values(), f => f.items.get(testId)); - } - - private applyFolderChange(evt: IWorkspaceFoldersChangeEvent) { - for (const folder of evt.removed) { - const existing = this.folders.get(folder.uri.toString()); - if (existing) { - this.folders.delete(folder.uri.toString()); - this.changes.addedOrRemoved(existing.root); - } - this.updateEmitter.fire(); - } + return this.items.get(testId); } /** * @inheritdoc */ - private applyDiff(folder: IWorkspaceFolder, diff: TestsDiff) { - const { items } = this.getOrCreateFolderElement(folder); - + private applyDiff(diff: TestsDiff) { for (const op of diff) { switch (op[0]) { case TestDiffOpType.Add: { - const item = this.createItem(op[1], folder); - this.storeItem(items, item); - this.changes.addedOrRemoved(item); + const item = this.createItem(op[1]); + this.storeItem(item); break; } case TestDiffOpType.Update: { const patch = op[1]; - const existing = items.get(patch.extId); + const existing = this.items.get(patch.extId); if (!existing) { break; } @@ -200,7 +175,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes } case TestDiffOpType.Remove: { - const toRemove = items.get(op[1]); + const toRemove = this.items.get(op[1]); if (!toRemove) { break; } @@ -211,7 +186,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes while (queue.length) { for (const item of queue.pop()!) { if (item instanceof ByLocationTestItemElement) { - queue.push(this.unstoreItem(items, item)); + queue.push(this.unstoreItem(this.items, item)); } } } @@ -243,30 +218,16 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return; } - const folder = element.folder; - const collection = [...this.listener.workspaceFolderCollections].find(([f]) => f.folder === folder); - collection?.[1].expand(element.test.item.extId, depth); + this.testService.collection.expand(element.test.item.extId, depth); } - protected createItem(item: InternalTestItem, folder: IWorkspaceFolder): ByLocationTestItemElement { - const { items, root } = this.getOrCreateFolderElement(folder); - const parent = item.parent ? items.get(item.parent)! : root; + protected createItem(item: InternalTestItem): ByLocationTestItemElement { + const parent = item.parent ? this.items.get(item.parent)! : null; return new ByLocationTestItemElement(item, parent, n => this.changes.addedOrRemoved(n)); } - protected getOrCreateFolderElement(folder: IWorkspaceFolder) { - let f = this.folders.get(folder.uri.toString()); - if (!f) { - f = { root: new ByLocationFolderElement(folder), items: new Map() }; - this.changes.addedOrRemoved(f.root); - this.folders.set(folder.uri.toString(), f); - } - - return f; - } - protected readonly addUpdated = (item: IActionableTestTreeElement) => { - const cast = item as ByLocationTestItemElement | ByLocationFolderElement; + const cast = item as ByLocationTestItemElement; this.changes.updated(cast); }; @@ -275,18 +236,16 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return { element: node }; } - // Omit the workspace folder or controller root if there are no siblings - if (node.depth < 2 && !peersHaveChildren(node, () => this.roots)) { - return NodeRenderDirective.Concat; - } + if (node.depth === 0) { + // Omit the test controller root if there are no siblings + if (!peersHaveChildren(node, () => this.roots)) { + return NodeRenderDirective.Concat; + } - // Omit folders/roots that have no child tests - if (node.depth < 2 && node.children.size === 0) { - return NodeRenderDirective.Omit; - } - - if (!(node instanceof ByLocationTestItemElement)) { - return { element: node, children: recurse(node.children) }; + // Omit roots that have no child tests + if (node.children.size === 0) { + return NodeRenderDirective.Omit; + } } return { @@ -299,7 +258,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes protected unstoreItem(items: Map, treeElement: ByLocationTestItemElement) { const parent = treeElement.parent; - parent.children.delete(treeElement); + parent?.children.delete(treeElement); items.delete(treeElement.test.item.extId); if (parent instanceof ByLocationTestItemElement) { refreshComputedState(computedStateAccessor, parent).forEach(this.addUpdated); @@ -308,9 +267,10 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes return treeElement.children; } - protected storeItem(items: Map, treeElement: ByLocationTestItemElement) { - treeElement.parent.children.add(treeElement); - items.set(treeElement.test.item.extId, treeElement); + protected storeItem(treeElement: ByLocationTestItemElement) { + treeElement.parent?.children.add(treeElement); + this.items.set(treeElement.test.item.extId, treeElement); + this.changes.addedOrRemoved(treeElement); const reveal = this.getRevealDepth(treeElement); if (reveal !== undefined) { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index 9c9bfcde9e4..8aed628b719 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestExplorerTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { ByLocationTestItemElement, ByLocationFolderElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; +import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; /** * Type of test element in the list. @@ -50,7 +49,7 @@ export class ByNameTestItemElement extends ByLocationTestItemElement { */ constructor( internal: InternalTestItem, - parentItem: ByLocationFolderElement | ByLocationTestItemElement, + parentItem: null | ByLocationTestItemElement, addedOrRemoved: (n: TestExplorerTreeElement) => void, private readonly actualParent?: ByNameTestItemElement, ) { @@ -114,8 +113,8 @@ export class ByNameTestItemElement extends ByLocationTestItemElement { * test root rather than the heirarchal parent. */ export class HierarchicalByNameProjection extends HierarchicalByLocationProjection { - constructor(listener: TestSubscriptionListener, @ITestResultService results: ITestResultService) { - super(listener, results); + constructor(@ITestService testService: ITestService, @ITestResultService results: ITestResultService) { + super(testService, results); const originalRenderNode = this.renderNode.bind(this); this.renderNode = (node, recurse) => { @@ -135,16 +134,18 @@ export class HierarchicalByNameProjection extends HierarchicalByLocationProjecti /** * @override */ - protected override createItem(item: InternalTestItem, folder: IWorkspaceFolder): ByLocationTestItemElement { - const { root, items } = this.getOrCreateFolderElement(folder); - const actualParent = item.parent ? items.get(item.parent) as ByNameTestItemElement : undefined; - for (const testRoot of root.children) { - if (testRoot.test.src.controller === item.src.controller) { - return new ByNameTestItemElement(item, testRoot, r => this.changes.addedOrRemoved(r), actualParent); - } + protected override createItem(item: InternalTestItem): ByLocationTestItemElement { + const actualParent = item.parent ? this.items.get(item.parent) as ByNameTestItemElement : undefined; + if (actualParent) { + return new ByNameTestItemElement( + item, + actualParent.parent as ByNameTestItemElement || actualParent, + r => this.changes.addedOrRemoved(r), + actualParent, + ); } - return new ByNameTestItemElement(item, root, r => this.changes.addedOrRemoved(r)); + return new ByNameTestItemElement(item, null, r => this.changes.addedOrRemoved(r)); } /** diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 2766a095d77..f25e955b0c4 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testCollection'; /** @@ -12,15 +12,13 @@ import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workb export class ByLocationTestItemElement extends TestItemTreeElement { private errorChild?: TestTreeErrorMessage; - public override readonly parent: ByLocationFolderElement | ByLocationTestItemElement; constructor( test: InternalTestItem, - parent: ByLocationFolderElement | ByLocationTestItemElement, + parent: null | ByLocationTestItemElement, protected readonly addedOrRemoved: (n: TestExplorerTreeElement) => void, ) { super({ ...test, item: { ...test.item } }, parent); - this.parent = parent; this.updateErrorVisiblity(); } @@ -41,10 +39,3 @@ export class ByLocationTestItemElement extends TestItemTreeElement { } } } - -/** - * Workspace folder in the location view. - */ -export class ByLocationFolderElement extends TestTreeWorkspaceFolder { - public override readonly children = new Set(); -} diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index eb66e315e25..c7e62c52e97 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -9,9 +9,8 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { InternalTestItem, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, InternalTestItem, ITestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; /** * Describes a rendering of tests in the explorer view. Different @@ -66,20 +65,15 @@ export interface IActionableTestTreeElement { */ depth: number; - /** - * Folder associated with this element. - */ - folder: IWorkspaceFolder; - /** * Tests to debug when the 'debug' context action is taken on this item. */ - debuggable: Iterable; + debuggable: Iterable; /** * Tests to run when the 'debug' context action is taken on this item. */ - runnable: Iterable; + runnable: Iterable; /** * State to show on the item. This is generally the item's computed state @@ -102,61 +96,6 @@ let idCounter = 0; const getId = () => String(idCounter++); -export class TestTreeWorkspaceFolder implements IActionableTestTreeElement { - /** - * @inheritdoc - */ - public readonly parent = null; - - /** - * @inheritdoc - */ - public readonly children = new Set(); - - /** - * @inheritdoc - */ - public readonly treeId = getId(); - - /** - * @inheritdoc - */ - public readonly depth = 0; - - /** - * Time it took this test/item to run. - */ - public duration: number | undefined; - - /** - * @inheritdoc - */ - public get runnable() { - return Iterable.concatNested(Iterable.map(this.children, c => c.runnable)); - } - - /** - * @inheritdoc - */ - public get debuggable() { - return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable)); - } - - /** - * @inheritdoc - */ - public state = TestResultState.Unset; - - /** - * @inheritdoc - */ - public get label() { - return this.folder.name; - } - - constructor(public readonly folder: IWorkspaceFolder) { } -} - export class TestItemTreeElement implements IActionableTestTreeElement { /** * @inheritdoc @@ -171,21 +110,14 @@ export class TestItemTreeElement implements IActionableTestTreeElement { /** * @inheritdoc */ - public depth: number = this.parent.depth + 1; - - /** - * @inheritdoc - */ - public get folder(): IWorkspaceFolder { - return this.parent.folder; - } + public depth: number = this.parent ? this.parent.depth + 1 : 0; /** * @inheritdoc */ public get runnable() { return this.test.item.runnable - ? Iterable.single({ testId: this.test.item.extId, src: this.test.src }) + ? Iterable.single(identifyTest(this.test)) : Iterable.empty(); } @@ -194,7 +126,7 @@ export class TestItemTreeElement implements IActionableTestTreeElement { */ public get debuggable() { return this.test.item.debuggable - ? Iterable.single({ testId: this.test.item.extId, src: this.test.src }) + ? Iterable.single(identifyTest(this.test)) : Iterable.empty(); } @@ -236,7 +168,7 @@ export class TestItemTreeElement implements IActionableTestTreeElement { constructor( public readonly test: InternalTestItem, - public readonly parent: TestItemTreeElement | TestTreeWorkspaceFolder, + public readonly parent: TestItemTreeElement | null = null, ) { } } @@ -254,7 +186,4 @@ export class TestTreeErrorMessage { ) { } } -export const isActionableTestTreeElement = (t: unknown): t is (TestItemTreeElement | TestTreeWorkspaceFolder) => - t instanceof TestItemTreeElement || t instanceof TestTreeWorkspaceFolder; - -export type TestExplorerTreeElement = TestItemTreeElement | TestTreeWorkspaceFolder | TestTreeErrorMessage; +export type TestExplorerTreeElement = TestItemTreeElement | TestTreeErrorMessage; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts index fe6ff452564..0606269b5d2 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts @@ -6,7 +6,7 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; -import { IActionableTestTreeElement, TestExplorerTreeElement, TestItemTreeElement, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { IActionableTestTreeElement, TestExplorerTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; export const testIdentityProvider: IIdentityProvider = { getId(element) { @@ -67,7 +67,7 @@ const pruneNodesNotInTree = (nodes: Set, tree: O /** * Helper to gather and bulk-apply tree updates. */ -export class NodeChangeList { +export class NodeChangeList { private changedParents = new Set(); private updatedNodes = new Set(); private omittedNodes = new WeakSet(); diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 494345e6712..7c0e65b3076 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Iterable } from 'vs/base/common/iterator'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -20,8 +19,6 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { FocusedViewContext } from 'vs/workbench/common/views'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -32,15 +29,14 @@ import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/t import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, InternalTestItem, ITestIdWithSrc, ITestItem, TestIdPath } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { getPathForTestInResult, ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { getAllTestsInHierarchy, getTestByPath, ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService'; -import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; +import { getTestByPath, IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -178,7 +174,7 @@ abstract class RunOrDebugSelectedAction extends ViewAction * @override */ public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise { - const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel); + const tests = this.getActionableTests(accessor.get(ITestService), view.viewModel); if (!tests.length) { return Promise.resolve(undefined); } @@ -186,23 +182,16 @@ abstract class RunOrDebugSelectedAction extends ViewAction return accessor.get(ITestService).runTests({ tests, debug: this.debug }); } - private getActionableTests(testCollection: IWorkspaceTestCollectionService, viewModel: TestingExplorerViewModel) { + private getActionableTests(testService: ITestService, viewModel: TestingExplorerViewModel) { const selected = viewModel.getSelectedTests(); - const tests: TestIdWithSrc[] = []; + let tests: ITestIdWithSrc[]; if (!selected.length) { - for (const folder of testCollection.workspaceFolders()) { - for (const child of folder.getChildren()) { - if (this.filter(child)) { - tests.push({ testId: child.item.extId, src: child.src }); - } - } - } + tests = ([...testService.collection.rootItems].map(identifyTest)); } else { - for (const treeElement of selected) { - if (treeElement instanceof TestItemTreeElement && this.filter(treeElement.test)) { - tests.push({ testId: treeElement.test.item.extId, src: treeElement.test.src }); - } - } + tests = selected + .map(treeElement => treeElement instanceof TestItemTreeElement && this.filter(treeElement.test) ? treeElement.test : undefined) + .filter(isDefined) + .map(identifyTest); } return tests; @@ -260,7 +249,7 @@ const showDiscoveringWhile = (progress: IProgressService, task: Promise): ); }; -abstract class RunOrDebugAllAllAction extends Action2 { +abstract class RunOrDebugAllTestsAction extends Action2 { constructor(id: string, title: string, icon: ThemeIcon, private readonly debug: boolean, private noTestsFoundError: string, keybinding: IAction2Options['keybinding']) { super({ id, @@ -288,38 +277,19 @@ abstract class RunOrDebugAllAllAction extends Action2 { public async run(accessor: ServicesAccessor) { const testService = accessor.get(ITestService); - const workspace = accessor.get(IWorkspaceContextService); const notifications = accessor.get(INotificationService); - const progress = accessor.get(IProgressService); - const tests: TestIdWithSrc[] = []; - const todo = workspace.getWorkspace().folders.map(async (folder) => { - const ref = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri); - try { - await waitForAllRoots(ref.object); - for (const root of ref.object.rootIds) { - const node = ref.object.getNodeById(root); - if (node && (this.debug ? node.item.debuggable : node.item.runnable)) { - tests.push({ testId: node.item.extId, src: node.src }); - } - } - } finally { - ref.dispose(); - } - }); - - await showDiscoveringWhile(progress, Promise.all(todo)); - - if (tests.length === 0) { + const roots = [...testService.collection.rootItems]; + if (!roots.length) { notifications.info(this.noTestsFoundError); return; } - await testService.runTests({ tests, debug: this.debug }); + await testService.runTests({ tests: roots.map(identifyTest), debug: this.debug }); } } -export class RunAllAction extends RunOrDebugAllAllAction { +export class RunAllAction extends RunOrDebugAllTestsAction { public static readonly ID = 'testing.runAll'; constructor() { super( @@ -336,7 +306,7 @@ export class RunAllAction extends RunOrDebugAllAllAction { } } -export class DebugAllAction extends RunOrDebugAllAllAction { +export class DebugAllAction extends RunOrDebugAllTestsAction { public static readonly ID = 'testing.debugAll'; constructor() { super( @@ -771,35 +741,19 @@ abstract class RunOrDebugAtCursor extends Action2 { } const testService = accessor.get(ITestService); - const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri); - - let bestDepth = -1; let bestNode: InternalTestItem | undefined; - try { - await showDiscoveringWhile(accessor.get(IProgressService), getAllTestsInHierarchy(collection.object)); - - const queue: [depth: number, nodes: Iterable][] = [[0, collection.object.rootIds]]; - while (queue.length > 0) { - const [depth, candidates] = queue.pop()!; - for (const id of candidates) { - const candidate = collection.object.getNodeById(id); - if (candidate) { - if (depth > bestDepth && this.filter(candidate) && candidate.item.range && Range.containsPosition(candidate.item.range, position)) { - bestDepth = depth; - bestNode = candidate; - } - - queue.push([depth + 1, candidate.children]); - } + await showDiscoveringWhile(accessor.get(IProgressService), (async () => { + for await (const test of testsInFile(testService.collection, model.uri)) { + if (this.filter(test) && test.item.range && Range.containsPosition(test.item.range, position)) { + bestNode = test; } } + })()); - if (bestNode) { - await this.runTest(testService, bestNode); - } - } finally { - collection.dispose(); + + if (bestNode) { + await this.runTest(testService, bestNode); } } @@ -830,7 +784,7 @@ export class RunAtCursor extends RunOrDebugAtCursor { protected runTest(service: ITestService, internalTest: InternalTestItem): Promise { return service.runTests({ debug: false, - tests: [{ testId: internalTest.item.extId, src: internalTest.src }], + tests: [identifyTest(internalTest)], }); } } @@ -857,7 +811,7 @@ export class DebugAtCursor extends RunOrDebugAtCursor { protected runTest(service: ITestService, internalTest: InternalTestItem): Promise { return service.runTests({ debug: true, - tests: [{ testId: internalTest.item.extId, src: internalTest.src }], + tests: [identifyTest(internalTest)], }); } } @@ -876,7 +830,7 @@ abstract class RunOrDebugCurrentFile extends Action2 { /** * @override */ - public async run(accessor: ServicesAccessor) { + public run(accessor: ServicesAccessor) { const control = accessor.get(IEditorService).activeTextEditorControl; const position = control?.getPosition(); const model = control?.getModel(); @@ -885,24 +839,18 @@ abstract class RunOrDebugCurrentFile extends Action2 { } const testService = accessor.get(ITestService); - const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri); - try { - await waitForAllRoots(collection.object); - - const roots = [...collection.object.rootIds] - .map(r => collection.object.getNodeById(r)) - .filter(isDefined) - .filter(n => this.filter(n)); - - if (roots.length) { - await this.runTest(testService, roots); + const demandedUri = model.uri.toString(); + for (const test of testService.collection.all) { + if (test.item.uri?.toString() === demandedUri) { + return this.runTest(testService, [test]); } - } finally { - collection.dispose(); } + + return undefined; } + protected abstract filter(node: InternalTestItem): boolean; protected abstract runTest(service: ITestService, node: InternalTestItem[]): Promise; @@ -934,7 +882,7 @@ export class RunCurrentFile extends RunOrDebugCurrentFile { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: false, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: internalTests.map(identifyTest), }); } } @@ -961,28 +909,20 @@ export class DebugCurrentFile extends RunOrDebugCurrentFile { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: true, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })) + tests: internalTests.map(identifyTest) }); } } export const runTestsByPath = async ( - workspaceTests: IWorkspaceTestCollectionService, + collection: IMainThreadTestCollection, progress: IProgressService, paths: ReadonlyArray, runTests: (tests: ReadonlyArray) => Promise, ): Promise => { - const subscription = workspaceTests.subscribeToWorkspaceTests(); - try { - const todo = Promise.all([...subscription.workspaceFolderCollections.values()].map( - c => Promise.all(paths.map(p => getTestByPath(c, p))), - )); - - const tests = flatten(await showDiscoveringWhile(progress, todo)).filter(isDefined); - return tests.length ? await runTests(tests) : undefined; - } finally { - subscription.dispose(); - } + const todo = Promise.all(paths.map(p => getTestByPath(collection, p))); + const tests = (await showDiscoveringWhile(progress, todo)).filter(isDefined); + return tests.length ? await runTests(tests) : undefined; }; abstract class RunOrDebugExtsByPath extends Action2 { @@ -992,7 +932,7 @@ abstract class RunOrDebugExtsByPath extends Action2 { public async run(accessor: ServicesAccessor, ...args: unknown[]) { const testService = accessor.get(ITestService); await runTestsByPath( - accessor.get(IWorkspaceTestCollectionService), + accessor.get(ITestService).collection, accessor.get(IProgressService), [...this.getTestExtIdsToRun(accessor, ...args)], tests => this.runTest(testService, tests), @@ -1092,7 +1032,7 @@ export class ReRunFailedTests extends RunOrDebugFailedTests { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: false, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: internalTests.map(identifyTest), }); } } @@ -1118,7 +1058,7 @@ export class DebugFailedTests extends RunOrDebugFailedTests { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: true, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: internalTests.map(identifyTest), }); } } @@ -1144,7 +1084,7 @@ export class ReRunLastRun extends RunOrDebugLastRun { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: false, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: internalTests.map(identifyTest), }); } } @@ -1170,7 +1110,7 @@ export class DebugLastRun extends RunOrDebugLastRun { protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { return service.runTests({ debug: true, - tests: internalTests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: internalTests.map(identifyTest), }); } } diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 545f5c5e593..a2035975fa8 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -26,7 +26,7 @@ import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbenc import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { TestIdPath, TestIdWithMaybeSrc, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestIdPath, ITestIdWithSrc, identifyTest } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -35,7 +35,6 @@ import { ITestResultService, TestResultService } from 'vs/workbench/contrib/test import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; -import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { allTestActions, runTestsByPath } from './testExplorerActions'; @@ -47,7 +46,6 @@ registerSingleton(ITestingAutoRun, TestingAutoRun, true); registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true); registerSingleton(ITestingPeekOpener, TestingPeekOpener); registerSingleton(ITestingProgressUiService, TestingProgressUiService); -registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService); const viewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Testing.ViewletId, @@ -110,17 +108,17 @@ registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations CommandsRegistry.registerCommand({ id: 'vscode.runTests', - handler: async (accessor: ServicesAccessor, tests: TestIdWithMaybeSrc[]) => { + handler: async (accessor: ServicesAccessor, tests: ITestIdWithSrc[]) => { const testService = accessor.get(ITestService); - testService.runTests({ debug: false, tests: tests.filter(t => !!t.testId) }); + testService.runTests({ debug: false, tests }); } }); CommandsRegistry.registerCommand({ id: 'vscode.debugTests', - handler: async (accessor: ServicesAccessor, tests: TestIdWithSrc[]) => { + handler: async (accessor: ServicesAccessor, tests: ITestIdWithSrc[]) => { const testService = accessor.get(ITestService); - testService.runTests({ debug: true, tests: tests.filter(t => t.src && t.testId) }); + testService.runTests({ debug: true, tests }); } }); @@ -147,12 +145,12 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor, debug: boolean, ...pathToTests: TestIdPath[]) => { const testService = accessor.get(ITestService); await runTestsByPath( - accessor.get(IWorkspaceTestCollectionService), + accessor.get(ITestService).collection, accessor.get(IProgressService), pathToTests, tests => testService.runTests({ debug: false, - tests: tests.map(t => ({ testId: t.item.extId, src: t.src })), + tests: tests.map(identifyTest), }), ); } diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index f18c8fd63ed..6628cfdddc1 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -7,7 +7,7 @@ import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, dispose, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; @@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { TestMessageSeverity, TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; @@ -31,11 +30,11 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants'; -import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, TestDiffOpType, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; function isOriginalInDiffEditor(codeEditorService: ICodeEditorService, codeEditor: ICodeEditor): boolean { const diffEditors = codeEditorService.listDiffEditors(); @@ -52,7 +51,6 @@ function isOriginalInDiffEditor(codeEditorService: ICodeEditorService, codeEdito const FONT_FAMILY_VAR = `--testMessageDecorationFontFamily`; export class TestingDecorations extends Disposable implements IEditorContribution { - private collection = this._register(new MutableDisposable>()); private currentUri?: URI; private lastDecorations: ITestDecoration[] = []; @@ -122,11 +120,18 @@ export class TestingDecorations extends Disposable implements IEditorContributio this.setDecorations(this.currentUri); } })); + this._register(Event.any(this.results.onResultsChanged, this.testService.excludeTests.onDidChange)(() => { if (this.currentUri) { this.setDecorations(this.currentUri); } })); + + this._register(this.testService.onDidProcessDiff(() => { + if (this.currentUri) { + this.setDecorations(this.currentUri); + } + })); } private attachModel(uri?: URI) { @@ -137,54 +142,37 @@ export class TestingDecorations extends Disposable implements IEditorContributio this.currentUri = uri; if (!uri) { - this.collection.value = undefined; this.clearDecorations(); return; } - this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, diff => { - this.setDecorations(uri!); - - for (const op of diff) { - switch (op[0]) { - case TestDiffOpType.Add: - if (!op[1].parent) { - this.collection.value?.object.expand(op[1].item.extId, Infinity); - } - break; - case TestDiffOpType.Remove: - TestingOutputPeekController.get(this.editor).removeIfPeekingForTest(op[1]); - break; + (async () => { + for await (const _test of testsInFile(this.testService.collection, uri)) { + // consume the iterator so that all tests in the file get expanded. Or + // at least until the URI changes. If new items are requested, changes + // will be trigged in the `onDidProcessDiff` callback. + if (this.currentUri !== uri) { + break; } } - }); - - for (const root of this.collection.value.object.rootIds) { - this.collection.value.object.expand(root, Infinity); - } + })(); this.setDecorations(uri); } private setDecorations(uri: URI): void { - const ref = this.collection.value; - if (!ref) { - return; - } - this.editor.changeDecorations(accessor => { const newDecorations: ITestDecoration[] = []; - for (const test of ref.object.all) { + for (const test of this.testService.collection.all) { const stateLookup = this.results.getStateById(test.item.extId); - if (test.item.range) { + if (test.item.range && test.item.uri?.toString() === uri.toString()) { const line = test.item.range.startLineNumber; const resultItem = stateLookup?.[1]; const existing = newDecorations.findIndex(d => d instanceof RunTestDecoration && d.line === line); if (existing !== -1) { - newDecorations[existing] = (newDecorations[existing] as RunTestDecoration).merge(test, ref.object, resultItem); + newDecorations[existing] = (newDecorations[existing] as RunTestDecoration).merge(test, resultItem); } else { - newDecorations.push(this.instantiationService.createInstance( - RunSingleTestDecoration, test, ref.object, this.editor, stateLookup?.[1])); + newDecorations.push(this.instantiationService.createInstance(RunSingleTestDecoration, test, this.editor, stateLookup?.[1])); } } @@ -360,7 +348,7 @@ abstract class RunTestDecoration extends Disposable { /** * Adds the test to this decoration. */ - public abstract merge(other: IncrementalTestCollectionItem, collection: IMainThreadTestCollection, resultItem: TestResultItem | undefined): RunTestDecoration; + public abstract merge(other: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration; /** * Called when the decoration is clicked on. @@ -417,14 +405,14 @@ abstract class RunTestDecoration extends Disposable { if (test.item.runnable) { testActions.push(new Action('testing.gutter.run', localize('run test', 'Run Test'), undefined, undefined, () => this.testService.runTests({ debug: false, - tests: [{ src: test.src, testId: test.item.extId }], + tests: [identifyTest(test)], }))); } if (test.item.debuggable) { testActions.push(new Action('testing.gutter.debug', localize('debug test', 'Debug Test'), undefined, undefined, () => this.testService.runTests({ debug: true, - tests: [{ src: test.src, testId: test.item.extId }], + tests: [identifyTest(test)], }))); } @@ -451,7 +439,6 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio constructor( private readonly tests: { test: IncrementalTestCollectionItem, - collection: IMainThreadTestCollection, resultItem: TestResultItem | undefined, }[], editor: ICodeEditor, @@ -463,8 +450,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio super(createRunTestDecoration(tests.map(t => t.test), tests.map(t => t.resultItem)), editor, testService, contextMenuService, commandService, configurationService); } - public override merge(test: IncrementalTestCollectionItem, collection: IMainThreadTestCollection, resultItem: TestResultItem | undefined): RunTestDecoration { - this.tests.push({ collection, test, resultItem }); + public override merge(test: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration { + this.tests.push({ test, resultItem }); this.editorDecoration = createRunTestDecoration(this.tests.map(t => t.test), this.tests.map(t => t.resultItem)); return this; } @@ -479,8 +466,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio allActions.push(new Action('testing.gutter.debugAll', localize('debug all test', 'Debug All Tests'), undefined, undefined, () => this.defaultDebug())); } - const testSubmenus = this.tests.map(({ collection, test }) => - new SubmenuAction(test.item.extId, test.item.label, this.getTestContextMenuActions(collection, test))); + const testSubmenus = this.tests.map(({ test }) => + new SubmenuAction(test.item.extId, test.item.label, this.getTestContextMenuActions(this.testService.collection, test))); return Separator.join(allActions, testSubmenus); } @@ -489,7 +476,7 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio return this.testService.runTests({ tests: this.tests .filter(({ test }) => test.item.runnable) - .map(({ test }) => ({ testId: test.item.extId, src: test.src })), + .map(({ test }) => identifyTest(test)), debug: false, }); } @@ -498,7 +485,7 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio return this.testService.runTests({ tests: this.tests .filter(({ test }) => test.item.debuggable) - .map(({ test }) => ({ testId: test.item.extId, src: test.src })), + .map(({ test }) => identifyTest(test)), debug: true, }); } @@ -507,7 +494,6 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio class RunSingleTestDecoration extends RunTestDecoration implements ITestDecoration { constructor( private readonly test: IncrementalTestCollectionItem, - private readonly collection: IMainThreadTestCollection, editor: ICodeEditor, private readonly resultItem: TestResultItem | undefined, @ITestService testService: ITestService, @@ -518,15 +504,15 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati super(createRunTestDecoration([test], [resultItem]), editor, testService, contextMenuService, commandService, configurationService); } - public override merge(test: IncrementalTestCollectionItem, collection: IMainThreadTestCollection, resultItem: TestResultItem | undefined): RunTestDecoration { + public override merge(test: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration { return new MultiRunTestDecoration([ - { collection: this.collection, test: this.test, resultItem: this.resultItem }, - { collection, test, resultItem }, + { test: this.test, resultItem: this.resultItem }, + { test, resultItem }, ], this.editor, this.testService, this.commandService, this.contextMenuService, this.configurationService); } protected override getContextMenuActions(e: IEditorMouseEvent) { - return this.getTestContextMenuActions(this.collection, this.test); + return this.getTestContextMenuActions(this.testService.collection, this.test); } protected override defaultRun() { @@ -535,7 +521,7 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati } return this.testService.runTests({ - tests: [{ testId: this.test.item.extId, src: this.test.src }], + tests: [identifyTest(this.test)], debug: false, }); } @@ -546,7 +532,7 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati } return this.testService.runTests({ - tests: [{ testId: this.test.item.extId, src: this.test.src }], + tests: [identifyTest(this.test)], debug: true, }); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 0c6b087f546..ff76301e3d1 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -19,7 +19,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { splitGlobAware } from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, dispose, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/testing'; @@ -48,27 +48,24 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { IActionableTestTreeElement, isActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage, TestTreeWorkspaceFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; +import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { testingHiddenIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { labelForTestInState, TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; -import { TestIdPath, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, TestIdPath, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates'; import { getPathForTestInResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService'; import { GoToTest, internalTestActionIds } from './testExplorerActions'; export class TestingExplorerView extends ViewPane { public viewModel!: TestingExplorerViewModel; private filterActionBar = this._register(new MutableDisposable()); - private readonly currentSubscription = new MutableDisposable>(); private container!: HTMLElement; private treeHeader!: HTMLElement; private discoveryProgress = this._register(new MutableDisposable()); @@ -77,7 +74,6 @@ export class TestingExplorerView extends ViewPane { constructor( options: IViewletViewOptions, - @IWorkspaceTestCollectionService private readonly testCollection: IWorkspaceTestCollectionService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService, @@ -86,8 +82,7 @@ export class TestingExplorerView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @IEditorService private readonly editorService: IEditorService, - @ITestExplorerFilterState private readonly filterState: TestExplorerFilterState, + @ITestService testService: ITestService, @ITelemetryService telemetryService: ITelemetryService, @ITestingProgressUiService private readonly testProgressService: ITestingProgressUiService, ) { @@ -101,14 +96,8 @@ export class TestingExplorerView extends ViewPane { } })); - this._register(this.filterState.currentDocumentOnly.onDidChange(() => { - this.currentSubscription.value = this.createSubscription(); - this.viewModel.replaceSubscription(this.currentSubscription.value.object); - })); - - this._register(editorService.onDidActiveEditorChange(() => { - this.currentSubscription.value = this.createSubscription(); - this.viewModel.replaceSubscription(this.currentSubscription.value.object); + this._register(testService.collection.onBusyProvidersChange(busy => { + this.updateDiscoveryProgress(busy); })); } @@ -155,23 +144,13 @@ export class TestingExplorerView extends ViewPane { })); const listContainer = dom.append(this.container, dom.$('.test-explorer-tree')); - this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility, this.currentSubscription.value?.object); + this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility); this._register(this.viewModel.onChangeWelcomeVisibility(() => this._onDidChangeViewWelcomeState.fire())); this._register(this.viewModel); if (this.viewModel.welcomeExperience !== WelcomeExperience.ForWorkspace) { this._onDidChangeViewWelcomeState.fire(); } - - this._register(this.onDidChangeBodyVisibility(visible => { - if (!visible && this.currentSubscription) { - this.currentSubscription.value = undefined; - this.viewModel.replaceSubscription(undefined); - } else if (visible && !this.currentSubscription.value) { - this.currentSubscription.value = this.createSubscription(); - this.viewModel.replaceSubscription(this.currentSubscription.value.object); - } - })); } /** @@ -220,22 +199,6 @@ export class TestingExplorerView extends ViewPane { this.container.style.height = `${height}px`; this.viewModel.layout(height - this.treeHeader.clientHeight, width); } - - private createSubscription(): IReference { - const currentUri = this.editorService.activeEditor?.resource; - const handle = this.filterState.currentDocumentOnly.value - ? (currentUri ? this.testCollection.subscribeToDocumentTests(currentUri) : TestSubscriptionListener.None) - : this.testCollection.subscribeToWorkspaceTests(); - const listener = handle.onBusyProvidersChange(() => this.updateDiscoveryProgress(handle.busyProviders)); - - return { - object: handle, - dispose: () => { - handle.dispose(); - listener.dispose(); - }, - }; - } } const enum WelcomeExperience { @@ -310,7 +273,6 @@ export class TestingExplorerViewModel extends Disposable { constructor( listContainer: HTMLElement, onDidChangeVisibility: Event, - private listener: TestSubscriptionListener | undefined, @IConfigurationService configurationService: IConfigurationService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @@ -340,7 +302,6 @@ export class TestingExplorerViewModel extends Disposable { new ListDelegate(), [ instantiationService.createInstance(TestItemRenderer, labels, this.actionRunner), - instantiationService.createInstance(WorkspaceFolderRenderer, labels, this.actionRunner), instantiationService.createInstance(ErrorRenderer), ], { @@ -446,15 +407,6 @@ export class TestingExplorerViewModel extends Disposable { this.tree.layout(height, width); } - /** - * Replaces the test listener and recalculates the tree. - */ - public replaceSubscription(listener: TestSubscriptionListener | undefined) { - this.listener = listener; - this.updatePreferredProjection(); - this.reevaluateWelcomeState(); - } - /** * Tries to reveal by extension ID. Queues the request if the extension * ID is not currently available. @@ -495,7 +447,7 @@ export class TestingExplorerViewModel extends Disposable { // If the node or any of its children are excluded, flip on the 'show // excluded tests' checkbox automatically. - for (let n: TestItemTreeElement | TestTreeWorkspaceFolder = element; n instanceof TestItemTreeElement; n = n.parent) { + for (let n: TestItemTreeElement | null = element; n instanceof TestItemTreeElement; n = n.parent) { if (n.test && this.testService.excludeTests.value.has(n.test.item.extId)) { this.filterState.showExcludedTests.value = true; break; @@ -581,18 +533,13 @@ export class TestingExplorerViewModel extends Disposable { if (toRun.length) { this.testService.runTests({ debug: false, - tests: toRun.map(t => ({ src: t.test.src, testId: t.test.item.extId })), + tests: toRun.map(t => identifyTest(t.test)), }); } } private reevaluateWelcomeState() { - const shouldShowWelcome = !!this.listener - && this.listener.busyProviders === 0 - && this.listener.pendingRootProviders === 0 - && this.listener.isEmpty; - - + const shouldShowWelcome = this.testService.collection.busyProviders === 0 && testCollectionIsEmpty(this.testService.collection); const welcomeExperience = shouldShowWelcome ? (this.filterState.currentDocumentOnly.value ? WelcomeExperience.ForDocument : WelcomeExperience.ForWorkspace) : WelcomeExperience.None; @@ -605,15 +552,11 @@ export class TestingExplorerViewModel extends Disposable { private updatePreferredProjection() { this.projection.clear(); - if (!this.listener) { - this.tree.setChildren(null, []); - return; - } if (this._viewMode.get() === TestExplorerViewMode.List) { - this.projection.value = this.instantiationService.createInstance(HierarchicalByNameProjection, this.listener); + this.projection.value = this.instantiationService.createInstance(HierarchicalByNameProjection); } else { - this.projection.value = this.instantiationService.createInstance(HierarchicalByLocationProjection, this.listener); + this.projection.value = this.instantiationService.createInstance(HierarchicalByLocationProjection); } const scheduler = new RunOnceScheduler(() => this.applyProjectionChanges(), 200); @@ -731,7 +674,7 @@ class TestsFilter implements ITreeFilter { return FilterResult.Include; } - for (let e: IActionableTestTreeElement | null = element; e instanceof TestItemTreeElement; e = e!.parent) { + for (let e: TestItemTreeElement | null = element; e instanceof TestItemTreeElement; e = e!.parent) { return e.test.item.uri?.toString() === this._filterToUri ? FilterResult.Include : FilterResult.Exclude; @@ -740,12 +683,12 @@ class TestsFilter implements ITreeFilter { return FilterResult.Inherit; } - private testFilterText(element: IActionableTestTreeElement) { + private testFilterText(element: TestItemTreeElement) { if (!this.filters) { return FilterResult.Include; } - for (let e: IActionableTestTreeElement | null = element; e; e = e.parent) { + for (let e: TestItemTreeElement | null = element; e; e = e.parent) { // start as included if the first glob is a negation let included = this.filters[0][0] === false ? FilterResult.Include : FilterResult.Inherit; const data = e.label.toLowerCase(); @@ -828,18 +771,18 @@ class TestExplorerActionRunner extends ActionRunner { const selection = this.getSelectedTests(); const contextIsSelected = selection.some(s => s === context); const actualContext = contextIsSelected ? selection : [context]; - const actionable = actualContext.filter(isActionableTestTreeElement); + const actionable = actualContext.filter((t): t is TestItemTreeElement => t instanceof TestItemTreeElement); // Is there a better way to do this? if (internalTestActionIds.has(action.id)) { await action.run(...actionable); } else { - await action.run(...actionable.map(a => a instanceof TestItemTreeElement ? a.test.item.extId : a.folder.uri)); + await action.run(...actionable.map(a => a.test.item.extId)); } } } -const getLabelForTestTreeElement = (element: IActionableTestTreeElement) => { +const getLabelForTestTreeElement = (element: TestItemTreeElement) => { let label = labelForTestInState(element.label, element.state); if (element instanceof TestItemTreeElement) { @@ -885,10 +828,6 @@ class ListDelegate implements IListVirtualDelegate { } getTemplateId(element: TestExplorerTreeElement) { - if (element instanceof TestTreeWorkspaceFolder) { - return WorkspaceFolderRenderer.ID; - } - if (element instanceof TestTreeErrorMessage) { return ErrorRenderer.ID; } @@ -950,7 +889,7 @@ interface IActionableElementTemplateData { templateDisposable: IDisposable[]; } -abstract class ActionableItemTemplateData extends Disposable +abstract class ActionableItemTemplateData extends Disposable implements ITreeRenderer { constructor( protected readonly labels: ResourceLabels, @@ -1080,38 +1019,11 @@ class TestItemRenderer extends ActionableItemTemplateData { const formatDuration = (ms: number) => ms < 10 ? ms.toFixed(1) : ms.toFixed(0); -class WorkspaceFolderRenderer extends ActionableItemTemplateData { - public static readonly ID = 'workspaceFolder'; - - /** - * @inheritdoc - */ - get templateId(): string { - return WorkspaceFolderRenderer.ID; - } - - /** - * @inheritdoc - */ - public override renderElement(node: ITreeNode, depth: number, data: IActionableElementTemplateData): void { - super.renderElement(node, depth, data); - - const label: IResourceLabelProps = { name: node.element.label }; - const options: IResourceLabelOptions = {}; - data.label.setResource(label, options); - - const icon = testingStatesToIcons.get(node.element.state); - data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : ''); - options.fileKind = FileKind.ROOT_FOLDER; - data.label.setResource(label, options); - } -} - const getActionableElementActions = ( contextKeyService: IContextKeyService, menuService: IMenuService, testService: ITestService, - element: IActionableTestTreeElement, + element: TestItemTreeElement, ) => { const test = element instanceof TestItemTreeElement ? element.test : undefined; const contextOverlay = contextKeyService.createOverlay([ diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 297b90dbb79..f450190b307 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -49,7 +49,6 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -66,7 +65,7 @@ import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { getPathForTestInResult, ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; -import { getAllTestsInHierarchy, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; class TestDto { @@ -215,45 +214,39 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener * Gets the message closest to the given position from a test in the file. */ private async getFileCandidateMessage(uri: URI, position: Position | null) { - const tests = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri); - try { - await getAllTestsInHierarchy(tests.object); + let best: TestUriWithDocument | undefined; + let bestDistance = Infinity; - let best: TestUriWithDocument | undefined; - let bestDistance = Infinity; - - // Get all tests for the document. In those, find one that has a test - // message closest to the cursor position. - for (const test of tests.object.all) { - const result = this.testResults.getStateById(test.item.extId); - if (!result) { - continue; - } - - mapFindTestMessage(result[1], (_task, message, messageIndex, taskIndex) => { - if (!message.location || message.location.uri.toString() !== uri.toString()) { - return; - } - - const distance = position ? Math.abs(position.lineNumber - message.location.range.startLineNumber) : 0; - if (!best || distance <= bestDistance) { - bestDistance = distance; - best = { - type: TestUriType.ResultMessage, - testExtId: result[1].item.extId, - resultId: result[0].id, - taskIndex, - messageIndex, - documentUri: uri, - }; - } - }); + // Get all tests for the document. In those, find one that has a test + // message closest to the cursor position. + const demandedUriStr = uri.toString(); + for (const test of this.testService.collection.all) { + const result = this.testResults.getStateById(test.item.extId); + if (!result) { + continue; } - return best; - } finally { - tests.dispose(); + mapFindTestMessage(result[1], (_task, message, messageIndex, taskIndex) => { + if (!message.location || message.location.uri.toString() !== demandedUriStr) { + return; + } + + const distance = position ? Math.abs(position.lineNumber - message.location.range.startLineNumber) : 0; + if (!best || distance <= bestDistance) { + bestDistance = distance; + best = { + type: TestUriType.ResultMessage, + testExtId: result[1].item.extId, + resultId: result[0].id, + taskIndex, + messageIndex, + documentUri: uri, + }; + } + }); } + + return best; } /** diff --git a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts new file mode 100644 index 00000000000..4055c1c6e31 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, ITestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; + +export class MainThreadTestCollection extends AbstractIncrementalTestCollection implements IMainThreadTestCollection { + private busyProvidersChangeEmitter = new Emitter(); + private retireTestEmitter = new Emitter(); + private expandPromises = new WeakMap; + }>(); + + /** + * @inheritdoc + */ + public get busyProviders() { + return this.busyControllerCount; + } + + /** + * @inheritdoc + */ + public get rootItems() { + return this.roots; + } + + /** + * @inheritdoc + */ + public get all() { + return this.getIterator(); + } + + private get rootIds() { + return Iterable.map(this.roots.values(), r => r.item.extId); + } + + public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event; + public readonly onDidRetireTest = this.retireTestEmitter.event; + + constructor(private readonly expandActual: (src: ITestIdWithSrc, levels: number) => Promise) { + super(); + } + + /** + * @inheritdoc + */ + public expand(testId: string, levels: number): Promise { + const test = this.items.get(testId); + if (!test) { + return Promise.resolve(); + } + + // simple cache to avoid duplicate/unnecessary expansion calls + const existing = this.expandPromises.get(test); + if (existing && existing.pendingLvl >= levels) { + return existing.prom; + } + + const prom = this.expandActual({ controllerId: test.controllerId, testId: test.item.extId }, levels); + const record = { doneLvl: existing ? existing.doneLvl : -1, pendingLvl: levels, prom }; + this.expandPromises.set(test, record); + + return prom.then(() => { + record.doneLvl = levels; + }); + } + + /** + * @inheritdoc + */ + public getNodeById(id: string) { + return this.items.get(id); + } + + /** + * @inheritdoc + */ + public getReviverDiff() { + const ops: TestsDiff = [[TestDiffOpType.IncrementPendingExtHosts, this.pendingRootCount]]; + + const queue = [this.rootIds]; + while (queue.length) { + for (const child of queue.pop()!) { + const item = this.items.get(child)!; + ops.push([TestDiffOpType.Add, { + controllerId: item.controllerId, + expand: item.expand, + item: item.item, + parent: item.parent, + }]); + queue.push(item.children); + } + } + + return ops; + } + + /** + * Applies the diff to the collection. + */ + public override apply(diff: TestsDiff) { + let prevBusy = this.busyControllerCount; + super.apply(diff); + + if (prevBusy !== this.busyControllerCount) { + this.busyProvidersChangeEmitter.fire(this.busyControllerCount); + } + } + + /** + * Clears everything from the collection, and returns a diff that applies + * that action. + */ + public clear() { + const ops: TestsDiff = []; + for (const root of this.roots) { + ops.push([TestDiffOpType.Remove, root.item.extId]); + } + + this.roots.clear(); + this.items.clear(); + + return ops; + } + + /** + * @override + */ + protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem { + return { ...internal, children: new Set() }; + } + + /** + * @override + */ + protected override retireTest(testId: string) { + this.retireTestEmitter.fire(testId); + } + + private *getIterator() { + const queue = [this.rootIds]; + while (queue.length) { + for (const id of queue.pop()!) { + const node = this.getNodeById(id)!; + yield node; + queue.push(node.children); + } + } + } +} diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 0d5d757d623..02bc07c007b 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mapFind } from 'vs/base/common/arrays'; import { DeferredPromise, isThenable, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { assertNever } from 'vs/base/common/types'; import { ExtHostTestItemEvent, ExtHostTestItemEventType, getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -21,47 +20,6 @@ export interface IHierarchyProvider { getChildren(node: TestItemRaw, token: CancellationToken): Iterable | AsyncIterable | undefined | null; } -/** - * @private - */ -export class OwnedTestCollection { - protected readonly testIdsToInternal = new Map>(); - - /** - * Gets test information by ID, if it was defined and still exists in this - * extension host. - */ - public getTestById(id: string, preferTree?: number): undefined | [ - tree: TestTree, - test: OwnedCollectionTestItem, - ] { - if (preferTree !== undefined) { - const tree = this.testIdsToInternal.get(preferTree); - const test = tree?.get(id); - if (test) { - return [tree!, test]; - } - } - return mapFind(this.testIdsToInternal.values(), t => { - const owned = t.get(id); - return owned && [t, owned]; - }); - } - - /** - * Creates a new test collection for a specific hierarchy for a workspace - * or document observation. - */ - public createForHierarchy() { - return new SingleUseTestCollection(this.createIdMap(treeIdCounter++)); - } - - protected createIdMap(id: number): IReference> { - const tree = new TestTree(id); - this.testIdsToInternal.set(tree.id, tree); - return { object: tree, dispose: () => this.testIdsToInternal.delete(tree.id) }; - } -} /** * @private */ @@ -90,8 +48,6 @@ export const enum TestPosition { IsSame, } -let treeIdCounter = 0; - /** * Test tree is (or will be after debt week 2020-03) the standard collection * for test trees. Internally it indexes tests by their extension ID in @@ -102,8 +58,6 @@ export class TestTree { private readonly _roots = new Set(); public readonly roots: ReadonlySet = this._roots; - constructor(public readonly id: number) { } - /** * Gets the size of the tree. */ @@ -191,41 +145,49 @@ export class TestTree { } } +type ResolveHandler = (item: TestItemRaw, token: CancellationToken) => void; + /** * Maintains tests created and registered for a single set of hierarchies * for a workspace or document. * @private */ -export class SingleUseTestCollection implements IDisposable { +export class SingleUseTestCollection extends Disposable { protected readonly testItemToInternal = new Map(); + private readonly debounceSendDiff = this._register(new RunOnceScheduler(() => this.flushDiff(), 200)); + private readonly diffOpEmitter = this._register(new Emitter()); + private _resolveHandler?: ResolveHandler; + + public readonly root = new TestItemImpl(`${this.controllerId}Root`, this.controllerId, undefined, undefined, undefined); + public readonly tree = new TestTree(); protected diff: TestsDiff = []; - private readonly debounceSendDiff = new RunOnceScheduler(() => this.flushDiff(), 200); - private readonly diffOpEmitter = new Emitter(); + + constructor( + private readonly controllerId: string, + ) { + super(); + this.addItemInner(this.root, null); + } + + /** + * Handler used for expanding test items. + */ + public set resolveHandler(handler: undefined | ((item: TestItemRaw, token: CancellationToken) => void)) { + this._resolveHandler = handler; + for (const test of this.testItemToInternal.values()) { + this.updateExpandability(test); + } + } /** * Fires when an operation happens that should result in a diff. */ public readonly onDidGenerateDiff = this.diffOpEmitter.event; - public get treeId() { - return this.testIdToInternal.object.id; - } - public get roots() { return Iterable.filter(this.testItemToInternal.values(), t => t.parent === null); } - constructor( - private readonly testIdToInternal: IReference>, - ) { } - - /** - * Adds a new root node to the collection. - */ - public addRoot(item: TestItemRaw, controllerId: string) { - this.addItem(item, controllerId, null); - } - /** * Gets test information by its reference, if it was defined and still exists * in this extension host. @@ -274,7 +236,7 @@ export class SingleUseTestCollection implements IDisposable { * item will be expanded. */ public expand(testId: string, levels: number): Promise | void { - const internal = this.testIdToInternal.object.get(testId); + const internal = this.tree.get(testId); if (!internal) { return; } @@ -297,18 +259,14 @@ export class SingleUseTestCollection implements IDisposable { } } - /** - * @inheritdoc - */ - public dispose() { + public override dispose() { for (const item of this.testItemToInternal.values()) { item.discoverCts?.dispose(true); getPrivateApiFor(item.actual).bus.dispose(); } this.diff = []; - this.testIdToInternal.dispose(); - this.debounceSendDiff.dispose(); + super.dispose(); } private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) { @@ -324,7 +282,7 @@ export class SingleUseTestCollection implements IDisposable { break; case ExtHostTestItemEventType.NewChild: - this.addItem(evt[1], internal.src.controller, internal); + this.addItemInner(evt[1], internal); break; case ExtHostTestItemEventType.SetProp: @@ -349,7 +307,7 @@ export class SingleUseTestCollection implements IDisposable { } } - private addItem(actual: TestItemRaw, controllerId: string, parent: OwnedCollectionTestItem | null) { + private addItemInner(actual: TestItemRaw, parent: OwnedCollectionTestItem | null) { if (!(actual instanceof TestItemImpl)) { throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); } @@ -358,27 +316,28 @@ export class SingleUseTestCollection implements IDisposable { throw new Error(`Attempted to add a single TestItem ${actual.id} multiple times to the tree`); } - if (this.testIdToInternal.object.has(actual.id)) { + if (this.tree.has(actual.id)) { throw new Error(`Attempted to insert a duplicate test item ID ${actual.id}`); } const parentId = parent ? parent.item.extId : null; - const expand = actual.resolveHandler ? TestItemExpandState.Expandable : TestItemExpandState.NotExpandable; // always expand root node to know if there are tests (and whether to show the welcome view) const pExpandLvls = parent ? parent.expandLevels : 1; - const src = { controller: controllerId, tree: this.testIdToInternal.object.id }; const internal: OwnedCollectionTestItem = { actual, parent: parentId, item: Convert.TestItem.from(actual), expandLevels: pExpandLvls /* intentionally undefined or 0 */ ? pExpandLvls - 1 : undefined, expand: TestItemExpandState.NotExpandable, // updated by `updateExpandability` down below - src, + controllerId: this.controllerId, }; - this.testIdToInternal.object.add(internal); + this.tree.add(internal); this.testItemToInternal.set(actual, internal); - this.pushDiff([TestDiffOpType.Add, { parent: parentId, src, expand, item: internal.item }]); + this.pushDiff([ + TestDiffOpType.Add, + { parent: parentId, controllerId: this.controllerId, expand: internal.expand, item: internal.item }, + ]); const api = getPrivateApiFor(actual); api.bus.event(this.onTestItemEvent.bind(this, internal)); @@ -390,7 +349,7 @@ export class SingleUseTestCollection implements IDisposable { // Discover any existing children that might have already been added for (const child of api.children.values()) { if (!this.testItemToInternal.has(child)) { - this.addItem(child, controllerId, internal); + this.addItemInner(child, internal); } } } @@ -402,7 +361,7 @@ export class SingleUseTestCollection implements IDisposable { */ private updateExpandability(internal: OwnedCollectionTestItem) { let newState: TestItemExpandState; - if (!internal.actual.resolveHandler) { + if (!this._resolveHandler) { newState = TestItemExpandState.NotExpandable; } else if (internal.actual.status === TestItemStatus.Pending) { newState = internal.discoverCts @@ -410,7 +369,9 @@ export class SingleUseTestCollection implements IDisposable { : TestItemExpandState.Expandable; } else { internal.initialExpand?.complete(); - newState = TestItemExpandState.Expanded; + newState = internal.actual.children.size > 0 + ? TestItemExpandState.Expanded + : TestItemExpandState.NotExpandable; } if (newState === internal.expand) { @@ -452,7 +413,7 @@ export class SingleUseTestCollection implements IDisposable { internal.discoverCts.dispose(true); } - if (!internal.actual.resolveHandler) { + if (!this._resolveHandler) { const p = new DeferredPromise(); p.complete(); return p; @@ -463,7 +424,7 @@ export class SingleUseTestCollection implements IDisposable { this.pushExpandStateUpdate(internal); internal.initialExpand = new DeferredPromise(); - internal.actual.resolveHandler(internal.discoverCts.token); + this._resolveHandler(internal.actual, internal.discoverCts.token); return internal.initialExpand; } @@ -483,10 +444,10 @@ export class SingleUseTestCollection implements IDisposable { } item.discoverCts?.dispose(true); - this.testIdToInternal.object.delete(item.item.extId); + this.tree.delete(item.item.extId); this.testItemToInternal.delete(item.actual); for (const child of item.actual.children.values()) { - queue.push(this.testIdToInternal.object.get(child.id)); + queue.push(this.tree.get(child.id)); } } } @@ -500,19 +461,4 @@ export class SingleUseTestCollection implements IDisposable { this.diffOpEmitter.fire(diff); } } - - /** - * Returns a diff sufficient to "revive" the collection to its current - * state. - */ - public reviveDiff() { - this.flushDiff(); // flush to synchronize so we don't later replay unsent data - - const diff: TestsDiff = []; - for (const { parent, src, expand, item } of this.testItemToInternal.values()) { - diff.push([TestDiffOpType.Add, { parent, src, expand, item }]); - } - - return diff; - } } diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 8bdf7e1ee5b..b3d0481425c 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -9,13 +9,14 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { TestMessageSeverity, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -export type TestIdWithSrc = Required; - -export interface TestIdWithMaybeSrc { +export interface ITestIdWithSrc { testId: string; - src?: { controller: string; tree: number }; + controllerId: string; } +export const identifyTest = (test: { controllerId: string, item: { extId: string } }): ITestIdWithSrc => + ({ testId: test.item.extId, controllerId: test.controllerId }); + /** * Defines the path to a test, as a list of test IDs. The last element of the * array is the test ID, and the predecessors are its parents, in order. @@ -26,7 +27,7 @@ export type TestIdPath = string[]; * Request to the main thread to run a set of tests. */ export interface RunTestsRequest { - tests: TestIdWithMaybeSrc[]; + tests: ITestIdWithSrc[]; exclude?: string[]; debug: boolean; isAutoRun?: boolean; @@ -46,10 +47,11 @@ export interface ExtensionRunTestsRequest { /** * Request from the main thread to run tests for a single controller. */ -export interface RunTestForProviderRequest { +export interface RunTestForControllerRequest { runId: string; + controllerId: string; excludeExtIds: string[]; - tests: TestIdWithSrc[]; + testIds: string[]; debug: boolean; } @@ -108,7 +110,7 @@ export const enum TestItemExpandState { * TestItem-like shape, butm with an ID and children as strings. */ export interface InternalTestItem { - src: { controller: string; tree: number }; + controllerId: string; expand: TestItemExpandState; parent: string | null; item: ITestItem; @@ -152,6 +154,8 @@ export interface TestResultItem { ownDuration?: number; /** True if the test was directly requested by the run (is not a child or parent) */ direct?: boolean; + /** Controller ID from whence this test came */ + controllerId: string; } export type SerializedTestResultItem = Omit @@ -255,7 +259,7 @@ export abstract class AbstractIncrementalTestCollection(); + protected readonly roots = new Set(); /** * Number of 'busy' controllers. @@ -278,8 +282,8 @@ export abstract class AbstractIncrementalTestCollection[] = [[op[1]]]; diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index 5c6afa47e74..523cf7cea50 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -208,8 +208,9 @@ interface TestResultItemWithChildren extends TestResultItem { children: TestResultItemWithChildren[]; } -const itemToNode = (item: ITestItem, parent: string | null): TestResultItemWithChildren => ({ +const itemToNode = (controllerId: string, item: ITestItem, parent: string | null): TestResultItemWithChildren => ({ parent, + controllerId, item: { ...item }, children: [], tasks: [], @@ -334,14 +335,14 @@ export class LiveTestResult implements ITestResult { * Add the chain of tests to the run. The first test in the chain should * be either a test root, or a previously-known test. */ - public addTestChainToRun(chain: ReadonlyArray) { + public addTestChainToRun(controllerId: string, chain: ReadonlyArray) { let parent = this.testById.get(chain[0].extId); if (!parent) { // must be a test root - parent = this.addTestToRun(chain[0], null); + parent = this.addTestToRun(controllerId, chain[0], null); } for (let i = 1; i < chain.length; i++) { - parent = this.addTestToRun(chain[i], parent.item.extId); + parent = this.addTestToRun(controllerId, chain[i], parent.item.extId); } for (let i = 0; i < this.tasks.length; i++) { @@ -492,8 +493,8 @@ export class LiveTestResult implements ITestResult { ); } - private addTestToRun(item: ITestItem, parent: string | null) { - const node = itemToNode(item, parent); + private addTestToRun(controllerId: string, item: ITestItem, parent: string | null) { + const node = itemToNode(controllerId, item, parent); node.direct = this.includedIds.has(item.extId); this.testById.set(item.extId, node); this.counts[TestResultState.Unset]++; diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 688c80c38c0..c2be63937f2 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -203,7 +203,9 @@ export class TestResultService implements ITestResultService { this._results = keep; this.persistScheduler.schedule(); - this.hasAnyResults.set(false); + if (keep.length === 0) { + this.hasAnyResults.set(false); + } this.changeResultEmitter.fire({ removed }); } diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 4c626df8fa1..ccb7dfdf461 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -5,45 +5,40 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdPath, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestIdWithSrc, RunTestForControllerRequest, RunTestsRequest, TestIdPath, TestItemExpandState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import * as extpath from 'vs/base/common/extpath'; +import { Iterable } from 'vs/base/common/iterator'; export const ITestService = createDecorator('testService'); export interface MainTestController { - expandTest(src: TestIdWithSrc, levels: number): Promise; - lookupTest(test: TestIdWithSrc): Promise; - runTests(request: RunTestForProviderRequest, token: CancellationToken): Promise; + expandTest(src: ITestIdWithSrc, levels: number): Promise; + runTests(request: RunTestForControllerRequest, token: CancellationToken): Promise; } export type TestDiffListener = (diff: TestsDiff) => void; export interface IMainThreadTestCollection extends AbstractIncrementalTestCollection { - onPendingRootProvidersChange: Event; onBusyProvidersChange: Event; - /** - * Number of test root sources who are yet to report. - */ - pendingRootProviders: number; - /** * Number of providers working to discover tests. */ busyProviders: number; /** - * Root node IDs. + * Root items, correspond to registered controllers. */ - rootIds: ReadonlySet; + rootItems: Iterable; /** - * Iterates over every test in the collection. + * Iterates over every test in the collection, in strictly descending + * order of depth. */ all: Iterable; @@ -76,22 +71,11 @@ export const getCollectionItemParents = function* (collection: IMainThreadTestCo } }; -export const waitForAllRoots = (collection: IMainThreadTestCollection, ct = CancellationToken.None) => { - if (collection.pendingRootProviders === 0 || ct.isCancellationRequested) { - return Promise.resolve(); - } +const expandFirstLevel = (collection: IMainThreadTestCollection) => + Promise.all([...collection.rootItems].map(r => collection.expand(r.item.extId, 0))); - const disposable = new DisposableStore(); - return new Promise(resolve => { - disposable.add(collection.onPendingRootProvidersChange(count => { - if (count === 0) { - resolve(); - } - })); - - disposable.add(ct.onCancellationRequested(() => resolve())); - }).finally(() => disposable.dispose()); -}; +export const testCollectionIsEmpty = (collection: IMainThreadTestCollection) => + !Iterable.some(collection.rootItems, r => r.children.size > 0); /** * Ensures the test with the given path exists in the collection, if possible. @@ -99,11 +83,9 @@ export const waitForAllRoots = (collection: IMainThreadTestCollection, ct = Canc * undefined. */ export const getTestByPath = async (collection: IMainThreadTestCollection, idPath: TestIdPath, ct = CancellationToken.None) => { - await waitForAllRoots(collection, ct); - // Expand all direct children since roots might well have different IDs, but // children should start matching. - await Promise.all([...collection.rootIds].map(r => collection.expand(r, 0))); + await expandFirstLevel(collection); if (ct.isCancellationRequested) { return undefined; @@ -134,8 +116,6 @@ export const getTestByPath = async (collection: IMainThreadTestCollection, idPat * If cancellation is requested, it will return early. */ export const getAllTestsInHierarchy = async (collection: IMainThreadTestCollection, ct = CancellationToken.None) => { - await waitForAllRoots(collection, ct); - if (ct.isCancellationRequested) { return; } @@ -143,11 +123,36 @@ export const getAllTestsInHierarchy = async (collection: IMainThreadTestCollecti let l: IDisposable; await Promise.race([ - Promise.all([...collection.rootIds].map(r => collection.expand(r, Infinity))), + Promise.all([...collection.rootItems].map(r => collection.expand(r.item.extId, Infinity))), new Promise(r => { l = ct.onCancellationRequested(r); }), ]).finally(() => l?.dispose()); }; +/** + * Iterator that expands to and iterates through tests in the file. Iterates + * in strictly descending order. + */ +export const testsInFile = async function* (collection: IMainThreadTestCollection, uri: URI) { + // Expand all direct children since roots will not have URIs, but children should. + await expandFirstLevel(collection); + + const demandUriStr = uri.toString(); + for (const test of collection.all) { + if (!test.item.uri) { + continue; + } + + const itemUriStr = test.item.uri.toString(); + if (itemUriStr === demandUriStr) { + yield test; + } + + if (extpath.isEqualOrParent(demandUriStr, itemUriStr) && test.expand === TestItemExpandState.Expandable) { + await collection.expand(test.item.extId, 1); + } + } +}; + /** * An instance of the RootProvider should be registered for each extension * host. @@ -158,22 +163,27 @@ export interface ITestRootProvider { export interface ITestService { readonly _serviceBrand: undefined; - readonly onShouldSubscribe: Event<{ resource: ExtHostTestingResource, uri: URI; }>; - readonly onShouldUnsubscribe: Event<{ resource: ExtHostTestingResource, uri: URI; }>; - readonly onDidChangeProviders: Event<{ delta: number; }>; /** * Fires when the user requests to cancel a test run -- or all runs, if no * runId is given. */ - readonly onCancelTestRun: Event<{ runId: string | undefined; }>; - readonly providers: number; - readonly subscriptions: ReadonlyArray<{ resource: ExtHostTestingResource, uri: URI; }>; + readonly onDidCancelTestRun: Event<{ runId: string | undefined; }>; /** * Set of test IDs the user asked to exclude. */ readonly excludeTests: MutableObservableValue>; + /** + * Test collection instance. + */ + readonly collection: IMainThreadTestCollection; + + /** + * Event that fires after a diff is processed. + */ + readonly onDidProcessDiff: Event; + /** * Sets whether a test is excluded. */ @@ -184,14 +194,6 @@ export interface ITestService { */ clearExcludedTests(): void; - /** - * Updates the number of sources who provide test roots when subscription - * is requested. This is equal to the number of extension hosts, and used - * with `TestDiffOpType.DeltaRootsComplete` to signal when all roots - * are available. - */ - registerRootProvider(provider: ITestRootProvider): IDisposable; - /** * Registers an interface that runs tests for the given provider ID. */ @@ -207,14 +209,10 @@ export interface ITestService { */ cancelTestRun(runId?: string): void; - publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void; - subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference; - - /** - * Looks up a test, by a request to extension hosts. + * Publishes a test diff for a controller. */ - lookupTest(test: TestIdWithSrc): Promise; + publishDiff(controllerId: string, diff: TestsDiff): void; /** * Requests to resubscribe to all active subscriptions, discarding old tests. diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 07a7103a567..a9bf1a2ea8a 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -3,48 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { groupBy, mapFind } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { groupBy } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; -import { isDefined } from 'vs/base/common/types'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { RunTestsRequest, ITestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestCollection, ITestRootProvider, ITestService, MainTestController, TestDiffListener } from 'vs/workbench/contrib/testing/common/testService'; - -type TestLocationIdent = { resource: ExtHostTestingResource, uri: URI }; - -const workspaceUnsubscribeDelay = 30_000; -const documentUnsubscribeDelay = 5_000; +import { ITestService, MainTestController } from 'vs/workbench/contrib/testing/common/testService'; export class TestService extends Disposable implements ITestService { declare readonly _serviceBrand: undefined; private testControllers = new Map(); - private readonly testSubscriptions = new Map; - disposeTimeout?: IDisposable, - listeners: number; - }>(); - private readonly subscribeEmitter = new Emitter(); - private readonly unsubscribeEmitter = new Emitter(); - private readonly busyStateChangeEmitter = new Emitter(); - private readonly changeProvidersEmitter = new Emitter<{ delta: number }>(); private readonly cancelExtensionTestRunEmitter = new Emitter<{ runId: string | undefined }>(); + private readonly processDiffEmitter = new Emitter(); private readonly providerCount: IContextKey; private readonly hasRunnable: IContextKey; private readonly hasDebuggable: IContextKey; @@ -53,8 +36,10 @@ export class TestService extends Disposable implements ITestService { * Test runs initiated by extensions are not included here. */ private readonly uiRunningTests = new Map(); - private readonly rootProviders = new Set(); + /** + * @inheritdoc + */ public readonly excludeTests = MutableObservableValue.stored(new StoredValue>({ key: 'excludedTestItems', scope: StorageScope.WORKSPACE, @@ -65,6 +50,21 @@ export class TestService extends Disposable implements ITestService { }, }, this.storageService), new Set()); + /** + * @inheritdoc + */ + public readonly onDidProcessDiff = this.processDiffEmitter.event; + + /** + * @inheritdoc + */ + public readonly onDidCancelTestRun = this.cancelExtensionTestRunEmitter.event; + + /** + * @inheritdoc + */ + public readonly collection = new MainThreadTestCollection(this.expandTest.bind(this)); + constructor( @IContextKeyService contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @@ -81,8 +81,8 @@ export class TestService extends Disposable implements ITestService { /** * @inheritdoc */ - public async expandTest(test: TestIdWithSrc, levels: number) { - await this.testControllers.get(test.src.controller)?.expandTest(test, levels); + public async expandTest(test: ITestIdWithSrc, levels: number) { + await this.testControllers.get(test.controllerId)?.expandTest(test, levels); } /** @@ -108,44 +108,6 @@ export class TestService extends Disposable implements ITestService { } } - /** - * Gets the current provider count. - */ - public get providers() { - return this.providerCount.get() || 0; - } - - /** - * Fired when extension hosts should pull events from their test factories. - */ - public readonly onShouldSubscribe = this.subscribeEmitter.event; - - /** - * Fired when extension hosts should stop pulling events from their test factories. - */ - public readonly onShouldUnsubscribe = this.unsubscribeEmitter.event; - - /** - * Fired when the number of providers change. - */ - public readonly onDidChangeProviders = this.changeProvidersEmitter.event; - - /** - * @inheritdoc - */ - public readonly onBusyStateChange = this.busyStateChangeEmitter.event; - - /** - * @inheritdoc - */ - public readonly onCancelTestRun = this.cancelExtensionTestRunEmitter.event; - - /** - * @inheritdoc - */ - public get subscriptions() { - return [...this.testSubscriptions].map(([, s]) => s.ident); - } /** * @inheritdoc @@ -162,43 +124,6 @@ export class TestService extends Disposable implements ITestService { } } - /** - * @inheritdoc - */ - public async lookupTest(test: TestIdWithSrc) { - for (const { collection } of this.testSubscriptions.values()) { - const node = collection.getNodeById(test.testId); - if (node) { - return node; - } - } - - return this.testControllers.get(test.src.controller)?.lookupTest(test); - } - - /** - * @inheritdoc - */ - public registerRootProvider(provider: ITestRootProvider) { - if (this.rootProviders.has(provider)) { - return toDisposable(() => { }); - } - - this.rootProviders.add(provider); - for (const { collection } of this.testSubscriptions.values()) { - collection.updatePendingRoots(1); - } - - return toDisposable(() => { - if (this.rootProviders.delete(provider)) { - for (const { collection } of this.testSubscriptions.values()) { - collection.updatePendingRoots(-1); - } - } - }); - } - - /** * @inheritdoc */ @@ -217,31 +142,19 @@ export class TestService extends Disposable implements ITestService { return result; } - const testsWithIds = req.tests.map(test => { - if (test.src) { - return test as TestIdWithSrc; - } - - const subscribed = mapFind(this.testSubscriptions.values(), s => s.collection.getNodeById(test.testId)); - if (!subscribed) { - return undefined; - } - - return { testId: test.testId, src: subscribed.src }; - }).filter(isDefined); - try { - const tests = groupBy(testsWithIds, (a, b) => a.src.controller === b.src.controller ? 0 : 1); + const tests = groupBy(req.tests, (a, b) => a.controllerId === b.controllerId ? 0 : 1); const cancelSource = new CancellationTokenSource(token); this.uiRunningTests.set(result.id, cancelSource); const requests = tests.map( - group => this.testControllers.get(group[0].src.controller)?.runTests( + group => this.testControllers.get(group[0].controllerId)?.runTests( { runId: result.id, debug: req.debug, excludeExtIds: req.exclude ?? [], - tests: group, + testIds: group.map(g => g.testId), + controllerId: group[0].controllerId, }, cancelSource.token, ).catch(err => { @@ -261,88 +174,17 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public resubscribeToAllTests() { - for (const subscription of this.testSubscriptions.values()) { - this.unsubscribeEmitter.fire(subscription.ident); - const diff = subscription.collection.clear(); - subscription.onDiff.fire(diff); - subscription.collection.pendingRootProviders = this.rootProviders.size; - this.subscribeEmitter.fire(subscription.ident); - } + // todo } /** * @inheritdoc */ - public subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference { - const subscriptionKey = getTestSubscriptionKey(resource, uri); - let subscription = this.testSubscriptions.get(subscriptionKey); - if (!subscription) { - subscription = { - ident: { resource, uri }, - collection: new MainThreadTestCollection( - this.rootProviders.size, - this.expandTest.bind(this), - ), - listeners: 0, - onDiff: new Emitter(), - }; - - subscription.collection.onDidRetireTest(testId => { - for (const result of this.testResults.results) { - if (result instanceof LiveTestResult) { - result.retire(testId); - } - } - }); - - this.subscribeEmitter.fire({ resource, uri }); - this.testSubscriptions.set(subscriptionKey, subscription); - } else if (subscription.disposeTimeout) { - subscription.disposeTimeout.dispose(); - subscription.disposeTimeout = undefined; - } - - subscription.listeners++; - - if (acceptDiff) { - acceptDiff(subscription.collection.getReviverDiff()); - } - - const listener = acceptDiff && subscription.onDiff.event(acceptDiff); - return { - object: subscription.collection, - dispose: () => { - listener?.dispose(); - - if (--subscription!.listeners > 0) { - return; - } - - - subscription!.disposeTimeout = disposableTimeout( - () => { - this.unsubscribeEmitter.fire({ resource, uri }); - this.testSubscriptions.delete(subscriptionKey); - }, - resource === ExtHostTestingResource.TextDocument ? documentUnsubscribeDelay : workspaceUnsubscribeDelay, - ); - } - }; - } - - /** - * @inheritdoc - */ - public publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff) { - const sub = this.testSubscriptions.get(getTestSubscriptionKey(resource, URI.revive(uri))); - if (!sub) { - return; - } - - sub.collection.apply(diff); - sub.onDiff.fire(diff); - this.hasDebuggable.set(!!this.findTest(t => t.item.debuggable)); - this.hasRunnable.set(!!this.findTest(t => t.item.runnable)); + public publishDiff(_controllerId: string, diff: TestsDiff) { + this.collection.apply(diff); + this.hasDebuggable.set(Iterable.some(this.collection.all, t => t.item.debuggable)); + this.hasRunnable.set(Iterable.some(this.collection.all, t => t.item.runnable)); + this.processDiffEmitter.fire(diff); } /** @@ -351,193 +193,13 @@ export class TestService extends Disposable implements ITestService { public registerTestController(id: string, controller: MainTestController): IDisposable { this.testControllers.set(id, controller); this.providerCount.set(this.testControllers.size); - this.changeProvidersEmitter.fire({ delta: 1 }); return toDisposable(() => { if (this.testControllers.delete(id)) { this.providerCount.set(this.testControllers.size); - this.changeProvidersEmitter.fire({ delta: -1 }); } }); } - - private findTest(predicate: (t: InternalTestItem) => boolean): InternalTestItem | undefined { - for (const { collection } of this.testSubscriptions.values()) { - for (const test of collection.all) { - if (predicate(test)) { - return test; - } - } - } - - return undefined; - } } -export class MainThreadTestCollection extends AbstractIncrementalTestCollection implements IMainThreadTestCollection { - private pendingRootChangeEmitter = new Emitter(); - private busyProvidersChangeEmitter = new Emitter(); - private retireTestEmitter = new Emitter(); - private expandPromises = new WeakMap; - }>(); - /** - * @inheritdoc - */ - public get pendingRootProviders() { - return this.pendingRootCount; - } - - /** - * Sets the number of pending root providers. - */ - public set pendingRootProviders(count: number) { - this.pendingRootCount = count; - this.pendingRootChangeEmitter.fire(count); - } - - /** - * @inheritdoc - */ - public get busyProviders() { - return this.busyControllerCount; - } - - /** - * @inheritdoc - */ - public get rootIds() { - return this.roots; - } - - /** - * @inheritdoc - */ - public get all() { - return this.getIterator(); - } - - public readonly onPendingRootProvidersChange = this.pendingRootChangeEmitter.event; - public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event; - public readonly onDidRetireTest = this.retireTestEmitter.event; - - constructor(pendingRootProviders: number, private readonly expandActual: (src: TestIdWithSrc, levels: number) => Promise) { - super(); - this.pendingRootCount = pendingRootProviders; - } - - /** - * @inheritdoc - */ - public expand(testId: string, levels: number): Promise { - const test = this.items.get(testId); - if (!test) { - return Promise.resolve(); - } - - // simple cache to avoid duplicate/unnecessary expansion calls - const existing = this.expandPromises.get(test); - if (existing && existing.pendingLvl >= levels) { - return existing.prom; - } - - const prom = this.expandActual({ src: test.src, testId: test.item.extId }, levels); - const record = { doneLvl: existing ? existing.doneLvl : -1, pendingLvl: levels, prom }; - this.expandPromises.set(test, record); - - return prom.then(() => { - record.doneLvl = levels; - }); - } - - /** - * @inheritdoc - */ - public getNodeById(id: string) { - return this.items.get(id); - } - - /** - * @inheritdoc - */ - public getReviverDiff() { - const ops: TestsDiff = [[TestDiffOpType.IncrementPendingExtHosts, this.pendingRootCount]]; - - const queue = [this.roots]; - while (queue.length) { - for (const child of queue.pop()!) { - const item = this.items.get(child)!; - ops.push([TestDiffOpType.Add, { - src: item.src, - expand: item.expand, - item: item.item, - parent: item.parent, - }]); - queue.push(item.children); - } - } - - return ops; - } - - - /** - * Applies the diff to the collection. - */ - public override apply(diff: TestsDiff) { - let prevBusy = this.busyControllerCount; - let prevPendingRoots = this.pendingRootCount; - super.apply(diff); - - if (prevBusy !== this.busyControllerCount) { - this.busyProvidersChangeEmitter.fire(this.busyControllerCount); - } - if (prevPendingRoots !== this.pendingRootCount) { - this.pendingRootChangeEmitter.fire(this.pendingRootCount); - } - } - - /** - * Clears everything from the collection, and returns a diff that applies - * that action. - */ - public clear() { - const ops: TestsDiff = []; - for (const root of this.roots) { - ops.push([TestDiffOpType.Remove, root]); - } - - this.roots.clear(); - this.items.clear(); - - return ops; - } - - /** - * @override - */ - protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem { - return { ...internal, children: new Set() }; - } - - /** - * @override - */ - protected override retireTest(testId: string) { - this.retireTestEmitter.fire(testId); - } - - private *getIterator() { - const queue = [this.rootIds]; - while (queue.length) { - for (const id of queue.pop()!) { - const node = this.getNodeById(id)!; - yield node; - queue.push(node.children); - } - } - } -} diff --git a/src/vs/workbench/contrib/testing/common/testStubs.ts b/src/vs/workbench/contrib/testing/common/testStubs.ts index ac98d283bd3..1d307ee4dd3 100644 --- a/src/vs/workbench/contrib/testing/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/common/testStubs.ts @@ -3,65 +3,47 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { TestItemImpl, TestItemStatus, TestResultState } from 'vs/workbench/api/common/extHostTypes'; +import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; +import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; -export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes'; export * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; +export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl[] = [], uri = URI.file('/')): TestItemImpl => { - const item = new TestItemImpl(idPrefix + label, label, uri, undefined); - if (children.length) { - item.status = TestItemStatus.Pending; - item.resolveHandler = () => { - for (const child of children) { - item.addChild(child); + + +/** + * Gets a main thread test collection initialized with the given set of + * roots/stubs. + */ +export const getInitializedMainTestCollection = async (singleUse = testStubs.nested()) => { + const c = new MainThreadTestCollection(async (t, l) => singleUse.expand(t.testId, l)); + await singleUse.expand(singleUse.root.id, Infinity); + c.apply(singleUse.collectDiff()); + return c; +}; + +export const testStubs = { + nested: (idPrefix = 'id-') => { + const collection = new TestSingleUseCollection('ctrlId'); + collection.root.label = 'root'; + collection.root.status = TestItemStatus.Pending; + collection.resolveHandler = item => { + if (item === collection.root) { + const a = new TestItemImpl(idPrefix + 'a', 'a', URI.file('/'), undefined, collection.root); + a.status = TestItemStatus.Pending; + new TestItemImpl(idPrefix + 'b', 'b', URI.file('/'), undefined, collection.root); + } else if (item.id === idPrefix + 'a') { + new TestItemImpl(idPrefix + 'aa', 'aa', URI.file('/'), undefined, item); + new TestItemImpl(idPrefix + 'ab', 'ab', URI.file('/'), undefined, item); } item.status = TestItemStatus.Resolved; }; - } - return item; -}; - -export const expandAllStubs = (stub: TestItemImpl) => { - const todo = [[stub]]; - for (const stubs of todo) { - for (const stub of stubs) { - if (stub.status !== TestItemStatus.Resolved) { - stub.resolveHandler!(CancellationToken.None); - todo.push([...stub.children.values()]); - } - } - } -}; - -export const testStubsChain = (stub: TestItemImpl, path: string[], slice = 0) => { - const tests = [stub]; - for (const segment of path) { - if (stub.status !== TestItemStatus.Resolved) { - stub.resolveHandler!(CancellationToken.None); - } - - stub = stub.children.get(segment)!; - if (!stub) { - throw new Error(`missing child ${segment}`); - } - - tests.push(stub); - } - - return tests.slice(slice); -}; - -export const testStubs = { - test: stubTest, - nested: (idPrefix = 'id-') => stubTest('root', idPrefix, [ - stubTest('a', idPrefix, [stubTest('aa', idPrefix), stubTest('ab', idPrefix)]), - stubTest('b', idPrefix), - ]), + return collection; + }, }; export const ReExportedTestRunState = TestResultState; diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts index 0751f8bb22b..6bf6c441182 100644 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts @@ -11,12 +11,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { TestDiffOpType, TestIdWithMaybeSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { identifyTest, ITestIdWithSrc, TestDiffOpType } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { isRunningTests, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { getCollectionItemParents, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; export interface ITestingAutoRun { /** @@ -36,7 +35,6 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { @ITestService private readonly testService: ITestService, @ITestResultService private readonly results: ITestResultService, @IConfigurationService private readonly configuration: IConfigurationService, - @IWorkspaceTestCollectionService private readonly workspaceTests: IWorkspaceTestCollectionService, ) { super(); this.enabled = TestingContextKeys.autoRun.bindTo(contextKeyService); @@ -67,7 +65,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { * Runs them on a debounce. */ private makeRunner() { - const rerunIds = new Map(); + const rerunIds = new Map(); const store = new DisposableStore(); const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); @@ -92,32 +90,25 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { } }, delay)); - const addToRerun = (test: TestIdWithMaybeSrc) => { - rerunIds.set(identifyTest(test), test); + const addToRerun = (test: ITestIdWithSrc) => { + rerunIds.set(getTestKey(test), test); if (!isRunningTests(this.results)) { scheduler.schedule(delay); } }; - const removeFromRerun = (test: TestIdWithMaybeSrc) => { - const id = identifyTest(test); - if (test.src) { - rerunIds.delete(id); - return; - } - - for (const test of rerunIds.keys()) { - if (test.startsWith(id)) { - rerunIds.delete(test); - } + const removeFromRerun = (test: ITestIdWithSrc) => { + rerunIds.delete(getTestKey(test)); + if (rerunIds.size === 0) { + scheduler.cancel(); } }; store.add(this.results.onTestChanged(evt => { if (evt.reason === TestResultItemChangeReason.Retired) { - addToRerun({ testId: evt.item.item.extId }); + addToRerun(identifyTest(evt.item)); } else if ((evt.reason === TestResultItemChangeReason.OwnStateChange || evt.reason === TestResultItemChangeReason.ComputedStateChange)) { - removeFromRerun({ testId: evt.item.item.extId }); + removeFromRerun(identifyTest(evt.item)); } })); @@ -128,39 +119,31 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { })); if (getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunMode) === AutoRunMode.AllInWorkspace) { - const listener = this.workspaceTests.subscribeToWorkspaceTests(); - store.add(listener); - listener.waitForAllRoots(cts.token).then(() => { - if (!cts.token.isCancellationRequested) { - for (const collection of listener.workspaceFolderCollections.values()) { - for (const rootId of collection.rootIds) { - const root = collection.getNodeById(rootId); - if (root) { addToRerun({ testId: root.item.extId, src: root.src }); } - } - } - } - }); - - store.add(listener.onDiff(({ diff, folder }) => { + store.add(this.testService.onDidProcessDiff(diff => { for (const entry of diff) { if (entry[0] === TestDiffOpType.Add) { const test = entry[1]; const isQueued = Iterable.some( - getCollectionItemParents(folder.collection, test), - t => rerunIds.has(identifyTest({ testId: t.item.extId, src: t.src })), + getCollectionItemParents(this.testService.collection, test), + t => rerunIds.has(getTestKey(identifyTest(test))), ); if (!isQueued) { - addToRerun({ testId: test.item.extId, src: test.src }); + addToRerun(identifyTest(test)); } } } })); + + + for (const root of this.testService.collection.rootItems) { + addToRerun(identifyTest(root)); + } } return store; } } -const identifyTest = (test: TestIdWithMaybeSrc) => test.src ? `${test.testId}\0${test.src.controller}` : `${test.testId}\0`; +const getTestKey = (test: ITestIdWithSrc) => `${test.controllerId}\0${test.testId}`; diff --git a/src/vs/workbench/contrib/testing/common/workspaceTestCollectionService.ts b/src/vs/workbench/contrib/testing/common/workspaceTestCollectionService.ts deleted file mode 100644 index 467daaf9e6f..00000000000 --- a/src/vs/workbench/contrib/testing/common/workspaceTestCollectionService.ts +++ /dev/null @@ -1,314 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; -import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; -import { IncrementalTestCollectionItem, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { IMainThreadTestCollection, ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService'; - -export interface ITestSubscriptionFolder { - folder: IWorkspaceFolder; - collection: IMainThreadTestCollection; - getChildren(): Iterable; -} - -export interface ITestSubscriptionItem extends IncrementalTestCollectionItem { - root: ITestSubscriptionFolder; -} - -export class TestSubscriptionListener extends Disposable { - public static override readonly None = new TestSubscriptionListener({ - busyProviders: 0, - onBusyProvidersChange: Event.None, - pendingRootProviders: 0, - workspaceFolderCollections: new Map(), - onDiff: Event.None, - onFolderChange: Event.None, - }, () => undefined); - - public get busyProviders() { - return this.subscription.busyProviders; - } - - public get pendingRootProviders() { - return this.subscription.pendingRootProviders; - } - - /** - * Returns whether there are any subscriptions with non-empty providers. - */ - public get isEmpty() { - for (const collection of this.workspaceFolderCollections.values()) { - if (Iterable.some(collection.all, t => !!t.parent)) { - return false; - } - } - - return true; - } - - public get workspaceFolderCollections() { - return this.subscription.workspaceFolderCollections; - } - - public readonly onBusyProvidersChange = this.subscription.onBusyProvidersChange; - public readonly onFolderChange = this.subscription.onFolderChange; - public readonly onDiff = this.subscription.onDiff; - - constructor( - private readonly subscription: TestSubscription, - onDispose: () => void, - ) { - super(); - this._register(toDisposable(onDispose)); - } - - public async waitForAllRoots(token?: CancellationToken) { - await Promise.all([...this.subscription.workspaceFolderCollections.values()].map( - (c) => waitForAllRoots(c, token), - )); - } -} - -/** - * Maintains an observable set of tests in the core. - */ -export interface IWorkspaceTestCollectionService { - readonly _serviceBrand: undefined; - - /** - * Gets all workspace folders we're listening to. - */ - workspaceFolders(): ReadonlyArray; - - /** - * Adds a listener that receives updates about tests. - */ - subscribeToWorkspaceTests(): TestSubscriptionListener; - - /** - * A pass-through method that creates a subscription listener for a document. - * Useful if you need the same TestSubscriptionListener shape, but otherwise - * you can `ITestService.subscribeToDiffs` directly. - */ - subscribeToDocumentTests(documentUri: URI): TestSubscriptionListener; -} - -export const IWorkspaceTestCollectionService = createDecorator('ITestingViewService'); - -export class WorkspaceTestCollectionService implements IWorkspaceTestCollectionService { - declare _serviceBrand: undefined; - - private subscription?: WorkspaceTestSubscription; - - public workspaceFolders() { - return this.subscription?.workspaceFolders || []; - } - - constructor( - @IInstantiationService protected instantiationService: IInstantiationService, - @IWorkspaceContextService protected workspaceContext: IWorkspaceContextService, - @ITestService protected testService: ITestService, - ) { } - - /** - * @inheritdoc - */ - public subscribeToWorkspaceTests(): TestSubscriptionListener { - if (!this.subscription) { - this.subscription = this.instantiationService.createInstance(WorkspaceTestSubscription); - } - - const listener = new TestSubscriptionListener(this.subscription, () => { - if (!this.subscription) { - return; - } - - this.subscription.removeListener(listener); - if (this.subscription.listenerCount === 0) { - this.subscription.dispose(); - this.subscription = undefined; - } - }); - - this.subscription.addListener(listener); - return listener; - } - - /** - * @inheritdoc - */ - public subscribeToDocumentTests(documentUri: URI): TestSubscriptionListener { - const folder = this.workspaceContext.getWorkspaceFolder(documentUri) - || this.workspaceContext.getWorkspace().folders[0]; - if (!folder) { - return TestSubscriptionListener.None; - } - - const subFolder: ITestSubscriptionFolder = { - folder, - get collection() { - return sub.object; - }, - getChildren: () => sub.object.all, - }; - - const store = new DisposableStore(); - const diffEmitter = store.add(new Emitter<{ folder: ITestSubscriptionFolder, diff: TestsDiff }>()); - const onDiff = (diff: TestsDiff) => diffEmitter.fire({ diff, folder: subFolder }); - const sub = store.add(this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, documentUri, onDiff)); - - return new TestSubscriptionListener({ - get busyProviders() { return sub.object.busyProviders; }, - onBusyProvidersChange: sub.object.onBusyProvidersChange, - get pendingRootProviders() { return sub.object.pendingRootProviders; }, - workspaceFolderCollections: new Map([[subFolder, sub.object]]), - onDiff: diffEmitter.event, - onFolderChange: Event.None, - }, () => store.dispose()); - } -} - - -export interface TestSubscription { - readonly onBusyProvidersChange: Event; - readonly busyProviders: number; - readonly pendingRootProviders: number; - readonly workspaceFolderCollections: Map; - readonly onFolderChange: Event; - readonly onDiff: Event<{ folder: ITestSubscriptionFolder, diff: TestsDiff }>; -} - -class WorkspaceTestSubscription extends Disposable implements TestSubscription { - private onDiffEmitter = this._register(new Emitter<{ folder: ITestSubscriptionFolder, diff: TestsDiff }>()); - private onFolderChangeEmitter = this._register(new Emitter()); - - private listeners = new Set(); - private pendingRootChangeEmitter = this._register(new Emitter()); - private busyProvidersChangeEmitter = this._register(new Emitter()); - private readonly collectionsForWorkspaces = new Map(); - - public readonly onPendingRootProvidersChange = this.pendingRootChangeEmitter.event; - public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event; - public readonly onDiff = this.onDiffEmitter.event; - public readonly onFolderChange = this.onFolderChangeEmitter.event; - - public get busyProviders() { - let total = 0; - for (const { collection } of this.collectionsForWorkspaces.values()) { - total += collection.busyProviders; - } - - return total; - } - - public get pendingRootProviders() { - let total = 0; - for (const { collection } of this.collectionsForWorkspaces.values()) { - total += collection.pendingRootProviders; - } - - return total; - } - - public get listenerCount() { - return this.listeners.size; - } - - public get workspaceFolders() { - return [...this.collectionsForWorkspaces.values()].map(v => v.folder); - } - - public get workspaceFolderCollections() { - return new Map([...this.collectionsForWorkspaces.values()].map(v => [v.folder, v.collection] as const)); - } - - constructor( - @IWorkspaceContextService workspaceContext: IWorkspaceContextService, - @ITestService private readonly testService: ITestService, - ) { - super(); - - this._register(toDisposable(() => { - for (const { listener } of this.collectionsForWorkspaces.values()) { - listener.dispose(); - } - })); - - this._register(workspaceContext.onDidChangeWorkspaceFolders(evt => { - for (const folder of evt.added) { - this.subscribeToWorkspace(folder); - } - - for (const folder of evt.removed) { - const existing = this.collectionsForWorkspaces.get(folder.uri.toString()); - if (existing) { - this.collectionsForWorkspaces.delete(folder.uri.toString()); - existing.listener.dispose(); - } - } - - this.onFolderChangeEmitter.fire(evt); - })); - - for (const folder of workspaceContext.getWorkspace().folders) { - this.subscribeToWorkspace(folder); - } - } - - public addListener(listener: TestSubscriptionListener) { - this.listeners.add(listener); - } - - public removeListener(listener: TestSubscriptionListener) { - this.listeners.delete(listener); - } - - private subscribeToWorkspace(folder: IWorkspaceFolder) { - const folderNode: ITestSubscriptionFolder = { - folder, - get collection() { - return ref.object; - }, - getChildren: function* () { - for (const rootId of ref.object.rootIds) { - const node = ref.object.getNodeById(rootId); - if (node) { - yield node; - } - } - }, - }; - - const ref = this.testService.subscribeToDiffs( - ExtHostTestingResource.Workspace, - folder.uri, - diff => this.onDiffEmitter.fire({ folder: folderNode, diff }), - ); - - const disposable = new DisposableStore(); - disposable.add(ref); - disposable.add(ref.object.onBusyProvidersChange( - () => this.pendingRootChangeEmitter.fire(this.pendingRootProviders))); - disposable.add(ref.object.onBusyProvidersChange( - () => this.busyProvidersChangeEmitter.fire(this.busyProviders))); - - this.collectionsForWorkspaces.set(folder.uri.toString(), { - listener: disposable, - collection: ref.object, - folder: folderNode, - }); - } -} diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 2da17fe0d96..2d8e8aacb91 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -6,21 +6,16 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; -import { TestResultState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; -import { makeTestWorkspaceFolder, TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { Convert, TestItemImpl, TestResultState } from 'vs/workbench/contrib/testing/common/testStubs'; +import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; class TestHierarchicalByLocationProjection extends HierarchicalByLocationProjection { - public get folderNodes() { - return [...this.folders.values()]; - } } suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { let harness: TestTreeTestHarness; - const folder1 = makeTestWorkspaceFolder('f1'); - const folder2 = makeTestWorkspaceFolder('f2'); let onTestChanged: Emitter; let resultsService: any; @@ -32,7 +27,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { getStateById: () => ({ state: { state: 0 }, computedState: 0 }), }; - harness = new TestTreeTestHarness([folder1, folder2], l => new TestHierarchicalByLocationProjection(l, resultsService as any)); + harness = new TestTreeTestHarness(l => new TestHierarchicalByLocationProjection(l, resultsService as any)); }); teardown(() => { @@ -40,106 +35,82 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { }); test('renders initial tree', async () => { - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(folder1); + harness.flush(); assert.deepStrictEqual(harness.tree.getRendered(), [ { e: 'a' }, { e: 'b' } ]); }); test('expands children', async () => { - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(folder1); + harness.flush(); harness.tree.expand(harness.projection.getElementByTestId('id-a')!); - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' } ]); }); - test('updates render if a second folder is added', async () => { - harness.c.addRoot(testStubs.nested('id1-'), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.nested('id2-'), 'a'); - harness.flush(folder2); - assert.deepStrictEqual(harness.tree.getRendered(), [ - { e: 'f1', children: [{ e: 'a' }, { e: 'b' }] }, - { e: 'f2', children: [{ e: 'a' }, { e: 'b' }] }, - ]); - - harness.tree.expand(harness.projection.getElementByTestId('id1-a')!); - assert.deepStrictEqual(harness.flush(folder1), [ - { e: 'f1', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, - { e: 'f2', children: [{ e: 'a' }, { e: 'b' }] }, - ]); - }); - - test('updates render if second folder is removed', async () => { - harness.c.addRoot(testStubs.nested('id1-'), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.nested('id2-'), 'a'); - harness.flush(folder2); - harness.onFolderChange.fire({ added: [], changed: [], removed: [folder1] }); - assert.deepStrictEqual(harness.flush(folder1), [ - { e: 'a' }, { e: 'b' }, - ]); - }); - test('updates render if second test provider appears', async () => { - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.test('root2', undefined, [testStubs.test('c')]), 'b'); - assert.deepStrictEqual(harness.flush(folder1), [ - { e: 'root', children: [{ e: 'a' }, { e: 'b' }] }, - { e: 'root2', children: [{ e: 'c' }] }, + harness.flush(); + harness.pushDiff([ + TestDiffOpType.Add, + { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'c', undefined, undefined, undefined)) }, + ], [ + TestDiffOpType.Add, + { controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'ca', undefined, undefined, undefined)) }, + ]); + + assert.deepStrictEqual(harness.flush(), [ + { e: 'c', children: [{ e: 'ca' }] }, + { e: 'root', children: [{ e: 'a' }, { e: 'b' }] } ]); }); test('updates nodes if they add children', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); harness.tree.expand(harness.projection.getElementByTestId('id-a')!); - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' } ]); - tests.children.get('id-a')!.addChild(testStubs.test('ac')); + new TestItemImpl('ac', 'ac', undefined, undefined, harness.c.root.children.get('id-a')!); - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, { e: 'b' } ]); }); test('updates nodes if they remove children', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); harness.tree.expand(harness.projection.getElementByTestId('id-a')!); - tests.children.get('id-a')!.children.get('id-ab')!.dispose(); + assert.deepStrictEqual(harness.flush(), [ + { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, + { e: 'b' } + ]); - assert.deepStrictEqual(harness.flush(folder1), [ + harness.c.root.children.get('id-a')!.children.get('id-ab')!.dispose(); + + assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }] }, { e: 'b' } ]); }); test('applies state changes', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); resultsService.getStateById = () => [undefined, resultInState(TestResultState.Failed)]; const resultInState = (state: TestResultState): TestResultItem => ({ - item: harness.c.itemToInternal.get(tests.children.get('id-a')!)!.item, + item: harness.c.itemToInternal.get(harness.c.root.children.get('id-a')!)!.item, parent: 'id-root', tasks: [], retired: false, ownComputedState: state, computedState: state, + controllerId: 'ctrl', }); // Applies the change: diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index a2466891c13..fe70f44ea51 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -4,81 +4,62 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { timeout } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; -import { makeTestWorkspaceFolder, TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultItemChange } from 'vs/workbench/contrib/testing/common/testResult'; +import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/common/testStubs'; +import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { - let harness: TestTreeTestHarness; - const folder1 = makeTestWorkspaceFolder('f1'); - const folder2 = makeTestWorkspaceFolder('f2'); + let harness: TestTreeTestHarness; + let onTestChanged: Emitter; + let resultsService: any; + setup(() => { - harness = new TestTreeTestHarness([folder1, folder2], l => new HierarchicalByNameProjection(l, { + onTestChanged = new Emitter(); + resultsService = { onResultsChanged: () => undefined, - onTestChanged: () => undefined, + onTestChanged: onTestChanged.event, getStateById: () => ({ state: { state: 0 }, computedState: 0 }), - } as any)); + }; + + harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l, resultsService as any)); }); teardown(() => { harness.dispose(); }); - test('renders initial tree', async () => { - await timeout(1000); - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(folder1); + test('renders initial tree', () => { + harness.flush(); assert.deepStrictEqual(harness.tree.getRendered(), [ { e: 'aa' }, { e: 'ab' }, { e: 'b' } ]); }); - test('updates render if a second folder is added', async () => { - harness.c.addRoot(testStubs.nested('id1-'), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.nested('id2-'), 'a'); - harness.flush(folder2); - assert.deepStrictEqual(harness.flush(folder1), [ - { e: 'f1', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, - { e: 'f2', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, - ]); - }); - - test('updates render if second folder is removed', async () => { - harness.c.addRoot(testStubs.nested('id1-'), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.nested('id2-'), 'a'); - harness.flush(folder2); - harness.onFolderChange.fire({ added: [], changed: [], removed: [folder1] }); - assert.deepStrictEqual(harness.flush(folder1), [ - { e: 'aa' }, { e: 'ab' }, { e: 'b' }, - ]); - }); - test('updates render if second test provider appears', async () => { - await timeout(100); - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(folder1); - harness.c.addRoot(testStubs.test('root2', undefined, [testStubs.test('c')]), 'b'); - harness.flush(folder1); - await timeout(10); - harness.flush(folder1); - await timeout(10); - assert.deepStrictEqual(harness.tree.getRendered(), [ + harness.flush(); + harness.pushDiff([ + TestDiffOpType.Add, + { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'root2', undefined, undefined, undefined)) }, + ], [ + TestDiffOpType.Add, + { controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'c', undefined, undefined, undefined)) }, + ]); + + assert.deepStrictEqual(harness.flush(), [ { e: 'root', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }, { e: 'root2', children: [{ e: 'c' }] }, ]); }); test('updates nodes if they add children', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); - tests.children.get('id-a')!.addChild(testStubs.test('ac')); + new TestItemImpl('ac', 'ac', undefined, undefined, harness.c.root.children.get('id-a')!); - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, { e: 'ab' }, { e: 'ac' }, @@ -87,26 +68,20 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { }); test('updates nodes if they remove children', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); + harness.c.root.children.get('id-a')!.children.get('id-ab')!.dispose(); - tests.children.get('id-a')!.children.get('id-ab')!.dispose(); - - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, { e: 'b' } ]); }); test('swaps when node is no longer leaf', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); + new TestItemImpl('ba', 'ba', undefined, undefined, harness.c.root.children.get('id-b')!); - tests.children.get('id-b')!.addChild(testStubs.test('ba')); - - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, { e: 'ab' }, { e: 'ba' }, @@ -114,17 +89,11 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { }); test('swaps when node is no longer runnable', async () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(folder1); + harness.flush(); + const ba = new TestItemImpl('ba', 'ba', undefined, undefined, harness.c.root.children.get('id-b')!); + ba.runnable = false; - const child = testStubs.test('ba'); - tests.children.get('id-b')!.addChild(child); - harness.flush(folder1); - - child.runnable = false; - - assert.deepStrictEqual(harness.flush(folder1), [ + assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, { e: 'ab' }, { e: 'b' }, diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 62e54befe2e..5eac3e033eb 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -6,11 +6,12 @@ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspaceFolder, IWorkspaceFolderData, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; -import { TestOwnedTestCollection, TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; +import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; type SerializedTree = { e: string; children?: SerializedTree[], data?: string }; @@ -73,39 +74,31 @@ export class TestObjectTree extends ObjectTree { const pos = (element: Element) => Number(element.parentElement!.parentElement!.getAttribute('aria-posinset')); -export const makeTestWorkspaceFolder = (name: string): IWorkspaceFolder => ({ - name, - uri: URI.file(`/${name}`), - index: 0, - toResource: path => URI.file(`/${name}/${path}`) -}); - // names are hard export class TestTreeTestHarness extends Disposable { - private readonly owned = new TestOwnedTestCollection(); - private readonly onDiff = this._register(new Emitter()); + private readonly onDiff = this._register(new Emitter()); public readonly onFolderChange = this._register(new Emitter()); - public readonly c: TestSingleUseCollection = this._register(this.owned.createForHierarchy()); private isProcessingDiff = false; public readonly projection: T; public readonly tree: TestObjectTree; - constructor(folders: IWorkspaceFolderData[], makeTree: (listener: TestSubscriptionListener) => T) { + constructor(makeTree: (listener: ITestService) => T, public readonly c = testStubs.nested()) { super(); + this._register(c); this.c.onDidGenerateDiff(d => this.c.setDiff(d /* don't clear during testing */)); + + const collection = new MainThreadTestCollection((src, levels) => { + this.c.expand(src.testId, levels); + if (!this.isProcessingDiff) { + this.onDiff.fire(this.c.collectDiff()); + } + return Promise.resolve(); + }); + this._register(this.onDiff.event(diff => collection.apply(diff))); + this.projection = this._register(makeTree({ - workspaceFolderCollections: folders.map(folder => [{ folder }, { - expand: (testId: string, levels: number) => { - this.c.expand(testId, levels); - if (!this.isProcessingDiff) { - this.onDiff.fire({ folder: { folder }, diff: this.c.collectDiff() }); - } - return Promise.resolve(); - }, - all: [], - }]), - onDiff: this.onDiff.event, - onFolderChange: this.onFolderChange.event, + collection, + onDidProcessDiff: this.onDiff.event, } as any)); this.tree = this._register(new TestObjectTree(t => 'label' in t ? t.label : t.message.toString())); this._register(this.tree.onDidChangeCollapseState(evt => { @@ -115,10 +108,14 @@ export class TestTreeTestHarness { - const c = new MainThreadTestCollection(0, async (t, l) => singleUse.expand(t.testId, l)); - const singleUse = new TestSingleUseCollection({ object: new TestTree(0), dispose: () => undefined }); - singleUse.addRoot(root, 'provider'); - await singleUse.expand('id-root', Infinity); - c.apply(singleUse.collectDiff()); - return c; -}; 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 f3c26aa064e..01a4df4463f 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -9,12 +9,12 @@ import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/comm import { Lazy } from 'vs/base/common/lazy'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; +import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; import { ITestTaskState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { getPathForTestInResult, HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, ReExportedTestRunState as TestRunState, TestItemImpl, TestResultState, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs'; -import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { Convert, getInitializedMainTestCollection, ReExportedTestRunState as TestRunState, TestResultState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; export const emptyOutputController = () => new LiveOutputController( @@ -30,7 +30,7 @@ suite('Workbench - Test Results Service', () => { let r: TestLiveTestResult; let changed = new Set(); - let tests: TestItemImpl; + let tests: SingleUseTestCollection; const defaultOpts = { exclude: [], @@ -57,8 +57,17 @@ suite('Workbench - Test Results Service', () => { r.addTask({ id: 't', name: undefined, running: true }); tests = testStubs.nested(); - r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from)); - r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-ab'], 1).map(Convert.TestItem.from)); + await tests.expand(tests.root.id, Infinity); + r.addTestChainToRun('ctrl', [ + Convert.TestItem.from(tests.root), + Convert.TestItem.from(tests.root.children.get('id-a')!), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!), + ]); + + r.addTestChainToRun('ctrl', [ + Convert.TestItem.from(tests.root.children.get('id-a')!), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-ab')!), + ]); }); suite('LiveTestResult', () => { @@ -128,7 +137,7 @@ suite('Workbench - Test Results Service', () => { }); assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Running); // update computed state: - assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running); + assert.deepStrictEqual(r.getStateById(tests.root.id)?.computedState, TestRunState.Running); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.ComputedStateChange }, { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, @@ -174,7 +183,7 @@ suite('Workbench - Test Results Service', () => { [TestRunState.Unset]: 3, }); - assert.deepStrictEqual(r.getStateById('id-root')?.ownComputedState, TestRunState.Unset); + assert.deepStrictEqual(r.getStateById(tests.root.id)?.ownComputedState, TestRunState.Unset); assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Passed); }); }); @@ -212,8 +221,8 @@ suite('Workbench - Test Results Service', () => { await timeout(0); // allow load promise to resolve assert.strictEqual(1, results.results.length); - const [rehydrated, actual] = results.getStateById('id-root')!; - const expected: any = { ...r.getStateById('id-root')! }; + const [rehydrated, actual] = results.getStateById(tests.root.id)!; + const expected: any = { ...r.getStateById(tests.root.id)! }; delete expected.tasks[0].duration; // delete undefined props that don't survive serialization delete expected.item.range; delete expected.item.description; @@ -296,17 +305,17 @@ suite('Workbench - Test Results Service', () => { assert.deepStrictEqual([...resultItemParents(r, r.getStateById('id-aa')!)], [ r.getStateById('id-aa'), r.getStateById('id-a'), - r.getStateById('id-root'), + r.getStateById(tests.root.id), ]); - assert.deepStrictEqual([...resultItemParents(r, r.getStateById('id-root')!)], [ - r.getStateById('id-root'), + assert.deepStrictEqual([...resultItemParents(r, r.getStateById(tests.root.id)!)], [ + r.getStateById(tests.root.id), ]); }); test('getPathForTestInResult', () => { assert.deepStrictEqual([...getPathForTestInResult(r.getStateById('id-aa')!, r)], [ - 'id-root', + tests.root.id, 'id-a', 'id-aa', ]); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index ef4c7c73820..5a9620377b9 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -8,7 +8,7 @@ import { range } from 'vs/base/common/arrays'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs'; +import { Convert, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -30,7 +30,12 @@ suite('Workbench - Test Result Storage', () => { t.addTask({ id: 't', name: undefined, running: true }); const tests = testStubs.nested(); - t.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from)); + tests.expand(tests.root.id, Infinity); + t.addTestChainToRun('ctrl', [ + Convert.TestItem.from(tests.root), + Convert.TestItem.from(tests.root.children.get('id-a')!), + Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa')!), + ]); if (addMessage) { t.appendMessage('id-a', 't', { diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 76470fd5f7d..e8f610ac44f 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -7,16 +7,15 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; -import { URI } from 'vs/base/common/uri'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { TestItemFilteredWrapper, TestRunCoordinator, TestRunDto } from 'vs/workbench/api/common/extHostTesting'; +import { TestRunCoordinator, TestRunDto } from 'vs/workbench/api/common/extHostTesting'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestMessage } from 'vs/workbench/api/common/extHostTypes'; import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; -import { expandAllStubs, stubTest, TestItemImpl, TestResultState, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs'; -import { TestOwnedTestCollection, TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; -import type { TestItem, TestRunRequest, TextDocument } from 'vscode'; +import { TestItemImpl, TestResultState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; +import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import type { TestItem, TestRunRequest } from 'vscode'; const simplify = (item: TestItem) => ({ id: item.id, @@ -64,39 +63,34 @@ const assertTreesEqual = (a: TestItem | undefined, b: TestItem suite('ExtHost Testing', () => { let single: TestSingleUseCollection; - let owned: TestOwnedTestCollection; setup(() => { - owned = new TestOwnedTestCollection(); - single = owned.createForHierarchy(); + single = testStubs.nested(); single.onDidGenerateDiff(d => single.setDiff(d /* don't clear during testing */)); }); teardown(() => { single.dispose(); - assert.strictEqual(!owned.idToInternal?.size, true, 'expected owned ids to be empty after dispose'); }); suite('OwnedTestCollection', () => { - test('adds a root recursively', () => { - const tests = testStubs.nested(); - single.addRoot(tests, 'pid'); - single.expand('id-root', Infinity); + test('adds a root recursively', async () => { + await single.expand(single.root.id, Infinity); assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Add, - { src: { tree: 0, controller: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } } + { controllerId: 'ctrlId', parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(single.root) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.Expandable, item: { ...convert.TestItem.from(stubTest('a')) } } + { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.Expandable, item: { ...convert.TestItem.from(single.tree.get('id-a')!.actual) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) } + { controllerId: 'ctrlId', parent: single.root.id, expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-b')!.actual) } ], [ TestDiffOpType.Update, - { extId: 'id-root', expand: TestItemExpandState.Expanded } + { extId: single.root.id, expand: TestItemExpandState.Expanded } ], [ TestDiffOpType.Update, @@ -104,11 +98,11 @@ suite('ExtHost Testing', () => { ], [ TestDiffOpType.Add, - { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) } + { controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-aa')!.actual) } ], [ TestDiffOpType.Add, - { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) } + { controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-ab')!.actual) } ], [ TestDiffOpType.Update, @@ -118,18 +112,14 @@ suite('ExtHost Testing', () => { }); test('no-ops if items not changed', () => { - const tests = testStubs.nested(); - single.addRoot(tests, 'pid'); single.collectDiff(); assert.deepStrictEqual(single.collectDiff(), []); }); test('watches property mutations', () => { - const tests = testStubs.nested(); - single.addRoot(tests, 'pid'); - single.expand('id-root', Infinity); + single.expand(single.root.id, Infinity); single.collectDiff(); - tests.children.get('id-a')!.description = 'Hello world'; /* item a */ + single.root.children.get('id-a')!.description = 'Hello world'; /* item a */ assert.deepStrictEqual(single.collectDiff(), [ [ @@ -139,38 +129,33 @@ suite('ExtHost Testing', () => { }); test('removes children', () => { - const tests = testStubs.nested(); - single.addRoot(tests, 'pid'); - single.expand('id-root', Infinity); + single.expand(single.root.id, Infinity); single.collectDiff(); - tests.children.get('id-a')!.dispose(); + single.root.children.get('id-a')!.dispose(); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Remove, 'id-a'], ]); - assert.deepStrictEqual([...owned.idToInternal].map(n => n.item.extId).sort(), ['id-b', 'id-root']); + assert.deepStrictEqual([...single.tree].map(n => n.item.extId).sort(), [single.root.id, 'id-b']); assert.strictEqual(single.itemToInternal.size, 2); }); test('adds new children', () => { - const tests = testStubs.nested(); - single.addRoot(tests, 'pid'); - single.expand('id-root', Infinity); + single.expand(single.root.id, Infinity); single.collectDiff(); - const child = stubTest('ac'); - tests.children.get('id-a')!.addChild(child); + const child = new TestItemImpl('id-ac', 'c', undefined, undefined, single.root.children.get('id-a')); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { - src: { tree: 0, controller: 'pid' }, + controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child), }], ]); assert.deepStrictEqual( - [...owned.idToInternal].map(n => n.item.extId).sort(), - ['id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b', 'id-root'], + [...single.tree].map(n => n.item.extId).sort(), + [single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'], ); assert.strictEqual(single.itemToInternal.size, 6); }); @@ -185,9 +170,9 @@ suite('ExtHost Testing', () => { // test('mirrors creation of the root', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); - // single.expand('id-root', Infinity); + // single.expand(single.root.id, Infinity); // m.apply(single.collectDiff()); - // assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual); + // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); @@ -195,13 +180,13 @@ suite('ExtHost Testing', () => { // const tests = testStubs.nested(); // single.addRoot(tests, 'pid'); // m.apply(single.collectDiff()); - // single.expand('id-root', Infinity); + // single.expand(single.root.id, Infinity); // tests.children!.splice(0, 1); // single.onItemChange(tests, 'pid'); - // single.expand('id-root', Infinity); + // single.expand(single.root.id, Infinity); // m.apply(single.collectDiff()); - // assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual); + // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); @@ -213,7 +198,7 @@ suite('ExtHost Testing', () => { // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); - // assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual); + // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // assert.strictEqual(m.length, single.itemToInternal.size); // }); @@ -225,7 +210,7 @@ suite('ExtHost Testing', () => { // single.onItemChange(tests, 'pid'); // m.apply(single.collectDiff()); - // assertTreesEqual(m.rootTestItems[0], owned.getTestById('id-root')![1].actual); + // assertTreesEqual(m.rootTestItems[0], owned.getTestById(single.root.id)![1].actual); // }); // suite('MirroredChangeCollector', () => { @@ -311,108 +296,6 @@ suite('ExtHost Testing', () => { // m.apply(single.collectDiff()); // }); // }); - - suite('TestItemFilteredWrapper', () => { - const textDocumentFilter = { - uri: URI.parse('file:///foo.ts'), - } as TextDocument; - - let testsWithLocation: TestItemImpl; - setup(async () => { - testsWithLocation = - stubTest('root', undefined, [ - stubTest('a', undefined, [ - stubTest('aa', undefined, undefined, URI.parse('file:///foo.ts')), - stubTest('ab', undefined, undefined, URI.parse('file:///foo.ts')) - ], URI.parse('file:///foo.ts')), - stubTest('b', undefined, [ - stubTest('ba', undefined, undefined, URI.parse('file:///bar.ts')), - stubTest('bb', undefined, undefined, URI.parse('file:///bar.ts')) - ], URI.parse('file:///bar.ts')), - stubTest('c', undefined, undefined, URI.parse('file:///baz.ts')), - ]); - - expandAllStubs(testsWithLocation); - }); - - teardown(() => { - TestItemFilteredWrapper.removeFilter(textDocumentFilter); - }); - - test('gets all actual properties', () => { - const testItem = stubTest('test1'); - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(testItem, textDocumentFilter); - wrapper.refreshMatch(); - - assert.strictEqual(testItem.debuggable, wrapper.debuggable); - assert.strictEqual(testItem.description, wrapper.description); - assert.strictEqual(testItem.label, wrapper.label); - assert.strictEqual(testItem.uri, wrapper.uri); - assert.strictEqual(testItem.runnable, wrapper.runnable); - }); - - test('gets no children if nothing matches Uri filter', () => { - const tests = testStubs.nested(); - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(tests, textDocumentFilter); - wrapper.refreshMatch(); - assert.strictEqual(wrapper.children.size, 0); - }); - - test('filter is applied to children', () => { - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); - wrapper.refreshMatch(); - assert.strictEqual(wrapper.label, 'root'); - - const children = [...wrapper.children.values()]; - assert.strictEqual(children.length, 1); - assert.strictEqual(children[0] instanceof TestItemFilteredWrapper, true); - assert.strictEqual(children[0].label, 'a'); - }); - - test('can get if node has matching filter', () => { - const rootWrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); - rootWrapper.refreshMatch(); - - const invisible = testsWithLocation.children.get('id-b')!; - const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); - const visible = testsWithLocation.children.get('id-a')!; - const visibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(visible, textDocumentFilter); - - // The root is always visible - assert.strictEqual(rootWrapper.hasNodeMatchingFilter, true); - assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, false); - assert.strictEqual(visibleWrapper.hasNodeMatchingFilter, true); - }); - - test('can reset cached value of hasNodeMatchingFilter on new children', () => { - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); - wrapper.refreshMatch(); - - const invisible = testsWithLocation.children.get('id-b')!; - const invisibleWrapper = TestItemFilteredWrapper.getWrapperForTestItem(invisible, textDocumentFilter); - - assert.strictEqual(wrapper.children.get('id-b'), undefined); - assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, false); - - invisible.addChild(stubTest('bc', undefined, undefined, URI.parse('file:///foo.ts'))); - assert.strictEqual(invisibleWrapper.hasNodeMatchingFilter, true); - assert.strictEqual(invisibleWrapper.children.size, 1); - assert.strictEqual(wrapper.children.get('id-b'), invisibleWrapper); - }); - - test('can reset cached value of hasNodeMatchingFilter on children removed', () => { - const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(testsWithLocation, textDocumentFilter); - wrapper.refreshMatch(); - - const itemB = testsWithLocation.children.get('id-b')!; - const visibleChild = stubTest('bc', undefined, undefined, URI.parse('file:///foo.ts')); - itemB.addChild(visibleChild); - assert.strictEqual(!!wrapper.children.get('id-b'), true); - - visibleChild.dispose(); - assert.strictEqual(!!wrapper.children.get('id-b'), false); - }); - }); }); suite('TestRunTracker', () => { @@ -422,10 +305,11 @@ suite('ExtHost Testing', () => { const req: TestRunRequest = { tests: [], debug: false }; const dto = TestRunDto.fromInternal({ + controllerId: 'ctrl', debug: false, excludeExtIds: [], runId: 'run-id', - tests: [], + testIds: [], }); setup(() => { @@ -438,8 +322,8 @@ suite('ExtHost Testing', () => { const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token); assert.strictEqual(tracker.isRunning, false); - const task1 = c.createTestRun(req, 'run1', true); - const task2 = c.createTestRun(req, 'run2', true); + const task1 = c.createTestRun('ctrl', req, 'run1', true); + const task2 = c.createTestRun('ctrl', req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.isRunning, true); @@ -457,7 +341,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from an extension request', () => { - const task1 = c.createTestRun(req, 'hello world', false); + const task1 = c.createTestRun('ctrl', req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.isRunning, true); @@ -471,8 +355,8 @@ suite('ExtHost Testing', () => { }] ]); - const task2 = c.createTestRun(req, 'run2', true); - const task3Detached = c.createTestRun({ ...req }, 'task3Detached', true); + const task2 = c.createTestRun('ctrl', req, 'run2', true); + const task3Detached = c.createTestRun('ctrl', { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); @@ -486,37 +370,46 @@ suite('ExtHost Testing', () => { }); test('adds tests to run smartly', () => { - const task1 = c.createTestRun(req, 'hello world', false); + const task1 = c.createTestRun('ctrl', req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; - const tests = testStubs.nested(); const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); + single.expand(single.root.id, Infinity); - task1.setState(testStubsChain(tests, ['id-a', 'id-aa']).pop()!, TestResultState.Passed); + task1.setState(single.root.children.get('id-a')!.children.get('id-aa')!, TestResultState.Passed); expectedArgs.push([ + 'ctrl', tracker.id, - testStubsChain(tests, ['id-a', 'id-aa']).map(convert.TestItem.from) + [ + convert.TestItem.from(single.root), + convert.TestItem.from(single.root.children.get('id-a')!), + convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-aa')!), + ] ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); - task1.setState(testStubsChain(tests, ['id-a', 'id-ab']).pop()!, TestResultState.Queued); + task1.setState(single.root.children.get('id-a')!.children.get('id-ab')!, TestResultState.Queued); expectedArgs.push([ + 'ctrl', tracker.id, - testStubsChain(tests, ['id-a', 'id-ab']).slice(1).map(convert.TestItem.from) + [ + convert.TestItem.from(single.root.children.get('id-a')!), + convert.TestItem.from(single.root.children.get('id-a')!.children.get('id-ab')!), + ], ]); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); - task1.setState(testStubsChain(tests, ['id-a', 'id-ab']).pop()!, TestResultState.Passed); + task1.setState(single.root.children.get('id-a')!.children.get('id-ab')!, TestResultState.Passed); assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); }); test('guards calls after runs are ended', () => { - const task = c.createTestRun(req, 'hello world', false); + const task = c.createTestRun('ctrl', req, 'hello world', false); task.end(); - task.setState(testStubs.nested(), TestResultState.Passed); - task.appendMessage(testStubs.nested(), new TestMessage('some message')); + task.setState(single.root, TestResultState.Passed); + task.appendMessage(single.root, new TestMessage('some message')); task.appendOutput('output'); assert.strictEqual(proxy.$addTestsToRun.called, false);