@@ -73,7 +73,7 @@ export function setupTestExplorer(context: ExtensionContext) {
73
73
const found = find ( ctrl . root ) ;
74
74
if ( found ) {
75
75
found . dispose ( ) ;
76
- removeIfEmpty ( found . parent ) ;
76
+ disposeIfEmpty ( found . parent ) ;
77
77
}
78
78
} ) ;
79
79
@@ -99,17 +99,26 @@ export function setupTestExplorer(context: ExtensionContext) {
99
99
) ;
100
100
}
101
101
102
+ // Construct an ID for an item.
103
+ // - Module: file:///path/to/mod?module
104
+ // - Package: file:///path/to/mod/pkg?package
105
+ // - File: file:///path/to/mod/file.go?file
106
+ // - Test: file:///path/to/mod/file.go?test#TestXxx
107
+ // - Benchmark: file:///path/to/mod/file.go?benchmark#BenchmarkXxx
108
+ // - Example: file:///path/to/mod/file.go?example#ExampleXxx
102
109
function testID ( uri : Uri , kind : string , name ?: string ) : string {
103
110
uri = uri . with ( { query : kind } ) ;
104
111
if ( name ) uri = uri . with ( { fragment : name } ) ;
105
112
return uri . toString ( ) ;
106
113
}
107
114
115
+ // Retrieve a child item.
108
116
function getItem ( parent : TestItem , uri : Uri , kind : string , name ?: string ) : TestItem | undefined {
109
117
return parent . children . get ( testID ( uri , kind , name ) ) ;
110
118
}
111
119
112
- function createItem (
120
+ // Create or Retrieve a child item.
121
+ function getOrCreateItem (
113
122
ctrl : TestController ,
114
123
parent : TestItem ,
115
124
label : string ,
@@ -126,7 +135,9 @@ function createItem(
126
135
return ctrl . createTestItem ( id , label , parent , uri . with ( { query : '' , fragment : '' } ) ) ;
127
136
}
128
137
129
- function createSubItem ( ctrl : TestController , item : TestItem , name : string ) : TestItem {
138
+ // Create or Retrieve a sub test or benchmark. The ID will be of the form:
139
+ // file:///path/to/mod/file.go?test#TestXxx/A/B/C
140
+ function getOrCreateSubTest ( ctrl : TestController , item : TestItem , name : string ) : TestItem {
130
141
let uri = Uri . parse ( item . id ) ;
131
142
uri = uri . with ( { fragment : `${ uri . fragment } /${ name } ` } ) ;
132
143
const existing = item . children . get ( uri . toString ( ) ) ;
@@ -141,7 +152,9 @@ function createSubItem(ctrl: TestController, item: TestItem, name: string): Test
141
152
return sub ;
142
153
}
143
154
144
- function removeIfEmpty ( item : TestItem ) {
155
+ // Dispose of the item if it has no children, recursively. This facilitates
156
+ // cleaning up package/file trees that contain no tests.
157
+ function disposeIfEmpty ( item : TestItem ) {
145
158
// Don't dispose of the root
146
159
if ( ! item . parent ) {
147
160
return ;
@@ -158,9 +171,10 @@ function removeIfEmpty(item: TestItem) {
158
171
}
159
172
160
173
item . dispose ( ) ;
161
- removeIfEmpty ( item . parent ) ;
174
+ disposeIfEmpty ( item . parent ) ;
162
175
}
163
176
177
+ // Retrieve or create an item for a Go module.
164
178
async function getModule ( ctrl : TestController , uri : Uri ) : Promise < TestItem > {
165
179
const existing = getItem ( ctrl . root , uri , 'module' ) ;
166
180
if ( existing ) {
@@ -172,25 +186,27 @@ async function getModule(ctrl: TestController, uri: Uri): Promise<TestItem> {
172
186
const contents = await workspace . fs . readFile ( goMod ) ;
173
187
const modLine = contents . toString ( ) . split ( '\n' , 2 ) [ 0 ] ;
174
188
const match = modLine . match ( / ^ m o d u l e (?< name > .* ?) (?: \s | \/ \/ | $ ) / ) ;
175
- const item = createItem ( ctrl , ctrl . root , match . groups . name , uri , 'module' ) ;
189
+ const item = getOrCreateItem ( ctrl , ctrl . root , match . groups . name , uri , 'module' ) ;
176
190
item . canResolveChildren = true ;
177
191
item . runnable = true ;
178
192
return item ;
179
193
}
180
194
195
+ // Retrieve or create an item for a workspace folder that is not a module.
181
196
async function getWorkspace ( ctrl : TestController , ws : WorkspaceFolder ) : Promise < TestItem > {
182
197
const existing = getItem ( ctrl . root , ws . uri , 'workspace' ) ;
183
198
if ( existing ) {
184
199
return existing ;
185
200
}
186
201
187
202
// Use the workspace folder name as the label
188
- const item = createItem ( ctrl , ctrl . root , ws . name , ws . uri , 'workspace' ) ;
203
+ const item = getOrCreateItem ( ctrl , ctrl . root , ws . name , ws . uri , 'workspace' ) ;
189
204
item . canResolveChildren = true ;
190
205
item . runnable = true ;
191
206
return item ;
192
207
}
193
208
209
+ // Retrieve or create an item for a Go package.
194
210
async function getPackage ( ctrl : TestController , uri : Uri ) : Promise < TestItem > {
195
211
let item : TestItem ;
196
212
@@ -210,7 +226,7 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
210
226
}
211
227
212
228
const label = uri . path . startsWith ( modUri . path ) ? uri . path . substring ( modUri . path . length + 1 ) : uri . path ;
213
- item = createItem ( ctrl , module , label , uri , 'package' ) ;
229
+ item = getOrCreateItem ( ctrl , module , label , uri , 'package' ) ;
214
230
} else if ( wsfolder ) {
215
231
// If the package is in a workspace folder, add it as a child of the workspace
216
232
const workspace = await getWorkspace ( ctrl , wsfolder ) ;
@@ -222,7 +238,7 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
222
238
const label = uri . path . startsWith ( wsfolder . uri . path )
223
239
? uri . path . substring ( wsfolder . uri . path . length + 1 )
224
240
: uri . path ;
225
- item = createItem ( ctrl , workspace , label , uri , 'package' ) ;
241
+ item = getOrCreateItem ( ctrl , workspace , label , uri , 'package' ) ;
226
242
} else {
227
243
// Otherwise, add it directly to the root
228
244
const existing = getItem ( ctrl . root , uri , 'package' ) ;
@@ -232,14 +248,15 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
232
248
233
249
const srcPath = path . join ( getCurrentGoPath ( uri ) , 'src' ) ;
234
250
const label = uri . path . startsWith ( srcPath ) ? uri . path . substring ( srcPath . length + 1 ) : uri . path ;
235
- item = createItem ( ctrl , ctrl . root , label , uri , 'package' ) ;
251
+ item = getOrCreateItem ( ctrl , ctrl . root , label , uri , 'package' ) ;
236
252
}
237
253
238
254
item . canResolveChildren = true ;
239
255
item . runnable = true ;
240
256
return item ;
241
257
}
242
258
259
+ // Retrieve or create an item for a Go file.
243
260
async function getFile ( ctrl : TestController , uri : Uri ) : Promise < TestItem > {
244
261
const dir = path . dirname ( uri . path ) ;
245
262
const pkg = await getPackage ( ctrl , uri . with ( { path : dir } ) ) ;
@@ -249,12 +266,16 @@ async function getFile(ctrl: TestController, uri: Uri): Promise<TestItem> {
249
266
}
250
267
251
268
const label = path . basename ( uri . path ) ;
252
- const item = createItem ( ctrl , pkg , label , uri , 'file' ) ;
269
+ const item = getOrCreateItem ( ctrl , pkg , label , uri , 'file' ) ;
253
270
item . canResolveChildren = true ;
254
271
item . runnable = true ;
255
272
return item ;
256
273
}
257
274
275
+ // Recursively process a Go AST symbol. If the symbol represents a test,
276
+ // benchmark, or example function, a test item will be created for it, if one
277
+ // does not already exist. If the symbol is not a function and contains
278
+ // children, those children will be processed recursively.
258
279
async function processSymbol (
259
280
ctrl : TestController ,
260
281
uri : Uri ,
@@ -286,14 +307,20 @@ async function processSymbol(
286
307
return existing ;
287
308
}
288
309
289
- const item = createItem ( ctrl , file , symbol . name , uri , kind , symbol . name ) ;
310
+ const item = getOrCreateItem ( ctrl , file , symbol . name , uri , kind , symbol . name ) ;
290
311
item . range = symbol . range ;
291
312
item . runnable = true ;
292
313
// item.debuggable = true;
293
314
symbols . set ( item , symbol ) ;
294
315
}
295
316
296
- async function loadFileTests ( ctrl : TestController , doc : TextDocument ) {
317
+ // Processes a Go document, calling processSymbol for each symbol in the
318
+ // document.
319
+ //
320
+ // Any previously existing tests that no longer have a corresponding symbol in
321
+ // the file will be disposed. If the document contains no tests, it will be
322
+ // disposed.
323
+ async function processDocument ( ctrl : TestController , doc : TextDocument ) {
297
324
const seen = new Set < string > ( ) ;
298
325
const item = await getFile ( ctrl , doc . uri ) ;
299
326
const symbols = await new GoDocumentSymbolProvider ( ) . provideDocumentSymbols ( doc , null ) ;
@@ -306,18 +333,19 @@ async function loadFileTests(ctrl: TestController, doc: TextDocument) {
306
333
}
307
334
}
308
335
309
- removeIfEmpty ( item ) ;
336
+ disposeIfEmpty ( item ) ;
310
337
}
311
338
339
+ // Reasons to stop walking
312
340
enum WalkStop {
313
- None = 0 ,
314
- Abort ,
315
- Current ,
316
- Files ,
317
- Directories
341
+ None = 0 , // Don't stop
342
+ Abort , // Abort the walk
343
+ Current , // Stop walking the current directory
344
+ Files , // Skip remaining files
345
+ Directories // Skip remaining directories
318
346
}
319
347
320
- // Recursively walk a directory, breadth first
348
+ // Recursively walk a directory, breadth first.
321
349
async function walk (
322
350
uri : Uri ,
323
351
cb : ( dir : Uri , file : string , type : FileType ) => Promise < WalkStop | undefined >
@@ -383,7 +411,10 @@ async function walk(
383
411
}
384
412
}
385
413
386
- async function walkWorkspaces ( uri : Uri ) {
414
+ // Walk the workspace, looking for Go modules. Returns a map indicating paths
415
+ // that are modules (value == true) and paths that are not modules but contain
416
+ // Go files (value == false).
417
+ async function walkWorkspaces ( uri : Uri ) : Promise < Map < string , boolean > > {
387
418
const found = new Map < string , boolean > ( ) ;
388
419
await walk ( uri , async ( dir , file , type ) => {
389
420
if ( type !== FileType . File ) {
@@ -402,6 +433,8 @@ async function walkWorkspaces(uri: Uri) {
402
433
return found ;
403
434
}
404
435
436
+ // Walk the workspace, calling the callback for any directory that contains a Go
437
+ // test file.
405
438
async function walkPackages ( uri : Uri , cb : ( uri : Uri ) => Promise < unknown > ) {
406
439
await walk ( uri , async ( dir , file ) => {
407
440
if ( file . endsWith ( '_test.go' ) ) {
@@ -411,6 +444,7 @@ async function walkPackages(uri: Uri, cb: (uri: Uri) => Promise<unknown>) {
411
444
} ) ;
412
445
}
413
446
447
+ // Handle opened documents, document changes, and file creation.
414
448
async function documentUpdate ( ctrl : TestController , doc : TextDocument ) {
415
449
if ( ! doc . uri . path . endsWith ( '_test.go' ) ) {
416
450
return ;
@@ -421,10 +455,12 @@ async function documentUpdate(ctrl: TestController, doc: TextDocument) {
421
455
return ;
422
456
}
423
457
424
- await loadFileTests ( ctrl , doc ) ;
458
+ await processDocument ( ctrl , doc ) ;
425
459
}
426
460
461
+ // TestController.resolveChildrenHandler callback
427
462
async function resolveChildren ( ctrl : TestController , item : TestItem ) {
463
+ // The user expanded the root item - find all modules and workspaces
428
464
if ( ! item . parent ) {
429
465
// Dispose of package entries at the root if they are now part of a workspace folder
430
466
const items = Array . from ( ctrl . root . children . values ( ) ) ;
@@ -461,15 +497,16 @@ async function resolveChildren(ctrl: TestController, item: TestItem) {
461
497
}
462
498
463
499
const uri = Uri . parse ( item . id ) ;
500
+
501
+ // The user expanded a module or workspace - find all packages
464
502
if ( uri . query === 'module' || uri . query === 'workspace' ) {
465
- // Create entries for all packages in the module or workspace
466
503
await walkPackages ( uri , async ( uri ) => {
467
504
await getPackage ( ctrl , uri ) ;
468
505
} ) ;
469
506
}
470
507
508
+ // The user expanded a module or package - find all files
471
509
if ( uri . query === 'module' || uri . query === 'package' ) {
472
- // Create entries for all test files in the package
473
510
for ( const [ file , type ] of await workspace . fs . readDirectory ( uri ) ) {
474
511
if ( type !== FileType . File || ! file . endsWith ( '_test.go' ) ) {
475
512
continue ;
@@ -479,13 +516,19 @@ async function resolveChildren(ctrl: TestController, item: TestItem) {
479
516
}
480
517
}
481
518
519
+ // The user expanded a file - find all functions
482
520
if ( uri . query === 'file' ) {
483
- // Create entries for all test functions in a file
484
521
const doc = await workspace . openTextDocument ( uri . with ( { query : '' , fragment : '' } ) ) ;
485
- await loadFileTests ( ctrl , doc ) ;
522
+ await processDocument ( ctrl , doc ) ;
486
523
}
524
+
525
+ // TODO(firelizzard18): If uri.query is test or benchmark, this is where we
526
+ // would discover sub tests or benchmarks, if that is feasible.
487
527
}
488
528
529
+ // Recursively find all tests, benchmarks, and examples within a
530
+ // module/package/etc, minus exclusions. Map tests to the package they are
531
+ // defined in, and track files.
489
532
async function collectTests (
490
533
ctrl : TestController ,
491
534
item : TestItem ,
@@ -523,6 +566,8 @@ async function collectTests(
523
566
return ;
524
567
}
525
568
569
+ // TestRunOutput is a fake OutputChannel that forwards all test output to the test API
570
+ // console.
526
571
class TestRunOutput < T > implements OutputChannel {
527
572
readonly name : string ;
528
573
constructor ( private run : TestRun < T > ) {
@@ -544,6 +589,9 @@ class TestRunOutput<T> implements OutputChannel {
544
589
dispose ( ) { }
545
590
}
546
591
592
+ // Resolve a test name to a test item. If the test name is TestXxx/Foo, Foo is
593
+ // created as a child of TestXxx. The same is true for TestXxx#Foo and
594
+ // TestXxx/#Foo.
547
595
function resolveTestName ( ctrl : TestController , tests : Record < string , TestItem > , name : string ) : TestItem | undefined {
548
596
if ( ! name ) {
549
597
return ;
@@ -556,12 +604,12 @@ function resolveTestName(ctrl: TestController, tests: Record<string, TestItem>,
556
604
}
557
605
558
606
for ( const part of parts . slice ( 1 ) ) {
559
- test = createSubItem ( ctrl , test , part ) ;
607
+ test = getOrCreateSubTest ( ctrl , test , part ) ;
560
608
}
561
609
return test ;
562
610
}
563
611
564
- // Process benchmark test events (see test_events.md)
612
+ // Process benchmark events (see test_events.md)
565
613
function consumeGoBenchmarkEvent < T > (
566
614
ctrl : TestController ,
567
615
run : TestRun < T > ,
@@ -643,6 +691,7 @@ function passBenchmarks<T>(run: TestRun<T>, items: Record<string, TestItem>, com
643
691
}
644
692
}
645
693
694
+ // Process test events (see test_events.md)
646
695
function consumeGoTestEvent < T > (
647
696
ctrl : TestController ,
648
697
run : TestRun < T > ,
@@ -687,6 +736,8 @@ function consumeGoTestEvent<T>(
687
736
}
688
737
}
689
738
739
+ // Search recorded test output for `file.go:123: Foo bar` and attach a message
740
+ // to the corresponding location.
690
741
function processRecordedOutput < T > ( run : TestRun < T > , test : TestItem , output : string [ ] ) {
691
742
// mostly copy and pasted from https://gitlab.com/firelizzard/vscode-go-test-adapter/-/blob/733443d229df68c90145a5ae7ed78ca64dec6f43/src/tests.ts
692
743
type message = { all : string ; error ?: string } ;
@@ -729,14 +780,16 @@ function processRecordedOutput<T>(run: TestRun<T>, test: TestItem, output: strin
729
780
}
730
781
}
731
782
783
+ // Execute tests - TestController.runTest callback
732
784
async function runTest < T > ( ctrl : TestController , request : TestRunRequest < T > ) {
733
785
const collected = new Map < string , TestItem [ ] > ( ) ;
734
786
const docs = new Set < Uri > ( ) ;
735
787
for ( const item of request . tests ) {
736
788
await collectTests ( ctrl , item , request . exclude , collected , docs ) ;
737
789
}
738
790
739
- // Ensure `go test` has the latest changes
791
+ // Save all documents that contain a test we're about to run, to ensure `go
792
+ // test` has the latest changes
740
793
await Promise . all (
741
794
Array . from ( docs ) . map ( ( uri ) => {
742
795
workspace . openTextDocument ( uri ) . then ( ( doc ) => doc . save ( ) ) ;
@@ -751,12 +804,13 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
751
804
const isMod = await isModSupported ( uri , true ) ;
752
805
const flags = getTestFlags ( goConfig ) ;
753
806
807
+ // Separate tests and benchmarks and mark them as queued for execution.
808
+ // Clear any sub tests/benchmarks generated by a previous run.
754
809
const tests : Record < string , TestItem > = { } ;
755
810
const benchmarks : Record < string , TestItem > = { } ;
756
811
for ( const item of items ) {
757
812
run . setState ( item , TestResultState . Queued ) ;
758
813
759
- // Clear any dynamic subtests generated by a previous run
760
814
item . canResolveChildren = false ;
761
815
Array . from ( item . children . values ( ) ) . forEach ( ( x ) => x . dispose ( ) ) ;
762
816
@@ -772,8 +826,8 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
772
826
const testFns = Object . keys ( tests ) ;
773
827
const benchmarkFns = Object . keys ( benchmarks ) ;
774
828
829
+ // Run tests
775
830
if ( testFns . length > 0 ) {
776
- // Run tests
777
831
await goTest ( {
778
832
goConfig,
779
833
flags,
@@ -785,8 +839,8 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
785
839
} ) ;
786
840
}
787
841
842
+ // Run benchmarks
788
843
if ( benchmarkFns . length > 0 ) {
789
- // Run benchmarks
790
844
const complete = new Set < TestItem > ( ) ;
791
845
await goTest ( {
792
846
goConfig,
@@ -803,6 +857,7 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
803
857
passBenchmarks ( run , benchmarks , complete ) ;
804
858
}
805
859
860
+ // Create test messages
806
861
for ( const [ test , output ] of record . entries ( ) ) {
807
862
processRecordedOutput ( run , test , output ) ;
808
863
}
0 commit comments