1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
14 "golang.org/x/tools/internal/jsonrpc2/servertest"
15 "golang.org/x/tools/internal/lsp/fake"
16 "golang.org/x/tools/internal/lsp/protocol"
19 // Env holds an initialized fake Editor, Workspace, and Server, which may be
20 // used for writing tests. It also provides adapter methods that call t.Fatal
21 // on any error, so that tests for the happy path may be written without
27 // Most tests should not need to access the scratch area, editor, server, or
28 // connection, but they are available if needed.
31 Server servertest.Connector
33 // mu guards the fields below, for the purpose of checking conditions on
34 // every change to diagnostics.
36 // For simplicity, each waiter gets a unique ID.
39 waiters map[int]*condition
42 // State encapsulates the server state TODO: explain more
44 // diagnostics are a map of relative path->diagnostics params
45 diagnostics map[string]*protocol.PublishDiagnosticsParams
46 logs []*protocol.LogMessageParams
47 showMessage []*protocol.ShowMessageParams
48 showMessageRequest []*protocol.ShowMessageRequestParams
50 registrations []*protocol.RegistrationParams
51 unregistrations []*protocol.UnregistrationParams
53 // outstandingWork is a map of token->work summary. All tokens are assumed to
54 // be string, though the spec allows for numeric tokens as well. When work
55 // completes, it is deleted from this map.
56 outstandingWork map[protocol.ProgressToken]*workProgress
57 completedWork map[string]int
60 type workProgress struct {
65 func (s State) String() string {
67 b.WriteString("#### log messages (see RPC logs for full text):\n")
68 for _, msg := range s.logs {
69 summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message)
70 if len(summary) > 60 {
71 summary = summary[:57] + "..."
73 // Some logs are quite long, and since they should be reproduced in the RPC
74 // logs on any failure we include here just a short summary.
75 fmt.Fprint(&b, "\t"+summary+"\n")
78 b.WriteString("#### diagnostics:\n")
79 for name, params := range s.diagnostics {
80 fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version))
81 for _, d := range params.Diagnostics {
82 fmt.Fprintf(&b, "\t\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message)
86 b.WriteString("#### outstanding work:\n")
87 for token, state := range s.outstandingWork {
90 name = fmt.Sprintf("!NO NAME(token: %s)", token)
92 fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent)
94 b.WriteString("#### completed work:\n")
95 for name, count := range s.completedWork {
96 fmt.Fprintf(&b, "\t%s: %d\n", name, count)
101 // A condition is satisfied when all expectations are simultaneously
102 // met. At that point, the 'met' channel is closed. On any failure, err is set
103 // and the failed channel is closed.
104 type condition struct {
105 expectations []Expectation
109 // NewEnv creates a new test environment using the given scratch environment
111 func NewEnv(ctx context.Context, t *testing.T, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) *Env {
113 conn := ts.Connect(ctx)
120 diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
121 outstandingWork: make(map[protocol.ProgressToken]*workProgress),
122 completedWork: make(map[string]int),
124 waiters: make(map[int]*condition),
126 var hooks fake.ClientHooks
128 hooks = fake.ClientHooks{
129 OnDiagnostics: env.onDiagnostics,
130 OnLogMessage: env.onLogMessage,
131 OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate,
132 OnProgress: env.onProgress,
133 OnShowMessage: env.onShowMessage,
134 OnShowMessageRequest: env.onShowMessageRequest,
135 OnRegistration: env.onRegistration,
136 OnUnregistration: env.onUnregistration,
139 editor, err := fake.NewEditor(sandbox, editorConfig).Connect(ctx, conn, hooks)
147 func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
151 pth := e.Sandbox.Workdir.URIToPath(d.URI)
152 e.state.diagnostics[pth] = d
153 e.checkConditionsLocked()
157 func (e *Env) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error {
161 e.state.showMessage = append(e.state.showMessage, m)
162 e.checkConditionsLocked()
166 func (e *Env) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error {
170 e.state.showMessageRequest = append(e.state.showMessageRequest, m)
171 e.checkConditionsLocked()
175 func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error {
179 e.state.logs = append(e.state.logs, m)
180 e.checkConditionsLocked()
184 func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error {
188 e.state.outstandingWork[m.Token] = &workProgress{}
192 func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error {
195 work, ok := e.state.outstandingWork[m.Token]
197 panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m))
199 v := m.Value.(map[string]interface{})
200 switch kind := v["kind"]; kind {
202 work.title = v["title"].(string)
204 if pct, ok := v["percentage"]; ok {
205 work.percent = pct.(float64)
208 title := e.state.outstandingWork[m.Token].title
209 e.state.completedWork[title] = e.state.completedWork[title] + 1
210 delete(e.state.outstandingWork, m.Token)
212 e.checkConditionsLocked()
216 func (e *Env) onRegistration(_ context.Context, m *protocol.RegistrationParams) error {
220 e.state.registrations = append(e.state.registrations, m)
221 e.checkConditionsLocked()
225 func (e *Env) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error {
229 e.state.unregistrations = append(e.state.unregistrations, m)
230 e.checkConditionsLocked()
234 func (e *Env) checkConditionsLocked() {
235 for id, condition := range e.waiters {
236 if v, _ := checkExpectations(e.state, condition.expectations); v != Unmet {
237 delete(e.waiters, id)
238 condition.verdict <- v
243 // checkExpectations reports whether s meets all expectations.
244 func checkExpectations(s State, expectations []Expectation) (Verdict, string) {
246 var summary strings.Builder
247 for _, e := range expectations {
249 if v > finalVerdict {
252 summary.WriteString(fmt.Sprintf("\t%v: %s\n", v, e.Description()))
254 return finalVerdict, summary.String()
257 // DiagnosticsFor returns the current diagnostics for the file. It is useful
258 // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic
259 // is not simply described by DiagnosticAt.
260 func (e *Env) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams {
263 return e.state.diagnostics[name]
266 // Await waits for all expectations to simultaneously be met. It should only be
267 // called from the main test goroutine.
268 func (e *Env) Await(expectations ...Expectation) {
271 // Before adding the waiter, we check if the condition is currently met or
272 // failed to avoid a race where the condition was realized before Await was
274 switch verdict, summary := checkExpectations(e.state, expectations); verdict {
280 e.T.Fatalf("unmeetable expectations:\n%s\nstate:\n%v", summary, e.state)
283 expectations: expectations,
284 verdict: make(chan Verdict),
286 e.waiters[e.nextWaiterID] = cond
294 case v := <-cond.verdict:
296 err = fmt.Errorf("condition has final verdict %v", v)
301 _, summary := checkExpectations(e.state, expectations)
303 // Debugging an unmet expectation can be tricky, so we put some effort into
304 // nicely formatting the failure.
306 e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state)