diff --git a/.eslintrc.json b/.eslintrc.json index 24b5b101e17..e5042d84920 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1009,6 +1009,7 @@ "end", "expand", "hide", + "invalidate", "open", "override", "receive", diff --git a/extensions/testing-editor-contributions/src/extension.ts b/extensions/testing-editor-contributions/src/extension.ts index fc6ab30b1ff..79b0c851532 100644 --- a/extensions/testing-editor-contributions/src/extension.ts +++ b/extensions/testing-editor-contributions/src/extension.ts @@ -3,410 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; - -const localize = nls.loadMessageBundle(); - -interface IDisposable { - dispose(): void; +export function activate() { + // no-op. This extension may be removed in the future } - -const enum Constants { - ConfigSection = 'testing', - EnableCodeLensConfig = 'enableCodeLens', - EnableDiagnosticsConfig = 'enableProblemDiagnostics', -} - -export function activate(context: vscode.ExtensionContext) { - const diagnostics = vscode.languages.createDiagnosticCollection(); - const services = new TestingEditorServices(diagnostics); - context.subscriptions.push( - services, - diagnostics, - vscode.languages.registerCodeLensProvider({ scheme: 'file' }, services), - ); -} - -class TestingConfig implements IDisposable { - private section = vscode.workspace.getConfiguration(Constants.ConfigSection); - private readonly changeEmitter = new vscode.EventEmitter(); - private readonly listener = vscode.workspace.onDidChangeConfiguration(evt => { - if (evt.affectsConfiguration(Constants.ConfigSection)) { - this.section = vscode.workspace.getConfiguration(Constants.ConfigSection); - this.changeEmitter.fire(); - } - }); - - public readonly onChange = this.changeEmitter.event; - - public get codeLens() { - return this.section.get(Constants.EnableCodeLensConfig, true); - } - - public get diagnostics() { - return this.section.get(Constants.EnableDiagnosticsConfig, false); - } - - public get isEnabled() { - return this.codeLens || this.diagnostics; - } - - public dispose() { - this.listener.dispose(); - } -} - -export class TestingEditorServices implements IDisposable, vscode.CodeLensProvider { - private readonly codeLensChangeEmitter = new vscode.EventEmitter(); - private readonly documents = new Map(); - private readonly config = new TestingConfig(); - private disposables: IDisposable[]; - private wasEnabled = this.config.isEnabled; - - /** - * @inheritdoc - */ - public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event; - - constructor(private readonly diagnostics: vscode.DiagnosticCollection) { - this.disposables = [ - new vscode.Disposable(() => this.expireAll()), - - this.config, - - vscode.window.onDidChangeVisibleTextEditors((editors) => { - if (!this.config.isEnabled) { - return; - } - - const expiredEditors = new Set(this.documents.keys()); - for (const editor of editors) { - const key = editor.document.uri.toString(); - this.ensure(key, editor.document); - expiredEditors.delete(key); - } - - for (const expired of expiredEditors) { - this.expire(expired); - } - }), - - vscode.workspace.onDidCloseTextDocument((document) => { - this.expire(document.uri.toString()); - }), - - this.config.onChange(() => { - if (!this.wasEnabled || this.config.isEnabled) { - this.attachToAllVisible(); - } else if (this.wasEnabled || !this.config.isEnabled) { - this.expireAll(); - } - - this.wasEnabled = this.config.isEnabled; - this.codeLensChangeEmitter.fire(); - }), - ]; - - if (this.config.isEnabled) { - this.attachToAllVisible(); - } - } - - /** - * @inheritdoc - */ - public provideCodeLenses(document: vscode.TextDocument) { - if (!this.config.codeLens) { - return []; - } - - return this.documents.get(document.uri.toString())?.provideCodeLenses() ?? []; - } - - /** - * Attach to all currently visible editors. - */ - private attachToAllVisible() { - for (const editor of vscode.window.visibleTextEditors) { - this.ensure(editor.document.uri.toString(), editor.document); - } - } - - /** - * Unattaches to all tests. - */ - private expireAll() { - for (const observer of this.documents.values()) { - observer.dispose(); - } - - this.documents.clear(); - } - - /** - * Subscribes to tests for the document URI. - */ - private ensure(key: string, document: vscode.TextDocument) { - const state = this.documents.get(key); - if (!state) { - const observer = new DocumentTestObserver(document, this.diagnostics, this.config); - this.documents.set(key, observer); - observer.onDidChangeCodeLenses(() => this.config.codeLens && this.codeLensChangeEmitter.fire()); - } - } - - /** - * Expires and removes the watcher for the document. - */ - private expire(key: string) { - const observer = this.documents.get(key); - if (!observer) { - return; - } - - observer.dispose(); - this.documents.delete(key); - } - - /** - * @override - */ - public dispose() { - this.disposables.forEach((d) => d.dispose()); - } -} - -class DocumentTestObserver implements IDisposable { - private readonly codeLensChangeEmitter = new vscode.EventEmitter(); - private readonly observer = vscode.test.createDocumentTestObserver(this.document); - private readonly disposables: IDisposable[]; - public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event; - private didHaveDiagnostics = this.config.diagnostics; - - constructor( - private readonly document: vscode.TextDocument, - private readonly diagnostics: vscode.DiagnosticCollection, - private readonly config: TestingConfig, - ) { - this.disposables = [ - this.observer, - this.codeLensChangeEmitter, - - config.onChange(() => { - if (this.didHaveDiagnostics && !config.diagnostics) { - this.diagnostics.set(document.uri, []); - } else if (!this.didHaveDiagnostics && config.diagnostics) { - this.updateDiagnostics(); - } - - this.didHaveDiagnostics = config.diagnostics; - }), - - this.observer.onDidChangeTest(() => { - this.updateDiagnostics(); - this.codeLensChangeEmitter.fire(); - }), - ]; - - } - - private updateDiagnostics() { - if (!this.config.diagnostics) { - return; - } - - const uriString = this.document.uri.toString(); - const diagnostics: vscode.Diagnostic[] = []; - for (const test of iterateOverTests(this.observer.tests)) { - for (const message of test.state.messages) { - if (message.location?.uri.toString() === uriString) { - diagnostics.push({ - range: message.location.range, - message: message.message.toString(), - severity: testToDiagnosticSeverity(message.severity), - }); - } - } - } - - this.diagnostics.set(this.document.uri, diagnostics); - } - - public provideCodeLenses(): vscode.CodeLens[] { - const lenses: vscode.CodeLens[] = []; - - for (const test of iterateOverTests(this.observer.tests)) { - const { debuggable = false, runnable = true } = test; - if (!test.location || !(debuggable || runnable)) { - continue; - } - - const summary = summarize(test); - - lenses.push({ - isResolved: true, - range: test.location.range, - command: { - title: `$(${testStateToIcon[summary.computedState]}) ${getLabelFor(test, summary)}`, - command: 'vscode.runTests', - arguments: [[test]], - tooltip: localize('tooltip.debug', 'Debug {0}', test.label), - }, - }); - - if (debuggable) { - lenses.push({ - isResolved: true, - range: test.location.range, - command: { - title: localize('action.debug', 'Debug'), - command: 'vscode.debugTests', - arguments: [[test]], - tooltip: localize('tooltip.debug', 'Debug {0}', test.label), - }, - }); - } - } - - return lenses; - } - - /** - * @override - */ - public dispose() { - this.diagnostics.set(this.document.uri, []); - this.disposables.forEach(d => d.dispose()); - } -} - -function getLabelFor(test: vscode.TestItem, summary: ITestSummary) { - if (summary.duration !== undefined) { - return localize( - 'tooltip.runStateWithDuration', - '{0}/{1} Tests Passed in {2}', - summary.passed, - summary.passed + summary.failed, - formatDuration(summary.duration), - ); - } - - if (summary.passed > 0 || summary.failed > 0) { - return localize('tooltip.runState', '{0}/{1} Tests Passed', summary.passed, summary.failed); - } - - if (test.state.runState === vscode.TestRunState.Passed) { - return test.state.duration !== undefined - ? localize('state.passedWithDuration', 'Passed in {0}', formatDuration(test.state.duration)) - : localize('state.passed', 'Passed'); - } - - if (isFailedState(test.state.runState)) { - return localize('state.failed', 'Failed'); - } - - return localize('action.run', 'Run Tests'); -} - -function formatDuration(duration: number) { - if (duration < 1_000) { - return `${Math.round(duration)}ms`; - } - - if (duration < 100_000) { - return `${(duration / 1000).toPrecision(3)}s`; - } - - return `${(duration / 1000 / 60).toPrecision(3)}m`; -} - -const statePriority: { [K in vscode.TestRunState]: number } = { - [vscode.TestRunState.Running]: 6, - [vscode.TestRunState.Queued]: 5, - [vscode.TestRunState.Errored]: 4, - [vscode.TestRunState.Failed]: 3, - [vscode.TestRunState.Passed]: 2, - [vscode.TestRunState.Skipped]: 1, - [vscode.TestRunState.Unset]: 0, -}; - -const maxPriority = (a: vscode.TestRunState, b: vscode.TestRunState) => - statePriority[a] > statePriority[b] ? a : b; - -const isFailedState = (s: vscode.TestRunState) => - s === vscode.TestRunState.Failed || s === vscode.TestRunState.Errored; - -interface ITestSummary { - passed: number; - failed: number; - duration: number | undefined; - computedState: vscode.TestRunState; -} - -function summarize(test: vscode.TestItem) { - let passed = 0; - let failed = 0; - let duration: number | undefined; - let computedState = test.state.runState; - - const queue = test.children ? [test.children] : []; - while (queue.length) { - for (const test of queue.pop()!) { - computedState = maxPriority(computedState, test.state.runState); - if (test.state.runState === vscode.TestRunState.Passed) { - passed++; - if (test.state.duration !== undefined) { - duration = test.state.duration + (duration ?? 0); - } - } else if (isFailedState(test.state.runState)) { - failed++; - if (test.state.duration !== undefined) { - duration = test.state.duration + (duration ?? 0); - } - } - - if (test.children) { - queue.push(test.children); - } - } - } - - return { passed, failed, duration, computedState }; -} - -function* iterateOverTests(tests: ReadonlyArray) { - const queue = [tests]; - while (queue.length) { - for (const test of queue.pop()!) { - yield test; - if (test.children) { - queue.push(test.children); - } - } - } -} - -const testStateToIcon: { [K in vscode.TestRunState]: string } = { - [vscode.TestRunState.Errored]: 'testing-error-icon', - [vscode.TestRunState.Failed]: 'testing-failed-icon', - [vscode.TestRunState.Passed]: 'testing-passed-icon', - [vscode.TestRunState.Queued]: 'testing-queued-icon', - [vscode.TestRunState.Skipped]: 'testing-skipped-icon', - [vscode.TestRunState.Unset]: 'beaker', - [vscode.TestRunState.Running]: 'loading~spin', -}; - -const testToDiagnosticSeverity = (severity: vscode.TestMessageSeverity | undefined) => { - switch (severity) { - case vscode.TestMessageSeverity.Hint: - return vscode.DiagnosticSeverity.Hint; - case vscode.TestMessageSeverity.Information: - return vscode.DiagnosticSeverity.Information; - case vscode.TestMessageSeverity.Warning: - return vscode.DiagnosticSeverity.Warning; - case vscode.TestMessageSeverity.Error: - default: - return vscode.DiagnosticSeverity.Error; - } -}; diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 633d8ca7e41..21e65570132 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -69,7 +69,7 @@ export class ObjectTree, TFilterData = void> extends this.model.updateElementHeight(element, height); } - resort(element: T, recursive = true): void { + resort(element: T | null, recursive = true): void { this.model.resort(element, recursive); } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a1b87145a47..becd34eed79 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2109,10 +2109,12 @@ declare module 'vscode' { export function registerTestProvider(testProvider: TestProvider): Disposable; /** - * Runs tests with the given options. If no options are given, then - * all tests are run. Returns the resulting test run. + * 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}. */ - export function runTests(options: TestRunOptions, cancellationToken?: CancellationToken): Thenable; + export function runTests(run: TestRunOptions, cancellationToken?: CancellationToken): Thenable; /** * Returns an observer that retrieves tests in the given workspace folder. @@ -2226,6 +2228,14 @@ declare module 'vscode' { */ readonly discoveredInitialTests?: Thenable; + /** + * An event that fires when a test becomes outdated, as a result of + * file changes, for example. In "watch" mode, tests that are outdated + * will be automatically re-run after a short delay. Firing a test + * with children will mark the entire subtree as outdated. + */ + readonly onDidInvalidateTest?: Event; + /** * Dispose will be called when there are no longer observers interested * in the hierarchy. @@ -2270,11 +2280,11 @@ declare module 'vscode' { * @todo this will eventually need to be able to return a summary report, coverage for example. */ // eslint-disable-next-line vscode-dts-provider-naming - runTests?(options: TestRunOptions, cancellationToken: CancellationToken): ProviderResult; + runTests?(options: TestRun, cancellationToken: CancellationToken): ProviderResult; } /** - * Options given to `TestProvider.runTests` + * Options given to {@link test.runTests} */ export interface TestRunOptions { /** @@ -2289,6 +2299,17 @@ declare module 'vscode' { debug: boolean; } + /** + * Options given to `TestProvider.runTests` + */ + export interface TestRun extends TestRunOptions { + /** + * 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. + */ + setState(test: T, state: TestState): void; + } + /** * A test item is an item shown in the "test explorer" view. It encompasses * both a suite and a test, since they have almost or identical capabilities. @@ -2337,12 +2358,6 @@ declare module 'vscode' { * Optional list of nested tests for this item. */ children?: TestItem[]; - - /** - * Test run state. Will generally be {@link TestRunState.Unset} by - * default. - */ - state: TestState; } /** @@ -2377,11 +2392,11 @@ declare module 'vscode' { * in order to update it. This allows consumers to quickly and easily check * for changes via object identity. */ - export class TestState { + export interface TestState { /** * Current state of the test. */ - readonly runState: TestRunState; + readonly state: TestRunState; /** * Optional duration of the test run, in milliseconds. @@ -2392,14 +2407,7 @@ declare module 'vscode' { * Associated test run message. Can, for example, contain assertion * failure information if the test fails. */ - readonly messages: ReadonlyArray>; - - /** - * @param state Run state to hold in the test state - * @param messages List of associated messages for the test - * @param duration Length of time the test run took, if appropriate. - */ - constructor(runState: TestRunState, messages?: TestMessage[], duration?: number); + readonly messages?: ReadonlyArray>; } /** diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 370f08df900..3b127f10a79 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -19,12 +19,6 @@ const reviveDiff = (diff: TestsDiff) => { if (item.item.location) { item.item.location.uri = URI.revive(item.item.location.uri); } - - for (const message of item.item.state.messages) { - if (message.location) { - message.location.uri = URI.revive(message.location.uri); - } - } } } }; @@ -37,30 +31,42 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh constructor( extHostContext: IExtHostContext, @ITestService private readonly testService: ITestService, - @ITestResultService resultService: ITestResultService, + @ITestResultService private readonly resultService: ITestResultService, ) { super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); - const testCompleteListener = this._register(new MutableDisposable()); - this._register(resultService.onNewTestResult(results => { - testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests })); - })); + // const testCompleteListener = this._register(new MutableDisposable()); + // todo(@connor4312): reimplement, maybe + // this._register(resultService.onResultsChanged(results => { + // testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: [] })); + // })); testService.updateRootProviderCount(1); - const lastCompleted = resultService.results.find(r => !r.isComplete); - if (lastCompleted) { - this.proxy.$publishTestResults({ tests: lastCompleted.tests }); - } - for (const { resource, uri } of this.testService.subscriptions) { this.proxy.$subscribeToTests(resource, uri); } } + /** + * @inheritdoc + */ + $updateTestStateInRun(runId: string, testId: string, state: ITestState): void { + const r = this.resultService.getResult(runId); + if (r && r instanceof LiveTestResult) { + for (const message of state.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + } + } + + r.updateState(testId, state); + } + } + /** * @inheritdoc */ @@ -105,8 +111,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this.testService.publishDiff(resource, URI.revive(uri), diff); } - public $runTests(req: RunTestsRequest, token: CancellationToken): Promise { - return this.testService.runTests(req, token); + public async $runTests(req: RunTestsRequest, token: CancellationToken): Promise { + const result = await this.testService.runTests(req, token); + return result.id; } public dispose() { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e13a14baa6b..82561916622 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1292,10 +1292,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // checkProposedApiEnabled(extension); return extHostTypes.TestMessageSeverity; }, - get TestState() { - // checkProposedApiEnabled(extension); - return extHostTypes.TestState; - }, get WorkspaceTrustState() { // checkProposedApiEnabled(extension); return extHostTypes.WorkspaceTrustState; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b4e3779cc4d..4460b821f65 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -58,7 +58,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, InternalTestResults, ITestState, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust'; @@ -1826,7 +1826,7 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; + $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; $subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; $lookupTest(test: TestIdWithProvider): Promise; @@ -1840,7 +1840,8 @@ export interface MainThreadTestingShape { $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $runTests(req: RunTestsRequest, token: CancellationToken): Promise; + $updateTestStateInRun(runId: string, testId: string, state: ITestState): void; + $runTests(req: RunTestsRequest, token: CancellationToken): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index fb30845668e..bab357e0043 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -16,11 +16,11 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; +import { TestItem, TestState } from 'vs/workbench/api/common/extHostTypeConverters'; import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; @@ -93,9 +93,7 @@ export class ExtHostTesting implements ExtHostTestingShape { // Find workspace items first, then owned tests, then document tests. // If a test instance exists in both the workspace and document, prefer // the workspace because it's less ephemeral. - .map(test => this.workspaceObservers.getMirroredTestDataByReference(test) - ?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test)) - ?? this.textDocumentObservers.getMirroredTestDataByReference(test)) + .map(this.getInternalTestForReference, this) .filter(isDefined) .map(item => ({ providerId: item.providerId, testId: item.id })), debug: req.debug @@ -219,10 +217,10 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { + public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { const provider = this.providers.get(req.providerId); if (!provider || !provider.runTests) { - return EMPTY_TEST_RESULT; + return; } const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual) @@ -230,16 +228,25 @@ export class ExtHostTesting implements ExtHostTestingShape { // Only send the actual TestItem's to the user to run. .map(t => t instanceof TestItemFilteredWrapper ? t.actual : t); if (!tests.length) { - return EMPTY_TEST_RESULT; + return; } try { - await provider.runTests({ tests, debug: req.debug }, cancellation); + await provider.runTests({ + setState: (test, state) => { + const internal = this.getInternalTestForReference(test); + if (internal) { + this.flushCollectionDiffs(); + this.proxy.$updateTestStateInRun(req.runId, internal.id, TestState.from(state)); + } + }, tests, debug: req.debug + }, cancellation); + for (const { collection } of this.testSubscriptions.values()) { collection.flushDiff(); // ensure all states are updated } - return EMPTY_TEST_RESULT; + return; } catch (e) { console.error(e); // so it appears to attached debuggers throw e; @@ -256,6 +263,28 @@ export class ExtHostTesting implements ExtHostTestingShape { return Promise.resolve(item); } + /** + * Flushes diff information for all collections to ensure state in the + * main thread is updated. + */ + private flushCollectionDiffs() { + for (const { collection } of this.testSubscriptions.values()) { + collection.flushDiff(); + } + } + + /** + * Gets the internal test item associated with the reference from the extension. + */ + private getInternalTestForReference(test: vscode.TestItem) { + // Find workspace items first, then owned tests, then document tests. + // If a test instance exists in both the workspace and document, prefer + // the workspace because it's less ephemeral. + return this.workspaceObservers.getMirroredTestDataByReference(test) + ?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test)) + ?? this.textDocumentObservers.getMirroredTestDataByReference(test); + } + private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy | undefined { if (!folder) { return; @@ -361,10 +390,6 @@ export class TestItemFilteredWrapper implements vscode.TestItem { return this.actual.runnable; } - public get state() { - return this.actual.state; - } - public get children() { // We only want children that match the filter. return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter); @@ -645,7 +670,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem { public get id() { return this.#internal.revived.id!; } public get label() { return this.#internal.revived.label; } public get description() { return this.#internal.revived.description; } - public get state() { return this.#internal.revived.state; } public get location() { return this.#internal.revived.location; } public get runnable() { return this.#internal.revived.runnable ?? true; } public get debuggable() { return this.#internal.revived.debuggable ?? false; } @@ -665,7 +689,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem { id: this.id, label: this.label, description: this.description, - state: this.state, location: this.location, runnable: this.runnable, debuggable: this.debuggable, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d1512e59b87..07c5c15550d 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1380,22 +1380,22 @@ export namespace NotebookDecorationRenderOptions { export namespace TestState { export function from(item: vscode.TestState): ITestState { return { - runState: item.runState, + state: item.state, duration: item.duration, - messages: item.messages.map(message => ({ + messages: item.messages?.map(message => ({ message: MarkdownString.fromStrict(message.message) || '', severity: message.severity, expectedOutput: message.expectedOutput, actualOutput: message.actualOutput, location: message.location ? location.from(message.location) : undefined, - })), + })) ?? [], }; } export function to(item: ITestState): vscode.TestState { - return new types.TestState( - item.runState, - item.messages.map(message => ({ + return { + state: item.state, + messages: item.messages.map(message => ({ message: typeof message.message === 'string' ? message.message : MarkdownString.to(message.message), severity: message.severity, expectedOutput: message.expectedOutput, @@ -1405,8 +1405,8 @@ export namespace TestState { uri: URI.revive(message.location.uri) }), })), - item.duration, - ); + duration: item.duration, + }; } } @@ -1420,7 +1420,6 @@ export namespace TestItem { debuggable: item.debuggable ?? false, description: item.description, runnable: item.runnable ?? true, - state: TestState.from(item.state), }; } @@ -1435,7 +1434,6 @@ export namespace TestItem { debuggable: item.debuggable, description: item.description, runnable: item.runnable, - state: TestState.to(item.state), }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index a4199cad15e..8bfa09d90a0 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2964,31 +2964,6 @@ export enum TestMessageSeverity { Hint = 3 } -@es5ClassCompat -export class TestState { - #runState: TestRunState; - #duration?: number; - #messages: ReadonlyArray>; - - public get runState() { - return this.#runState; - } - - public get duration() { - return this.#duration; - } - - public get messages() { - return this.#messages; - } - - constructor(runState: TestRunState, messages: vscode.TestMessage[] = [], duration?: number) { - this.#runState = runState; - this.#messages = Object.freeze(messages.map(m => Object.freeze(m))); - this.#duration = duration; - } -} - export type RequiredTestItem = vscode.RequiredTestItem; export type TestItem = vscode.TestItem; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index e1858ddb9ee..0129e797ade 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -10,13 +10,28 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; +const computedStateAccessor: IComputedStateAccessor = { + getOwnState: i => i.state, + getCurrentComputedState: i => i.state, + setComputedState: (i, s) => i.state = s, + getChildren: i => i.children.values(), + *getParents(i) { + for (let parent = i.parentItem; parent; parent = parent.parentItem) { + yield parent; + } + }, +}; + /** * Projection that lists tests in their traditional tree view. */ @@ -40,11 +55,42 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes */ public readonly onUpdate = this.updateEmitter.event; - constructor(listener: TestSubscriptionListener) { + constructor(listener: TestSubscriptionListener, @ITestResultService private readonly results: ITestResultService) { super(); this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff))); this._register(listener.onFolderChange(this.applyFolderChange, this)); + // when test results are cleared, recalculate all state + this._register(results.onResultsChanged((evt) => { + if (!('removed' in evt)) { + return; + } + + for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) { + const lookup = this.results.getStateByExtId(inTree.test.item.extId)?.[1]; + inTree.ownState = lookup?.state.state ?? TestRunState.Unset; + const computed = lookup?.computedState ?? TestRunState.Unset; + if (computed !== inTree.state) { + inTree.state = computed; + this.addUpdated(inTree); + } + } + + this.updateEmitter.fire(); + })); + + // when test states change, reflect in the tree + this._register(results.onTestChanged(([, { item, state, computedState }]) => { + for (const i of this.items.values()) { + if (i.test.item.extId === item.extId) { + i.ownState = state.state; + refreshComputedState(computedStateAccessor, i, this.addUpdated, computedState); + this.updateEmitter.fire(); + return; + } + } + })); + for (const [folder, collection] of listener.workspaceFolderCollections) { for (const node of collection.all) { this.storeItem(this.createItem(node, folder.folder)); @@ -96,7 +142,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const locationChanged = !locationsEqual(existing.location, item.item.location); if (locationChanged) { this.locations.remove(existing); } - existing.update(item, this.addUpdated); + existing.update(item); if (locationChanged) { this.locations.add(existing); } this.addUpdated(existing); break; @@ -172,5 +218,11 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes item.parentItem.children.add(item); this.items.set(item.test.id, item); this.locations.add(item); + + const prevState = this.results.getStateByExtId(item.test.item.extId)?.[1]; + if (prevState) { + item.ownState = prevState.state.state; + refreshComputedState(computedStateAccessor, item, 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 b263ca46c16..8775e2454d0 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -10,6 +10,7 @@ import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; /** @@ -64,9 +65,9 @@ export class HierarchicalByNameElement extends HierarchicalElement { /** * @override */ - public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) { + public update(actual: InternalTestItem) { const wasRunnable = this.test.item.runnable; - super.update(actual, addUpdated); + super.update(actual); if (this.test.item.runnable !== wasRunnable) { this.updateLeafTestState(); @@ -117,8 +118,8 @@ export class HierarchicalByNameElement extends HierarchicalElement { * test root rather than the heirarchal parent. */ export class HierarchicalByNameProjection extends HierarchicalByLocationProjection { - constructor(listener: TestSubscriptionListener) { - super(listener); + constructor(listener: TestSubscriptionListener, @ITestResultService results: ITestResultService) { + super(listener, results); const originalRenderNode = this.renderNode.bind(this); this.renderNode = (node, recurse) => { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 07bd0d0c1a7..fb1dc153526 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -7,7 +7,6 @@ import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; -import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; /** @@ -15,7 +14,6 @@ import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testi */ export class HierarchicalElement implements ITestTreeElement { public readonly children = new Set(); - public computedState: TestRunState | undefined; public readonly depth: number = this.parentItem.depth + 1; public get treeId() { @@ -26,10 +24,6 @@ export class HierarchicalElement implements ITestTreeElement { return this.test.item.label; } - public get state() { - return this.test.item.state.runState; - } - public get location() { return this.test.item.location; } @@ -46,16 +40,15 @@ export class HierarchicalElement implements ITestTreeElement { : Iterable.empty(); } + public state = TestRunState.Unset; + public ownState = TestRunState.Unset; + constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) { this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese } - public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) { - const stateChange = actual.item.state.runState !== this.state; + public update(actual: InternalTestItem) { Object.assign(this.test, actual); - if (stateChange) { - refreshComputedState(this, addUpdated); - } } } @@ -80,67 +73,12 @@ export class HierarchicalFolder implements ITestTreeElement { return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable)); } + public state = TestRunState.Unset; + public ownState = TestRunState.Unset; + constructor(private readonly folder: IWorkspaceFolder) { } public get label() { return this.folder.name; } } - -/** - * Gets the computed state for the node. - */ -export const getComputedState = (node: ITestTreeElement) => { - if (node.computedState === undefined) { - node.computedState = node.state ?? TestRunState.Unset; - for (const child of node.children) { - node.computedState = maxPriority(node.computedState, getComputedState(child)); - } - } - - return node.computedState; -}; - -/** - * Refreshes the computed state for the node and its parents. Any changes - * elements cause `addUpdated` to be called. - */ -export const refreshComputedState = (node: ITestTreeElement, addUpdated: (n: ITestTreeElement) => void) => { - if (node.computedState === undefined) { - return; - } - - const oldPriority = statePriority[node.computedState]; - node.computedState = undefined; - const newState = getComputedState(node); - const newPriority = statePriority[getComputedState(node)]; - if (newPriority === oldPriority) { - return; - } - - addUpdated(node); - if (newPriority > oldPriority) { - // Update all parents to ensure they're at least this priority. - for (let parent = node.parentItem; parent; parent = parent.parentItem) { - const prev = parent.computedState; - if (prev !== undefined && statePriority[prev] >= newPriority) { - break; - } - - parent.computedState = newState; - addUpdated(parent); - } - } else if (newPriority < oldPriority) { - // Re-render all parents of this node whose computed priority might have come from this node - for (let parent = node.parentItem; parent; parent = parent.parentItem) { - const prev = parent.computedState; - if (prev === undefined || statePriority[prev] > oldPriority) { - break; - } - - parent.computedState = undefined; - parent.computedState = getComputedState(parent); - addUpdated(parent); - } - } -}; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 78459dfe316..4a9e0770d5e 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -40,13 +40,6 @@ export interface ITestTreeProjection extends IDisposable { export interface ITestTreeElement { - /** - * Computed element state. Will be set automatically if not initially provided. - * The projection is responsible for clearing (or updating) this if it - * becomes invalid. - */ - computedState: TestRunState | undefined; - readonly children: Set; /** @@ -85,9 +78,11 @@ export interface ITestTreeElement { readonly debuggable: Iterable; /** - * State of of the tree item. Mostly used for deriving the computed state. + * Element state to display. */ - readonly state?: TestRunState; + state: TestRunState; + + readonly ownState: TestRunState; readonly label: string; readonly parentItem: ITestTreeElement | null; } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts deleted file mode 100644 index ea6f7c7919a..00000000000 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts +++ /dev/null @@ -1,321 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { Emitter } from 'vs/base/common/event'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; -import { Location as ModeLocation } from 'vs/editor/common/modes'; -import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; -import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; -import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; -import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; -import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; -import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; - -interface IStatusTestItem extends IncrementalTestCollectionItem { - treeElements: Map; - previousState: TestRunState; - depth: number; - parentItem?: IStatusTestItem; - location?: ModeLocation; -} - -type TreeElement = StateElement | TestStateElement; - -class TestStateElement implements ITestTreeElement { - public computedState = this.state; - - public get treeId() { - return `sltest:${this.test.id}`; - } - - public get label() { - return this.test.item.label; - } - - public get location() { - return this.test.item.location; - } - - public get runnable(): Iterable { - // if this item is runnable and all its children are in the same state, - // we can run all of them in one go. This will eventually be true - // for leaf nodes, whose treeElements contain only their own state. - if (this.test.item.runnable && this.test.treeElements.size === 1) { - return [{ testId: this.test.id, providerId: this.test.providerId }]; - } - - return Iterable.concatNested(Iterable.map(this.children, c => c.runnable)); - } - - public get debuggable(): Iterable { - // same logic as runnable above - if (this.test.item.debuggable && this.test.treeElements.size === 1) { - return [{ testId: this.test.id, providerId: this.test.providerId }]; - } - - return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable)); - } - - public readonly depth = this.test.depth; - public readonly children = new Set(); - - constructor( - public readonly state: TestRunState, - public readonly test: IStatusTestItem, - public readonly parentItem: TestStateElement | StateElement, - ) { - parentItem.children.add(this); - } - - public remove() { - this.parentItem.children.delete(this); - } -} - -/** - * Shows tests in a hierarchical way, but grouped by status. This is more - * complex than it may look at first glance, because nodes can appear in - * multiple places if they have children with different statuses. - */ -export class StateByLocationProjection extends AbstractIncrementalTestCollection implements ITestTreeProjection { - private readonly updateEmitter = new Emitter(); - private readonly changes = new NodeChangeList(); - private readonly locations = new TestLocationStore(); - private readonly disposable = new DisposableStore(); - - /** - * @inheritdoc - */ - public readonly onUpdate = this.updateEmitter.event; - - /** - * Root elements for states in the tree. - */ - protected readonly stateRoots = new Map>(); - - constructor(listener: TestSubscriptionListener) { - super(); - - this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff))); - - const firstDiff: TestsDiff = []; - for (const [, collection] of listener.workspaceFolderCollections) { - firstDiff.push(...collection.getReviverDiff()); - } - - this.apply(firstDiff); - } - - /** - * Frees listeners associated with the projection. - */ - public dispose() { - this.disposable.dispose(); - } - - /** - * @inheritdoc - */ - public getTestAtPosition(uri: URI, position: Position) { - const item = this.locations.getTestAtPosition(uri, position); - if (!item) { - return undefined; - } - - for (const state of statesInOrder) { - const element = item.treeElements.get(state); - if (element) { - return element; - } - } - - return undefined; - } - - /** - * @inheritdoc - */ - public applyTo(tree: ObjectTree) { - this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values()); - } - - private readonly renderNode: NodeRenderFn = (node, recurse) => { - if (node.depth === 1 /* test provider */) { - if (node.children.size === 0) { - return NodeRenderDirective.Omit; - } else if (!peersHaveChildren(node, () => this.stateRoots.values())) { - return NodeRenderDirective.Concat; - } - } - - return { - element: node, - children: recurse(node.children), - }; - }; - - /** - * @override - */ - protected createChangeCollector(): IncrementalChangeCollector { - return { - add: node => { - this.resolveNodesRecursive(node); - this.locations.add(node); - }, - remove: (node, isNested) => { - this.locations.remove(node); - - if (!isNested) { - for (const state of node.treeElements.keys()) { - this.pruneStateElements(node, state, true); - } - } - }, - update: node => { - const isRunning = isRunningState(node.item.state.runState); - if (node.item.state.runState !== node.previousState) { - if (isRunning && node.treeElements.has(node.previousState)) { - node.treeElements.get(node.previousState)!.computedState = TestRunState.Running; - } else { - this.pruneStateElements(node, node.previousState); - this.resolveNodesRecursive(node); - } - } else if (!isRunning) { - const previous = node.treeElements.get(node.item.state.runState); - if (previous) { - previous.computedState = node.item.state.runState; - } - } - - const locationChanged = !locationsEqual(node.location, node.item.location); - if (locationChanged) { - this.locations.remove(node); - node.location = node.item.location; - this.locations.add(node); - } - - const treeNode = node.treeElements.get(node.previousState)!; - this.changes.updated(treeNode); - }, - complete: () => { - this.updateEmitter.fire(); - } - }; - } - - /** - * Ensures tree nodes for the item state are present in the tree. - */ - protected resolveNodesRecursive(item: IStatusTestItem) { - const state = item.item.state.runState; - item.previousState = item.item.state.runState; - - // Create a list of items until the current item who don't have a tree node for the status yet - let chain: IStatusTestItem[] = []; - for (let i: IStatusTestItem | undefined = item; i && !i.treeElements.has(state); i = i.parentItem) { - chain.push(i); - } - - for (let i = chain.length - 1; i >= 0; i--) { - const item2 = chain[i]; - // the loop would have stopped pushing parents when either it reaches - // the root, or it reaches a parent who already has a node for this state. - const parent = item2.parentItem?.treeElements.get(state) ?? this.getOrCreateStateElement(state); - const node = this.createElement(state, item2, parent); - - item2.treeElements.set(state, node); - parent.children.add(node); - - if (i === chain.length - 1) { - this.changes.added(node); - } - } - } - - protected createElement(state: TestRunState, item: IStatusTestItem, parent: TreeElement) { - return new TestStateElement(state, item, parent); - } - - - /** - * Recursively (from the leaf to the root) removes tree elements if there's - * no children who have the given state left. - * - * Returns true if it resulted in a node being removed. - */ - protected pruneStateElements(item: IStatusTestItem | undefined, state: TestRunState, force = false) { - if (!item) { - const stateRoot = this.stateRoots.get(state); - if (stateRoot?.children.size === 0) { - this.changes.removed(stateRoot); - this.stateRoots.delete(state); - return true; - } - - return false; - } - - const node = item.treeElements.get(state); - if (!node) { - return false; - } - - // Check to make sure we aren't in the state, and there's no child with the - // state. For the unset state, only show the node if it's a leaf or it - // has children in the unset state. - if (!force) { - if (item.item.state.runState === state && !(state === TestRunState.Unset && item.children.size > 0)) { - return false; - } - - for (const childId of item.children) { - if (this.items.get(childId)?.treeElements.has(state)) { - return false; - } - } - } - - // If so, proceed to deletion and recurse upwards. - item.treeElements.delete(state); - node.remove(); - - if (!this.pruneStateElements(item.parentItem, state)) { - this.changes.removed(node); - } - - return true; - } - - protected getOrCreateStateElement(state: TestRunState) { - let s = this.stateRoots.get(state); - if (!s) { - s = new StateElement(state); - this.changes.added(s); - this.stateRoots.set(state, s); - } - - return s; - } - - protected createItem(item: InternalTestItem, parentItem?: IStatusTestItem): IStatusTestItem { - return { - ...item, - depth: parentItem ? parentItem.depth + 1 : 1, - parentItem: parentItem, - previousState: item.item.state.runState, - location: item.item.location, - children: new Set(), - treeElements: new Map(), - }; - } -} diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts deleted file mode 100644 index cd7ad2a077b..00000000000 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts +++ /dev/null @@ -1,289 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { Emitter } from 'vs/base/common/event'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; -import { Location as ModeLocation } from 'vs/editor/common/modes'; -import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; -import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; -import { ListElementType } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore'; -import { NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; -import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; -import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { isRunningState } from 'vs/workbench/contrib/testing/common/testingStates'; -import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; - -class ListTestStateElement implements ITestTreeElement { - public computedState = this.test.item.state.runState; - - public get treeId() { - return `sntest:${this.test.id}`; - } - - public get label() { - return this.test.item.label; - } - - public get location() { - return this.test.item.location; - } - - public get runnable(): Iterable { - return this.test.item.runnable - ? [{ testId: this.test.id, providerId: this.test.providerId }] - : Iterable.empty(); - } - - public get debuggable(): Iterable { - return this.test.item.debuggable - ? [{ testId: this.test.id, providerId: this.test.providerId }] - : Iterable.empty(); - } - - public get description() { - let description: string | undefined; - for (let parent = this.test.parentItem; parent && parent.depth > 0; parent = parent.parentItem) { - description = description ? `${parent.item.label} › ${description}` : parent.item.label; - } - - return description; - } - - public readonly depth = 1; - public readonly children = new Set(); - - constructor( - public readonly test: IStatusListTestItem, - public readonly parentItem: StateElement, - ) { - parentItem.children.add(this); - } - - public remove() { - this.parentItem.children.delete(this); - } -} - -interface IStatusListTestItem extends IncrementalTestCollectionItem { - node?: ListTestStateElement; - type: ListElementType; - previousState: TestRunState; - depth: number; - parentItem?: IStatusListTestItem; - location?: ModeLocation; -} - -type TreeElement = StateElement | ListTestStateElement; - -/** - * Projection that shows tests in a flat list (grouped by status). - */ -export class StateByNameProjection extends AbstractIncrementalTestCollection implements ITestTreeProjection { - private readonly updateEmitter = new Emitter(); - private readonly changes = new NodeChangeList(); - private readonly locations = new TestLocationStore(); - private readonly disposable = new DisposableStore(); - - /** - * @inheritdoc - */ - public readonly onUpdate = this.updateEmitter.event; - - /** - * Root elements for states in the tree. - */ - protected readonly stateRoots = new Map>(); - - constructor(listener: TestSubscriptionListener) { - super(); - - this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff))); - - const firstDiff: TestsDiff = []; - for (const [, collection] of listener.workspaceFolderCollections) { - firstDiff.push(...collection.getReviverDiff()); - } - - this.apply(firstDiff); - } - - /** - * Frees listeners associated with the projection. - */ - public dispose() { - this.disposable.dispose(); - } - - /** - * @inheritdoc - */ - public getTestAtPosition(uri: URI, position: Position) { - return this.locations.getTestAtPosition(uri, position)?.node; - } - - /** - * @inheritdoc - */ - public applyTo(tree: ObjectTree) { - this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values()); - } - - private readonly renderNode: NodeRenderFn = (node, recurse) => { - return { - element: node, - children: node instanceof StateElement ? recurse(node.children) : undefined, - }; - }; - - /** - * @override - */ - protected createChangeCollector(): IncrementalChangeCollector { - return { - add: node => { - this.resolveNodesRecursive(node); - this.locations.add(node); - }, - remove: (node, isNested) => { - if (node.node) { - this.locations.remove(node); - } - - // for the top node being deleted, we need to update parents. For - // others we only need to remove them from the locations cache. - if (isNested) { - this.removeNodeSingle(node); - } else { - this.removeNode(node); - } - }, - update: node => { - if (node.item.state.runState !== node.previousState && node.node) { - if (isRunningState(node.item.state.runState)) { - node.node.computedState = node.item.state.runState; - } else { - this.removeNode(node); - } - } - - node.previousState = node.item.state.runState; - this.resolveNodesRecursive(node); - - const locationChanged = !locationsEqual(node.location, node.item.location); - if (locationChanged) { - this.locations.remove(node); - node.location = node.item.location; - this.locations.add(node); - } - - if (node.node) { - this.changes.updated(node.node); - } - }, - complete: () => { - this.updateEmitter.fire(); - } - }; - } - - /** - * Ensures tree nodes for the item state are present in the tree. - */ - protected resolveNodesRecursive(item: IStatusListTestItem) { - const newType = Iterable.some(item.children, c => this.items.get(c)?.type !== ListElementType.BranchWithoutLeaf) - ? ListElementType.BranchWithLeaf - : item.item.runnable - ? ListElementType.TestLeaf - : ListElementType.BranchWithoutLeaf; - - if (newType === item.type) { - return; - } - - const isVisible = newType === ListElementType.TestLeaf; - const wasVisible = item.type === ListElementType.TestLeaf; - item.type = newType; - - if (!isVisible && wasVisible && item.node) { - this.removeNodeSingle(item); - } else if (isVisible && !wasVisible) { - const state = item.item.state.runState; - item.node = item.node || new ListTestStateElement(item, this.getOrCreateStateElement(state)); - this.changes.added(item.node); - } - - if (item.parentItem) { - this.resolveNodesRecursive(item.parentItem); - } - } - - /** - * Recursively (from the leaf to the root) removes tree elements if there's - * no children who have the given state left. - * - * Returns true if it resulted in a node being removed. - */ - private removeNode(item: IStatusListTestItem) { - if (!item.node) { - return; - } - - this.removeNodeSingle(item); - - if (item.parentItem) { - this.resolveNodesRecursive(item.parentItem); - } - } - - private removeNodeSingle(item: IStatusListTestItem) { - if (!item.node) { - return; - } - - item.node.remove(); - this.changes.removed(item.node); - - const parent = item.node.parentItem; - item.node = undefined; - item.type = ListElementType.Unset; - - if (parent.children.size === 0) { - this.changes.removed(parent); - this.stateRoots.delete(parent.state); - } - } - - private getOrCreateStateElement(state: TestRunState) { - let s = this.stateRoots.get(state); - if (!s) { - s = new StateElement(state); - this.changes.added(s); - this.stateRoots.set(state, s); - } - - return s; - } - - /** - * @override - */ - protected createItem(item: InternalTestItem, parentItem?: IStatusListTestItem): IStatusListTestItem { - return { - ...item, - type: ListElementType.Unset, - depth: parentItem ? parentItem.depth + 1 : 0, - parentItem: parentItem, - previousState: item.item.state.runState, - location: item.item.location, - children: new Set(), - }; - } -} diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts deleted file mode 100644 index 5b3fed98aca..00000000000 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Iterable } from 'vs/base/common/iterator'; -import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; -import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; -import { testStateNames } from 'vs/workbench/contrib/testing/common/constants'; - -/** - * Base state node element, used in both name and location grouping. - */ -export class StateElement implements ITestTreeElement { - public computedState = this.state; - - public get treeId() { - return `sestate:${this.state}`; - } - - public readonly depth = 0; - public readonly label = testStateNames[this.state]; - public readonly parentItem = null; - public readonly children = new Set(); - - public get runnable() { - return Iterable.concatNested(Iterable.map(this.children, c => c.runnable)); - } - - public get debuggable() { - return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable)); - } - - constructor(public readonly state: TestRunState) { } -} diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index db03e28f7c9..6125b257fa7 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -21,9 +21,10 @@ import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { FocusedViewContext } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestExplorerViewSorting, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestResult, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; @@ -101,10 +102,10 @@ abstract class RunOrDebugAction extends ViewAction { /** * @override */ - public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise { + public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise { const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel); if (!tests.length) { - return Promise.resolve(EMPTY_TEST_RESULT); + return Promise.resolve(undefined); } return accessor.get(ITestService).runTests({ tests, debug: this.debug }); @@ -327,18 +328,18 @@ export class TestingViewAsTreeAction extends ViewAction { } -export class TestingGroupByLocationAction extends ViewAction { +export class TestingSortByNameAction extends ViewAction { constructor() { super({ - id: 'testing.groupByLocation', + id: 'testing.sortByName', viewId: Testing.ExplorerViewId, - title: localize('testing.groupByLocation', "Sort by Name"), + title: localize('testing.sortByName', "Sort by Name"), f1: false, - toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByLocation), + toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByName), menu: { id: MenuId.ViewTitle, order: 10, - group: 'groupBy', + group: 'sortBy', when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) } }); @@ -348,22 +349,22 @@ export class TestingGroupByLocationAction extends ViewAction { +export class TestingSortByLocationAction extends ViewAction { constructor() { super({ - id: 'testing.groupByStatus', + id: 'testing.sortByLocation', viewId: Testing.ExplorerViewId, - title: localize('testing.groupByStatus', "Sort by Status"), + title: localize('testing.sortByLocation', "Sort by Location"), f1: false, - toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByStatus), + toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByLocation), menu: { id: MenuId.ViewTitle, order: 10, - group: 'groupBy', + group: 'sortBy', when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) } }); @@ -373,7 +374,7 @@ export class TestingGroupByStatusAction extends ViewAction * @override */ public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) { - view.viewModel.viewGrouping = TestExplorerViewGrouping.ByStatus; + view.viewModel.viewSorting = TestExplorerViewSorting.ByLocation; } } @@ -427,6 +428,24 @@ export class RefreshTestsAction extends Action2 { } } +export class ClearTestResultsAction extends Action2 { + constructor() { + super({ + id: 'testing.clearTestResults', + title: localize('testing.clearResults', "Clear All Results"), + category, + f1: true + }); + } + + /** + * @override + */ + public run(accessor: ServicesAccessor) { + accessor.get(ITestResultService).clear(); + } +} + export class EditFocusedTest extends ViewAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 8d8bf3dafad..ce6a99f15c0 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -83,13 +83,14 @@ registerAction2(Action.TestingViewAsTreeAction); registerAction2(Action.CancelTestRunAction); registerAction2(Action.RunSelectedAction); registerAction2(Action.DebugSelectedAction); -registerAction2(Action.TestingGroupByLocationAction); -registerAction2(Action.TestingGroupByStatusAction); +registerAction2(Action.TestingSortByNameAction); +registerAction2(Action.TestingSortByLocationAction); registerAction2(Action.RefreshTestsAction); registerAction2(Action.CollapseAllAction); registerAction2(Action.RunAllAction); registerAction2(Action.DebugAllAction); registerAction2(Action.EditFocusedTest); +registerAction2(Action.ClearTestResultsAction); registerAction2(CloseTestPeek); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index e0d3fba41ee..01d809934ca 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -28,8 +28,8 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; -import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService'; export class TestingDecorations extends Disposable implements IEditorContribution { @@ -39,6 +39,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio constructor( private readonly editor: ICodeEditor, @ITestService private readonly testService: ITestService, + @ITestResultService private readonly results: ITestResultService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -62,6 +63,15 @@ export class TestingDecorations extends Disposable implements IEditorContributio } this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri)); + this._register(this.results.onTestChanged(([, changed]) => { + if (changed.item.location?.uri.toString() === uri.toString()) { + this.setDecorations(uri); + } + })); + this._register(this.results.onResultsChanged(() => { + this.setDecorations(uri); + })); + this.setDecorations(uri); } @@ -74,19 +84,25 @@ export class TestingDecorations extends Disposable implements IEditorContributio this.editor.changeDecorations(accessor => { const newDecorations: ITestDecoration[] = []; for (const test of ref.object.all) { + const stateLookup = this.results.getStateByExtId(test.item.extId); if (hasValidLocation(uri, test.item)) { newDecorations.push(this.instantiationService.createInstance( - RunTestDecoration, test, ref.object, test.item.location, this.editor)); + RunTestDecoration, test, ref.object, test.item.location, this.editor, stateLookup?.[1].computedState)); } - for (let i = 0; i < test.item.state.messages.length; i++) { - const m = test.item.state.messages[i]; + if (!stateLookup) { + continue; + } + + const [result, stateItem] = stateLookup; + for (let i = 0; i < stateItem.state.messages.length; i++) { + const m = stateItem.state.messages[i]; if (hasValidLocation(uri, m)) { const uri = buildTestUri({ - type: TestUriType.LiveMessage, + type: TestUriType.ResultActualOutput, messageIndex: i, - providerId: test.providerId, - testId: test.id, + resultId: result.id, + testId: stateItem.item.extId, }); newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); @@ -138,7 +154,7 @@ const firstLineRange = (originalRange: IRange) => ({ endColumn: 1, }); -class RunTestDecoration implements ITestDecoration { +class RunTestDecoration extends Disposable implements ITestDecoration { /** * @inheritdoc */ @@ -156,25 +172,16 @@ class RunTestDecoration implements ITestDecoration { private readonly collection: IMainThreadTestCollection, private readonly location: ModeLocation, private readonly editor: ICodeEditor, + computedState: TestRunState | undefined, @ITestService private readonly testService: ITestService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ICommandService private readonly commandService: ICommandService, ) { + super(); this.line = location.range.startLineNumber; - const queue = [test.children]; - let state = this.test.item.state.runState; - while (queue.length) { - for (const child of queue.pop()!) { - const node = collection.getNodeById(child); - if (node) { - state = maxPriority(node.item.state.runState, state); - } - } - } - - const icon = state !== TestRunState.Unset - ? testingStatesToIcons.get(state)! + const icon = computedState !== undefined && computedState !== TestRunState.Unset + ? testingStatesToIcons.get(computedState)! : test.children.size > 0 ? testingRunAllIcon : testingRunIcon; this.editorDecoration = { diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index f433739f569..81c3e26bf0b 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -5,12 +5,12 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import * as aria from 'vs/base/browser/ui/aria/aria'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; -import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction, IActionViewItem } from 'vs/base/common/actions'; import { DeferredPromise } from 'vs/base/common/async'; import { Color, RGBA } from 'vs/base/common/color'; @@ -47,14 +47,10 @@ import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/comm import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { getComputedState } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; -import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation'; -import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName'; -import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; -import { TestExplorerViewGrouping, TestExplorerViewMode, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; @@ -188,7 +184,7 @@ export class TestingExplorerViewModel extends Disposable { public projection!: ITestTreeProjection; private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService); - private readonly _viewGrouping = TestingContextKeys.viewGrouping.bindTo(this.contextKeyService); + private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService); /** * Fires when the selected tests change. @@ -210,18 +206,18 @@ export class TestingExplorerViewModel extends Disposable { } - public get viewGrouping() { - return this._viewGrouping.get() ?? TestExplorerViewGrouping.ByLocation; + public get viewSorting() { + return this._viewSorting.get() ?? TestExplorerViewSorting.ByLocation; } - public set viewGrouping(newGrouping: TestExplorerViewGrouping) { - if (newGrouping === this._viewGrouping.get()) { + public set viewSorting(newSorting: TestExplorerViewSorting) { + if (newSorting === this._viewSorting.get()) { return; } - this._viewGrouping.set(newGrouping); - this.updatePreferredProjection(); - this.storageService.store('testing.viewGrouping', newGrouping, StorageScope.WORKSPACE, StorageTarget.USER); + this._viewSorting.set(newSorting); + this.tree.resort(null); + this.storageService.store('testing.viewSorting', newSorting, StorageScope.WORKSPACE, StorageTarget.USER); } constructor( @@ -229,16 +225,17 @@ export class TestingExplorerViewModel extends Disposable { onDidChangeVisibility: Event, private listener: TestSubscriptionListener | undefined, @ITestExplorerFilterState filterState: TestExplorerFilterState, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @ICodeEditorService codeEditorService: ICodeEditorService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ITestResultService private readonly testResults: ITestResultService, ) { super(); this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode); - this._viewGrouping.set(this.storageService.get('testing.viewGrouping', StorageScope.WORKSPACE, TestExplorerViewGrouping.ByLocation) as TestExplorerViewGrouping); + this._viewSorting.set(this.storageService.get('testing.viewSorting', StorageScope.WORKSPACE, TestExplorerViewSorting.ByLocation) as TestExplorerViewSorting); const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility })); @@ -261,7 +258,7 @@ export class TestingExplorerViewModel extends Disposable { simpleKeyboardNavigation: true, identityProvider: instantiationService.createInstance(IdentityProvider), hideTwistiesOfChildlessElements: true, - sorter: instantiationService.createInstance(TreeSorter), + sorter: instantiationService.createInstance(TreeSorter, this), keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider), accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider), filter: this.filter, @@ -293,6 +290,10 @@ export class TestingExplorerViewModel extends Disposable { tracker.deactivate(); } })); + + this._register(testResults.onResultsChanged(() => { + this.tree.resort(null); + })); } /** @@ -380,16 +381,18 @@ export class TestingExplorerViewModel extends Disposable { * Tries to peek the first test error, if the item is in a failed state. */ private async tryPeekError(item: ITestTreeElement) { - if (!item.test || !isFailedState(item.test.item.state.runState)) { + const lookup = item.test && this.testResults.getStateByExtId(item.test.item.extId); + if (!lookup || !isFailedState(lookup[1].state.state)) { return false; } - const index = item.test.item.state.messages.findIndex(m => !!m.location); + const [result, test] = lookup; + const index = test.state.messages.findIndex(m => !!m.location); if (index === -1) { return; } - const message = item.test.item.state.messages[index]; + const message = test.state.messages[index]; const pane = await this.editorService.openEditor({ resource: message.location!.uri, options: { selection: message.location!.range, preserveFocus: true } @@ -401,10 +404,10 @@ export class TestingExplorerViewModel extends Disposable { } TestingOutputPeekController.get(control).show(buildTestUri({ - type: TestUriType.LiveMessage, + type: TestUriType.ResultMessage, messageIndex: index, - providerId: item.test.providerId, - testId: item.test.id, + resultId: result.id, + testId: item.test!.item.extId, })); return true; @@ -417,18 +420,10 @@ export class TestingExplorerViewModel extends Disposable { return; } - if (this._viewGrouping.get() === TestExplorerViewGrouping.ByLocation) { - if (this._viewMode.get() === TestExplorerViewMode.List) { - this.projection = new HierarchicalByNameProjection(this.listener); - } else { - this.projection = new HierarchicalByLocationProjection(this.listener); - } + if (this._viewMode.get() === TestExplorerViewMode.List) { + this.projection = this.instantiationService.createInstance(HierarchicalByNameProjection, this.listener); } else { - if (this._viewMode.get() === TestExplorerViewMode.List) { - this.projection = new StateByNameProjection(this.listener); - } else { - this.projection = new StateByLocationProjection(this.listener); - } + this.projection = this.instantiationService.createInstance(HierarchicalByLocationProjection, this.listener); } this.projection.onUpdate(this.deferUpdate, this); @@ -569,9 +564,19 @@ class TestsFilter implements ITreeFilter { } class TreeSorter implements ITreeSorter { + constructor(private readonly viewModel: TestingExplorerViewModel) { } + public compare(a: ITestTreeElement, b: ITestTreeElement): number { - if (a instanceof StateElement && b instanceof StateElement) { - return cmpPriority(a.computedState, b.computedState); + let delta = cmpPriority(a.state, b.state); + if (delta !== 0) { + return delta; + } + + if (this.viewModel.viewSorting === TestExplorerViewSorting.ByLocation && a.location && b.location && a.location.uri.toString() === b.location.uri.toString()) { + delta = a.location.range.startLineNumber - b.location.range.startLineNumber; + if (delta !== 0) { + return delta; + } } return a.label.localeCompare(b.label); @@ -587,7 +592,7 @@ class ListAccessibilityProvider implements IListAccessibilityProvider class TestRunProgress { private current?: { update: IProgress; deferred: DeferredPromise }; private badge = new MutableDisposable(); - private readonly resultLister = this.resultService.onNewTestResult(result => { + private readonly resultLister = this.resultService.onResultsChanged(result => { + if (!('started' in result)) { + return; + } + this.updateProgress(); this.updateBadge(); - result.onChange(this.throttledProgressUpdate, this); - result.onComplete(() => { + result.started.onChange(this.throttledProgressUpdate, this); + result.started.onComplete(() => { this.throttledProgressUpdate(); this.updateBadge(); }); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 9c2dc7a003c..081c8893ad9 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -27,15 +27,15 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic import { EditorModel } from 'vs/workbench/common/editor'; import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, ITestMessage, ITestState } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; interface ITestDto { + test: ITestItem, messageIndex: number; - test: InternalTestItem; + state: ITestState; expectedUri: URI; actualUri: URI; messageUri: URI; @@ -66,7 +66,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo private readonly editor: ICodeEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITestResultService private readonly testResults: ITestResultService, - @ITestService private readonly testService: ITestService, @IContextKeyService contextKeyService: IContextKeyService, ) { super(); @@ -83,7 +82,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return; } - const message = dto.test.item.state.messages[dto.messageIndex]; + const message = dto.state.messages[dto.messageIndex]; if (!message?.location) { return; } @@ -120,28 +119,14 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return undefined; } - if ('resultId' in parts) { - const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId); - return test && { - test, - messageIndex: parts.messageIndex, - expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }), - actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }), - messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }), - }; - } - - const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId }); - if (!test) { - return; - } - - return { - test, + const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testId); + return test && { + test: test.item, + state: test.state, messageIndex: parts.messageIndex, - expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }), - actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }), - messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }), + expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }), + actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }), + messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }), }; } } @@ -236,14 +221,14 @@ class TestingDiffOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) { - const message = test.item.state.messages[messageIndex]; + public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) { + const message = state.messages[messageIndex]; if (!message?.location) { return; } this.show(message.location.range, hintDiffPeekHeight(message)); - this.setTitle(message.message.toString(), test.item.label); + this.setTitle(message.message.toString(), test.label); const [original, modified] = await Promise.all([ this.modelService.createModelReference(expectedUri), @@ -285,14 +270,14 @@ class TestingMessageOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ test, messageIndex, messageUri }: ITestDto) { - const message = test.item.state.messages[messageIndex]; + public async setModel({ state, test, messageIndex, messageUri }: ITestDto) { + const message = state.messages[messageIndex]; if (!message?.location) { return; } this.show(message.location.range, hintPeekStrHeight(message.message.toString())); - this.setTitle(message.message.toString(), test.item.label); + this.setTitle(message.message.toString(), test.label); const modelRef = this.model.value = await this.modelService.createModelReference(messageUri); if (this.preview.value) { diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index d828393d01c..817cef54b7e 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -20,9 +20,9 @@ export const enum TestExplorerViewMode { Tree = 'true' } -export const enum TestExplorerViewGrouping { +export const enum TestExplorerViewSorting { ByLocation = 'location', - ByStatus = 'status', + ByName = 'name', } export const testStateNames: { [K in TestRunState]: string } = { diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts new file mode 100644 index 00000000000..e8bb0d16a2c --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; +import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; + +/** + * Accessor for nodes in get and refresh computed state. + */ +export interface IComputedStateAccessor { + getOwnState(item: T): TestRunState | undefined; + getCurrentComputedState(item: T): TestRunState; + setComputedState(item: T, state: TestRunState): void; + getChildren(item: T): IterableIterator; + getParents(item: T): IterableIterator; +} + +/** + * Gets the computed state for the node. + * @param force whether to refresh the computed state for this node, even + * if it was previously set. + */ + +export const getComputedState = (accessor: IComputedStateAccessor, node: T, force = false) => { + let computed = accessor.getCurrentComputedState(node); + if (computed === undefined || force) { + computed = accessor.getOwnState(node) ?? TestRunState.Unset; + for (const child of accessor.getChildren(node)) { + computed = maxPriority(computed, getComputedState(accessor, child)); + } + + accessor.setComputedState(node, computed); + } + + return computed; +}; +/** + * Refreshes the computed state for the node and its parents. Any changes + * elements cause `addUpdated` to be called. + */ + +export const refreshComputedState = ( + accessor: IComputedStateAccessor, + node: T, + addUpdated: (node: T) => void, + explicitNewComputedState?: TestRunState, +) => { + const oldState = accessor.getCurrentComputedState(node); + const oldPriority = statePriority[oldState]; + const newState = explicitNewComputedState ?? getComputedState(accessor, node, true); + const newPriority = statePriority[newState]; + if (newPriority === oldPriority) { + return; + } + + accessor.setComputedState(node, newState); + addUpdated(node); + + if (newPriority > oldPriority) { + // Update all parents to ensure they're at least this priority. + for (const parent of accessor.getParents(node)) { + const prev = accessor.getCurrentComputedState(parent); + if (prev !== undefined && statePriority[prev] >= newPriority) { + break; + } + + accessor.setComputedState(parent, newState); + addUpdated(parent); + } + } else if (newPriority < oldPriority) { + // Re-render all parents of this node whose computed priority might have come from this node + for (const parent of accessor.getParents(node)) { + const prev = accessor.getCurrentComputedState(parent); + if (prev === undefined || statePriority[prev] > oldPriority) { + break; + } + + accessor.setComputedState(parent, getComputedState(accessor, parent, true)); + addUpdated(parent); + } + } +}; diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 41f4c891fc0..c997e237d7a 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -204,7 +204,6 @@ const keyMap: { [K in keyof Omit]: null } = { id: null, label: null, location: null, - state: null, debuggable: null, description: null, runnable: null diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 566146dc027..8ceb490a2dc 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -26,24 +26,12 @@ export interface RunTestsRequest { * Request from the main thread to run tests for a single provider. */ export interface RunTestForProviderRequest { + runId: string; providerId: string; ids: string[]; debug: boolean; } -/** - * Response to a {@link RunTestsRequest} - */ -export interface RunTestsResult { - // todo -} - -export const EMPTY_TEST_RESULT: RunTestsResult = {}; - -export const collectTestResults = (results: ReadonlyArray) => { - return results[0] || {}; // todo -}; - export interface ITestMessage { message: string | IMarkdownString; severity: TestMessageSeverity | undefined; @@ -53,7 +41,7 @@ export interface ITestMessage { } export interface ITestState { - runState: TestRunState; + state: TestRunState; duration: number | undefined; messages: ITestMessage[]; } @@ -70,7 +58,6 @@ export interface ITestItem { description: string | undefined; runnable: boolean; debuggable: boolean; - state: ITestState; } /** @@ -84,7 +71,7 @@ export interface InternalTestItem { } export interface InternalTestItemWithChildren extends InternalTestItem { - children: InternalTestItemWithChildren[]; + children: this[]; } export interface InternalTestResults { diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 992081d3a77..702630e8390 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -4,17 +4,52 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; 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 { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; -import { IncrementalTestCollectionItem, InternalTestItemWithChildren, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; +import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; +import { IncrementalTestCollectionItem, ITestState, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; +import { statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; +/** + * Count of the number of tests in each run state. + */ export type TestStateCount = { [K in TestRunState]: number }; +export interface ITestResult { + /** + * Count of the number of tests in each run state. + */ + readonly counts: Readonly; + + /** + * Unique ID of this set of test results. + */ + readonly id: string; + + /** + * Gets whether the test run has finished. + */ + readonly isComplete: boolean; + + /** + * Gets the state of the test by its extension-assigned ID. + */ + getStateByExtId(testExtId: string): TestResultItem | undefined; + + /** + * Serializes the test result. Used to save and restore results + * in the workspace. + */ + toJSON(): ISerializedResults; +} + const makeEmptyCounts = () => { const o: Partial = {}; for (const state of statesInOrder) { @@ -24,7 +59,7 @@ const makeEmptyCounts = () => { return o as TestStateCount; }; -export const sumCounts = (counts: TestStateCount[]) => { +export const sumCounts = (counts: Iterable) => { const total = makeEmptyCounts(); for (const count of counts) { for (const state of statesInOrder) { @@ -35,28 +70,97 @@ export const sumCounts = (counts: TestStateCount[]) => { return total; }; -const makeNode = ( +const queuedState: ITestState = { + duration: undefined, + messages: [], + state: TestRunState.Queued +}; + +const unsetState: ITestState = { + duration: undefined, + messages: [], + state: TestRunState.Unset +}; + +const itemToNode = ( + item: IncrementalTestCollectionItem, + byExtId: Map, + byInternalId: 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 }, + state: unsetState, + computedState: TestRunState.Unset, + }; + + byExtId.set(n.item.extId, n); + byInternalId.set(n.id, n); + + return n; +}; + +const makeParents = ( + collection: IMainThreadTestCollection, + child: IncrementalTestCollectionItem, + byExtId: Map, + byInternalId: Map, +) => { + const parent = child.parent && collection.getNodeById(child.parent); + if (!parent) { + return; + } + + let parentResultItem = byInternalId.get(parent.id); + if (parentResultItem) { + parentResultItem.children.add(child.id); + return; // no need to recurse, all parents already in result + } + + parentResultItem = itemToNode(parent, byExtId, byInternalId); + parentResultItem.children = new Set([child.id]); + makeParents(collection, parent, byExtId, byInternalId); +}; + +const makeNodeAndChildren = ( collection: IMainThreadTestCollection, test: IncrementalTestCollectionItem, + byExtId: Map, + byInternalId: Map, ): TestResultItem => { - const mapped: TestResultItem = { ...test, children: [] }; + const existing = byInternalId.get(test.id); + if (existing) { + return existing; + } + + const mapped = itemToNode(test, byExtId, byInternalId); for (const childId of test.children) { const child = collection.getNodeById(childId); if (child) { - mapped.children.push(makeNode(collection, child)); + makeNodeAndChildren(collection, child, byExtId, byInternalId); } } return mapped; }; -export interface TestResultItem extends InternalTestItemWithChildren { } +interface ISerializedResults { + id: string; + counts: TestStateCount; + items: Iterable<[extId: string, item: TestResultItem]>; +} + +interface TestResultItem extends IncrementalTestCollectionItem { + state: ITestState; + computedState: TestRunState; +} /** * Results of a test. These are created when the test initially started running * and marked as "complete" when the run finishes. */ -export class TestResult { +export class LiveTestResult implements ITestResult { /** * Creates a new TestResult, pulling tests from the associated list * of collections. @@ -65,27 +169,29 @@ export class TestResult { collections: ReadonlyArray, tests: ReadonlyArray, ) { - const mapped: TestResultItem[] = []; + const testByExtId = new Map(); + const testByInternalId = new Map(); for (const test of tests) { for (const collection of collections) { const node = collection.getNodeById(test.testId); - if (node) { - mapped.push(makeNode(collection, node)); - break; + if (!node) { + continue; } + + makeNodeAndChildren(collection, node, testByExtId, testByInternalId); + makeParents(collection, node, testByExtId, testByInternalId); } } - return new TestResult(mapped); + return new LiveTestResult(collections, testByExtId, testByInternalId); } - private completeEmitter = new Emitter(); - private changeEmitter = new Emitter(); + private readonly completeEmitter = new Emitter(); + private readonly changeEmitter = new Emitter(); private _complete = false; - private _cachedCounts?: { [K in TestRunState]: number }; - public onChange = this.changeEmitter.event; - public onComplete = this.completeEmitter.event; + public readonly onChange = this.changeEmitter.event; + public readonly onComplete = this.completeEmitter.event; /** * Unique ID for referring to this set of test results. @@ -93,34 +199,122 @@ export class TestResult { public readonly id = generateUuid(); /** - * Gets whether the test run has finished. + * @inheritdoc */ public get isComplete() { return this._complete; } /** - * Gets a count of tests in each state. + * @inheritdoc */ - public get counts() { - if (this._cachedCounts) { - return this._cachedCounts; - } + public readonly counts: { [K in TestRunState]: number } = makeEmptyCounts(); - const counts = makeEmptyCounts(); - this.forEachTest(({ item }) => { - counts[item.state.runState]++; - }); - - if (this._complete) { - this._cachedCounts = counts; - } - - return counts; + /** + * Gets all tests involved in the run by ID. + */ + public get tests() { + return this.testByInternalId.values(); } - constructor(public readonly tests: TestResultItem[]) { } + private readonly computedStateAccessor: IComputedStateAccessor = { + getOwnState: i => i.state.state, + getCurrentComputedState: i => i.computedState, + setComputedState: (i, s) => i.computedState = s, + getChildren: i => { + const { testByInternalId } = this; + return (function* () { + for (const childId of i.children) { + const child = testByInternalId.get(childId); + if (child) { + yield child; + } + } + })(); + }, + getParents: i => { + const { testByInternalId } = this; + return (function* () { + for (let parentId = i.parent; parentId;) { + const parent = testByInternalId.get(parentId); + if (!parent) { + break; + } + yield parent; + parentId = parent.parent; + } + })(); + }, + }; + + constructor( + private readonly collections: ReadonlyArray, + private readonly testByExtId: Map, + private readonly testByInternalId: Map, + ) { + this.counts[TestRunState.Unset] = testByInternalId.size; + } + + /** + * @inheritdoc + */ + public getStateByExtId(extTestId: string) { + return this.testByExtId.get(extTestId); + } + + /** + * Updates all tests in the collection to the given state. + */ + public setAllToState(state: ITestState, when: (_t: TestResultItem) => boolean) { + for (const test of this.testByInternalId.values()) { + if (when(test)) { + this.counts[state.state]--; + test.state = state; + this.counts[state.state]++; + refreshComputedState(this.computedStateAccessor, test, t => this.changeEmitter.fire(t)); + } + } + } + + /** + * Updates the state of the test by its internal ID. + */ + public updateState(testId: string, state: ITestState) { + let entry = this.testByInternalId.get(testId); + if (!entry) { + entry = this.addTestToRun(testId); + } + if (!entry) { + return; + } + + if (state.state === entry.state.state) { + entry.state = state; + this.changeEmitter.fire(entry); + } else { + this.counts[entry.state.state]--; + entry.state = state; + this.counts[entry.state.state]++; + refreshComputedState(this.computedStateAccessor, entry, t => this.changeEmitter.fire(t)); + } + } + + /** + * 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. + */ + private addTestToRun(testId: string) { + for (const collection of this.collections) { + let test = collection.getNodeById(testId); + if (test) { + return makeNodeAndChildren(collection, test, this.testByExtId, this.testByInternalId); + } + } + + return undefined; + } /** * Notifies the service that all tests are complete. @@ -130,109 +324,209 @@ export class TestResult { throw new Error('cannot complete a test result multiple times'); } - // shallow clone test items to 'disconnect' them from the underlying - // connection and stop state changes. Also, marked any still-running - // tests as skipped. - this.forEachTest(test => { - test.item = { ...test.item }; - if (isRunningState(test.item.state.runState)) { - test.item.state = { ...test.item.state, runState: TestRunState.Skipped }; - } - }); - + // un-queue any tests that weren't explicitly updated + this.setAllToState(unsetState, t => t.state.state === TestRunState.Queued); this._complete = true; this.completeEmitter.fire(); } /** - * Fires the 'change' event, should be called by the runner. + * @inheritdoc */ - public notifyChanged() { - this.changeEmitter.fire(); - } - - private forEachTest(fn: (test: TestResultItem) => void) { - const queue = [this.tests]; - while (queue.length) { - for (const test of queue.pop()!) { - fn(test); - queue.push(test.children); - } - } + public toJSON(): ISerializedResults { + return { id: this.id, counts: this.counts, items: [...this.testByExtId.entries()] }; } } +/** + * Test results hydrated from a previously-serialized test run. + */ +class HydratedTestResult implements ITestResult { + /** + * @inheritdoc + */ + public readonly counts = this.serialized.counts; + + /** + * @inheritdoc + */ + public readonly id = this.serialized.id; + + /** + * @inheritdoc + */ + public readonly isComplete = true; + + private readonly map = new Map(); + + constructor(private readonly serialized: ISerializedResults) { + for (const [key, value] of serialized.items) { + this.map.set(key, value); + + for (const message of value.state.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + } + } + } + } + + /** + * @inheritdoc + */ + public getStateByExtId(extTestId: string) { + return this.map.get(extTestId); + } + + /** + * @inheritdoc + */ + public toJSON(): ISerializedResults { + return this.serialized; + } +} + +export type ResultChangeEvent = + | { completed: LiveTestResult } + | { started: LiveTestResult } + | { removed: ITestResult[] }; + export interface ITestResultService { readonly _serviceBrand: undefined; + /** + * Fired after any results are added, removed, or completed. + */ + readonly onResultsChanged: Event; /** - * List of test results. Currently running tests are always at the top. + * Fired when a test changed it state, or its computed state is updated. */ - readonly results: TestResult[]; + readonly onTestChanged: Event<[results: ITestResult, item: TestResultItem]>; /** - * Fired after a new event is added to the 'active' array. + * List of known test results. */ - readonly onNewTestResult: Event; + readonly results: ReadonlyArray; + + /** + * Discards all completed test results. + */ + clear(): void; /** * Adds a new test result to the collection. */ - push(result: TestResult): TestResult; + push(result: LiveTestResult): LiveTestResult; /** * Looks up a set of test results by ID. */ - lookup(resultId: string): TestResult | undefined; + getResult(resultId: string): ITestResult | undefined; + + /** + * Looks up a test's most recent state, by its extension-assigned ID. + */ + getStateByExtId(extId: string): [results: ITestResult, item: TestResultItem] | undefined; } export const ITestResultService = createDecorator('testResultService'); -const RETAIN_LAST_RESULTS = 16; +const RETAIN_LAST_RESULTS = 64; export class TestResultService implements ITestResultService { declare _serviceBrand: undefined; - private newResultEmitter = new Emitter(); + private changeResultEmitter = new Emitter(); + private testChangeEmitter = new Emitter<[results: ITestResult, item: TestResultItem]>(); /** * @inheritdoc */ - public results: TestResult[] = []; + public results: ITestResult[] = []; /** * @inheritdoc */ - public readonly onNewTestResult = this.newResultEmitter.event; + public readonly onResultsChanged = this.changeResultEmitter.event; + + /** + * @inheritdoc + */ + public readonly onTestChanged = this.testChangeEmitter.event; private readonly isRunning: IContextKey; + private readonly serializedResults: StoredValue; - constructor(@IContextKeyService contextKeyService: IContextKeyService) { + constructor(@IContextKeyService contextKeyService: IContextKeyService, @IStorageService storage: IStorageService) { this.isRunning = TestingContextKeys.isRunning.bindTo(contextKeyService); + this.serializedResults = new StoredValue({ + key: 'testResults', + scope: StorageScope.WORKSPACE, + target: StorageTarget.MACHINE + }, storage); + + for (const value of this.serializedResults.get([])) { + this.results.push(new HydratedTestResult(value)); + } } /** * @inheritdoc */ - public push(result: TestResult): TestResult { + public getStateByExtId(extId: string): [results: ITestResult, item: TestResultItem] | undefined { + for (const result of this.results) { + const lookup = result.getStateByExtId(extId); + if (lookup && lookup.computedState !== TestRunState.Unset) { + return [result, lookup]; + } + } + + return undefined; + } + + /** + * @inheritdoc + */ + public push(result: LiveTestResult): LiveTestResult { this.results.unshift(result); if (this.results.length > RETAIN_LAST_RESULTS) { this.results.pop(); } - result.onComplete(this.onComplete, this); + result.onComplete(() => this.onComplete(result)); + result.onChange(t => this.testChangeEmitter.fire([result, t]), this.testChangeEmitter); this.isRunning.set(true); - this.newResultEmitter.fire(result); + this.changeResultEmitter.fire({ started: result }); + result.setAllToState(queuedState, () => true); return result; } /** * @inheritdoc */ - public lookup(id: string) { + public getResult(id: string) { return this.results.find(r => r.id === id); } - private onComplete() { + /** + * @inheritdoc + */ + public clear() { + const keep: ITestResult[] = []; + const removed: ITestResult[] = []; + for (const result of this.results) { + if (result.isComplete) { + removed.push(result); + } else { + keep.push(result); + } + } + + this.results = keep; + this.serializedResults.store(this.results.map(r => r.toJSON())); + this.changeResultEmitter.fire({ removed }); + } + + private onComplete(result: LiveTestResult) { // move the complete test run down behind any still-running ones for (let i = 0; i < this.results.length - 2; i++) { if (this.results[i].isComplete && !this.results[i + 1].isComplete) { @@ -241,5 +535,7 @@ export class TestResultService implements ITestResultService { } this.isRunning.set(!this.results[0]?.isComplete); + this.serializedResults.store(this.results.map(r => r.toJSON())); + this.changeResultEmitter.fire({ completed: result }); } } diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index d9fbf494aad..61639ba1874 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -9,13 +9,14 @@ import { IDisposable, IReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResult } from 'vs/workbench/contrib/testing/common/testResultService'; export const ITestService = createDecorator('testService'); export interface MainTestController { lookupTest(test: TestIdWithProvider): Promise; - runTests(request: RunTestForProviderRequest, token: CancellationToken): Promise; + runTests(request: RunTestForProviderRequest, token: CancellationToken): Promise; } export type TestDiffListener = (diff: TestsDiff) => void; @@ -84,7 +85,7 @@ export interface ITestService { registerTestController(id: string, controller: MainTestController): void; unregisterTestController(id: string): void; - runTests(req: RunTestsRequest, token?: CancellationToken): Promise; + runTests(req: RunTestsRequest, token?: CancellationToken): Promise; cancelTestRun(req: RunTestsRequest): void; publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void; subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference; diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 7ff73345c35..8a18a20daa5 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -8,15 +8,14 @@ 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 } 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'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; -import { AbstractIncrementalTestCollection, collectTestResults, EMPTY_TEST_RESULT, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { ITestResultService, TestResult } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestResult, ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService, MainTestController, TestDiffListener } from 'vs/workbench/contrib/testing/common/testService'; type TestLocationIdent = { resource: ExtHostTestingResource, uri: URI }; @@ -40,14 +39,9 @@ export class TestService extends Disposable implements ITestService { private readonly busyStateChangeEmitter = new Emitter(); private readonly changeProvidersEmitter = new Emitter<{ delta: number }>(); private readonly providerCount: IContextKey; - private readonly runStartedEmitter = new Emitter(); - private readonly runCompletedEmitter = new Emitter<{ req: RunTestsRequest, result: RunTestsResult }>(); private readonly runningTests = new Map(); private rootProviderCount = 0; - public readonly onTestRunStarted = this.runStartedEmitter.event; - public readonly onTestRunCompleted = this.runCompletedEmitter.event; - constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService) { super(); this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService); @@ -126,35 +120,32 @@ export class TestService extends Disposable implements ITestService { /** * @inheritdoc */ - public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise { - let result: TestResult | undefined; + public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise { 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, () => result?.notifyChanged())); - result = this.testResults.push(TestResult.from(subscriptions.map(s => s.object), req.tests)); + .map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri)); + const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req.tests)); try { const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1); const cancelSource = new CancellationTokenSource(token); + this.runningTests.set(req, cancelSource); + const requests = tests.map(group => { const providerId = group[0].providerId; const controller = this.testControllers.get(providerId); - return controller?.runTests({ providerId, debug: req.debug, ids: group.map(t => t.testId) }, cancelSource.token).catch(err => { + return controller?.runTests( + { runId: result.id, providerId, debug: req.debug, ids: group.map(t => t.testId) }, + cancelSource.token, + ).catch(err => { this.notificationService.error(localize('testError', 'An error occurred attempting to run tests: {0}', err.message)); - return EMPTY_TEST_RESULT; }); - }).filter(isDefined); - - if (requests.length === 0) { - return EMPTY_TEST_RESULT; - } - - this.runningTests.set(req, cancelSource); - const result = collectTestResults(await Promise.all(requests)); - this.runningTests.delete(req); + }); + await Promise.all(requests); return result; } finally { + this.runningTests.delete(req); subscriptions.forEach(s => s.dispose()); result.markComplete(); } diff --git a/src/vs/workbench/contrib/testing/common/testStubs.ts b/src/vs/workbench/contrib/testing/common/testStubs.ts index 33a9b5295ad..e1d61278a1f 100644 --- a/src/vs/workbench/contrib/testing/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/common/testStubs.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestItem, TestRunState, TestState } from 'vs/workbench/api/common/extHostTypes'; +import { TestItem, TestRunState } from 'vs/workbench/api/common/extHostTypes'; export const stubTest = (label: string): TestItem => ({ label, location: undefined, - state: new TestState(TestRunState.Unset), debuggable: true, runnable: true, description: '' diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index e03a0659ef5..c63a507d15f 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -10,7 +10,6 @@ import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/s import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; /** * A content provider that returns various outputs for tests. This is used @@ -20,8 +19,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode constructor( @ITextModelService textModelResolverService: ITextModelService, @IModelService private readonly modelService: IModelService, - @ITestService private readonly testService: ITestService, - @ITestService private readonly resultService: ITestResultService, + @ITestResultService private readonly resultService: ITestResultService, ) { textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this); } @@ -40,9 +38,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode return null; } - const test = 'providerId' in parsed - ? await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId }) - : this.resultService.lookup(parsed.resultId)?.tests.find(t => t.id === parsed.testId); + const test = this.resultService.getResult(parsed.resultId)?.getStateByExtId(parsed.testId); if (!test) { return null; @@ -51,16 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode let text: string | undefined; switch (parsed.type) { case TestUriType.ResultActualOutput: - case TestUriType.LiveActualOutput: - text = test.item.state.messages[parsed.messageIndex]?.actualOutput; + text = test.state.messages[parsed.messageIndex]?.actualOutput; break; case TestUriType.ResultExpectedOutput: - case TestUriType.LiveExpectedOutput: - text = test.item.state.messages[parsed.messageIndex]?.expectedOutput; + text = test.state.messages[parsed.messageIndex]?.expectedOutput; break; case TestUriType.ResultMessage: - case TestUriType.LiveMessage: - text = test.item.state.messages[parsed.messageIndex]?.message.toString(); + text = test.state.messages[parsed.messageIndex]?.message.toString(); break; } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 64c192351d7..a7cbbb57a0e 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -5,12 +5,12 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ViewContainerLocation } from 'vs/workbench/common/views'; -import { TestExplorerViewMode, TestExplorerViewGrouping } from 'vs/workbench/contrib/testing/common/constants'; +import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants'; export namespace TestingContextKeys { export const providerCount = new RawContextKey('testing.providerCount', 0); export const viewMode = new RawContextKey('testing.explorerViewMode', TestExplorerViewMode.List); - export const viewGrouping = new RawContextKey('testing.explorerViewGrouping', TestExplorerViewGrouping.ByLocation); + export const viewSorting = new RawContextKey('testing.explorerViewSorting', TestExplorerViewSorting.ByLocation); export const isRunning = new RawContextKey('testing.isRunning', false); export const isInPeek = new RawContextKey('testing.isInPeek', true); export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false); diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 59aa0b93b17..f7eef7b5e45 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -14,12 +14,12 @@ export type TreeStateNode = { statusNode: true; state: TestRunState; priority: n */ export const statePriority: { [K in TestRunState]: number } = { [TestRunState.Running]: 6, - [TestRunState.Queued]: 5, - [TestRunState.Errored]: 4, - [TestRunState.Failed]: 3, - [TestRunState.Passed]: 2, - [TestRunState.Skipped]: 1, - [TestRunState.Unset]: 0, + [TestRunState.Errored]: 5, + [TestRunState.Failed]: 4, + [TestRunState.Passed]: 3, + [TestRunState.Queued]: 2, + [TestRunState.Unset]: 1, + [TestRunState.Skipped]: 0, }; export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed; diff --git a/src/vs/workbench/contrib/testing/common/testingUri.ts b/src/vs/workbench/contrib/testing/common/testingUri.ts index e66995a46cf..5f99df72ba0 100644 --- a/src/vs/workbench/contrib/testing/common/testingUri.ts +++ b/src/vs/workbench/contrib/testing/common/testingUri.ts @@ -8,29 +8,11 @@ import { URI } from 'vs/base/common/uri'; export const TEST_DATA_SCHEME = 'vscode-test-data'; export const enum TestUriType { - LiveMessage, - LiveActualOutput, - LiveExpectedOutput, ResultMessage, ResultActualOutput, ResultExpectedOutput, } -interface ILiveTestUri { - providerId: string; - testId: string; -} - -interface ILiveTestMessageReference extends ILiveTestUri { - type: TestUriType.LiveMessage; - messageIndex: number; -} - -interface ILiveTestOutputReference extends ILiveTestUri { - type: TestUriType.LiveActualOutput | TestUriType.LiveExpectedOutput; - messageIndex: number; -} - interface IResultTestUri { resultId: string; testId: string; @@ -48,13 +30,10 @@ interface IResultTestOutputReference extends IResultTestUri { export type ParsedTestUri = | IResultTestMessageReference - | IResultTestOutputReference - | ILiveTestMessageReference - | ILiveTestOutputReference; + | IResultTestOutputReference; const enum TestUriParts { Results = 'results', - Live = 'live', Messages = 'message', Text = 'text', @@ -78,15 +57,6 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => { case TestUriParts.ExpectedOutput: return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; } - } else if (type === TestUriParts.Live) { - switch (part) { - case TestUriParts.Text: - return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveMessage }; - case TestUriParts.ActualOutput: - return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveActualOutput }; - case TestUriParts.ExpectedOutput: - return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveExpectedOutput }; - } } } @@ -96,7 +66,7 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => { export const buildTestUri = (parsed: ParsedTestUri): URI => { const uriParts = { scheme: TEST_DATA_SCHEME, - authority: 'resultId' in parsed ? TestUriParts.Results : TestUriParts.Live + authority: TestUriParts.Results }; const msgRef = (locationId: string, index: number, ...remaining: string[]) => URI.from({ @@ -111,12 +81,6 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => { return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput); case TestUriType.ResultMessage: return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text); - case TestUriType.LiveActualOutput: - return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ActualOutput); - case TestUriType.LiveExpectedOutput: - return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ExpectedOutput); - case TestUriType.LiveMessage: - return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.Text); default: throw new Error('Invalid test uri'); } diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 3975417f6c3..343ac3cdecf 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { const folder1 = makeTestWorkspaceFolder('f1'); const folder2 = makeTestWorkspaceFolder('f2'); setup(() => { - harness = new TestTreeTestHarness(l => new HierarchicalByLocationProjection(l)); + harness = new TestTreeTestHarness(l => new HierarchicalByLocationProjection(l, { + onResultsChanged: () => undefined, + onTestChanged: () => undefined, + getStateByExtId: () => ({ state: { state: 0 }, computedState: 0 }), + } as any)); }); teardown(() => { diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index b699c910290..c08d3eaf637 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { const folder1 = makeTestWorkspaceFolder('f1'); const folder2 = makeTestWorkspaceFolder('f2'); setup(() => { - harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l)); + harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l, { + onResultsChanged: () => undefined, + onTestChanged: () => undefined, + getStateByExtId: () => ({ state: { state: 0 }, computedState: 0 }), + } as any)); }); teardown(() => { diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts deleted file mode 100644 index 8472bd48c4d..00000000000 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation'; -import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; -import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; - -suite('Workbench - Testing Explorer State by Location Projection', () => { - let harness: TestTreeTestHarness; - setup(() => { - harness = new TestTreeTestHarness(l => new StateByLocationProjection(l)); - }); - - teardown(() => { - harness.dispose(); - }); - - test('renders initial tree', () => { - harness.c.addRoot(testStubs.nested(), 'a'); - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } - ]); - }); - - test('expands if second root is added', () => { - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(); - harness.c.addRoot({ - ...testStubs.test('root2'), - children: [testStubs.test('c')] - }, 'b'); - assert.deepStrictEqual(harness.flush(), [ - { - e: 'Unset', children: [ - { e: 'root', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, - { e: 'root2', children: [{ e: 'c' }] }, - ] - } - ]); - }); - - test('recompacts if second root children are removed', () => { - harness.c.addRoot(testStubs.nested(), 'a'); - harness.flush(); - const root2 = { - ...testStubs.test('root2'), - children: [testStubs.test('c')] - }; - - harness.c.addRoot(root2, 'b'); - harness.flush(); - - root2.children.pop(); - harness.c.onItemChange(root2, 'b'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } - ]); - }); - - test('updates nodes if they change', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[0].label = 'changed'; - harness.c.onItemChange(tests.children[0], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'changed', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] } - ]); - }); - - test('updates nodes if they add children', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[0].children?.push(testStubs.test('ac')); - harness.c.onItemChange(tests.children[0], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, { e: 'b' }] } - ]); - }); - - test('updates nodes if they remove children', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[0].children?.pop(); - harness.c.onItemChange(tests.children[0], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }] }, { e: 'b' }] } - ]); - }); - - test('moves nodes when states change', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - const subchild = tests.children[0].children![0]; - subchild.state = { runState: TestRunState.Passed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Passed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, - ]); - - subchild.state = { runState: TestRunState.Failed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, - ]); - - subchild.state = { runState: TestRunState.Unset, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, - ]); - }); - - test('does not move when state is running', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - const subchild = tests.children[0].children![0]; - subchild.state = { runState: TestRunState.Running, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }, - ]); - - subchild.state = { runState: TestRunState.Failed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] }, - { e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] }, - ]); - }); -}); diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts deleted file mode 100644 index 2670617d2a3..00000000000 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName'; -import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; -import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; - -suite('Workbench - Testing Explorer State by Name Projection', () => { - let harness: TestTreeTestHarness; - setup(() => { - harness = new TestTreeTestHarness(l => new StateByNameProjection(l)); - }); - - teardown(() => { - harness.dispose(); - }); - - test('renders initial tree', () => { - harness.c.addRoot(testStubs.nested(), 'a'); - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } - ]); - }); - - test('swaps when node becomes leaf', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[0].children = []; - harness.c.onItemChange(tests.children[0], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'a' }, { e: 'b' }] } - ]); - }); - - test('swaps when node is no longer leaf', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[1].children = [testStubs.test('ba')]; - harness.c.onItemChange(tests.children[1], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ba' }] } - ]); - }); - - test('swaps when node is no longer runnable', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - tests.children[1].children = [testStubs.test('ba')]; - harness.c.onItemChange(tests.children[0], 'a'); - harness.flush(); - - tests.children[1].children[0].runnable = false; - harness.c.onItemChange(tests.children[1].children[0], 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } - ]); - }); - - test('moves nodes when states change', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - const subchild = tests.children[0].children![0]; - subchild.state = { runState: TestRunState.Passed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Passed', children: [{ e: 'aa' }] }, - { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, - ]); - - subchild.state = { runState: TestRunState.Failed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Failed', children: [{ e: 'aa' }] }, - { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, - ]); - - subchild.state = { runState: TestRunState.Unset, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } - ]); - }); - - test('does not move when state is running', () => { - const tests = testStubs.nested(); - harness.c.addRoot(tests, 'a'); - harness.flush(); - - const subchild = tests.children[0].children![0]; - subchild.state = { runState: TestRunState.Running, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] } - ]); - - subchild.state = { runState: TestRunState.Failed, messages: [] }; - harness.c.onItemChange(subchild, 'a'); - - assert.deepStrictEqual(harness.flush(), [ - { e: 'Failed', children: [{ e: 'aa' }] }, - { e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] }, - ]); - }); -}); 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 d28b37b0063..41d534e5d69 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,6 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb suite('Workbench - Testing URIs', () => { test('round trip', () => { const uris: ParsedTestUri[] = [ - { type: TestUriType.LiveActualOutput, messageIndex: 42, providerId: 'p', testId: 't' }, - { type: TestUriType.LiveExpectedOutput, messageIndex: 42, providerId: 'p', testId: 't' }, - { type: TestUriType.LiveMessage, messageIndex: 42, providerId: 'p', testId: 't' }, { type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' }, { type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' }, { type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' }, diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 1be5d1bc114..a60900d2ea8 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -326,7 +326,6 @@ suite('ExtHost Testing', () => { assert.strictEqual(testItem.label, wrapper.label); assert.strictEqual(testItem.location, wrapper.location); assert.strictEqual(testItem.runnable, wrapper.runnable); - assert.strictEqual(testItem.state, wrapper.state); }); test('gets no children if nothing matches Uri filter', () => {