blob: 8bc100f3b10785a81c977c320118a3d4e285ba94 [file] [log] [blame]
// 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.Server
modes 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.NewServer(context.Background(), ss)
return &Runner{
ts: ts,
modes: 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 in all 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()
tests := []struct {
name string
mode EnvMode
makeServer func(context.Context, *testing.T) (*servertest.Server, func())
}{
{"singleton", Singleton, r.singletonEnv},
{"shared", Shared, r.sharedEnv},
{"forwarded", Forwarded, r.forwardedEnv},
}
for _, tc := range tests {
tc := tc
if r.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.makeServer(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.Server, func()) {
ss := lsprpc.NewStreamServer(cache.New(nil), false)
ts := servertest.NewServer(ctx, ss)
cleanup := func() {
ts.Close()
}
return ts, cleanup
}
func (r *Runner) sharedEnv(ctx context.Context, t *testing.T) (*servertest.Server, func()) {
return r.ts, func() {}
}
func (r *Runner) forwardedEnv(ctx context.Context, t *testing.T) (*servertest.Server, func()) {
forwarder := lsprpc.NewForwarder(r.ts.Addr, false)
ts2 := servertest.NewServer(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.Server
// 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.Server) *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
}
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()
}