From 0fa9525dc1c5f0389775e59d83065e4d32558a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20M?= Date: Sun, 14 Jan 2024 12:05:44 +0100 Subject: [PATCH] test/gopls: add tests covering stretchr test suites --- extension/src/goTest.ts | 19 +-- extension/src/goTest/run.ts | 17 +-- extension/src/testUtils.ts | 63 +++++++- extension/test/gopls/codelens.test.ts | 140 +++++++++++++++++- extension/test/gopls/goTest.run.test.ts | 5 + extension/test/integration/test.test.ts | 13 ++ .../stretchrTestSuite/another_suite_test.go | 7 + .../test/testdata/stretchrTestSuite/go.mod | 7 +- 8 files changed, 240 insertions(+), 31 deletions(-) create mode 100644 extension/test/testdata/stretchrTestSuite/another_suite_test.go diff --git a/extension/src/goTest.ts b/extension/src/goTest.ts index ea27f6fe48..edc2460605 100644 --- a/extension/src/goTest.ts +++ b/extension/src/goTest.ts @@ -18,12 +18,12 @@ import { getBenchmarkFunctions, getTestFlags, getTestFunctionDebugArgs, - getSuiteToTestMap, - getTestFunctions, + getTestFunctionsAndTestSuite, getTestTags, goTest, TestConfig, - SuiteToTestMap + SuiteToTestMap, + getTestFunctions } from './testUtils'; // lastTestConfig holds a reference to the last executed TestConfig which allows @@ -54,9 +54,11 @@ async function _testAtCursor( throw new NotFoundError('No tests found. Current file is not a test file.'); } - const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions; - const testFunctions = (await getFunctions(goCtx, editor.document)) ?? []; - const suiteToTest = await getSuiteToTestMap(goCtx, editor.document); + const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite( + cmd === 'benchmark', + goCtx, + editor.document + ); // We use functionName if it was provided as argument // Otherwise find any test function containing the cursor. const testFunctionName = @@ -95,8 +97,7 @@ async function _subTestAtCursor( } await editor.document.save(); - const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? []; - const suiteToTest = await getSuiteToTestMap(goCtx, editor.document); + const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(false, 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)); @@ -164,7 +165,7 @@ async function _subTestAtCursor( export function testAtCursor(cmd: TestAtCursorCmd): CommandFactory { return (ctx, goCtx) => (args: any) => { const goConfig = getGoConfig(); - _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => { + return _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => { if (err instanceof NotFoundError) { vscode.window.showInformationMessage(err.message); } else { diff --git a/extension/src/goTest/run.ts b/extension/src/goTest/run.ts index 526e5cca24..4bc4fbcbfb 100644 --- a/extension/src/goTest/run.ts +++ b/extension/src/goTest/run.ts @@ -21,14 +21,7 @@ import vscode = require('vscode'); import { outputChannel } from '../goStatus'; import { isModSupported } from '../goModules'; import { getGoConfig } from '../config'; -import { - getBenchmarkFunctions, - getTestFlags, - getSuiteToTestMap, - getTestFunctions, - goTest, - GoTestOutput -} from '../testUtils'; +import { getTestFlags, getTestFunctionsAndTestSuite, goTest, GoTestOutput } from '../testUtils'; import { GoTestResolver } from './resolve'; import { dispose, forEachAsync, GoTest, Workspace } from './utils'; import { GoTestProfiler, ProfilingOptions } from './profile'; @@ -168,9 +161,11 @@ export class GoTestRunner { await doc.save(); 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); + const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite( + kind === 'benchmark', + this.goCtx, + doc + ); // TODO Can we get output from the debug session, in order to check for // run/pass/fail events? diff --git a/extension/src/testUtils.ts b/extension/src/testUtils.ts index 0155cf6de5..4070f6e6d1 100644 --- a/extension/src/testUtils.ts +++ b/extension/src/testUtils.ts @@ -155,27 +155,76 @@ export async function getTestFunctions( doc: vscode.TextDocument, token?: vscode.CancellationToken ): Promise { + const result = await getTestFunctionsAndTestifyHint(goCtx, doc, token); + return result.testFunctions; +} + +/** + * Returns all Go unit test functions in the given source file and an hint if testify is used. + * + * @param doc A Go source file + */ +export async function getTestFunctionsAndTestifyHint( + goCtx: GoExtensionContext, + doc: vscode.TextDocument, + token?: vscode.CancellationToken +): Promise<{ testFunctions?: vscode.DocumentSymbol[]; foundTestifyTestFunction?: boolean }> { const documentSymbolProvider = GoDocumentSymbolProvider(goCtx, true); const symbols = await documentSymbolProvider.provideDocumentSymbols(doc); if (!symbols || symbols.length === 0) { - return; + return {}; } const symbol = symbols[0]; if (!symbol) { - return; + return {}; } const children = symbol.children; // 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( + + const allTestFunctions = children.filter( (sym) => - (sym.kind === vscode.SymbolKind.Function || sym.kind === vscode.SymbolKind.Method) && + sym.kind === vscode.SymbolKind.Function && // Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482 !testMainRegex.test(doc.lineAt(sym.range.start.line).text) && - (testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name) || (testify && testMethodRegex.test(sym.name))) + (testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name)) ); + + const allTestMethods = testify + ? children.filter((sym) => sym.kind === vscode.SymbolKind.Method && testMethodRegex.test(sym.name)) + : []; + + return { + testFunctions: allTestFunctions.concat(allTestMethods), + foundTestifyTestFunction: allTestMethods.length > 0 + }; +} + +/** + * Returns all the Go test functions (or benchmark) from the given Go source file, and the associated test suites when testify is used. + * + * @param doc A Go source file + */ +export async function getTestFunctionsAndTestSuite( + isBenchmark: boolean, + goCtx: GoExtensionContext, + doc: vscode.TextDocument +): Promise<{ testFunctions: vscode.DocumentSymbol[]; suiteToTest: SuiteToTestMap }> { + if (isBenchmark) { + return { + testFunctions: (await getBenchmarkFunctions(goCtx, doc)) ?? [], + suiteToTest: {} + }; + } + + const { testFunctions, foundTestifyTestFunction } = await getTestFunctionsAndTestifyHint(goCtx, doc); + + return { + testFunctions: testFunctions ?? [], + suiteToTest: foundTestifyTestFunction ? await getSuiteToTestMap(goCtx, doc) : {} + }; } /** @@ -210,9 +259,7 @@ export function getTestFunctionDebugArgs( const instanceMethod = extractInstanceTestName(testFunctionName); if (instanceMethod) { const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc); - const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`]; - const testSuiteTests = ['-testify.m', `^${instanceMethod}$`]; - return [...testSuiteRuns, ...testSuiteTests]; + return ['-test.run', `^${testFns.map((t) => t.name).join('|')}$/^${instanceMethod}$`]; } else { return ['-test.run', `^${testFunctionName}$`]; } diff --git a/extension/test/gopls/codelens.test.ts b/extension/test/gopls/codelens.test.ts index 31c1fcdb3a..ac37516d9a 100644 --- a/extension/test/gopls/codelens.test.ts +++ b/extension/test/gopls/codelens.test.ts @@ -11,9 +11,10 @@ import sinon = require('sinon'); import vscode = require('vscode'); import { updateGoVarsFromConfig } from '../../src/goInstallTools'; import { GoRunTestCodeLensProvider } from '../../src/goRunTestCodelens'; -import { subTestAtCursor } from '../../src/goTest'; +import { subTestAtCursor, testAtCursor } from '../../src/goTest'; import { MockExtensionContext } from '../mocks/MockContext'; import { Env } from './goplsTestEnv.utils'; +import * as testUtils from '../../src/testUtils'; suite('Code lenses for testing and benchmarking', function () { this.timeout(20000); @@ -200,4 +201,141 @@ suite('Code lenses for testing and benchmarking', function () { // Results should match `go test -list`. assert.deepStrictEqual(found, ['TestNotMain']); }); + + test('Debug - debugs a test with cursor on t.Run line', async () => { + const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true)); + + const editor = await vscode.window.showTextDocument(document); + editor.selection = new vscode.Selection(7, 4, 7, 4); + const result = await subTestAtCursor('debug')(ctx, env.goCtx)([]); + assert.strictEqual(result, true); + + assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging'); + const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration; + gotConfig.program = ''; + assert.deepStrictEqual(gotConfig, { + name: 'Debug Test', + type: 'go', + request: 'launch', + args: ['-test.run', '^TestSample$/^sample_test_passing$'], + buildFlags: '', + env: {}, + sessionID: undefined, + mode: 'test', + envFile: null, + program: '' + }); + }); +}); + +suite('Code lenses with stretchr/testify/suite', function () { + const ctx = MockExtensionContext.new(); + + const testdataDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'stretchrTestSuite'); + const env = new Env(); + + this.afterEach(async function () { + // Note: this shouldn't use () => {...}. Arrow functions do not have 'this'. + // I don't know why but this.currentTest.state does not have the expected value when + // used with teardown. + env.flushTrace(this.currentTest?.state === 'failed'); + ctx.teardown(); + sinon.restore(); + }); + + suiteSetup(async () => { + await updateGoVarsFromConfig({}); + await env.startGopls(undefined, undefined, testdataDir); + }); + + suiteTeardown(async () => { + await env.teardown(); + }); + + test('Run test at cursor', async () => { + const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true)); + + const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go'))); + editor.selection = new vscode.Selection(25, 4, 25, 4); + + const result = await testAtCursor('test')(ctx, env.goCtx)([]); + assert.strictEqual(result, true); + + assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest'); + const gotConfig = goTestStub.getCall(0).args[0]; + assert.deepStrictEqual(gotConfig.functions, ['(*ExampleTestSuite).TestExample', 'TestExampleTestSuite']); + }); + + test('Run test at cursor in different file than test suite definition', async () => { + const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true)); + + const editor = await vscode.window.showTextDocument( + vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go')) + ); + editor.selection = new vscode.Selection(3, 4, 3, 4); + + const result = await testAtCursor('test')(ctx, env.goCtx)([]); + assert.strictEqual(result, true); + + assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest'); + const gotConfig = goTestStub.getCall(0).args[0]; + assert.deepStrictEqual(gotConfig.functions, [ + '(*ExampleTestSuite).TestExampleInAnotherFile', + 'TestExampleTestSuite' + ]); + }); + + test('Debug test at cursor', async () => { + const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true)); + + const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go'))); + editor.selection = new vscode.Selection(25, 4, 25, 4); + + const result = await testAtCursor('debug')(ctx, env.goCtx)([]); + assert.strictEqual(result, true); + + assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging'); + const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration; + gotConfig.program = ''; + assert.deepStrictEqual(gotConfig, { + name: 'Debug Test', + type: 'go', + request: 'launch', + args: ['-test.run', '^TestExampleTestSuite$/^TestExample$'], + buildFlags: '', + env: {}, + sessionID: undefined, + mode: 'test', + envFile: null, + program: '' + }); + }); + + test('Debug test at cursor in different file than test suite definition', async () => { + const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true)); + + const editor = await vscode.window.showTextDocument( + vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go')) + ); + editor.selection = new vscode.Selection(3, 4, 3, 4); + + const result = await testAtCursor('debug')(ctx, env.goCtx)([]); + assert.strictEqual(result, true); + + assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging'); + const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration; + gotConfig.program = ''; + assert.deepStrictEqual(gotConfig, { + name: 'Debug Test', + type: 'go', + request: 'launch', + args: ['-test.run', '^TestExampleTestSuite$/^TestExampleInAnotherFile$'], + buildFlags: '', + env: {}, + sessionID: undefined, + mode: 'test', + envFile: null, + program: '' + }); + }); }); diff --git a/extension/test/gopls/goTest.run.test.ts b/extension/test/gopls/goTest.run.test.ts index 112959a42d..d583dc99e9 100644 --- a/extension/test/gopls/goTest.run.test.ts +++ b/extension/test/gopls/goTest.run.test.ts @@ -8,12 +8,17 @@ import { GoTestExplorer } from '../../src/goTest/explore'; import { MockExtensionContext } from '../mocks/MockContext'; import { GoTest } from '../../src/goTest/utils'; import { Env } from './goplsTestEnv.utils'; +import { updateGoVarsFromConfig } from '../../src/goInstallTools'; suite('Go Test Runner', () => { const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata'); let testExplorer: GoTestExplorer; + suiteSetup(async () => { + await updateGoVarsFromConfig({}); + }); + suite('parseOutput', () => { const ctx = MockExtensionContext.new(); suiteSetup(async () => { diff --git a/extension/test/integration/test.test.ts b/extension/test/integration/test.test.ts index 1ca2431a47..500b176216 100644 --- a/extension/test/integration/test.test.ts +++ b/extension/test/integration/test.test.ts @@ -97,6 +97,19 @@ suite('Test Go Test Args', () => { flags: ['-run', 'TestC'] }); }); + test('use -testify.m for methods', () => { + runTest({ + expectedArgs: + 'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...', + expectedOutArgs: + 'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...', + functions: [ + '(*ExampleTestSuite).TestExample', + '(*ExampleTestSuite).TestAnotherExample', + 'TestExampleTestSuite' + ] + }); + }); }); suite('Test Go Test', function () { diff --git a/extension/test/testdata/stretchrTestSuite/another_suite_test.go b/extension/test/testdata/stretchrTestSuite/another_suite_test.go new file mode 100644 index 0000000000..1af57e7d34 --- /dev/null +++ b/extension/test/testdata/stretchrTestSuite/another_suite_test.go @@ -0,0 +1,7 @@ +package main_test + +func (suite *ExampleTestSuite) TestExampleInAnotherFile() { + if suite.VariableThatShouldStartAtFive != 5 { + suite.T().Fatalf("%d != %d", 5, suite.VariableThatShouldStartAtFive) + } +} diff --git a/extension/test/testdata/stretchrTestSuite/go.mod b/extension/test/testdata/stretchrTestSuite/go.mod index 272ea9c8a5..7703985823 100644 --- a/extension/test/testdata/stretchrTestSuite/go.mod +++ b/extension/test/testdata/stretchrTestSuite/go.mod @@ -1,10 +1,13 @@ module example/a -go 1.16 +go 1.21 + +require github.com/stretchr/testify v1.7.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/stretchr/testify v1.7.0 + github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect )