Skip to content

Commit 09deeca

Browse files
committed
src/goTest: fix multifile suite test fails to debug
1 parent 073136d commit 09deeca

File tree

3 files changed

+94
-16
lines changed

3 files changed

+94
-16
lines changed

src/goTest.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
getBenchmarkFunctions,
1818
getTestFlags,
1919
getTestFunctionDebugArgs,
20+
getSuiteToTestMap,
2021
getTestFunctions,
2122
getTestTags,
2223
goTest,
23-
TestConfig
24+
TestConfig,
25+
SuiteToTestMap
2426
} from './testUtils';
2527

2628
// lastTestConfig holds a reference to the last executed TestConfig which allows
@@ -52,6 +54,7 @@ async function _testAtCursor(
5254

5355
const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
5456
const testFunctions = (await getFunctions(goCtx, editor.document)) ?? [];
57+
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
5558
// We use functionName if it was provided as argument
5659
// Otherwise find any test function containing the cursor.
5760
const testFunctionName =
@@ -65,9 +68,9 @@ async function _testAtCursor(
6568
await editor.document.save();
6669

6770
if (cmd === 'debug') {
68-
return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
71+
return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
6972
} else if (cmd === 'benchmark' || cmd === 'test') {
70-
return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
73+
return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
7174
} else {
7275
throw new Error(`Unsupported command: ${cmd}`);
7376
}
@@ -125,13 +128,14 @@ async function runTestAtCursor(
125128
editor: vscode.TextEditor,
126129
testFunctionName: string,
127130
testFunctions: vscode.DocumentSymbol[],
131+
suiteToTest: SuiteToTestMap,
128132
goConfig: vscode.WorkspaceConfiguration,
129133
cmd: TestAtCursorCmd,
130134
args: any
131135
) {
132136
const testConfigFns = [testFunctionName];
133137
if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
134-
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions).map((t) => t.name));
138+
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
135139
}
136140

137141
const isMod = await isModSupported(editor.document.uri);
@@ -169,6 +173,7 @@ export const subTestAtCursor: CommandFactory = (ctx, goCtx) => {
169173
await editor.document.save();
170174
try {
171175
const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
176+
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
172177
// We use functionName if it was provided as argument
173178
// Otherwise find any test function containing the cursor.
174179
const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
@@ -214,7 +219,7 @@ export const subTestAtCursor: CommandFactory = (ctx, goCtx) => {
214219

215220
const subTestName = testFunctionName + '/' + subtest;
216221

217-
return await runTestAtCursor(editor, subTestName, testFunctions, goConfig, 'test', args);
222+
return await runTestAtCursor(editor, subTestName, testFunctions, suiteToTest, goConfig, 'test', args);
218223
} catch (err) {
219224
vscode.window.showInformationMessage('Unable to run subtest: ' + (err as any).toString());
220225
console.error(err);
@@ -236,11 +241,12 @@ export async function debugTestAtCursor(
236241
editorOrDocument: vscode.TextEditor | vscode.TextDocument,
237242
testFunctionName: string,
238243
testFunctions: vscode.DocumentSymbol[],
244+
suiteToFunc: SuiteToTestMap,
239245
goConfig: vscode.WorkspaceConfiguration,
240246
sessionID?: string
241247
) {
242248
const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
243-
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
249+
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
244250
const tags = getTestTags(goConfig);
245251
const buildFlags = tags ? ['-tags', tags] : [];
246252
const flagsFromConfig = getTestFlags(goConfig);

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';
@@ -161,6 +168,7 @@ export class GoTestRunner {
161168
const goConfig = getGoConfig(test.uri);
162169
const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
163170
const testFunctions = await getFunctions(this.goCtx, doc, token);
171+
const suiteToTest = await getSuiteToTestMap(this.goCtx, doc, token);
164172

165173
// TODO Can we get output from the debug session, in order to check for
166174
// run/pass/fail events?
@@ -189,7 +197,8 @@ export class GoTestRunner {
189197

190198
const run = this.ctrl.createTestRun(request, `Debug ${name}`);
191199
if (!testFunctions) return;
192-
const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);
200+
201+
const started = await debugTestAtCursor(doc, name, testFunctions, suiteToTest, goConfig, id);
193202
if (!started) {
194203
subs.forEach((s) => s.dispose());
195204
run.end();

src/testUtils.ts

+71-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 fs = require('fs');
1415

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

4951
/**
5052
* Input to goTest.
@@ -159,7 +161,7 @@ export async function getTestFunctions(
159161
}
160162
const children = symbol.children;
161163

162-
// With gopls dymbol provider symbols, the symbols have the imports of all
164+
// With gopls symbol provider, the symbols have the imports of all
163165
// the package, so suite tests from all files will be found.
164166
const testify = importsTestify(symbols);
165167
return children.filter(
@@ -194,14 +196,15 @@ export function extractInstanceTestName(symbolName: string): string {
194196
export function getTestFunctionDebugArgs(
195197
document: vscode.TextDocument,
196198
testFunctionName: string,
197-
testFunctions: vscode.DocumentSymbol[]
199+
testFunctions: vscode.DocumentSymbol[],
200+
suiteToFunc: SuiteToTestMap
198201
): string[] {
199202
if (benchmarkRegex.test(testFunctionName)) {
200203
return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
201204
}
202205
const instanceMethod = extractInstanceTestName(testFunctionName);
203206
if (instanceMethod) {
204-
const testFns = findAllTestSuiteRuns(document, testFunctions);
207+
const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc);
205208
const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`];
206209
const testSuiteTests = ['-testify.m', `^${instanceMethod}$`];
207210
return [...testSuiteRuns, ...testSuiteTests];
@@ -217,12 +220,22 @@ export function getTestFunctionDebugArgs(
217220
*/
218221
export function findAllTestSuiteRuns(
219222
doc: vscode.TextDocument,
220-
allTests: vscode.DocumentSymbol[]
223+
allTests: vscode.DocumentSymbol[],
224+
suiteToFunc: SuiteToTestMap
221225
): vscode.DocumentSymbol[] {
222-
// get non-instance test functions
223-
const testFunctions = allTests?.filter((t) => !testMethodRegex.test(t.name));
224-
// filter further to ones containing suite.Run()
225-
return testFunctions?.filter((t) => doc.getText(t.range).includes('suite.Run(')) ?? [];
226+
const suites = allTests
227+
// Find all tests with receivers.
228+
?.map((e) => e.name.match(testMethodRegex))
229+
.filter((e) => e?.length === 3)
230+
// Take out receiever, strip leading *.
231+
.map((e) => e && e[1].replace(/^\*/g, ''))
232+
// Map receiver name to test that runs "suite.Run".
233+
.map((e) => e && suiteToFunc[e])
234+
// Filter out empty results.
235+
.filter((e): e is vscode.DocumentSymbol => !!e);
236+
237+
// Dedup.
238+
return [...new Set(suites)];
226239
}
227240

228241
/**
@@ -249,6 +262,56 @@ export async function getBenchmarkFunctions(
249262
return children.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name));
250263
}
251264

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

0 commit comments

Comments
 (0)