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);