Skip to content

Commit 306b9e5

Browse files
committed
test/gopls: add tests covering stretchr test suites
1 parent e79bcf8 commit 306b9e5

File tree

8 files changed

+239
-28
lines changed

8 files changed

+239
-28
lines changed

extension/src/goTest.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import {
1818
getBenchmarkFunctions,
1919
getTestFlags,
2020
getTestFunctionDebugArgs,
21-
getSuiteToTestMap,
22-
getTestFunctions,
21+
getTestFunctionsAndTestSuite,
2322
getTestTags,
2423
goTest,
2524
TestConfig,
26-
SuiteToTestMap
25+
SuiteToTestMap,
26+
getTestFunctions
2727
} from './testUtils';
2828

2929
// lastTestConfig holds a reference to the last executed TestConfig which allows
@@ -54,9 +54,11 @@ async function _testAtCursor(
5454
throw new NotFoundError('No tests found. Current file is not a test file.');
5555
}
5656

57-
const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
58-
const testFunctions = (await getFunctions(goCtx, editor.document)) ?? [];
59-
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
57+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
58+
cmd === 'benchmark',
59+
goCtx,
60+
editor.document
61+
);
6062
// We use functionName if it was provided as argument
6163
// Otherwise find any test function containing the cursor.
6264
const testFunctionName =
@@ -95,8 +97,7 @@ async function _subTestAtCursor(
9597
}
9698

