Skip to content

Commit e79bcf8

Browse files
nirhaasCr4zySheep
authored andcommitted
src/goTest: fix multifile suite test fails to debug
1 parent dbc9084 commit e79bcf8

File tree

3 files changed

+98
-17
lines changed

3 files changed

+98
-17
lines changed

extension/src/goTest.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
getBenchmarkFunctions,
1919
getTestFlags,
2020
getTestFunctionDebugArgs,
21+
getSuiteToTestMap,
2122
getTestFunctions,
2223
getTestTags,
2324
goTest,
24-
TestConfig
25+
TestConfig,
26+
SuiteToTestMap
2527
} from './testUtils';
2628

2729
// lastTestConfig holds a reference to the last executed TestConfig which allows
@@ -54,6 +56,7 @@ async function _testAtCursor(
5456

5557
const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
5658
const testFunctions = (await getFunctions(goCtx, editor.document)) ?? [];
59+
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
5760
// We use functionName if it was provided as argument
5861
// Otherwise find any test function containing the cursor.
5962
const testFunctionName =
@@ -67,9 +70,9 @@ async function _testAtCursor(
6770
await editor.document.save();
6871

6972
if (cmd === 'debug') {
70-
return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
73+
return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
7174
} else if (cmd === 'benchmark' || cmd === 'test') {
72-
return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
75+
return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
7376
} else {
7477
throw new Error(`Unsupported command: ${cmd}`);
7578
}
@@ -93,6 +96,7 @@ async function _subTestAtCursor(
9396

9497
await editor.document.save();
9598
const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
99+
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
96100
// We use functionName if it was provided as argument
97101
// Otherwise find any test function containing the cursor.
98102
const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
@@ -142,9 +146,9 @@ async function _subTestAtCursor(
142146
const escapedName = escapeSubTestName(testFunctionName, subTestName);
143147

144148
if (cmd === 'debug') {
145-
return debugTestAtCursor(editor, escapedName, testFunctions, goConfig);
149+
return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig);
146150
} else if (cmd === 'test') {
147-
return runTestAtCursor(editor, escapedName, testFunctions, goConfig, cmd, args);
151+
return runTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, cmd, args);
148152
} else {
149153
throw new Error(`Unsupported command: ${cmd}`);
150154
}
@@ -202,13 +206,14 @@ async function runTestAtCursor(
202206
editor: vscode.TextEditor,
203207
testFunctionName: string,
204208
testFunctions: vscode.DocumentSymbol[],
209+
suiteToTest: SuiteToTestMap,
205210
goConfig: vscode.WorkspaceConfiguration,
206211
cmd: TestAtCursorCmd,
207212
args: any
208213
) {
209214
const testConfigFns = [testFunctionName];
210215
if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
211-
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions).map((t) => t.name));
216+
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
212217
}
213218

