mirror of
https://github.com/Microsoft/vscode
synced 2024-08-26 20:39:37 +00:00
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
This commit is contained in:
parent
afe8a3475b
commit
387bab6993
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -18,6 +18,7 @@
|
|||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
"cli/target/**": true,
|
||||
".build/**": true,
|
||||
"out/**": true,
|
||||
"out-build/**": true,
|
||||
|
|
|
@ -727,7 +727,7 @@ interface MirroredCollectionTestItem extends IncrementalTestCollectionItem {
|
|||
depth: number;
|
||||
}
|
||||
|
||||
class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollectionTestItem> {
|
||||
class MirroredChangeCollector implements IncrementalChangeCollector<MirroredCollectionTestItem> {
|
||||
private readonly added = new Set<MirroredCollectionTestItem>();
|
||||
private readonly updated = new Set<MirroredCollectionTestItem>();
|
||||
private readonly removed = new Set<MirroredCollectionTestItem>();
|
||||
|
@ -739,20 +739,19 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
|
|||
}
|
||||
|
||||
constructor(private readonly emitter: Emitter<vscode.TestsChangeEvent>) {
|
||||
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<MirroredCollect
|
|||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @inheritdoc
|
||||
*/
|
||||
public override remove(node: MirroredCollectionTestItem): void {
|
||||
public remove(node: MirroredCollectionTestItem): void {
|
||||
if (this.added.has(node)) {
|
||||
this.added.delete(node);
|
||||
return;
|
||||
|
@ -780,7 +779,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
|
|||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @inheritdoc
|
||||
*/
|
||||
public getChangeEvent(): vscode.TestsChangeEvent {
|
||||
const { added, updated, removed } = this;
|
||||
|
@ -791,7 +790,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
|
|||
};
|
||||
}
|
||||
|
||||
public override complete() {
|
||||
public complete() {
|
||||
if (!this.isEmpty) {
|
||||
this.emitter.fire(this.getChangeEvent());
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
|
|||
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
|
||||
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
|
||||
const category = Categories.Test;
|
||||
|
||||
|
@ -669,10 +671,15 @@ abstract class ExecuteTestAtCursor extends Action2 {
|
|||
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
|
||||
super({
|
||||
...options,
|
||||
menu: {
|
||||
menu: [{
|
||||
id: MenuId.CommandPalette,
|
||||
when: hasAnyTestProvider,
|
||||
},
|
||||
}, {
|
||||
id: MenuId.EditorContext,
|
||||
group: 'testing',
|
||||
order: group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug,
|
||||
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -749,6 +756,8 @@ abstract class ExecuteTestAtCursor extends Action2 {
|
|||
group: this.group,
|
||||
tests: bestNodes.length ? bestNodes : bestNodesBefore,
|
||||
});
|
||||
} else if (isCodeEditor(activeControl)) {
|
||||
MessageController.get(activeControl)?.showMessage(localize('noTestsAtCursor', "No tests found here"), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -787,10 +796,16 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
|
|||
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
|
||||
super({
|
||||
...options,
|
||||
menu: {
|
||||
menu: [{
|
||||
id: MenuId.CommandPalette,
|
||||
when: TestingContextKeys.capabilityToContextKey[group].isEqualTo(true),
|
||||
},
|
||||
}, {
|
||||
id: MenuId.EditorContext,
|
||||
group: 'testing',
|
||||
// add 0.1 to be after the "at cursor" commands
|
||||
order: (group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug) + 0.1,
|
||||
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -830,6 +845,10 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
|
|||
});
|
||||
}
|
||||
|
||||
if (isCodeEditor(control)) {
|
||||
MessageController.get(control)?.showMessage(localize('noTestsInFile', "No tests found in this file"), position);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,8 +200,8 @@ export class TestingDecorationService extends Disposable implements ITestingDeco
|
|||
const map = model.changeDecorations(accessor => {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<IncrementalTestCollectionItem> implements IMainThreadTestCollection {
|
||||
private testsByUrl = new ResourceMap<Set<IncrementalTestCollectionItem>>();
|
||||
|
||||
private busyProvidersChangeEmitter = new Emitter<number>();
|
||||
private expandPromises = new WeakMap<IncrementalTestCollectionItem, {
|
||||
pendingLvl: number;
|
||||
|
@ -78,6 +82,13 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
|
|||
return this.items.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public getNodeByUrl(uri: URI): Iterable<IncrementalTestCollectionItem> {
|
||||
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<IncrementalTestCollectionItem> = {
|
||||
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<IncrementalTestCollectionItem> {
|
||||
return this.changeCollector;
|
||||
}
|
||||
|
||||
private *getIterator() {
|
||||
const queue = [this.rootIds];
|
||||
while (queue.length) {
|
||||
|
|
|
@ -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<IncrementalTestCollectionItem>;
|
||||
|
||||
/**
|
||||
* Requests that children be revealed for the given test. "Levels" may
|
||||
* be infinite.
|
||||
|
|
|
@ -41,6 +41,8 @@ export class TestService extends Disposable implements ITestService {
|
|||
private readonly providerCount: IContextKey<number>;
|
||||
private readonly canRefreshTests: IContextKey<boolean>;
|
||||
private readonly isRefreshingTests: IContextKey<boolean>;
|
||||
private readonly activeEditorHasTests: IContextKey<boolean>;
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
if (req.isUiTriggered === false) {
|
||||
return;
|
||||
|
|
|
@ -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<T> {
|
||||
export interface IncrementalChangeCollector<T> {
|
||||
/**
|
||||
* 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<T extends IncrementalTes
|
|||
|
||||
for (const op of diff) {
|
||||
switch (op.op) {
|
||||
case TestDiffOpType.Add: {
|
||||
const internalTest = InternalTestItem.deserialize(op.item);
|
||||
const parentId = TestId.parentId(internalTest.item.extId)?.toString();
|
||||
if (!parentId) {
|
||||
const created = this.createItem(internalTest);
|
||||
this.roots.add(created);
|
||||
this.items.set(internalTest.item.extId, created);
|
||||
changes.add(created);
|
||||
} else if (this.items.has(parentId)) {
|
||||
const parent = this.items.get(parentId)!;
|
||||
parent.children.add(internalTest.item.extId);
|
||||
const created = this.createItem(internalTest, parent);
|
||||
this.items.set(internalTest.item.extId, created);
|
||||
changes.add(created);
|
||||
}
|
||||
|
||||
if (internalTest.expand === TestItemExpandState.BusyExpanding) {
|
||||
this.busyControllerCount++;
|
||||
}
|
||||
case TestDiffOpType.Add:
|
||||
this.add(InternalTestItem.deserialize(op.item), changes);
|
||||
break;
|
||||
}
|
||||
|
||||
case TestDiffOpType.Update: {
|
||||
const patch = ITestItemUpdate.deserialize(op.item);
|
||||
const existing = this.items.get(patch.extId);
|
||||
if (!existing) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (patch.expand !== undefined) {
|
||||
if (existing.expand === TestItemExpandState.BusyExpanding) {
|
||||
this.busyControllerCount--;
|
||||
}
|
||||
if (patch.expand === TestItemExpandState.BusyExpanding) {
|
||||
this.busyControllerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
applyTestItemUpdate(existing, patch);
|
||||
changes.update(existing);
|
||||
case TestDiffOpType.Update:
|
||||
this.update(ITestItemUpdate.deserialize(op.item), changes);
|
||||
break;
|
||||
}
|
||||
|
||||
case TestDiffOpType.Remove: {
|
||||
const toRemove = this.items.get(op.itemId);
|
||||
if (!toRemove) {
|
||||
break;
|
||||
}
|
||||
|
||||
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<string>[] = [[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<T extends IncrementalTes
|
|||
}
|
||||
}
|
||||
|
||||
changes.complete();
|
||||
changes.complete?.();
|
||||
}
|
||||
|
||||
protected add(item: InternalTestItem, changes: IncrementalChangeCollector<T>
|
||||
) {
|
||||
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<T>
|
||||
) {
|
||||
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<T>) {
|
||||
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<string>[] = [[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<T extends IncrementalTes
|
|||
/**
|
||||
* Called before a diff is applied to create a new change collector.
|
||||
*/
|
||||
protected createChangeCollector() {
|
||||
return new IncrementalChangeCollector<T>();
|
||||
protected createChangeCollector(): IncrementalChangeCollector<T> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<boolean> } = {
|
||||
[TestRunProfileBitset.Run]: hasRunnableTests,
|
||||
|
|
Loading…
Reference in a new issue