|  | // Copyright 2023 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | // Package cmdtest contains the test suite for the command line behavior of gopls. | 
|  | package cmd_test | 
|  |  | 
|  | // This file defines integration tests of each gopls subcommand that | 
|  | // fork+exec the command in a separate process. | 
|  | // | 
|  | // (Rather than execute 'go build gopls' during the test, we reproduce | 
|  | // the main entrypoint in the test executable.) | 
|  | // | 
|  | // The purpose of this test is to exercise client-side logic such as | 
|  | // argument parsing and formatting of LSP RPC responses, not server | 
|  | // behavior; see lsp_test for that. | 
|  | // | 
|  | // All tests run in parallel. | 
|  | // | 
|  | // TODO(adonovan): | 
|  | // - Use markers to represent positions in the input and in assertions. | 
|  | // - Coverage of cross-cutting things like cwd, environ, span parsing, etc. | 
|  | // - Subcommands that accept -write and -diff flags implement them | 
|  | //   consistently; factor their tests. | 
|  | // - Add missing test for 'vulncheck' subcommand. | 
|  | // - Add tests for client-only commands: serve, bug, help, api-json, licenses. | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "math/rand" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "strings" | 
|  | "testing" | 
|  |  | 
|  | "golang.org/x/tools/gopls/internal/cmd" | 
|  | "golang.org/x/tools/gopls/internal/debug" | 
|  | "golang.org/x/tools/gopls/internal/protocol" | 
|  | "golang.org/x/tools/gopls/internal/util/bug" | 
|  | "golang.org/x/tools/gopls/internal/version" | 
|  | "golang.org/x/tools/internal/testenv" | 
|  | "golang.org/x/tools/internal/tool" | 
|  | "golang.org/x/tools/txtar" | 
|  | ) | 
|  |  | 
|  | // TestVersion tests the 'version' subcommand (info.go). | 
|  | func TestVersion(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, "") | 
|  |  | 
|  | // There's not much we can robustly assert about the actual version. | 
|  | want := version.Version() // e.g. "master" | 
|  |  | 
|  | // basic | 
|  | { | 
|  | res := gopls(t, tree, "version") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(want) | 
|  | } | 
|  |  | 
|  | // basic, with version override | 
|  | { | 
|  | res := goplsWithEnv(t, tree, []string{"TEST_GOPLS_VERSION=v1.2.3"}, "version") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`v1\.2\.3`) | 
|  | } | 
|  |  | 
|  | // -json flag | 
|  | { | 
|  | res := gopls(t, tree, "version", "-json") | 
|  | res.checkExit(true) | 
|  | var v debug.ServerVersion | 
|  | if res.toJSON(&v) { | 
|  | if v.Version != want { | 
|  | t.Errorf("expected Version %q, got %q (%v)", want, v.Version, res) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestCheck tests the 'check' subcommand (check.go). | 
|  | func TestCheck(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | var _ = fmt.Sprintf("%s", 123) | 
|  |  | 
|  | -- b.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | var _ = fmt.Sprintf("%d", "123") | 
|  | -- c/c.go -- | 
|  | package c | 
|  | var C int | 
|  | -- c/c2.go -- | 
|  | package c | 
|  | var C int | 
|  | -- d/d.go -- | 
|  | package d | 
|  |  | 
|  | import "io/ioutil" | 
|  |  | 
|  | var _ = ioutil.ReadFile | 
|  | `) | 
|  |  | 
|  | // no files | 
|  | { | 
|  | res := gopls(t, tree, "check") | 
|  | res.checkExit(true) | 
|  | if res.stdout != "" { | 
|  | t.Errorf("unexpected output: %v", res) | 
|  | } | 
|  | } | 
|  |  | 
|  | // one file | 
|  | { | 
|  | res := gopls(t, tree, "check", "./a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") | 
|  | } | 
|  |  | 
|  | // two files | 
|  | { | 
|  | res := gopls(t, tree, "check", "./a.go", "./b.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) | 
|  | res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) | 
|  | } | 
|  |  | 
|  | // diagnostic with related information spanning files | 
|  | { | 
|  | res := gopls(t, tree, "check", "./c/c2.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`c2.go:2:5-6: C redeclared in this block`) | 
|  | res.checkStdout(`c.go:2:5-6: - other declaration of C`) | 
|  | } | 
|  |  | 
|  | // No deprecated (hint) diagnostic without -severity. | 
|  | { | 
|  | res := gopls(t, tree, "check", "./d/d.go") | 
|  | res.checkExit(true) | 
|  | if len(res.stdout) > 0 { | 
|  | t.Errorf("check ./d/d.go returned unexpected output:\n%s", res.stdout) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Deprecated (hint) diagnostics with -severity=hint | 
|  | { | 
|  | res := gopls(t, tree, "check", "-severity=hint", "./d/d.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`ioutil.ReadFile is deprecated`) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestCallHierarchy tests the 'call_hierarchy' subcommand (call_hierarchy.go). | 
|  | func TestCallHierarchy(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func f() {} | 
|  | func g() { | 
|  | f() | 
|  | } | 
|  | func h() { | 
|  | f() | 
|  | f() | 
|  | } | 
|  | `) | 
|  | // missing position | 
|  | { | 
|  | res := gopls(t, tree, "call_hierarchy") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // wrong place | 
|  | { | 
|  | res := gopls(t, tree, "call_hierarchy", "a.go:1") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("identifier not found") | 
|  | } | 
|  | // f is called once from g and twice from h. | 
|  | { | 
|  | res := gopls(t, tree, "call_hierarchy", "a.go:2:6") | 
|  | res.checkExit(true) | 
|  | // We use regexp '.' as an OS-agnostic path separator. | 
|  | res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") | 
|  | res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") | 
|  | res.checkStdout("identifier: function f in ..a.go:2:6-7") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestCodeLens tests the 'codelens' subcommand (codelens.go). | 
|  | func TestCodeLens(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a/a.go -- | 
|  | package a | 
|  | -- a/a_test.go -- | 
|  | package a_test | 
|  | import "testing" | 
|  | func TestPass(t *testing.T) {} | 
|  | func TestFail(t *testing.T) { t.Fatal("fail") } | 
|  | `) | 
|  | // missing position | 
|  | { | 
|  | res := gopls(t, tree, "codelens") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("requires a file name") | 
|  | } | 
|  | // list code lenses | 
|  | { | 
|  | res := gopls(t, tree, "codelens", "./a/a_test.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`a_test.go:3: "run test" \[gopls.run_tests\]`) | 
|  | res.checkStdout(`a_test.go:4: "run test" \[gopls.run_tests\]`) | 
|  | } | 
|  | // no codelens with title/position | 
|  | { | 
|  | res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:1", "nope") | 
|  | res.checkExit(false) | 
|  | res.checkStderr(`no code lens at .* with title "nope"`) | 
|  | } | 
|  | // run the passing test | 
|  | { | 
|  | res := gopls(t, tree, "-v", "codelens", "-exec", "./a/a_test.go:3", "run test") | 
|  | res.checkExit(true) | 
|  | res.checkStderr(`PASS: TestPass`)         // from go test | 
|  | res.checkStderr("Info: all tests passed") // from gopls.test | 
|  | } | 
|  | // run the failing test | 
|  | { | 
|  | res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:4", "run test") | 
|  | res.checkExit(false) | 
|  | res.checkStderr(`FAIL	example.com/a`) | 
|  | res.checkStderr("Info: 1 / 1 tests failed") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestDefinition tests the 'definition' subcommand (definition.go). | 
|  | func TestDefinition(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | func f() { | 
|  | fmt.Println() | 
|  | } | 
|  | func g() { | 
|  | f() | 
|  | } | 
|  | `) | 
|  | // missing position | 
|  | { | 
|  | res := gopls(t, tree, "definition") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // intra-package | 
|  | { | 
|  | res := gopls(t, tree, "definition", "a.go:7:2") // "f()" | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go:3:6-7: defined here as func f") | 
|  | } | 
|  | // cross-package | 
|  | { | 
|  | res := gopls(t, tree, "definition", "a.go:4:7") // "Println" | 
|  | res.checkExit(true) | 
|  | res.checkStdout("print.go.* defined here as func fmt.Println") | 
|  | res.checkStdout("Println formats using the default formats for its operands") | 
|  | } | 
|  | // -json and -markdown | 
|  | { | 
|  | res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") | 
|  | res.checkExit(true) | 
|  | var defn cmd.Definition | 
|  | if res.toJSON(&defn) { | 
|  | if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { | 
|  | t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestExecute tests the 'execute' subcommand (execute.go). | 
|  | func TestExecute(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- hello.go -- | 
|  | package a | 
|  | func main() {} | 
|  |  | 
|  | -- hello_test.go -- | 
|  | package a | 
|  | import "testing" | 
|  | func TestHello(t *testing.T) { | 
|  | t.Fatal("oops") | 
|  | } | 
|  | `) | 
|  | // missing command name | 
|  | { | 
|  | res := gopls(t, tree, "execute") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("requires a command") | 
|  | } | 
|  | // bad command | 
|  | { | 
|  | res := gopls(t, tree, "execute", "gopls.foo") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("unrecognized command: gopls.foo") | 
|  | } | 
|  | // too few arguments | 
|  | { | 
|  | res := gopls(t, tree, "execute", "gopls.run_tests") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expected 1 input arguments, got 0") | 
|  | } | 
|  | // too many arguments | 
|  | { | 
|  | res := gopls(t, tree, "execute", "gopls.run_tests", "null", "null") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expected 1 input arguments, got 2") | 
|  | } | 
|  | // argument is not JSON | 
|  | { | 
|  | res := gopls(t, tree, "execute", "gopls.run_tests", "hello") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("argument 1 is not valid JSON: invalid character 'h'") | 
|  | } | 
|  | // add import, show diff | 
|  | hello := "file://" + filepath.ToSlash(tree) + "/hello.go" | 
|  | { | 
|  | res := gopls(t, tree, "execute", "-d", "gopls.add_import", `{"ImportPath": "fmt", "URI": "`+hello+`"}`) | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`[+]import "fmt"`) | 
|  | } | 
|  | // list known packages (has a result) | 
|  | { | 
|  | res := gopls(t, tree, "execute", "gopls.list_known_packages", `{"URI": "`+hello+`"}`) | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`"fmt"`) | 
|  | res.checkStdout(`"encoding/json"`) | 
|  | } | 
|  | // run tests | 
|  | { | 
|  | helloTest := "file://" + filepath.ToSlash(tree) + "/hello_test.go" | 
|  | res := gopls(t, tree, "execute", "gopls.run_tests", `{"URI": "`+helloTest+`", "Tests": ["TestHello"]}`) | 
|  | res.checkExit(false) | 
|  | res.checkStderr(`hello_test.go:4: oops`) | 
|  | res.checkStderr(`1 / 1 tests failed`) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestFoldingRanges tests the 'folding_ranges' subcommand (folding_range.go). | 
|  | func TestFoldingRanges(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func f(x int) { | 
|  | // hello | 
|  | } | 
|  | `) | 
|  | // missing filename | 
|  | { | 
|  | res := gopls(t, tree, "folding_ranges") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // success | 
|  | { | 
|  | res := gopls(t, tree, "folding_ranges", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("2:8-2:13") // params (x int) | 
|  | res.checkStdout("2:16-4:1") //   body { ... } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestFormat tests the 'format' subcommand (format.go). | 
|  | func TestFormat(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- a.go -- | 
|  | package a ;  func f ( ) { } | 
|  | `) | 
|  | const want = `package a | 
|  |  | 
|  | func f() {} | 
|  | ` | 
|  |  | 
|  | // no files => nop | 
|  | { | 
|  | res := gopls(t, tree, "format") | 
|  | res.checkExit(true) | 
|  | } | 
|  | // default => print formatted result | 
|  | { | 
|  | res := gopls(t, tree, "format", "a.go") | 
|  | res.checkExit(true) | 
|  | if res.stdout != want { | 
|  | t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) | 
|  | } | 
|  | } | 
|  | // start/end position not supported (unless equal to start/end of file) | 
|  | { | 
|  | res := gopls(t, tree, "format", "a.go:1-2") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("only full file formatting supported") | 
|  | } | 
|  | // -list: show only file names | 
|  | { | 
|  | res := gopls(t, tree, "format", "-list", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go") | 
|  | } | 
|  | // -diff prints a unified diff | 
|  | { | 
|  | res := gopls(t, tree, "format", "-diff", "a.go") | 
|  | res.checkExit(true) | 
|  | // We omit the filenames as they vary by OS. | 
|  | want := ` | 
|  | -package a ;  func f ( ) { } | 
|  | +package a | 
|  | + | 
|  | +func f() {} | 
|  | ` | 
|  | res.checkStdout(regexp.QuoteMeta(want)) | 
|  | } | 
|  | // -write updates the file | 
|  | { | 
|  | res := gopls(t, tree, "format", "-write", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("^$") // empty | 
|  | checkContent(t, filepath.Join(tree, "a.go"), want) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestHighlight tests the 'highlight' subcommand (highlight.go). | 
|  | func TestHighlight(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | func f() { | 
|  | fmt.Println() | 
|  | fmt.Println() | 
|  | } | 
|  | `) | 
|  |  | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "highlight") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // all occurrences of Println | 
|  | { | 
|  | res := gopls(t, tree, "highlight", "a.go:4:7") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go:4:6-13") | 
|  | res.checkStdout("a.go:5:6-13") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestImplementations tests the 'implementation' subcommand (implementation.go). | 
|  | func TestImplementations(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | // types.CheckExpr, now used in the rangeint modernizer, had a | 
|  | // data race (#71817) that was fixed in go1.25 and backported | 
|  | // to go1.24 but not to go1.23. Although in principle it could | 
|  | // affect a lot of tests, it (weirdly) only seems to show up | 
|  | // in this one (#72082). Rather than backport again, we | 
|  | // suppress this test. | 
|  | testenv.NeedsGo1Point(t, 24) | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | type T int | 
|  | func (T) String() string { return "" } | 
|  | `) | 
|  |  | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "implementation") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // T.String | 
|  | { | 
|  | res := gopls(t, tree, "implementation", "a.go:4:10") | 
|  | res.checkExit(true) | 
|  | // TODO(adonovan): extract and check the content of the reported ranges? | 
|  | // We use regexp '.' as an OS-agnostic path separator. | 
|  | res.checkStdout("fmt.print.go:")     // fmt.Stringer.String | 
|  | res.checkStdout("runtime.error.go:") // runtime.stringer.String | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestImports tests the 'imports' subcommand (imports.go). | 
|  | func TestImports(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- a.go -- | 
|  | package a | 
|  | func _() { | 
|  | fmt.Println() | 
|  | } | 
|  | `) | 
|  |  | 
|  | want := ` | 
|  | package a | 
|  |  | 
|  | import "fmt" | 
|  | func _() { | 
|  | fmt.Println() | 
|  | } | 
|  | `[1:] | 
|  |  | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "imports") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // default: print with imports | 
|  | { | 
|  | res := gopls(t, tree, "imports", "a.go") | 
|  | res.checkExit(true) | 
|  | if res.stdout != want { | 
|  | t.Errorf("imports: got <<%s>>, want <<%s>>", res.stdout, want) | 
|  | } | 
|  | } | 
|  | // -diff: show a unified diff | 
|  | { | 
|  | res := gopls(t, tree, "imports", "-diff", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) | 
|  | } | 
|  | // -write: update file | 
|  | { | 
|  | res := gopls(t, tree, "imports", "-write", "a.go") | 
|  | res.checkExit(true) | 
|  | checkContent(t, filepath.Join(tree, "a.go"), want) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestLinks tests the 'links' subcommand (links.go). | 
|  | func TestLinks(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- a.go -- | 
|  | // Link in package doc: https://pkg.go.dev/ | 
|  | package a | 
|  |  | 
|  | // Link in internal comment: https://go.dev/cl | 
|  |  | 
|  | // Doc comment link: https://blog.go.dev/ | 
|  | func f() {} | 
|  | `) | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "links") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // success | 
|  | { | 
|  | res := gopls(t, tree, "links", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("https://go.dev/cl") | 
|  | res.checkStdout("https://pkg.go.dev") | 
|  | res.checkStdout("https://blog.go.dev/") | 
|  | } | 
|  | // -json | 
|  | { | 
|  | res := gopls(t, tree, "links", "-json", "a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("https://pkg.go.dev") | 
|  | res.checkStdout("https://go.dev/cl") | 
|  | res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 | 
|  | var links []protocol.DocumentLink | 
|  | if res.toJSON(&links) { | 
|  | // Check just one of the three locations. | 
|  | if got, want := fmt.Sprint(links[2].Range), "5:21-5:41"; got != want { | 
|  | t.Errorf("wrong link location: got %v, want %v", got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestReferences tests the 'references' subcommand (references.go). | 
|  | func TestReferences(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | func f() { | 
|  | fmt.Println() | 
|  | } | 
|  |  | 
|  | -- b.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | func g() { | 
|  | fmt.Println() | 
|  | } | 
|  | `) | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "references") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // fmt.Println | 
|  | { | 
|  | res := gopls(t, tree, "references", "a.go:4:10") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go:4:6-13") | 
|  | res.checkStdout("b.go:4:6-13") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestSignature tests the 'signature' subcommand (signature.go). | 
|  | func TestSignature(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | import "fmt" | 
|  | func f() { | 
|  | fmt.Println(123) | 
|  | } | 
|  | `) | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "signature") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // at 123 inside fmt.Println() call | 
|  | { | 
|  | res := gopls(t, tree, "signature", "a.go:4:15") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("Println\\(a ...") | 
|  | res.checkStdout("Println formats using the default formats...") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestPrepareRename tests the 'prepare_rename' subcommand (prepare_rename.go). | 
|  | func TestPrepareRename(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func oldname() {} | 
|  | `) | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "prepare_rename") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // in 'package' keyword | 
|  | { | 
|  | res := gopls(t, tree, "prepare_rename", "a.go:1:3") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("request is not valid at the given position") | 
|  | } | 
|  | // in 'package' identifier (not supported by client) | 
|  | { | 
|  | res := gopls(t, tree, "prepare_rename", "a.go:1:9") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("can't rename package") | 
|  | } | 
|  | // in func oldname | 
|  | { | 
|  | res := gopls(t, tree, "prepare_rename", "a.go:2:9") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go:2:6-13") // all of "oldname" | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestRename tests the 'rename' subcommand (rename.go). | 
|  | func TestRename(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func oldname() {} | 
|  | `) | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "rename") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 2 arguments") | 
|  | } | 
|  | // missing newname | 
|  | { | 
|  | res := gopls(t, tree, "rename", "a.go:1:3") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 2 arguments") | 
|  | } | 
|  | // in 'package' keyword | 
|  | { | 
|  | res := gopls(t, tree, "rename", "a.go:1:3", "newname") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("no identifier found") | 
|  | } | 
|  | // in 'package' identifier | 
|  | { | 
|  | res := gopls(t, tree, "rename", "a.go:1:9", "newname") | 
|  | res.checkExit(false) | 
|  | res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) | 
|  | } | 
|  | // success, func oldname (and -diff) | 
|  | { | 
|  | res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) | 
|  | res.checkStdout(regexp.QuoteMeta("+func newname() {}")) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestSymbols tests the 'symbols' subcommand (symbols.go). | 
|  | func TestSymbols(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func f() | 
|  | var v int | 
|  | const c = 0 | 
|  | `) | 
|  | // no files | 
|  | { | 
|  | res := gopls(t, tree, "symbols") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // success | 
|  | { | 
|  | res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) | 
|  | res.checkExit(true) | 
|  | res.checkStdout("f Function 2:6-2:7") | 
|  | res.checkStdout("v Variable 3:5-3:6") | 
|  | res.checkStdout("c Constant 4:7-4:8") | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestSemtok tests the 'semtok' subcommand (semantictokens.go). | 
|  | func TestSemtok(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func f() | 
|  | var v int | 
|  | const c = 0 | 
|  | `) | 
|  | // no files | 
|  | { | 
|  | res := gopls(t, tree, "semtok") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expected one file name") | 
|  | } | 
|  | // success | 
|  | { | 
|  | res := gopls(t, tree, "semtok", "a.go") | 
|  | res.checkExit(true) | 
|  | got := res.stdout | 
|  | want := ` | 
|  | /*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a | 
|  | /*⇒4,keyword,[]*/func /*⇒1,function,[definition signature]*/f() | 
|  | /*⇒3,keyword,[]*/var /*⇒1,variable,[definition number]*/v /*⇒3,type,[defaultLibrary number]*/int | 
|  | /*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly number]*/c = /*⇒1,number,[]*/0 | 
|  | `[1:] | 
|  | if got != want { | 
|  | t.Errorf("semtok: got <<%s>>, want <<%s>>", got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestStats(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | -- b/b.go -- | 
|  | package b | 
|  | -- testdata/foo.go -- | 
|  | package foo | 
|  | `) | 
|  |  | 
|  | // Trigger a bug report with a distinctive string | 
|  | // and check that it was durably recorded. | 
|  | oops := fmt.Sprintf("oops-%d", rand.Int()) | 
|  | { | 
|  | env := []string{"TEST_GOPLS_BUG=" + oops} | 
|  | res := goplsWithEnv(t, tree, env, "bug") | 
|  | res.checkExit(true) | 
|  | } | 
|  |  | 
|  | res := gopls(t, tree, "stats") | 
|  | res.checkExit(true) | 
|  |  | 
|  | var stats cmd.GoplsStats | 
|  | if err := json.Unmarshal([]byte(res.stdout), &stats); err != nil { | 
|  | t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) | 
|  | } | 
|  |  | 
|  | // a few sanity checks | 
|  | checks := []struct { | 
|  | field string | 
|  | got   int | 
|  | want  int | 
|  | }{ | 
|  | { | 
|  | "WorkspaceStats.Views[0].WorkspaceModules", | 
|  | stats.WorkspaceStats.Views[0].WorkspacePackages.Modules, | 
|  | 1, | 
|  | }, | 
|  | { | 
|  | "WorkspaceStats.Views[0].WorkspacePackages", | 
|  | stats.WorkspaceStats.Views[0].WorkspacePackages.Packages, | 
|  | 2, | 
|  | }, | 
|  | {"DirStats.Files", stats.DirStats.Files, 4}, | 
|  | {"DirStats.GoFiles", stats.DirStats.GoFiles, 2}, | 
|  | {"DirStats.ModFiles", stats.DirStats.ModFiles, 1}, | 
|  | {"DirStats.TestdataFiles", stats.DirStats.TestdataFiles, 1}, | 
|  | } | 
|  | for _, check := range checks { | 
|  | if check.got != check.want { | 
|  | t.Errorf("stats.%s = %d, want %d", check.field, check.got, check.want) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check that we got a BugReport with the expected message. | 
|  | { | 
|  | got := fmt.Sprint(stats.BugReports) | 
|  | wants := []string{ | 
|  | "cmd/info.go", // File containing call to bug.Report | 
|  | oops,          // Description | 
|  | } | 
|  | for _, want := range wants { | 
|  | if !strings.Contains(got, want) { | 
|  | t.Errorf("BugReports does not contain %q. Got:<<%s>>", want, got) | 
|  | break | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check that -anon suppresses fields containing user information. | 
|  | { | 
|  | res2 := gopls(t, tree, "stats", "-anon") | 
|  | res2.checkExit(true) | 
|  |  | 
|  | var stats2 cmd.GoplsStats | 
|  | if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { | 
|  | t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) | 
|  | } | 
|  | if got := len(stats2.BugReports); got > 0 { | 
|  | t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) | 
|  | } | 
|  | var stats2AsMap map[string]any | 
|  | if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { | 
|  | t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) | 
|  | } | 
|  | // GOPACKAGESDRIVER is user information, but is ok to print zero value. | 
|  | if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; ok && v != "" { | 
|  | t.Errorf(`Got GOPACKAGESDRIVER=(%v, %v); want ("", true(found))`, v, ok) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check that -anon suppresses fields containing non-zero user information. | 
|  | { | 
|  | res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") | 
|  | res3.checkExit(true) | 
|  |  | 
|  | var statsAsMap3 map[string]any | 
|  | if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { | 
|  | t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) | 
|  | } | 
|  | // GOPACKAGESDRIVER is user information, want non-empty value to be omitted. | 
|  | if v, ok := statsAsMap3["GOPACKAGESDRIVER"]; ok { | 
|  | t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", false(not found))`, v, ok) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestCodeAction tests the 'codeaction' subcommand (codeaction.go). | 
|  | func TestCodeAction(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a/a.go -- | 
|  | package a | 
|  | type T int | 
|  | func f() (int, string) { return } | 
|  |  | 
|  | -- a/b.go -- | 
|  | package a | 
|  | import "io" | 
|  | var _ io.Reader = C{} | 
|  | type C struct{} | 
|  | `) | 
|  |  | 
|  | // no arguments | 
|  | { | 
|  | res := gopls(t, tree, "codeaction") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects at least 1 argument") | 
|  | } | 
|  | // list code actions in file | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "a/a.go") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`edit	"Fill in return values" \[quickfix\]`) | 
|  | res.checkStdout(`command	"Browse documentation for package a" \[source.doc\]`) | 
|  | } | 
|  | // list code actions in file, filtering by title | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "-title=Browse.*doc", "a/a.go") | 
|  | res.checkExit(true) | 
|  | got := res.stdout | 
|  | want := `command	"Browse documentation for package a" [source.doc]` + | 
|  | "\n" + | 
|  | `command	"Browse gopls feature documentation" [gopls.doc.features]` + | 
|  | "\n" | 
|  | if got != want { | 
|  | t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) | 
|  | } | 
|  | } | 
|  | // list code actions in file, filtering (hierarchically) by kind | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "-kind=source", "a/a.go") | 
|  | res.checkExit(true) | 
|  | got := res.stdout | 
|  | want := `command	"Browse documentation for package a" [source.doc]` + | 
|  | "\n" + | 
|  | `command	"Split package \"a\"" [source.splitPackage]` + | 
|  | "\n" + | 
|  | `command	"Show compiler optimization details for \"a\"" [source.toggleCompilerOptDetails]` + | 
|  | "\n" | 
|  | if got != want { | 
|  | t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) | 
|  | } | 
|  | } | 
|  | // list code actions at position (of io.Reader) | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "a/b.go:#31") | 
|  | res.checkExit(true) | 
|  | res.checkStdout(`command	"Browse documentation for type io.Reader" \[source.doc]`) | 
|  | } | 
|  | // list quick fixes at position (of type T) | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "-kind=quickfix", "a/a.go:#15") | 
|  | res.checkExit(true) | 
|  | got := res.stdout | 
|  | want := `edit	"Fill in return values" [quickfix]` + "\n" | 
|  | if got != want { | 
|  | t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) | 
|  | } | 
|  | } | 
|  | // success, with explicit CodeAction kind and diagnostics span. | 
|  | { | 
|  | res := gopls(t, tree, "codeaction", "-kind=quickfix", "-exec", "a/b.go:#40") | 
|  | res.checkExit(true) | 
|  | got := res.stdout | 
|  | want := ` | 
|  | package a | 
|  |  | 
|  | import "io" | 
|  |  | 
|  | var _ io.Reader = C{} | 
|  |  | 
|  | type C struct{} | 
|  |  | 
|  | // Read implements io.Reader. | 
|  | func (c C) Read(p []byte) (n int, err error) { | 
|  | panic("unimplemented") | 
|  | } | 
|  | `[1:] | 
|  | if got != want { | 
|  | t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (workspace_symbol.go). | 
|  | func TestWorkspaceSymbol(t *testing.T) { | 
|  | t.Parallel() | 
|  |  | 
|  | tree := writeTree(t, ` | 
|  | -- go.mod -- | 
|  | module example.com | 
|  | go 1.18 | 
|  |  | 
|  | -- a.go -- | 
|  | package a | 
|  | func someFunctionName() | 
|  | `) | 
|  | // no files | 
|  | { | 
|  | res := gopls(t, tree, "workspace_symbol") | 
|  | res.checkExit(false) | 
|  | res.checkStderr("expects 1 argument") | 
|  | } | 
|  | // success | 
|  | { | 
|  | res := gopls(t, tree, "workspace_symbol", "meFun") | 
|  | res.checkExit(true) | 
|  | res.checkStdout("a.go:2:6-22 someFunctionName Function") | 
|  | } | 
|  | } | 
|  |  | 
|  | // -- test framework -- | 
|  |  | 
|  | func TestMain(m *testing.M) { | 
|  | switch os.Getenv("ENTRYPOINT") { | 
|  | case "goplsMain": | 
|  | goplsMain() | 
|  | default: | 
|  | os.Exit(m.Run()) | 
|  | } | 
|  | } | 
|  |  | 
|  | // This function is a stand-in for gopls.main in ../../../../main.go. | 
|  | func goplsMain() { | 
|  | // Panic on bugs (unlike the production gopls command), | 
|  | // except in tests that inject calls to bug.Report. | 
|  | if os.Getenv("TEST_GOPLS_BUG") == "" { | 
|  | bug.PanicOnBugs = true | 
|  | } | 
|  |  | 
|  | if v := os.Getenv("TEST_GOPLS_VERSION"); v != "" { | 
|  | version.VersionOverride = v | 
|  | } | 
|  |  | 
|  | tool.Main(context.Background(), cmd.New(), os.Args[1:]) | 
|  | } | 
|  |  | 
|  | // writeTree extracts a txtar archive into a new directory and returns its path. | 
|  | func writeTree(t *testing.T, archive string) string { | 
|  | root := t.TempDir() | 
|  |  | 
|  | // This unfortunate step is required because gopls output | 
|  | // expands symbolic links in its input file names (arguably it | 
|  | // should not), and on macOS the temp dir is in /var -> private/var. | 
|  | root, err := filepath.EvalSymlinks(root) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | for _, f := range txtar.Parse([]byte(archive)).Files { | 
|  | filename := filepath.Join(root, f.Name) | 
|  | if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := os.WriteFile(filename, f.Data, 0666); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | } | 
|  | return root | 
|  | } | 
|  |  | 
|  | // gopls executes gopls in a child process. | 
|  | func gopls(t *testing.T, dir string, args ...string) *result { | 
|  | return goplsWithEnv(t, dir, nil, args...) | 
|  | } | 
|  |  | 
|  | func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *result { | 
|  | testenv.NeedsTool(t, "go") | 
|  |  | 
|  | // Catch inadvertent use of dir=".", which would make | 
|  | // the ReplaceAll below unpredictable. | 
|  | if !filepath.IsAbs(dir) { | 
|  | t.Fatalf("dir is not absolute: %s", dir) | 
|  | } | 
|  |  | 
|  | goplsCmd := exec.Command(os.Args[0], args...) | 
|  | goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") | 
|  | goplsCmd.Env = append(goplsCmd.Env, "GOPACKAGESDRIVER=off") | 
|  | goplsCmd.Env = append(goplsCmd.Env, env...) | 
|  | goplsCmd.Dir = dir | 
|  | goplsCmd.Stdout = new(bytes.Buffer) | 
|  | goplsCmd.Stderr = new(bytes.Buffer) | 
|  |  | 
|  | cmdErr := goplsCmd.Run() | 
|  |  | 
|  | stdout := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stdout), dir, ".") | 
|  | stderr := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stderr), dir, ".") | 
|  | exitcode := 0 | 
|  | if cmdErr != nil { | 
|  | if exitErr, ok := cmdErr.(*exec.ExitError); ok { | 
|  | exitcode = exitErr.ExitCode() | 
|  | } else { | 
|  | stderr = cmdErr.Error() // (execve failure) | 
|  | exitcode = -1 | 
|  | } | 
|  | } | 
|  | res := &result{ | 
|  | t:        t, | 
|  | command:  "gopls " + strings.Join(args, " "), | 
|  | exitcode: exitcode, | 
|  | stdout:   stdout, | 
|  | stderr:   stderr, | 
|  | } | 
|  | if false { | 
|  | t.Log(res) | 
|  | } | 
|  | return res | 
|  | } | 
|  |  | 
|  | // A result holds the result of a gopls invocation, and provides assertion helpers. | 
|  | type result struct { | 
|  | t              *testing.T | 
|  | command        string | 
|  | exitcode       int | 
|  | stdout, stderr string | 
|  | } | 
|  |  | 
|  | func (res *result) String() string { | 
|  | return fmt.Sprintf("%s: exit=%d stdout=<<%s>> stderr=<<%s>>", | 
|  | res.command, res.exitcode, res.stdout, res.stderr) | 
|  | } | 
|  |  | 
|  | // checkExit asserts that gopls returned the expected exit code. | 
|  | func (res *result) checkExit(success bool) { | 
|  | res.t.Helper() | 
|  | if (res.exitcode == 0) != success { | 
|  | res.t.Errorf("%s: exited with code %d, want success: %t (%s)", | 
|  | res.command, res.exitcode, success, res) | 
|  | } | 
|  | } | 
|  |  | 
|  | // checkStdout asserts that the gopls standard output matches the pattern. | 
|  | func (res *result) checkStdout(pattern string) { | 
|  | res.t.Helper() | 
|  | res.checkOutput(pattern, "stdout", res.stdout) | 
|  | } | 
|  |  | 
|  | // checkStderr asserts that the gopls standard error matches the pattern. | 
|  | func (res *result) checkStderr(pattern string) { | 
|  | res.t.Helper() | 
|  | res.checkOutput(pattern, "stderr", res.stderr) | 
|  | } | 
|  |  | 
|  | func (res *result) checkOutput(pattern, name, content string) { | 
|  | res.t.Helper() | 
|  | if match, err := regexp.MatchString(pattern, content); err != nil { | 
|  | res.t.Errorf("invalid regexp: %v", err) | 
|  | } else if !match { | 
|  | res.t.Errorf("%s: %s does not match [%s]; got <<%s>>", | 
|  | res.command, name, pattern, content) | 
|  | } | 
|  | } | 
|  |  | 
|  | // toJSON decodes res.stdout as JSON into to *ptr and reports its success. | 
|  | func (res *result) toJSON(ptr any) bool { | 
|  | if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { | 
|  | res.t.Errorf("invalid JSON %v", err) | 
|  | return false | 
|  | } | 
|  | return true | 
|  | } | 
|  |  | 
|  | // checkContent checks that the contents of the file are as expected. | 
|  | func checkContent(t *testing.T, filename, want string) { | 
|  | data, err := os.ReadFile(filename) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | return | 
|  | } | 
|  | if got := string(data); got != want { | 
|  | t.Errorf("content of %s is <<%s>>, want <<%s>>", filename, got, want) | 
|  | } | 
|  | } |