src/goTest: add single-test debugging
Change-Id: I2dac932cfe0854d57157353d5de6eae89158d87d
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/348571
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/src/goTest.ts b/src/goTest.ts
index 748ff8c..09875ad 100644
--- a/src/goTest.ts
+++ b/src/goTest.ts
@@ -207,14 +207,23 @@
/**
* Debugs the test at cursor.
+ * @param editorOrDocument The text document (or editor) that defines the test.
+ * @param testFunctionName The name of the test function.
+ * @param testFunctions All test function symbols defined by the document.
+ * @param goConfig Go configuration, i.e. flags, tags, environment, etc.
+ * @param sessionID If specified, `sessionID` is added to the debug
+ * configuration and can be used to identify the debug session.
+ * @returns Whether the debug session was successfully started.
*/
-async function debugTestAtCursor(
- editor: vscode.TextEditor,
+export async function debugTestAtCursor(
+ editorOrDocument: vscode.TextEditor | vscode.TextDocument,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[],
- goConfig: vscode.WorkspaceConfiguration
+ goConfig: vscode.WorkspaceConfiguration,
+ sessionID?: string
) {
- const args = getTestFunctionDebugArgs(editor.document, testFunctionName, testFunctions);
+ const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
+ const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
const tags = getTestTags(goConfig);
const buildFlags = tags ? ['-tags', tags] : [];
const flagsFromConfig = getTestFlags(goConfig);
@@ -230,17 +239,18 @@
}
buildFlags.push(x);
});
- const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri);
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(doc.uri);
const debugConfig: vscode.DebugConfiguration = {
name: 'Debug Test',
type: 'go',
request: 'launch',
mode: 'test',
- program: path.dirname(editor.document.fileName),
+ program: path.dirname(doc.fileName),
env: goConfig.get('testEnvVars', {}),
envFile: goConfig.get('testEnvFile'),
args,
- buildFlags: buildFlags.join(' ')
+ buildFlags: buildFlags.join(' '),
+ sessionID
};
lastDebugConfig = debugConfig;
lastDebugWorkspaceFolder = workspaceFolder;
diff --git a/src/goTest/run.ts b/src/goTest/run.ts
index a15b66d..a004be2 100644
--- a/src/goTest/run.ts
+++ b/src/goTest/run.ts
@@ -4,6 +4,7 @@
*--------------------------------------------------------*/
import {
CancellationToken,
+ DebugSession,
Location,
OutputChannel,
Position,
@@ -20,10 +21,13 @@
import { outputChannel } from '../goStatus';
import { isModSupported } from '../goModules';
import { getGoConfig } from '../config';
-import { getTestFlags, goTest, GoTestOutput } from '../testUtils';
+import { getBenchmarkFunctions, getTestFlags, getTestFunctions, goTest, GoTestOutput } from '../testUtils';
import { GoTestResolver } from './resolve';
import { dispose, forEachAsync, GoTest, Workspace } from './utils';
import { GoTestProfiler, ProfilingOptions } from './profile';
+import { debugTestAtCursor } from '../goTest';
+
+let debugSessionID = 0;
type CollectedTest = { item: TestItem; explicitlyIncluded?: boolean };
@@ -90,6 +94,21 @@
true
);
+ ctrl.createRunProfile(
+ 'Go (Debug)',
+ TestRunProfileKind.Debug,
+ async (request, token) => {
+ try {
+ await this.debug(request, token);
+ } catch (error) {
+ const m = 'Failed to debug tests';
+ outputChannel.appendLine(`${m}: ${error}`);
+ await vscode.window.showErrorMessage(m);
+ }
+ },
+ true
+ );
+
const pprof = ctrl.createRunProfile(
'Go (Profile)',
TestRunProfileKind.Run,
@@ -112,6 +131,91 @@
};
}
+ async debug(request: TestRunRequest, token?: CancellationToken) {
+ if (!request.include) {
+ await vscode.window.showErrorMessage('The Go test explorer does not support debugging multiple tests');
+ return;
+ }
+
+ const collected = new Map<TestItem, CollectedTest[]>();
+ const files = new Set<TestItem>();
+ for (const item of request.include) {
+ await this.collectTests(item, true, request.exclude || [], collected, files);
+ }
+
+ const tests = Array.from(collected.values()).reduce((a, b) => a.concat(b), []);
+ if (tests.length > 1) {
+ await vscode.window.showErrorMessage('The Go test explorer does not support debugging multiple tests');
+ return;
+ }
+
+ const test = tests[0].item;
+ const { kind, name } = GoTest.parseId(test.id);
+ const doc = await vscode.workspace.openTextDocument(test.uri);
+ await doc.save();
+
+ const goConfig = getGoConfig(test.uri);
+ const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
+ const testFunctions = await getFunctions(doc, token);
+
+ // TODO Can we get output from the debug session, in order to check for
+ // run/pass/fail events?
+
+ const id = `debug #${debugSessionID++} ${name}`;
+ const subs: vscode.Disposable[] = [];
+ const sessionPromise = new Promise<DebugSession>((resolve) => {
+ subs.push(
+ vscode.debug.onDidStartDebugSession((s) => {
+ if (s.configuration.sessionID === id) {
+ resolve(s);
+ subs.forEach((s) => s.dispose());
+ }
+ })
+ );
+
+ if (token) {
+ subs.push(
+ token.onCancellationRequested(() => {
+ resolve(null);
+ subs.forEach((s) => s.dispose());
+ })
+ );
+ }
+ });
+
+ const run = this.ctrl.createTestRun(request, `Debug ${name}`);
+ const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);
+ if (!started) {
+ subs.forEach((s) => s.dispose());
+ run.end();
+ return;
+ }
+
+ const session = await sessionPromise;
+ if (!session) {
+ run.end();
+ return;
+ }
+
+ token.onCancellationRequested(() => vscode.debug.stopDebugging(session));
+
+ await new Promise<void>((resolve) => {
+ const sub = vscode.debug.onDidTerminateDebugSession(didTerminateSession);
+
+ token?.onCancellationRequested(() => {
+ resolve();
+ sub.dispose();
+ });
+
+ function didTerminateSession(s: DebugSession) {
+ if (s.id !== session.id) return;
+ resolve();
+ sub.dispose();
+ }
+ });
+ run.end();
+ }
+
// Execute tests - TestController.runTest callback
async run(request: TestRunRequest, token?: CancellationToken, options: ProfilingOptions = {}): Promise<boolean> {
const collected = new Map<TestItem, CollectedTest[]>();