From 6816ec868d646bcd22fe257afcb315cab3bb2a58 Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Wed, 18 Sep 2019 23:51:24 -0400 Subject: [PATCH] gopls: refactor the cmd tests This allows them to be run from the gopls module as well to test the code with the hooks installed. Change-Id: I3079a04ffe3bd221ccc2523e746cbed384e05e2f Reviewed-on: https://go-review.googlesource.com/c/tools/+/196321 Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- gopls/test/gopls_test.go | 38 ++++ internal/lsp/cmd/cmd_test.go | 167 +++--------------- .../lsp/cmd/{check_test.go => test/check.go} | 4 +- internal/lsp/cmd/test/cmdtest.go | 161 +++++++++++++++++ .../definition.go} | 34 +--- .../cmd/{format_test.go => test/format.go} | 4 +- .../cmd/{rename_test.go => test/rename.go} | 4 +- 7 files changed, 235 insertions(+), 177 deletions(-) create mode 100644 gopls/test/gopls_test.go rename internal/lsp/cmd/{check_test.go => test/check.go} (97%) create mode 100644 internal/lsp/cmd/test/cmdtest.go rename internal/lsp/cmd/{definition_test.go => test/definition.go} (64%) rename internal/lsp/cmd/{format_test.go => test/format.go} (97%) rename internal/lsp/cmd/{rename_test.go => test/rename.go} (96%) diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go new file mode 100644 index 0000000000..5245579277 --- /dev/null +++ b/gopls/test/gopls_test.go @@ -0,0 +1,38 @@ +// Copyright 2019 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 gopls_test + +import ( + "context" + "os" + "testing" + + "golang.org/x/tools/go/packages/packagestest" + "golang.org/x/tools/gopls/internal/hooks" + cmdtest "golang.org/x/tools/internal/lsp/cmd/test" + "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/testenv" +) + +func TestMain(m *testing.M) { + testenv.ExitIfSmallMachine() + os.Exit(m.Run()) +} + +func TestCommandLine(t *testing.T) { + packagestest.TestAll(t, testCommandLine) +} + +func testCommandLine(t *testing.T, exporter packagestest.Exporter) { + const testdata = "../../internal/lsp/testdata" + if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() { + t.Skip("testdata directory not present") + } + ctx := context.Background() + hooks.Install(ctx) + data := tests.Load(t, exporter, testdata) + defer data.Exported.Cleanup() + tests.Run(t, cmdtest.NewRunner(exporter, data, tests.Context(t)), data) +} diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go index 4dad9dbd69..1440459779 100644 --- a/internal/lsp/cmd/cmd_test.go +++ b/internal/lsp/cmd/cmd_test.go @@ -5,18 +5,19 @@ package cmd_test import ( - "bytes" - "context" - "io/ioutil" + "fmt" "os" "path/filepath" - "strconv" - "strings" + "regexp" + "runtime" "testing" "golang.org/x/tools/go/packages/packagestest" + "golang.org/x/tools/internal/lsp/cmd" + cmdtest "golang.org/x/tools/internal/lsp/cmd/test" "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/tool" ) func TestMain(m *testing.M) { @@ -24,12 +25,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -type runner struct { - exporter packagestest.Exporter - data *tests.Data - ctx context.Context -} - func TestCommandLine(t *testing.T) { packagestest.TestAll(t, testCommandLine) } @@ -37,138 +32,32 @@ func TestCommandLine(t *testing.T) { func testCommandLine(t *testing.T, exporter packagestest.Exporter) { data := tests.Load(t, exporter, "../testdata") defer data.Exported.Cleanup() + tests.Run(t, cmdtest.NewRunner(exporter, data, tests.Context(t)), data) +} - r := &runner{ - exporter: exporter, - data: data, - ctx: tests.Context(t), +func TestDefinitionHelpExample(t *testing.T) { + // TODO: https://golang.org/issue/32794. + t.Skip() + if runtime.GOOS == "android" { + t.Skip("not all source files are available on android") } - tests.Run(t, r, data) -} - -func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { - //TODO: add command line folding range tests when it works -} - -func (r *runner) Highlight(t *testing.T, data tests.Highlights) { - //TODO: add command line highlight tests when it works -} - -func (r *runner) Reference(t *testing.T, data tests.References) { - //TODO: add command line references tests when it works -} - -func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) { - //TODO: add command line prepare rename tests when it works -} - -func (r *runner) Symbol(t *testing.T, data tests.Symbols) { - //TODO: add command line symbol tests when it works -} - -func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { - //TODO: add command line signature tests when it works -} - -func (r *runner) Link(t *testing.T, data tests.Links) { - //TODO: add command line link tests when it works -} - -func (r *runner) Import(t *testing.T, data tests.Imports) { - //TODO: add command line imports tests when it works -} - -func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) { - //TODO: add suggested fix tests when it works -} - -func captureStdOut(t testing.TB, f func()) string { - r, out, err := os.Pipe() + dir, err := os.Getwd() if err != nil { - t.Fatal(err) + t.Errorf("could not get wd: %v", err) + return } - old := os.Stdout - defer func() { - os.Stdout = old - out.Close() - r.Close() - }() - os.Stdout = out - f() - out.Close() - data, err := ioutil.ReadAll(r) - if err != nil { - t.Fatal(err) - } - return string(data) -} - -// normalizePaths replaces all paths present in s with just the fragment portion -// this is used to make golden files not depend on the temporary paths of the files -func normalizePaths(data *tests.Data, s string) string { - type entry struct { - path string - index int - fragment string - } - match := make([]entry, 0, len(data.Exported.Modules)) - // collect the initial state of all the matchers - for _, m := range data.Exported.Modules { - for fragment := range m.Files { - filename := data.Exported.File(m.Name, fragment) - index := strings.Index(s, filename) - if index >= 0 { - match = append(match, entry{filename, index, fragment}) - } - if slash := filepath.ToSlash(filename); slash != filename { - index := strings.Index(s, slash) - if index >= 0 { - match = append(match, entry{slash, index, fragment}) - } - } - quoted := strconv.Quote(filename) - if escaped := quoted[1 : len(quoted)-1]; escaped != filename { - index := strings.Index(s, escaped) - if index >= 0 { - match = append(match, entry{escaped, index, fragment}) - } - } - } - } - // result should be the same or shorter than the input - buf := bytes.NewBuffer(make([]byte, 0, len(s))) - last := 0 - for { - // find the nearest path match to the start of the buffer - next := -1 - nearest := len(s) - for i, c := range match { - if c.index >= 0 && nearest > c.index { - nearest = c.index - next = i - } - } - // if there are no matches, we copy the rest of the string and are done - if next < 0 { - buf.WriteString(s[last:]) - return buf.String() - } - // we have a match - n := &match[next] - // copy up to the start of the match - buf.WriteString(s[last:n.index]) - // skip over the filename - last = n.index + len(n.path) - // add in the fragment instead - buf.WriteString(n.fragment) - // see what the next match for this path is - n.index = strings.Index(s[last:], n.path) - if n.index >= 0 { - n.index += last + thisFile := filepath.Join(dir, "definition.go") + baseArgs := []string{"query", "definition"} + expect := regexp.MustCompile(`(?s)^[\w/\\:_-]+flag[/\\]flag.go:\d+:\d+-\d+: defined here as FlagSet struct {.*}$`) + for _, query := range []string{ + fmt.Sprintf("%v:%v:%v", thisFile, cmd.ExampleLine, cmd.ExampleColumn), + fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} { + args := append(baseArgs, query) + got := cmdtest.CaptureStdOut(t, func() { + _ = tool.Run(tests.Context(t), cmd.New("gopls-test", "", nil), args) + }) + if !expect.MatchString(got) { + t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got) } } } diff --git a/internal/lsp/cmd/check_test.go b/internal/lsp/cmd/test/check.go similarity index 97% rename from internal/lsp/cmd/check_test.go rename to internal/lsp/cmd/test/check.go index 5752b873d8..78ea9d508a 100644 --- a/internal/lsp/cmd/check_test.go +++ b/internal/lsp/cmd/test/check.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cmd_test +package cmdtest import ( "fmt" @@ -24,7 +24,7 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) { fname := uri.Filename() args := []string{"-remote=internal", "check", fname} app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env) - out := captureStdOut(t, func() { + out := CaptureStdOut(t, func() { _ = tool.Run(r.ctx, app, args) }) // parse got into a collection of reports diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go new file mode 100644 index 0000000000..33523239a1 --- /dev/null +++ b/internal/lsp/cmd/test/cmdtest.go @@ -0,0 +1,161 @@ +// Copyright 2019 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 cmdtest + +import ( + "bytes" + "context" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "golang.org/x/tools/go/packages/packagestest" + "golang.org/x/tools/internal/lsp/tests" +) + +type runner struct { + exporter packagestest.Exporter + data *tests.Data + ctx context.Context +} + +func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context) tests.Tests { + return &runner{ + exporter: exporter, + data: data, + ctx: ctx, + } +} + +func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { + //TODO: add command line completions tests when it works +} + +func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { + //TODO: add command line folding range tests when it works +} + +func (r *runner) Highlight(t *testing.T, data tests.Highlights) { + //TODO: add command line highlight tests when it works +} + +func (r *runner) Reference(t *testing.T, data tests.References) { + //TODO: add command line references tests when it works +} + +func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) { + //TODO: add command line prepare rename tests when it works +} + +func (r *runner) Symbol(t *testing.T, data tests.Symbols) { + //TODO: add command line symbol tests when it works +} + +func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { + //TODO: add command line signature tests when it works +} + +func (r *runner) Link(t *testing.T, data tests.Links) { + //TODO: add command line link tests when it works +} + +func (r *runner) Import(t *testing.T, data tests.Imports) { + //TODO: add command line imports tests when it works +} + +func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) { + //TODO: add suggested fix tests when it works +} + +func CaptureStdOut(t testing.TB, f func()) string { + r, out, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + old := os.Stdout + defer func() { + os.Stdout = old + out.Close() + r.Close() + }() + os.Stdout = out + f() + out.Close() + data, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + return string(data) +} + +// normalizePaths replaces all paths present in s with just the fragment portion +// this is used to make golden files not depend on the temporary paths of the files +func normalizePaths(data *tests.Data, s string) string { + type entry struct { + path string + index int + fragment string + } + match := make([]entry, 0, len(data.Exported.Modules)) + // collect the initial state of all the matchers + for _, m := range data.Exported.Modules { + for fragment := range m.Files { + filename := data.Exported.File(m.Name, fragment) + index := strings.Index(s, filename) + if index >= 0 { + match = append(match, entry{filename, index, fragment}) + } + if slash := filepath.ToSlash(filename); slash != filename { + index := strings.Index(s, slash) + if index >= 0 { + match = append(match, entry{slash, index, fragment}) + } + } + quoted := strconv.Quote(filename) + if escaped := quoted[1 : len(quoted)-1]; escaped != filename { + index := strings.Index(s, escaped) + if index >= 0 { + match = append(match, entry{escaped, index, fragment}) + } + } + } + } + // result should be the same or shorter than the input + buf := bytes.NewBuffer(make([]byte, 0, len(s))) + last := 0 + for { + // find the nearest path match to the start of the buffer + next := -1 + nearest := len(s) + for i, c := range match { + if c.index >= 0 && nearest > c.index { + nearest = c.index + next = i + } + } + // if there are no matches, we copy the rest of the string and are done + if next < 0 { + buf.WriteString(s[last:]) + return buf.String() + } + // we have a match + n := &match[next] + // copy up to the start of the match + buf.WriteString(s[last:n.index]) + // skip over the filename + last = n.index + len(n.path) + // add in the fragment instead + buf.WriteString(n.fragment) + // see what the next match for this path is + n.index = strings.Index(s[last:], n.path) + if n.index >= 0 { + n.index += last + } + } +} diff --git a/internal/lsp/cmd/definition_test.go b/internal/lsp/cmd/test/definition.go similarity index 64% rename from internal/lsp/cmd/definition_test.go rename to internal/lsp/cmd/test/definition.go index 5737fc0776..158d943188 100644 --- a/internal/lsp/cmd/definition_test.go +++ b/internal/lsp/cmd/test/definition.go @@ -2,13 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cmd_test +package cmdtest import ( "fmt" - "os" - "path/filepath" - "regexp" "runtime" "strings" "testing" @@ -36,33 +33,6 @@ var godefModes = []godefMode{ jsonGoDef, } -func TestDefinitionHelpExample(t *testing.T) { - // TODO: https://golang.org/issue/32794. - t.Skip() - if runtime.GOOS == "android" { - t.Skip("not all source files are available on android") - } - dir, err := os.Getwd() - if err != nil { - t.Errorf("could not get wd: %v", err) - return - } - thisFile := filepath.Join(dir, "definition.go") - baseArgs := []string{"query", "definition"} - expect := regexp.MustCompile(`(?s)^[\w/\\:_-]+flag[/\\]flag.go:\d+:\d+-\d+: defined here as FlagSet struct {.*}$`) - for _, query := range []string{ - fmt.Sprintf("%v:%v:%v", thisFile, cmd.ExampleLine, cmd.ExampleColumn), - fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} { - args := append(baseArgs, query) - got := captureStdOut(t, func() { - _ = tool.Run(tests.Context(t), cmd.New("gopls-test", "", nil), args) - }) - if !expect.MatchString(got) { - t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got) - } - } -} - func (r *runner) Definition(t *testing.T, data tests.Definitions) { // TODO: https://golang.org/issue/32794. t.Skip() @@ -82,7 +52,7 @@ func (r *runner) Definition(t *testing.T, data tests.Definitions) { args = append(args, "definition") uri := d.Src.URI() args = append(args, fmt.Sprint(d.Src)) - got := captureStdOut(t, func() { + got := CaptureStdOut(t, func() { app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env) _ = tool.Run(r.ctx, app, args) }) diff --git a/internal/lsp/cmd/format_test.go b/internal/lsp/cmd/test/format.go similarity index 97% rename from internal/lsp/cmd/format_test.go rename to internal/lsp/cmd/test/format.go index 878246189d..11e34198c3 100644 --- a/internal/lsp/cmd/format_test.go +++ b/internal/lsp/cmd/test/format.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cmd_test +package cmdtest import ( "os/exec" @@ -38,7 +38,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) { continue } app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Config.Env) - got := captureStdOut(t, func() { + got := CaptureStdOut(t, func() { _ = tool.Run(r.ctx, app, append([]string{"-remote=internal", "format"}, args...)) }) got = normalizePaths(r.data, got) diff --git a/internal/lsp/cmd/rename_test.go b/internal/lsp/cmd/test/rename.go similarity index 96% rename from internal/lsp/cmd/rename_test.go rename to internal/lsp/cmd/test/rename.go index 73d3296c6e..b7fd272938 100644 --- a/internal/lsp/cmd/rename_test.go +++ b/internal/lsp/cmd/test/rename.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cmd_test +package cmdtest import ( "fmt" @@ -40,7 +40,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) { } args = append(args, loc, tag) var err error - got := captureStdOut(t, func() { + got := CaptureStdOut(t, func() { err = tool.Run(r.ctx, app, args) }) if err != nil {