1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
|
// 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 integration
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"cuelang.org/go/internal/golangorgx/gopls/protocol"
"cuelang.org/go/internal/golangorgx/gopls/test/integration/fake"
"cuelang.org/go/internal/golangorgx/tools/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,
OnShowDocument: a.onShowDocument,
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
showDocument []*protocol.ShowDocumentParams
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
}
// 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, params.Version)
for _, d := range params.Diagnostics {
fmt.Fprintf(&b, "\t\t%d:%d [%s]: %s\n", d.Range.Start.Line, 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) onShowDocument(_ context.Context, params *protocol.ShowDocumentParams) error {
a.mu.Lock()
defer a.mu.Unlock()
a.state.showDocument = append(a.state.showDocument, params)
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.T.Helper()
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
}
|