github.com/v2fly/tools@v0.100.0/internal/lsp/regtest/env.go (about) 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. 4 5 package regtest 6 7 import ( 8 "context" 9 "fmt" 10 "strings" 11 "sync" 12 "testing" 13 14 "github.com/v2fly/tools/internal/jsonrpc2/servertest" 15 "github.com/v2fly/tools/internal/lsp/fake" 16 "github.com/v2fly/tools/internal/lsp/protocol" 17 ) 18 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 22 // checking errors. 23 type Env struct { 24 T *testing.T 25 Ctx context.Context 26 27 // Most tests should not need to access the scratch area, editor, server, or 28 // connection, but they are available if needed. 29 Sandbox *fake.Sandbox 30 Editor *fake.Editor 31 Server servertest.Connector 32 33 // mu guards the fields below, for the purpose of checking conditions on 34 // every change to diagnostics. 35 mu sync.Mutex 36 // For simplicity, each waiter gets a unique ID. 37 nextWaiterID int 38 state State 39 waiters map[int]*condition 40 } 41 42 // State encapsulates the server state TODO: explain more 43 type State struct { 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 49 50 registrations []*protocol.RegistrationParams 51 unregistrations []*protocol.UnregistrationParams 52 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]uint64 58 } 59 60 type workProgress struct { 61 title, msg string 62 percent float64 63 } 64 65 func (s State) String() string { 66 var b strings.Builder 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] + "..." 72 } 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") 76 } 77 b.WriteString("\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) 83 } 84 } 85 b.WriteString("\n") 86 b.WriteString("#### outstanding work:\n") 87 for token, state := range s.outstandingWork { 88 name := state.title 89 if name == "" { 90 name = fmt.Sprintf("!NO NAME(token: %s)", token) 91 } 92 fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent) 93 } 94 b.WriteString("#### completed work:\n") 95 for name, count := range s.completedWork { 96 fmt.Fprintf(&b, "\t%s: %d\n", name, count) 97 } 98 return b.String() 99 } 100 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 106 verdict chan Verdict 107 } 108 109 // NewEnv creates a new test environment using the given scratch environment 110 // and gopls server. 111 func NewEnv(ctx context.Context, t *testing.T, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) *Env { 112 t.Helper() 113 conn := ts.Connect(ctx) 114 env := &Env{ 115 T: t, 116 Ctx: ctx, 117 Sandbox: sandbox, 118 Server: ts, 119 state: State{ 120 diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), 121 outstandingWork: make(map[protocol.ProgressToken]*workProgress), 122 completedWork: make(map[string]uint64), 123 }, 124 waiters: make(map[int]*condition), 125 } 126 var hooks fake.ClientHooks 127 if withHooks { 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, 137 } 138 } 139 editor, err := fake.NewEditor(sandbox, editorConfig).Connect(ctx, conn, hooks) 140 if err != nil { 141 t.Fatal(err) 142 } 143 env.Editor = editor 144 return env 145 } 146 147 func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { 148 e.mu.Lock() 149 defer e.mu.Unlock() 150 151 pth := e.Sandbox.Workdir.URIToPath(d.URI) 152 e.state.diagnostics[pth] = d 153 e.checkConditionsLocked() 154 return nil 155 } 156 157 func (e *Env) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { 158 e.mu.Lock() 159 defer e.mu.Unlock() 160 161 e.state.showMessage = append(e.state.showMessage, m) 162 e.checkConditionsLocked() 163 return nil 164 } 165 166 func (e *Env) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { 167 e.mu.Lock() 168 defer e.mu.Unlock() 169 170 e.state.showMessageRequest = append(e.state.showMessageRequest, m) 171 e.checkConditionsLocked() 172 return nil 173 } 174 175 func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { 176 e.mu.Lock() 177 defer e.mu.Unlock() 178 179 e.state.logs = append(e.state.logs, m) 180 e.checkConditionsLocked() 181 return nil 182 } 183 184 func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { 185 e.mu.Lock() 186 defer e.mu.Unlock() 187 188 e.state.outstandingWork[m.Token] = &workProgress{} 189 return nil 190 } 191 192 func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { 193 e.mu.Lock() 194 defer e.mu.Unlock() 195 work, ok := e.state.outstandingWork[m.Token] 196 if !ok { 197 panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) 198 } 199 v := m.Value.(map[string]interface{}) 200 switch kind := v["kind"]; kind { 201 case "begin": 202 work.title = v["title"].(string) 203 if msg, ok := v["message"]; ok { 204 work.msg = msg.(string) 205 } 206 case "report": 207 if pct, ok := v["percentage"]; ok { 208 work.percent = pct.(float64) 209 } 210 if msg, ok := v["message"]; ok { 211 work.msg = msg.(string) 212 } 213 case "end": 214 title := e.state.outstandingWork[m.Token].title 215 e.state.completedWork[title] = e.state.completedWork[title] + 1 216 delete(e.state.outstandingWork, m.Token) 217 } 218 e.checkConditionsLocked() 219 return nil 220 } 221 222 func (e *Env) onRegistration(_ context.Context, m *protocol.RegistrationParams) error { 223 e.mu.Lock() 224 defer e.mu.Unlock() 225 226 e.state.registrations = append(e.state.registrations, m) 227 e.checkConditionsLocked() 228 return nil 229 } 230 231 func (e *Env) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error { 232 e.mu.Lock() 233 defer e.mu.Unlock() 234 235 e.state.unregistrations = append(e.state.unregistrations, m) 236 e.checkConditionsLocked() 237 return nil 238 } 239 240 func (e *Env) checkConditionsLocked() { 241 for id, condition := range e.waiters { 242 if v, _ := checkExpectations(e.state, condition.expectations); v != Unmet { 243 delete(e.waiters, id) 244 condition.verdict <- v 245 } 246 } 247 } 248 249 // checkExpectations reports whether s meets all expectations. 250 func checkExpectations(s State, expectations []Expectation) (Verdict, string) { 251 finalVerdict := Met 252 var summary strings.Builder 253 for _, e := range expectations { 254 v := e.Check(s) 255 if v > finalVerdict { 256 finalVerdict = v 257 } 258 summary.WriteString(fmt.Sprintf("\t%v: %s\n", v, e.Description())) 259 } 260 return finalVerdict, summary.String() 261 } 262 263 // DiagnosticsFor returns the current diagnostics for the file. It is useful 264 // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic 265 // is not simply described by DiagnosticAt. 266 func (e *Env) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { 267 e.mu.Lock() 268 defer e.mu.Unlock() 269 return e.state.diagnostics[name] 270 } 271 272 // Await waits for all expectations to simultaneously be met. It should only be 273 // called from the main test goroutine. 274 func (e *Env) Await(expectations ...Expectation) { 275 e.T.Helper() 276 e.mu.Lock() 277 // Before adding the waiter, we check if the condition is currently met or 278 // failed to avoid a race where the condition was realized before Await was 279 // called. 280 switch verdict, summary := checkExpectations(e.state, expectations); verdict { 281 case Met: 282 e.mu.Unlock() 283 return 284 case Unmeetable: 285 failure := fmt.Sprintf("unmeetable expectations:\n%s\nstate:\n%v", summary, e.state) 286 e.mu.Unlock() 287 e.T.Fatal(failure) 288 } 289 cond := &condition{ 290 expectations: expectations, 291 verdict: make(chan Verdict), 292 } 293 e.waiters[e.nextWaiterID] = cond 294 e.nextWaiterID++ 295 e.mu.Unlock() 296 297 var err error 298 select { 299 case <-e.Ctx.Done(): 300 err = e.Ctx.Err() 301 case v := <-cond.verdict: 302 if v != Met { 303 err = fmt.Errorf("condition has final verdict %v", v) 304 } 305 } 306 e.mu.Lock() 307 defer e.mu.Unlock() 308 _, summary := checkExpectations(e.state, expectations) 309 310 // Debugging an unmet expectation can be tricky, so we put some effort into 311 // nicely formatting the failure. 312 if err != nil { 313 e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state) 314 } 315 }