diff --git a/extension/src/goRunTestCodelens.ts b/extension/src/goRunTestCodelens.ts index aa41ebd055..8cf37c2886 100644 --- a/extension/src/goRunTestCodelens.ts +++ b/extension/src/goRunTestCodelens.ts @@ -109,27 +109,44 @@ export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider { return codelens; } - const simpleRunRegex = /t.Run\("([^"]+)",/; + // Captures receiver name and function name. + // reason is: function name coming from f.name is not compatible with struct methods. + const testSuiteRegex = /func \((.*?) .*?\) (Test.*?)\(/; for (const f of testFunctions) { - const functionName = f.name; + let functionName = f.name; + const line = document.lineAt(f.range.start.line); + const testSuiteMatch = line.text.match(testSuiteRegex); + if (testSuiteMatch) functionName = testSuiteMatch[2] + var receiverName = testSuiteMatch ? testSuiteMatch[1] : 't'; codelens.push( new CodeLens(f.range, { title: 'run test', command: 'go.test.cursor', - arguments: [{ functionName }] + arguments: [{ + functionName: functionName, + isTestSuite: !!testSuiteMatch + }] }), new CodeLens(f.range, { title: 'debug test', command: 'go.debug.cursor', - arguments: [{ functionName }] + arguments: [{ + functionName: functionName, + isTestSuite: !!testSuiteMatch + }] }) ); + // Dynamic regex for capturing receiverName.Run("testName", ...) + // receiver name is either t for normal test functions or the receiver of a test suite. + // example: func (s *testSuite) TestFunc() // Returns 's' + let testCaseRegex = new RegExp(receiverName + "\.Run\\(\"([^\"]+)\","); + for (let i = f.range.start.line; i < f.range.end.line; i++) { const line = document.lineAt(i); - const simpleMatch = line.text.match(simpleRunRegex); + const simpleMatch = line.text.match(testCaseRegex); // BUG: this does not handle nested subtests. This should // be solved once codelens is handled by gopls and not by @@ -141,12 +158,20 @@ export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider { new CodeLens(line.range, { title: 'run test', command: 'go.subtest.cursor', - arguments: [{ functionName, subTestName }] + arguments: [{ + functionName: functionName, + subTestName: subTestName, + isTestSuite: !!testSuiteMatch + }] }), new CodeLens(line.range, { title: 'debug test', command: 'go.debug.subtest.cursor', - arguments: [{ functionName, subTestName }] + arguments: [{ + functionName: functionName, + subTestName: subTestName, + isTestSuite: !!testSuiteMatch + }] }) ); } diff --git a/extension/src/goTest.ts b/extension/src/goTest.ts index 22c972e82b..d5ad5aa3e9 100644 --- a/extension/src/goTest.ts +++ b/extension/src/goTest.ts @@ -44,7 +44,7 @@ async function _testAtCursor( goCtx: GoExtensionContext, goConfig: vscode.WorkspaceConfiguration, cmd: TestAtCursorCmd, - args: any + args?: SubTestAtCursorArgs ) { const editor = vscode.window.activeTextEditor; if (!editor) { @@ -72,7 +72,7 @@ async function _testAtCursor( await editor.document.save(); if (cmd === 'debug') { - return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig); + return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, undefined, args); } else if (cmd === 'benchmark' || cmd === 'test') { return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args); } else { @@ -117,7 +117,7 @@ async function _subTestAtCursor( // We use functionName if it was provided as argument // Otherwise find any test function containing the cursor. const currentTestFunctions = args?.functionName - ? testFunctions.filter((func) => func.name === args.functionName) + ? testFunctions.filter((func) => func.name.endsWith(String(args.functionName))) : testFunctions.filter((func) => func.range.contains(editor.selection.start)); const testFunctionName = args && args.functionName ? args.functionName : currentTestFunctions.map((el) => el.name)[0]; @@ -165,7 +165,7 @@ async function _subTestAtCursor( const escapedName = escapeSubTestName(testFunctionName, subTestName); if (cmd === 'debug') { - return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig); + return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, undefined, args); } else if (cmd === 'test') { return runTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, cmd, args); } else { @@ -226,6 +226,10 @@ type TestAtCursor = { * Flags to be passed to `go test`. */ flags?: string[]; + /** + * Whether it's a test suite. + */ + isTestSuite?: boolean; }; /** @@ -252,6 +256,7 @@ async function runTestAtCursor( flags: getTestFlags(goConfig, args), functions: testConfigFns, isBenchmark: cmd === 'benchmark', + isTestSuite: !!args?.isTestSuite, isMod, applyCodeCoverage: goConfig.get('coverOnSingleTest') }; @@ -273,10 +278,10 @@ export function subTestAtCursor(cmd: SubTestAtCursorCmd): CommandFactory { * codelens provided by {@link GoRunTestCodeLensProvider}, args * specifies the function and subtest names. */ - args?: [SubTestAtCursorArgs] + args?:SubTestAtCursorArgs ) => { try { - return await _subTestAtCursor(goCtx, getGoConfig(), cmd, args?.[0]); + return await _subTestAtCursor(goCtx, getGoConfig(), cmd, args); } catch (err) { if (err instanceof NotFoundError) { vscode.window.showInformationMessage(err.message); @@ -303,10 +308,11 @@ export async function debugTestAtCursor( testFunctions: vscode.DocumentSymbol[], suiteToFunc: SuiteToTestMap, goConfig: vscode.WorkspaceConfiguration, - sessionID?: string + sessionID?: string, + testArgs?: { isTestSuite?: boolean }, ) { const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument; - const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc); + const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc, testArgs?.isTestSuite); const tags = getTestTags(goConfig); const buildFlags = tags ? ['-tags', tags] : []; const flagsFromConfig = getTestFlags(goConfig); diff --git a/extension/src/testUtils.ts b/extension/src/testUtils.ts index 4070f6e6d1..97692fb13b 100644 --- a/extension/src/testUtils.ts +++ b/extension/src/testUtils.ts @@ -85,6 +85,10 @@ export interface TestConfig { * Whether this is a benchmark. */ isBenchmark?: boolean; + /** + * Whether this is a test suite + */ + isTestSuite?: boolean; /** * Whether the tests are being run in a project that uses Go modules */ @@ -246,12 +250,14 @@ export function extractInstanceTestName(symbolName: string): string { * @param document The document containing the tests * @param testFunctionName The test function to get the debug args * @param testFunctions The test functions found in the document + * @param isTestSuite Indicator if the test function is part of a test suite */ export function getTestFunctionDebugArgs( document: vscode.TextDocument, testFunctionName: string, testFunctions: vscode.DocumentSymbol[], - suiteToFunc: SuiteToTestMap + suiteToFunc: SuiteToTestMap, + isTestSuite ?: boolean, ): string[] { if (benchmarkRegex.test(testFunctionName)) { return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^']; @@ -261,7 +267,7 @@ export function getTestFunctionDebugArgs( const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc); return ['-test.run', `^${testFns.map((t) => t.name).join('|')}$/^${instanceMethod}$`]; } else { - return ['-test.run', `^${testFunctionName}$`]; + return ['-test.run', `${!!isTestSuite ? '/': ''}^${testFunctionName}$`]; } } /** @@ -736,7 +742,14 @@ function targetArgs(testconfig: TestConfig): Array { // which will result in the correct thing to happen if (testFunctions.length > 0) { if (testFunctions.length === 1) { - params = params.concat(['-run', util.format('^%s$', testFunctions[0])]); + // If it's a test suite, it means the test function is not a root test function. + // By prepending '/', the command will interpret and execute all child test functions + // that matches the pattern of anyTestSuite/givenFunctionName/givenCaseName. + if (testconfig.isTestSuite) { + params = params.concat(['-run', util.format('/^%s$', testFunctions[0])]); + } else { + params = params.concat(['-run', util.format('^%s$', testFunctions[0])]); + } } else { params = params.concat(['-run', util.format('^(%s)$', testFunctions.join('|'))]); }