diff --git a/.vscode/settings.json b/.vscode/settings.json index 7eede8c30a9..a97841683c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -86,5 +86,5 @@ }, "typescript.tsc.autoDetect": "off", "notebook.experimental.useMarkdownRenderer": true, - "testing.autoRun.mode": "onlyPreviouslyRun", + "testing.autoRun.mode": "rerun", } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 730348fea1f..ae767c9487a 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2142,10 +2142,9 @@ declare module 'vscode' { export function registerTestController(testController: TestController): Disposable; /** - * Runs tests. The "run" contains the list of tests to run as well as a - * method that can be used to update their state. At the point in time - * that "run" is called, all tests given in the run have their state - * automatically set to {@link TestRunState.Queued}. + * Requests that tests be run by their controller. + * @param run Run options to use + * @param token Cancellation token for the test run */ export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; @@ -2344,11 +2343,9 @@ declare module 'vscode' { readonly name?: string; /** - * Updates the state of the test in the run. By default, all tests involved - * in the run will have a "queued" state until they are updated by this method. - * - * Calling with method with nodes outside the {@link TestRunRequest.tests} - * or in the {@link TestRunRequest.exclude} array will no-op. + * Updates the state of the test in the run. Calling with method with nodes + * outside the {@link TestRunRequest.tests} or in the + * {@link TestRunRequest.exclude} array will no-op. * * @param test The test to update * @param state The state to assign to the test @@ -2687,6 +2684,19 @@ declare module 'vscode' { */ readonly range?: Range; + /** + * State of the test in each task. In the common case, a test will only + * be executed in a single task and the length of this array will be 1. + */ + readonly taskStates: ReadonlyArray; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly[]; + } + + export interface TestSnapshoptTaskState { /** * Current result of the test. */ @@ -2703,11 +2713,6 @@ declare module 'vscode' { * failure information if the test fails. */ readonly messages: ReadonlyArray; - - /** - * Optional list of nested tests for this item. - */ - readonly children: Readonly[]; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 8b9bb6a4b71..64f6a4a307c 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { emptyStream } from 'vs/base/common/stream'; import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } 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 { getTestSubscriptionKey, ISerializedTestResults, ITestMessage, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { HydratedTestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ExtensionRunTestsRequest, getTestSubscriptionKey, 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'; @@ -48,7 +47,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh 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); @@ -72,43 +70,64 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void { - this.resultService.push(new HydratedTestResult( - results, - () => Promise.resolve( - results.output - ? bufferToStream(VSBuffer.fromString(results.output)) - : emptyStream(), - ), - persist, - )); - } - - /** - * @inheritdoc - */ - public $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void { - const r = this.resultService.getResult(runId); - if (r && r instanceof LiveTestResult) { - r.updateState(testId, state, duration); + $addTestsToRun(runId: string, tests: ITestItem[]): void { + for (const test of tests) { + test.uri = URI.revive(test.uri); + if (test.range) { + test.range = Range.lift(test.range); + } } + + this.withLiveRun(runId, r => r.addTestChainToRun(tests)); } /** * @inheritdoc */ - public $appendOutputToRun(runId: string, output: VSBuffer): void { - const r = this.resultService.getResult(runId); - if (r && r instanceof LiveTestResult) { - r.output.append(output); - } + $startedExtensionTestRun(req: ExtensionRunTestsRequest): void { + this.resultService.createLiveResult(req); + } + + /** + * @inheritdoc + */ + $startedTestRunTask(runId: string, task: ITestRunTask): void { + this.withLiveRun(runId, r => r.addTask(task)); + } + + /** + * @inheritdoc + */ + $finishedTestRunTask(runId: string, taskId: string): void { + this.withLiveRun(runId, r => r.markTaskComplete(taskId)); + } + + /** + * @inheritdoc + */ + $finishedExtensionTestRun(runId: string): void { + this.withLiveRun(runId, r => r.markComplete()); + } + + /** + * @inheritdoc + */ + public $updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void { + this.withLiveRun(runId, r => r.updateState(testId, taskId, state, duration)); + } + + /** + * @inheritdoc + */ + public $appendOutputToRun(runId: string, _taskId: string, output: VSBuffer): void { + this.withLiveRun(runId, r => r.output.append(output)); } /** * @inheritdoc */ - public $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void { + public $appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void { const r = this.resultService.getResult(runId); if (r && r instanceof LiveTestResult) { if (message.location) { @@ -116,7 +135,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh message.location.range = Range.lift(message.location.range); } - r.appendMessage(testId, message); + r.appendMessage(testId, taskId, message); } } @@ -180,4 +199,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh } this.testSubscriptions.clear(); } + + private withLiveRun(runId: string, fn: (run: LiveTestResult) => T): T | undefined { + const r = this.resultService.getResult(runId); + return r && r instanceof LiveTestResult ? fn(r) : undefined; + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5b345dc1fc6..78de328359b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -337,7 +337,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const test: typeof vscode.test = { registerTestController(provider) { checkProposedApiEnabled(extension); - return extHostTesting.registerTestController(provider); + return extHostTesting.registerTestController(extension.identifier.value, provider); }, createDocumentTestObserver(document) { checkProposedApiEnabled(extension); @@ -354,9 +354,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createTestItem(options: vscode.TestItemOptions, data?: T) { return new extHostTypes.TestItemImpl(options.id, options.label, options.uri, data); }, - createTestRunTask() { + createTestRunTask(request, name, persist) { checkProposedApiEnabled(extension); - throw new Error('todo'); + return extHostTesting.createTestRunTask(extension.identifier.value, request, name, persist); }, get onDidChangeTestResults() { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b8849cacb41..85976159dfb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -56,7 +56,7 @@ import { Dto } from 'vs/base/common/types'; import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage, ITestItem, ITestRunTask, ExtensionRunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -1997,16 +1997,39 @@ export interface ExtHostTestingShape { } export interface MainThreadTestingShape { + /** Registeres that there's a test controller with the given ID */ $registerTestController(id: 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; + /** Publishes that new tests were available on the given source. */ $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void; - $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void; - $appendOutputToRun(runId: string, output: VSBuffer): void; + /** Request by an extension to run tests. */ $runTests(req: RunTestsRequest, token: CancellationToken): Promise; - $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void; + + // --- 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; + /** 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. */ + $appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void; + /** Appends raw output to the test run.. */ + $appendOutputToRun(runId: string, taskId: string, output: VSBuffer): void; + /** Signals a task in a test run started. */ + $startedTestRunTask(runId: string, task: ITestRunTask): void; + /** Signals a task in a test run ended. */ + $finishedTestRunTask(runId: string, taskId: string): void; + /** Start a new extension-provided test run. */ + $startedExtensionTestRun(req: ExtensionRunTestsRequest): void; + /** Signals that an extension-provided test run finished. */ + $finishedExtensionTestRun(runId: string): void; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 4a165d13ca8..e8aad5e6eec 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { mapFind } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { Barrier, DeferredPromise, disposableTimeout, isThenable } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; +import { Iterable } from 'vs/base/common/iterator'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; @@ -23,16 +24,20 @@ import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { Disposable, 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, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { private readonly resultsChangedEmitter = new Emitter(); - private readonly controllers = new Map>(); + private readonly controllers = new Map + }>(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); + private readonly runQueue: TestRunQueue; private readonly testControllers = new Map(controller: vscode.TestController): vscode.Disposable { + public registerTestController(extensionId: string, controller: vscode.TestController): vscode.Disposable { const controllerId = generateUuid(); - this.controllers.set(controllerId, controller); + this.controllers.set(controllerId, { instance: controller, extensionId }); this.proxy.$registerTestController(controllerId); // give the ext a moment to register things rather than synchronously invoking within activate() @@ -105,10 +111,10 @@ export class ExtHostTesting implements ExtHostTestingShape { } /** - * Implements vscode.test.publishTestResults + * Implements vscode.test.createTestRunTask */ - public publishExtensionProvidedResults(results: vscode.TestRunResult, persist: boolean): void { - this.proxy.$publishExtensionProvidedResults(Convert.TestResults.from(generateUuid(), results), persist); + public createTestRunTask(extensionId: string, request: vscode.TestRunRequest, name: string | undefined, persist = true): vscode.TestRunTask { + return this.runQueue.createTestRunTask(extensionId, request, name, persist); } /** @@ -188,8 +194,8 @@ export class ExtHostTesting implements ExtHostTestingShape { const collection = disposable.add(this.ownedTests.createForHierarchy( diff => this.proxy.$publishDiff(resource, uriComponents, diff))); disposable.add(toDisposable(() => cancellation.dispose(true))); - for (const [id, provider] of this.controllers) { - subscribeFn(id, provider); + for (const [id, controller] of this.controllers) { + subscribeFn(id, controller.instance); } // note: we don't increment the root count initially -- this is done by the @@ -239,14 +245,14 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { - const provider = this.controllers.get(req.tests[0].src.provider); - if (!provider) { + public async $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise { + const controller = this.controllers.get(req.tests[0].src.controller); + if (!controller) { return; } const includeTests = req.tests - .map(({ testId, src }) => this.ownedTests.getTestById(testId, src.tree)) + .map(({ testId, src }) => this.ownedTests.getTestById(testId, src?.tree)) .filter(isDefined) .map(([_tree, test]) => test); @@ -261,50 +267,19 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const isExcluded = (test: vscode.TestItem) => { - // for test providers that don't support excluding natively, - // make sure not to report excluded result otherwise summaries will be off. - for (const [tree, exclude] of excludeTests) { - const e = tree.comparePositions(exclude, test.id); - if (e === TestPosition.IsChild || e === TestPosition.IsSame) { - return true; - } - } - - return false; + const publicReq: vscode.TestRunRequest = { + tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)), + exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)), + debug: req.debug, }; - try { - await provider.runTests({ - appendOutput: message => { - this.proxy.$appendOutputToRun(req.runId, VSBuffer.fromString(message)); - }, - appendMessage: (test, message) => { - if (!isExcluded(test)) { - this.flushCollectionDiffs(); - this.proxy.$appendTestMessageInRun(req.runId, test.id, Convert.TestMessage.from(message)); - } - }, - setState: (test, state, duration) => { - if (!isExcluded(test)) { - this.flushCollectionDiffs(); - this.proxy.$updateTestStateInRun(req.runId, test.id, state, duration); - } - }, - tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)), - exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)), - debug: req.debug, - }, cancellation); - - for (const { collection } of this.testControllers.values()) { - collection.flushDiff(); // ensure all states are updated - } - - return; - } catch (e) { - console.error(e); // so it appears to attached debuggers - throw e; - } + await this.runQueue.enqueueRun({ + dto: TestRunDto.fromInternal(req), + token, + extensionId: controller.extensionId, + req: publicReq, + doRun: () => controller!.instance.runTests(publicReq, token) + }); } public $lookupTest(req: TestIdWithSrc): Promise { @@ -340,6 +315,224 @@ export class ExtHostTesting implements ExtHostTestingShape { } } +/** + * Queues runs for a single extension and provides the currently-executing + * run so that `createTestRunTask` can be properly correlated. + */ +class TestRunQueue { + private readonly state = new Map, + factory: (name: string | undefined) => TestRunTask, + }, + queue: (() => (Promise | void))[]; + }>(); + + constructor(private readonly proxy: MainThreadTestingShape) { } + + /** + * Registers and enqueues a test run. `doRun` will be called when an + * invokation to {@link TestController.runTests} should be called. + */ + public enqueueRun(opts: { + extensionId: string, + req: vscode.TestRunRequest, + dto: TestRunDto, + token: CancellationToken, + doRun: () => Thenable | void, + }, + ) { + let record = this.state.get(opts.extensionId); + if (!record) { + record = { queue: [], current: undefined as any }; + this.state.set(opts.extensionId, record); + } + + const deferred = new DeferredPromise(); + const runner = () => { + const tasks: TestRunTask[] = []; + const shared = new Set(); + record!.current = { + publicReq: opts.req, + factory: name => { + const task = new TestRunTask(name, opts.dto, shared, this.proxy); + tasks.push(task); + opts.token.onCancellationRequested(() => task.end()); + return task; + }, + }; + + this.invokeRunner(opts.extensionId, opts.dto.id, opts.doRun, tasks).finally(() => deferred.complete()); + }; + + record.queue.push(runner); + if (record.queue.length === 1) { + runner(); + } + + return deferred.p; + } + + /** + * Implements the public `createTestRunTask` API. + */ + public createTestRunTask(extensionId: string, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRunTask { + const state = this.state.get(extensionId); + // If the request is for the currently-executing `runTests`, then correlate + // it to that existing run. Otherwise return a new, detached run. + if (state?.current.publicReq === request) { + return state.current.factory(name); + } + + const dto = TestRunDto.fromPublic(request); + const task = new TestRunTask(name, dto, new Set(), this.proxy); + this.proxy.$startedExtensionTestRun({ + debug: request.debug, + exclude: request.exclude?.map(t => t.id) ?? [], + id: dto.id, + tests: request.tests.map(t => t.id), + persist: persist + }); + task.onEnd.wait().then(() => this.proxy.$finishedExtensionTestRun(dto.id)); + return task; + } + + private invokeRunner(extensionId: string, runId: string, fn: () => Thenable | void, tasks: TestRunTask[]): Promise { + try { + const res = fn(); + if (isThenable(res)) { + return res + .then(() => this.handleInvokeResult(extensionId, runId, tasks, undefined)) + .catch(err => this.handleInvokeResult(extensionId, runId, tasks, err)); + } else { + return this.handleInvokeResult(extensionId, runId, tasks, undefined); + } + } catch (e) { + return this.handleInvokeResult(extensionId, runId, tasks, e); + } + } + + private async handleInvokeResult(extensionId: string, runId: string, tasks: TestRunTask[], error?: Error) { + const record = this.state.get(extensionId); + if (!record) { + return; + } + + record.queue.shift(); + if (record.queue.length > 0) { + record.queue[0](); + } else { + this.state.delete(extensionId); + } + + await Promise.all(tasks.map(t => t.onEnd.wait())); + } +} + +class TestRunDto { + public static fromPublic(request: vscode.TestRunRequest) { + return new TestRunDto( + generateUuid(), + new Set(request.tests.map(t => t.id)), + new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()), + ); + } + + public static fromInternal(request: RunTestForProviderRequest) { + return new TestRunDto( + request.runId, + new Set(request.tests.map(t => t.testId)), + new Set(request.excludeExtIds), + ); + } + + constructor( + public readonly id: string, + private readonly include: ReadonlySet, + private readonly exclude: ReadonlySet, + ) { } + + public isIncluded(test: vscode.TestItem) { + for (let t: vscode.TestItem | undefined = test; t; t = t.parent) { + if (this.include.has(t.id)) { + return true; + } else if (this.exclude.has(t.id)) { + return false; + } + } + + return true; + } +} + +class TestRunTask implements vscode.TestRunTask { + readonly #proxy: MainThreadTestingShape; + readonly #req: TestRunDto; + readonly #taskId = generateUuid(); + readonly #sharedIds: Set; + public readonly onEnd = new Barrier(); + + constructor( + public readonly name: string | undefined, + dto: TestRunDto, + sharedTestIds: Set, + proxy: MainThreadTestingShape, + ) { + this.#proxy = proxy; + this.#req = dto; + this.#sharedIds = sharedTestIds; + proxy.$startedTestRunTask(dto.id, { id: this.#taskId, name, running: true }); + } + + setState(test: vscode.TestItem, state: vscode.TestResultState, duration?: number): void { + if (this.#req.isIncluded(test)) { + this.ensureTestIsKnown(test); + this.#proxy.$updateTestStateInRun(this.#req.id, this.#taskId, test.id, state, duration); + } + } + + appendMessage(test: vscode.TestItem, message: vscode.TestMessage): void { + if (this.#req.isIncluded(test)) { + this.ensureTestIsKnown(test); + this.#proxy.$appendTestMessageInRun(this.#req.id, this.#taskId, test.id, Convert.TestMessage.from(message)); + } + } + + appendOutput(output: string): void { + this.#proxy.$appendOutputToRun(this.#req.id, this.#taskId, VSBuffer.fromString(output)); + } + + end(): void { + this.#proxy.$finishedTestRunTask(this.#req.id, this.#taskId); + this.onEnd.open(); + } + + private ensureTestIsKnown(test: vscode.TestItem) { + const sent = this.#sharedIds; + if (sent.has(test.id)) { + return; + } + + const chain: ITestItem[] = []; + while (true) { + chain.unshift(Convert.TestItem.from(test)); + + if (sent.has(test.id)) { + break; + } + + sent.add(test.id); + if (!test.parent) { + break; + } + + test = test.parent; + } + + this.#proxy.$addTestsToRun(this.#req.id, chain); + } +} + export const createDefaultDocumentTestRoot = async ( provider: vscode.TestController, document: vscode.TextDocument, @@ -439,7 +632,9 @@ export class TestItemFilteredWrapper extends TestItemImpl { (this as Record)[evt[1]] = evt[2]; break; case ExtHostTestItemEventType.NewChild: - TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this).refreshMatch(); + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this); + getPrivateApiFor(wrapper).parent = actual; + wrapper.refreshMatch(); break; default: wrapperApi.bus.fire(evt); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 22b33ba5a0f..389b2811c4d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -28,7 +28,7 @@ import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebo import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; @@ -1702,52 +1702,13 @@ export namespace TestItem { } export namespace TestResults { - export function from(id: string, results: vscode.TestRunResult): ISerializedTestResults { - const serialized: ISerializedTestResults = { - completedAt: results.completedAt, - id, - output: results.output, - items: [], - }; - - const queue: [parent: SerializedTestResultItem | null, children: Iterable][] = [ - [null, results.results], - ]; - - while (queue.length) { - const [parent, children] = queue.pop()!; - for (const item of children) { - const serializedItem: SerializedTestResultItem = { - children: item.children?.map(c => c.id) ?? [], - computedState: item.state, - item: TestItem.fromResultSnapshot(item), - state: { - state: item.state, - duration: item.duration, - messages: item.messages.map(TestMessage.from), - }, - retired: undefined, - expand: TestItemExpandState.Expanded, - parent: parent?.item.extId ?? null, - src: { provider: '', tree: -1 }, - direct: !parent, - }; - - serialized.items.push(serializedItem); - if (item.children) { - queue.push([serializedItem, item.children]); - } - } - } - - return serialized; - } - const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestResultSnapshot => ({ ...TestItem.toPlain(item.item), - state: item.state.state, - duration: item.state.duration, - messages: item.state.messages.map(TestMessage.to), + taskStates: item.tasks.map(t => ({ + state: t.state, + duration: t.duration, + messages: t.messages.map(TestMessage.to), + })), children: item.children .map(c => byInternalId.get(c)) .filter(isDefined) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4bf416b06ac..6a124b5f76b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3248,6 +3248,7 @@ const testItemPropAccessor = >( api: IExtHostTestItemApi, key: K, defaultValue: vscode.TestItem[K], + equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean ) => { let value = defaultValue; return { @@ -3257,7 +3258,7 @@ const testItemPropAccessor = >( return value; }, set(newValue: vscode.TestItem[K]) { - if (newValue !== value) { + if (!equals(value, newValue)) { value = newValue; api.bus.fire([ExtHostTestItemEventType.SetProp, key, newValue]); } @@ -3265,6 +3266,12 @@ const testItemPropAccessor = >( }; }; +const strictEqualComparator = (a: T, b: T) => a === b; +const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefined) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.isEqual(b); +}; export class TestItemImpl implements vscode.TestItem { public readonly id!: string; @@ -3305,11 +3312,11 @@ export class TestItemImpl implements vscode.TestItem { enumerable: true, writable: false, }, - range: testItemPropAccessor(api, 'range', undefined), - description: testItemPropAccessor(api, 'description', undefined), - runnable: testItemPropAccessor(api, 'runnable', true), - debuggable: testItemPropAccessor(api, 'debuggable', true), - status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved), + range: testItemPropAccessor(api, 'range', undefined, rangeComparator), + description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator), + runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator), + debuggable: testItemPropAccessor(api, 'debuggable', true, strictEqualComparator), + status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator), }); } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 5db66fff1c6..ebc4021f651 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -68,7 +68,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) { const lookup = this.results.getStateById(inTree.test.item.extId)?.[1]; - inTree.ownState = lookup?.state.state ?? TestResultState.Unset; const computed = lookup?.computedState ?? TestResultState.Unset; if (computed !== inTree.state) { inTree.state = computed; @@ -84,7 +83,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes this._register(results.onTestChanged(({ item: result }) => { const item = this.items.get(result.item.extId); if (item) { - item.ownState = result.state.state; item.retired = result.retired; refreshComputedState(computedStateAccessor, item, this.addUpdated, result.computedState); this.addUpdated(item); @@ -270,7 +268,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1]; if (prevState) { - treeElement.ownState = prevState.state.state; treeElement.retired = prevState.retired; refreshComputedState(computedStateAccessor, treeElement, this.addUpdated, prevState.computedState); } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index 41a5824a3a5..8d62b8ecc89 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -140,7 +140,7 @@ export class HierarchicalByNameProjection extends HierarchicalByLocationProjecti const parent = this.getOrCreateFolderElement(folder); const actualParent = item.parent ? this.items.get(item.parent) as HierarchicalByNameElement : undefined; for (const testRoot of parent.children) { - if (testRoot.test.src.provider === item.src.provider) { + if (testRoot.test.src.controller === item.src.controller) { return new HierarchicalByNameElement(item, testRoot, r => this.changes.addedOrRemoved(r), actualParent); } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 323b6424b5d..324e28dff1f 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -55,7 +55,6 @@ export class HierarchicalElement implements ITestTreeElement { public state = TestResultState.Unset; public retired = false; - public ownState = TestResultState.Unset; constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) { this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese @@ -97,7 +96,6 @@ export class HierarchicalFolder implements ITestTreeElement { public retired = false; public state = TestResultState.Unset; - public ownState = TestResultState.Unset; constructor(public readonly folder: IWorkspaceFolder) { } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index a456d777b8b..e076f828940 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -112,7 +112,6 @@ export interface ITestTreeElement { */ readonly retired: boolean; - readonly ownState: TestResultState; readonly label: string; readonly parentItem: ITestTreeElement | null; } diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 02669c967ec..f9d69a0298a 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -72,10 +72,8 @@ .monaco-action-bar .action-item > .action-label { - width: 16px; - height: 100%; - line-height: 22px; - margin-right: 8px; + padding: 2px; + margin-right: 2px; } .monaco-workbench .part > .title > .title-actions .action-label.codicon-testing-autorun::after { diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 3e60eb44ced..d208ba4b30b 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -440,8 +440,9 @@ export class ShowMostRecentOutputAction extends Action2 { constructor() { super({ id: 'testing.showMostRecentOutput', - title: localize('testing.showMostRecentOutput', "Show Most Recent Output"), - f1: false, + title: localize('testing.showMostRecentOutput', "Show Output"), + f1: true, + category, icon: Codicon.terminal, menu: { id: MenuId.ViewTitle, @@ -878,7 +879,7 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsById { const resultSet = results[i]; for (const test of resultSet.tests) { const path = this.getPathForTest(test, resultSet).join(sep); - if (isFailedState(test.state.state)) { + if (isFailedState(test.ownComputedState)) { paths.add(path); } else { paths.delete(path); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index fae2288cb23..c7305a1a8b0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -185,17 +185,21 @@ export class TestingDecorations extends Disposable implements IEditorContributio continue; // do not show decorations for outdated tests } - for (let i = 0; i < stateItem.state.messages.length; i++) { - const m = stateItem.state.messages[i]; - if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { - const uri = buildTestUri({ - type: TestUriType.ResultActualOutput, - messageIndex: i, - resultId: result.id, - testExtId: stateItem.item.extId, - }); + for (let taskId = 0; taskId < stateItem.tasks.length; taskId++) { + const state = stateItem.tasks[taskId]; + for (let i = 0; i < state.messages.length; i++) { + const m = state.messages[i]; + if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { + const uri = buildTestUri({ + type: TestUriType.ResultActualOutput, + messageIndex: i, + taskIndex: taskId, + resultId: result.id, + testExtId: stateItem.item.extId, + }); - newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); + newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); + } } } } @@ -275,7 +279,7 @@ class RunTestDecoration extends Disposable implements ITestDecoration { : test.children.size > 0 ? testingRunAllIcon : testingRunIcon; const hoverMessage = new MarkdownString('', true).appendText(localize('failedHoverMessage', '{0} has failed. ', test.item.label)); - if (stateItem?.state.messages.length) { + if (stateItem?.tasks.some(s => s.messages.length > 0)) { const args = encodeURIComponent(JSON.stringify([test.item.extId])); hoverMessage.appendMarkdown(`[${localize('failedPeekAction', 'Peek Error')}](command:vscode.peekTestError?${args})`); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 542763578e0..803ee309d85 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -475,7 +475,7 @@ export class TestingExplorerViewModel extends Disposable { */ private async tryPeekError(item: ITestTreeElement) { const lookup = item.test && this.testResults.getStateById(item.test.item.extId); - return lookup && isFailedState(lookup[1].state.state) + return lookup && lookup[1].tasks.some(s => isFailedState(s.state)) ? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true }) : false; } @@ -638,9 +638,9 @@ class TestsFilter implements ITreeFilter { case TestExplorerStateFilter.All: return FilterResult.Include; case TestExplorerStateFilter.OnlyExecuted: - return element.ownState !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; + return element.state !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; case TestExplorerStateFilter.OnlyFailed: - return isFailedState(element.ownState) ? FilterResult.Include : FilterResult.Inherit; + return isFailedState(element.state) ? FilterResult.Include : FilterResult.Inherit; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 53a647c5267..e6b54d9e2c0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -31,7 +31,7 @@ import { EditorModel } from 'vs/workbench/common/editor'; import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { ITestItem, ITestMessage, ITestState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, ITestMessage, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; @@ -42,7 +42,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic interface ITestDto { test: ITestItem, messageIndex: number; - state: ITestState; + messages: ITestMessage[]; expectedUri: URI; actualUri: URI; messageUri: URI; @@ -78,12 +78,12 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener * @returns a boolean if a peek was opened */ public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial) { - const index = test.state.messages.findIndex(m => !!m.location); - if (index === -1) { + const candidate = this.getCandidateMessage(test); + if (!candidate) { return false; } - const message = test.state.messages[index]; + const message = candidate.message; const pane = await this.editorService.openEditor({ resource: message.location!.uri, options: { selection: message.location!.range, revealIfOpened: true, ...options } @@ -96,7 +96,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener TestingOutputPeekController.get(control).show(buildTestUri({ type: TestUriType.ResultMessage, - messageIndex: index, + taskIndex: candidate.taskId, + messageIndex: candidate.index, resultId: result.id, testExtId: test.item.extId, })); @@ -112,7 +113,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener return; } - if (!isFailedState(evt.item.state.state) || !evt.item.state.messages.length) { + const candidate = this.getCandidateMessage(evt.item); + if (!candidate) { return; } @@ -137,6 +139,24 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener this.tryPeekFirstError(evt.result, evt.item); } + + private getCandidateMessage(test: TestResultItem) { + for (let taskId = 0; taskId < test.tasks.length; taskId++) { + const { messages, state } = test.tasks[taskId]; + if (!isFailedState(state)) { + continue; + } + + const index = messages.findIndex(m => !!m.location); + if (index === -1) { + continue; + } + + return { taskId, index, message: messages[index] }; + } + + return undefined; + } } /** @@ -205,7 +225,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return; } - const message = dto.state.messages[dto.messageIndex]; + const message = dto.messages[dto.messageIndex]; if (!message?.location) { return; } @@ -253,7 +273,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo * else, then clear the peek. */ private closePeekOnTestChange(evt: TestResultItemChange) { - if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.state.state) { + if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.ownComputedState) { return; } @@ -273,9 +293,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo } const test = this.testResults.getResult(parts.resultId)?.getStateById(parts.testExtId); + if (!test || !test.tasks[parts.taskIndex]) { + return; + } + return test && { test: test.item, - state: test.state, + messages: test.tasks[parts.taskIndex].messages, messageIndex: parts.messageIndex, expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }), actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }), @@ -382,8 +406,8 @@ class TestingDiffOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) { - const message = state.messages[messageIndex]; + public async setModel({ test, messages, messageIndex, expectedUri, actualUri }: ITestDto) { + const message = messages[messageIndex]; if (!message?.location) { return; } @@ -440,8 +464,8 @@ class TestingMessageOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ state, test, messageIndex, messageUri }: ITestDto) { - const message = state.messages[messageIndex]; + public async setModel({ messages, test, messageIndex, messageUri }: ITestDto) { + const message = messages[messageIndex]; if (!message?.location) { return; } diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 3f9b148ba18..f901dace837 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -211,8 +211,8 @@ export class SingleUseTestCollection implements IDisposable { /** * Adds a new root node to the collection. */ - public addRoot(item: TestItemRaw, providerId: string) { - this.addItem(item, providerId, null); + public addRoot(item: TestItemRaw, controllerId: string) { + this.addItem(item, controllerId, null); } /** @@ -313,7 +313,7 @@ export class SingleUseTestCollection implements IDisposable { break; case ExtHostTestItemEventType.NewChild: - this.addItem(evt[1], internal.src.provider, internal); + this.addItem(evt[1], internal.src.controller, internal); break; case ExtHostTestItemEventType.SetProp: @@ -335,7 +335,7 @@ export class SingleUseTestCollection implements IDisposable { } } - private addItem(actual: TestItemRaw, providerId: string, parent: OwnedCollectionTestItem | null) { + private addItem(actual: TestItemRaw, controllerId: string, 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`); } @@ -351,7 +351,7 @@ export class SingleUseTestCollection implements IDisposable { const parentId = parent ? parent.item.extId : null; const expand = actual.resolveHandler ? TestItemExpandState.Expandable : TestItemExpandState.NotExpandable; const pExpandLvls = parent?.expandLevels; - const src = { provider: providerId, tree: this.testIdToInternal.object.id }; + const src = { controller: controllerId, tree: this.testIdToInternal.object.id }; const internal: OwnedCollectionTestItem = { actual, parent: parentId, @@ -366,6 +366,7 @@ export class SingleUseTestCollection implements IDisposable { this.pushDiff([TestDiffOpType.Add, { parent: parentId, src, expand, item: internal.item }]); const api = getPrivateApiFor(actual); + api.parent = parent?.actual; api.bus.event(this.onTestItemEvent.bind(this, internal)); // important that this comes after binding the event bus otherwise we @@ -374,7 +375,7 @@ export class SingleUseTestCollection implements IDisposable { // Discover any existing children that might have already been added for (const child of api.children.values()) { - this.addItem(child, providerId, internal); + this.addItem(child, controllerId, internal); } } diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 63dff937e30..eaf56c459fb 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -9,9 +9,11 @@ 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 interface TestIdWithSrc { +export type TestIdWithSrc = Required; + +export interface TestIdWithMaybeSrc { testId: string; - src: { provider: string; tree: number }; + src?: { controller: string; tree: number }; } /** @@ -24,14 +26,25 @@ export type TestIdPath = string[]; * Request to the main thread to run a set of tests. */ export interface RunTestsRequest { - tests: TestIdWithSrc[]; + tests: TestIdWithMaybeSrc[]; exclude?: string[]; debug: boolean; isAutoRun?: boolean; } /** - * Request from the main thread to run tests for a single provider. + * Request to the main thread to run a set of tests. + */ +export interface ExtensionRunTestsRequest { + id: string; + tests: string[]; + exclude: string[]; + debug: boolean; + persist: boolean; +} + +/** + * Request from the main thread to run tests for a single controller. */ export interface RunTestForProviderRequest { runId: string; @@ -56,17 +69,23 @@ export interface ITestMessage { location: IRichLocation | undefined; } -export interface ITestState { +export interface ITestTaskState { state: TestResultState; duration: number | undefined; messages: ITestMessage[]; } +export interface ITestRunTask { + id: string; + name: string | undefined; + running: boolean; +} + /** * The TestItem from .d.ts, as a plain object without children. */ export interface ITestItem { - /** ID of the test given by the test provider */ + /** ID of the test given by the test controller */ extId: string; label: string; children?: never; @@ -88,7 +107,7 @@ export const enum TestItemExpandState { * TestItem-like shape, butm with an ID and children as strings. */ export interface InternalTestItem { - src: { provider: string; tree: number }; + src: { controller: string; tree: number }; expand: TestItemExpandState; parent: string | null; item: ITestItem; @@ -115,9 +134,15 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate /** * Test result item used in the main thread. */ -export interface TestResultItem extends IncrementalTestCollectionItem { - /** Current state of this test */ - state: ITestState; +export interface TestResultItem { + /** Parent ID, if any */ + parent: string | null; + /** Raw test item properties */ + item: ITestItem; + /** State of this test in various tasks */ + tasks: ITestTaskState[]; + /** State of this test as a computation of its tasks */ + ownComputedState: TestResultState; /** Computed state based on children */ computedState: TestResultState; /** True if the test is outdated */ @@ -141,6 +166,8 @@ export interface ISerializedTestResults { output?: string; /** Subset of test result items */ items: SerializedTestResultItem[]; + /** Tasks involved in the run. */ + tasks: ITestRunTask[]; } export const enum TestDiffOpType { @@ -150,7 +177,7 @@ export const enum TestDiffOpType { Update, /** Removes a test (and all its children) */ Remove, - /** Changes the number of providers who are yet to publish their collection roots. */ + /** Changes the number of controllers who are yet to publish their collection roots. */ DeltaRootsComplete, /** Retires a test/result */ Retire, @@ -226,9 +253,9 @@ export abstract class AbstractIncrementalTestCollection(); /** - * Number of 'busy' providers. + * Number of 'busy' controllers. */ - protected busyProviderCount = 0; + protected busyControllerCount = 0; /** * Number of pending roots. @@ -259,7 +286,7 @@ export abstract class AbstractIncrementalTestCollection; + /** + * List of this result's subtasks. + */ + tasks: ReadonlyArray; + /** * Gets the state of the test by its extension-assigned ID. */ @@ -166,77 +170,20 @@ export class LiveOutputController { } } +interface TestResultItemWithChildren extends TestResultItem { + /** Children in the run */ + children: TestResultItemWithChildren[]; +} -const itemToNode = ( - item: IncrementalTestCollectionItem, - byExtId: Map, -): TestResultItem => { - const n: TestResultItem = { - ...item, - // shallow-clone the test to take a 'snapshot' of it at the point in time where tests run - item: { ...item.item }, - children: new Set(item.children), - state: { - duration: undefined, - messages: [], - state: TestResultState.Unset - }, - computedState: TestResultState.Unset, - retired: false, - }; - - byExtId.set(n.item.extId, n); - - return n; -}; - -const makeParents = ( - collection: IMainThreadTestCollection, - child: IncrementalTestCollectionItem, - byExtId: Map, -) => { - const parent = child.parent && collection.getNodeById(child.parent); - if (!parent) { - return; - } - - let parentResultItem = byExtId.get(parent.item.extId); - if (parentResultItem) { - parentResultItem.children.add(child.item.extId); - return; // no need to recurse, all parents already in result - } - - parentResultItem = itemToNode(parent, byExtId); - parentResultItem.children = new Set([child.item.extId]); - makeParents(collection, parent, byExtId); -}; - -const makeNodeAndChildren = ( - collection: IMainThreadTestCollection, - test: IncrementalTestCollectionItem, - excluded: ReadonlySet, - byExtId: Map, - isExecutedDirectly = true, -): TestResultItem => { - const existing = byExtId.get(test.item.extId); - if (existing) { - return existing; - } - - const mapped = itemToNode(test, byExtId); - if (isExecutedDirectly) { - mapped.direct = true; - } - - for (const childId of test.children) { - const child = collection.getNodeById(childId); - if (child && !excluded.has(childId)) { - makeNodeAndChildren(collection, child, excluded, byExtId, false); - } - } - - return mapped; -}; +const itemToNode = (item: ITestItem, parent: string | null): TestResultItemWithChildren => ({ + parent, + item: { ...item }, + children: [], + tasks: [], + ownComputedState: TestResultState.Unset, + computedState: TestResultState.Unset, + retired: false, +}); export const enum TestResultItemChangeReason { Retired, @@ -255,39 +202,29 @@ export type TestResultItemChange = { item: TestResultItem; result: ITestResult } * and marked as "complete" when the run finishes. */ export class LiveTestResult implements ITestResult { - /** - * Creates a new TestResult, pulling tests from the associated list - * of collections. - */ - public static from( - resultId: string, - collections: ReadonlyArray, - output: LiveOutputController, - req: RunTestsRequest, - ) { - const testByExtId = new Map(); - const excludeSet = new Set(req.exclude); - for (const test of req.tests) { - for (const collection of collections) { - const node = collection.getNodeById(test.testId); - if (!node) { - continue; - } - - makeNodeAndChildren(collection, node, excludeSet, testByExtId); - makeParents(collection, node, testByExtId); - } - } - - return new LiveTestResult(resultId, collections, testByExtId, excludeSet, output, !!req.isAutoRun); - } - private readonly completeEmitter = new Emitter(); private readonly changeEmitter = new Emitter(); + private readonly testById = new Map(); private _completedAt?: number; public readonly onChange = this.changeEmitter.event; public readonly onComplete = this.completeEmitter.event; + public readonly tasks: ITestRunTask[] = []; + + /** + * Test IDs directly included in this run. + */ + public readonly includedIds: ReadonlySet; + + /** + * Test IDs excluded from this run. + */ + public readonly excludedIds: ReadonlySet; + + /** + * Gets whether this test is from an auto-run. + */ + public readonly isAutoRun: boolean; /** * @inheritdoc @@ -308,21 +245,11 @@ export class LiveTestResult implements ITestResult { return this.testById.values(); } - private readonly computedStateAccessor: IComputedStateAccessor = { - getOwnState: i => i.state.state, + private readonly computedStateAccessor: IComputedStateAccessor = { + getOwnState: i => i.ownComputedState, getCurrentComputedState: i => i.computedState, setComputedState: (i, s) => i.computedState = s, - getChildren: i => { - const { testById: testByExtId } = this; - return (function* () { - for (const childId of i.children) { - const child = testByExtId.get(childId); - if (child) { - yield child; - } - } - })(); - }, + getChildren: i => i.children[Symbol.iterator](), getParents: i => { const { testById: testByExtId } = this; return (function* () { @@ -341,13 +268,12 @@ export class LiveTestResult implements ITestResult { constructor( public readonly id: string, - private readonly collections: ReadonlyArray, - private readonly testById: Map, - private readonly excluded: ReadonlySet, public readonly output: LiveOutputController, - public readonly isAutoRun: boolean, + private readonly req: ExtensionRunTestsRequest | RunTestsRequest, ) { - this.counts[TestResultState.Unset] = testById.size; + this.isAutoRun = 'isAutoRun' in this.req && !!this.req.isAutoRun; + this.includedIds = new Set(req.tests.map(t => typeof t === 'string' ? t : t.testId)); + this.excludedIds = new Set(req.exclude); } /** @@ -358,47 +284,71 @@ export class LiveTestResult implements ITestResult { } /** - * Updates all tests in the collection to the given state. + * Adds a new run task to the results. */ - public setAllToState(state: TestResultState, when: (_t: TestResultItem) => boolean) { - for (const test of this.testById.values()) { - if (when(test)) { - this.fireUpdateAndRefresh(test, state); - } + public addTask(task: ITestRunTask) { + const index = this.tasks.length; + this.tasks.push(task); + + for (const test of this.tests) { + test.tasks.push({ duration: undefined, messages: [], state: TestResultState.Unset }); + this.fireUpdateAndRefresh(test, index, TestResultState.Queued); } } + /** + * 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) { + let parent = this.testById.get(chain[0].extId); + if (!parent) { // must be a test root + parent = this.addTestToRun(chain[0], null); + } + + for (let i = 1; i < chain.length; i++) { + parent = this.addTestToRun(chain[i], parent.item.extId); + } + + for (let i = 0; i < this.tasks.length; i++) { + this.fireUpdateAndRefresh(parent, i, TestResultState.Queued); + } + + return undefined; + } + /** * Updates the state of the test by its internal ID. */ - public updateState(testId: string, state: TestResultState, duration?: number) { - const entry = this.testById.get(testId) ?? this.addTestToRun(testId); + public updateState(testId: string, taskId: string, state: TestResultState, duration?: number) { + const entry = this.testById.get(testId); if (!entry) { return; } + const index = this.mustGetTaskIndex(taskId); if (duration !== undefined) { - entry.state.duration = duration; + entry.tasks[index].duration = duration; } - this.fireUpdateAndRefresh(entry, state); + this.fireUpdateAndRefresh(entry, index, state); } /** * Appends a message for the test in the run. */ - public appendMessage(testId: string, message: ITestMessage) { - const entry = this.testById.get(testId) ?? this.addTestToRun(testId); + public appendMessage(testId: string, taskId: string, message: ITestMessage) { + const entry = this.testById.get(testId); if (!entry) { return; } - entry.state.messages.push(message); + entry.tasks[this.mustGetTaskIndex(taskId)].messages.push(message); this.changeEmitter.fire({ item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, - previous: entry.state.state, + previous: entry.ownComputedState, }); } @@ -409,24 +359,6 @@ export class LiveTestResult implements ITestResult { return this.output.read(); } - private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) { - const previous = entry.state.state; - if (newState === previous) { - return; - } - - entry.state.state = newState; - this.counts[previous]--; - this.counts[newState]++; - refreshComputedState(this.computedStateAccessor, entry, t => - this.changeEmitter.fire( - t === entry - ? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous } - : { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange } - ), - ); - } - /** * Marks a test as retired. This can trigger it to be rerun in live mode. */ @@ -436,11 +368,10 @@ export class LiveTestResult implements ITestResult { return; } - const queue: Iterable[] = [[root.item.extId]]; + const queue = [[root]]; while (queue.length) { - for (const id of queue.pop()!) { - const entry = this.testById.get(id); - if (entry && !entry.retired) { + for (const entry of queue.pop()!) { + if (!entry.retired) { entry.retired = true; queue.push(entry.children); this.changeEmitter.fire({ @@ -456,23 +387,15 @@ export class LiveTestResult implements ITestResult { } /** - * Adds a test, by its ID, to the test run. This can end up being called - * if tests were started while discovery was still happening, so initially - * we didn't serialize/capture the test. + * Marks the task in the test run complete. */ - private addTestToRun(testId: string) { - for (const collection of this.collections) { - let test = collection.getNodeById(testId); - if (test) { - const originalSize = this.testById.size; - makeParents(collection, test, this.testById); - const node = makeNodeAndChildren(collection, test, this.excluded, this.testById, false); - this.counts[TestResultState.Unset] += this.testById.size - originalSize; - return node; - } - } - - return undefined; + public markTaskComplete(taskId: string) { + this.tasks[this.mustGetTaskIndex(taskId)].running = false; + this.setAllToState( + TestResultState.Unset, + taskId, + t => t.state === TestResultState.Queued || t.state === TestResultState.Running, + ); } /** @@ -483,11 +406,11 @@ export class LiveTestResult implements ITestResult { throw new Error('cannot complete a test result multiple times'); } - // un-queue any tests that weren't explicitly updated - this.setAllToState( - TestResultState.Unset, - t => t.state.state === TestResultState.Queued || t.state.state === TestResultState.Running, - ); + for (const task of this.tasks) { + if (task.running) { + this.markTaskComplete(task.id); + } + } this._completedAt = Date.now(); this.completeEmitter.fire(); @@ -497,16 +420,80 @@ export class LiveTestResult implements ITestResult { * @inheritdoc */ public toJSON(): ISerializedTestResults | undefined { - return this.completedAt ? this.doSerialize.getValue() : undefined; + return this.completedAt && !('persist' in this.req && this.req.persist === false) + ? this.doSerialize.getValue() + : undefined; + } + + /** + * Updates all tests in the collection to the given state. + */ + protected setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) { + const index = this.mustGetTaskIndex(taskId); + for (const test of this.testById.values()) { + if (when(test.tasks[index], test)) { + this.fireUpdateAndRefresh(test, index, state); + } + } + } + + private fireUpdateAndRefresh(entry: TestResultItem, taskIndex: number, newState: TestResultState) { + const previousOwnComputed = entry.ownComputedState; + entry.tasks[taskIndex].state = newState; + const newOwnComputed = maxPriority(...entry.tasks.map(t => t.state)); + if (newOwnComputed === previousOwnComputed) { + return; + } + + entry.ownComputedState = newOwnComputed; + this.counts[previousOwnComputed]--; + this.counts[newOwnComputed]++; + refreshComputedState(this.computedStateAccessor, entry, t => + this.changeEmitter.fire( + t === entry + ? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous: previousOwnComputed } + : { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange } + ), + ); + } + + private addTestToRun(item: ITestItem, parent: string | null) { + const node = itemToNode(item, parent); + node.direct = this.includedIds.has(item.extId); + this.testById.set(item.extId, node); + this.counts[TestResultState.Unset]++; + + if (parent) { + this.testById.get(parent)?.children.push(node); + } + + if (this.tasks.length) { + for (let i = 0; i < this.tasks.length; i++) { + node.tasks.push({ duration: undefined, messages: [], state: TestResultState.Queued }); + } + } + + return node; + } + + private mustGetTaskIndex(taskId: string) { + const index = this.tasks.findIndex(t => t.id === taskId); + if (index === -1) { + throw new Error(`Unknown task ${taskId} in updateState`); + } + + return index; } private readonly doSerialize = new Lazy((): ISerializedTestResults => ({ id: this.id, completedAt: this.completedAt!, + tasks: this.tasks, items: [...this.testById.values()].map(entry => ({ ...entry, retired: undefined, - children: [...entry.children], + src: undefined, + children: [...entry.children.map(c => c.item.extId)], })), })); } @@ -530,6 +517,11 @@ export class HydratedTestResult implements ITestResult { */ public readonly completedAt: number; + /** + * @inheritdoc + */ + public readonly tasks: ITestRunTask[]; + /** * @inheritdoc */ @@ -546,19 +538,22 @@ export class HydratedTestResult implements ITestResult { ) { this.id = serialized.id; this.completedAt = serialized.completedAt; + this.tasks = serialized.tasks; for (const item of serialized.items) { - const cast: TestResultItem = { ...item, retired: true, children: new Set(item.children) }; + const cast: TestResultItem = { ...item, retired: true }; cast.item.uri = URI.revive(cast.item.uri); - for (const message of cast.state.messages) { - if (message.location) { - message.location.uri = URI.revive(message.location.uri); - message.location.range = Range.lift(message.location.range); + for (const task of cast.tasks) { + for (const message of task.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + message.location.range = Range.lift(message.location.range); + } } } - this.counts[item.state.state]++; + this.counts[item.ownComputedState]++; this.testById.set(item.item.extId, cast); } } diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 8db013579e6..2657bbed62e 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -7,17 +7,14 @@ import { findFirstInSorted } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; -import { Iterable } from 'vs/base/common/iterator'; -import { equals } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; export type ResultChangeEvent = | { completed: LiveTestResult } @@ -50,7 +47,7 @@ export interface ITestResultService { /** * Creates a new, live test result. */ - createLiveResult(collections: ReadonlyArray, req: RunTestsRequest): LiveTestResult; + createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest): LiveTestResult; /** * Adds a new test result to the collection. @@ -70,14 +67,6 @@ export interface ITestResultService { export const ITestResultService = createDecorator('testResultService'); -/** - * Returns if the tests in the results are exactly equal. Check the counts - * first as a cheap check before starting to iterate. - */ -const resultsEqual = (a: ITestResult, b: ITestResult) => - a.completedAt === b.completedAt && equals(a.counts, b.counts) && Iterable.equals(a.tests, b.tests, - (at, bt) => equals(at.state, bt.state) && equals(at.item, bt.item)); - export class TestResultService implements ITestResultService { declare _serviceBrand: undefined; private changeResultEmitter = new Emitter(); @@ -135,9 +124,13 @@ export class TestResultService implements ITestResultService { /** * @inheritdoc */ - public createLiveResult(collections: ReadonlyArray, req: RunTestsRequest) { - const id = generateUuid(); - return this.push(LiveTestResult.from(id, collections, this.storage.getOutputController(id), req)); + public createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest) { + if ('id' in req) { + return this.push(new LiveTestResult(req.id, this.storage.getOutputController(req.id), req)); + } else { + const id = generateUuid(); + return this.push(new LiveTestResult(id, this.storage.getOutputController(id), req)); + } } /** @@ -148,11 +141,6 @@ export class TestResultService implements ITestResultService { this.results.unshift(result); } else { const index = findFirstInSorted(this.results, r => r.completedAt !== undefined && r.completedAt <= result.completedAt!); - const prev = this.results[index]; - if (prev && resultsEqual(result, prev)) { - return result; - } - this.results.splice(index, 0, result); this.persistScheduler.schedule(); } @@ -166,7 +154,6 @@ export class TestResultService implements ITestResultService { result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter); this.isRunning.set(true); this.changeResultEmitter.fire({ started: result }); - result.setAllToState(TestResultState.Queued, () => true); } else { this.changeResultEmitter.fire({ inserted: result }); // If this is not a new result, go through each of its tests. For each diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index a4f609925b5..850e17a2cd4 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { groupBy } from 'vs/base/common/arrays'; +import { groupBy, mapFind } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; 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 { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -74,7 +75,7 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async expandTest(test: TestIdWithSrc, levels: number) { - await this.testControllers.get(test.src.provider)?.expandTest(test, levels); + await this.testControllers.get(test.src.controller)?.expandTest(test, levels); } /** @@ -159,7 +160,7 @@ export class TestService extends Disposable implements ITestService { } } - return this.testControllers.get(test.src.provider)?.lookupTest(test); + return this.testControllers.get(test.src.controller)?.lookupTest(test); } /** @@ -193,18 +194,27 @@ export class TestService extends Disposable implements ITestService { req.exclude = [...this.excludeTests.value]; } - const subscriptions = [...this.testSubscriptions.values()] - .filter(v => req.tests.some(t => v.collection.getNodeById(t.testId))) - .map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri)); - const result = this.testResults.createLiveResult(subscriptions.map(s => s.object), req); + const result = this.testResults.createLiveResult(req); + 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(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1); + const tests = groupBy(testsWithIds, (a, b) => a.src.controller === b.src.controller ? 0 : 1); const cancelSource = new CancellationTokenSource(token); this.runningTests.set(req, cancelSource); const requests = tests.map( - group => this.testControllers.get(group[0].src.provider)?.runTests( + group => this.testControllers.get(group[0].src.controller)?.runTests( { runId: result.id, debug: req.debug, @@ -221,7 +231,6 @@ export class TestService extends Disposable implements ITestService { return result; } finally { this.runningTests.delete(req); - subscriptions.forEach(s => s.dispose()); result.markComplete(); } } @@ -372,7 +381,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * @inheritdoc */ public get busyProviders() { - return this.busyProviderCount; + return this.busyControllerCount; } /** @@ -457,12 +466,12 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * Applies the diff to the collection. */ public override apply(diff: TestsDiff) { - let prevBusy = this.busyProviderCount; + let prevBusy = this.busyControllerCount; let prevPendingRoots = this.pendingRootCount; super.apply(diff); - if (prevBusy !== this.busyProviderCount) { - this.busyProvidersChangeEmitter.fire(this.busyProviderCount); + if (prevBusy !== this.busyControllerCount) { + this.busyProvidersChangeEmitter.fire(this.busyControllerCount); } if (prevPendingRoots !== this.pendingRootCount) { this.pendingRootChangeEmitter.fire(this.pendingRootCount); diff --git a/src/vs/workbench/contrib/testing/common/testStubs.ts b/src/vs/workbench/contrib/testing/common/testStubs.ts index d2b70221e39..d159f66d391 100644 --- a/src/vs/workbench/contrib/testing/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/common/testStubs.ts @@ -3,9 +3,13 @@ * 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'; +export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes'; +export * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; + export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl[] = []): TestItemImpl => { const item = new TestItemImpl(idPrefix + label, label, URI.file('/'), undefined); if (children.length) { @@ -22,6 +26,24 @@ export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl return item; }; +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, [ diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts index 36a375b2a11..1e44ba1ba3f 100644 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts @@ -10,7 +10,7 @@ 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 { InternalTestItem, TestDiffOpType, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestIdWithMaybeSrc } 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 { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -67,7 +67,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { */ private makeRunner() { let isRunning = false; - const rerunIds = new Map(); + const rerunIds = new Map(); const store = new DisposableStore(); const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); @@ -91,8 +91,8 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { } }, delay)); - const addToRerun = (test: InternalTestItem) => { - rerunIds.set(`${test.item.extId}/${test.src.provider}`, ({ testId: test.item.extId, src: test.src })); + const addToRerun = (test: TestIdWithMaybeSrc) => { + rerunIds.set(`${test.testId}/${test.src?.controller}`, test); if (!isRunning) { scheduler.schedule(delay); } @@ -100,7 +100,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { store.add(this.results.onTestChanged(evt => { if (evt.reason === TestResultItemChangeReason.Retired) { - addToRerun(evt.item); + addToRerun({ testId: evt.item.item.extId }); } })); @@ -113,7 +113,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { for (const [, collection] of sub.workspaceFolderCollections) { for (const rootId of collection.rootIds) { const root = collection.getNodeById(rootId); - if (root) { addToRerun(root); } + if (root) { addToRerun({ testId: root.item.extId, src: root.src }); } } } } @@ -122,7 +122,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { store.add(sub.onDiff(([, diff]) => { for (const entry of diff) { if (entry[0] === TestDiffOpType.Add) { - addToRerun(entry[1]); + addToRerun({ testId: entry[1].item.extId, src: entry[1].src }); } } })); diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 49a8b21e2ec..38930d4c833 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -47,13 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode let text: string | undefined; switch (parsed.type) { case TestUriType.ResultActualOutput: - text = test.state.messages[parsed.messageIndex]?.actualOutput; + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.actualOutput; break; case TestUriType.ResultExpectedOutput: - text = test.state.messages[parsed.messageIndex]?.expectedOutput; + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.expectedOutput; break; case TestUriType.ResultMessage: - text = test.state.messages[parsed.messageIndex]?.message.toString(); + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.message.toString(); break; } diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 2d7f9bd0db8..977b6867261 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -34,7 +34,25 @@ export const stateNodes = Object.entries(statePriority).reduce( export const cmpPriority = (a: TestResultState, b: TestResultState) => statePriority[b] - statePriority[a]; -export const maxPriority = (a: TestResultState, b: TestResultState) => statePriority[a] > statePriority[b] ? a : b; +export const maxPriority = (...states: TestResultState[]) => { + switch (states.length) { + case 0: + return TestResultState.Unset; + case 1: + return states[0]; + case 2: + return statePriority[states[0]] > statePriority[states[1]] ? states[0] : states[1]; + default: + let max = states[0]; + for (let i = 1; i < states.length; i++) { + if (statePriority[max] < statePriority[states[i]]) { + max = states[i]; + } + } + + return max; + } +}; export const statesInOrder = Object.keys(statePriority).map(s => Number(s) as TestResultState).sort(cmpPriority); diff --git a/src/vs/workbench/contrib/testing/common/testingUri.ts b/src/vs/workbench/contrib/testing/common/testingUri.ts index 9503c99e152..7f19b9db3ec 100644 --- a/src/vs/workbench/contrib/testing/common/testingUri.ts +++ b/src/vs/workbench/contrib/testing/common/testingUri.ts @@ -15,6 +15,7 @@ export const enum TestUriType { interface IResultTestUri { resultId: string; + taskIndex: number; testExtId: string; } @@ -46,17 +47,18 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => { const [locationId, ...request] = uri.path.slice(1).split('/'); if (request[0] === TestUriParts.Messages) { - const index = Number(request[1]); - const part = request[2]; + const taskIndex = Number(request[1]); + const index = Number(request[2]); + const part = request[3]; const testExtId = uri.query; if (type === TestUriParts.Results) { switch (part) { case TestUriParts.Text: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultMessage }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultMessage }; case TestUriParts.ActualOutput: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput }; case TestUriParts.ExpectedOutput: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; } } } @@ -69,20 +71,20 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => { scheme: TEST_DATA_SCHEME, authority: TestUriParts.Results }; - const msgRef = (locationId: string, index: number, ...remaining: string[]) => + const msgRef = (locationId: string, ...remaining: (string | number)[]) => URI.from({ ...uriParts, query: parsed.testExtId, - path: ['', locationId, TestUriParts.Messages, index, ...remaining].join('/'), + path: ['', locationId, TestUriParts.Messages, ...remaining].join('/'), }); switch (parsed.type) { case TestUriType.ResultActualOutput: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ActualOutput); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ActualOutput); case TestUriType.ResultExpectedOutput: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ExpectedOutput); case TestUriType.ResultMessage: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.Text); default: throw new Error('Invalid test uri'); } 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 63bad953408..089e459cbc9 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -9,11 +9,11 @@ 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 { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestTaskState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, 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 { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs'; +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 { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -23,34 +23,60 @@ export const emptyOutputController = () => new LiveOutputController( ); suite('Workbench - Test Results Service', () => { - const getLabelsIn = (it: Iterable) => [...it].map(t => t.item.label).sort(); + const getLabelsIn = (it: Iterable) => [...it].map(t => t.item.label).sort(); const getChangeSummary = () => [...changed] .map(c => ({ reason: c.reason, label: c.item.item.label })) .sort((a, b) => a.label.localeCompare(b.label)); - let r: LiveTestResult; + let r: TestLiveTestResult; let changed = new Set(); + let tests: TestItemImpl; + + const defaultOpts = { + exclude: [], + debug: false, + id: 'x', + persist: true, + }; + + class TestLiveTestResult extends LiveTestResult { + public setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) { + super.setAllToState(state, taskId, when); + } + } setup(async () => { changed = new Set(); - r = LiveTestResult.from( + r = new TestLiveTestResult( 'foo', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false }, + { ...defaultOpts, tests: ['id-a'] }, ); r.onChange(e => changed.add(e)); + 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)); }); suite('LiveTestResult', () => { - test('is empty if no tests are requesteed', async () => { - const r = LiveTestResult.from('', [await getInitializedMainTestCollection()], emptyOutputController(), { tests: [], debug: false }); - assert.deepStrictEqual(getLabelsIn(r.tests), []); + test('is empty if no tests are yet present', async () => { + assert.deepStrictEqual(getLabelsIn(new TestLiveTestResult( + 'foo', + emptyOutputController(), + { ...defaultOpts, tests: ['id-a'] }, + ).tests), []); }); - test('does not change or retire initially', () => { - assert.deepStrictEqual(0, changed.size); + test('initially queues with update', () => { + assert.deepStrictEqual(getChangeSummary(), [ + { label: 'a', reason: TestResultItemChangeReason.ComputedStateChange }, + { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'ab', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, + ]); }); test('initializes with the subtree of requested tests', () => { @@ -60,19 +86,29 @@ suite('Workbench - Test Results Service', () => { test('initializes with valid counts', () => { assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), - [TestRunState.Unset]: 4 + [TestRunState.Queued]: 2, + [TestRunState.Unset]: 2, }); }); test('setAllToState', () => { - r.setAllToState(TestRunState.Queued, t => t.item.label !== 'root'); + changed.clear(); + r.setAllToState(TestRunState.Queued, 't', (_, t) => t.item.label !== 'root'); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), [TestRunState.Unset]: 1, [TestRunState.Queued]: 3, }); - assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Queued); + r.setAllToState(TestRunState.Passed, 't', (_, t) => t.item.label !== 'root'); + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Unset]: 1, + [TestRunState.Passed]: 3, + }); + + assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestRunState.Passed); + assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestRunState.Passed); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, @@ -82,22 +118,26 @@ suite('Workbench - Test Results Service', () => { }); test('updateState', () => { - r.updateState('id-a', TestRunState.Running); + changed.clear(); + r.updateState('id-aa', 't', TestRunState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), + [TestRunState.Unset]: 2, [TestRunState.Running]: 1, - [TestRunState.Unset]: 3, + [TestRunState.Queued]: 1, }); - assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Running); + assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Running); // update computed state: assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running); assert.deepStrictEqual(getChangeSummary(), [ - { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'a', reason: TestResultItemChangeReason.ComputedStateChange }, + { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, ]); }); test('retire', () => { + changed.clear(); r.retire('id-a'); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.Retired }, @@ -110,21 +150,20 @@ suite('Workbench - Test Results Service', () => { assert.strictEqual(changed.size, 0); }); - test('addTestToRun', () => { - r.updateState('id-b', TestRunState.Running); + test('ignores outside run', () => { + changed.clear(); + r.updateState('id-b', 't', TestRunState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), - [TestRunState.Running]: 1, - [TestRunState.Unset]: 4, + [TestRunState.Queued]: 2, + [TestRunState.Unset]: 2, }); - assert.deepStrictEqual(r.getStateById('id-b')?.state.state, TestRunState.Running); - // update computed state: - assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running); + assert.deepStrictEqual(r.getStateById('id-b'), undefined); }); test('markComplete', () => { - r.setAllToState(TestRunState.Queued, () => true); - r.updateState('id-aa', TestRunState.Passed); + r.setAllToState(TestRunState.Queued, 't', () => true); + r.updateState('id-aa', 't', TestRunState.Passed); changed.clear(); r.markComplete(); @@ -135,8 +174,8 @@ suite('Workbench - Test Results Service', () => { [TestRunState.Unset]: 3, }); - assert.deepStrictEqual(r.getStateById('id-root')?.state.state, TestRunState.Unset); - assert.deepStrictEqual(r.getStateById('id-aa')?.state.state, TestRunState.Passed); + assert.deepStrictEqual(r.getStateById('id-root')?.ownComputedState, TestRunState.Unset); + assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Passed); }); }); @@ -160,7 +199,7 @@ suite('Workbench - Test Results Service', () => { test('serializes and re-hydrates', async () => { results.push(r); - r.updateState('id-aa', TestRunState.Passed); + r.updateState('id-aa', 't', TestRunState.Passed); r.markComplete(); await timeout(0); // allow persistImmediately async to happen @@ -175,12 +214,12 @@ suite('Workbench - Test Results Service', () => { const [rehydrated, actual] = results.getStateById('id-root')!; const expected: any = { ...r.getStateById('id-root')! }; - delete expected.state.duration; // delete undefined props that don't survive serialization + delete expected.tasks[0].duration; // delete undefined props that don't survive serialization delete expected.item.range; delete expected.item.description; expected.item.uri = actual.item.uri; - assert.deepStrictEqual(actual, { ...expected, retired: true }); + assert.deepStrictEqual(actual, { ...expected, src: undefined, retired: true, children: ['id-a'] }); assert.deepStrictEqual(rehydrated.counts, r.counts); assert.strictEqual(typeof rehydrated.completedAt, 'number'); }); @@ -189,11 +228,10 @@ suite('Workbench - Test Results Service', () => { results.push(r); r.markComplete(); - const r2 = results.push(LiveTestResult.from( + const r2 = results.push(new LiveTestResult( '', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } + { ...defaultOpts, tests: [] } )); results.clear(); @@ -202,11 +240,10 @@ suite('Workbench - Test Results Service', () => { test('keeps ongoing tests on top', async () => { results.push(r); - const r2 = results.push(LiveTestResult.from( + const r2 = results.push(new LiveTestResult( '', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } + { ...defaultOpts, tests: [] } )); assert.deepStrictEqual(results.results, [r2, r]); @@ -219,10 +256,12 @@ suite('Workbench - Test Results Service', () => { const makeHydrated = async (completedAt = 42, state = TestRunState.Passed) => new HydratedTestResult({ completedAt, id: 'some-id', + tasks: [{ id: 't', running: false, name: undefined }], items: [{ ...(await getInitializedMainTestCollection()).getNodeById('id-a')!, - state: { state, duration: 0, messages: [] }, + tasks: [{ state, duration: 0, messages: [] }], computedState: state, + ownComputedState: state, retired: undefined, children: [], }] @@ -235,16 +274,14 @@ suite('Workbench - Test Results Service', () => { assert.deepStrictEqual(results.results, [r, hydrated]); }); - test('deduplicates identical results', async () => { + test('inserts in correct order', async () => { results.push(r); const hydrated1 = await makeHydrated(); results.push(hydrated1); - const hydrated2 = await makeHydrated(); - results.push(hydrated2); assert.deepStrictEqual(results.results, [r, hydrated1]); }); - test('does not deduplicate if different completedAt', async () => { + test('inserts in correct order 2', async () => { results.push(r); const hydrated1 = await makeHydrated(); results.push(hydrated1); @@ -252,14 +289,5 @@ suite('Workbench - Test Results Service', () => { results.push(hydrated2); assert.deepStrictEqual(results.results, [r, hydrated1, hydrated2]); }); - - test('does not deduplicate if different tests', async () => { - results.push(r); - const hydrated1 = await makeHydrated(); - results.push(hydrated1); - const hydrated2 = await makeHydrated(undefined, TestRunState.Failed); - results.push(hydrated2); - assert.deepStrictEqual(results.results, [r, hydrated2, hydrated1]); - }); }); }); 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 33237417545..1d2d205854d 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -8,24 +8,32 @@ 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 { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl'; -import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { Convert, testStubs, testStubsChain } 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'; suite('Workbench - Test Result Storage', () => { let storage: InMemoryResultStorage; - let collection: MainThreadTestCollection; const makeResult = (addMessage?: string) => { - const t = LiveTestResult.from( + const t = new LiveTestResult( '', - [collection], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false } + { + tests: [], + exclude: [], + debug: false, + id: 'x', + persist: true, + } ); + + t.addTask({ id: 't', name: undefined, running: true }); + const tests = testStubs.nested(); + t.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from)); + if (addMessage) { - t.appendMessage('id-a', { + t.appendMessage('id-a', 't', { message: addMessage, actualOutput: undefined, expectedOutput: undefined, @@ -41,7 +49,6 @@ suite('Workbench - Test Result Storage', () => { assert.deepStrictEqual((await storage.read()).map(r => r.id), stored.map(s => s.id)); setup(async () => { - collection = await getInitializedMainTestCollection(); storage = new InMemoryResultStorage(new TestStorageService(), new NullLogService()); }); @@ -68,7 +75,7 @@ suite('Workbench - Test Result Storage', () => { test('limits stored result by budget', async () => { const r = range(100).map(() => makeResult('a'.repeat(2048))); await storage.persist(r); - await assertStored(r.slice(0, 41)); + await assertStored(r.slice(0, 46)); }); test('always stores the min number of results', async () => { diff --git a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts index e9c1d60412e..b2e1bdf7112 100644 --- a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts @@ -9,9 +9,9 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb suite('Workbench - Testing URIs', () => { test('round trip', () => { const uris: ParsedTestUri[] = [ - { type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, - { type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, - { type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultActualOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultExpectedOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultMessage, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, ]; for (const uri of uris) { diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 7f6cb0343c2..1236700012e 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -75,19 +75,19 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } } + { src: { tree: 0, controller: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('a')) } } + { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('a')) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) } + { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) } + { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) } ], [ TestDiffOpType.Update, @@ -95,7 +95,7 @@ suite('ExtHost Testing', () => { ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) } + { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) } ], [ TestDiffOpType.Update, @@ -149,7 +149,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { - src: { tree: 0, provider: 'pid' }, + src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child),