blob: 39698f37334881105347e1e8e337a0632ed35bd0 [file] [log] [blame]
// 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
`)
// 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`)
}
}
// 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.test\]`)
res.checkStdout(`a_test.go:4: "run test" \[gopls.test\]`)
}
// 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, "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()
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]*/f()
/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary number]*/int
/*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/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]interface{}
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.go --
package a
type T int
func f() (int, string) { return }
-- 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.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.go")
res.checkExit(true)
got := res.stdout
want := `command "Browse gopls feature documentation" [gopls.doc.features]` +
"\n" +
`command "Browse documentation for package a" [source.doc]` +
"\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.go")
res.checkExit(true)
got := res.stdout
want := `command "Browse documentation for package a" [source.doc]` +
"\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", "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.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", "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 interface{}) 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)
}
}