diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 91623f4eefd..b0da121837d 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -1013,3 +1013,61 @@ export class IntervalCounter { } //#endregion + +export type ValueCallback = (value: T | Promise) => void; + +/** + * Creates a promise whose resolution or rejection can be controlled imperatively. + */ +export class DeferredPromise { + + private completeCallback!: ValueCallback; + private errorCallback!: (err: any) => void; + private rejected = false; + private resolved = false; + + public get isRejected() { + return this.rejected; + } + + public get isResolved() { + return this.resolved; + } + + public get isSettled() { + return this.rejected || this.resolved; + } + + public p: Promise; + + constructor() { + this.p = new Promise((c, e) => { + this.completeCallback = c; + this.errorCallback = e; + }); + } + + public complete(value: T) { + return new Promise(resolve => { + this.completeCallback(value); + this.resolved = true; + resolve(); + }); + } + + public error(err: any) { + return new Promise(resolve => { + this.errorCallback(err); + this.rejected = true; + resolve(); + }); + } + + public cancel() { + new Promise(resolve => { + this.errorCallback(errors.canceled()); + this.rejected = true; + resolve(); + }); + } +} diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 43a0d0c4f46..616a4fd52e2 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -780,4 +780,31 @@ suite('Async', () => { assert.equal(ct1!.isCancellationRequested, true, 'should cancel a'); assert.equal(ct2!.isCancellationRequested, true, 'should cancel b'); }); + + suite('DeferredPromise', () => { + test('resolves', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isResolved, false); + deferred.complete(42); + assert.strictEqual(await deferred.p, 42); + assert.strictEqual(deferred.isResolved, true); + }); + + test('rejects', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isRejected, false); + const err = new Error('oh no!'); + deferred.error(err); + assert.strictEqual(await deferred.p.catch(e => e), err); + assert.strictEqual(deferred.isRejected, true); + }); + + test('cancels', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isRejected, false); + deferred.cancel(); + assert.strictEqual((await deferred.p.catch(e => e)).name, 'Canceled'); + assert.strictEqual(deferred.isRejected, true); + }); + }); }); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index c09ff722421..63b0541b482 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -5,47 +5,10 @@ import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { canceled } from 'vs/base/common/errors'; import { isWindows } from 'vs/base/common/platform'; export type ValueCallback = (value: T | Promise) => void; -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: any) => void; - - public p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - resolve(); - }); - } - - public error(err: any) { - return new Promise(resolve => { - this.errorCallback(err); - resolve(); - }); - } - - public cancel() { - new Promise(resolve => { - this.errorCallback(canceled()); - resolve(); - }); - } -} - export function toResource(this: any, path: string) { if (isWindows) { return URI.file(join('C:\\', btoa(this.test.fullTitle()), path)); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 70903d93e39..8059d31508a 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -621,16 +621,9 @@ class TextDocumentTestObserverFactory extends AbstractTestObserverFactory { const uriString = resourceUri.toString(); this.diffListeners.set(uriString, onDiff); - const disposeListener = this.documents.onDidRemoveDocuments(evt => { - if (evt.some(delta => delta.document.uri.toString() === uriString)) { - this.unlisten(resourceUri); - } - }); - this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri); return new Disposable(() => { this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri); - disposeListener.dispose(); this.diffListeners.delete(uriString); }); } diff --git a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts index 13eb041293d..19179f52ae7 100644 --- a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts +++ b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts @@ -5,9 +5,9 @@ import * as assert from 'assert'; import * as errors from 'vs/base/common/errors'; -import { DeferredPromise } from 'vs/base/test/common/utils'; import { QueryType, IFileQuery } from 'vs/workbench/services/search/common/search'; import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState'; +import { DeferredPromise } from 'vs/base/common/async'; suite('FileQueryCacheState', () => { diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index 0a99a6a7af8..11d57d0cf1e 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { timeout } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; -import { DeferredPromise } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 12c0029c2a3..07bd0d0c1a7 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -7,7 +7,7 @@ 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/browser/testExplorerTree'; +import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; /** diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts index b60493f3b28..7500b77cfda 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts @@ -7,11 +7,8 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; -import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections'; -export const isRunningState = (s: TestRunState) => s === TestRunState.Queued || s === TestRunState.Running; - export const testIdentityProvider: IIdentityProvider = { getId(element) { return element.treeId; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts index b56bdad656a..ea6f7c7919a 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts @@ -14,10 +14,10 @@ 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 { isRunningState, NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes'; -import { statesInOrder } from 'vs/workbench/contrib/testing/browser/testExplorerTree'; 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 { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts index cd08be93fab..cd7ad2a077b 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts @@ -15,9 +15,10 @@ 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 { isRunningState, NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; +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 { diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 52aaeebc760..2678d4260a9 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -33,8 +33,10 @@ import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import * as Action from './testExplorerActions'; +import { ITestResultService, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; registerSingleton(ITestService, TestService); +registerSingleton(ITestResultService, TestResultService); registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService); const viewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index dc0f2989071..085b01a9a6e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -10,6 +10,7 @@ import { 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 { DeferredPromise } from 'vs/base/common/async'; import { throttle } from 'vs/base/common/decorators'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -30,7 +31,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IProgress, IProgressService, IProgressStep } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -47,13 +48,14 @@ import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/ 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 { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/browser/testExplorerTree'; import { TestingExplorerFilter, TestingFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestExplorerViewGrouping, TestExplorerViewMode } from 'vs/workbench/contrib/testing/common/constants'; -import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; +import { ITestResultService, sumCounts } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DebugAction, RunAction } from './testExplorerActions'; @@ -105,7 +107,7 @@ export class TestingExplorerView extends ViewPane { this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility, this.currentSubscription, this.filterState); this._register(this.viewModel); - this.getProgressIndicator().show(true); + this._register(this.instantiationService.createInstance(TestRunProgress, this.getProgressLocation())); this._register(this.onDidChangeBodyVisibility(visible => { if (!visible && this.currentSubscription) { @@ -127,7 +129,7 @@ export class TestingExplorerView extends ViewPane { this.filter.saveState(); } - private updateProgressIndicator(busy: number) { + private updateDiscoveryProgress(busy: number) { if (!busy && this.finishDiscovery) { this.finishDiscovery(); this.finishDiscovery = undefined; @@ -148,7 +150,7 @@ export class TestingExplorerView extends ViewPane { private createSubscription() { const handle = this.testCollection.subscribeToWorkspaceTests(); - handle.subscription.onBusyProvidersChange(() => this.updateProgressIndicator(handle.subscription.busyProviders)); + handle.subscription.onBusyProvidersChange(() => this.updateDiscoveryProgress(handle.subscription.busyProviders)); return handle; } } @@ -647,3 +649,48 @@ class TestsRenderer implements ITreeRenderer; deferred: DeferredPromise }; + private readonly resultLister = this.resultService.onNewTestResult(result => { + this.update(); + result.onChange(this.throttledUpdate, this); + result.onComplete(this.throttledUpdate, this); + }); + + constructor( + private readonly location: string, + @IProgressService private readonly progress: IProgressService, + @ITestResultService private readonly resultService: ITestResultService, + ) { } + + public dispose() { + this.resultLister.dispose(); + this.current?.deferred.complete(); + this.current = undefined; + } + + @throttle(200) + private throttledUpdate() { + this.update(); + } + + private update() { + const running = this.resultService.results.filter(r => !r.isComplete); + if (!running.length) { + this.current?.deferred.complete(); + this.current = undefined; + } else if (!this.current) { + this.progress.withProgress({ location: this.location, total: 100 }, update => { + this.current = { update, deferred: new DeferredPromise() }; + this.update(); + return this.current.deferred.p; + }); + } else { + const count = sumCounts(running.map(r => r.counts)); + const completed = count[TestRunState.Errored] + count[TestRunState.Failed] + count[TestRunState.Passed]; + const total = completed + count[TestRunState.Queued] + count[TestRunState.Running]; + this.current.update.report({ increment: completed, total }); + } + } +} diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts new file mode 100644 index 00000000000..082a82a8204 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -0,0 +1,226 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; +import { IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; +import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; + +export type TestStateCount = { [K in TestRunState]: number }; + +const makeEmptyCounts = () => { + const o: Partial = {}; + for (const state of statesInOrder) { + o[state] = 0; + } + + return o as TestStateCount; +}; + +export const sumCounts = (counts: TestStateCount[]) => { + const total = makeEmptyCounts(); + for (const count of counts) { + for (const state of statesInOrder) { + total[state] += count[state]; + } + } + + return total; +}; + +const makeNode = ( + collection: IMainThreadTestCollection, + test: IncrementalTestCollectionItem, +): TestResultItem => { + const mapped: TestResultItem = { ...test, children: [], results: makeEmptyCounts() }; + mapped.results[test.item.state.runState]++; + + for (const childId of test.children) { + const child = collection.getNodeById(childId); + if (child) { + mapped.children.push(makeNode(collection, child)); + } + } + + return mapped; +}; + +export interface TestResultItem extends InternalTestItem { + children: TestResultItem[] + results: { [K in TestRunState]: number }; +} + +/** + * Results of a test. These are created when the test initially started running + * and marked as "complete" when the run finishes. + */ +export class TestResult { + /** + * Creates a new TestResult, pulling tests from the associated list + * of collections. + */ + public static from( + collections: ReadonlyArray, + tests: ReadonlyArray, + ) { + const mapped: TestResultItem[] = []; + for (const test of tests) { + for (const collection of collections) { + const node = collection.getNodeById(test.testId); + if (node) { + mapped.push(makeNode(collection, node)); + break; + } + } + } + + return new TestResult(mapped); + } + + private completeEmitter = new Emitter(); + private changeEmitter = new Emitter(); + private _complete = false; + private _cachedCounts?: { [K in TestRunState]: number }; + + public onChange = this.changeEmitter.event; + public onComplete = this.completeEmitter.event; + + /** + * Gets whether the test run has finished. + */ + public get isComplete() { + return this._complete; + } + + /** + * Gets a count of tests in each state. + */ + public get counts() { + if (this._cachedCounts) { + return this._cachedCounts; + } + + const counts = makeEmptyCounts(); + this.forEachTest(({ item }) => { + counts[item.state.runState]++; + }); + + if (this._complete) { + this._cachedCounts = counts; + } + + return counts; + } + + constructor(public readonly tests: TestResultItem[]) { } + + + /** + * Notifies the service that all tests are complete. + */ + public markComplete() { + if (this._complete) { + 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 }; + } + }); + + this._complete = true; + this.completeEmitter.fire(); + } + + /** + * Fires the 'change' event, should be called by the runner. + */ + 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); + } + } + } +} + +export interface ITestResultService { + readonly _serviceBrand: undefined; + + /** + * List of test results. Currently running tests are always at the top. + */ + readonly results: TestResult[]; + + /** + * Fired after a new event is added to the 'active' array. + */ + readonly onNewTestResult: Event; + + /** + * Adds a new test result to the collection. + */ + push(result: TestResult): TestResult; +} + +export const ITestResultService = createDecorator('testResultService'); + +const RETAIN_LAST_RESULTS = 10; + +export class TestResultService implements ITestResultService { + declare _serviceBrand: undefined; + private newResultEmitter = new Emitter(); + + /** + * @inheritdoc + */ + public readonly results: TestResult[] = []; + + /** + * @inheritdoc + */ + public readonly onNewTestResult = this.newResultEmitter.event; + + private readonly isRunning: IContextKey; + + constructor(@IContextKeyService contextKeyService: IContextKeyService) { + this.isRunning = TestingContextKeys.isRunning.bindTo(contextKeyService); + } + + /** + * @inheritdoc + */ + public push(result: TestResult): TestResult { + this.results.unshift(result); + if (this.results.length > RETAIN_LAST_RESULTS) { + this.results.pop(); + } + + result.onComplete(this.reorder, this); + this.reorder(); + this.newResultEmitter.fire(result); + return result; + } + + private reorder() { + this.results.sort((a, b) => (a.isComplete ? 0 : 1) - (b.isComplete ? 0 : 1)); + this.isRunning.set(this.results.length > 0 && !this.results[0].isComplete); + } +} diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index a9e7fd053c6..c922f3e6156 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -75,10 +75,7 @@ export interface ITestService { readonly onDidChangeProviders: Event<{ delta: number; }>; readonly providers: number; readonly subscriptions: ReadonlyArray<{ resource: ExtHostTestingResource, uri: URI; }>; - readonly testRuns: Iterable; - readonly onTestRunStarted: Event; - readonly onTestRunCompleted: Event<{ req: RunTestsRequest, result: RunTestsResult; }>; registerTestController(id: string, controller: MainTestController): void; unregisterTestController(id: string): void; diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 11a620c3eeb..288540017a9 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -16,6 +16,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati 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 { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestResultService, TestResult } 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 }; @@ -39,7 +40,6 @@ 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 isRunning: IContextKey; private readonly runStartedEmitter = new Emitter(); private readonly runCompletedEmitter = new Emitter<{ req: RunTestsRequest, result: RunTestsResult }>(); private readonly runningTests = new Map(); @@ -48,10 +48,9 @@ export class TestService extends Disposable implements ITestService { public readonly onTestRunStarted = this.runStartedEmitter.event; public readonly onTestRunCompleted = this.runCompletedEmitter.event; - constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService) { + constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService) { super(); this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService); - this.isRunning = TestingContextKeys.isRunning.bindTo(contextKeyService); } /** @@ -128,32 +127,37 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise { - const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1); - const cancelSource = new CancellationTokenSource(token); - 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 => { - this.notificationService.error(localize('testError', 'An error occurred attempting to run tests: {0}', err.message)); + let result: TestResult | undefined; + 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.collection), req.tests)); + + try { + const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1); + const cancelSource = new CancellationTokenSource(token); + 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 => { + 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; - }); - }).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); + + return result; + } finally { + subscriptions.forEach(s => s.dispose()); + result.markComplete(); } - - this.runningTests.set(req, cancelSource); - this.runStartedEmitter.fire(req); - this.isRunning.set(true); - - const result = collectTestResults(await Promise.all(requests)); - - this.runningTests.delete(req); - this.runCompletedEmitter.fire({ req, result }); - this.isRunning.set(this.runningTests.size > 0); - - return result; } /** diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerTree.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts similarity index 93% rename from src/vs/workbench/contrib/testing/browser/testExplorerTree.ts rename to src/vs/workbench/contrib/testing/common/testingStates.ts index e6510f57f07..3664745ea7e 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerTree.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -38,3 +38,5 @@ export const cmpPriority = (a: TestRunState, b: TestRunState) => statePriority[b export const maxPriority = (a: TestRunState, b: TestRunState) => statePriority[a] > statePriority[b] ? a : b; export const statesInOrder = Object.keys(statePriority).map(s => Number(s) as TestRunState).sort(cmpPriority); + +export const isRunningState = (s: TestRunState) => s === TestRunState.Queued || s === TestRunState.Running; diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index e1e2b6b5570..daf00408cab 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as arrays from 'vs/base/common/arrays'; +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,13 +14,13 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { URI as uri } from 'vs/base/common/uri'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, ITextQuery, pathIncludedInQuery, QueryType, SearchError, SearchErrorCode, SearchProviderType, isFileMatch, isProgressMessage } from 'vs/workbench/services/search/common/search'; +import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class SearchService extends Disposable implements ISearchService { @@ -514,41 +515,3 @@ export class RemoteSearchService extends SearchService { } registerSingleton(ISearchService, RemoteSearchService, true); - -export type ValueCallback = (value: T | Promise) => void; -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: any) => void; - - public p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - resolve(); - }); - } - - public error(err: any) { - return new Promise(resolve => { - this.errorCallback(err); - resolve(); - }); - } - - public cancel() { - new Promise(resolve => { - this.errorCallback(canceled()); - resolve(); - }); - } -} -