golang.org/x/tools/gopls@v0.15.3/internal/test/marker/marker_test.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package marker 6 7 // This file defines the marker test framework. 8 // See doc.go for extensive documentation. 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/json" 14 "flag" 15 "fmt" 16 "go/token" 17 "go/types" 18 "io/fs" 19 "log" 20 "os" 21 "path" 22 "path/filepath" 23 "reflect" 24 "regexp" 25 "runtime" 26 "sort" 27 "strings" 28 "testing" 29 30 "github.com/google/go-cmp/cmp" 31 32 "golang.org/x/tools/go/expect" 33 "golang.org/x/tools/gopls/internal/cache" 34 "golang.org/x/tools/gopls/internal/debug" 35 "golang.org/x/tools/gopls/internal/hooks" 36 "golang.org/x/tools/gopls/internal/lsprpc" 37 "golang.org/x/tools/gopls/internal/protocol" 38 "golang.org/x/tools/gopls/internal/test/compare" 39 "golang.org/x/tools/gopls/internal/test/integration" 40 "golang.org/x/tools/gopls/internal/test/integration/fake" 41 "golang.org/x/tools/gopls/internal/util/bug" 42 "golang.org/x/tools/gopls/internal/util/safetoken" 43 "golang.org/x/tools/internal/diff" 44 "golang.org/x/tools/internal/diff/myers" 45 "golang.org/x/tools/internal/jsonrpc2" 46 "golang.org/x/tools/internal/jsonrpc2/servertest" 47 "golang.org/x/tools/internal/testenv" 48 "golang.org/x/tools/txtar" 49 ) 50 51 var update = flag.Bool("update", false, "if set, update test data during marker tests") 52 53 func TestMain(m *testing.M) { 54 bug.PanicOnBugs = true 55 testenv.ExitIfSmallMachine() 56 // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. 57 os.Setenv("GOPACKAGESDRIVER", "off") 58 os.Exit(m.Run()) 59 } 60 61 // Test runs the marker tests from the testdata directory. 62 // 63 // See package documentation for details on how marker tests work. 64 // 65 // These tests were inspired by (and in many places copied from) a previous 66 // iteration of the marker tests built on top of the packagestest framework. 67 // Key design decisions motivating this reimplementation are as follows: 68 // - The old tests had a single global session, causing interaction at a 69 // distance and several awkward workarounds. 70 // - The old tests could not be safely parallelized, because certain tests 71 // manipulated the server options 72 // - Relatedly, the old tests did not have a logic grouping of assertions into 73 // a single unit, resulting in clusters of files serving clusters of 74 // entangled assertions. 75 // - The old tests used locations in the source as test names and as the 76 // identity of golden content, meaning that a single edit could change the 77 // name of an arbitrary number of subtests, and making it difficult to 78 // manually edit golden content. 79 // - The old tests did not hew closely to LSP concepts, resulting in, for 80 // example, each marker implementation doing its own position 81 // transformations, and inventing its own mechanism for configuration. 82 // - The old tests had an ad-hoc session initialization process. The integration 83 // test environment has had more time devoted to its initialization, and has a 84 // more convenient API. 85 // - The old tests lacked documentation, and often had failures that were hard 86 // to understand. By starting from scratch, we can revisit these aspects. 87 func Test(t *testing.T) { 88 if testing.Short() { 89 builder := os.Getenv("GO_BUILDER_NAME") 90 // Note that HasPrefix(builder, "darwin-" only matches legacy builders. 91 // LUCI builder names start with x_tools-goN.NN. 92 // We want to exclude solaris on both legacy and LUCI builders, as 93 // it is timing out. 94 if strings.HasPrefix(builder, "darwin-") || strings.Contains(builder, "solaris") { 95 t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin and solaris builders") 96 } 97 } 98 // The marker tests must be able to run go/packages.Load. 99 testenv.NeedsGoPackages(t) 100 101 const dir = "testdata" 102 tests, err := loadMarkerTests(dir) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 // Opt: use a shared cache. 108 cache := cache.New(nil) 109 110 for _, test := range tests { 111 test := test 112 t.Run(test.name, func(t *testing.T) { 113 t.Parallel() 114 if test.skipReason != "" { 115 t.Skip(test.skipReason) 116 } 117 for _, goos := range test.skipGOOS { 118 if runtime.GOOS == goos { 119 t.Skipf("skipping on %s due to -skip_goos", runtime.GOOS) 120 } 121 } 122 123 // TODO(rfindley): it may be more useful to have full support for build 124 // constraints. 125 if test.minGoVersion != "" { 126 var go1point int 127 if _, err := fmt.Sscanf(test.minGoVersion, "go1.%d", &go1point); err != nil { 128 t.Fatalf("parsing -min_go version: %v", err) 129 } 130 testenv.NeedsGo1Point(t, go1point) 131 } 132 if test.maxGoVersion != "" { 133 var go1point int 134 if _, err := fmt.Sscanf(test.maxGoVersion, "go1.%d", &go1point); err != nil { 135 t.Fatalf("parsing -max_go version: %v", err) 136 } 137 testenv.SkipAfterGo1Point(t, go1point) 138 } 139 if test.cgo { 140 testenv.NeedsTool(t, "cgo") 141 } 142 config := fake.EditorConfig{ 143 Settings: test.settings, 144 CapabilitiesJSON: test.capabilities, 145 Env: test.env, 146 } 147 if _, ok := config.Settings["diagnosticsDelay"]; !ok { 148 if config.Settings == nil { 149 config.Settings = make(map[string]any) 150 } 151 config.Settings["diagnosticsDelay"] = "10ms" 152 } 153 // inv: config.Settings != nil 154 155 run := &markerTestRun{ 156 test: test, 157 env: newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config), 158 settings: config.Settings, 159 values: make(map[expect.Identifier]any), 160 diags: make(map[protocol.Location][]protocol.Diagnostic), 161 extraNotes: make(map[protocol.DocumentURI]map[string][]*expect.Note), 162 } 163 // TODO(rfindley): make it easier to clean up the integration test environment. 164 defer run.env.Editor.Shutdown(context.Background()) // ignore error 165 defer run.env.Sandbox.Close() // ignore error 166 167 // Open all files so that we operate consistently with LSP clients, and 168 // (pragmatically) so that we have a Mapper available via the fake 169 // editor. 170 // 171 // This also allows avoiding mutating the editor state in tests. 172 for file := range test.files { 173 run.env.OpenFile(file) 174 } 175 // Wait for the didOpen notifications to be processed, then collect 176 // diagnostics. 177 var diags map[string]*protocol.PublishDiagnosticsParams 178 run.env.AfterChange(integration.ReadAllDiagnostics(&diags)) 179 for path, params := range diags { 180 uri := run.env.Sandbox.Workdir.URI(path) 181 for _, diag := range params.Diagnostics { 182 loc := protocol.Location{ 183 URI: uri, 184 Range: protocol.Range{ 185 Start: diag.Range.Start, 186 End: diag.Range.Start, // ignore end positions 187 }, 188 } 189 run.diags[loc] = append(run.diags[loc], diag) 190 } 191 } 192 193 var markers []marker 194 for _, note := range test.notes { 195 mark := marker{run: run, note: note} 196 if fn, ok := valueMarkerFuncs[note.Name]; ok { 197 fn(mark) 198 } else if _, ok := actionMarkerFuncs[note.Name]; ok { 199 markers = append(markers, mark) // save for later 200 } else { 201 uri := mark.uri() 202 if run.extraNotes[uri] == nil { 203 run.extraNotes[uri] = make(map[string][]*expect.Note) 204 } 205 run.extraNotes[uri][note.Name] = append(run.extraNotes[uri][note.Name], note) 206 } 207 } 208 209 // Invoke each remaining marker in the test. 210 for _, mark := range markers { 211 actionMarkerFuncs[mark.note.Name](mark) 212 } 213 214 // Any remaining (un-eliminated) diagnostics are an error. 215 if !test.ignoreExtraDiags { 216 for loc, diags := range run.diags { 217 for _, diag := range diags { 218 t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message) 219 } 220 } 221 } 222 223 // TODO(rfindley): use these for whole-file marker tests. 224 for uri, extras := range run.extraNotes { 225 for name, extra := range extras { 226 if len(extra) > 0 { 227 t.Errorf("%s: %d unused %q markers", run.env.Sandbox.Workdir.URIToPath(uri), len(extra), name) 228 } 229 } 230 } 231 232 formatted, err := formatTest(test) 233 if err != nil { 234 t.Errorf("formatTest: %v", err) 235 } else if *update { 236 filename := filepath.Join(dir, test.name) 237 if err := os.WriteFile(filename, formatted, 0644); err != nil { 238 t.Error(err) 239 } 240 } else { 241 // On go 1.19 and later, verify that the testdata has not changed. 242 // 243 // On earlier Go versions, the golden test data varies due to different 244 // markdown escaping. 245 // 246 // Only check this if the test hasn't already failed, otherwise we'd 247 // report duplicate mismatches of golden data. 248 if testenv.Go1Point() >= 19 && !t.Failed() { 249 // Otherwise, verify that formatted content matches. 250 if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { 251 t.Errorf("formatted test does not match on-disk content:\n%s", diff) 252 } 253 } 254 } 255 }) 256 } 257 258 if abs, err := filepath.Abs(dir); err == nil && t.Failed() { 259 t.Logf("(Filenames are relative to %s.)", abs) 260 } 261 } 262 263 // A marker holds state for the execution of a single @marker 264 // annotation in the source. 265 type marker struct { 266 run *markerTestRun 267 note *expect.Note 268 } 269 270 // ctx returns the mark context. 271 func (m marker) ctx() context.Context { return m.run.env.Ctx } 272 273 // T returns the testing.TB for this mark. 274 func (m marker) T() testing.TB { return m.run.env.T } 275 276 // server returns the LSP server for the marker test run. 277 func (m marker) editor() *fake.Editor { return m.run.env.Editor } 278 279 // server returns the LSP server for the marker test run. 280 func (m marker) server() protocol.Server { return m.run.env.Editor.Server } 281 282 // uri returns the URI of the file containing the marker. 283 func (mark marker) uri() protocol.DocumentURI { 284 return mark.run.env.Sandbox.Workdir.URI(mark.run.test.fset.File(mark.note.Pos).Name()) 285 } 286 287 // document returns a protocol.TextDocumentIdentifier for the current file. 288 func (mark marker) document() protocol.TextDocumentIdentifier { 289 return protocol.TextDocumentIdentifier{URI: mark.uri()} 290 } 291 292 // path returns the relative path to the file containing the marker. 293 func (mark marker) path() string { 294 return mark.run.env.Sandbox.Workdir.RelPath(mark.run.test.fset.File(mark.note.Pos).Name()) 295 } 296 297 // mapper returns a *protocol.Mapper for the current file. 298 func (mark marker) mapper() *protocol.Mapper { 299 mapper, err := mark.editor().Mapper(mark.path()) 300 if err != nil { 301 mark.T().Fatalf("failed to get mapper for current mark: %v", err) 302 } 303 return mapper 304 } 305 306 // errorf reports an error with a prefix indicating the position of the marker note. 307 // 308 // It formats the error message using mark.sprintf. 309 func (mark marker) errorf(format string, args ...any) { 310 msg := mark.sprintf(format, args...) 311 // TODO(adonovan): consider using fmt.Fprintf(os.Stderr)+t.Fail instead of 312 // t.Errorf to avoid reporting uninteresting positions in the Go source of 313 // the driver. However, this loses the order of stderr wrt "FAIL: TestFoo" 314 // subtest dividers. 315 mark.T().Errorf("%s: %s", mark.run.fmtPos(mark.note.Pos), msg) 316 } 317 318 // valueMarkerFunc returns a wrapper around a function that allows it to be 319 // called during the processing of value markers (e.g. @value(v, 123)) with marker 320 // arguments converted to function parameters. The provided function's first 321 // parameter must be of type 'marker', and it must return a value. 322 // 323 // Unlike action markers, which are executed for actions such as test 324 // assertions, value markers are all evaluated first, and each computes 325 // a value that is recorded by its identifier, which is the marker's first 326 // argument. These values may be referred to from an action marker by 327 // this identifier, e.g. @action(... , v, ...). 328 // 329 // For example, given a fn with signature 330 // 331 // func(mark marker, label, details, kind string) CompletionItem 332 // 333 // The result of valueMarkerFunc can associated with @item notes, and invoked 334 // as follows: 335 // 336 // //@item(FooCompletion, "Foo", "func() int", "func") 337 // 338 // The provided fn should not mutate the test environment. 339 func valueMarkerFunc(fn any) func(marker) { 340 ftype := reflect.TypeOf(fn) 341 if ftype.NumIn() == 0 || ftype.In(0) != markerType { 342 panic(fmt.Sprintf("value marker function %#v must accept marker as its first argument", ftype)) 343 } 344 if ftype.NumOut() != 1 { 345 panic(fmt.Sprintf("value marker function %#v must have exactly 1 result", ftype)) 346 } 347 348 return func(mark marker) { 349 if len(mark.note.Args) == 0 || !is[expect.Identifier](mark.note.Args[0]) { 350 mark.errorf("first argument to a value marker function must be an identifier") 351 return 352 } 353 id := mark.note.Args[0].(expect.Identifier) 354 if alt, ok := mark.run.values[id]; ok { 355 mark.errorf("%s already declared as %T", id, alt) 356 return 357 } 358 args := append([]any{mark}, mark.note.Args[1:]...) 359 argValues, err := convertArgs(mark, ftype, args) 360 if err != nil { 361 mark.errorf("converting args: %v", err) 362 return 363 } 364 results := reflect.ValueOf(fn).Call(argValues) 365 mark.run.values[id] = results[0].Interface() 366 } 367 } 368 369 // actionMarkerFunc returns a wrapper around a function that allows it to be 370 // called during the processing of action markers (e.g. @action("abc", 123)) 371 // with marker arguments converted to function parameters. The provided 372 // function's first parameter must be of type 'marker', and it must not return 373 // any values. 374 // 375 // The provided fn should not mutate the test environment. 376 func actionMarkerFunc(fn any) func(marker) { 377 ftype := reflect.TypeOf(fn) 378 if ftype.NumIn() == 0 || ftype.In(0) != markerType { 379 panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype)) 380 } 381 if ftype.NumOut() != 0 { 382 panic(fmt.Sprintf("action marker function %#v cannot have results", ftype)) 383 } 384 385 return func(mark marker) { 386 args := append([]any{mark}, mark.note.Args...) 387 argValues, err := convertArgs(mark, ftype, args) 388 if err != nil { 389 mark.errorf("converting args: %v", err) 390 return 391 } 392 reflect.ValueOf(fn).Call(argValues) 393 } 394 } 395 396 func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) { 397 var ( 398 argValues []reflect.Value 399 pnext int // next param index 400 p reflect.Type // current param 401 ) 402 for i, arg := range args { 403 if i < ftype.NumIn() { 404 p = ftype.In(pnext) 405 pnext++ 406 } else if p == nil || !ftype.IsVariadic() { 407 // The actual number of arguments expected by the mark varies, depending 408 // on whether this is a value marker or an action marker. 409 // 410 // Since this error indicates a bug, probably OK to have an imprecise 411 // error message here. 412 return nil, fmt.Errorf("too many arguments to %s", mark.note.Name) 413 } 414 elemType := p 415 if ftype.IsVariadic() && pnext == ftype.NumIn() { 416 elemType = p.Elem() 417 } 418 var v reflect.Value 419 if id, ok := arg.(expect.Identifier); ok && id == "_" { 420 v = reflect.Zero(elemType) 421 } else { 422 a, err := convert(mark, arg, elemType) 423 if err != nil { 424 return nil, err 425 } 426 v = reflect.ValueOf(a) 427 } 428 argValues = append(argValues, v) 429 } 430 // Check that we have sufficient arguments. If the function is variadic, we 431 // do not need arguments for the final parameter. 432 if pnext < ftype.NumIn()-1 || pnext == ftype.NumIn()-1 && !ftype.IsVariadic() { 433 // Same comment as above: OK to be vague here. 434 return nil, fmt.Errorf("not enough arguments to %s", mark.note.Name) 435 } 436 return argValues, nil 437 } 438 439 // is reports whether arg is a T. 440 func is[T any](arg any) bool { 441 _, ok := arg.(T) 442 return ok 443 } 444 445 // Supported value marker functions. See [valueMarkerFunc] for more details. 446 var valueMarkerFuncs = map[string]func(marker){ 447 "loc": valueMarkerFunc(locMarker), 448 "item": valueMarkerFunc(completionItemMarker), 449 } 450 451 // Supported action marker functions. See [actionMarkerFunc] for more details. 452 var actionMarkerFuncs = map[string]func(marker){ 453 "acceptcompletion": actionMarkerFunc(acceptCompletionMarker), 454 "codeaction": actionMarkerFunc(codeActionMarker), 455 "codeactionedit": actionMarkerFunc(codeActionEditMarker), 456 "codeactionerr": actionMarkerFunc(codeActionErrMarker), 457 "codelenses": actionMarkerFunc(codeLensesMarker), 458 "complete": actionMarkerFunc(completeMarker), 459 "def": actionMarkerFunc(defMarker), 460 "diag": actionMarkerFunc(diagMarker), 461 "documentlink": actionMarkerFunc(documentLinkMarker), 462 "foldingrange": actionMarkerFunc(foldingRangeMarker), 463 "format": actionMarkerFunc(formatMarker), 464 "highlight": actionMarkerFunc(highlightMarker), 465 "hover": actionMarkerFunc(hoverMarker), 466 "hovererr": actionMarkerFunc(hoverErrMarker), 467 "implementation": actionMarkerFunc(implementationMarker), 468 "incomingcalls": actionMarkerFunc(incomingCallsMarker), 469 "inlayhints": actionMarkerFunc(inlayhintsMarker), 470 "outgoingcalls": actionMarkerFunc(outgoingCallsMarker), 471 "preparerename": actionMarkerFunc(prepareRenameMarker), 472 "rank": actionMarkerFunc(rankMarker), 473 "rankl": actionMarkerFunc(ranklMarker), 474 "refs": actionMarkerFunc(refsMarker), 475 "rename": actionMarkerFunc(renameMarker), 476 "renameerr": actionMarkerFunc(renameErrMarker), 477 "selectionrange": actionMarkerFunc(selectionRangeMarker), 478 "signature": actionMarkerFunc(signatureMarker), 479 "snippet": actionMarkerFunc(snippetMarker), 480 "suggestedfix": actionMarkerFunc(suggestedfixMarker), 481 "suggestedfixerr": actionMarkerFunc(suggestedfixErrMarker), 482 "symbol": actionMarkerFunc(symbolMarker), 483 "token": actionMarkerFunc(tokenMarker), 484 "typedef": actionMarkerFunc(typedefMarker), 485 "workspacesymbol": actionMarkerFunc(workspaceSymbolMarker), 486 } 487 488 // markerTest holds all the test data extracted from a test txtar archive. 489 // 490 // See the documentation for RunMarkerTests for more information on the archive 491 // format. 492 type markerTest struct { 493 name string // relative path to the txtar file in the testdata dir 494 fset *token.FileSet // fileset used for parsing notes 495 content []byte // raw test content 496 archive *txtar.Archive // original test archive 497 settings map[string]any // gopls settings 498 capabilities []byte // content of capabilities.json file 499 env map[string]string // editor environment 500 proxyFiles map[string][]byte // proxy content 501 files map[string][]byte // data files from the archive (excluding special files) 502 notes []*expect.Note // extracted notes from data files 503 golden map[expect.Identifier]*Golden // extracted golden content, by identifier name 504 505 skipReason string // the skip reason extracted from the "skip" archive file 506 flags []string // flags extracted from the special "flags" archive file. 507 508 // Parsed flags values. 509 minGoVersion string 510 maxGoVersion string 511 cgo bool 512 writeGoSum []string // comma separated dirs to write go sum for 513 skipGOOS []string // comma separated GOOS values to skip 514 ignoreExtraDiags bool 515 filterBuiltins bool 516 filterKeywords bool 517 } 518 519 // flagSet returns the flagset used for parsing the special "flags" file in the 520 // test archive. 521 func (t *markerTest) flagSet() *flag.FlagSet { 522 flags := flag.NewFlagSet(t.name, flag.ContinueOnError) 523 flags.StringVar(&t.minGoVersion, "min_go", "", "if set, the minimum go1.X version required for this test") 524 flags.StringVar(&t.maxGoVersion, "max_go", "", "if set, the maximum go1.X version required for this test") 525 flags.BoolVar(&t.cgo, "cgo", false, "if set, requires cgo (both the cgo tool and CGO_ENABLED=1)") 526 flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories") 527 flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values") 528 flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics") 529 flags.BoolVar(&t.filterBuiltins, "filter_builtins", true, "if set, filter builtins from completion results") 530 flags.BoolVar(&t.filterKeywords, "filter_keywords", true, "if set, filter keywords from completion results") 531 return flags 532 } 533 534 // stringListValue implements flag.Value. 535 type stringListValue []string 536 537 func (l *stringListValue) Set(s string) error { 538 if s != "" { 539 for _, d := range strings.Split(s, ",") { 540 *l = append(*l, strings.TrimSpace(d)) 541 } 542 } 543 return nil 544 } 545 546 func (l stringListValue) String() string { 547 return strings.Join([]string(l), ",") 548 } 549 550 func (t *markerTest) getGolden(id expect.Identifier) *Golden { 551 golden, ok := t.golden[id] 552 // If there was no golden content for this identifier, we must create one 553 // to handle the case where -update is set: we need a place to store 554 // the updated content. 555 if !ok { 556 golden = &Golden{id: id} 557 558 // TODO(adonovan): the separation of markerTest (the 559 // static aspects) from markerTestRun (the dynamic 560 // ones) is evidently bogus because here we modify 561 // markerTest during execution. Let's merge the two. 562 t.golden[id] = golden 563 } 564 return golden 565 } 566 567 // Golden holds extracted golden content for a single @<name> prefix. 568 // 569 // When -update is set, golden captures the updated golden contents for later 570 // writing. 571 type Golden struct { 572 id expect.Identifier 573 data map[string][]byte // key "" => @id itself 574 updated map[string][]byte 575 } 576 577 // Get returns golden content for the given name, which corresponds to the 578 // relative path following the golden prefix @<name>/. For example, to access 579 // the content of @foo/path/to/result.json from the Golden associated with 580 // @foo, name should be "path/to/result.json". 581 // 582 // If -update is set, the given update function will be called to get the 583 // updated golden content that should be written back to testdata. 584 // 585 // Marker functions must use this method instead of accessing data entries 586 // directly otherwise the -update operation will delete those entries. 587 // 588 // TODO(rfindley): rethink the logic here. We may want to separate Get and Set, 589 // and not delete golden content that isn't set. 590 func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) { 591 if existing, ok := g.updated[name]; ok { 592 // Multiple tests may reference the same golden data, but if they do they 593 // must agree about its expected content. 594 if diff := compare.NamedText("existing", "updated", string(existing), string(updated)); diff != "" { 595 t.Errorf("conflicting updates for golden data %s/%s:\n%s", g.id, name, diff) 596 } 597 } 598 if g.updated == nil { 599 g.updated = make(map[string][]byte) 600 } 601 g.updated[name] = updated 602 if *update { 603 return updated, true 604 } 605 606 res, ok := g.data[name] 607 return res, ok 608 } 609 610 // loadMarkerTests walks the given dir looking for .txt files, which it 611 // interprets as a txtar archive. 612 // 613 // See the documentation for RunMarkerTests for more details on the test data 614 // archive. 615 func loadMarkerTests(dir string) ([]*markerTest, error) { 616 var tests []*markerTest 617 err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { 618 if strings.HasSuffix(path, ".txt") { 619 content, err := os.ReadFile(path) 620 if err != nil { 621 return err 622 } 623 624 name := strings.TrimPrefix(path, dir+string(filepath.Separator)) 625 test, err := loadMarkerTest(name, content) 626 if err != nil { 627 return fmt.Errorf("%s: %v", path, err) 628 } 629 tests = append(tests, test) 630 } 631 return err 632 }) 633 return tests, err 634 } 635 636 func loadMarkerTest(name string, content []byte) (*markerTest, error) { 637 archive := txtar.Parse(content) 638 if len(archive.Files) == 0 { 639 return nil, fmt.Errorf("txtar file has no '-- filename --' sections") 640 } 641 if bytes.Contains(archive.Comment, []byte("\n-- ")) { 642 // This check is conservative, but the comment is only a comment. 643 return nil, fmt.Errorf("ill-formed '-- filename --' header in comment") 644 } 645 test := &markerTest{ 646 name: name, 647 fset: token.NewFileSet(), 648 content: content, 649 archive: archive, 650 files: make(map[string][]byte), 651 golden: make(map[expect.Identifier]*Golden), 652 } 653 for _, file := range archive.Files { 654 switch { 655 case file.Name == "skip": 656 reason := strings.ReplaceAll(string(file.Data), "\n", " ") 657 reason = strings.TrimSpace(reason) 658 test.skipReason = reason 659 660 case file.Name == "flags": 661 test.flags = strings.Fields(string(file.Data)) 662 663 case file.Name == "settings.json": 664 if err := json.Unmarshal(file.Data, &test.settings); err != nil { 665 return nil, err 666 } 667 668 case file.Name == "capabilities.json": 669 test.capabilities = file.Data // lazily unmarshalled by the editor 670 671 case file.Name == "env": 672 test.env = make(map[string]string) 673 fields := strings.Fields(string(file.Data)) 674 for _, field := range fields { 675 key, value, ok := strings.Cut(field, "=") 676 if !ok { 677 return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field) 678 } 679 test.env[key] = value 680 } 681 682 case strings.HasPrefix(file.Name, "@"): // golden content 683 idstring, name, _ := strings.Cut(file.Name[len("@"):], "/") 684 id := expect.Identifier(idstring) 685 // Note that a file.Name of just "@id" gives (id, name) = ("id", ""). 686 if _, ok := test.golden[id]; !ok { 687 test.golden[id] = &Golden{ 688 id: id, 689 data: make(map[string][]byte), 690 } 691 } 692 test.golden[id].data[name] = file.Data 693 694 case strings.HasPrefix(file.Name, "proxy/"): 695 name := file.Name[len("proxy/"):] 696 if test.proxyFiles == nil { 697 test.proxyFiles = make(map[string][]byte) 698 } 699 test.proxyFiles[name] = file.Data 700 701 default: // ordinary file content 702 notes, err := expect.Parse(test.fset, file.Name, file.Data) 703 if err != nil { 704 return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) 705 } 706 707 // Reject common misspelling: "// @mark". 708 // TODO(adonovan): permit "// @" within a string. Detect multiple spaces. 709 if i := bytes.Index(file.Data, []byte("// @")); i >= 0 { 710 line := 1 + bytes.Count(file.Data[:i], []byte("\n")) 711 return nil, fmt.Errorf("%s:%d: unwanted space before marker (// @)", file.Name, line) 712 } 713 714 // The 'go list' command doesn't work correct with modules named 715 // testdata", so don't allow it as a module name (golang/go#65406). 716 // (Otherwise files within it will end up in an ad hoc 717 // package, "command-line-arguments/$TMPDIR/...".) 718 if filepath.Base(file.Name) == "go.mod" && 719 bytes.Contains(file.Data, []byte("module testdata")) { 720 return nil, fmt.Errorf("'testdata' is not a valid module name") 721 } 722 723 test.notes = append(test.notes, notes...) 724 test.files[file.Name] = file.Data 725 } 726 727 // Print a warning if we see what looks like "-- filename --" 728 // without the second "--". It's not necessarily wrong, 729 // but it should almost never appear in our test inputs. 730 if bytes.Contains(file.Data, []byte("\n-- ")) { 731 log.Printf("ill-formed '-- filename --' header in %s?", file.Name) 732 } 733 } 734 735 // Parse flags after loading files, as they may have been set by the "flags" 736 // file. 737 if err := test.flagSet().Parse(test.flags); err != nil { 738 return nil, fmt.Errorf("parsing flags: %v", err) 739 } 740 741 return test, nil 742 } 743 744 // formatTest formats the test as a txtar archive. 745 func formatTest(test *markerTest) ([]byte, error) { 746 arch := &txtar.Archive{ 747 Comment: test.archive.Comment, 748 } 749 750 updatedGolden := make(map[string][]byte) 751 for id, g := range test.golden { 752 for name, data := range g.updated { 753 filename := "@" + path.Join(string(id), name) // name may be "" 754 updatedGolden[filename] = data 755 } 756 } 757 758 // Preserve the original ordering of archive files. 759 for _, file := range test.archive.Files { 760 switch file.Name { 761 // Preserve configuration files exactly as they were. They must have parsed 762 // if we got this far. 763 case "skip", "flags", "settings.json", "capabilities.json", "env": 764 arch.Files = append(arch.Files, file) 765 default: 766 if _, ok := test.files[file.Name]; ok { // ordinary file 767 arch.Files = append(arch.Files, file) 768 } else if strings.HasPrefix(file.Name, "proxy/") { // proxy file 769 arch.Files = append(arch.Files, file) 770 } else if data, ok := updatedGolden[file.Name]; ok { // golden file 771 arch.Files = append(arch.Files, txtar.File{Name: file.Name, Data: data}) 772 delete(updatedGolden, file.Name) 773 } 774 } 775 } 776 777 // ...followed by any new golden files. 778 var newGoldenFiles []txtar.File 779 for filename, data := range updatedGolden { 780 // TODO(rfindley): it looks like this implicitly removes trailing newlines 781 // from golden content. Is there any way to fix that? Perhaps we should 782 // just make the diff tolerant of missing newlines? 783 newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data}) 784 } 785 // Sort new golden files lexically. 786 sort.Slice(newGoldenFiles, func(i, j int) bool { 787 return newGoldenFiles[i].Name < newGoldenFiles[j].Name 788 }) 789 arch.Files = append(arch.Files, newGoldenFiles...) 790 791 return txtar.Format(arch), nil 792 } 793 794 // newEnv creates a new environment for a marker test. 795 // 796 // TODO(rfindley): simplify and refactor the construction of testing 797 // environments across integration tests, marker tests, and benchmarks. 798 func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byte, writeGoSum []string, config fake.EditorConfig) *integration.Env { 799 sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ 800 RootDir: t.TempDir(), 801 Files: files, 802 ProxyFiles: proxyFiles, 803 }) 804 if err != nil { 805 t.Fatal(err) 806 } 807 808 for _, dir := range writeGoSum { 809 if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "..."}, []string{"GOWORK=off"}, true); err != nil { 810 t.Fatal(err) 811 } 812 } 813 814 // Put a debug instance in the context to prevent logging to stderr. 815 // See associated TODO in runner.go: we should revisit this pattern. 816 ctx := context.Background() 817 ctx = debug.WithInstance(ctx, "off") 818 819 awaiter := integration.NewAwaiter(sandbox.Workdir) 820 ss := lsprpc.NewStreamServer(cache, false, hooks.Options) 821 server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) 822 const skipApplyEdits = true // capture edits but don't apply them 823 editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks(), skipApplyEdits) 824 if err != nil { 825 sandbox.Close() // ignore error 826 t.Fatal(err) 827 } 828 if err := awaiter.Await(ctx, integration.InitialWorkspaceLoad); err != nil { 829 sandbox.Close() // ignore error 830 t.Fatal(err) 831 } 832 return &integration.Env{ 833 T: t, 834 Ctx: ctx, 835 Editor: editor, 836 Sandbox: sandbox, 837 Awaiter: awaiter, 838 } 839 } 840 841 // A markerTestRun holds the state of one run of a marker test archive. 842 type markerTestRun struct { 843 test *markerTest 844 env *integration.Env 845 settings map[string]any 846 847 // Collected information. 848 // Each @diag/@suggestedfix marker eliminates an entry from diags. 849 values map[expect.Identifier]any 850 diags map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start 851 852 // Notes that weren't associated with a top-level marker func. They may be 853 // consumed by another marker (e.g. @codelenses collects @codelens markers). 854 // Any notes that aren't consumed are flagged as an error. 855 extraNotes map[protocol.DocumentURI]map[string][]*expect.Note 856 } 857 858 // sprintf returns a formatted string after applying pre-processing to 859 // arguments of the following types: 860 // - token.Pos: formatted using (*markerTestRun).fmtPos 861 // - protocol.Location: formatted using (*markerTestRun).fmtLoc 862 func (c *marker) sprintf(format string, args ...any) string { 863 if false { 864 _ = fmt.Sprintf(format, args...) // enable vet printf checker 865 } 866 var args2 []any 867 for _, arg := range args { 868 switch arg := arg.(type) { 869 case token.Pos: 870 args2 = append(args2, c.run.fmtPos(arg)) 871 case protocol.Location: 872 args2 = append(args2, c.run.fmtLoc(arg)) 873 default: 874 args2 = append(args2, arg) 875 } 876 } 877 return fmt.Sprintf(format, args2...) 878 } 879 880 // fmtLoc formats the given pos in the context of the test, using 881 // archive-relative paths for files and including the line number in the full 882 // archive file. 883 func (run *markerTestRun) fmtPos(pos token.Pos) string { 884 file := run.test.fset.File(pos) 885 if file == nil { 886 run.env.T.Errorf("position %d not in test fileset", pos) 887 return "<invalid location>" 888 } 889 m, err := run.env.Editor.Mapper(file.Name()) 890 if err != nil { 891 run.env.T.Errorf("%s", err) 892 return "<invalid location>" 893 } 894 loc, err := m.PosLocation(file, pos, pos) 895 if err != nil { 896 run.env.T.Errorf("Mapper(%s).PosLocation failed: %v", file.Name(), err) 897 } 898 return run.fmtLoc(loc) 899 } 900 901 // fmtLoc formats the given location in the context of the test, using 902 // archive-relative paths for files and including the line number in the full 903 // archive file. 904 func (run *markerTestRun) fmtLoc(loc protocol.Location) string { 905 formatted := run.fmtLocDetails(loc, true) 906 if formatted == "" { 907 run.env.T.Errorf("unable to find %s in test archive", loc) 908 return "<invalid location>" 909 } 910 return formatted 911 } 912 913 // See fmtLoc. If includeTxtPos is not set, the position in the full archive 914 // file is omitted. 915 // 916 // If the location cannot be found within the archive, fmtLocDetails returns "". 917 func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos bool) string { 918 if loc == (protocol.Location{}) { 919 return "" 920 } 921 lines := bytes.Count(run.test.archive.Comment, []byte("\n")) 922 var name string 923 for _, f := range run.test.archive.Files { 924 lines++ // -- separator -- 925 uri := run.env.Sandbox.Workdir.URI(f.Name) 926 if uri == loc.URI { 927 name = f.Name 928 break 929 } 930 lines += bytes.Count(f.Data, []byte("\n")) 931 } 932 if name == "" { 933 return "" 934 } 935 m, err := run.env.Editor.Mapper(name) 936 if err != nil { 937 run.env.T.Errorf("internal error: %v", err) 938 return "<invalid location>" 939 } 940 start, end, err := m.RangeOffsets(loc.Range) 941 if err != nil { 942 run.env.T.Errorf("error formatting location %s: %v", loc, err) 943 return "<invalid location>" 944 } 945 var ( 946 startLine, startCol8 = m.OffsetLineCol8(start) 947 endLine, endCol8 = m.OffsetLineCol8(end) 948 ) 949 innerSpan := fmt.Sprintf("%d:%d", startLine, startCol8) // relative to the embedded file 950 outerSpan := fmt.Sprintf("%d:%d", lines+startLine, startCol8) // relative to the archive file 951 if start != end { 952 if endLine == startLine { 953 innerSpan += fmt.Sprintf("-%d", endCol8) 954 outerSpan += fmt.Sprintf("-%d", endCol8) 955 } else { 956 innerSpan += fmt.Sprintf("-%d:%d", endLine, endCol8) 957 outerSpan += fmt.Sprintf("-%d:%d", lines+endLine, endCol8) 958 } 959 } 960 961 if includeTxtPos { 962 return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, run.test.name, outerSpan) 963 } else { 964 return fmt.Sprintf("%s:%s", name, innerSpan) 965 } 966 } 967 968 // ---- converters ---- 969 970 // converter is the signature of argument converters. 971 // A converter should return an error rather than calling marker.errorf(). 972 // 973 // type converter func(marker, any) (any, error) 974 975 // Types with special conversions. 976 var ( 977 goldenType = reflect.TypeOf(&Golden{}) 978 locationType = reflect.TypeOf(protocol.Location{}) 979 markerType = reflect.TypeOf(marker{}) 980 stringMatcherType = reflect.TypeOf(stringMatcher{}) 981 ) 982 983 func convert(mark marker, arg any, paramType reflect.Type) (any, error) { 984 // Handle stringMatcher and golden parameters before resolving identifiers, 985 // because golden content lives in a separate namespace from other 986 // identifiers. 987 switch paramType { 988 case stringMatcherType: 989 return convertStringMatcher(mark, arg) 990 case goldenType: 991 id, ok := arg.(expect.Identifier) 992 if !ok { 993 return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg) 994 } 995 return mark.run.test.getGolden(id), nil 996 } 997 if id, ok := arg.(expect.Identifier); ok { 998 if arg, ok := mark.run.values[id]; ok { 999 if !reflect.TypeOf(arg).AssignableTo(paramType) { 1000 return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) 1001 } 1002 return arg, nil 1003 } 1004 } 1005 if paramType == locationType { 1006 return convertLocation(mark, arg) 1007 } 1008 if reflect.TypeOf(arg).AssignableTo(paramType) { 1009 return arg, nil // no conversion required 1010 } 1011 return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) 1012 } 1013 1014 // convertLocation converts a string or regexp argument into the protocol 1015 // location corresponding to the first position of the string (or first match 1016 // of the regexp) in the line preceding the note. 1017 func convertLocation(mark marker, arg any) (protocol.Location, error) { 1018 switch arg := arg.(type) { 1019 case string: 1020 startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos) 1021 if err != nil { 1022 return protocol.Location{}, err 1023 } 1024 idx := bytes.Index(preceding, []byte(arg)) 1025 if idx < 0 { 1026 return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding) 1027 } 1028 off := startOff + idx 1029 return m.OffsetLocation(off, off+len(arg)) 1030 case *regexp.Regexp: 1031 return findRegexpInLine(mark.run, mark.note.Pos, arg) 1032 default: 1033 return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) 1034 } 1035 } 1036 1037 // findRegexpInLine searches the partial line preceding pos for a match for the 1038 // regular expression re, returning a location spanning the first match. If re 1039 // contains exactly one subgroup, the position of this subgroup match is 1040 // returned rather than the position of the full match. 1041 func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { 1042 startOff, preceding, m, err := linePreceding(run, pos) 1043 if err != nil { 1044 return protocol.Location{}, err 1045 } 1046 1047 matches := re.FindSubmatchIndex(preceding) 1048 if len(matches) == 0 { 1049 return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) 1050 } 1051 var start, end int 1052 switch len(matches) { 1053 case 2: 1054 // no subgroups: return the range of the regexp expression 1055 start, end = matches[0], matches[1] 1056 case 4: 1057 // one subgroup: return its range 1058 start, end = matches[2], matches[3] 1059 default: 1060 return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) 1061 } 1062 1063 return m.OffsetLocation(start+startOff, end+startOff) 1064 } 1065 1066 func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) { 1067 file := run.test.fset.File(pos) 1068 posn := safetoken.Position(file, pos) 1069 lineStart := file.LineStart(posn.Line) 1070 startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) 1071 if err != nil { 1072 return 0, nil, nil, err 1073 } 1074 m, err := run.env.Editor.Mapper(file.Name()) 1075 if err != nil { 1076 return 0, nil, nil, err 1077 } 1078 return startOff, m.Content[startOff:endOff], m, nil 1079 } 1080 1081 // convertStringMatcher converts a string, regexp, or identifier 1082 // argument into a stringMatcher. The string is a substring of the 1083 // expected error, the regexp is a pattern than matches the expected 1084 // error, and the identifier is a golden file containing the expected 1085 // error. 1086 func convertStringMatcher(mark marker, arg any) (stringMatcher, error) { 1087 switch arg := arg.(type) { 1088 case string: 1089 return stringMatcher{substr: arg}, nil 1090 case *regexp.Regexp: 1091 return stringMatcher{pattern: arg}, nil 1092 case expect.Identifier: 1093 golden := mark.run.test.getGolden(arg) 1094 return stringMatcher{golden: golden}, nil 1095 default: 1096 return stringMatcher{}, fmt.Errorf("cannot convert %T to wantError (want: string, regexp, or identifier)", arg) 1097 } 1098 } 1099 1100 // A stringMatcher represents an expectation of a specific string value. 1101 // 1102 // It may be indicated in one of three ways, in 'expect' notation: 1103 // - an identifier 'foo', to compare (exactly) with the contents of the golden 1104 // section @foo; 1105 // - a pattern expression re"ab.*c", to match against a regular expression; 1106 // - a string literal "abc", to check for a substring. 1107 type stringMatcher struct { 1108 golden *Golden 1109 pattern *regexp.Regexp 1110 substr string 1111 } 1112 1113 func (sc stringMatcher) String() string { 1114 if sc.golden != nil { 1115 return fmt.Sprintf("content from @%s entry", sc.golden.id) 1116 } else if sc.pattern != nil { 1117 return fmt.Sprintf("content matching %#q", sc.pattern) 1118 } else { 1119 return fmt.Sprintf("content with substring %q", sc.substr) 1120 } 1121 } 1122 1123 // checkErr asserts that the given error matches the stringMatcher's expectations. 1124 func (sc stringMatcher) checkErr(mark marker, err error) { 1125 if err == nil { 1126 mark.errorf("@%s succeeded unexpectedly, want %v", mark.note.Name, sc) 1127 return 1128 } 1129 sc.check(mark, err.Error()) 1130 } 1131 1132 // check asserts that the given content matches the stringMatcher's expectations. 1133 func (sc stringMatcher) check(mark marker, got string) { 1134 if sc.golden != nil { 1135 compareGolden(mark, []byte(got), sc.golden) 1136 } else if sc.pattern != nil { 1137 // Content must match the regular expression pattern. 1138 if !sc.pattern.MatchString(got) { 1139 mark.errorf("got %q, does not match pattern %#q", got, sc.pattern) 1140 } 1141 1142 } else if !strings.Contains(got, sc.substr) { 1143 // Content must contain the expected substring. 1144 mark.errorf("got %q, want substring %q", got, sc.substr) 1145 } 1146 } 1147 1148 // checkChangedFiles compares the files changed by an operation with their expected (golden) state. 1149 func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { 1150 // Check changed files match expectations. 1151 for filename, got := range changed { 1152 if want, ok := golden.Get(mark.T(), filename, got); !ok { 1153 mark.errorf("%s: unexpected change to file %s; got:\n%s", 1154 mark.note.Name, filename, got) 1155 1156 } else if string(got) != string(want) { 1157 mark.errorf("%s: wrong file content for %s: got:\n%s\nwant:\n%s\ndiff:\n%s", 1158 mark.note.Name, filename, got, want, 1159 compare.Bytes(want, got)) 1160 } 1161 } 1162 1163 // Report unmet expectations. 1164 for filename := range golden.data { 1165 if _, ok := changed[filename]; !ok { 1166 want, _ := golden.Get(mark.T(), filename, nil) 1167 mark.errorf("%s: missing change to file %s; want:\n%s", 1168 mark.note.Name, filename, want) 1169 } 1170 } 1171 } 1172 1173 // checkDiffs computes unified diffs for each changed file, and compares with 1174 // the diff content stored in the given golden directory. 1175 func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { 1176 diffs := make(map[string]string) 1177 for name, after := range changed { 1178 before := mark.run.env.FileContent(name) 1179 // TODO(golang/go#64023): switch back to diff.Strings. 1180 // The attached issue is only one obstacle to switching. 1181 // Another is that different diff algorithms produce 1182 // different results, so if we commit diffs in test 1183 // expectations, then we need to either (1) state 1184 // which diff implementation they use and never change 1185 // it, or (2) don't compare diffs, but instead apply 1186 // the "want" diff and check that it produces the 1187 // "got" output. Option 2 is more robust, as it allows 1188 // the test expectation to use any valid diff. 1189 edits := myers.ComputeEdits(before, string(after)) 1190 d, err := diff.ToUnified("before", "after", before, edits, 0) 1191 if err != nil { 1192 // Can't happen: edits are consistent. 1193 log.Fatalf("internal error in diff.ToUnified: %v", err) 1194 } 1195 // Trim the unified header from diffs, as it is unnecessary and repetitive. 1196 difflines := strings.Split(d, "\n") 1197 if len(difflines) >= 2 && strings.HasPrefix(difflines[1], "+++") { 1198 diffs[name] = strings.Join(difflines[2:], "\n") 1199 } else { 1200 diffs[name] = d 1201 } 1202 } 1203 // Check changed files match expectations. 1204 for filename, got := range diffs { 1205 if want, ok := golden.Get(mark.T(), filename, []byte(got)); !ok { 1206 mark.errorf("%s: unexpected change to file %s; got diff:\n%s", 1207 mark.note.Name, filename, got) 1208 1209 } else if got != string(want) { 1210 mark.errorf("%s: wrong diff for %s:\n\ngot:\n%s\n\nwant:\n%s\n", 1211 mark.note.Name, filename, got, want) 1212 } 1213 } 1214 // Report unmet expectations. 1215 for filename := range golden.data { 1216 if _, ok := changed[filename]; !ok { 1217 want, _ := golden.Get(mark.T(), filename, nil) 1218 mark.errorf("%s: missing change to file %s; want:\n%s", 1219 mark.note.Name, filename, want) 1220 } 1221 } 1222 } 1223 1224 // ---- marker functions ---- 1225 1226 // TODO(rfindley): consolidate documentation of these markers. They are already 1227 // documented above, so much of the documentation here is redundant. 1228 1229 // completionItem is a simplified summary of a completion item. 1230 type completionItem struct { 1231 Label, Detail, Kind, Documentation string 1232 } 1233 1234 func completionItemMarker(mark marker, label string, other ...string) completionItem { 1235 if len(other) > 3 { 1236 mark.errorf("too many arguments to @item: expect at most 4") 1237 } 1238 item := completionItem{ 1239 Label: label, 1240 } 1241 if len(other) > 0 { 1242 item.Detail = other[0] 1243 } 1244 if len(other) > 1 { 1245 item.Kind = other[1] 1246 } 1247 if len(other) > 2 { 1248 item.Documentation = other[2] 1249 } 1250 return item 1251 } 1252 1253 func rankMarker(mark marker, src protocol.Location, items ...completionItem) { 1254 list := mark.run.env.Completion(src) 1255 var got []string 1256 // Collect results that are present in items, preserving their order. 1257 for _, g := range list.Items { 1258 for _, w := range items { 1259 if g.Label == w.Label { 1260 got = append(got, g.Label) 1261 break 1262 } 1263 } 1264 } 1265 var want []string 1266 for _, w := range items { 1267 want = append(want, w.Label) 1268 } 1269 if diff := cmp.Diff(want, got); diff != "" { 1270 mark.errorf("completion rankings do not match (-want +got):\n%s", diff) 1271 } 1272 } 1273 1274 func ranklMarker(mark marker, src protocol.Location, labels ...string) { 1275 list := mark.run.env.Completion(src) 1276 var got []string 1277 // Collect results that are present in items, preserving their order. 1278 for _, g := range list.Items { 1279 for _, label := range labels { 1280 if g.Label == label { 1281 got = append(got, g.Label) 1282 break 1283 } 1284 } 1285 } 1286 if diff := cmp.Diff(labels, got); diff != "" { 1287 mark.errorf("completion rankings do not match (-want +got):\n%s", diff) 1288 } 1289 } 1290 1291 func snippetMarker(mark marker, src protocol.Location, item completionItem, want string) { 1292 list := mark.run.env.Completion(src) 1293 var ( 1294 found bool 1295 got string 1296 all []string // for errors 1297 ) 1298 items := filterBuiltinsAndKeywords(mark, list.Items) 1299 for _, i := range items { 1300 all = append(all, i.Label) 1301 if i.Label == item.Label { 1302 found = true 1303 if i.TextEdit != nil { 1304 got = i.TextEdit.NewText 1305 } 1306 break 1307 } 1308 } 1309 if !found { 1310 mark.errorf("no completion item found matching %s (got: %v)", item.Label, all) 1311 return 1312 } 1313 if got != want { 1314 mark.errorf("snippets do not match: got %q, want %q", got, want) 1315 } 1316 } 1317 1318 // completeMarker implements the @complete marker, running 1319 // textDocument/completion at the given src location and asserting that the 1320 // results match the expected results. 1321 func completeMarker(mark marker, src protocol.Location, want ...completionItem) { 1322 list := mark.run.env.Completion(src) 1323 items := filterBuiltinsAndKeywords(mark, list.Items) 1324 var got []completionItem 1325 for i, item := range items { 1326 simplified := completionItem{ 1327 Label: item.Label, 1328 Detail: item.Detail, 1329 Kind: fmt.Sprint(item.Kind), 1330 } 1331 if item.Documentation != nil { 1332 switch v := item.Documentation.Value.(type) { 1333 case string: 1334 simplified.Documentation = v 1335 case protocol.MarkupContent: 1336 simplified.Documentation = strings.TrimSpace(v.Value) // trim newlines 1337 } 1338 } 1339 // Support short-hand notation: if Detail, Kind, or Documentation are omitted from the 1340 // item, don't match them. 1341 if i < len(want) { 1342 if want[i].Detail == "" { 1343 simplified.Detail = "" 1344 } 1345 if want[i].Kind == "" { 1346 simplified.Kind = "" 1347 } 1348 if want[i].Documentation == "" { 1349 simplified.Documentation = "" 1350 } 1351 } 1352 got = append(got, simplified) 1353 } 1354 if len(want) == 0 { 1355 want = nil // got is nil if empty 1356 } 1357 if diff := cmp.Diff(want, got); diff != "" { 1358 mark.errorf("Completion(...) returned unexpect results (-want +got):\n%s", diff) 1359 } 1360 } 1361 1362 // filterBuiltinsAndKeywords filters out builtins and keywords from completion 1363 // results. 1364 // 1365 // It over-approximates, and does not detect if builtins are shadowed. 1366 func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem { 1367 keep := 0 1368 for _, item := range items { 1369 if mark.run.test.filterKeywords && item.Kind == protocol.KeywordCompletion { 1370 continue 1371 } 1372 if mark.run.test.filterBuiltins && types.Universe.Lookup(item.Label) != nil { 1373 continue 1374 } 1375 items[keep] = item 1376 keep++ 1377 } 1378 return items[:keep] 1379 } 1380 1381 // acceptCompletionMarker implements the @acceptCompletion marker, running 1382 // textDocument/completion at the given src location and accepting the 1383 // candidate with the given label. The resulting source must match the provided 1384 // golden content. 1385 func acceptCompletionMarker(mark marker, src protocol.Location, label string, golden *Golden) { 1386 list := mark.run.env.Completion(src) 1387 var selected *protocol.CompletionItem 1388 for _, item := range list.Items { 1389 if item.Label == label { 1390 selected = &item 1391 break 1392 } 1393 } 1394 if selected == nil { 1395 mark.errorf("Completion(...) did not return an item labeled %q", label) 1396 return 1397 } 1398 filename := mark.path() 1399 mapper := mark.mapper() 1400 patched, _, err := protocol.ApplyEdits(mapper, append([]protocol.TextEdit{*selected.TextEdit}, selected.AdditionalTextEdits...)) 1401 1402 if err != nil { 1403 mark.errorf("ApplyProtocolEdits failed: %v", err) 1404 return 1405 } 1406 changes := map[string][]byte{filename: patched} 1407 // Check the file state. 1408 checkChangedFiles(mark, changes, golden) 1409 } 1410 1411 // defMarker implements the @def marker, running textDocument/definition at 1412 // the given src location and asserting that there is exactly one resulting 1413 // location, matching dst. 1414 // 1415 // TODO(rfindley): support a variadic destination set. 1416 func defMarker(mark marker, src, dst protocol.Location) { 1417 got := mark.run.env.GoToDefinition(src) 1418 if got != dst { 1419 mark.errorf("definition location does not match:\n\tgot: %s\n\twant %s", 1420 mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) 1421 } 1422 } 1423 1424 func typedefMarker(mark marker, src, dst protocol.Location) { 1425 got := mark.run.env.TypeDefinition(src) 1426 if got != dst { 1427 mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s", 1428 mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) 1429 } 1430 } 1431 1432 func foldingRangeMarker(mark marker, g *Golden) { 1433 env := mark.run.env 1434 ranges, err := mark.server().FoldingRange(env.Ctx, &protocol.FoldingRangeParams{ 1435 TextDocument: mark.document(), 1436 }) 1437 if err != nil { 1438 mark.errorf("foldingRange failed: %v", err) 1439 return 1440 } 1441 var edits []protocol.TextEdit 1442 insert := func(line, char uint32, text string) { 1443 pos := protocol.Position{Line: line, Character: char} 1444 edits = append(edits, protocol.TextEdit{ 1445 Range: protocol.Range{ 1446 Start: pos, 1447 End: pos, 1448 }, 1449 NewText: text, 1450 }) 1451 } 1452 for i, rng := range ranges { 1453 insert(rng.StartLine, rng.StartCharacter, fmt.Sprintf("<%d kind=%q>", i, rng.Kind)) 1454 insert(rng.EndLine, rng.EndCharacter, fmt.Sprintf("</%d>", i)) 1455 } 1456 filename := mark.path() 1457 mapper, err := env.Editor.Mapper(filename) 1458 if err != nil { 1459 mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) 1460 return 1461 } 1462 got, _, err := protocol.ApplyEdits(mapper, edits) 1463 if err != nil { 1464 mark.errorf("ApplyProtocolEdits failed: %v", err) 1465 return 1466 } 1467 want, _ := g.Get(mark.T(), "", got) 1468 if diff := compare.Bytes(want, got); diff != "" { 1469 mark.errorf("foldingRange mismatch:\n%s", diff) 1470 } 1471 } 1472 1473 // formatMarker implements the @format marker. 1474 func formatMarker(mark marker, golden *Golden) { 1475 edits, err := mark.server().Formatting(mark.ctx(), &protocol.DocumentFormattingParams{ 1476 TextDocument: mark.document(), 1477 }) 1478 var got []byte 1479 if err != nil { 1480 got = []byte(err.Error() + "\n") // all golden content is newline terminated 1481 } else { 1482 env := mark.run.env 1483 filename := mark.path() 1484 mapper, err := env.Editor.Mapper(filename) 1485 if err != nil { 1486 mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) 1487 } 1488 1489 got, _, err = protocol.ApplyEdits(mapper, edits) 1490 if err != nil { 1491 mark.errorf("ApplyProtocolEdits failed: %v", err) 1492 return 1493 } 1494 } 1495 1496 compareGolden(mark, got, golden) 1497 } 1498 1499 func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) { 1500 highlights := mark.run.env.DocumentHighlight(src) 1501 var got []protocol.Range 1502 for _, h := range highlights { 1503 got = append(got, h.Range) 1504 } 1505 1506 var want []protocol.Range 1507 for _, d := range dsts { 1508 want = append(want, d.Range) 1509 } 1510 1511 sortRanges := func(s []protocol.Range) { 1512 sort.Slice(s, func(i, j int) bool { 1513 return protocol.CompareRange(s[i], s[j]) < 0 1514 }) 1515 } 1516 1517 sortRanges(got) 1518 sortRanges(want) 1519 1520 if diff := cmp.Diff(want, got); diff != "" { 1521 mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff) 1522 } 1523 } 1524 1525 func hoverMarker(mark marker, src, dst protocol.Location, sc stringMatcher) { 1526 content, gotDst := mark.run.env.Hover(src) 1527 if gotDst != dst { 1528 mark.errorf("hover location does not match:\n\tgot: %s\n\twant %s)", mark.run.fmtLoc(gotDst), mark.run.fmtLoc(dst)) 1529 } 1530 gotMD := "" 1531 if content != nil { 1532 gotMD = content.Value 1533 } 1534 sc.check(mark, gotMD) 1535 } 1536 1537 func hoverErrMarker(mark marker, src protocol.Location, em stringMatcher) { 1538 _, _, err := mark.editor().Hover(mark.ctx(), src) 1539 em.checkErr(mark, err) 1540 } 1541 1542 // locMarker implements the @loc marker. It is executed before other 1543 // markers, so that locations are available. 1544 func locMarker(mark marker, loc protocol.Location) protocol.Location { return loc } 1545 1546 // diagMarker implements the @diag marker. It eliminates diagnostics from 1547 // the observed set in mark.test. 1548 func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) { 1549 if _, ok := removeDiagnostic(mark, loc, re); !ok { 1550 mark.errorf("no diagnostic at %v matches %q", loc, re) 1551 } 1552 } 1553 1554 // removeDiagnostic looks for a diagnostic matching loc at the given position. 1555 // 1556 // If found, it returns (diag, true), and eliminates the matched diagnostic 1557 // from the unmatched set. 1558 // 1559 // If not found, it returns (protocol.Diagnostic{}, false). 1560 func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) { 1561 loc.Range.End = loc.Range.Start // diagnostics ignore end position. 1562 diags := mark.run.diags[loc] 1563 for i, diag := range diags { 1564 if re.MatchString(diag.Message) { 1565 mark.run.diags[loc] = append(diags[:i], diags[i+1:]...) 1566 return diag, true 1567 } 1568 } 1569 return protocol.Diagnostic{}, false 1570 } 1571 1572 // renameMarker implements the @rename(location, new, golden) marker. 1573 func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) { 1574 changed, err := rename(mark.run.env, loc, newName) 1575 if err != nil { 1576 mark.errorf("rename failed: %v. (Use @renameerr for expected errors.)", err) 1577 return 1578 } 1579 checkDiffs(mark, changed, golden) 1580 } 1581 1582 // renameErrMarker implements the @renamererr(location, new, error) marker. 1583 func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr stringMatcher) { 1584 _, err := rename(mark.run.env, loc, newName) 1585 wantErr.checkErr(mark, err) 1586 } 1587 1588 func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) { 1589 ranges, err := mark.server().SelectionRange(mark.ctx(), &protocol.SelectionRangeParams{ 1590 TextDocument: mark.document(), 1591 Positions: []protocol.Position{loc.Range.Start}, 1592 }) 1593 if err != nil { 1594 mark.errorf("SelectionRange failed: %v", err) 1595 return 1596 } 1597 var buf bytes.Buffer 1598 m := mark.mapper() 1599 for i, path := range ranges { 1600 fmt.Fprintf(&buf, "Ranges %d:", i) 1601 rng := path 1602 for { 1603 s, e, err := m.RangeOffsets(rng.Range) 1604 if err != nil { 1605 mark.errorf("RangeOffsets failed: %v", err) 1606 return 1607 } 1608 1609 var snippet string 1610 if e-s < 30 { 1611 snippet = string(m.Content[s:e]) 1612 } else { 1613 snippet = string(m.Content[s:s+15]) + "..." + string(m.Content[e-15:e]) 1614 } 1615 1616 fmt.Fprintf(&buf, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n")) 1617 1618 if rng.Parent == nil { 1619 break 1620 } 1621 rng = *rng.Parent 1622 } 1623 buf.WriteRune('\n') 1624 } 1625 compareGolden(mark, buf.Bytes(), g) 1626 } 1627 1628 func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) { 1629 tokens := mark.run.env.SemanticTokensRange(loc) 1630 if len(tokens) != 1 { 1631 mark.errorf("got %d tokens, want 1", len(tokens)) 1632 return 1633 } 1634 tok := tokens[0] 1635 if tok.TokenType != tokenType { 1636 mark.errorf("token type = %q, want %q", tok.TokenType, tokenType) 1637 } 1638 if tok.Mod != mod { 1639 mark.errorf("token mod = %q, want %q", tok.Mod, mod) 1640 } 1641 } 1642 1643 func signatureMarker(mark marker, src protocol.Location, label string, active int64) { 1644 got := mark.run.env.SignatureHelp(src) 1645 if label == "" { 1646 // A null result is expected. 1647 // (There's no point having a @signatureerr marker 1648 // because the server handler suppresses all errors.) 1649 if got != nil && len(got.Signatures) > 0 { 1650 mark.errorf("signatureHelp = %v, want 0 signatures", got) 1651 } 1652 return 1653 } 1654 if got == nil || len(got.Signatures) != 1 { 1655 mark.errorf("signatureHelp = %v, want exactly 1 signature", got) 1656 return 1657 } 1658 if got := got.Signatures[0].Label; got != label { 1659 mark.errorf("signatureHelp: got label %q, want %q", got, label) 1660 } 1661 if got := int64(got.ActiveParameter); got != active { 1662 mark.errorf("signatureHelp: got active parameter %d, want %d", got, active) 1663 } 1664 } 1665 1666 // rename returns the new contents of the files that would be modified 1667 // by renaming the identifier at loc to newName. 1668 func rename(env *integration.Env, loc protocol.Location, newName string) (map[string][]byte, error) { 1669 // We call Server.Rename directly, instead of 1670 // env.Editor.Rename(env.Ctx, loc, newName) 1671 // to isolate Rename from PrepareRename, and because we don't 1672 // want to modify the file system in a scenario with multiple 1673 // @rename markers. 1674 1675 editMap, err := env.Editor.Server.Rename(env.Ctx, &protocol.RenameParams{ 1676 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 1677 Position: loc.Range.Start, 1678 NewName: newName, 1679 }) 1680 if err != nil { 1681 return nil, err 1682 } 1683 1684 fileChanges := make(map[string][]byte) 1685 if err := applyDocumentChanges(env, editMap.DocumentChanges, fileChanges); err != nil { 1686 return nil, fmt.Errorf("applying document changes: %v", err) 1687 } 1688 return fileChanges, nil 1689 } 1690 1691 // applyDocumentChanges applies the given document changes to the editor buffer 1692 // content, recording the resulting contents in the fileChanges map. It is an 1693 // error for a change to an edit a file that is already present in the 1694 // fileChanges map. 1695 func applyDocumentChanges(env *integration.Env, changes []protocol.DocumentChanges, fileChanges map[string][]byte) error { 1696 getMapper := func(path string) (*protocol.Mapper, error) { 1697 if _, ok := fileChanges[path]; ok { 1698 return nil, fmt.Errorf("internal error: %s is already edited", path) 1699 } 1700 return env.Editor.Mapper(path) 1701 } 1702 1703 for _, change := range changes { 1704 if change.RenameFile != nil { 1705 // rename 1706 oldFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.OldURI) 1707 mapper, err := getMapper(oldFile) 1708 if err != nil { 1709 return err 1710 } 1711 newFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.NewURI) 1712 fileChanges[newFile] = mapper.Content 1713 } else { 1714 // edit 1715 filename := env.Sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) 1716 mapper, err := getMapper(filename) 1717 if err != nil { 1718 return err 1719 } 1720 patched, _, err := protocol.ApplyEdits(mapper, protocol.AsTextEdits(change.TextDocumentEdit.Edits)) 1721 if err != nil { 1722 return err 1723 } 1724 fileChanges[filename] = patched 1725 } 1726 } 1727 1728 return nil 1729 } 1730 1731 func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { 1732 // Request the range from start.Start to end.End. 1733 loc := start 1734 loc.Range.End = end.Range.End 1735 1736 // Apply the fix it suggests. 1737 changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) 1738 if err != nil { 1739 mark.errorf("codeAction failed: %v", err) 1740 return 1741 } 1742 1743 // Check the file state. 1744 checkChangedFiles(mark, changed, g) 1745 } 1746 1747 func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { 1748 changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) 1749 if err != nil { 1750 mark.errorf("codeAction failed: %v", err) 1751 return 1752 } 1753 1754 checkDiffs(mark, changed, g) 1755 } 1756 1757 func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) { 1758 loc := start 1759 loc.Range.End = end.Range.End 1760 _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) 1761 wantErr.checkErr(mark, err) 1762 } 1763 1764 // codeLensesMarker runs the @codelenses() marker, collecting @codelens marks 1765 // in the current file and comparing with the result of the 1766 // textDocument/codeLens RPC. 1767 func codeLensesMarker(mark marker) { 1768 type codeLens struct { 1769 Range protocol.Range 1770 Title string 1771 } 1772 1773 lenses := mark.run.env.CodeLens(mark.path()) 1774 var got []codeLens 1775 for _, lens := range lenses { 1776 title := "" 1777 if lens.Command != nil { 1778 title = lens.Command.Title 1779 } 1780 got = append(got, codeLens{lens.Range, title}) 1781 } 1782 1783 var want []codeLens 1784 mark.consumeExtraNotes("codelens", actionMarkerFunc(func(_ marker, loc protocol.Location, title string) { 1785 want = append(want, codeLens{loc.Range, title}) 1786 })) 1787 1788 for _, s := range [][]codeLens{got, want} { 1789 sort.Slice(s, func(i, j int) bool { 1790 li, lj := s[i], s[j] 1791 if c := protocol.CompareRange(li.Range, lj.Range); c != 0 { 1792 return c < 0 1793 } 1794 return li.Title < lj.Title 1795 }) 1796 } 1797 1798 if diff := cmp.Diff(want, got); diff != "" { 1799 mark.errorf("codelenses: unexpected diff (-want +got):\n%s", diff) 1800 } 1801 } 1802 1803 func documentLinkMarker(mark marker, g *Golden) { 1804 var b bytes.Buffer 1805 links := mark.run.env.DocumentLink(mark.path()) 1806 for _, l := range links { 1807 if l.Target == nil { 1808 mark.errorf("%s: nil link target", l.Range) 1809 continue 1810 } 1811 loc := protocol.Location{URI: mark.uri(), Range: l.Range} 1812 fmt.Fprintln(&b, mark.run.fmtLocDetails(loc, false), *l.Target) 1813 } 1814 1815 compareGolden(mark, b.Bytes(), g) 1816 } 1817 1818 // consumeExtraNotes runs the provided func for each extra note with the given 1819 // name, and deletes all matching notes. 1820 func (mark marker) consumeExtraNotes(name string, f func(marker)) { 1821 uri := mark.uri() 1822 notes := mark.run.extraNotes[uri][name] 1823 delete(mark.run.extraNotes[uri], name) 1824 1825 for _, note := range notes { 1826 f(marker{run: mark.run, note: note}) 1827 } 1828 } 1829 1830 // suggestedfixMarker implements the @suggestedfix(location, regexp, 1831 // kind, golden) marker. It acts like @diag(location, regexp), to set 1832 // the expectation of a diagnostic, but then it applies the first code 1833 // action of the specified kind suggested by the matched diagnostic. 1834 func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { 1835 loc.Range.End = loc.Range.Start // diagnostics ignore end position. 1836 // Find and remove the matching diagnostic. 1837 diag, ok := removeDiagnostic(mark, loc, re) 1838 if !ok { 1839 mark.errorf("no diagnostic at %v matches %q", loc, re) 1840 return 1841 } 1842 1843 // Apply the fix it suggests. 1844 changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) 1845 if err != nil { 1846 mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) 1847 return 1848 } 1849 1850 // Check the file state. 1851 checkDiffs(mark, changed, golden) 1852 } 1853 1854 func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr stringMatcher) { 1855 loc.Range.End = loc.Range.Start // diagnostics ignore end position. 1856 // Find and remove the matching diagnostic. 1857 diag, ok := removeDiagnostic(mark, loc, re) 1858 if !ok { 1859 mark.errorf("no diagnostic at %v matches %q", loc, re) 1860 return 1861 } 1862 1863 // Apply the fix it suggests. 1864 _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) 1865 wantErr.checkErr(mark, err) 1866 } 1867 1868 // codeAction executes a textDocument/codeAction request for the specified 1869 // location and kind. If diag is non-nil, it is used as the code action 1870 // context. 1871 // 1872 // The resulting map contains resulting file contents after the code action is 1873 // applied. Currently, this function does not support code actions that return 1874 // edits directly; it only supports code action commands. 1875 func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { 1876 changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) 1877 if err != nil { 1878 return nil, err 1879 } 1880 fileChanges := make(map[string][]byte) 1881 if err := applyDocumentChanges(env, changes, fileChanges); err != nil { 1882 return nil, fmt.Errorf("applying document changes: %v", err) 1883 } 1884 return fileChanges, nil 1885 } 1886 1887 // codeActionChanges executes a textDocument/codeAction request for the 1888 // specified location and kind, and captures the resulting document changes. 1889 // If diag is non-nil, it is used as the code action context. 1890 // If titles is non-empty, the code action title must be present among the provided titles. 1891 func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChanges, error) { 1892 // Request all code actions that apply to the diagnostic. 1893 // (The protocol supports filtering using Context.Only={actionKind} 1894 // but we can give a better error if we don't filter.) 1895 params := &protocol.CodeActionParams{ 1896 TextDocument: protocol.TextDocumentIdentifier{URI: uri}, 1897 Range: rng, 1898 Context: protocol.CodeActionContext{ 1899 Only: nil, // => all kinds 1900 }, 1901 } 1902 if diag != nil { 1903 params.Context.Diagnostics = []protocol.Diagnostic{*diag} 1904 } 1905 1906 actions, err := env.Editor.Server.CodeAction(env.Ctx, params) 1907 if err != nil { 1908 return nil, err 1909 } 1910 1911 // Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite). 1912 var candidates []protocol.CodeAction 1913 for _, act := range actions { 1914 if act.Kind == protocol.CodeActionKind(actionKind) { 1915 if len(titles) > 0 { 1916 for _, f := range titles { 1917 if act.Title == f { 1918 candidates = append(candidates, act) 1919 break 1920 } 1921 } 1922 } else { 1923 candidates = append(candidates, act) 1924 } 1925 } 1926 } 1927 if len(candidates) != 1 { 1928 for _, act := range actions { 1929 env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) 1930 } 1931 return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) 1932 } 1933 action := candidates[0] 1934 1935 // Apply the codeAction. 1936 // 1937 // Spec: 1938 // "If a code action provides an edit and a command, first the edit is 1939 // executed and then the command." 1940 // An action may specify an edit and/or a command, to be 1941 // applied in that order. But since applyDocumentChanges(env, 1942 // action.Edit.DocumentChanges) doesn't compose, for now we 1943 // assert that actions return one or the other. 1944 1945 // Resolve code action edits first if the client has resolve support 1946 // and the code action has no edits. 1947 if action.Edit == nil { 1948 editSupport, err := env.Editor.EditResolveSupport() 1949 if err != nil { 1950 return nil, err 1951 } 1952 if editSupport { 1953 resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action) 1954 if err != nil { 1955 return nil, err 1956 } 1957 action.Edit = resolved.Edit 1958 } 1959 } 1960 1961 if action.Edit != nil { 1962 if action.Edit.Changes != nil { 1963 env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title) 1964 } 1965 if action.Edit.DocumentChanges != nil { 1966 if action.Command != nil { 1967 env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title) 1968 } 1969 return action.Edit.DocumentChanges, nil 1970 } 1971 } 1972 1973 if action.Command != nil { 1974 // This is a typical CodeAction command: 1975 // 1976 // Title: "Implement error" 1977 // Command: gopls.apply_fix 1978 // Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}] 1979 // 1980 // The client makes an ExecuteCommand RPC to the server, 1981 // which dispatches it to the ApplyFix handler. 1982 // ApplyFix dispatches to the "stub_methods" suggestedfix hook (the meat). 1983 // The server then makes an ApplyEdit RPC to the client, 1984 // whose Awaiter hook gathers the edits instead of applying them. 1985 1986 _ = env.Awaiter.TakeDocumentChanges() // reset (assuming Env is confined to this thread) 1987 1988 if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ 1989 Command: action.Command.Command, 1990 Arguments: action.Command.Arguments, 1991 }); err != nil { 1992 return nil, err 1993 } 1994 return env.Awaiter.TakeDocumentChanges(), nil 1995 } 1996 1997 return nil, nil 1998 } 1999 2000 // refsMarker implements the @refs marker. 2001 func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { 2002 refs := func(includeDeclaration bool, want []protocol.Location) error { 2003 got, err := mark.server().References(mark.ctx(), &protocol.ReferenceParams{ 2004 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), 2005 Context: protocol.ReferenceContext{ 2006 IncludeDeclaration: includeDeclaration, 2007 }, 2008 }) 2009 if err != nil { 2010 return err 2011 } 2012 2013 return compareLocations(mark, got, want) 2014 } 2015 2016 for _, includeDeclaration := range []bool{false, true} { 2017 // Ignore first 'want' location if we didn't request the declaration. 2018 // TODO(adonovan): don't assume a single declaration: 2019 // there may be >1 if corresponding methods are considered. 2020 want := want 2021 if !includeDeclaration && len(want) > 0 { 2022 want = want[1:] 2023 } 2024 if err := refs(includeDeclaration, want); err != nil { 2025 mark.errorf("refs(includeDeclaration=%t) failed: %v", 2026 includeDeclaration, err) 2027 } 2028 } 2029 } 2030 2031 // implementationMarker implements the @implementation marker. 2032 func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { 2033 got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{ 2034 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), 2035 }) 2036 if err != nil { 2037 mark.errorf("implementation at %s failed: %v", src, err) 2038 return 2039 } 2040 if err := compareLocations(mark, got, want); err != nil { 2041 mark.errorf("implementation: %v", err) 2042 } 2043 } 2044 2045 func itemLocation(item protocol.CallHierarchyItem) protocol.Location { 2046 return protocol.Location{ 2047 URI: item.URI, 2048 Range: item.Range, 2049 } 2050 } 2051 2052 func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { 2053 getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { 2054 calls, err := mark.server().IncomingCalls(mark.ctx(), &protocol.CallHierarchyIncomingCallsParams{Item: item}) 2055 if err != nil { 2056 return nil, err 2057 } 2058 var locs []protocol.Location 2059 for _, call := range calls { 2060 locs = append(locs, itemLocation(call.From)) 2061 } 2062 return locs, nil 2063 } 2064 callHierarchy(mark, src, getCalls, want) 2065 } 2066 2067 func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { 2068 getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { 2069 calls, err := mark.server().OutgoingCalls(mark.ctx(), &protocol.CallHierarchyOutgoingCallsParams{Item: item}) 2070 if err != nil { 2071 return nil, err 2072 } 2073 var locs []protocol.Location 2074 for _, call := range calls { 2075 locs = append(locs, itemLocation(call.To)) 2076 } 2077 return locs, nil 2078 } 2079 callHierarchy(mark, src, getCalls, want) 2080 } 2081 2082 type callHierarchyFunc = func(protocol.CallHierarchyItem) ([]protocol.Location, error) 2083 2084 func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) { 2085 items, err := mark.server().PrepareCallHierarchy(mark.ctx(), &protocol.CallHierarchyPrepareParams{ 2086 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), 2087 }) 2088 if err != nil { 2089 mark.errorf("PrepareCallHierarchy failed: %v", err) 2090 return 2091 } 2092 if nitems := len(items); nitems != 1 { 2093 mark.errorf("PrepareCallHierarchy returned %d items, want exactly 1", nitems) 2094 return 2095 } 2096 if loc := itemLocation(items[0]); loc != src { 2097 mark.errorf("PrepareCallHierarchy found call %v, want %v", loc, src) 2098 return 2099 } 2100 calls, err := getCalls(items[0]) 2101 if err != nil { 2102 mark.errorf("call hierarchy failed: %v", err) 2103 return 2104 } 2105 if calls == nil { 2106 calls = []protocol.Location{} 2107 } 2108 // TODO(rfindley): why aren't call hierarchy results stable? 2109 sortLocs := func(locs []protocol.Location) { 2110 sort.Slice(locs, func(i, j int) bool { 2111 return protocol.CompareLocation(locs[i], locs[j]) < 0 2112 }) 2113 } 2114 sortLocs(want) 2115 sortLocs(calls) 2116 if d := cmp.Diff(want, calls); d != "" { 2117 mark.errorf("call hierarchy: unexpected results (-want +got):\n%s", d) 2118 } 2119 } 2120 2121 func inlayhintsMarker(mark marker, g *Golden) { 2122 hints := mark.run.env.InlayHints(mark.path()) 2123 2124 // Map inlay hints to text edits. 2125 edits := make([]protocol.TextEdit, len(hints)) 2126 for i, hint := range hints { 2127 var paddingLeft, paddingRight string 2128 if hint.PaddingLeft { 2129 paddingLeft = " " 2130 } 2131 if hint.PaddingRight { 2132 paddingRight = " " 2133 } 2134 edits[i] = protocol.TextEdit{ 2135 Range: protocol.Range{Start: hint.Position, End: hint.Position}, 2136 NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight), 2137 } 2138 } 2139 2140 m := mark.mapper() 2141 got, _, err := protocol.ApplyEdits(m, edits) 2142 if err != nil { 2143 mark.errorf("ApplyProtocolEdits: %v", err) 2144 return 2145 } 2146 2147 compareGolden(mark, got, g) 2148 } 2149 2150 func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) { 2151 params := &protocol.PrepareRenameParams{ 2152 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), 2153 } 2154 got, err := mark.server().PrepareRename(mark.ctx(), params) 2155 if err != nil { 2156 mark.T().Fatal(err) 2157 } 2158 if placeholder == "" { 2159 if got != nil { 2160 mark.errorf("PrepareRename(...) = %v, want nil", got) 2161 } 2162 return 2163 } 2164 want := &protocol.PrepareRenameResult{Range: spn.Range, Placeholder: placeholder} 2165 if diff := cmp.Diff(want, got); diff != "" { 2166 mark.errorf("mismatching PrepareRename result:\n%s", diff) 2167 } 2168 } 2169 2170 // symbolMarker implements the @symbol marker. 2171 func symbolMarker(mark marker, golden *Golden) { 2172 // Retrieve information about all symbols in this file. 2173 symbols, err := mark.server().DocumentSymbol(mark.ctx(), &protocol.DocumentSymbolParams{ 2174 TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, 2175 }) 2176 if err != nil { 2177 mark.errorf("DocumentSymbol request failed: %v", err) 2178 return 2179 } 2180 2181 // Format symbols one per line, sorted (in effect) by first column, a dotted name. 2182 var lines []string 2183 for _, symbol := range symbols { 2184 // Each result element is a union of (legacy) 2185 // SymbolInformation and (new) DocumentSymbol, 2186 // so we ascertain which one and then transcode. 2187 data, err := json.Marshal(symbol) 2188 if err != nil { 2189 mark.T().Fatal(err) 2190 } 2191 if _, ok := symbol.(map[string]any)["location"]; ok { 2192 // This case is not reached because Editor initialization 2193 // enables HierarchicalDocumentSymbolSupport. 2194 // TODO(adonovan): test this too. 2195 var sym protocol.SymbolInformation 2196 if err := json.Unmarshal(data, &sym); err != nil { 2197 mark.T().Fatal(err) 2198 } 2199 mark.errorf("fake Editor doesn't support SymbolInformation") 2200 2201 } else { 2202 var sym protocol.DocumentSymbol // new hierarchical hotness 2203 if err := json.Unmarshal(data, &sym); err != nil { 2204 mark.T().Fatal(err) 2205 } 2206 2207 // Print each symbol in the response tree. 2208 var visit func(sym protocol.DocumentSymbol, prefix []string) 2209 visit = func(sym protocol.DocumentSymbol, prefix []string) { 2210 var out strings.Builder 2211 out.WriteString(strings.Join(prefix, ".")) 2212 fmt.Fprintf(&out, " %q", sym.Detail) 2213 if delta := sym.Range.End.Line - sym.Range.Start.Line; delta > 0 { 2214 fmt.Fprintf(&out, " +%d lines", delta) 2215 } 2216 lines = append(lines, out.String()) 2217 2218 for _, child := range sym.Children { 2219 visit(child, append(prefix, child.Name)) 2220 } 2221 } 2222 visit(sym, []string{sym.Name}) 2223 } 2224 } 2225 sort.Strings(lines) 2226 lines = append(lines, "") // match trailing newline in .txtar file 2227 got := []byte(strings.Join(lines, "\n")) 2228 2229 // Compare with golden. 2230 want, ok := golden.Get(mark.T(), "", got) 2231 if !ok { 2232 mark.errorf("%s: missing golden file @%s", mark.note.Name, golden.id) 2233 } else if diff := cmp.Diff(string(got), string(want)); diff != "" { 2234 mark.errorf("%s: unexpected output: got:\n%s\nwant:\n%s\ndiff:\n%s", 2235 mark.note.Name, got, want, diff) 2236 } 2237 } 2238 2239 // compareLocations returns an error message if got and want are not 2240 // the same set of locations. The marker is used only for fmtLoc. 2241 func compareLocations(mark marker, got, want []protocol.Location) error { 2242 toStrings := func(locs []protocol.Location) []string { 2243 strs := make([]string, len(locs)) 2244 for i, loc := range locs { 2245 strs[i] = mark.run.fmtLoc(loc) 2246 } 2247 sort.Strings(strs) 2248 return strs 2249 } 2250 if diff := cmp.Diff(toStrings(want), toStrings(got)); diff != "" { 2251 return fmt.Errorf("incorrect result locations: (got %d, want %d):\n%s", 2252 len(got), len(want), diff) 2253 } 2254 return nil 2255 } 2256 2257 func workspaceSymbolMarker(mark marker, query string, golden *Golden) { 2258 params := &protocol.WorkspaceSymbolParams{ 2259 Query: query, 2260 } 2261 2262 gotSymbols, err := mark.server().Symbol(mark.ctx(), params) 2263 if err != nil { 2264 mark.errorf("Symbol(%q) failed: %v", query, err) 2265 return 2266 } 2267 var got bytes.Buffer 2268 for _, s := range gotSymbols { 2269 // Omit the txtar position of the symbol location; otherwise edits to the 2270 // txtar archive lead to unexpected failures. 2271 loc := mark.run.fmtLocDetails(s.Location, false) 2272 // TODO(rfindley): can we do better here, by detecting if the location is 2273 // relative to GOROOT? 2274 if loc == "" { 2275 loc = "<unknown>" 2276 } 2277 fmt.Fprintf(&got, "%s %s %s\n", loc, s.Name, s.Kind) 2278 } 2279 2280 compareGolden(mark, got.Bytes(), golden) 2281 } 2282 2283 // compareGolden compares the content of got with that of g.Get(""), reporting 2284 // errors on any mismatch. 2285 // 2286 // TODO(rfindley): use this helper in more places. 2287 func compareGolden(mark marker, got []byte, g *Golden) { 2288 want, ok := g.Get(mark.T(), "", got) 2289 if !ok { 2290 mark.errorf("missing golden file @%s", g.id) 2291 return 2292 } 2293 // Normalize newline termination: archive files (i.e. Golden content) can't 2294 // contain non-newline terminated files, except in the special case where the 2295 // file is completely empty. 2296 // 2297 // Note that txtar partitions a contiguous byte slice, so we must copy before 2298 // appending. 2299 normalize := func(s []byte) []byte { 2300 if n := len(s); n > 0 && s[n-1] != '\n' { 2301 s = append(s[:n:n], '\n') // don't mutate array 2302 } 2303 return s 2304 } 2305 got = normalize(got) 2306 want = normalize(want) 2307 if diff := compare.Bytes(want, got); diff != "" { 2308 mark.errorf("%s does not match @%s:\n%s", mark.note.Name, g.id, diff) 2309 } 2310 }