mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 13:46:13 +00:00
testing: commands to run tests at current cursor and in file
Refs https://github.com/microsoft/vscode/issues/116589
This commit is contained in:
parent
07e3bcf7ea
commit
a0e0324a8d
|
@ -6,6 +6,7 @@
|
|||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
|
@ -18,6 +19,7 @@ const reviveDiff = (diff: TestsDiff) => {
|
|||
const item = entry[1];
|
||||
if (item.item.location) {
|
||||
item.item.location.uri = URI.revive(item.item.location.uri);
|
||||
item.item.location.range = Range.lift(item.item.location.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +73,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
|||
for (const message of state.messages) {
|
||||
if (message.location) {
|
||||
message.location.uri = URI.revive(message.location.uri);
|
||||
message.location.range = Range.lift(message.location.range);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1507,7 +1507,7 @@ export namespace TestState {
|
|||
severity: message.severity,
|
||||
expectedOutput: message.expectedOutput,
|
||||
actualOutput: message.actualOutput,
|
||||
location: message.location ? location.from(message.location) : undefined,
|
||||
location: message.location ? location.from(message.location) as any : undefined,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
|
@ -1536,7 +1536,7 @@ export namespace TestItem {
|
|||
return {
|
||||
extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label),
|
||||
label: item.label,
|
||||
location: item.location ? location.from(item.location) : undefined,
|
||||
location: item.location ? location.from(item.location) as any : undefined,
|
||||
debuggable: item.debuggable ?? false,
|
||||
description: item.description,
|
||||
runnable: item.runnable ?? true,
|
||||
|
|
|
@ -6,25 +6,22 @@
|
|||
import { findFirstInSorted } from 'vs/base/common/arrays';
|
||||
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 { Range } from 'vs/editor/common/core/range';
|
||||
import { IRichLocation } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
|
||||
export const locationsEqual = (a: ModeLocation | undefined, b: ModeLocation | undefined) => {
|
||||
export const locationsEqual = (a: IRichLocation | undefined, b: IRichLocation | undefined) => {
|
||||
if (a === undefined || b === undefined) {
|
||||
return b === a;
|
||||
}
|
||||
|
||||
return a.uri.toString() === b.uri.toString()
|
||||
&& a.range.startLineNumber === b.range.startLineNumber
|
||||
&& a.range.startColumn === b.range.startColumn
|
||||
&& a.range.endLineNumber === b.range.endLineNumber
|
||||
&& a.range.endColumn === b.range.endColumn;
|
||||
return a.uri.toString() === b.uri.toString() && a.range.equalsRange(b.range);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores and looks up test-item-like-objects by their uri/range. Used to
|
||||
* implement the 'reveal' action efficiently.
|
||||
*/
|
||||
export class TestLocationStore<T extends { location?: ModeLocation, depth: number }> {
|
||||
export class TestLocationStore<T extends { location?: IRichLocation, depth: number }> {
|
||||
private readonly itemsByUri = new Map<string, T[]>();
|
||||
|
||||
public hasTestInDocument(uri: URI) {
|
||||
|
@ -39,12 +36,7 @@ export class TestLocationStore<T extends { location?: ModeLocation, depth: numbe
|
|||
|
||||
return tests.find(test => {
|
||||
const range = test.location?.range;
|
||||
return range
|
||||
&& new Position(range.startLineNumber, range.startColumn).isBeforeOrEqual(position)
|
||||
&& position.isBeforeOrEqual(new Position(
|
||||
range.endLineNumber ?? range.startLineNumber,
|
||||
range.endColumn ?? range.startColumn,
|
||||
));
|
||||
return range && Range.lift(range).containsPosition(position);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -26,8 +26,9 @@ import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testi
|
|||
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
|
||||
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 { ITestService, waitForAllRoots, waitForAllTests } from 'vs/workbench/contrib/testing/common/testService';
|
||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
const category = localize('testing.category', 'Test');
|
||||
|
||||
|
@ -94,7 +95,7 @@ export class RunAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
|
||||
abstract class RunOrDebugSelectedAction extends ViewAction<TestingExplorerView> {
|
||||
constructor(id: string, title: string, icon: ThemeIcon, private readonly debug: boolean) {
|
||||
super({
|
||||
id,
|
||||
|
@ -103,6 +104,7 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
|
|||
viewId: Testing.ExplorerViewId,
|
||||
f1: true,
|
||||
category,
|
||||
precondition: FocusedViewContext.isEqualTo(Testing.ExplorerViewId),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -143,7 +145,7 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
|
|||
protected abstract filter(item: InternalTestItem): boolean;
|
||||
}
|
||||
|
||||
export class RunSelectedAction extends RunOrDebugAction {
|
||||
export class RunSelectedAction extends RunOrDebugSelectedAction {
|
||||
constructor(
|
||||
) {
|
||||
super(
|
||||
|
@ -162,7 +164,7 @@ export class RunSelectedAction extends RunOrDebugAction {
|
|||
}
|
||||
}
|
||||
|
||||
export class DebugSelectedAction extends RunOrDebugAction {
|
||||
export class DebugSelectedAction extends RunOrDebugSelectedAction {
|
||||
constructor() {
|
||||
super(
|
||||
'testing.debugSelected',
|
||||
|
@ -502,3 +504,166 @@ export class ToggleAutoRun extends Action2 {
|
|||
accessor.get(ITestingAutoRun).toggle();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RunOrDebugAtCursor extends Action2 {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async run(accessor: ServicesAccessor) {
|
||||
const control = accessor.get(IEditorService).activeTextEditorControl;
|
||||
const position = control?.getPosition();
|
||||
const model = control?.getModel();
|
||||
if (!position || !model || !('uri' in model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const testService = accessor.get(ITestService);
|
||||
const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri);
|
||||
|
||||
let bestDepth = -1;
|
||||
let bestNode: InternalTestItem | undefined;
|
||||
|
||||
try {
|
||||
await waitForAllTests(collection.object);
|
||||
const queue: [depth: number, nodes: Iterable<string>][] = [[0, collection.object.rootIds]];
|
||||
while (queue.length > 0) {
|
||||
const [depth, candidates] = queue.pop()!;
|
||||
for (const id of candidates) {
|
||||
const candidate = collection.object.getNodeById(id);
|
||||
if (candidate) {
|
||||
if (depth > bestDepth && this.filter(candidate) && candidate.item.location?.range.containsPosition(position)) {
|
||||
bestDepth = depth;
|
||||
bestNode = candidate;
|
||||
}
|
||||
|
||||
queue.push([depth + 1, candidate.children]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestNode) {
|
||||
await this.runTest(testService, bestNode);
|
||||
}
|
||||
} finally {
|
||||
collection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract filter(node: InternalTestItem): boolean;
|
||||
|
||||
protected abstract runTest(service: ITestService, node: InternalTestItem): Promise<ITestResult>;
|
||||
}
|
||||
|
||||
export class RunAtCursor extends RunOrDebugAtCursor {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.runAtCursor',
|
||||
title: localize('testing.runAtCursor', "Run Test at Cursor"),
|
||||
f1: true,
|
||||
category,
|
||||
});
|
||||
}
|
||||
|
||||
protected filter(node: InternalTestItem): boolean {
|
||||
return node.item.runnable;
|
||||
}
|
||||
|
||||
protected runTest(service: ITestService, node: InternalTestItem): Promise<ITestResult> {
|
||||
return service.runTests({ debug: false, tests: [{ testId: node.id, providerId: node.providerId }] });
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugAtCursor extends RunOrDebugAtCursor {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.debugAtCursor',
|
||||
title: localize('testing.debugAtCursor', "Debug Test at Cursor"),
|
||||
f1: true,
|
||||
category,
|
||||
});
|
||||
}
|
||||
|
||||
protected filter(node: InternalTestItem): boolean {
|
||||
return node.item.debuggable;
|
||||
}
|
||||
|
||||
protected runTest(service: ITestService, node: InternalTestItem): Promise<ITestResult> {
|
||||
return service.runTests({ debug: true, tests: [{ testId: node.id, providerId: node.providerId }] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class RunOrDebugCurrentFile extends Action2 {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async run(accessor: ServicesAccessor) {
|
||||
const control = accessor.get(IEditorService).activeTextEditorControl;
|
||||
const position = control?.getPosition();
|
||||
const model = control?.getModel();
|
||||
if (!position || !model || !('uri' in model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const testService = accessor.get(ITestService);
|
||||
const collection = testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, model.uri);
|
||||
|
||||
try {
|
||||
await waitForAllTests(collection.object);
|
||||
|
||||
const roots = [...collection.object.rootIds]
|
||||
.map(r => collection.object.getNodeById(r))
|
||||
.filter(isDefined)
|
||||
.filter(n => this.filter(n));
|
||||
|
||||
if (roots.length) {
|
||||
await this.runTest(testService, roots);
|
||||
}
|
||||
} finally {
|
||||
collection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract filter(node: InternalTestItem): boolean;
|
||||
|
||||
protected abstract runTest(service: ITestService, node: InternalTestItem[]): Promise<ITestResult>;
|
||||
}
|
||||
|
||||
export class RunCurrentFile extends RunOrDebugCurrentFile {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.runCurrentFile',
|
||||
title: localize('testing.runCurrentFile', "Run Tests in Current File"),
|
||||
f1: true,
|
||||
category,
|
||||
});
|
||||
}
|
||||
|
||||
protected filter(node: InternalTestItem): boolean {
|
||||
return node.item.runnable;
|
||||
}
|
||||
|
||||
protected runTest(service: ITestService, nodes: InternalTestItem[]): Promise<ITestResult> {
|
||||
return service.runTests({ debug: false, tests: nodes.map(node => ({ testId: node.id, providerId: node.providerId })) });
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugCurrentFile extends RunOrDebugCurrentFile {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'testing.debugCurrentFile',
|
||||
title: localize('testing.debugCurrentFile', "Debug Tests in Current File"),
|
||||
f1: true,
|
||||
category,
|
||||
});
|
||||
}
|
||||
|
||||
protected filter(node: InternalTestItem): boolean {
|
||||
return node.item.debuggable;
|
||||
}
|
||||
|
||||
protected runTest(service: ITestService, nodes: InternalTestItem[]): Promise<ITestResult> {
|
||||
return service.runTests({ debug: true, tests: nodes.map(node => ({ testId: node.id, providerId: node.providerId })) });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,6 +97,10 @@ registerAction2(Action.DebugAllAction);
|
|||
registerAction2(Action.EditFocusedTest);
|
||||
registerAction2(Action.ClearTestResultsAction);
|
||||
registerAction2(Action.ToggleAutoRun);
|
||||
registerAction2(Action.DebugAtCursor);
|
||||
registerAction2(Action.RunAtCursor);
|
||||
registerAction2(Action.DebugCurrentFile);
|
||||
registerAction2(Action.RunCurrentFile);
|
||||
registerAction2(CloseTestPeek);
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
||||
|
|
|
@ -14,7 +14,6 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
|||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { overviewRulerError, overviewRulerInfo, overviewRulerWarning } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
|
@ -27,7 +26,7 @@ import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from
|
|||
import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||
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 { IncrementalTestCollectionItem, IRichLocation, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||
import { ITestResultService, TestResultItem } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
|
@ -144,7 +143,7 @@ interface ITestDecoration extends IDisposable {
|
|||
click(e: IEditorMouseEvent): boolean;
|
||||
}
|
||||
|
||||
const hasValidLocation = <T extends { location?: ModeLocation }>(editorUri: URI, t: T): t is T & { location: ModeLocation } =>
|
||||
const hasValidLocation = <T extends { location?: IRichLocation }>(editorUri: URI, t: T): t is T & { location: IRichLocation } =>
|
||||
t.location?.uri.toString() === editorUri.toString();
|
||||
|
||||
const firstLineRange = (originalRange: IRange) => ({
|
||||
|
@ -170,7 +169,7 @@ class RunTestDecoration extends Disposable implements ITestDecoration {
|
|||
constructor(
|
||||
private readonly test: IncrementalTestCollectionItem,
|
||||
private readonly collection: IMainThreadTestCollection,
|
||||
private readonly location: ModeLocation,
|
||||
private readonly location: IRichLocation,
|
||||
private readonly editor: ICodeEditor,
|
||||
stateItem: TestResultItem | undefined,
|
||||
@ITestService private readonly testService: ITestService,
|
||||
|
@ -282,7 +281,7 @@ class TestMessageDecoration implements ITestDecoration {
|
|||
constructor(
|
||||
{ message, severity }: ITestMessage,
|
||||
private readonly messageUri: URI,
|
||||
location: ModeLocation,
|
||||
location: IRichLocation,
|
||||
private readonly editor: ICodeEditor,
|
||||
@ICodeEditorService private readonly editorService: ICodeEditorService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
|
@ -33,12 +33,20 @@ export interface RunTestForProviderRequest {
|
|||
debug: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location with a fully-instantiated Range and URI.
|
||||
*/
|
||||
export interface IRichLocation {
|
||||
range: Range;
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export interface ITestMessage {
|
||||
message: string | IMarkdownString;
|
||||
severity: TestMessageSeverity | undefined;
|
||||
expectedOutput: string | undefined;
|
||||
actualOutput: string | undefined;
|
||||
location: ModeLocation | undefined;
|
||||
location: IRichLocation | undefined;
|
||||
}
|
||||
|
||||
export interface ITestState {
|
||||
|
@ -55,7 +63,7 @@ export interface ITestItem {
|
|||
extId: string;
|
||||
label: string;
|
||||
children?: never;
|
||||
location: ModeLocation | undefined;
|
||||
location: IRichLocation | undefined;
|
||||
description: string | undefined;
|
||||
runnable: boolean;
|
||||
debuggable: boolean;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
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';
|
||||
|
@ -426,9 +427,15 @@ class HydratedTestResult implements ITestResult {
|
|||
|
||||
for (const item of serialized.items) {
|
||||
const cast: TestResultItem = { ...item, retired: true, children: new Set(item.children) };
|
||||
if (cast.item.location) {
|
||||
cast.item.location.uri = URI.revive(cast.item.location.uri);
|
||||
cast.item.location.range = Range.lift(cast.item.location.range);
|
||||
}
|
||||
|
||||
for (const message of cast.state.messages) {
|
||||
if (message.location) {
|
||||
message.location.uri = URI.revive(message.location.uri);
|
||||
message.location.range = Range.lift(message.location.range);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,23 @@ export const waitForAllRoots = (collection: IMainThreadTestCollection, timeout =
|
|||
}).finally(() => listener.dispose());
|
||||
};
|
||||
|
||||
export const waitForAllTests = (collection: IMainThreadTestCollection, timeout = 3000) => {
|
||||
if (collection.busyProviders === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let listener: IDisposable;
|
||||
return new Promise<void>(resolve => {
|
||||
listener = collection.onBusyProvidersChange(count => {
|
||||
if (count === 0) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(resolve, timeout);
|
||||
}).finally(() => listener.dispose());
|
||||
};
|
||||
|
||||
export interface ITestService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onShouldSubscribe: Event<{ resource: ExtHostTestingResource, uri: URI; }>;
|
||||
|
|
Loading…
Reference in a new issue