Skip to content

Commit

Permalink
testing: commands to run tests at current cursor and in file
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 committed Feb 13, 2021
1 parent 07e3bcf commit a0e0324
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 28 deletions.
3 changes: 3 additions & 0 deletions src/vs/workbench/api/browser/mainThreadTesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})) ?? [],
};
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
});
}

Expand Down
173 changes: 169 additions & 4 deletions src/vs/workbench/contrib/testing/browser/testExplorerActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand All @@ -103,6 +104,7 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
viewId: Testing.ExplorerViewId,
f1: true,
category,
precondition: FocusedViewContext.isEqualTo(Testing.ExplorerViewId),
});
}

Expand Down Expand Up @@ -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(
Expand All @@ -162,7 +164,7 @@ export class RunSelectedAction extends RunOrDebugAction {
}
}

export class DebugSelectedAction extends RunOrDebugAction {
export class DebugSelectedAction extends RunOrDebugSelectedAction {
constructor() {
super(
'testing.debugSelected',
Expand Down Expand Up @@ -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 })) });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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) => ({
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions src/vs/workbench/contrib/testing/common/testCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit a0e0324

Please sign in to comment.