From 387bab699387a791bf2c34e84d985e0c33796703 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Nov 2022 15:32:57 -0500 Subject: [PATCH] testing: add test actions to editor context menus (#167091) * testing: add test actions to editor context menus Fixes #130548 * add an efficient test by uri lookup, and editor context key --- .vscode/settings.json | 1 + src/vs/workbench/api/common/extHostTesting.ts | 19 +- .../testing/browser/testExplorerActions.ts | 27 ++- .../testing/browser/testingDecorations.ts | 4 +- .../common/mainThreadTestCollection.ts | 47 ++++- .../contrib/testing/common/testService.ts | 6 + .../contrib/testing/common/testServiceImpl.ts | 15 ++ .../contrib/testing/common/testTypes.ts | 169 ++++++++++-------- .../testing/common/testingContextKeys.ts | 1 + 9 files changed, 195 insertions(+), 94 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3abb868dc0c..845320922da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "search.exclude": { "**/node_modules": true, "**/bower_components": true, + "cli/target/**": true, ".build/**": true, "out/**": true, "out-build/**": true, diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 9df368705cd..2e23c169b87 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -727,7 +727,7 @@ interface MirroredCollectionTestItem extends IncrementalTestCollectionItem { depth: number; } -class MirroredChangeCollector extends IncrementalChangeCollector { +class MirroredChangeCollector implements IncrementalChangeCollector { private readonly added = new Set(); private readonly updated = new Set(); private readonly removed = new Set(); @@ -739,20 +739,19 @@ class MirroredChangeCollector extends IncrementalChangeCollector) { - super(); } /** - * @override + * @inheritdoc */ - public override add(node: MirroredCollectionTestItem): void { + public add(node: MirroredCollectionTestItem): void { this.added.add(node); } /** - * @override + * @inheritdoc */ - public override update(node: MirroredCollectionTestItem): void { + public update(node: MirroredCollectionTestItem): void { Object.assign(node.revived, Convert.TestItem.toPlain(node.item)); if (!this.added.has(node)) { this.updated.add(node); @@ -760,9 +759,9 @@ class MirroredChangeCollector extends IncrementalChangeCollector { const newDecorations: ITestDecoration[] = []; const runDecorations = new TestDecorations<{ line: number; id: ''; test: IncrementalTestCollectionItem; resultItem: TestResultItem | undefined }>(); - for (const test of this.testService.collection.all) { - if (!test.item.range || test.item.uri?.toString() !== uriStr) { + for (const test of this.testService.collection.getNodeByUrl(model.uri)) { + if (!test.item.range) { continue; } diff --git a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts index 4ac746110c3..c1de21e7820 100644 --- a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts @@ -5,10 +5,14 @@ import { Emitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; export class MainThreadTestCollection extends AbstractIncrementalTestCollection implements IMainThreadTestCollection { + private testsByUrl = new ResourceMap>(); + private busyProvidersChangeEmitter = new Emitter(); private expandPromises = new WeakMap { + return this.testsByUrl.get(uri) || Iterable.empty(); + } + /** * @inheritdoc */ @@ -138,6 +149,40 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< return { ...internal, children: new Set() }; } + private readonly changeCollector: IncrementalChangeCollector = { + add: node => { + if (!node.item.uri) { + return; + } + + const s = this.testsByUrl.get(node.item.uri); + if (!s) { + this.testsByUrl.set(node.item.uri, new Set([node])); + } else { + s.add(node); + } + }, + remove: node => { + if (!node.item.uri) { + return; + } + + const s = this.testsByUrl.get(node.item.uri); + if (!s) { + return; + } + + s.delete(node); + if (s.size === 0) { + this.testsByUrl.delete(node.item.uri); + } + }, + }; + + protected override createChangeCollector(): IncrementalChangeCollector { + return this.changeCollector; + } + private *getIterator() { const queue = [this.rootIds]; while (queue.length) { diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 9da1e073307..80704ac4d07 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -61,6 +61,12 @@ export interface IMainThreadTestCollection extends AbstractIncrementalTestCollec */ getNodeById(id: string): IncrementalTestCollectionItem | undefined; + /** + * Gets all tests that have the given URL. Tests returned from this + * method are *not* in any particular order. + */ + getNodeByUrl(uri: URI): Iterable; + /** * Requests that children be revealed for the given test. "Levels" may * be infinite. diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 0467fa176c7..d383918413f 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -41,6 +41,8 @@ export class TestService extends Disposable implements ITestService { private readonly providerCount: IContextKey; private readonly canRefreshTests: IContextKey; private readonly isRefreshingTests: IContextKey; + private readonly activeEditorHasTests: IContextKey; + /** * Cancellation for runs requested by the user being managed by the UI. * Test runs initiated by extensions are not included here. @@ -97,6 +99,9 @@ export class TestService extends Disposable implements ITestService { this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService); this.canRefreshTests = TestingContextKeys.canRefreshTests.bindTo(contextKeyService); this.isRefreshingTests = TestingContextKeys.isRefreshingTests.bindTo(contextKeyService); + this.activeEditorHasTests = TestingContextKeys.activeEditorHasTests.bindTo(contextKeyService); + + this._register(editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys())); } /** @@ -228,6 +233,7 @@ export class TestService extends Disposable implements ITestService { public publishDiff(_controllerId: string, diff: TestsDiff) { this.willProcessDiffEmitter.fire(diff); this.collection.apply(diff); + this.updateEditorContextKeys(); this.didProcessDiffEmitter.fire(diff); } @@ -313,6 +319,15 @@ export class TestService extends Disposable implements ITestService { return disposable; } + private updateEditorContextKeys() { + const uri = this.editorService.activeEditor?.resource; + if (uri) { + this.activeEditorHasTests.set(!Iterable.isEmpty(this.collection.getNodeByUrl(uri))); + } else { + this.activeEditorHasTests.set(false); + } + } + private async saveAllBeforeTest(req: ResolvedTestRunRequest, configurationService: IConfigurationService = this.configurationService, editorService: IEditorService = this.editorService): Promise { if (req.isUiTriggered === false) { return; diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index b93713245cd..0eae7d8c52c 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -626,26 +626,26 @@ export interface IncrementalTestCollectionItem extends InternalTestItem { * and called with diff changes as they're applied. This is used in the * ext host to create a cohesive change event from a diff. */ -export class IncrementalChangeCollector { +export interface IncrementalChangeCollector { /** * A node was added. */ - public add(node: T): void { } + add?(node: T): void; /** * A node in the collection was updated. */ - public update(node: T): void { } + update?(node: T): void; /** * A node was removed. */ - public remove(node: T, isNestedOperation: boolean): void { } + remove?(node: T, isNestedOperation: boolean): void; /** * Called when the diff has been applied. */ - public complete(): void { } + complete?(): void; } /** @@ -687,80 +687,17 @@ export abstract class AbstractIncrementalTestCollection[] = [[op.itemId]]; - while (queue.length) { - for (const itemId of queue.pop()!) { - const existing = this.items.get(itemId); - if (existing) { - queue.push(existing.children); - this.items.delete(itemId); - changes.remove(existing, existing !== toRemove); - - if (existing.expand === TestItemExpandState.BusyExpanding) { - this.busyControllerCount--; - } - } - } - } + case TestDiffOpType.Remove: + this.remove(op.itemId, changes); break; - } case TestDiffOpType.Retire: this.retireTest(op.itemId); @@ -780,7 +717,85 @@ export abstract class AbstractIncrementalTestCollection + ) { + const parentId = TestId.parentId(item.item.extId)?.toString(); + let created: T; + if (!parentId) { + created = this.createItem(item); + this.roots.add(created); + this.items.set(item.item.extId, created); + } else if (this.items.has(parentId)) { + const parent = this.items.get(parentId)!; + parent.children.add(item.item.extId); + created = this.createItem(item, parent); + this.items.set(item.item.extId, created); + } else { + console.error(`Test with unknown parent ID: ${JSON.stringify(item)}`); + return; + } + + changes.add?.(created); + if (item.expand === TestItemExpandState.BusyExpanding) { + this.busyControllerCount++; + } + + return created; + } + + protected update(patch: ITestItemUpdate, changes: IncrementalChangeCollector + ) { + const existing = this.items.get(patch.extId); + if (!existing) { + return; + } + + if (patch.expand !== undefined) { + if (existing.expand === TestItemExpandState.BusyExpanding) { + this.busyControllerCount--; + } + if (patch.expand === TestItemExpandState.BusyExpanding) { + this.busyControllerCount++; + } + } + + applyTestItemUpdate(existing, patch); + changes.update?.(existing); + return existing; + } + + protected remove(itemId: string, changes: IncrementalChangeCollector) { + const toRemove = this.items.get(itemId); + if (!toRemove) { + return; + } + + const parentId = TestId.parentId(toRemove.item.extId)?.toString(); + if (parentId) { + const parent = this.items.get(parentId)!; + parent.children.delete(toRemove.item.extId); + } else { + this.roots.delete(toRemove); + } + + const queue: Iterable[] = [[itemId]]; + while (queue.length) { + for (const itemId of queue.pop()!) { + const existing = this.items.get(itemId); + if (existing) { + queue.push(existing.children); + this.items.delete(itemId); + changes.remove?.(existing, existing !== toRemove); + + if (existing.expand === TestItemExpandState.BusyExpanding) { + this.busyControllerCount--; + } + } + } + } } /** @@ -802,8 +817,8 @@ export abstract class AbstractIncrementalTestCollection(); + protected createChangeCollector(): IncrementalChangeCollector { + return {}; } /** diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index d575013bc69..5bdcb36c2ef 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -17,6 +17,7 @@ export namespace TestingContextKeys { export const hasCoverableTests = new RawContextKey('testing.hasCoverableTests', false, { type: 'boolean', description: localize('testing.hasCoverableTests', 'Indicates whether any test controller has registered a coverage configuration') }); export const hasNonDefaultProfile = new RawContextKey('testing.hasNonDefaultProfile', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') }); export const hasConfigurableProfile = new RawContextKey('testing.hasConfigurableProfile', false, { type: 'boolean', description: localize('testing.hasConfigurableConfig', 'Indicates whether any test configuration can be configured') }); + export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests,