Skip to content

src/goTest: fix multifile suite test fails to debug #2415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/goTest.ts
Original file line number Diff line number Diff line change
@@ -17,10 +17,12 @@ import {
getBenchmarkFunctions,
getTestFlags,
getTestFunctionDebugArgs,
getSuiteToTestMap,
getTestFunctions,
getTestTags,
goTest,
TestConfig
TestConfig,
SuiteToTestMap
} from './testUtils';

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

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

if (cmd === 'debug') {
return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
} else if (cmd === 'benchmark' || cmd === 'test') {
return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
} else {
throw new Error(`Unsupported command: ${cmd}`);
}
@@ -125,13 +128,14 @@ async function runTestAtCursor(
editor: vscode.TextEditor,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[],
suiteToTest: SuiteToTestMap,
goConfig: vscode.WorkspaceConfiguration,
cmd: TestAtCursorCmd,
args: any
) {
const testConfigFns = [testFunctionName];
if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions).map((t) => t.name));
testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
}

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

const subTestName = testFunctionName + '/' + subtest;

return await runTestAtCursor(editor, subTestName, testFunctions, goConfig, 'test', args);
return await runTestAtCursor(editor, subTestName, testFunctions, suiteToTest, goConfig, 'test', args);
} catch (err) {
vscode.window.showInformationMessage('Unable to run subtest: ' + (err as any).toString());
console.error(err);
@@ -236,11 +241,12 @@ export async function debugTestAtCursor(
editorOrDocument: vscode.TextEditor | vscode.TextDocument,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[],
suiteToFunc: SuiteToTestMap,
goConfig: vscode.WorkspaceConfiguration,
sessionID?: string
) {
const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
const tags = getTestTags(goConfig);
const buildFlags = tags ? ['-tags', tags] : [];
const flagsFromConfig = getTestFlags(goConfig);
13 changes: 11 additions & 2 deletions src/goTest/run.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,14 @@ import vscode = require('vscode');
import { outputChannel } from '../goStatus';
import { isModSupported } from '../goModules';
import { getGoConfig } from '../config';
import { getBenchmarkFunctions, getTestFlags, getTestFunctions, goTest, GoTestOutput } from '../testUtils';
import {
getBenchmarkFunctions,
getTestFlags,
getSuiteToTestMap,
getTestFunctions,
goTest,
GoTestOutput
} from '../testUtils';
import { GoTestResolver } from './resolve';
import { dispose, forEachAsync, GoTest, Workspace } from './utils';
import { GoTestProfiler, ProfilingOptions } from './profile';
@@ -161,6 +168,7 @@ export class GoTestRunner {
const goConfig = getGoConfig(test.uri);
const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
const testFunctions = await getFunctions(this.goCtx, doc, token);
const suiteToTest = await getSuiteToTestMap(this.goCtx, doc, token);

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

const run = this.ctrl.createTestRun(request, `Debug ${name}`);
if (!testFunctions) return;
const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);

const started = await debugTestAtCursor(doc, name, testFunctions, suiteToTest, goConfig, id);
if (!started) {
subs.forEach((s) => s.dispose());
run.end();
82 changes: 74 additions & 8 deletions src/testUtils.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import cp = require('child_process');
import path = require('path');
import util = require('util');
import vscode = require('vscode');
import { promises as fs } from 'fs';

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

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

// With gopls dymbol provider symbols, the symbols have the imports of all
// With gopls symbol provider, the symbols have the imports of all
// the package, so suite tests from all files will be found.
const testify = importsTestify(symbols);
return children.filter(
@@ -194,14 +196,15 @@ export function extractInstanceTestName(symbolName: string): string {
export function getTestFunctionDebugArgs(
document: vscode.TextDocument,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[]
testFunctions: vscode.DocumentSymbol[],
suiteToFunc: SuiteToTestMap
): string[] {
if (benchmarkRegex.test(testFunctionName)) {
return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
}
const instanceMethod = extractInstanceTestName(testFunctionName);
if (instanceMethod) {
const testFns = findAllTestSuiteRuns(document, testFunctions);
const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc);
const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`];
const testSuiteTests = ['-testify.m', `^${instanceMethod}$`];
return [...testSuiteRuns, ...testSuiteTests];
@@ -217,12 +220,22 @@ export function getTestFunctionDebugArgs(
*/
export function findAllTestSuiteRuns(
doc: vscode.TextDocument,
allTests: vscode.DocumentSymbol[]
allTests: vscode.DocumentSymbol[],
suiteToFunc: SuiteToTestMap
): vscode.DocumentSymbol[] {
// get non-instance test functions
const testFunctions = allTests?.filter((t) => !testMethodRegex.test(t.name));
// filter further to ones containing suite.Run()
return testFunctions?.filter((t) => doc.getText(t.range).includes('suite.Run(')) ?? [];
const suites = allTests
// Find all tests with receivers.
?.map((e) => e.name.match(testMethodRegex))
.filter((e) => e?.length === 3)
// Take out receiever, strip leading *.
.map((e) => e && e[1].replace(/^\*/g, ''))
// Map receiver name to test that runs "suite.Run".
.map((e) => e && suiteToFunc[e])
// Filter out empty results.
.filter((e): e is vscode.DocumentSymbol => !!e);

// Dedup.
return [...new Set(suites)];
}

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

export type SuiteToTestMap = Record<string, vscode.DocumentSymbol>;

/**
* Returns a mapping between a package's function receivers to
* the test method that initiated them with "suite.Run".
*
* @param the URI of a Go source file.
* @return function symbols from all source files of the package, mapped by target suite names.
*/
export async function getSuiteToTestMap(
goCtx: GoExtensionContext,
doc: vscode.TextDocument,
token?: vscode.CancellationToken
) {
// Get all the package documents.
const packageDir = path.parse(doc.fileName).dir;
const packageContent = await fs.readdir(packageDir, { withFileTypes: true });
const packageFilenames = packageContent
// Only go files.
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter((name) => name.endsWith('.go'));
const packageDocs = await Promise.all(
packageFilenames.map((e) => path.join(packageDir, e)).map(vscode.workspace.openTextDocument)
);

const suiteToTest: SuiteToTestMap = {};
for (const packageDoc of packageDocs) {
const funcs = await getTestFunctions(goCtx, packageDoc, token);
if (!funcs) {
continue;
}

for (const func of funcs) {
const funcText = packageDoc.getText(func.range);

// Matches run suites of the types:
// type1: suite.Run(t, MySuite{
// type1: suite.Run(t, &MySuite{
// type2: suite.Run(t, new(MySuite)
const matchRunSuite = funcText.match(runTestSuiteRegex);
if (!matchRunSuite) {
continue;
}

const g = matchRunSuite.groups;
suiteToTest[g?.type1 || g?.type2 || ''] = func;
}
}

return suiteToTest;
}

/**
* go test -json output format.
* which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format