golang.org/x/tools/gopls@v0.15.3/internal/cmd/integration_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 cmdtest contains the test suite for the command line behavior of gopls. 6 package cmd_test 7 8 // This file defines integration tests of each gopls subcommand that 9 // fork+exec the command in a separate process. 10 // 11 // (Rather than execute 'go build gopls' during the test, we reproduce 12 // the main entrypoint in the test executable.) 13 // 14 // The purpose of this test is to exercise client-side logic such as 15 // argument parsing and formatting of LSP RPC responses, not server 16 // behavior; see lsp_test for that. 17 // 18 // All tests run in parallel. 19 // 20 // TODO(adonovan): 21 // - Use markers to represent positions in the input and in assertions. 22 // - Coverage of cross-cutting things like cwd, environ, span parsing, etc. 23 // - Subcommands that accept -write and -diff flags implement them 24 // consistently; factor their tests. 25 // - Add missing test for 'vulncheck' subcommand. 26 // - Add tests for client-only commands: serve, bug, help, api-json, licenses. 27 28 import ( 29 "bytes" 30 "context" 31 "encoding/json" 32 "fmt" 33 "math/rand" 34 "os" 35 "os/exec" 36 "path/filepath" 37 "regexp" 38 "strings" 39 "testing" 40 41 "golang.org/x/tools/gopls/internal/cmd" 42 "golang.org/x/tools/gopls/internal/debug" 43 "golang.org/x/tools/gopls/internal/hooks" 44 "golang.org/x/tools/gopls/internal/protocol" 45 "golang.org/x/tools/gopls/internal/util/bug" 46 "golang.org/x/tools/gopls/internal/version" 47 "golang.org/x/tools/internal/testenv" 48 "golang.org/x/tools/internal/tool" 49 "golang.org/x/tools/txtar" 50 ) 51 52 // TestVersion tests the 'version' subcommand (../info.go). 53 func TestVersion(t *testing.T) { 54 t.Parallel() 55 56 tree := writeTree(t, "") 57 58 // There's not much we can robustly assert about the actual version. 59 want := version.Version() // e.g. "master" 60 61 // basic 62 { 63 res := gopls(t, tree, "version") 64 res.checkExit(true) 65 res.checkStdout(want) 66 } 67 68 // basic, with version override 69 { 70 res := goplsWithEnv(t, tree, []string{"TEST_GOPLS_VERSION=v1.2.3"}, "version") 71 res.checkExit(true) 72 res.checkStdout(`v1\.2\.3`) 73 } 74 75 // -json flag 76 { 77 res := gopls(t, tree, "version", "-json") 78 res.checkExit(true) 79 var v debug.ServerVersion 80 if res.toJSON(&v) { 81 if v.Version != want { 82 t.Errorf("expected Version %q, got %q (%v)", want, v.Version, res) 83 } 84 } 85 } 86 } 87 88 // TestCheck tests the 'check' subcommand (../check.go). 89 func TestCheck(t *testing.T) { 90 t.Parallel() 91 92 tree := writeTree(t, ` 93 -- go.mod -- 94 module example.com 95 go 1.18 96 97 -- a.go -- 98 package a 99 import "fmt" 100 var _ = fmt.Sprintf("%s", 123) 101 102 -- b.go -- 103 package a 104 import "fmt" 105 var _ = fmt.Sprintf("%d", "123") 106 `) 107 108 // no files 109 { 110 res := gopls(t, tree, "check") 111 res.checkExit(true) 112 if res.stdout != "" { 113 t.Errorf("unexpected output: %v", res) 114 } 115 } 116 117 // one file 118 { 119 res := gopls(t, tree, "check", "./a.go") 120 res.checkExit(true) 121 res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") 122 } 123 124 // two files 125 { 126 res := gopls(t, tree, "check", "./a.go", "./b.go") 127 res.checkExit(true) 128 res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) 129 res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) 130 } 131 } 132 133 // TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go). 134 func TestCallHierarchy(t *testing.T) { 135 t.Parallel() 136 137 tree := writeTree(t, ` 138 -- go.mod -- 139 module example.com 140 go 1.18 141 142 -- a.go -- 143 package a 144 func f() {} 145 func g() { 146 f() 147 } 148 func h() { 149 f() 150 f() 151 } 152 `) 153 // missing position 154 { 155 res := gopls(t, tree, "call_hierarchy") 156 res.checkExit(false) 157 res.checkStderr("expects 1 argument") 158 } 159 // wrong place 160 { 161 res := gopls(t, tree, "call_hierarchy", "a.go:1") 162 res.checkExit(false) 163 res.checkStderr("identifier not found") 164 } 165 // f is called once from g and twice from h. 166 { 167 res := gopls(t, tree, "call_hierarchy", "a.go:2:6") 168 res.checkExit(true) 169 // We use regexp '.' as an OS-agnostic path separator. 170 res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") 171 res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") 172 res.checkStdout("identifier: function f in ..a.go:2:6-7") 173 } 174 } 175 176 // TestCodeLens tests the 'codelens' subcommand (../codelens.go). 177 func TestCodeLens(t *testing.T) { 178 t.Parallel() 179 180 tree := writeTree(t, ` 181 -- go.mod -- 182 module example.com 183 go 1.18 184 185 -- a/a.go -- 186 package a 187 -- a/a_test.go -- 188 package a_test 189 import "testing" 190 func TestPass(t *testing.T) {} 191 func TestFail(t *testing.T) { t.Fatal("fail") } 192 `) 193 // missing position 194 { 195 res := gopls(t, tree, "codelens") 196 res.checkExit(false) 197 res.checkStderr("requires a file name") 198 } 199 // list code lenses 200 { 201 res := gopls(t, tree, "codelens", "./a/a_test.go") 202 res.checkExit(true) 203 res.checkStdout(`a_test.go:3: "run test" \[gopls.test\]`) 204 res.checkStdout(`a_test.go:4: "run test" \[gopls.test\]`) 205 } 206 // no codelens with title/position 207 { 208 res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:1", "nope") 209 res.checkExit(false) 210 res.checkStderr(`no code lens at .* with title "nope"`) 211 } 212 // run the passing test 213 { 214 res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:3", "run test") 215 res.checkExit(true) 216 res.checkStderr(`PASS: TestPass`) // from go test 217 res.checkStderr("Info: all tests passed") // from gopls.test 218 } 219 // run the failing test 220 { 221 res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:4", "run test") 222 res.checkExit(false) 223 res.checkStderr(`FAIL example.com/a`) 224 res.checkStderr("Info: 1 / 1 tests failed") 225 } 226 } 227 228 // TestDefinition tests the 'definition' subcommand (../definition.go). 229 func TestDefinition(t *testing.T) { 230 t.Parallel() 231 232 tree := writeTree(t, ` 233 -- go.mod -- 234 module example.com 235 go 1.18 236 237 -- a.go -- 238 package a 239 import "fmt" 240 func f() { 241 fmt.Println() 242 } 243 func g() { 244 f() 245 } 246 `) 247 // missing position 248 { 249 res := gopls(t, tree, "definition") 250 res.checkExit(false) 251 res.checkStderr("expects 1 argument") 252 } 253 // intra-package 254 { 255 res := gopls(t, tree, "definition", "a.go:7:2") // "f()" 256 res.checkExit(true) 257 res.checkStdout("a.go:3:6-7: defined here as func f") 258 } 259 // cross-package 260 { 261 res := gopls(t, tree, "definition", "a.go:4:7") // "Println" 262 res.checkExit(true) 263 res.checkStdout("print.go.* defined here as func fmt.Println") 264 res.checkStdout("Println formats using the default formats for its operands") 265 } 266 // -json and -markdown 267 { 268 res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") 269 res.checkExit(true) 270 var defn cmd.Definition 271 if res.toJSON(&defn) { 272 if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { 273 t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) 274 } 275 } 276 } 277 } 278 279 // TestExecute tests the 'execute' subcommand (../execute.go). 280 func TestExecute(t *testing.T) { 281 t.Parallel() 282 283 tree := writeTree(t, ` 284 -- go.mod -- 285 module example.com 286 go 1.18 287 288 -- hello.go -- 289 package a 290 func main() {} 291 292 -- hello_test.go -- 293 package a 294 import "testing" 295 func TestHello(t *testing.T) { 296 t.Fatal("oops") 297 } 298 `) 299 // missing command name 300 { 301 res := gopls(t, tree, "execute") 302 res.checkExit(false) 303 res.checkStderr("requires a command") 304 } 305 // bad command 306 { 307 res := gopls(t, tree, "execute", "gopls.foo") 308 res.checkExit(false) 309 res.checkStderr("unrecognized command: gopls.foo") 310 } 311 // too few arguments 312 { 313 res := gopls(t, tree, "execute", "gopls.run_tests") 314 res.checkExit(false) 315 res.checkStderr("expected 1 input arguments, got 0") 316 } 317 // too many arguments 318 { 319 res := gopls(t, tree, "execute", "gopls.run_tests", "null", "null") 320 res.checkExit(false) 321 res.checkStderr("expected 1 input arguments, got 2") 322 } 323 // argument is not JSON 324 { 325 res := gopls(t, tree, "execute", "gopls.run_tests", "hello") 326 res.checkExit(false) 327 res.checkStderr("argument 1 is not valid JSON: invalid character 'h'") 328 } 329 // add import, show diff 330 hello := "file://" + filepath.ToSlash(tree) + "/hello.go" 331 { 332 res := gopls(t, tree, "execute", "-d", "gopls.add_import", `{"ImportPath": "fmt", "URI": "`+hello+`"}`) 333 res.checkExit(true) 334 res.checkStdout(`[+]import "fmt"`) 335 } 336 // list known packages (has a result) 337 { 338 res := gopls(t, tree, "execute", "gopls.list_known_packages", `{"URI": "`+hello+`"}`) 339 res.checkExit(true) 340 res.checkStdout(`"fmt"`) 341 res.checkStdout(`"encoding/json"`) 342 } 343 // run tests 344 { 345 helloTest := "file://" + filepath.ToSlash(tree) + "/hello_test.go" 346 res := gopls(t, tree, "execute", "gopls.run_tests", `{"URI": "`+helloTest+`", "Tests": ["TestHello"]}`) 347 res.checkExit(false) 348 res.checkStderr(`hello_test.go:4: oops`) 349 res.checkStderr(`1 / 1 tests failed`) 350 } 351 } 352 353 // TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). 354 func TestFoldingRanges(t *testing.T) { 355 t.Parallel() 356 357 tree := writeTree(t, ` 358 -- go.mod -- 359 module example.com 360 go 1.18 361 362 -- a.go -- 363 package a 364 func f(x int) { 365 // hello 366 } 367 `) 368 // missing filename 369 { 370 res := gopls(t, tree, "folding_ranges") 371 res.checkExit(false) 372 res.checkStderr("expects 1 argument") 373 } 374 // success 375 { 376 res := gopls(t, tree, "folding_ranges", "a.go") 377 res.checkExit(true) 378 res.checkStdout("2:8-2:13") // params (x int) 379 res.checkStdout("2:16-4:1") // body { ... } 380 } 381 } 382 383 // TestFormat tests the 'format' subcommand (../format.go). 384 func TestFormat(t *testing.T) { 385 t.Parallel() 386 387 tree := writeTree(t, ` 388 -- a.go -- 389 package a ; func f ( ) { } 390 `) 391 const want = `package a 392 393 func f() {} 394 ` 395 396 // no files => nop 397 { 398 res := gopls(t, tree, "format") 399 res.checkExit(true) 400 } 401 // default => print formatted result 402 { 403 res := gopls(t, tree, "format", "a.go") 404 res.checkExit(true) 405 if res.stdout != want { 406 t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) 407 } 408 } 409 // start/end position not supported (unless equal to start/end of file) 410 { 411 res := gopls(t, tree, "format", "a.go:1-2") 412 res.checkExit(false) 413 res.checkStderr("only full file formatting supported") 414 } 415 // -list: show only file names 416 { 417 res := gopls(t, tree, "format", "-list", "a.go") 418 res.checkExit(true) 419 res.checkStdout("a.go") 420 } 421 // -diff prints a unified diff 422 { 423 res := gopls(t, tree, "format", "-diff", "a.go") 424 res.checkExit(true) 425 // We omit the filenames as they vary by OS. 426 want := ` 427 -package a ; func f ( ) { } 428 +package a 429 + 430 +func f() {} 431 ` 432 res.checkStdout(regexp.QuoteMeta(want)) 433 } 434 // -write updates the file 435 { 436 res := gopls(t, tree, "format", "-write", "a.go") 437 res.checkExit(true) 438 res.checkStdout("^$") // empty 439 checkContent(t, filepath.Join(tree, "a.go"), want) 440 } 441 } 442 443 // TestHighlight tests the 'highlight' subcommand (../highlight.go). 444 func TestHighlight(t *testing.T) { 445 t.Parallel() 446 447 tree := writeTree(t, ` 448 -- a.go -- 449 package a 450 import "fmt" 451 func f() { 452 fmt.Println() 453 fmt.Println() 454 } 455 `) 456 457 // no arguments 458 { 459 res := gopls(t, tree, "highlight") 460 res.checkExit(false) 461 res.checkStderr("expects 1 argument") 462 } 463 // all occurrences of Println 464 { 465 res := gopls(t, tree, "highlight", "a.go:4:7") 466 res.checkExit(true) 467 res.checkStdout("a.go:4:6-13") 468 res.checkStdout("a.go:5:6-13") 469 } 470 } 471 472 // TestImplementations tests the 'implementation' subcommand (../implementation.go). 473 func TestImplementations(t *testing.T) { 474 t.Parallel() 475 476 tree := writeTree(t, ` 477 -- a.go -- 478 package a 479 import "fmt" 480 type T int 481 func (T) String() string { return "" } 482 `) 483 484 // no arguments 485 { 486 res := gopls(t, tree, "implementation") 487 res.checkExit(false) 488 res.checkStderr("expects 1 argument") 489 } 490 // T.String 491 { 492 res := gopls(t, tree, "implementation", "a.go:4:10") 493 res.checkExit(true) 494 // TODO(adonovan): extract and check the content of the reported ranges? 495 // We use regexp '.' as an OS-agnostic path separator. 496 res.checkStdout("fmt.print.go:") // fmt.Stringer.String 497 res.checkStdout("runtime.error.go:") // runtime.stringer.String 498 } 499 } 500 501 // TestImports tests the 'imports' subcommand (../imports.go). 502 func TestImports(t *testing.T) { 503 t.Parallel() 504 505 tree := writeTree(t, ` 506 -- a.go -- 507 package a 508 func _() { 509 fmt.Println() 510 } 511 `) 512 513 want := ` 514 package a 515 516 import "fmt" 517 func _() { 518 fmt.Println() 519 } 520 `[1:] 521 522 // no arguments 523 { 524 res := gopls(t, tree, "imports") 525 res.checkExit(false) 526 res.checkStderr("expects 1 argument") 527 } 528 // default: print with imports 529 { 530 res := gopls(t, tree, "imports", "a.go") 531 res.checkExit(true) 532 if res.stdout != want { 533 t.Errorf("imports: got <<%s>>, want <<%s>>", res.stdout, want) 534 } 535 } 536 // -diff: show a unified diff 537 { 538 res := gopls(t, tree, "imports", "-diff", "a.go") 539 res.checkExit(true) 540 res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) 541 } 542 // -write: update file 543 { 544 res := gopls(t, tree, "imports", "-write", "a.go") 545 res.checkExit(true) 546 checkContent(t, filepath.Join(tree, "a.go"), want) 547 } 548 } 549 550 // TestLinks tests the 'links' subcommand (../links.go). 551 func TestLinks(t *testing.T) { 552 t.Parallel() 553 554 tree := writeTree(t, ` 555 -- a.go -- 556 // Link in package doc: https://pkg.go.dev/ 557 package a 558 559 // Link in internal comment: https://go.dev/cl 560 561 // Doc comment link: https://blog.go.dev/ 562 func f() {} 563 `) 564 // no arguments 565 { 566 res := gopls(t, tree, "links") 567 res.checkExit(false) 568 res.checkStderr("expects 1 argument") 569 } 570 // success 571 { 572 res := gopls(t, tree, "links", "a.go") 573 res.checkExit(true) 574 res.checkStdout("https://go.dev/cl") 575 res.checkStdout("https://pkg.go.dev") 576 res.checkStdout("https://blog.go.dev/") 577 } 578 // -json 579 { 580 res := gopls(t, tree, "links", "-json", "a.go") 581 res.checkExit(true) 582 res.checkStdout("https://pkg.go.dev") 583 res.checkStdout("https://go.dev/cl") 584 res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 585 var links []protocol.DocumentLink 586 if res.toJSON(&links) { 587 // Check just one of the three locations. 588 if got, want := fmt.Sprint(links[2].Range), "5:21-5:41"; got != want { 589 t.Errorf("wrong link location: got %v, want %v", got, want) 590 } 591 } 592 } 593 } 594 595 // TestReferences tests the 'references' subcommand (../references.go). 596 func TestReferences(t *testing.T) { 597 t.Parallel() 598 599 tree := writeTree(t, ` 600 -- go.mod -- 601 module example.com 602 go 1.18 603 604 -- a.go -- 605 package a 606 import "fmt" 607 func f() { 608 fmt.Println() 609 } 610 611 -- b.go -- 612 package a 613 import "fmt" 614 func g() { 615 fmt.Println() 616 } 617 `) 618 // no arguments 619 { 620 res := gopls(t, tree, "references") 621 res.checkExit(false) 622 res.checkStderr("expects 1 argument") 623 } 624 // fmt.Println 625 { 626 res := gopls(t, tree, "references", "a.go:4:10") 627 res.checkExit(true) 628 res.checkStdout("a.go:4:6-13") 629 res.checkStdout("b.go:4:6-13") 630 } 631 } 632 633 // TestSignature tests the 'signature' subcommand (../signature.go). 634 func TestSignature(t *testing.T) { 635 t.Parallel() 636 637 tree := writeTree(t, ` 638 -- go.mod -- 639 module example.com 640 go 1.18 641 642 -- a.go -- 643 package a 644 import "fmt" 645 func f() { 646 fmt.Println(123) 647 } 648 `) 649 // no arguments 650 { 651 res := gopls(t, tree, "signature") 652 res.checkExit(false) 653 res.checkStderr("expects 1 argument") 654 } 655 // at 123 inside fmt.Println() call 656 { 657 res := gopls(t, tree, "signature", "a.go:4:15") 658 res.checkExit(true) 659 res.checkStdout("Println\\(a ...") 660 res.checkStdout("Println formats using the default formats...") 661 } 662 } 663 664 // TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go). 665 func TestPrepareRename(t *testing.T) { 666 t.Parallel() 667 668 tree := writeTree(t, ` 669 -- go.mod -- 670 module example.com 671 go 1.18 672 673 -- a.go -- 674 package a 675 func oldname() {} 676 `) 677 // no arguments 678 { 679 res := gopls(t, tree, "prepare_rename") 680 res.checkExit(false) 681 res.checkStderr("expects 1 argument") 682 } 683 // in 'package' keyword 684 { 685 res := gopls(t, tree, "prepare_rename", "a.go:1:3") 686 res.checkExit(false) 687 res.checkStderr("request is not valid at the given position") 688 } 689 // in 'package' identifier (not supported by client) 690 { 691 res := gopls(t, tree, "prepare_rename", "a.go:1:9") 692 res.checkExit(false) 693 res.checkStderr("can't rename package") 694 } 695 // in func oldname 696 { 697 res := gopls(t, tree, "prepare_rename", "a.go:2:9") 698 res.checkExit(true) 699 res.checkStdout("a.go:2:6-13") // all of "oldname" 700 } 701 } 702 703 // TestRename tests the 'rename' subcommand (../rename.go). 704 func TestRename(t *testing.T) { 705 t.Parallel() 706 707 tree := writeTree(t, ` 708 -- go.mod -- 709 module example.com 710 go 1.18 711 712 -- a.go -- 713 package a 714 func oldname() {} 715 `) 716 // no arguments 717 { 718 res := gopls(t, tree, "rename") 719 res.checkExit(false) 720 res.checkStderr("expects 2 arguments") 721 } 722 // missing newname 723 { 724 res := gopls(t, tree, "rename", "a.go:1:3") 725 res.checkExit(false) 726 res.checkStderr("expects 2 arguments") 727 } 728 // in 'package' keyword 729 { 730 res := gopls(t, tree, "rename", "a.go:1:3", "newname") 731 res.checkExit(false) 732 res.checkStderr("no identifier found") 733 } 734 // in 'package' identifier 735 { 736 res := gopls(t, tree, "rename", "a.go:1:9", "newname") 737 res.checkExit(false) 738 res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) 739 } 740 // success, func oldname (and -diff) 741 { 742 res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") 743 res.checkExit(true) 744 res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) 745 res.checkStdout(regexp.QuoteMeta("+func newname() {}")) 746 } 747 } 748 749 // TestSymbols tests the 'symbols' subcommand (../symbols.go). 750 func TestSymbols(t *testing.T) { 751 t.Parallel() 752 753 tree := writeTree(t, ` 754 -- go.mod -- 755 module example.com 756 go 1.18 757 758 -- a.go -- 759 package a 760 func f() 761 var v int 762 const c = 0 763 `) 764 // no files 765 { 766 res := gopls(t, tree, "symbols") 767 res.checkExit(false) 768 res.checkStderr("expects 1 argument") 769 } 770 // success 771 { 772 res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) 773 res.checkExit(true) 774 res.checkStdout("f Function 2:6-2:7") 775 res.checkStdout("v Variable 3:5-3:6") 776 res.checkStdout("c Constant 4:7-4:8") 777 } 778 } 779 780 // TestSemtok tests the 'semtok' subcommand (../semantictokens.go). 781 func TestSemtok(t *testing.T) { 782 t.Parallel() 783 784 tree := writeTree(t, ` 785 -- go.mod -- 786 module example.com 787 go 1.18 788 789 -- a.go -- 790 package a 791 func f() 792 var v int 793 const c = 0 794 `) 795 // no files 796 { 797 res := gopls(t, tree, "semtok") 798 res.checkExit(false) 799 res.checkStderr("expected one file name") 800 } 801 // success 802 { 803 res := gopls(t, tree, "semtok", "a.go") 804 res.checkExit(true) 805 got := res.stdout 806 want := ` 807 /*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a 808 /*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f() 809 /*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary]*/int 810 /*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/c = /*⇒1,number,[]*/0 811 `[1:] 812 if got != want { 813 t.Errorf("semtok: got <<%s>>, want <<%s>>", got, want) 814 } 815 } 816 } 817 818 func TestStats(t *testing.T) { 819 t.Parallel() 820 821 tree := writeTree(t, ` 822 -- go.mod -- 823 module example.com 824 go 1.18 825 826 -- a.go -- 827 package a 828 -- b/b.go -- 829 package b 830 -- testdata/foo.go -- 831 package foo 832 `) 833 834 // Trigger a bug report with a distinctive string 835 // and check that it was durably recorded. 836 oops := fmt.Sprintf("oops-%d", rand.Int()) 837 { 838 env := []string{"TEST_GOPLS_BUG=" + oops} 839 res := goplsWithEnv(t, tree, env, "bug") 840 res.checkExit(true) 841 } 842 843 res := gopls(t, tree, "stats") 844 res.checkExit(true) 845 846 var stats cmd.GoplsStats 847 if err := json.Unmarshal([]byte(res.stdout), &stats); err != nil { 848 t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) 849 } 850 851 // a few sanity checks 852 checks := []struct { 853 field string 854 got int 855 want int 856 }{ 857 { 858 "WorkspaceStats.Views[0].WorkspaceModules", 859 stats.WorkspaceStats.Views[0].WorkspacePackages.Modules, 860 1, 861 }, 862 { 863 "WorkspaceStats.Views[0].WorkspacePackages", 864 stats.WorkspaceStats.Views[0].WorkspacePackages.Packages, 865 2, 866 }, 867 {"DirStats.Files", stats.DirStats.Files, 4}, 868 {"DirStats.GoFiles", stats.DirStats.GoFiles, 2}, 869 {"DirStats.ModFiles", stats.DirStats.ModFiles, 1}, 870 {"DirStats.TestdataFiles", stats.DirStats.TestdataFiles, 1}, 871 } 872 for _, check := range checks { 873 if check.got != check.want { 874 t.Errorf("stats.%s = %d, want %d", check.field, check.got, check.want) 875 } 876 } 877 878 // Check that we got a BugReport with the expected message. 879 { 880 got := fmt.Sprint(stats.BugReports) 881 wants := []string{ 882 "cmd/info.go", // File containing call to bug.Report 883 oops, // Description 884 } 885 for _, want := range wants { 886 if !strings.Contains(got, want) { 887 t.Errorf("BugReports does not contain %q. Got:<<%s>>", want, got) 888 break 889 } 890 } 891 } 892 893 // Check that -anon suppresses fields containing user information. 894 { 895 res2 := gopls(t, tree, "stats", "-anon") 896 res2.checkExit(true) 897 898 var stats2 cmd.GoplsStats 899 if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { 900 t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) 901 } 902 if got := len(stats2.BugReports); got > 0 { 903 t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) 904 } 905 var stats2AsMap map[string]any 906 if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { 907 t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) 908 } 909 // GOPACKAGESDRIVER is user information, but is ok to print zero value. 910 if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; ok && v != "" { 911 t.Errorf(`Got GOPACKAGESDRIVER=(%v, %v); want ("", true(found))`, v, ok) 912 } 913 } 914 915 // Check that -anon suppresses fields containing non-zero user information. 916 { 917 res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") 918 res3.checkExit(true) 919 920 var statsAsMap3 map[string]interface{} 921 if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { 922 t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) 923 } 924 // GOPACKAGESDRIVER is user information, want non-empty value to be omitted. 925 if v, ok := statsAsMap3["GOPACKAGESDRIVER"]; ok { 926 t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", false(not found))`, v, ok) 927 } 928 } 929 } 930 931 // TestFix tests the 'fix' subcommand (../suggested_fix.go). 932 func TestFix(t *testing.T) { 933 t.Parallel() 934 935 tree := writeTree(t, ` 936 -- go.mod -- 937 module example.com 938 go 1.18 939 940 -- a.go -- 941 package a 942 type T int 943 func f() (int, string) { return } 944 945 -- b.go -- 946 package a 947 import "io" 948 var _ io.Reader = C{} 949 type C struct{} 950 `) 951 952 // no arguments 953 { 954 res := gopls(t, tree, "fix") 955 res.checkExit(false) 956 res.checkStderr("expects at least 1 argument") 957 } 958 // success with default kinds, {quickfix}. 959 // -a is always required because no fix is currently "preferred" (!) 960 { 961 res := gopls(t, tree, "fix", "-a", "a.go") 962 res.checkExit(true) 963 got := res.stdout 964 want := ` 965 package a 966 type T int 967 func f() (int, string) { return 0, "" } 968 969 `[1:] 970 if got != want { 971 t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) 972 } 973 } 974 // success, with explicit CodeAction kind and diagnostics span. 975 { 976 res := gopls(t, tree, "fix", "-a", "b.go:#40", "quickfix") 977 res.checkExit(true) 978 got := res.stdout 979 want := ` 980 package a 981 982 import "io" 983 984 var _ io.Reader = C{} 985 986 type C struct{} 987 988 // Read implements io.Reader. 989 func (c C) Read(p []byte) (n int, err error) { 990 panic("unimplemented") 991 } 992 `[1:] 993 if got != want { 994 t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) 995 } 996 } 997 } 998 999 // TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go). 1000 func TestWorkspaceSymbol(t *testing.T) { 1001 t.Parallel() 1002 1003 tree := writeTree(t, ` 1004 -- go.mod -- 1005 module example.com 1006 go 1.18 1007 1008 -- a.go -- 1009 package a 1010 func someFunctionName() 1011 `) 1012 // no files 1013 { 1014 res := gopls(t, tree, "workspace_symbol") 1015 res.checkExit(false) 1016 res.checkStderr("expects 1 argument") 1017 } 1018 // success 1019 { 1020 res := gopls(t, tree, "workspace_symbol", "meFun") 1021 res.checkExit(true) 1022 res.checkStdout("a.go:2:6-22 someFunctionName Function") 1023 } 1024 } 1025 1026 // -- test framework -- 1027 1028 func TestMain(m *testing.M) { 1029 switch os.Getenv("ENTRYPOINT") { 1030 case "goplsMain": 1031 goplsMain() 1032 default: 1033 os.Exit(m.Run()) 1034 } 1035 } 1036 1037 // This function is a stand-in for gopls.main in ../../../../main.go. 1038 func goplsMain() { 1039 // Panic on bugs (unlike the production gopls command), 1040 // except in tests that inject calls to bug.Report. 1041 if os.Getenv("TEST_GOPLS_BUG") == "" { 1042 bug.PanicOnBugs = true 1043 } 1044 1045 if v := os.Getenv("TEST_GOPLS_VERSION"); v != "" { 1046 version.VersionOverride = v 1047 } 1048 1049 tool.Main(context.Background(), cmd.New(hooks.Options), os.Args[1:]) 1050 } 1051 1052 // writeTree extracts a txtar archive into a new directory and returns its path. 1053 func writeTree(t *testing.T, archive string) string { 1054 root := t.TempDir() 1055 1056 // This unfortunate step is required because gopls output 1057 // expands symbolic links in its input file names (arguably it 1058 // should not), and on macOS the temp dir is in /var -> private/var. 1059 root, err := filepath.EvalSymlinks(root) 1060 if err != nil { 1061 t.Fatal(err) 1062 } 1063 1064 for _, f := range txtar.Parse([]byte(archive)).Files { 1065 filename := filepath.Join(root, f.Name) 1066 if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { 1067 t.Fatal(err) 1068 } 1069 if err := os.WriteFile(filename, f.Data, 0666); err != nil { 1070 t.Fatal(err) 1071 } 1072 } 1073 return root 1074 } 1075 1076 // gopls executes gopls in a child process. 1077 func gopls(t *testing.T, dir string, args ...string) *result { 1078 return goplsWithEnv(t, dir, nil, args...) 1079 } 1080 1081 func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *result { 1082 testenv.NeedsTool(t, "go") 1083 1084 // Catch inadvertent use of dir=".", which would make 1085 // the ReplaceAll below unpredictable. 1086 if !filepath.IsAbs(dir) { 1087 t.Fatalf("dir is not absolute: %s", dir) 1088 } 1089 1090 goplsCmd := exec.Command(os.Args[0], args...) 1091 goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") 1092 goplsCmd.Env = append(goplsCmd.Env, "GOPACKAGESDRIVER=off") 1093 goplsCmd.Env = append(goplsCmd.Env, env...) 1094 goplsCmd.Dir = dir 1095 goplsCmd.Stdout = new(bytes.Buffer) 1096 goplsCmd.Stderr = new(bytes.Buffer) 1097 1098 cmdErr := goplsCmd.Run() 1099 1100 stdout := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stdout), dir, ".") 1101 stderr := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stderr), dir, ".") 1102 exitcode := 0 1103 if cmdErr != nil { 1104 if exitErr, ok := cmdErr.(*exec.ExitError); ok { 1105 exitcode = exitErr.ExitCode() 1106 } else { 1107 stderr = cmdErr.Error() // (execve failure) 1108 exitcode = -1 1109 } 1110 } 1111 res := &result{ 1112 t: t, 1113 command: "gopls " + strings.Join(args, " "), 1114 exitcode: exitcode, 1115 stdout: stdout, 1116 stderr: stderr, 1117 } 1118 if false { 1119 t.Log(res) 1120 } 1121 return res 1122 } 1123 1124 // A result holds the result of a gopls invocation, and provides assertion helpers. 1125 type result struct { 1126 t *testing.T 1127 command string 1128 exitcode int 1129 stdout, stderr string 1130 } 1131 1132 func (res *result) String() string { 1133 return fmt.Sprintf("%s: exit=%d stdout=<<%s>> stderr=<<%s>>", 1134 res.command, res.exitcode, res.stdout, res.stderr) 1135 } 1136 1137 // checkExit asserts that gopls returned the expected exit code. 1138 func (res *result) checkExit(success bool) { 1139 res.t.Helper() 1140 if (res.exitcode == 0) != success { 1141 res.t.Errorf("%s: exited with code %d, want success: %t (%s)", 1142 res.command, res.exitcode, success, res) 1143 } 1144 } 1145 1146 // checkStdout asserts that the gopls standard output matches the pattern. 1147 func (res *result) checkStdout(pattern string) { 1148 res.t.Helper() 1149 res.checkOutput(pattern, "stdout", res.stdout) 1150 } 1151 1152 // checkStderr asserts that the gopls standard error matches the pattern. 1153 func (res *result) checkStderr(pattern string) { 1154 res.t.Helper() 1155 res.checkOutput(pattern, "stderr", res.stderr) 1156 } 1157 1158 func (res *result) checkOutput(pattern, name, content string) { 1159 res.t.Helper() 1160 if match, err := regexp.MatchString(pattern, content); err != nil { 1161 res.t.Errorf("invalid regexp: %v", err) 1162 } else if !match { 1163 res.t.Errorf("%s: %s does not match [%s]; got <<%s>>", 1164 res.command, name, pattern, content) 1165 } 1166 } 1167 1168 // toJSON decodes res.stdout as JSON into to *ptr and reports its success. 1169 func (res *result) toJSON(ptr interface{}) bool { 1170 if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { 1171 res.t.Errorf("invalid JSON %v", err) 1172 return false 1173 } 1174 return true 1175 } 1176 1177 // checkContent checks that the contents of the file are as expected. 1178 func checkContent(t *testing.T, filename, want string) { 1179 data, err := os.ReadFile(filename) 1180 if err != nil { 1181 t.Error(err) 1182 return 1183 } 1184 if got := string(data); got != want { 1185 t.Errorf("content of %s is <<%s>>, want <<%s>>", filename, got, want) 1186 } 1187 }