@@ -36,7 +36,7 @@ import (
3636 "golang.org/x/tools/txtar"
3737)
3838
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" )
4040
4141// RunMarkerTests runs "marker" tests in the given test data directory.
4242//
@@ -90,9 +90,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
9090//
9191// There are three types of file within the test archive that are given special
9292// 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.
9697// - "settings.json": this file is parsed as JSON, and used as the
9798// session configuration (see gopls/doc/settings.md)
9899// - "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
116117// - hover(src, dst location, g Golden): perform a textDocument/hover at the
117118// src location, and checks that the result is the dst location, with hover
118119// 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
120121// locations may be referenced by other markers.
121122//
122123// # Argument conversion
123124//
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>.
136146//
137147// # Example
138148//
@@ -152,10 +162,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m
152162// In this example, the @hover annotation tells the test runner to run the
153163// hoverMarker function, which has parameters:
154164//
155- // (env *Env , src, dsc protocol.Location, g *Golden).
165+ // (c *markerContext , src, dsc protocol.Location, g *Golden).
156166//
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.
159169//
160170// Argument converters translate the "b" and "abc" arguments into locations by
161171// 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
182192// at Go tip. Each test function can normalize golden content for older Go
183193// versions.
184194//
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.
186196//
187197// # TODO
188198//
@@ -304,8 +314,7 @@ func RunMarkerTests(t *testing.T, dir string) {
304314 for _ , note := range notes {
305315 mi , ok := markers [note .Name ]
306316 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 )
309318 continue
310319 }
311320 if err := runMarker (c , mi , note ); err != nil {
@@ -326,7 +335,7 @@ func RunMarkerTests(t *testing.T, dir string) {
326335 // so we can now update the test data.
327336 // TODO(rfindley): even when -update is not set, compare updated content with
328337 // actual content.
329- if * updateGolden {
338+ if * update {
330339 if err := writeMarkerTests (dir , tests ); err != nil {
331340 t .Fatalf ("failed to -update: %v" , err )
332341 }
@@ -335,11 +344,10 @@ func RunMarkerTests(t *testing.T, dir string) {
335344
336345// runMarker calls mi.fn with the arguments coerced from note.
337346func runMarker (c * markerContext , mi markerInfo , note * expect.Note ) error {
338- posn := safetoken .StartPosition (c .test .fset , note .Pos )
339347 // The first converter corresponds to the *Env argument. All others
340348 // must be coerced from the marker syntax.
341349 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 )
343351 }
344352
345353 args := []reflect.Value {reflect .ValueOf (c )}
@@ -353,7 +361,7 @@ func runMarker(c *markerContext, mi markerInfo, note *expect.Note) error {
353361 }
354362 out , err := mi .converters [i ](c , note , in )
355363 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 )
357365 }
358366 args = append (args , reflect .ValueOf (out ))
359367 }
@@ -426,9 +434,9 @@ type Golden struct {
426434//
427435// If -update is set, the given update function will be called to get the
428436// 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 ()
432440 if existing , ok := g .updated [name ]; ok {
433441 // Multiple tests may reference the same golden data, but if they do they
434442 // must agree about its expected content.
@@ -483,61 +491,67 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) {
483491 golden : make (map [string ]* Golden ),
484492 }
485493 for _ , file := range archive .Files {
486- if file .Name == "flags" {
494+ switch {
495+ case file .Name == "flags" :
487496 test .flags = strings .Fields (string (file .Data ))
488497 if err := test .flagSet ().Parse (test .flags ); err != nil {
489498 return nil , fmt .Errorf ("parsing flags: %v" , err )
490499 }
491- continue
492- }
493- if file .Name == "settings.json" {
500+
501+ case file .Name == "settings.json" :
494502 if err := json .Unmarshal (file .Data , & test .settings ); err != nil {
495503 return nil , err
496504 }
497- continue
498- }
499- if file .Name == "env" {
505+
506+ case file .Name == "env" :
500507 test .env = make (map [string ]string )
501508 fields := strings .Fields (string (file .Data ))
502509 for _ , field := range fields {
503510 // 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 {
506513 return nil , fmt .Errorf ("env vars must be formatted as var=value, got %q" , field )
507514 }
508- test .env [field [: idx ]] = field [ idx + 1 :]
515+ test .env [key ] = value
509516 }
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 {
516521 return nil , fmt .Errorf ("golden file path %q must contain '/'" , file .Name )
517522 }
518- goldenID := file . Name [len ("@" ):idx ]
523+ goldenID := prefix [len ("@" ):]
519524 if _ , ok := test .golden [goldenID ]; ! ok {
520525 test .golden [goldenID ] = & Golden {
521526 id : goldenID ,
522527 data : make (map [string ][]byte ),
523528 }
524529 }
525- test .golden [goldenID ].data [file .Name [idx + len ("/" ):]] = file .Data
526- continue
527- }
530+ test .golden [goldenID ].data [name ] = file .Data
528531
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
533539 }
534- test .notes = append (test .notes , notes ... )
535- test .files [file .Name ] = file .Data
536540 }
537541
538542 return test , nil
539543}
540544
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+
541555// writeMarkerTests writes the updated golden content to the test data files.
542556func writeMarkerTests (dir string , tests []* MarkerTest ) error {
543557 for _ , test := range tests {
@@ -657,8 +671,29 @@ type markerContext struct {
657671 diags map [protocol.Location ][]protocol.Diagnostic
658672}
659673
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+
660695// 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
662697// archive file.
663698func (c markerContext ) fmtLoc (loc protocol.Location ) string {
664699 if loc == (protocol.Location {}) {
@@ -688,12 +723,14 @@ func (c markerContext) fmtLoc(loc protocol.Location) string {
688723
689724 innerSpan := fmt .Sprintf ("%d:%d" , s .Start ().Line (), s .Start ().Column ()) // relative to the embedded file
690725 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+ }
697734 }
698735
699736 return fmt .Sprintf ("%s:%s (%s:%s)" , name , innerSpan , c .test .name , outerSpan )
0 commit comments