@@ -11,6 +11,7 @@ import cp = require('child_process');
1111import  path  =  require( 'path' ) ; 
1212import  util  =  require( 'util' ) ; 
1313import  vscode  =  require( 'vscode' ) ; 
14+ import  {  promises  as  fs  }  from  'fs' ; 
1415
1516import  {  applyCodeCoverageToAllEditors  }  from  './goCover' ; 
1617import  {  toolExecutionEnvironment  }  from  './goEnv' ; 
@@ -50,6 +51,7 @@ const testMethodRegex = /^\(([^)]+)\)\.(Test|Test\P{Ll}.*)$/u;
5051const  benchmarkRegex  =  / ^ B e n c h m a r k $ | ^ B e n c h m a r k \P{ Ll}  .* / u; 
5152const  fuzzFuncRegx  =  / ^ F u z z $ | ^ F u z z \P{ Ll}  .* / u; 
5253const  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; 
5355
5456/** 
5557 * Input to goTest. 
@@ -153,27 +155,76 @@ export async function getTestFunctions(
153155	doc : vscode . TextDocument , 
154156	token ?: vscode . CancellationToken 
155157) : 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  } >  { 
156172	const  documentSymbolProvider  =  GoDocumentSymbolProvider ( goCtx ,  true ) ; 
157173	const  symbols  =  await  documentSymbolProvider . provideDocumentSymbols ( doc ) ; 
158174	if  ( ! symbols  ||  symbols . length  ===  0 )  { 
159- 		return ; 
175+ 		return   { } ; 
160176	} 
161177	const  symbol  =  symbols [ 0 ] ; 
162178	if  ( ! symbol )  { 
163- 		return ; 
179+ 		return   { } ; 
164180	} 
165181	const  children  =  symbol . children ; 
166182
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 
168184	// the package, so suite tests from all files will be found. 
169185	const  testify  =  importsTestify ( symbols ) ; 
170- 	return  children . filter ( 
186+ 
187+ 	const  allTestFunctions  =  children . filter ( 
171188		( sym )  => 
172- 			( sym . kind  ===  vscode . SymbolKind . Function   ||   sym . kind   ===   vscode . SymbolKind . Method )  && 
189+ 			sym . kind  ===  vscode . SymbolKind . Function  && 
173190			// Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482 
174191			! 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 ) ) 
176193	) ; 
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+ 	} ; 
177228} 
178229
179230/** 
@@ -199,17 +250,16 @@ export function extractInstanceTestName(symbolName: string): string {
199250export  function  getTestFunctionDebugArgs ( 
200251	document : vscode . TextDocument , 
201252	testFunctionName : string , 
202- 	testFunctions : vscode . DocumentSymbol [ ] 
253+ 	testFunctions : vscode . DocumentSymbol [ ] , 
254+ 	suiteToFunc : SuiteToTestMap 
203255) : string [ ]  { 
204256	if  ( benchmarkRegex . test ( testFunctionName ) )  { 
205257		return  [ '-test.bench' ,  '^'  +  testFunctionName  +  '$' ,  '-test.run' ,  'a^' ] ; 
206258	} 
207259	const  instanceMethod  =  extractInstanceTestName ( testFunctionName ) ; 
208260	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 }  $` ] ; 
213263	}  else  { 
214264		return  [ '-test.run' ,  `^${ testFunctionName }  $` ] ; 
215265	} 
@@ -222,12 +272,22 @@ export function getTestFunctionDebugArgs(
222272 */ 
223273export  function  findAllTestSuiteRuns ( 
224274	doc : vscode . TextDocument , 
225- 	allTests : vscode . DocumentSymbol [ ] 
275+ 	allTests : vscode . DocumentSymbol [ ] , 
276+ 	suiteToFunc : SuiteToTestMap 
226277) : 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 ) ] ; 
231291} 
232292
233293/** 
@@ -254,6 +314,59 @@ export async function getBenchmarkFunctions(
254314	return  children . filter ( ( sym )  =>  sym . kind  ===  vscode . SymbolKind . Function  &&  benchmarkRegex . test ( sym . name ) ) ; 
255315} 
256316
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+ 
257370/** 
258371 * go test -json output format. 
259372 * which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format 
0 commit comments