Skip to content

Commit 9dbddfb

Browse files
committed
Add support for VS Code's test runner
Currently behind a preview flag. Fixes #3147.
1 parent 16e9fe9 commit 9dbddfb

11 files changed

Lines changed: 368 additions & 66 deletions

File tree

‎package.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,6 +2335,12 @@
23352335
"description": "EXPERIMENTAL: Whether to enable custom tracking of Flutter UI guidelines (to hide some latency of waiting for the next Flutter Outline).",
23362336
"scope": "window"
23372337
},
2338+
"dart.previewVsCodeTestRunner": {
2339+
"type": "boolean",
2340+
"default": false,
2341+
"description": "PREVIEW: Whether to enable reporting tests into the VS Code Test Runner (requires restart).",
2342+
"scope": "window"
2343+
},
23382344
"dart.evaluateToStringInDebugViews": {
23392345
"type": "boolean",
23402346
"default": true,

‎src/extension/commands/test.ts‎

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { WorkspaceContext } from "../../shared/workspace";
1515
import { DasFileTracker } from "../analysis/file_tracker_das";
1616
import { LspFileTracker } from "../analysis/file_tracker_lsp";
1717
import { isDartDocument } from "../editors";
18+
import { VsCodeTestController } from "../test/vs_test_controller";
1819
import { isInsideFlutterProject, isTestFile } from "../utils";
1920

2021
const CURSOR_IS_IN_TEST = "dart-code:cursorIsInTest";
@@ -29,14 +30,14 @@ export type SuiteList = [SuiteNode, string[]];
2930
abstract class TestCommands implements vs.Disposable {
3031
private disposables: vs.Disposable[] = [];
3132

32-
constructor(protected readonly logger: Logger, private readonly testModel: TestModel, protected readonly wsContext: WorkspaceContext, protected readonly flutterCapabilities: FlutterCapabilities) {
33+
constructor(protected readonly logger: Logger, private readonly testModel: TestModel, protected readonly wsContext: WorkspaceContext, private readonly vsCodeTestController: VsCodeTestController | undefined, protected readonly flutterCapabilities: FlutterCapabilities) {
3334
this.disposables.push(
34-
vs.commands.registerCommand("dart.startDebuggingTest", (treeNode: SuiteNode | GroupNode | TestNode, testRun?: vs.TestRun) => this.runTests(treeNode, this.getTestNames(treeNode), true, false, treeNode instanceof TestNode)),
35-
vs.commands.registerCommand("dart.startWithoutDebuggingTest", (treeNode: SuiteNode | GroupNode | TestNode, testRun?: vs.TestRun) => this.runTests(treeNode, this.getTestNames(treeNode), false, false, treeNode instanceof TestNode)),
36-
vs.commands.registerCommand("dart.startDebuggingSkippedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTests(treeNode, this.getTestNames(treeNode, TestStatus.Skipped), true, false, true)),
37-
vs.commands.registerCommand("dart.startWithoutDebuggingSkippedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTests(treeNode, this.getTestNames(treeNode, TestStatus.Skipped), false, false, true)),
38-
vs.commands.registerCommand("dart.startDebuggingFailedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTests(treeNode, this.getTestNames(treeNode, TestStatus.Failed), true, false, false)),
39-
vs.commands.registerCommand("dart.startWithoutDebuggingFailedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTests(treeNode, this.getTestNames(treeNode, TestStatus.Failed), false, false, false)),
35+
vs.commands.registerCommand("dart.startDebuggingTest", (treeNode: SuiteNode | GroupNode | TestNode, testRun?: vs.TestRun) => this.runTestsForNode(treeNode, this.getTestNames(treeNode), true, false, treeNode instanceof TestNode, undefined, testRun)),
36+
vs.commands.registerCommand("dart.startWithoutDebuggingTest", (treeNode: SuiteNode | GroupNode | TestNode, testRun?: vs.TestRun) => this.runTestsForNode(treeNode, this.getTestNames(treeNode), false, false, treeNode instanceof TestNode, undefined, testRun)),
37+
vs.commands.registerCommand("dart.startDebuggingSkippedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTestsForNode(treeNode, this.getTestNames(treeNode, TestStatus.Skipped), true, false, true)),
38+
vs.commands.registerCommand("dart.startWithoutDebuggingSkippedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTestsForNode(treeNode, this.getTestNames(treeNode, TestStatus.Skipped), false, false, true)),
39+
vs.commands.registerCommand("dart.startDebuggingFailedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTestsForNode(treeNode, this.getTestNames(treeNode, TestStatus.Failed), true, false, false)),
40+
vs.commands.registerCommand("dart.startWithoutDebuggingFailedTests", (treeNode: SuiteNode | GroupNode | TestNode) => this.runTestsForNode(treeNode, this.getTestNames(treeNode, TestStatus.Failed), false, false, false)),
4041
vs.commands.registerCommand("dart.runAllSkippedTestsWithoutDebugging", () => this.runAllSkippedTests()),
4142
vs.commands.registerCommand("dart.runAllFailedTestsWithoutDebugging", () => this.runAllFailedTests()),
4243
vs.commands.registerCommand("dart.runTestAtCursor", () => this.runTestAtCursor(false), this),
@@ -89,21 +90,33 @@ abstract class TestCommands implements vs.Disposable {
8990
break;
9091
const suiteName = path.basename(node.suiteData.path);
9192
progress.report({ message: suiteName });
92-
await this.runTests(node, failedTestNames, false, true, onlyOfType === TestStatus.Skipped, token);
93+
await this.runTestsForNode(node, failedTestNames, false, true, onlyOfType === TestStatus.Skipped, token);
9394
progress.report({ message: suiteName, increment: failedTestNames.length * percentProgressPerTest });
9495
}
9596
},
9697
);
9798
}
9899

99-
private async runTests(treeNode: GroupNode | SuiteNode | TestNode, testNames: string[] | undefined, debug: boolean, suppressPromptOnErrors: boolean, runSkippedTests: boolean, token?: vs.CancellationToken, testRun?: vs.TestRun) {
100-
// TODO: We should report these to a VS Code test run?
101-
// https://code.visualstudio.com/api/extension-guides/testing#publishonly-controllers
100+
private async runTestsForNode(treeNode: GroupNode | SuiteNode | TestNode, testNames: string[] | undefined, debug: boolean, suppressPromptOnErrors: boolean, runSkippedTests: boolean, token?: vs.CancellationToken, testRun?: vs.TestRun) {
101+
const programPath = fsPath(treeNode.suiteData.path);
102+
const isGroup = treeNode instanceof GroupNode;
103+
const canRunSkippedTest = this.flutterCapabilities.supportsRunSkippedTests || !isInsideFlutterProject(vs.Uri.file(treeNode.suiteData.path));
104+
const shouldRunSkippedTests = runSkippedTests && canRunSkippedTest;
105+
106+
return this.runTests(programPath, debug, testNames, isGroup, shouldRunSkippedTests, suppressPromptOnErrors, undefined, testRun, token);
107+
}
108+
109+
private runTests(programPath: string, debug: boolean, testNames: string[] | undefined, isGroup: boolean, shouldRunSkippedTests: boolean, suppressPromptOnErrors: boolean, launchTemplate: any | undefined, testRun: vs.TestRun | undefined, token: vs.CancellationToken | undefined) {
110+
const vsTest = this.vsCodeTestController?.controller;
111+
if (vsTest)
112+
testRun ??= vsTest.createTestRun(new vs.TestRunRequest(), undefined, true);
102113

103114
const subs: vs.Disposable[] = [];
104115
return new Promise<void>(async (resolve, reject) => {
105116
// Construct a unique ID for this session so we can track when it completes.
106117
const dartCodeDebugSessionID = `session-${getRandomInt(0x1000, 0x10000).toString(16)}`;
118+
if (testRun)
119+
this.vsCodeTestController?.registerTestRun(dartCodeDebugSessionID, testRun);
107120
if (token) {
108121
subs.push(vs.debug.onDidStartDebugSession((e) => {
109122
if (e.configuration.dartCodeDebugSessionID === dartCodeDebugSessionID)
@@ -114,27 +127,28 @@ abstract class TestCommands implements vs.Disposable {
114127
if (e.configuration.dartCodeDebugSessionID === dartCodeDebugSessionID)
115128
resolve();
116129
}));
117-
const programPath = fsPath(treeNode.suiteData.path);
118-
const canRunSkippedTest = this.flutterCapabilities.supportsRunSkippedTests || !isInsideFlutterProject(vs.Uri.file(treeNode.suiteData.path));
119-
const shouldRunSkippedTests = runSkippedTests && canRunSkippedTest;
120130
const didStart = await vs.debug.startDebugging(
121-
vs.workspace.getWorkspaceFolder(vs.Uri.file(treeNode.suiteData.path)),
131+
vs.workspace.getWorkspaceFolder(vs.Uri.file(programPath)),
122132
{
123133
dartCodeDebugSessionID,
124134
suppressPromptOnErrors,
125135
...getLaunchConfig(
126136
!debug,
127137
programPath,
128138
testNames,
129-
treeNode instanceof GroupNode,
139+
isGroup,
130140
shouldRunSkippedTests,
141+
launchTemplate,
131142
),
132143
name: `Tests ${path.basename(programPath)}`,
133144
}
134145
);
135146
if (!didStart)
136147
reject();
137-
}).finally(() => disposeAll(subs));
148+
}).finally(() => {
149+
testRun?.end();
150+
disposeAll(subs);
151+
});
138152
}
139153

140154
private getTestNames(treeNode: TreeNode, onlyOfStatus?: TestStatus): string[] | undefined {
@@ -164,10 +178,18 @@ abstract class TestCommands implements vs.Disposable {
164178

165179
private startTestFromOutline(noDebug: boolean, test: TestOutlineInfo, launchTemplate: any | undefined) {
166180
const canRunSkippedTest = !test.isGroup && (this.flutterCapabilities.supportsRunSkippedTests || !isInsideFlutterProject(vs.Uri.file(test.file)));
167-
168-
return vs.debug.startDebugging(
169-
vs.workspace.getWorkspaceFolder(vs.Uri.file(test.file)),
170-
getLaunchConfig(noDebug, test.file, [test.fullName], test.isGroup, canRunSkippedTest, launchTemplate),
181+
const shouldRunSkippedTests = canRunSkippedTest; // These are the same when running directly, since we always run skipped.
182+
183+
return this.runTests(
184+
test.file,
185+
!noDebug,
186+
[test.fullName],
187+
test.isGroup,
188+
shouldRunSkippedTests,
189+
false,
190+
launchTemplate,
191+
undefined,
192+
undefined,
171193
);
172194
}
173195

@@ -296,8 +318,8 @@ abstract class TestCommands implements vs.Disposable {
296318
}
297319

298320
export class DasTestCommands extends TestCommands {
299-
constructor(logger: Logger, testModel: TestModel, wsContext: WorkspaceContext, private readonly fileTracker: DasFileTracker, flutterCapabilities: FlutterCapabilities) {
300-
super(logger, testModel, wsContext, flutterCapabilities);
321+
constructor(logger: Logger, testModel: TestModel, wsContext: WorkspaceContext, vsCodeTestController: VsCodeTestController | undefined, private readonly fileTracker: DasFileTracker, flutterCapabilities: FlutterCapabilities) {
322+
super(logger, testModel, wsContext, vsCodeTestController, flutterCapabilities);
301323
}
302324

303325
protected testForCursor(editor: vs.TextEditor): TestOutlineInfo | undefined {
@@ -327,8 +349,8 @@ export class DasTestCommands extends TestCommands {
327349
}
328350

329351
export class LspTestCommands extends TestCommands {
330-
constructor(logger: Logger, testModel: TestModel, wsContext: WorkspaceContext, private readonly fileTracker: LspFileTracker, flutterCapabilities: FlutterCapabilities) {
331-
super(logger, testModel, wsContext, flutterCapabilities);
352+
constructor(logger: Logger, testModel: TestModel, wsContext: WorkspaceContext, vsCodeTestController: VsCodeTestController | undefined, private readonly fileTracker: LspFileTracker, flutterCapabilities: FlutterCapabilities) {
353+
super(logger, testModel, wsContext, vsCodeTestController, flutterCapabilities);
332354
}
333355

334356
protected testForCursor(editor: vs.TextEditor): LspTestOutlineInfo | undefined {

‎src/extension/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class Config {
115115
get previewFlutterUiGuidesCustomTracking(): boolean { return this.getConfig<boolean>("previewFlutterUiGuidesCustomTracking", false); }
116116
get previewHotReloadOnSaveWatcher(): boolean { return this.getConfig<boolean>("previewHotReloadOnSaveWatcher", false); }
117117
get previewLsp(): undefined | boolean { return this.getConfig<null | boolean>("previewLsp", null); }
118+
get previewVsCodeTestRunner(): boolean { return this.getConfig<boolean>("previewVsCodeTestRunner", false); }
118119
get promptToRunIfErrors(): boolean { return this.getConfig<boolean>("promptToRunIfErrors", true); }
119120
get pubTestLogFile(): undefined | string { return createFolderForFile(resolvePaths(this.getConfig<null | string>("pubTestLogFile", null))); }
120121
get sdkPath(): undefined | string { return resolvePaths(this.getConfig<null | string>("sdkPath", null)); }

‎src/extension/extension.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import { StatusBarVersionTracker } from "./sdk/status_bar_version_tracker";
100100
import { checkForStandardDartSdkUpdates } from "./sdk/update_check";
101101
import { SdkUtils } from "./sdk/utils";
102102
import { DartTerminalLinkProvider } from "./terminal/link_provider";
103+
import { VsCodeTestController } from "./test/vs_test_controller";
103104
import { handleNewProjects, showUserPrompts } from "./user_prompts";
104105
import * as util from "./utils";
105106
import { addToLogHeader, clearLogHeader, getExtensionLogPath, getLogHeader } from "./utils/log";
@@ -485,6 +486,9 @@ export async function activate(context: vs.ExtensionContext, isRestart: boolean
485486
const testModel = new TestModel(config, util.isPathInsideFlutterProject);
486487
const testCoordinator = new TestSessionCoordinator(logger, testModel);
487488
context.subscriptions.push(testCoordinator);
489+
const vsCodeTestController = config.previewVsCodeTestRunner ? new VsCodeTestController(logger, testModel) : undefined;
490+
if (vsCodeTestController)
491+
context.subscriptions.push(vsCodeTestController);
488492

489493
const analyzerCommands = new AnalyzerCommands(context, logger, analyzer, analytics);
490494

@@ -569,9 +573,9 @@ export async function activate(context: vs.ExtensionContext, isRestart: boolean
569573
context.subscriptions.push(new LoggingCommands(logger, context.logPath));
570574
context.subscriptions.push(new OpenInOtherEditorCommands(logger, sdks));
571575
if (dasAnalyzer)
572-
context.subscriptions.push(new DasTestCommands(logger, testModel, workspaceContext, dasAnalyzer.fileTracker, flutterCapabilities));
576+
context.subscriptions.push(new DasTestCommands(logger, testModel, workspaceContext, vsCodeTestController, dasAnalyzer.fileTracker, flutterCapabilities));
573577
if (lspAnalyzer)
574-
context.subscriptions.push(new LspTestCommands(logger, testModel, workspaceContext, lspAnalyzer.fileTracker, flutterCapabilities));
578+
context.subscriptions.push(new LspTestCommands(logger, testModel, workspaceContext, vsCodeTestController, lspAnalyzer.fileTracker, flutterCapabilities));
575579

576580
if (lspClient && lspAnalyzer) {
577581
// TODO: LSP equivs of the others...

‎src/extension/flutter/flutter_outline_view.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export abstract class FlutterOutlineProvider implements vs.TreeDataProvider<Flut
5656

5757
protected abstract loadExistingOutline(): Promise<void>;
5858

59-
public async setContexts(selection: FlutterWidgetItem[]) {
59+
public async setContexts(selection: FlutterWidgetItem[] | undefined) {
6060
// Unmark the old node as being selected.
6161
if (this.lastSelectedWidget) {
6262
this.lastSelectedWidget.contextValue = undefined;

0 commit comments

Comments
 (0)