214219
const isMod = await isModSupported(editor.document.uri);
@@ -259,11 +264,12 @@ export async function debugTestAtCursor(
259264
editorOrDocument: vscode.TextEditor | vscode.TextDocument,
260265
testFunctionName: string,
261266
testFunctions: vscode.DocumentSymbol[],
267+
suiteToFunc: SuiteToTestMap,
262268
goConfig: vscode.WorkspaceConfiguration,
263269
sessionID?: string
264270
) {
265271
const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
266-
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
272+
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
267273
const tags = getTestTags(goConfig);
268274
const buildFlags = tags ? ['-tags', tags] : [];
269275
const flagsFromConfig = getTestFlags(goConfig);

extension/src/goTest/run.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ import vscode = require('vscode');
2121
import { outputChannel } from '../goStatus';
2222
import { isModSupported } from '../goModules';
2323
import { getGoConfig } from '../config';
24-
import { getBenchmarkFunctions, getTestFlags, getTestFunctions, goTest, GoTestOutput } from '../testUtils';
24+
import {
25+
getBenchmarkFunctions,
26+
getTestFlags,
27+
getSuiteToTestMap,
28+
getTestFunctions,
29+
goTest,
30+
GoTestOutput
31+
} from '../testUtils';
2532
import { GoTestResolver } from './resolve';
2633
import { dispose, forEachAsync, GoTest, Workspace } from './utils';
2734
import { GoTestProfiler, ProfilingOptions } from './profile';
@@ -163,6 +170,7 @@ export class GoTestRunner {
163170
const goConfig = getGoConfig(test.uri);
164171
const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
165172
const testFunctions = await getFunctions(this.goCtx, doc, token);
173+
const suiteToTest = await getSuiteToTestMap(this.goCtx, doc, token);
166174

167175
// TODO Can we get output from the debug session, in order to check for
168176
// run/pass/fail events?
@@ -191,7 +199,8 @@ export class GoTestRunner {
191199

192200
const run = this.ctrl.createTestRun(request, `Debug ${name}`);
193201
if (!testFunctions) return;
194-
const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, goConfig, id);
202+
const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, suiteToTest, goConfig, id);
203+
195204
if (!started) {
196205
subs.forEach((s) => s.dispose());
197206
run.end();

extension/src/testUtils.ts

+74-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import cp = require('child_process');
1111
import path = require('path');
1212
import util = require('util');
1313
import vscode = require('vscode');
14+
import { promises as fs } from 'fs';
1415

1516
import { applyCodeCoverageToAllEditors } from './goCover';
1617
import { toolExecutionEnvironment } from './goEnv';
@@ -50,6 +51,7 @@ const testMethodRegex = /^\(([^)]+)\)\.(Test|Test\P{Ll}.*)$/u;
5051
const benchmarkRegex = /^Benchmark$|^Benchmark\P{Ll}.*/u;
5152
const fuzzFuncRegx = /^Fuzz$|^Fuzz\P{Ll}.*/u;
5253
const testMainRegex = /TestMain\(.*\*testing.M\)/;
54+
const runTestSuiteRegex = /^\s*suite\.Run\(\w+,\s*(?:&?(?<type1>\w+)\{|new\((?<type2>\w+)\))/mu;
5355

5456
/**
5557
* Input to goTest.
@@ -164,7 +166,7 @@ export async function getTestFunctions(
164166
}
165167
const children = symbol.children;
166168

167-
// With gopls dymbol provider symbols, the symbols have the imports of all
169+
// With gopls symbol provider, the symbols have the imports of all
168170
// the package, so suite tests from all files will be found.
169171
const testify = importsTestify(symbols);
170172
return children.filter(
@@ -199,14 +201,15 @@ export function extractInstanceTestName(symbolName: string): string {
199201
export function getTestFunctionDebugArgs(
200202
document: vscode.TextDocument,
201203
testFunctionName: string,
202-
testFunctions: vscode.DocumentSymbol[]
204+
testFunctions: vscode.DocumentSymbol[],
205+
suiteToFunc: SuiteToTestMap
203206
): string[] {
204207
if (benchmarkRegex.test(testFunctionName)) {
205208
return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
206209
}
207210
const instanceMethod = extractInstanceTestName(testFunctionName);
208211
if (instanceMethod) {
209-
const testFns = findAllTestSuiteRuns(document, testFunctions);
212+
const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc);
210213
const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`];
211214
const testSuiteTests = ['-testify.m', `^${instanceMethod}$`];
212215
return [...testSuiteRuns, ...testSuiteTests];
@@ -222,12 +225,22 @@ export function getTestFunctionDebugArgs(
222225
*/
223226
export function findAllTestSuiteRuns(
224227
doc: vscode.TextDocument,
225-
allTests: vscode.DocumentSymbol[]
228+
allTests: vscode.DocumentSymbol[],
229+
suiteToFunc: SuiteToTestMap
226230
): vscode.DocumentSymbol[] {
227-
// get non-instance test functions
228-
const testFunctions = allTests?.filter((t) => !testMethodRegex.test(t.name));
229-
// filter further to ones containing suite.Run()
230-
return testFunctions?.filter((t) => doc.getText(t.range).includes('suite.Run(')) ?? [];
231+
const suites = allTests
232+
// Find all tests with receivers.
233+
?.map((e) => e.name.match(testMethodRegex))
234+
.filter((e) => e?.length === 3)
235+
// Take out receiever, strip leading *.
236+
.map((e) => e && e[1].replace(/^\*/g, ''))
237+
// Map receiver name to test that runs "suite.Run".
238+
.map((e) => e && suiteToFunc[e])
239+
// Filter out empty results.
240+
.filter((e): e is vscode.DocumentSymbol => !!e);
241+
242+
// Dedup.
243+
return [...new Set(suites)];
231244
}
232245

233246
/**
@@ -254,6 +267,59 @@ export async function getBenchmarkFunctions(
254267
return children.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name));
255268
}
256269

270+
export type SuiteToTestMap = Record<string, vscode.DocumentSymbol>;
271+
272+
/**
273+
* Returns a mapping between a package's function receivers to
274+
* the test method that initiated them with "suite.Run".
275+
*
276+
* @param the URI of a Go source file.
277+
* @return function symbols from all source files of the package, mapped by target suite names.
278+
*/
279+
export async function getSuiteToTestMap(
280+
goCtx: GoExtensionContext,
281+
doc: vscode.TextDocument,
282+
token?: vscode.CancellationToken
283+
) {
284+
// Get all the package documents.
285+
const packageDir = path.parse(doc.fileName).dir;
286+
const packageContent = await fs.readdir(packageDir, { withFileTypes: true });
287+
const packageFilenames = packageContent
288+
// Only go files.
289+
.filter((dirent) => dirent.isFile())
290+
.map((dirent) => dirent.name)
291+
.filter((name) => name.endsWith('.go'));
292+
const packageDocs = await Promise.all(
293+
packageFilenames.map((e) => path.join(packageDir, e)).map(vscode.workspace.openTextDocument)
294+
);
295+
296+
const suiteToTest: SuiteToTestMap = {};
297+
for (const packageDoc of packageDocs) {
298+
const funcs = await getTestFunctions(goCtx, packageDoc, token);
299+
if (!funcs) {
300+
continue;
301+
}
302+
303+
for (const func of funcs) {
304+
const funcText = packageDoc.getText(func.range);
305+
306+
// Matches run suites of the types:
307+
// type1: suite.Run(t, MySuite{
308+
// type1: suite.Run(t, &MySuite{
309+
// type2: suite.Run(t, new(MySuite)
310+
const matchRunSuite = funcText.match(runTestSuiteRegex);
311+
if (!matchRunSuite) {
312+
continue;
313+
}
314+
315+
const g = matchRunSuite.groups;
316+
suiteToTest[g?.type1 || g?.type2 || ''] = func;
317+
}
318+
}
319+
320+
return suiteToTest;
321+
}
322+
257323
/**
258324
* go test -json output format.
259325
* which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format

0 commit comments

Comments
 (0)