9799
await editor.document.save();
98-
const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
99-
const suiteToTest = await getSuiteToTestMap(goCtx, editor.document);
100+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(false, goCtx, editor.document);
100101
// We use functionName if it was provided as argument
101102
// Otherwise find any test function containing the cursor.
102103
const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
@@ -164,7 +165,7 @@ async function _subTestAtCursor(
164165
export function testAtCursor(cmd: TestAtCursorCmd): CommandFactory {
165166
return (ctx, goCtx) => (args: any) => {
166167
const goConfig = getGoConfig();
167-
_testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
168+
return _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
168169
if (err instanceof NotFoundError) {
169170
vscode.window.showInformationMessage(err.message);
170171
} else {

extension/src/goTest/run.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,7 @@ import vscode = require('vscode');
2121
import { outputChannel } from '../goStatus';
2222
import { isModSupported } from '../goModules';
2323
import { getGoConfig } from '../config';
24-
import {
25-
getBenchmarkFunctions,
26-
getTestFlags,
27-
getSuiteToTestMap,
28-
getTestFunctions,
29-
goTest,
30-
GoTestOutput
31-
} from '../testUtils';
24+
import { getTestFlags, getTestFunctionsAndTestSuite, goTest, GoTestOutput } from '../testUtils';
3225
import { GoTestResolver } from './resolve';
3326
import { dispose, forEachAsync, GoTest, Workspace } from './utils';
3427
import { GoTestProfiler, ProfilingOptions } from './profile';
@@ -168,9 +161,11 @@ export class GoTestRunner {
168161
await doc.save();
169162

170163
const goConfig = getGoConfig(test.uri);
171-
const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
172-
const testFunctions = await getFunctions(this.goCtx, doc, token);
173-
const suiteToTest = await getSuiteToTestMap(this.goCtx, doc, token);
164+
const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
165+
kind === 'benchmark',
166+
this.goCtx,
167+
doc
168+
);
174169

175170
// TODO Can we get output from the debug session, in order to check for
176171
// run/pass/fail events?

extension/src/testUtils.ts

+54-5
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,76 @@ export async function getTestFunctions(
155155
doc: vscode.TextDocument,
156156
token?: vscode.CancellationToken
157157
): Promise<vscode.DocumentSymbol[] | undefined> {
158+
const result = await getTestFunctionsAndTestifyHint(goCtx, doc, token);
159+
return result.testFunctions;
160+
}
161+
162+
/**
163+
* Returns all Go unit test functions in the given source file and an hint if testify is used.
164+
*
165+
* @param doc A Go source file
166+
*/
167+
export async function getTestFunctionsAndTestifyHint(
168+
goCtx: GoExtensionContext,
169+
doc: vscode.TextDocument,
170+
token?: vscode.CancellationToken
171+
): Promise<{ testFunctions?: vscode.DocumentSymbol[]; foundTestifyTestFunction?: boolean }> {
158172
const documentSymbolProvider = GoDocumentSymbolProvider(goCtx, true);
159173
const symbols = await documentSymbolProvider.provideDocumentSymbols(doc);
160174
if (!symbols || symbols.length === 0) {
161-
return;
175+
return {};
162176
}
163177
const symbol = symbols[0];
164178
if (!symbol) {
165-
return;
179+
return {};
166180
}
167181
const children = symbol.children;
168182

169183
// With gopls symbol provider, the symbols have the imports of all
170184
// the package, so suite tests from all files will be found.
171185
const testify = importsTestify(symbols);
172-
return children.filter(
186+
187+
const allTestFunctions = children.filter(
173188
(sym) =>
174-
(sym.kind === vscode.SymbolKind.Function || sym.kind === vscode.SymbolKind.Method) &&
189+
sym.kind === vscode.SymbolKind.Function &&
175190
// Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482
176191
!testMainRegex.test(doc.lineAt(sym.range.start.line).text) &&
177-
(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name) || (testify && testMethodRegex.test(sym.name)))
192+
(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name))
178193
);
194+
195+
const allTestMethods = testify
196+
? children.filter((sym) => sym.kind === vscode.SymbolKind.Method && testMethodRegex.test(sym.name))
197+
: [];
198+
199+
return {
200+
testFunctions: allTestFunctions.concat(allTestMethods),
201+
foundTestifyTestFunction: allTestMethods.length > 0
202+
};
203+
}
204+
205+
/**
206+
* Returns all the Go test functions (or benchmark) from the given Go source file, and the associated test suites when testify is used.
207+
*
208+
* @param doc A Go source file
209+
*/
210+
export async function getTestFunctionsAndTestSuite(
211+
isBenchmark: boolean,
212+
goCtx: GoExtensionContext,
213+
doc: vscode.TextDocument
214+
): Promise<{ testFunctions: vscode.DocumentSymbol[]; suiteToTest: SuiteToTestMap }> {
215+
if (isBenchmark) {
216+
return {
217+
testFunctions: (await getBenchmarkFunctions(goCtx, doc)) ?? [],
218+
suiteToTest: {}
219+
};
220+
}
221+
222+
const { testFunctions, foundTestifyTestFunction } = await getTestFunctionsAndTestifyHint(goCtx, doc);
223+
224+
return {
225+
testFunctions: testFunctions ?? [],
226+
suiteToTest: foundTestifyTestFunction ? await getSuiteToTestMap(goCtx, doc) : {}
227+
};
179228
}
180229

181230
/**

extension/test/gopls/codelens.test.ts

+139-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import sinon = require('sinon');
1111
import vscode = require('vscode');
1212
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
1313
import { GoRunTestCodeLensProvider } from '../../src/goRunTestCodelens';
14-
import { subTestAtCursor } from '../../src/goTest';
14+
import { subTestAtCursor, testAtCursor } from '../../src/goTest';
1515
import { MockExtensionContext } from '../mocks/MockContext';
1616
import { Env } from './goplsTestEnv.utils';
17+
import * as testUtils from '../../src/testUtils';
1718

1819
suite('Code lenses for testing and benchmarking', function () {
1920
this.timeout(20000);
@@ -200,4 +201,141 @@ suite('Code lenses for testing and benchmarking', function () {
200201
// Results should match `go test -list`.
201202
assert.deepStrictEqual(found, ['TestNotMain']);
202203
});
204+
205+
test('Debug - debugs a test with cursor on t.Run line', async () => {
206+
const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
207+
208+
const editor = await vscode.window.showTextDocument(document);
209+
editor.selection = new vscode.Selection(7, 4, 7, 4);
210+
const result = await subTestAtCursor('debug')(ctx, env.goCtx)([]);
211+
assert.strictEqual(result, true);
212+
213+
assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
214+
const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
215+
gotConfig.program = '';
216+
assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
217+
name: 'Debug Test',
218+
type: 'go',
219+
request: 'launch',
220+
args: ['-test.run', '^TestSample$/^sample_test_passing$'],
221+
buildFlags: '',
222+
env: {},
223+
sessionID: undefined,
224+
mode: 'test',
225+
envFile: null,
226+
program: ''
227+
});
228+
});
229+
});
230+
231+
suite('Code lenses with stretchr/testify/suite', function () {
232+
const ctx = MockExtensionContext.new();
233+
234+
const testdataDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'stretchrTestSuite');
235+
const env = new Env();
236+
237+
this.afterEach(async function () {
238+
// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
239+
// I don't know why but this.currentTest.state does not have the expected value when
240+
// used with teardown.
241+
env.flushTrace(this.currentTest?.state === 'failed');
242+
ctx.teardown();
243+
sinon.restore();
244+
});
245+
246+
suiteSetup(async () => {
247+
await updateGoVarsFromConfig({});
248+
await env.startGopls(undefined, undefined, testdataDir);
249+
});
250+
251+
suiteTeardown(async () => {
252+
await env.teardown();
253+
});
254+
255+
test('Run test at cursor', async () => {
256+
const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true));
257+
258+
const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go')));
259+
editor.selection = new vscode.Selection(25, 4, 25, 4);
260+
261+
const result = await testAtCursor('test')(ctx, env.goCtx)([]);
262+
assert.strictEqual(result, true);
263+
264+
assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest');
265+
const gotConfig = goTestStub.getCall(0).args[0];
266+
assert.deepStrictEqual(gotConfig.functions, ['(*ExampleTestSuite).TestExample', 'TestExampleTestSuite']);
267+
});
268+
269+
test('Run test at cursor in different file than test suite definition', async () => {
270+
const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true));
271+
272+
const editor = await vscode.window.showTextDocument(
273+
vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go'))
274+
);
275+
editor.selection = new vscode.Selection(3, 4, 3, 4);
276+
277+
const result = await testAtCursor('test')(ctx, env.goCtx)([]);
278+
assert.strictEqual(result, true);
279+
280+
assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest');
281+
const gotConfig = goTestStub.getCall(0).args[0];
282+
assert.deepStrictEqual(gotConfig.functions, [
283+
'(*ExampleTestSuite).TestExampleInAnotherFile',
284+
'TestExampleTestSuite'
285+
]);
286+
});
287+
288+
test('Debug test at cursor', async () => {
289+
const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
290+
291+
const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go')));
292+
editor.selection = new vscode.Selection(25, 4, 25, 4);
293+
294+
const result = await testAtCursor('debug')(ctx, env.goCtx)([]);
295+
assert.strictEqual(result, true);
296+
297+
assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
298+
const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
299+
gotConfig.program = '';
300+
assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
301+
name: 'Debug Test',
302+
type: 'go',
303+
request: 'launch',
304+
args: ['-test.run', '^TestExampleTestSuite$', '-testify.m', '^TestExample$'],
305+
buildFlags: '',
306+
env: {},
307+
sessionID: undefined,
308+
mode: 'test',
309+
envFile: null,
310+
program: ''
311+
});
312+
});
313+
314+
test('Debug test at cursor in different file than test suite definition', async () => {
315+
const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
316+
317+
const editor = await vscode.window.showTextDocument(
318+
vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go'))
319+
);
320+
editor.selection = new vscode.Selection(3, 4, 3, 4);
321+
322+
const result = await testAtCursor('debug')(ctx, env.goCtx)([]);
323+
assert.strictEqual(result, true);
324+
325+
assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
326+
const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
327+
gotConfig.program = '';
328+
assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
329+
name: 'Debug Test',
330+
type: 'go',
331+
request: 'launch',
332+
args: ['-test.run', '^TestExampleTestSuite$', '-testify.m', '^TestExampleInAnotherFile$'],
333+
buildFlags: '',
334+
env: {},
335+
sessionID: undefined,
336+
mode: 'test',
337+
envFile: null,
338+
program: ''
339+
});
340+
});
203341
});

extension/test/gopls/goTest.run.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import { GoTestExplorer } from '../../src/goTest/explore';
88
import { MockExtensionContext } from '../mocks/MockContext';
99
import { GoTest } from '../../src/goTest/utils';
1010
import { Env } from './goplsTestEnv.utils';
11+
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
1112

1213
suite('Go Test Runner', () => {
1314
const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
1415

1516
let testExplorer: GoTestExplorer;
1617

18+
suiteSetup(async () => {
19+
await updateGoVarsFromConfig({});
20+
});
21+
1722
suite('parseOutput', () => {
1823
const ctx = MockExtensionContext.new();
1924
suiteSetup(async () => {

extension/test/integration/test.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ suite('Test Go Test Args', () => {
9797
flags: ['-run', 'TestC']
9898
});
9999
});
100+
test('use -testify.m for methods', () => {
101+
runTest({
102+
expectedArgs:
103+
'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
104+
expectedOutArgs:
105+
'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
106+
functions: [
107+
'(*ExampleTestSuite).TestExample',
108+
'(*ExampleTestSuite).TestAnotherExample',
109+
'TestExampleTestSuite'
110+
]
111+
});
112+
});
100113
});
101114

102115
suite('Test Go Test', function () {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main_test
2+
3+
func (suite *ExampleTestSuite) TestExampleInAnotherFile() {
4+
if suite.VariableThatShouldStartAtFive != 5 {
5+
suite.T().Fatalf("%d != %d", 5, suite.VariableThatShouldStartAtFive)
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
module example/a
22

3-
go 1.16
3+
go 1.21
4+
5+
require github.com/stretchr/testify v1.7.0
46

57
require (
68
github.com/davecgh/go-spew v1.1.1 // indirect
79
github.com/kr/pretty v0.1.0 // indirect
8-
github.com/stretchr/testify v1.7.0
10+
github.com/pmezard/go-difflib v1.0.0 // indirect
911
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
12+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
1013
)

0 commit comments

Comments
 (0)