Skip to content

Commit 7bfbcaf

Browse files
Cr4zySheepgopherbot
authored andcommitted
src/goTest: fix multifile suite test fails to debug
I have resumed the pull request #2415 and added the missing tests. Here is the original description: Collect a packages suites and maps their name with the caller function. This mapping is used to fix a bug where vscode-go was formatting wrong arguments for dlv (-test.run). Fixes #2414 Change-Id: Id6ac5d153fa1dbcdb7591b2bd0ee78bfa95686c6 GitHub-Last-Rev: 0fa9525 GitHub-Pull-Request: #3128 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/555676 Reviewed-by: Than McIntosh <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]> Auto-Submit: Hyang-Ah Hana Kim <[email protected]> Commit-Queue: Hyang-Ah Hana Kim <[email protected]>
1 parent 5b0d6db commit 7bfbcaf

File tree

8 files changed

+325
-35
lines changed

8 files changed

+325
-35
lines changed

extension/src/goTest.ts

+19-12
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
getBenchmarkFunctions,
1919
getTestFlags,
2020
getTestFunctionDebugArgs,
21-
getTestFunctions,
21+
getTestFunctionsAndTestSuite,
2222
getTestTags,
2323
goTest,
24-
TestConfig
24+
TestConfig,
25+
SuiteToTestMap,
26+
getTestFunctions
2527
} from './testUtils';
2628

2729
// lastTestConfig holds a reference to the last executed TestConfig which allows
@@ -52,8 +54,11 @@ async function _testAtCursor(
5254
throw new NotFoundError('No tests found. Current file is not a test file.');
5355
}
5456

