Skip to content

Commit

Permalink
src/goTest: fix multifile suite test fails to debug
Browse files Browse the repository at this point in the history
  • Loading branch information
nirhaas committed Sep 14, 2022
1 parent f39f7ed commit 19b8cd4
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 16 deletions.
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

0 comments on commit 19b8cd4

Please sign in to comment.