mirror of
https://github.com/golang/go
synced 2024-11-05 18:36:10 -07:00
5fddd300b6
To prevent misleading errors from outstanding go command invocations at test completion, properly shutdown the LSP connection before cleaning up exported files. Change-Id: I9ad175060fefc5b914e544c5f58b9b6658405edc Reviewed-on: https://go-review.googlesource.com/c/tools/+/238546 Reviewed-by: Rebecca Stambler <rstambler@golang.org> Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
259 lines
7.4 KiB
Go
259 lines
7.4 KiB
Go
// 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"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"golang.org/x/tools/go/packages/packagestest"
|
|
"golang.org/x/tools/internal/jsonrpc2/servertest"
|
|
"golang.org/x/tools/internal/lsp/cache"
|
|
"golang.org/x/tools/internal/lsp/cmd"
|
|
"golang.org/x/tools/internal/lsp/debug"
|
|
"golang.org/x/tools/internal/lsp/lsprpc"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
"golang.org/x/tools/internal/lsp/tests"
|
|
"golang.org/x/tools/internal/span"
|
|
"golang.org/x/tools/internal/tool"
|
|
)
|
|
|
|
type runner struct {
|
|
exporter packagestest.Exporter
|
|
data *tests.Data
|
|
ctx context.Context
|
|
options func(*source.Options)
|
|
normalizers []normalizer
|
|
remote string
|
|
}
|
|
|
|
type normalizer struct {
|
|
path string
|
|
slashed string
|
|
escaped string
|
|
fragment string
|
|
}
|
|
|
|
func TestCommandLine(testdata string, options func(*source.Options)) func(*testing.T, packagestest.Exporter) {
|
|
return func(t *testing.T, exporter packagestest.Exporter) {
|
|
if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() {
|
|
t.Skip("testdata directory not present")
|
|
}
|
|
ctx := tests.Context(t)
|
|
ts := NewTestServer(ctx, options)
|
|
data := tests.Load(t, exporter, testdata)
|
|
for _, datum := range data {
|
|
defer datum.Exported.Cleanup()
|
|
t.Run(tests.FormatFolderName(datum.Folder), func(t *testing.T) {
|
|
t.Helper()
|
|
tests.Run(t, NewRunner(exporter, datum, ctx, ts.Addr, options), datum)
|
|
})
|
|
}
|
|
cmd.CloseTestConnections(ctx)
|
|
}
|
|
}
|
|
|
|
func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer {
|
|
ctx = debug.WithInstance(ctx, "", "")
|
|
cache := cache.New(ctx, options)
|
|
ss := lsprpc.NewStreamServer(cache)
|
|
return servertest.NewTCPServer(ctx, ss, nil)
|
|
}
|
|
|
|
func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
|
|
r := &runner{
|
|
exporter: exporter,
|
|
data: data,
|
|
ctx: ctx,
|
|
options: options,
|
|
normalizers: make([]normalizer, 0, len(data.Exported.Modules)),
|
|
remote: remote,
|
|
}
|
|
// build the path normalizing patterns
|
|
for _, m := range data.Exported.Modules {
|
|
for fragment := range m.Files {
|
|
n := normalizer{
|
|
path: data.Exported.File(m.Name, fragment),
|
|
fragment: fragment,
|
|
}
|
|
if n.slashed = filepath.ToSlash(n.path); n.slashed == n.path {
|
|
n.slashed = ""
|
|
}
|
|
quoted := strconv.Quote(n.path)
|
|
if n.escaped = quoted[1 : len(quoted)-1]; n.escaped == n.path {
|
|
n.escaped = ""
|
|
}
|
|
r.normalizers = append(r.normalizers, n)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
|
|
//TODO: add command line completions tests when it works
|
|
}
|
|
|
|
func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
|
|
rStdout, wStdout, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldStdout := os.Stdout
|
|
rStderr, wStderr, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldStderr := os.Stderr
|
|
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() {
|
|
io.Copy(stdout, rStdout)
|
|
wg.Done()
|
|
}()
|
|
go func() {
|
|
io.Copy(stderr, rStderr)
|
|
wg.Done()
|
|
}()
|
|
os.Stdout, os.Stderr = wStdout, wStderr
|
|
app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
|
|
remote := r.remote
|
|
err = tool.Run(tests.Context(t),
|
|
app,
|
|
append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...))
|
|
if err != nil {
|
|
fmt.Fprint(os.Stderr, err)
|
|
}
|
|
wStdout.Close()
|
|
wStderr.Close()
|
|
wg.Wait()
|
|
os.Stdout, os.Stderr = oldStdout, oldStderr
|
|
rStdout.Close()
|
|
rStderr.Close()
|
|
return stdout.String(), stderr.String()
|
|
}
|
|
|
|
// NormalizeGoplsCmd runs the gopls command and normalizes its output.
|
|
func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) {
|
|
stdout, stderr := r.runGoplsCmd(t, args...)
|
|
return r.Normalize(stdout), r.Normalize(stderr)
|
|
}
|
|
|
|
// NormalizePrefix normalizes a single path at the front of the input string.
|
|
func (r *runner) NormalizePrefix(s string) string {
|
|
for _, n := range r.normalizers {
|
|
if t := strings.TrimPrefix(s, n.path); t != s {
|
|
return n.fragment + t
|
|
}
|
|
if t := strings.TrimPrefix(s, n.slashed); t != s {
|
|
return n.fragment + t
|
|
}
|
|
if t := strings.TrimPrefix(s, n.escaped); t != s {
|
|
return n.fragment + t
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Normalize 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 (r *runner) Normalize(s string) string {
|
|
type entry struct {
|
|
path string
|
|
index int
|
|
fragment string
|
|
}
|
|
match := make([]entry, 0, len(r.normalizers))
|
|
// collect the initial state of all the matchers
|
|
for _, n := range r.normalizers {
|
|
index := strings.Index(s, n.path)
|
|
if index >= 0 {
|
|
match = append(match, entry{n.path, index, n.fragment})
|
|
}
|
|
if n.slashed != "" {
|
|
index := strings.Index(s, n.slashed)
|
|
if index >= 0 {
|
|
match = append(match, entry{n.slashed, index, n.fragment})
|
|
}
|
|
}
|
|
if n.escaped != "" {
|
|
index := strings.Index(s, n.escaped)
|
|
if index >= 0 {
|
|
match = append(match, entry{n.escaped, index, n.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
|
|
}
|
|
}
|
|
}
|