@@ -36,7 +36,7 @@ import (
36
36
"golang.org/x/tools/txtar"
37
37
)
38
38
39
- var updateGolden = flag .Bool ("update" , false , "if set, update test data during marker tests" )
39
+ var update = flag .Bool ("update" , false , "if set, update test data during marker tests" )
40
40
41
41
// RunMarkerTests runs "marker" tests in the given test data directory.
42
42
//
@@ -90,9 +90,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
90
90
//
91
91
// There are three types of file within the test archive that are given special
92
92
// treatment by the test runner:
93
- // - "flags": this file is parsed as flags configuring the MarkerTest
94
- // instance. For example, -min_go=go1.18 sets the minimum required Go version
95
- // for the test.
93
+ // - "flags": this file is treated as a whitespace-separated list of flags
94
+ // that configure the MarkerTest instance. For example, -min_go=go1.18 sets
95
+ // the minimum required Go version for the test.
96
+ // TODO(rfindley): support flag values containing whitespace.
96
97
// - "settings.json": this file is parsed as JSON, and used as the
97
98
// session configuration (see gopls/doc/settings.md)
98
99
// - "env": this file is parsed as a list of VAR=VALUE fields specifying the
@@ -116,23 +117,32 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
116
117
// - hover(src, dst location, g Golden): perform a textDocument/hover at the
117
118
// src location, and checks that the result is the dst location, with hover
118
119
// content matching "hover.md" in the golden data g.
119
- // - loc(name, location): specifies the name of a location in the source. These
120
+ // - loc(name, location): specifies the name for a location in the source. These
120
121
// locations may be referenced by other markers.
121
122
//
122
123
// # Argument conversion
123
124
//
124
- // In additon to the types supported by go/expect, the marker test runner
125
- // applies the following argument conversions from argument type to parameter
126
- // type:
127
- // - string->regexp: the argument parsed as a regular expressions
128
- // - string->location: the location of the first instance of the
129
- // argument in the partial line preceding the note
130
- // - regexp->location: the location of the first match for the argument in
131
- // the partial line preceding the note If the regular expression contains
132
- // exactly one subgroup, the position of the subgroup is used rather than the
133
- // position of the submatch.
134
- // - name->location: the named location corresponding to the argument
135
- // - name->Golden: the golden content prefixed by @<argument>
125
+ // Marker arguments are first parsed by the go/expect package, which accepts
126
+ // the following tokens as defined by the Go spec:
127
+ // - string, int64, float64, and rune literals
128
+ // - true and false
129
+ // - nil
130
+ // - identifiers (type expect.Identifier)
131
+ // - regular expressions, denoted the two tokens re"abc" (type *regexp.Regexp)
132
+ //
133
+ // These values are passed as arguments to the corresponding parameter of the
134
+ // test function. Additional value conversions may occur for these argument ->
135
+ // parameter type pairs:
136
+ // - string->regexp: the argument is parsed as a regular expressions.
137
+ // - string->location: the argument is converted to the location of the first
138
+ // instance of the argument in the partial line preceding the note.
139
+ // - regexp->location: the argument is converted to the location of the first
140
+ // match for the argument in the partial line preceding the note. If the
141
+ // regular expression contains exactly one subgroup, the position of the
142
+ // subgroup is used rather than the position of the submatch.
143
+ // - name->location: the argument is replaced by the named location.
144
+ // - name->Golden: the argument is used to look up golden content prefixed by
145
+ // @<argument>.
136
146
//
137
147
// # Example
138
148
//
@@ -152,10 +162,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
152
162
// In this example, the @hover annotation tells the test runner to run the
153
163
// hoverMarker function, which has parameters:
154
164
//
155
- // (env *Env , src, dsc protocol.Location, g *Golden).
165
+ // (c *markerContext , src, dsc protocol.Location, g *Golden).
156
166
//
157
- // The env argument holds the implicit test environment , including fake editor
158
- // with open files, and sandboxed directory.
167
+ // The first argument holds the test context , including fake editor with open
168
+ // files, and sandboxed directory.
159
169
//
160
170
// Argument converters translate the "b" and "abc" arguments into locations by
161
171
// interpreting each one as a regular expression and finding the location of
@@ -182,7 +192,7 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
182
192
// at Go tip. Each test function can normalize golden content for older Go
183
193
// versions.
184
194
//
185
- // -update does not cause missing @diag markers to be added.
195
+ // Note that -update does not cause missing @diag or @loc markers to be added.
186
196
//
187
197
// # TODO
188
198
//
@@ -304,8 +314,7 @@ func RunMarkerTests(t *testing.T, dir string) {
304
314
for _ , note := range notes {
305
315
mi , ok := markers [note .Name ]
306
316
if ! ok {
307
- posn := safetoken .StartPosition (test .fset , note .Pos )
308
- t .Errorf ("%s: no marker function named %s" , posn , note .Name )
317
+ t .Errorf ("%s: no marker function named %s" , c .fmtPos (note .Pos ), note .Name )
309
318
continue
310
319
}
311
320
if err := runMarker (c , mi , note ); err != nil {
@@ -326,7 +335,7 @@ func RunMarkerTests(t *testing.T, dir string) {
326
335
// so we can now update the test data.
327
336
// TODO(rfindley): even when -update is not set, compare updated content with
328
337
// actual content.
329
- if * updateGolden {
338
+ if * update {
330
339
if err := writeMarkerTests (dir , tests ); err != nil {
331
340
t .Fatalf ("failed to -update: %v" , err )
332
341
}
@@ -335,11 +344,10 @@ func RunMarkerTests(t *testing.T, dir string) {
335
344
336
345
// runMarker calls mi.fn with the arguments coerced from note.
337
346
func runMarker (c * markerContext , mi markerInfo , note * expect.Note ) error {
338
- posn := safetoken .StartPosition (c .test .fset , note .Pos )
339
347
// The first converter corresponds to the *Env argument. All others
340
348
// must be coerced from the marker syntax.
341
349
if got , want := len (note .Args ), len (mi .converters ); got != want {
342
- return fmt .Errorf ("%s: got %d arguments to %s, expect %d" , posn , got , note .Name , want )
350
+ return fmt .Errorf ("%s: got %d arguments to %s, expect %d" , c . fmtPos ( note . Pos ) , got , note .Name , want )
343
351
}
344
352
345
353
args := []reflect.Value {reflect .ValueOf (c )}
@@ -353,7 +361,7 @@ func runMarker(c *markerContext, mi markerInfo, note *expect.Note) error {
353
361
}
354
362
out , err := mi .converters [i ](c , note , in )
355
363
if err != nil {
356
- return fmt .Errorf ("%s: converting argument #%d of %s (%v): %v" , posn , i , note .Name , in , err )
364
+ return fmt .Errorf ("%s: converting argument #%d of %s (%v): %v" , c . fmtPos ( note . Pos ) , i , note .Name , in , err )
357
365
}
358
366
args = append (args , reflect .ValueOf (out ))
359
367
}
@@ -426,9 +434,9 @@ type Golden struct {
426
434
//
427
435
// If -update is set, the given update function will be called to get the
428
436
// updated golden content that should be written back to testdata.
429
- func (g * Golden ) Get (t testing.TB , name string , update func () []byte ) []byte {
430
- if * updateGolden {
431
- d := update ()
437
+ func (g * Golden ) Get (t testing.TB , name string , getUpdate func () []byte ) []byte {
438
+ if * update {
439
+ d := getUpdate ()
432
440
if existing , ok := g .updated [name ]; ok {
433
441
// Multiple tests may reference the same golden data, but if they do they
434
442
// must agree about its expected content.
@@ -483,61 +491,67 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) {
483
491
golden : make (map [string ]* Golden ),
484
492
}
485
493
for _ , file := range archive .Files {
486
- if file .Name == "flags" {
494
+ switch {
495
+ case file .Name == "flags" :
487
496
test .flags = strings .Fields (string (file .Data ))
488
497
if err := test .flagSet ().Parse (test .flags ); err != nil {
489
498
return nil , fmt .Errorf ("parsing flags: %v" , err )
490
499
}
491
- continue
492
- }
493
- if file .Name == "settings.json" {
500
+
501
+ case file .Name == "settings.json" :
494
502
if err := json .Unmarshal (file .Data , & test .settings ); err != nil {
495
503
return nil , err
496
504
}
497
- continue
498
- }
499
- if file .Name == "env" {
505
+
506
+ case file .Name == "env" :
500
507
test .env = make (map [string ]string )
501
508
fields := strings .Fields (string (file .Data ))
502
509
for _ , field := range fields {
503
510
// TODO: use strings.Cut once we are on 1.18+.
504
- idx := strings . IndexByte (field , '=' )
505
- if idx < 0 {
511
+ key , value , ok := cut (field , "=" )
512
+ if ! ok {
506
513
return nil , fmt .Errorf ("env vars must be formatted as var=value, got %q" , field )
507
514
}
508
- test .env [field [: idx ]] = field [ idx + 1 :]
515
+ test .env [key ] = value
509
516
}
510
- continue
511
- }
512
- if strings .HasPrefix (file .Name , "@" ) { // golden content
513
- // TODO: use strings.Cut once we are on 1.18+.
514
- idx := strings .IndexByte (file .Name , '/' )
515
- if idx < 0 {
517
+
518
+ case strings .HasPrefix (file .Name , "@" ): // golden content
519
+ prefix , name , ok := cut (file .Name , "/" )
520
+ if ! ok {
516
521
return nil , fmt .Errorf ("golden file path %q must contain '/'" , file .Name )
517
522
}
518
- goldenID := file . Name [len ("@" ):idx ]
523
+ goldenID := prefix [len ("@" ):]
519
524
if _ , ok := test .golden [goldenID ]; ! ok {
520
525
test .golden [goldenID ] = & Golden {
521
526
id : goldenID ,
522
527
data : make (map [string ][]byte ),
523
528
}
524
529
}
525
- test .golden [goldenID ].data [file .Name [idx + len ("/" ):]] = file .Data
526
- continue
527
- }
530
+ test .golden [goldenID ].data [name ] = file .Data
528
531
529
- // ordinary file content
530
- notes , err := expect .Parse (test .fset , file .Name , file .Data )
531
- if err != nil {
532
- return nil , fmt .Errorf ("parsing notes in %q: %v" , file .Name , err )
532
+ default : // ordinary file content
533
+ notes , err := expect .Parse (test .fset , file .Name , file .Data )
534
+ if err != nil {
535
+ return nil , fmt .Errorf ("parsing notes in %q: %v" , file .Name , err )
536
+ }
537
+ test .notes = append (test .notes , notes ... )
538
+ test .files [file .Name ] = file .Data
533
539
}
534
- test .notes = append (test .notes , notes ... )
535
- test .files [file .Name ] = file .Data
536
540
}
537
541
538
542
return test , nil
539
543
}
540
544
545
+ // cut is a copy of strings.Cut.
546
+ //
547
+ // TODO: once we only support Go 1.18+, just use strings.Cut.
548
+ func cut (s , sep string ) (before , after string , found bool ) {
549
+ if i := strings .Index (s , sep ); i >= 0 {
550
+ return s [:i ], s [i + len (sep ):], true
551
+ }
552
+ return s , "" , false
553
+ }
554
+
541
555
// writeMarkerTests writes the updated golden content to the test data files.
542
556
func writeMarkerTests (dir string , tests []* MarkerTest ) error {
543
557
for _ , test := range tests {
@@ -657,8 +671,29 @@ type markerContext struct {
657
671
diags map [protocol.Location ][]protocol.Diagnostic
658
672
}
659
673
674
+ // fmtLoc formats the given pos in the context of the test, using
675
+ // archive-relative paths for files and including the line number in the full
676
+ // archive file.
677
+ func (c markerContext ) fmtPos (pos token.Pos ) string {
678
+ file := c .test .fset .File (pos )
679
+ if file == nil {
680
+ c .env .T .Errorf ("position %d not in test fileset" , pos )
681
+ return "<invalid location>"
682
+ }
683
+ m := c .env .Editor .Mapper (file .Name ())
684
+ if m == nil {
685
+ c .env .T .Errorf ("%s is not open" , file .Name ())
686
+ return "<invalid location>"
687
+ }
688
+ loc , err := m .PosLocation (file , pos , pos )
689
+ if err != nil {
690
+ c .env .T .Errorf ("Mapper(%s).PosLocation failed: %v" , file .Name (), err )
691
+ }
692
+ return c .fmtLoc (loc )
693
+ }
694
+
660
695
// fmtLoc formats the given location in the context of the test, using
661
- // archive-relative paths for files, and including the line number in the full
696
+ // archive-relative paths for files and including the line number in the full
662
697
// archive file.
663
698
func (c markerContext ) fmtLoc (loc protocol.Location ) string {
664
699
if loc == (protocol.Location {}) {
@@ -688,12 +723,14 @@ func (c markerContext) fmtLoc(loc protocol.Location) string {
688
723
689
724
innerSpan := fmt .Sprintf ("%d:%d" , s .Start ().Line (), s .Start ().Column ()) // relative to the embedded file
690
725
outerSpan := fmt .Sprintf ("%d:%d" , lines + s .Start ().Line (), s .Start ().Column ()) // relative to the archive file
691
- if s .End ().Line () == s .Start ().Line () {
692
- innerSpan += fmt .Sprintf ("-%d" , s .End ().Column ())
693
- outerSpan += fmt .Sprintf ("-%d" , s .End ().Column ())
694
- } else {
695
- innerSpan += fmt .Sprintf ("-%d:%d" , s .End ().Line (), s .End ().Column ())
696
- innerSpan += fmt .Sprintf ("-%d:%d" , lines + s .End ().Line (), s .End ().Column ())
726
+ if s .Start () != s .End () {
727
+ if s .End ().Line () == s .Start ().Line () {
728
+ innerSpan += fmt .Sprintf ("-%d" , s .End ().Column ())
729
+ outerSpan += fmt .Sprintf ("-%d" , s .End ().Column ())
730
+ } else {
731
+ innerSpan += fmt .Sprintf ("-%d:%d" , s .End ().Line (), s .End ().Column ())
732
+ innerSpan += fmt .Sprintf ("-%d:%d" , lines + s .End ().Line (), s .End ().Column ())
733
+ }
697
734
}
698
735
699
736
return fmt .Sprintf ("%s:%s (%s:%s)" , name , innerSpan , c .test .name , outerSpan )
0 commit comments