2020-02-06 17:50:37 -07:00
|
|
|
// Copyright 2020 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 regtest provides an environment for writing regression tests.
|
|
|
|
package regtest
|
|
|
|
|
|
|
|
import (
|
2020-02-10 09:34:13 -07:00
|
|
|
"bytes"
|
2020-02-06 17:50:37 -07:00
|
|
|
"context"
|
|
|
|
"fmt"
|
2020-02-10 09:34:13 -07:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2020-02-06 17:50:37 -07:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"golang.org/x/tools/internal/jsonrpc2/servertest"
|
|
|
|
"golang.org/x/tools/internal/lsp/cache"
|
2020-02-19 08:18:21 -07:00
|
|
|
"golang.org/x/tools/internal/lsp/debug"
|
2020-02-06 17:50:37 -07:00
|
|
|
"golang.org/x/tools/internal/lsp/fake"
|
|
|
|
"golang.org/x/tools/internal/lsp/lsprpc"
|
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
|
|
)
|
|
|
|
|
|
|
|
// EnvMode is a bitmask that defines in which execution environments a test
|
|
|
|
// should run.
|
|
|
|
type EnvMode int
|
|
|
|
|
|
|
|
const (
|
2020-02-10 09:34:13 -07:00
|
|
|
// Singleton mode uses a separate cache for each test.
|
2020-02-06 17:50:37 -07:00
|
|
|
Singleton EnvMode = 1 << iota
|
2020-02-10 09:34:13 -07:00
|
|
|
// Shared mode uses a Shared cache.
|
2020-02-06 17:50:37 -07:00
|
|
|
Shared
|
2020-02-10 09:34:13 -07:00
|
|
|
// Forwarded forwards connections to an in-process gopls instance.
|
2020-02-06 17:50:37 -07:00
|
|
|
Forwarded
|
2020-02-10 09:34:13 -07:00
|
|
|
// SeparateProcess runs a separate gopls process, and forwards connections to
|
|
|
|
// it.
|
|
|
|
SeparateProcess
|
|
|
|
// NormalModes runs tests in all modes.
|
2020-02-27 10:31:02 -07:00
|
|
|
NormalModes = Singleton | Forwarded
|
2020-02-06 17:50:37 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
// A Runner runs tests in gopls execution environments, as specified by its
|
|
|
|
// modes. For modes that share state (for example, a shared cache or common
|
|
|
|
// remote), any tests that execute on the same Runner will share the same
|
|
|
|
// state.
|
|
|
|
type Runner struct {
|
2020-02-09 12:44:03 -07:00
|
|
|
defaultModes EnvMode
|
|
|
|
timeout time.Duration
|
2020-02-10 09:34:13 -07:00
|
|
|
goplsPath string
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
ts *servertest.TCPServer
|
|
|
|
socketDir string
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewTestRunner creates a Runner with its shared state initialized, ready to
|
|
|
|
// run tests.
|
2020-02-10 09:34:13 -07:00
|
|
|
func NewTestRunner(modes EnvMode, testTimeout time.Duration, goplsPath string) *Runner {
|
2020-02-06 17:50:37 -07:00
|
|
|
return &Runner{
|
2020-02-09 12:44:03 -07:00
|
|
|
defaultModes: modes,
|
|
|
|
timeout: testTimeout,
|
2020-02-10 09:34:13 -07:00
|
|
|
goplsPath: goplsPath,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Modes returns the bitmask of environment modes this runner is configured to
|
|
|
|
// test.
|
|
|
|
func (r *Runner) Modes() EnvMode {
|
|
|
|
return r.defaultModes
|
|
|
|
}
|
|
|
|
|
|
|
|
// getTestServer gets the test server instance to connect to, or creates one if
|
|
|
|
// it doesn't exist.
|
|
|
|
func (r *Runner) getTestServer() *servertest.TCPServer {
|
|
|
|
r.mu.Lock()
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
if r.ts == nil {
|
2020-02-28 08:30:03 -07:00
|
|
|
ctx := context.Background()
|
|
|
|
ctx = debug.WithInstance(ctx, "", "")
|
|
|
|
ss := lsprpc.NewStreamServer(cache.New(ctx, nil), false)
|
2020-02-10 09:34:13 -07:00
|
|
|
r.ts = servertest.NewTCPServer(context.Background(), ss)
|
|
|
|
}
|
|
|
|
return r.ts
|
|
|
|
}
|
|
|
|
|
|
|
|
// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
|
|
|
|
// tests. It's a trick to allow tests to find a binary to use to start a gopls
|
|
|
|
// subprocess.
|
|
|
|
const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
|
|
|
|
|
|
|
|
func (r *Runner) getRemoteSocket(t *testing.T) string {
|
|
|
|
t.Helper()
|
|
|
|
r.mu.Lock()
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
const daemonFile = "gopls-test-daemon"
|
|
|
|
if r.socketDir != "" {
|
|
|
|
return filepath.Join(r.socketDir, daemonFile)
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
2020-02-10 09:34:13 -07:00
|
|
|
|
|
|
|
if r.goplsPath == "" {
|
|
|
|
t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured")
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
r.socketDir, err = ioutil.TempDir("", "gopls-regtests")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("creating tempdir: %v", err)
|
|
|
|
}
|
|
|
|
socket := filepath.Join(r.socketDir, daemonFile)
|
2020-02-25 09:00:37 -07:00
|
|
|
args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"}
|
2020-02-10 09:34:13 -07:00
|
|
|
cmd := exec.Command(r.goplsPath, args...)
|
|
|
|
cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
|
|
|
|
var stderr bytes.Buffer
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
go func() {
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String()))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return socket
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close cleans up resource that have been allocated to this workspace.
|
|
|
|
func (r *Runner) Close() error {
|
2020-02-10 09:34:13 -07:00
|
|
|
r.mu.Lock()
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
if r.ts != nil {
|
|
|
|
r.ts.Close()
|
|
|
|
}
|
|
|
|
if r.socketDir != "" {
|
|
|
|
os.RemoveAll(r.socketDir)
|
|
|
|
}
|
|
|
|
return nil
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
|
|
|
|
2020-02-09 12:44:03 -07:00
|
|
|
// Run executes the test function in the default configured gopls execution
|
|
|
|
// modes. For each a test run, a new workspace is created containing the
|
|
|
|
// un-txtared files specified by filedata.
|
2020-02-06 17:50:37 -07:00
|
|
|
func (r *Runner) Run(t *testing.T, filedata string, test func(context.Context, *testing.T, *Env)) {
|
|
|
|
t.Helper()
|
2020-02-09 12:44:03 -07:00
|
|
|
r.RunInMode(r.defaultModes, t, filedata, test)
|
|
|
|
}
|
2020-02-06 17:50:37 -07:00
|
|
|
|
2020-02-09 12:44:03 -07:00
|
|
|
// RunInMode runs the test in the execution modes specified by the modes bitmask.
|
|
|
|
func (r *Runner) RunInMode(modes EnvMode, t *testing.T, filedata string, test func(ctx context.Context, t *testing.T, e *Env)) {
|
|
|
|
t.Helper()
|
2020-02-06 17:50:37 -07:00
|
|
|
tests := []struct {
|
2020-02-09 11:38:49 -07:00
|
|
|
name string
|
|
|
|
mode EnvMode
|
|
|
|
getConnector func(context.Context, *testing.T) (servertest.Connector, func())
|
2020-02-06 17:50:37 -07:00
|
|
|
}{
|
|
|
|
{"singleton", Singleton, r.singletonEnv},
|
|
|
|
{"shared", Shared, r.sharedEnv},
|
|
|
|
{"forwarded", Forwarded, r.forwardedEnv},
|
2020-02-10 09:34:13 -07:00
|
|
|
{"separate_process", SeparateProcess, r.separateProcessEnv},
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range tests {
|
|
|
|
tc := tc
|
2020-02-09 12:44:03 -07:00
|
|
|
if modes&tc.mode == 0 {
|
2020-02-06 17:50:37 -07:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
|
|
|
|
defer cancel()
|
|
|
|
ws, err := fake.NewWorkspace("lsprpc", []byte(filedata))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer ws.Close()
|
2020-02-09 11:38:49 -07:00
|
|
|
ts, cleanup := tc.getConnector(ctx, t)
|
2020-02-06 17:50:37 -07:00
|
|
|
defer cleanup()
|
|
|
|
env := NewEnv(ctx, t, ws, ts)
|
2020-02-18 18:53:06 -07:00
|
|
|
defer func() {
|
|
|
|
if err := env.E.Shutdown(ctx); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}()
|
2020-02-06 17:50:37 -07:00
|
|
|
test(ctx, t, env)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-09 11:38:49 -07:00
|
|
|
func (r *Runner) singletonEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
2020-02-28 08:30:03 -07:00
|
|
|
ctx = debug.WithInstance(ctx, "", "")
|
|
|
|
ss := lsprpc.NewStreamServer(cache.New(ctx, nil), false)
|
2020-02-09 11:38:49 -07:00
|
|
|
ts := servertest.NewPipeServer(ctx, ss)
|
2020-02-06 17:50:37 -07:00
|
|
|
cleanup := func() {
|
|
|
|
ts.Close()
|
|
|
|
}
|
|
|
|
return ts, cleanup
|
|
|
|
}
|
|
|
|
|
2020-02-09 11:38:49 -07:00
|
|
|
func (r *Runner) sharedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
2020-02-10 09:34:13 -07:00
|
|
|
return r.getTestServer(), func() {}
|
2020-02-06 17:50:37 -07:00
|
|
|
}
|
|
|
|
|
2020-02-09 11:38:49 -07:00
|
|
|
func (r *Runner) forwardedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
2020-02-28 08:30:03 -07:00
|
|
|
ctx = debug.WithInstance(ctx, "", "")
|
2020-02-10 09:34:13 -07:00
|
|
|
ts := r.getTestServer()
|
2020-02-28 08:30:03 -07:00
|
|
|
forwarder := lsprpc.NewForwarder("tcp", ts.Addr, false)
|
2020-02-10 09:34:13 -07:00
|
|
|
ts2 := servertest.NewPipeServer(ctx, forwarder)
|
|
|
|
cleanup := func() {
|
|
|
|
ts2.Close()
|
|
|
|
}
|
|
|
|
return ts2, cleanup
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Runner) separateProcessEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
2020-02-28 08:30:03 -07:00
|
|
|
ctx = debug.WithInstance(ctx, "", "")
|
2020-02-10 09:34:13 -07:00
|
|
|
socket := r.getRemoteSocket(t)
|
2020-02-18 10:47:19 -07:00
|
|
|
// TODO(rfindley): can we use the autostart behavior here, instead of
|
|
|
|
// pre-starting the remote?
|
2020-02-28 08:30:03 -07:00
|
|
|
forwarder := lsprpc.NewForwarder("unix", socket, false)
|
2020-02-10 09:34:13 -07:00
|
|
|
ts2 := servertest.NewPipeServer(ctx, forwarder)
|
2020-02-06 17:50:37 -07:00
|
|
|
cleanup := func() {
|
|
|
|
ts2.Close()
|
|
|
|
}
|
|
|
|
return ts2, cleanup
|
|
|
|
}
|
|
|
|
|
|
|
|
// Env holds an initialized fake Editor, Workspace, and Server, which may be
|
|
|
|
// used for writing tests. It also provides adapter methods that call t.Fatal
|
|
|
|
// on any error, so that tests for the happy path may be written without
|
|
|
|
// checking errors.
|
|
|
|
type Env struct {
|
|
|
|
t *testing.T
|
|
|
|
ctx context.Context
|
|
|
|
|
|
|
|
// Most tests should not need to access the workspace or editor, or server,
|
|
|
|
// but they are available if needed.
|
|
|
|
W *fake.Workspace
|
|
|
|
E *fake.Editor
|
2020-02-09 11:38:49 -07:00
|
|
|
Server servertest.Connector
|
2020-02-06 17:50:37 -07:00
|
|
|
|
|
|
|
// mu guards the fields below, for the purpose of checking conditions on
|
|
|
|
// every change to diagnostics.
|
|
|
|
mu sync.Mutex
|
|
|
|
// For simplicity, each waiter gets a unique ID.
|
|
|
|
nextWaiterID int
|
|
|
|
lastDiagnostics map[string]*protocol.PublishDiagnosticsParams
|
|
|
|
waiters map[int]*diagnosticCondition
|
|
|
|
}
|
|
|
|
|
|
|
|
// A diagnosticCondition is satisfied when all expectations are simultaneously
|
|
|
|
// met. At that point, the 'met' channel is closed.
|
|
|
|
type diagnosticCondition struct {
|
|
|
|
expectations []DiagnosticExpectation
|
|
|
|
met chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewEnv creates a new test environment using the given workspace and gopls
|
|
|
|
// server.
|
2020-02-09 11:38:49 -07:00
|
|
|
func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest.Connector) *Env {
|
2020-02-06 17:50:37 -07:00
|
|
|
t.Helper()
|
|
|
|
conn := ts.Connect(ctx)
|
|
|
|
editor, err := fake.NewConnectedEditor(ctx, ws, conn)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
env := &Env{
|
|
|
|
t: t,
|
|
|
|
ctx: ctx,
|
|
|
|
W: ws,
|
|
|
|
E: editor,
|
|
|
|
Server: ts,
|
|
|
|
lastDiagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
|
|
|
|
waiters: make(map[int]*diagnosticCondition),
|
|
|
|
}
|
|
|
|
env.E.Client().OnDiagnostics(env.onDiagnostics)
|
|
|
|
return env
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
|
|
|
|
pth := e.W.URIToPath(d.URI)
|
|
|
|
e.lastDiagnostics[pth] = d
|
|
|
|
|
|
|
|
for id, condition := range e.waiters {
|
|
|
|
if meetsCondition(e.lastDiagnostics, condition.expectations) {
|
|
|
|
delete(e.waiters, id)
|
|
|
|
close(condition.met)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func meetsCondition(m map[string]*protocol.PublishDiagnosticsParams, expectations []DiagnosticExpectation) bool {
|
|
|
|
for _, e := range expectations {
|
|
|
|
if !e.IsMet(m) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// A DiagnosticExpectation is a condition that must be met by the current set
|
|
|
|
// of diagnostics.
|
|
|
|
type DiagnosticExpectation struct {
|
|
|
|
IsMet func(map[string]*protocol.PublishDiagnosticsParams) bool
|
|
|
|
Description string
|
|
|
|
}
|
|
|
|
|
|
|
|
// EmptyDiagnostics asserts that diagnostics are empty for the
|
|
|
|
// workspace-relative path name.
|
|
|
|
func EmptyDiagnostics(name string) DiagnosticExpectation {
|
|
|
|
isMet := func(diags map[string]*protocol.PublishDiagnosticsParams) bool {
|
|
|
|
ds, ok := diags[name]
|
|
|
|
return ok && len(ds.Diagnostics) == 0
|
|
|
|
}
|
|
|
|
return DiagnosticExpectation{
|
|
|
|
IsMet: isMet,
|
|
|
|
Description: fmt.Sprintf("empty diagnostics for %q", name),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DiagnosticAt asserts that there is a diagnostic entry at the position
|
|
|
|
// specified by line and col, for the workspace-relative path name.
|
|
|
|
func DiagnosticAt(name string, line, col int) DiagnosticExpectation {
|
|
|
|
isMet := func(diags map[string]*protocol.PublishDiagnosticsParams) bool {
|
|
|
|
ds, ok := diags[name]
|
|
|
|
if !ok || len(ds.Diagnostics) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, d := range ds.Diagnostics {
|
|
|
|
if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return DiagnosticExpectation{
|
|
|
|
IsMet: isMet,
|
|
|
|
Description: fmt.Sprintf("diagnostic in %q at (line:%d, column:%d)", name, line, col),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Await waits for all diagnostic expectations to simultaneously be met.
|
|
|
|
func (e *Env) Await(expectations ...DiagnosticExpectation) {
|
|
|
|
// NOTE: in the future this mechanism extend beyond just diagnostics, for
|
|
|
|
// example by modifying IsMet to be a func(*Env) boo. However, that would
|
|
|
|
// require careful checking of conditions around every state change, so for
|
|
|
|
// now we just limit the scope to diagnostic conditions.
|
|
|
|
|
|
|
|
e.t.Helper()
|
|
|
|
e.mu.Lock()
|
|
|
|
// Before adding the waiter, we check if the condition is currently met to
|
|
|
|
// avoid a race where the condition was realized before Await was called.
|
|
|
|
if meetsCondition(e.lastDiagnostics, expectations) {
|
|
|
|
e.mu.Unlock()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
met := make(chan struct{})
|
|
|
|
e.waiters[e.nextWaiterID] = &diagnosticCondition{
|
|
|
|
expectations: expectations,
|
|
|
|
met: met,
|
|
|
|
}
|
|
|
|
e.nextWaiterID++
|
|
|
|
e.mu.Unlock()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-e.ctx.Done():
|
|
|
|
// Debugging an unmet expectation can be tricky, so we put some effort into
|
|
|
|
// nicely formatting the failure.
|
|
|
|
var descs []string
|
|
|
|
for _, e := range expectations {
|
|
|
|
descs = append(descs, e.Description)
|
|
|
|
}
|
|
|
|
e.mu.Lock()
|
|
|
|
diagString := formatDiagnostics(e.lastDiagnostics)
|
|
|
|
e.mu.Unlock()
|
|
|
|
e.t.Fatalf("waiting on (%s):\nerr:%v\ndiagnostics:\n%s", strings.Join(descs, ", "), e.ctx.Err(), diagString)
|
|
|
|
case <-met:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatDiagnostics(diags map[string]*protocol.PublishDiagnosticsParams) string {
|
|
|
|
var b strings.Builder
|
|
|
|
for name, params := range diags {
|
|
|
|
b.WriteString(name + ":\n")
|
|
|
|
for _, d := range params.Diagnostics {
|
|
|
|
b.WriteString(fmt.Sprintf("\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|