55-
const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
56-
const testFunctions = (await getFunctions(goCtx, editor.document)) ?? [];
57+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
58+
cmd === 'benchmark',
59+
goCtx,
60+
editor.document
61+
);
5762
// We use functionName if it was provided as argument
5863
// Otherwise find any test function containing the cursor.
5964
const testFunctionName =
@@ -67,9 +72,9 @@ async function _testAtCursor(
6772
await editor.document.save();
6873

6974
if (cmd === 'debug') {
70-
return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
75+
return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
7176
} else if (cmd === 'benchmark' || cmd === 'test') {
72-
return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
77+
return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
7378
} else {
7479
throw new Error(`Unsupported command: ${cmd}`);
7580
}
@@ -92,7 +97,7 @@ async function _subTestAtCursor(
9297
}
9398

9499
await editor.document.save();
95-
const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
100+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(false, goCtx, editor.document);
96101
// We use functionName if it was provided as argument
97102
// Otherwise find any test function containing the cursor.
98103
const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
@@ -142,9 +147,9 @@ async function _subTestAtCursor(
142147
const escapedName = escapeSubTestName(testFunctionName, subTestName);
143148

144149
if (cmd === 'debug') {
145-
return debugTestAtCursor(editor, escapedName, testFunctions, goConfig);
150+
return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig);
146151
} else if (cmd === 'test') {
147-
return runTestAtCursor(editor, escapedName, testFunctions, goConfig, cmd, args);
152+
return runTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, cmd, args);
148153
} else {
149154
throw new Error(`Unsupported command: ${cmd}`);
150155
}
@@ -160,7 +165,7 @@ async function _subTestAtCursor(
160165
export function testAtCursor(cmd: TestAtCursorCmd): CommandFactory {
161166
return (ctx, goCtx) => (args: any) => {
162167
const goConfig = getGoConfig();
163-
_testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
168+
return _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
164169
if (err instanceof NotFoundError) {
165170
vscode.window.showInformationMessage(err.message);
166171
} else {
@@ -202,13 +207,14 @@ async function runTestAtCursor(
202207
editor: vscode.TextEditor,
203208
testFunctionName: string,
204209
testFunctions: vscode.DocumentSymbol[],
210+
suiteToTest: SuiteToTestMap,
205211
goConfig: vscode.WorkspaceConfiguration,
206212
cmd: TestAtCursorCmd,
207213
args: any
208214
) {
209215
const testConfigFns = [testFunctionName];
210216
if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
211-
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions).map((t) => t.name));
217+
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
212218
}
213219

214220
const isMod = await isModSupported(editor.document.uri);
@@ -259,11 +265,12 @@ export async function debugTestAtCursor(
259265
editorOrDocument: vscode.TextEditor | vscode.TextDocument,
260266
testFunctionName: string,
261267
testFunctions: vscode.DocumentSymbol[],
268+
suiteToFunc: SuiteToTestMap,
262269
goConfig: vscode.WorkspaceConfiguration,
263270
sessionID?: string
264271
) {
265272
const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
266-
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
273+
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
267274
const tags = getTestTags(goConfig);
268275
const buildFlags = tags ? ['-tags', tags] : [];
269276
const flagsFromConfig = getTestFlags(goConfig);

extension/src/goTest/run.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ 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 { getTestFlags, getTestFunctionsAndTestSuite, goTest, GoTestOutput } from '../testUtils';
2525
import { GoTestResolver } from './resolve';
2626
import { dispose, forEachAsync, GoTest, Workspace } from './utils';
2727
import { GoTestProfiler, ProfilingOptions } from './profile';
@@ -161,8 +161,11 @@ export class GoTestRunner {
161161
await doc.save();
162162

163163
const goConfig = getGoConfig(test.uri);
164-
const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
165-
const testFunctions = await getFunctions(this.goCtx, doc, token);
164+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
165+
kind === 'benchmark',
166+
this.goCtx,
167+
doc
168+
);
166169

167170
// TODO Can we get output from the debug session, in order to check for
168171
// run/pass/fail events?
@@ -191,7 +194,8 @@ export class GoTestRunner {
191194

192195
const run = this.ctrl.createTestRun(request, `Debug ${name}`);
193196
if (!testFunctions) return;
194-
const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, goConfig, id);
197+
const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, suiteToTest, goConfig, id);
198+
195199
if (!started) {
196200
subs.forEach((s) => s.dispose());
197201
run.end();

extension/src/testUtils.ts

+129-16
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.
@@ -153,27 +155,76 @@ export async function getTestFunctions(
153155
doc: vscode.TextDocument,
154156
token?: vscode.CancellationToken
155157
): Promise<vscode.DocumentSymbol[] | undefined> {
158+
const result = await getTestFunctionsAndTestifyHint(goCtx, doc, token);
159+
return result.testFunctions;
160+
}
161+
162+
/**
163+
* Returns all Go unit test functions in the given source file and an hint if testify is used.
164+
*
165+
* @param doc A Go source file
166+
*/
167+
export async function getTestFunctionsAndTestifyHint(
168+
goCtx: GoExtensionContext,
169+
doc: vscode.TextDocument,
170+
token?: vscode.CancellationToken
171+
): Promise<{ testFunctions?: vscode.DocumentSymbol[]; foundTestifyTestFunction?: boolean }> {
156172
const documentSymbolProvider = GoDocumentSymbolProvider(goCtx, true);
157173
const symbols = await documentSymbolProvider.provideDocumentSymbols(doc);
158174
if (!symbols || symbols.length === 0) {
159-
return;
175+
return {};
160176
}
161177
const symbol = symbols[0];
162178
if (!symbol) {
163-
return;
179+
return {};
164180
}
165181
const children = symbol.children;
166182

167-
// With gopls dymbol provider symbols, the symbols have the imports of all
183+
// With gopls symbol provider, the symbols have the imports of all
168184
// the package, so suite tests from all files will be found.
169185
const testify = importsTestify(symbols);
170-
return children.filter(
186+
187+
const allTestFunctions = children.filter(
171188
(sym) =>
172-
(sym.kind === vscode.SymbolKind.Function || sym.kind === vscode.SymbolKind.Method) &&
189+
sym.kind === vscode.SymbolKind.Function &&
173190
// Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482
174191
!testMainRegex.test(doc.lineAt(sym.range.start.line).text) &&
175-
(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name) || (testify && testMethodRegex.test(sym.name)))
192+
(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name))
176193
);
194+
195+
const allTestMethods = testify
196+
? children.filter((sym) => sym.kind === vscode.SymbolKind.Method && testMethodRegex.test(sym.name))
197+
: [];
198+
199+
return {
200+
testFunctions: allTestFunctions.concat(allTestMethods),
201+
foundTestifyTestFunction: allTestMethods.length > 0
202+
};
203+
}
204+
205+
/**
206+
* Returns all the Go test functions (or benchmark) from the given Go source file, and the associated test suites when testify is used.
207+
*
208+
* @param doc A Go source file
209+
*/
210+
export async function getTestFunctionsAndTestSuite(
211+
isBenchmark: boolean,
212+
goCtx: GoExtensionContext,
213+
doc: vscode.TextDocument
214+
): Promise<{ testFunctions: vscode.DocumentSymbol[]; suiteToTest: SuiteToTestMap }> {
215+
if (isBenchmark) {
216+
return {
217+
testFunctions: (await getBenchmarkFunctions(goCtx, doc)) ?? [],
218+
suiteToTest: {}
219+
};
220+
}
221+
222+
const { testFunctions, foundTestifyTestFunction } = await getTestFunctionsAndTestifyHint(goCtx, doc);
223+
224+
return {
225+
testFunctions: testFunctions ?? [],
226+
suiteToTest: foundTestifyTestFunction ? await getSuiteToTestMap(goCtx, doc) : {}
227+
};
177228
}
178229

179230
/**
@@ -199,17 +250,16 @@ export function extractInstanceTestName(symbolName: string): string {
199250
export function getTestFunctionDebugArgs(
200251
document: vscode.TextDocument,
201252
testFunctionName: string,
202-
testFunctions: vscode.DocumentSymbol[]
253+
testFunctions: vscode.DocumentSymbol[],
254+
suiteToFunc: SuiteToTestMap
203255
): string[] {
204256
if (benchmarkRegex.test(testFunctionName)) {
205257
return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
206258
}
207259
const instanceMethod = extractInstanceTestName(testFunctionName);
208260
if (instanceMethod) {
209-
const testFns = findAllTestSuiteRuns(document, testFunctions);
210-
const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`];
211-
const testSuiteTests = ['-testify.m', `^${instanceMethod}$`];
212-
return [...testSuiteRuns, ...testSuiteTests];
261+
const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc);
262+
return ['-test.run', `^${testFns.map((t) => t.name).join('|')}$/^${instanceMethod}$`];
213263
} else {
214264
return ['-test.run', `^${testFunctionName}$`];
215265
}
@@ -222,12 +272,22 @@ export function getTestFunctionDebugArgs(
222272
*/
223273
export function findAllTestSuiteRuns(
224274
doc: vscode.TextDocument,
225-
allTests: vscode.DocumentSymbol[]
275+
allTests: vscode.DocumentSymbol[],
276+
suiteToFunc: SuiteToTestMap
226277
): 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(')) ?? [];
278+
const suites = allTests
279+
// Find all tests with receivers.
280+
?.map((e) => e.name.match(testMethodRegex))
281+
.filter((e) => e?.length === 3)
282+
// Take out receiever, strip leading *.
283+
.map((e) => e && e[1].replace(/^\*/g, ''))
284+
// Map receiver name to test that runs "suite.Run".
285+
.map((e) => e && suiteToFunc[e])
286+
// Filter out empty results.
287+
.filter((e): e is vscode.DocumentSymbol => !!e);
288+
289+
// Dedup.
290+
return [...new Set(suites)];
231291
}
232292

233293
/**
@@ -254,6 +314,59 @@ export async function getBenchmarkFunctions(
254314
return children.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name));
255315
}
256316

317+
export type SuiteToTestMap = Record<string, vscode.DocumentSymbol>;
318+
319+
/**
320+
* Returns a mapping between a package's function receivers to
321+
* the test method that initiated them with "suite.Run".
322+
*
323+
* @param the URI of a Go source file.
324+
* @return function symbols from all source files of the package, mapped by target suite names.
325+
*/
326+
export async function getSuiteToTestMap(
327+
goCtx: GoExtensionContext,
328+
doc: vscode.TextDocument,
329+
token?: vscode.CancellationToken
330+
) {
331+
// Get all the package documents.
332+
const packageDir = path.parse(doc.fileName).dir;
333+
const packageContent = await fs.readdir(packageDir, { withFileTypes: true });
334+
const packageFilenames = packageContent
335+
// Only go files.
336+
.filter((dirent) => dirent.isFile())
337+
.map((dirent) => dirent.name)
338+
.filter((name) => name.endsWith('.go'));
339+
const packageDocs = await Promise.all(
340+
packageFilenames.map((e) => path.join(packageDir, e)).map(vscode.workspace.openTextDocument)
341+
);
342+
343+
const suiteToTest: SuiteToTestMap = {};
344+
for (const packageDoc of packageDocs) {
345+
const funcs = await getTestFunctions(goCtx, packageDoc, token);
346+
if (!funcs) {
347+
continue;
348+
}
349+
350+
for (const func of funcs) {
351+
const funcText = packageDoc.getText(func.range);
352+
353+
// Matches run suites of the types:
354+
// type1: suite.Run(t, MySuite{
355+
// type1: suite.Run(t, &MySuite{
356+
// type2: suite.Run(t, new(MySuite)
357+
const matchRunSuite = funcText.match(runTestSuiteRegex);
358+
if (!matchRunSuite) {
359+
continue;
360+
}
361+
362+
const g = matchRunSuite.groups;
363+
suiteToTest[g?.type1 || g?.type2 || ''] = func;
364+
}
365+
}
366+
367+
return suiteToTest;
368+
}
369+
257370
/**
258371
* go test -json output format.
259372
* which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format

0 commit comments

Comments
 (0)