aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/regtest/env.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/regtest/env.go')
-rw-r--r--gopls/internal/lsp/regtest/env.go391
1 files changed, 391 insertions, 0 deletions
diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go
new file mode 100644
index 000000000..91335bdab
--- /dev/null
+++ b/gopls/internal/lsp/regtest/env.go
@@ -0,0 +1,391 @@
+// 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
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+ "testing"
+
+ "golang.org/x/tools/gopls/internal/lsp/fake"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+)
+
+// Env holds the building blocks of an editor testing environment, providing
+// wrapper methods that hide the boilerplate of plumbing contexts and checking
+// errors.
+type Env struct {
+ T testing.TB // TODO(rfindley): rename to TB
+ Ctx context.Context
+
+ // Most tests should not need to access the scratch area, editor, server, or
+ // connection, but they are available if needed.
+ Sandbox *fake.Sandbox
+ Server servertest.Connector
+
+ // Editor is owned by the Env, and shut down
+ Editor *fake.Editor
+
+ Awaiter *Awaiter
+}
+
+// An Awaiter keeps track of relevant LSP state, so that it may be asserted
+// upon with Expectations.
+//
+// Wire it into a fake.Editor using Awaiter.Hooks().
+//
+// TODO(rfindley): consider simply merging Awaiter with the fake.Editor. It
+// probably is not worth its own abstraction.
+type Awaiter struct {
+ workdir *fake.Workdir
+
+ mu sync.Mutex
+ // For simplicity, each waiter gets a unique ID.
+ nextWaiterID int
+ state State
+ waiters map[int]*condition
+}
+
+func NewAwaiter(workdir *fake.Workdir) *Awaiter {
+ return &Awaiter{
+ workdir: workdir,
+ state: State{
+ diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
+ work: make(map[protocol.ProgressToken]*workProgress),
+ },
+ waiters: make(map[int]*condition),
+ }
+}
+
+// Hooks returns LSP client hooks required for awaiting asynchronous expectations.
+func (a *Awaiter) Hooks() fake.ClientHooks {
+ return fake.ClientHooks{
+ OnDiagnostics: a.onDiagnostics,
+ OnLogMessage: a.onLogMessage,
+ OnWorkDoneProgressCreate: a.onWorkDoneProgressCreate,
+ OnProgress: a.onProgress,
+ OnShowMessage: a.onShowMessage,
+ OnShowMessageRequest: a.onShowMessageRequest,
+ OnRegisterCapability: a.onRegisterCapability,
+ OnUnregisterCapability: a.onUnregisterCapability,
+ OnApplyEdit: a.onApplyEdit,
+ }
+}
+
+// State encapsulates the server state TODO: explain more
+type State struct {
+ // diagnostics are a map of relative path->diagnostics params
+ diagnostics map[string]*protocol.PublishDiagnosticsParams
+ logs []*protocol.LogMessageParams
+ showMessage []*protocol.ShowMessageParams
+ showMessageRequest []*protocol.ShowMessageRequestParams
+
+ registrations []*protocol.RegistrationParams
+ registeredCapabilities map[string]protocol.Registration
+ unregistrations []*protocol.UnregistrationParams
+ documentChanges []protocol.DocumentChanges // collected from ApplyEdit downcalls
+
+ // outstandingWork is a map of token->work summary. All tokens are assumed to
+ // be string, though the spec allows for numeric tokens as well. When work
+ // completes, it is deleted from this map.
+ work map[protocol.ProgressToken]*workProgress
+}
+
+// outstandingWork counts started but not complete work items by title.
+func (s State) outstandingWork() map[string]uint64 {
+ outstanding := make(map[string]uint64)
+ for _, work := range s.work {
+ if !work.complete {
+ outstanding[work.title]++
+ }
+ }
+ return outstanding
+}
+
+// completedWork counts complete work items by title.
+func (s State) completedWork() map[string]uint64 {
+ completed := make(map[string]uint64)
+ for _, work := range s.work {
+ if work.complete {
+ completed[work.title]++
+ }
+ }
+ return completed
+}
+
+// startedWork counts started (and possibly complete) work items.
+func (s State) startedWork() map[string]uint64 {
+ started := make(map[string]uint64)
+ for _, work := range s.work {
+ started[work.title]++
+ }
+ return started
+}
+
+type workProgress struct {
+ title, msg, endMsg string
+ percent float64
+ complete bool // seen 'end'.
+}
+
+// This method, provided for debugging, accesses mutable fields without a lock,
+// so it must not be called concurrent with any State mutation.
+func (s State) String() string {
+ var b strings.Builder
+ b.WriteString("#### log messages (see RPC logs for full text):\n")
+ for _, msg := range s.logs {
+ summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message)
+ if len(summary) > 60 {
+ summary = summary[:57] + "..."
+ }
+ // Some logs are quite long, and since they should be reproduced in the RPC
+ // logs on any failure we include here just a short summary.
+ fmt.Fprint(&b, "\t"+summary+"\n")
+ }
+ b.WriteString("\n")
+ b.WriteString("#### diagnostics:\n")
+ for name, params := range s.diagnostics {
+ fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version))
+ for _, d := range params.Diagnostics {
+ fmt.Fprintf(&b, "\t\t(%d, %d) [%s]: %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Source, d.Message)
+ }
+ }
+ b.WriteString("\n")
+ b.WriteString("#### outstanding work:\n")
+ for token, state := range s.work {
+ if state.complete {
+ continue
+ }
+ name := state.title
+ if name == "" {
+ name = fmt.Sprintf("!NO NAME(token: %s)", token)
+ }
+ fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent)
+ }
+ b.WriteString("#### completed work:\n")
+ for name, count := range s.completedWork() {
+ fmt.Fprintf(&b, "\t%s: %d\n", name, count)
+ }
+ return b.String()
+}
+
+// A condition is satisfied when all expectations are simultaneously
+// met. At that point, the 'met' channel is closed. On any failure, err is set
+// and the failed channel is closed.
+type condition struct {
+ expectations []Expectation
+ verdict chan Verdict
+}
+
+func (a *Awaiter) onApplyEdit(_ context.Context, params *protocol.ApplyWorkspaceEditParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.documentChanges = append(a.state.documentChanges, params.Edit.DocumentChanges...)
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ pth := a.workdir.URIToPath(d.URI)
+ a.state.diagnostics[pth] = d
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.showMessage = append(a.state.showMessage, m)
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.showMessageRequest = append(a.state.showMessageRequest, m)
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.logs = append(a.state.logs, m)
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.work[m.Token] = &workProgress{}
+ return nil
+}
+
+func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ work, ok := a.state.work[m.Token]
+ if !ok {
+ panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m))
+ }
+ v := m.Value.(map[string]interface{})
+ switch kind := v["kind"]; kind {
+ case "begin":
+ work.title = v["title"].(string)
+ if msg, ok := v["message"]; ok {
+ work.msg = msg.(string)
+ }
+ case "report":
+ if pct, ok := v["percentage"]; ok {
+ work.percent = pct.(float64)
+ }
+ if msg, ok := v["message"]; ok {
+ work.msg = msg.(string)
+ }
+ case "end":
+ work.complete = true
+ if msg, ok := v["message"]; ok {
+ work.endMsg = msg.(string)
+ }
+ }
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onRegisterCapability(_ context.Context, m *protocol.RegistrationParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.registrations = append(a.state.registrations, m)
+ if a.state.registeredCapabilities == nil {
+ a.state.registeredCapabilities = make(map[string]protocol.Registration)
+ }
+ for _, reg := range m.Registrations {
+ a.state.registeredCapabilities[reg.Method] = reg
+ }
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) onUnregisterCapability(_ context.Context, m *protocol.UnregistrationParams) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.state.unregistrations = append(a.state.unregistrations, m)
+ a.checkConditionsLocked()
+ return nil
+}
+
+func (a *Awaiter) checkConditionsLocked() {
+ for id, condition := range a.waiters {
+ if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet {
+ delete(a.waiters, id)
+ condition.verdict <- v
+ }
+ }
+}
+
+// takeDocumentChanges returns any accumulated document changes (from
+// server ApplyEdit RPC downcalls) and resets the list.
+func (a *Awaiter) takeDocumentChanges() []protocol.DocumentChanges {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ res := a.state.documentChanges
+ a.state.documentChanges = nil
+ return res
+}
+
+// checkExpectations reports whether s meets all expectations.
+func checkExpectations(s State, expectations []Expectation) (Verdict, string) {
+ finalVerdict := Met
+ var summary strings.Builder
+ for _, e := range expectations {
+ v := e.Check(s)
+ if v > finalVerdict {
+ finalVerdict = v
+ }
+ fmt.Fprintf(&summary, "%v: %s\n", v, e.Description)
+ }
+ return finalVerdict, summary.String()
+}
+
+// Await blocks until the given expectations are all simultaneously met.
+//
+// Generally speaking Await should be avoided because it blocks indefinitely if
+// gopls ends up in a state where the expectations are never going to be met.
+// Use AfterChange or OnceMet instead, so that the runner knows when to stop
+// waiting.
+func (e *Env) Await(expectations ...Expectation) {
+ e.T.Helper()
+ if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil {
+ e.T.Fatal(err)
+ }
+}
+
+// OnceMet blocks until the precondition is met by the state or becomes
+// unmeetable. If it was met, OnceMet checks that the state meets all
+// expectations in mustMeets.
+func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) {
+ e.Await(OnceMet(precondition, mustMeets...))
+}
+
+// Await waits for all expectations to simultaneously be met. It should only be
+// called from the main test goroutine.
+func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error {
+ a.mu.Lock()
+ // Before adding the waiter, we check if the condition is currently met or
+ // failed to avoid a race where the condition was realized before Await was
+ // called.
+ switch verdict, summary := checkExpectations(a.state, expectations); verdict {
+ case Met:
+ a.mu.Unlock()
+ return nil
+ case Unmeetable:
+ err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state)
+ a.mu.Unlock()
+ return err
+ }
+ cond := &condition{
+ expectations: expectations,
+ verdict: make(chan Verdict),
+ }
+ a.waiters[a.nextWaiterID] = cond
+ a.nextWaiterID++
+ a.mu.Unlock()
+
+ var err error
+ select {
+ case <-ctx.Done():
+ err = ctx.Err()
+ case v := <-cond.verdict:
+ if v != Met {
+ err = fmt.Errorf("condition has final verdict %v", v)
+ }
+ }
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ _, summary := checkExpectations(a.state, expectations)
+
+ // Debugging an unmet expectation can be tricky, so we put some effort into
+ // nicely formatting the failure.
+ if err != nil {
+ return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state)
+ }
+ return nil
+}