// 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 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.NewTCPServer(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 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 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.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 } 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() }