@@ -11,6 +11,7 @@ import cp = require('child_process');
11
11
import path = require( 'path' ) ;
12
12
import util = require( 'util' ) ;
13
13
import vscode = require( 'vscode' ) ;
14
+ import { promises as fs } from 'fs' ;
14
15
15
16
import { applyCodeCoverageToAllEditors } from './goCover' ;
16
17
import { toolExecutionEnvironment } from './goEnv' ;
@@ -50,6 +51,7 @@ const testMethodRegex = /^\(([^)]+)\)\.(Test|Test\P{Ll}.*)$/u;
50
51
const benchmarkRegex = / ^ B e n c h m a r k $ | ^ B e n c h m a r k \P{ Ll} .* / u;
51
52
const fuzzFuncRegx = / ^ F u z z $ | ^ F u z z \P{ Ll} .* / u;
52
53
const testMainRegex = / T e s t M a i n \( .* \* t e s t i n g .M \) / ;
54
+ const runTestSuiteRegex = / ^ \s * s u i t e \. R u n \( \w + , \s * (?: & ? (?< type1 > \w + ) \{ | n e w \( (?< type2 > \w + ) \) ) / mu;
53
55
54
56
/**
55
57
* Input to goTest.
@@ -153,27 +155,76 @@ export async function getTestFunctions(
153
155
doc : vscode . TextDocument ,
154
156
token ?: vscode . CancellationToken
155
157
) : 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 } > {
156
172
const documentSymbolProvider = GoDocumentSymbolProvider ( goCtx , true ) ;
157
173
const symbols = await documentSymbolProvider . provideDocumentSymbols ( doc ) ;
158
174
if ( ! symbols || symbols . length === 0 ) {
159
- return ;
175
+ return { } ;
160
176
}
161
177
const symbol = symbols [ 0 ] ;
162
178
if ( ! symbol ) {
163
- return ;
179
+ return { } ;
164
180
}
165
181
const children = symbol . children ;
166
182
167
- // With gopls dymbol provider symbols , the symbols have the imports of all
183
+ // With gopls symbol provider, the symbols have the imports of all
168
184
// the package, so suite tests from all files will be found.
169
185
const testify = importsTestify ( symbols ) ;
170
- return children . filter (
186
+
187
+ const allTestFunctions = children . filter (
171
188
( sym ) =>
172
- ( sym . kind === vscode . SymbolKind . Function || sym . kind === vscode . SymbolKind . Method ) &&
189
+ sym . kind === vscode . SymbolKind . Function &&
173
190
// Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482
174
191
! testMainRegex . test ( doc . lineAt ( sym . range . start . line ) . text ) &&
175
- ( testFuncRegex . test ( sym . name ) || fuzzFuncRegx . test ( sym . name ) || ( testify && testMethodRegex . test ( sym . name ) ) )
192
+ ( testFuncRegex . test ( sym . name ) || fuzzFuncRegx . test ( sym . name ) )
176
193
) ;
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
+ } ;
177
228
}
178
229
179
230
/**
@@ -199,17 +250,16 @@ export function extractInstanceTestName(symbolName: string): string {
199
250
export function getTestFunctionDebugArgs (
200
251
document : vscode . TextDocument ,
201
252
testFunctionName : string ,
202
- testFunctions : vscode . DocumentSymbol [ ]
253
+ testFunctions : vscode . DocumentSymbol [ ] ,
254
+ suiteToFunc : SuiteToTestMap
203
255
) : string [ ] {
204
256
if ( benchmarkRegex . test ( testFunctionName ) ) {
205
257
return [ '-test.bench' , '^' + testFunctionName + '$' , '-test.run' , 'a^' ] ;
206
258
}
207
259
const instanceMethod = extractInstanceTestName ( testFunctionName ) ;
208
260
if ( instanceMethod ) {
209
- const testFns = findAllTestSuiteRuns ( document , testFunctions ) ;
210
- const testSuiteRuns = [ '-test.run' , `^${ testFns . map ( ( t ) => t . name ) . join ( '|' ) } $` ] ;
211
- const testSuiteTests = [ '-testify.m' , `^${ instanceMethod } $` ] ;
212
- return [ ...testSuiteRuns , ...testSuiteTests ] ;
261
+ const testFns = findAllTestSuiteRuns ( document , testFunctions , suiteToFunc ) ;
262
+ return [ '-test.run' , `^${ testFns . map ( ( t ) => t . name ) . join ( '|' ) } $/^${ instanceMethod } $` ] ;
213
263
} else {
214
264
return [ '-test.run' , `^${ testFunctionName } $` ] ;
215
265
}
@@ -222,12 +272,22 @@ export function getTestFunctionDebugArgs(
222
272
*/
223
273
export function findAllTestSuiteRuns (
224
274
doc : vscode . TextDocument ,
225
- allTests : vscode . DocumentSymbol [ ]
275
+ allTests : vscode . DocumentSymbol [ ] ,
276
+ suiteToFunc : SuiteToTestMap
226
277
) : vscode . DocumentSymbol [ ] {
227
- // get non-instance test functions
228
- const testFunctions = allTests ?. filter ( ( t ) => ! testMethodRegex . test ( t . name ) ) ;
229
- // filter further to ones containing suite.Run()
230
- return testFunctions ?. filter ( ( t ) => doc . getText ( t . range ) . includes ( 'suite.Run(' ) ) ?? [ ] ;
278
+ const suites = allTests
279
+ // Find all tests with receivers.
280
+ ?. map ( ( e ) => e . name . match ( testMethodRegex ) )
281
+ . filter ( ( e ) => e ?. length === 3 )
282
+ // Take out receiever, strip leading *.
283
+ . map ( ( e ) => e && e [ 1 ] . replace ( / ^ \* / g, '' ) )
284
+ // Map receiver name to test that runs "suite.Run".
285
+ . map ( ( e ) => e && suiteToFunc [ e ] )
286
+ // Filter out empty results.
287
+ . filter ( ( e ) : e is vscode . DocumentSymbol => ! ! e ) ;
288
+
289
+ // Dedup.
290
+ return [ ...new Set ( suites ) ] ;
231
291
}
232
292
233
293
/**
@@ -254,6 +314,59 @@ export async function getBenchmarkFunctions(
254
314
return children . filter ( ( sym ) => sym . kind === vscode . SymbolKind . Function && benchmarkRegex . test ( sym . name ) ) ;
255
315
}
256
316
317
+ export type SuiteToTestMap = Record < string , vscode . DocumentSymbol > ;
318
+
319
+ /**
320
+ * Returns a mapping between a package's function receivers to
321
+ * the test method that initiated them with "suite.Run".
322
+ *
323
+ * @param the URI of a Go source file.
324
+ * @return function symbols from all source files of the package, mapped by target suite names.
325
+ */
326
+ export async function getSuiteToTestMap (
327
+ goCtx : GoExtensionContext ,
328
+ doc : vscode . TextDocument ,
329
+ token ?: vscode . CancellationToken
330
+ ) {
331
+ // Get all the package documents.
332
+ const packageDir = path . parse ( doc . fileName ) . dir ;
333
+ const packageContent = await fs . readdir ( packageDir , { withFileTypes : true } ) ;
334
+ const packageFilenames = packageContent
335
+ // Only go files.
336
+ . filter ( ( dirent ) => dirent . isFile ( ) )
337
+ . map ( ( dirent ) => dirent . name )
338
+ . filter ( ( name ) => name . endsWith ( '.go' ) ) ;
339
+ const packageDocs = await Promise . all (
340
+ packageFilenames . map ( ( e ) => path . join ( packageDir , e ) ) . map ( vscode . workspace . openTextDocument )
341
+ ) ;
342
+
343
+ const suiteToTest : SuiteToTestMap = { } ;
344
+ for ( const packageDoc of packageDocs ) {
345
+ const funcs = await getTestFunctions ( goCtx , packageDoc , token ) ;
346
+ if ( ! funcs ) {
347
+ continue ;
348
+ }
349
+
350
+ for ( const func of funcs ) {
351
+ const funcText = packageDoc . getText ( func . range ) ;
352
+
353
+ // Matches run suites of the types:
354
+ // type1: suite.Run(t, MySuite{
355
+ // type1: suite.Run(t, &MySuite{
356
+ // type2: suite.Run(t, new(MySuite)
357
+ const matchRunSuite = funcText . match ( runTestSuiteRegex ) ;
358
+ if ( ! matchRunSuite ) {
359
+ continue ;
360
+ }
361
+
362
+ const g = matchRunSuite . groups ;
363
+ suiteToTest [ g ?. type1 || g ?. type2 || '' ] = func ;
364
+ }
365
+ }
366
+
367
+ return suiteToTest ;
368
+ }
369
+
257
370
/**
258
371
* go test -json output format.
259
372
* which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format
0 commit comments