Skip to content
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

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions src/goTest/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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();
Expand Down
82 changes: 74 additions & 8 deletions src/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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];
Expand All @@ -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)];
}

/**
Expand All @@ -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
Expand Down