Skip to content

Commit 10a39ef

Browse files
committed
gopls/internal/lsp/regtest: address additional comments on marker.go
This CL addresses code review comments from earlier CLs that were easier to to in a follow-up. Change-Id: Ib4bb4cd828377727bdc6dae606fb03d4e06024a6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/466143 Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]>
1 parent 69920f2 commit 10a39ef

File tree

1 file changed

+100
-63
lines changed

1 file changed

+100
-63
lines changed

gopls/internal/lsp/regtest/marker.go

+100-63
Original file line numberDiff line numberDiff line change
@@ -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.
337346
func 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.
542556
func 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.
663698
func (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

Comments
 (0)