mirror of
https://github.com/golang/go
synced 2024-11-18 23:54:41 -07:00
741f65b509
Add a forwarder handler that alters messages before forwarding, for now, it just intercepts the "exit" message. Also, make it easier to write regression tests for a shared gopls instance, by adding a helper that instantiates two connected environments, and only runs in the shared execution modes. Updates golang/go#36879 Updates golang/go#34111 Change-Id: I7673f72ab71b5c7fd6ad65d274c15132a942e06a Reviewed-on: https://go-review.googlesource.com/c/tools/+/218778 Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
349 lines
10 KiB
Go
349 lines
10 KiB
Go
// 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 (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/tools/internal/jsonrpc2/servertest"
|
|
"golang.org/x/tools/internal/lsp/cache"
|
|
"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 (
|
|
// Singleton mode uses a separate cache for each test
|
|
Singleton EnvMode = 1 << iota
|
|
// Shared mode uses a Shared cache
|
|
Shared
|
|
// Forwarded forwards connections
|
|
Forwarded
|
|
// AllModes runs tests in all modes
|
|
AllModes = Singleton | Shared | Forwarded
|
|
)
|
|
|
|
// 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 {
|
|
ts *servertest.TCPServer
|
|
defaultModes EnvMode
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewTestRunner creates a Runner with its shared state initialized, ready to
|
|
// run tests.
|
|
func NewTestRunner(modes EnvMode, testTimeout time.Duration) *Runner {
|
|
ss := lsprpc.NewStreamServer(cache.New(nil), false)
|
|
ts := servertest.NewTCPServer(context.Background(), ss)
|
|
return &Runner{
|
|
ts: ts,
|
|
defaultModes: modes,
|
|
timeout: testTimeout,
|
|
}
|
|
}
|
|
|
|
// Close cleans up resource that have been allocated to this workspace.
|
|
func (r *Runner) Close() error {
|
|
return r.ts.Close()
|
|
}
|
|
|
|
// 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.
|
|
func (r *Runner) Run(t *testing.T, filedata string, test func(context.Context, *testing.T, *Env)) {
|
|
t.Helper()
|
|
r.RunInMode(r.defaultModes, t, filedata, test)
|
|
}
|
|
|
|
// 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()
|
|
tests := []struct {
|
|
name string
|
|
mode EnvMode
|
|
getConnector func(context.Context, *testing.T) (servertest.Connector, func())
|
|
}{
|
|
{"singleton", Singleton, r.singletonEnv},
|
|
{"shared", Shared, r.sharedEnv},
|
|
{"forwarded", Forwarded, r.forwardedEnv},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
if modes&tc.mode == 0 {
|
|
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()
|
|
ts, cleanup := tc.getConnector(ctx, t)
|
|
defer cleanup()
|
|
env := NewEnv(ctx, t, ws, ts)
|
|
test(ctx, t, env)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (r *Runner) singletonEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
|
ss := lsprpc.NewStreamServer(cache.New(nil), false)
|
|
ts := servertest.NewPipeServer(ctx, ss)
|
|
cleanup := func() {
|
|
ts.Close()
|
|
}
|
|
return ts, cleanup
|
|
}
|
|
|
|
func (r *Runner) sharedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
|
return r.ts, func() {}
|
|
}
|
|
|
|
func (r *Runner) forwardedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
|
|
forwarder := lsprpc.NewForwarder(r.ts.Addr, false)
|
|
ts2 := servertest.NewTCPServer(ctx, forwarder)
|
|
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
|
|
Server servertest.Connector
|
|
|
|
// 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.
|
|
func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest.Connector) *Env {
|
|
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
|
|
}
|
|
|
|
// RemoveFileFromWorkspace deletes a file on disk but does nothing in the
|
|
// editor. It calls t.Fatal on any error.
|
|
func (e *Env) RemoveFileFromWorkspace(name string) {
|
|
e.t.Helper()
|
|
if err := e.W.RemoveFile(e.ctx, name); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// OpenFile opens a file in the editor, calling t.Fatal on any error.
|
|
func (e *Env) OpenFile(name string) {
|
|
e.t.Helper()
|
|
if err := e.E.OpenFile(e.ctx, name); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// CreateBuffer creates a buffer in the editor, calling t.Fatal on any error.
|
|
func (e *Env) CreateBuffer(name string, content string) {
|
|
e.t.Helper()
|
|
if err := e.E.CreateBuffer(e.ctx, name, content); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// EditBuffer applies edits to an editor buffer, calling t.Fatal on any error.
|
|
func (e *Env) EditBuffer(name string, edits ...fake.Edit) {
|
|
e.t.Helper()
|
|
if err := e.E.EditBuffer(e.ctx, name, edits); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// GoToDefinition goes to definition in the editor, calling t.Fatal on any
|
|
// error.
|
|
func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) {
|
|
e.t.Helper()
|
|
n, p, err := e.E.GoToDefinition(e.ctx, name, pos)
|
|
if err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
return n, p
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// CloseEditor shuts down the editor, calling t.Fatal on any error.
|
|
func (e *Env) CloseEditor() {
|
|
e.t.Helper()
|
|
if err := e.E.ShutdownAndExit(e.ctx); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|