github.com/v2fly/tools@v0.100.0/internal/lsp/regtest/expectation.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 "fmt" 9 "regexp" 10 "strings" 11 12 "github.com/v2fly/tools/internal/lsp" 13 "github.com/v2fly/tools/internal/lsp/fake" 14 "github.com/v2fly/tools/internal/lsp/protocol" 15 "github.com/v2fly/tools/internal/testenv" 16 ) 17 18 // An Expectation asserts that the state of the editor at a point in time 19 // matches an expected condition. This is used for signaling in tests when 20 // certain conditions in the editor are met. 21 type Expectation interface { 22 // Check determines whether the state of the editor satisfies the 23 // expectation, returning the results that met the condition. 24 Check(State) Verdict 25 // Description is a human-readable description of the expectation. 26 Description() string 27 } 28 29 var ( 30 // InitialWorkspaceLoad is an expectation that the workspace initial load has 31 // completed. It is verified via workdone reporting. 32 InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1) 33 ) 34 35 // A Verdict is the result of checking an expectation against the current 36 // editor state. 37 type Verdict int 38 39 // Order matters for the following constants: verdicts are sorted in order of 40 // decisiveness. 41 const ( 42 // Met indicates that an expectation is satisfied by the current state. 43 Met Verdict = iota 44 // Unmet indicates that an expectation is not currently met, but could be met 45 // in the future. 46 Unmet 47 // Unmeetable indicates that an expectation cannot be satisfied in the 48 // future. 49 Unmeetable 50 ) 51 52 func (v Verdict) String() string { 53 switch v { 54 case Met: 55 return "Met" 56 case Unmet: 57 return "Unmet" 58 case Unmeetable: 59 return "Unmeetable" 60 } 61 return fmt.Sprintf("unrecognized verdict %d", v) 62 } 63 64 // SimpleExpectation holds an arbitrary check func, and implements the Expectation interface. 65 type SimpleExpectation struct { 66 check func(State) Verdict 67 description string 68 } 69 70 // Check invokes e.check. 71 func (e SimpleExpectation) Check(s State) Verdict { 72 return e.check(s) 73 } 74 75 // Description returns e.descriptin. 76 func (e SimpleExpectation) Description() string { 77 return e.description 78 } 79 80 // OnceMet returns an Expectation that, once the precondition is met, asserts 81 // that mustMeet is met. 82 func OnceMet(precondition Expectation, mustMeet Expectation) *SimpleExpectation { 83 check := func(s State) Verdict { 84 switch pre := precondition.Check(s); pre { 85 case Unmeetable: 86 return Unmeetable 87 case Met: 88 verdict := mustMeet.Check(s) 89 if verdict != Met { 90 return Unmeetable 91 } 92 return Met 93 default: 94 return Unmet 95 } 96 } 97 return &SimpleExpectation{ 98 check: check, 99 description: fmt.Sprintf("once %q is met, must have %q", precondition.Description(), mustMeet.Description()), 100 } 101 } 102 103 // ReadDiagnostics is an 'expectation' that is used to read diagnostics 104 // atomically. It is intended to be used with 'OnceMet'. 105 func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation { 106 check := func(s State) Verdict { 107 diags, ok := s.diagnostics[fileName] 108 if !ok { 109 return Unmeetable 110 } 111 *into = *diags 112 return Met 113 } 114 return &SimpleExpectation{ 115 check: check, 116 description: fmt.Sprintf("read diagnostics for %q", fileName), 117 } 118 } 119 120 // NoOutstandingWork asserts that there is no work initiated using the LSP 121 // $/progress API that has not completed. 122 func NoOutstandingWork() SimpleExpectation { 123 check := func(s State) Verdict { 124 if len(s.outstandingWork) == 0 { 125 return Met 126 } 127 return Unmet 128 } 129 return SimpleExpectation{ 130 check: check, 131 description: "no outstanding work", 132 } 133 } 134 135 // NoShowMessage asserts that the editor has not received a ShowMessage. 136 func NoShowMessage() SimpleExpectation { 137 check := func(s State) Verdict { 138 if len(s.showMessage) == 0 { 139 return Met 140 } 141 return Unmeetable 142 } 143 return SimpleExpectation{ 144 check: check, 145 description: "no ShowMessage received", 146 } 147 } 148 149 // ShownMessage asserts that the editor has received a ShownMessage with the 150 // given title. 151 func ShownMessage(title string) SimpleExpectation { 152 check := func(s State) Verdict { 153 for _, m := range s.showMessage { 154 if strings.Contains(m.Message, title) { 155 return Met 156 } 157 } 158 return Unmet 159 } 160 return SimpleExpectation{ 161 check: check, 162 description: "received ShowMessage", 163 } 164 } 165 166 // ShowMessageRequest asserts that the editor has received a ShowMessageRequest 167 // with an action item that has the given title. 168 func ShowMessageRequest(title string) SimpleExpectation { 169 check := func(s State) Verdict { 170 if len(s.showMessageRequest) == 0 { 171 return Unmet 172 } 173 // Only check the most recent one. 174 m := s.showMessageRequest[len(s.showMessageRequest)-1] 175 if len(m.Actions) == 0 || len(m.Actions) > 1 { 176 return Unmet 177 } 178 if m.Actions[0].Title == title { 179 return Met 180 } 181 return Unmet 182 } 183 return SimpleExpectation{ 184 check: check, 185 description: "received ShowMessageRequest", 186 } 187 } 188 189 // DoneWithOpen expects all didOpen notifications currently sent by the editor 190 // to be completely processed. 191 func (e *Env) DoneWithOpen() Expectation { 192 opens := e.Editor.Stats().DidOpen 193 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens) 194 } 195 196 // DoneWithChange expects all didChange notifications currently sent by the 197 // editor to be completely processed. 198 func (e *Env) DoneWithChange() Expectation { 199 changes := e.Editor.Stats().DidChange 200 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) 201 } 202 203 // DoneWithSave expects all didSave notifications currently sent by the editor 204 // to be completely processed. 205 func (e *Env) DoneWithSave() Expectation { 206 saves := e.Editor.Stats().DidSave 207 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves) 208 } 209 210 // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications 211 // currently sent by the editor to be completely processed. 212 func (e *Env) DoneWithChangeWatchedFiles() Expectation { 213 changes := e.Editor.Stats().DidChangeWatchedFiles 214 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes) 215 } 216 217 // DoneWithClose expects all didClose notifications currently sent by the 218 // editor to be completely processed. 219 func (e *Env) DoneWithClose() Expectation { 220 changes := e.Editor.Stats().DidClose 221 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes) 222 } 223 224 // CompletedWork expects a work item to have been completed >= atLeast times. 225 // 226 // Since the Progress API doesn't include any hidden metadata, we must use the 227 // progress notification title to identify the work we expect to be completed. 228 func CompletedWork(title string, atLeast uint64) SimpleExpectation { 229 check := func(s State) Verdict { 230 if s.completedWork[title] >= atLeast { 231 return Met 232 } 233 return Unmet 234 } 235 return SimpleExpectation{ 236 check: check, 237 description: fmt.Sprintf("completed work %q at least %d time(s)", title, atLeast), 238 } 239 } 240 241 // OutstandingWork expects a work item to be outstanding. The given title must 242 // be an exact match, whereas the given msg must only be contained in the work 243 // item's message. 244 func OutstandingWork(title, msg string) SimpleExpectation { 245 check := func(s State) Verdict { 246 for _, work := range s.outstandingWork { 247 if work.title == title && strings.Contains(work.msg, msg) { 248 return Met 249 } 250 } 251 return Unmet 252 } 253 return SimpleExpectation{ 254 check: check, 255 description: fmt.Sprintf("outstanding work: %s", title), 256 } 257 } 258 259 // LogExpectation is an expectation on the log messages received by the editor 260 // from gopls. 261 type LogExpectation struct { 262 check func([]*protocol.LogMessageParams) Verdict 263 description string 264 } 265 266 // Check implements the Expectation interface. 267 func (e LogExpectation) Check(s State) Verdict { 268 return e.check(s.logs) 269 } 270 271 // Description implements the Expectation interface. 272 func (e LogExpectation) Description() string { 273 return e.description 274 } 275 276 // NoErrorLogs asserts that the client has not received any log messages of 277 // error severity. 278 func NoErrorLogs() LogExpectation { 279 return NoLogMatching(protocol.Error, "") 280 } 281 282 // LogMatching asserts that the client has received a log message 283 // of type typ matching the regexp re. 284 func LogMatching(typ protocol.MessageType, re string, count int) LogExpectation { 285 rec, err := regexp.Compile(re) 286 if err != nil { 287 panic(err) 288 } 289 check := func(msgs []*protocol.LogMessageParams) Verdict { 290 var found int 291 for _, msg := range msgs { 292 if msg.Type == typ && rec.Match([]byte(msg.Message)) { 293 found++ 294 } 295 } 296 if found == count { 297 return Met 298 } 299 return Unmet 300 } 301 return LogExpectation{ 302 check: check, 303 description: fmt.Sprintf("log message matching %q", re), 304 } 305 } 306 307 // NoLogMatching asserts that the client has not received a log message 308 // of type typ matching the regexp re. If re is an empty string, any log 309 // message is considered a match. 310 func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { 311 var r *regexp.Regexp 312 if re != "" { 313 var err error 314 r, err = regexp.Compile(re) 315 if err != nil { 316 panic(err) 317 } 318 } 319 check := func(msgs []*protocol.LogMessageParams) Verdict { 320 for _, msg := range msgs { 321 if msg.Type != typ { 322 continue 323 } 324 if r == nil || r.Match([]byte(msg.Message)) { 325 return Unmeetable 326 } 327 } 328 return Met 329 } 330 return LogExpectation{ 331 check: check, 332 description: fmt.Sprintf("no log message matching %q", re), 333 } 334 } 335 336 // RegistrationExpectation is an expectation on the capability registrations 337 // received by the editor from gopls. 338 type RegistrationExpectation struct { 339 check func([]*protocol.RegistrationParams) Verdict 340 description string 341 } 342 343 // Check implements the Expectation interface. 344 func (e RegistrationExpectation) Check(s State) Verdict { 345 return e.check(s.registrations) 346 } 347 348 // Description implements the Expectation interface. 349 func (e RegistrationExpectation) Description() string { 350 return e.description 351 } 352 353 // RegistrationMatching asserts that the client has received a capability 354 // registration matching the given regexp. 355 func RegistrationMatching(re string) RegistrationExpectation { 356 rec, err := regexp.Compile(re) 357 if err != nil { 358 panic(err) 359 } 360 check := func(params []*protocol.RegistrationParams) Verdict { 361 for _, p := range params { 362 for _, r := range p.Registrations { 363 if rec.Match([]byte(r.Method)) { 364 return Met 365 } 366 } 367 } 368 return Unmet 369 } 370 return RegistrationExpectation{ 371 check: check, 372 description: fmt.Sprintf("registration matching %q", re), 373 } 374 } 375 376 // UnregistrationExpectation is an expectation on the capability 377 // unregistrations received by the editor from gopls. 378 type UnregistrationExpectation struct { 379 check func([]*protocol.UnregistrationParams) Verdict 380 description string 381 } 382 383 // Check implements the Expectation interface. 384 func (e UnregistrationExpectation) Check(s State) Verdict { 385 return e.check(s.unregistrations) 386 } 387 388 // Description implements the Expectation interface. 389 func (e UnregistrationExpectation) Description() string { 390 return e.description 391 } 392 393 // UnregistrationMatching asserts that the client has received an 394 // unregistration whose ID matches the given regexp. 395 func UnregistrationMatching(re string) UnregistrationExpectation { 396 rec, err := regexp.Compile(re) 397 if err != nil { 398 panic(err) 399 } 400 check := func(params []*protocol.UnregistrationParams) Verdict { 401 for _, p := range params { 402 for _, r := range p.Unregisterations { 403 if rec.Match([]byte(r.Method)) { 404 return Met 405 } 406 } 407 } 408 return Unmet 409 } 410 return UnregistrationExpectation{ 411 check: check, 412 description: fmt.Sprintf("unregistration matching %q", re), 413 } 414 } 415 416 // A DiagnosticExpectation is a condition that must be met by the current set 417 // of diagnostics for a file. 418 type DiagnosticExpectation struct { 419 // optionally, the position of the diagnostic and the regex used to calculate it. 420 pos *fake.Pos 421 re string 422 423 // optionally, the message that the diagnostic should contain. 424 message string 425 426 // whether the expectation is that the diagnostic is present, or absent. 427 present bool 428 429 // path is the scratch workdir-relative path to the file being asserted on. 430 path string 431 } 432 433 // Check implements the Expectation interface. 434 func (e DiagnosticExpectation) Check(s State) Verdict { 435 diags, ok := s.diagnostics[e.path] 436 if !ok { 437 if !e.present { 438 return Met 439 } 440 return Unmet 441 } 442 443 found := false 444 for _, d := range diags.Diagnostics { 445 if e.pos != nil { 446 if d.Range.Start.Line != uint32(e.pos.Line) || d.Range.Start.Character != uint32(e.pos.Column) { 447 continue 448 } 449 } 450 if e.message != "" { 451 if !strings.Contains(d.Message, e.message) { 452 continue 453 } 454 } 455 found = true 456 break 457 } 458 459 if found == e.present { 460 return Met 461 } 462 return Unmet 463 } 464 465 // Description implements the Expectation interface. 466 func (e DiagnosticExpectation) Description() string { 467 desc := e.path + ":" 468 if !e.present { 469 desc += " no" 470 } 471 desc += " diagnostic" 472 if e.pos != nil { 473 desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Column) 474 if e.re != "" { 475 desc += fmt.Sprintf(" (location of %q)", e.re) 476 } 477 } 478 if e.message != "" { 479 desc += fmt.Sprintf(" with message %q", e.message) 480 } 481 return desc 482 } 483 484 // EmptyDiagnostics asserts that empty diagnostics are sent for the 485 // workspace-relative path name. 486 func EmptyDiagnostics(name string) Expectation { 487 check := func(s State) Verdict { 488 if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 { 489 return Met 490 } 491 return Unmet 492 } 493 return SimpleExpectation{ 494 check: check, 495 description: "empty diagnostics", 496 } 497 } 498 499 // NoDiagnostics asserts that no diagnostics are sent for the 500 // workspace-relative path name. It should be used primarily in conjunction 501 // with a OnceMet, as it has to check that all outstanding diagnostics have 502 // already been delivered. 503 func NoDiagnostics(name string) Expectation { 504 check := func(s State) Verdict { 505 if _, ok := s.diagnostics[name]; !ok { 506 return Met 507 } 508 return Unmet 509 } 510 return SimpleExpectation{ 511 check: check, 512 description: "no diagnostics", 513 } 514 } 515 516 // AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for 517 // the current edited version of the buffer corresponding to the given 518 // workdir-relative pathname. 519 func (e *Env) AnyDiagnosticAtCurrentVersion(name string) Expectation { 520 version := e.Editor.BufferVersion(name) 521 check := func(s State) Verdict { 522 diags, ok := s.diagnostics[name] 523 if ok && diags.Version == int32(version) { 524 return Met 525 } 526 return Unmet 527 } 528 return SimpleExpectation{ 529 check: check, 530 description: fmt.Sprintf("any diagnostics at version %d", version), 531 } 532 } 533 534 // DiagnosticAtRegexp expects that there is a diagnostic entry at the start 535 // position matching the regexp search string re in the buffer specified by 536 // name. Note that this currently ignores the end position. 537 func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation { 538 e.T.Helper() 539 pos := e.RegexpSearch(name, re) 540 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true} 541 } 542 543 // DiagnosticAtRegexpWithMessage is like DiagnosticAtRegexp, but it also 544 // checks for the content of the diagnostic message, 545 func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpectation { 546 e.T.Helper() 547 pos := e.RegexpSearch(name, re) 548 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg} 549 } 550 551 // DiagnosticAt asserts that there is a diagnostic entry at the position 552 // specified by line and col, for the workdir-relative path name. 553 func DiagnosticAt(name string, line, col int) DiagnosticExpectation { 554 return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true} 555 } 556 557 // NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start 558 // position matching the regexp search string re in the buffer specified by 559 // name. Note that this currently ignores the end position. 560 // This should only be used in combination with OnceMet for a given condition, 561 // otherwise it may always succeed. 562 func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { 563 e.T.Helper() 564 pos := e.RegexpSearch(name, re) 565 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false} 566 } 567 568 // NoDiagnosticAt asserts that there is no diagnostic entry at the position 569 // specified by line and col, for the workdir-relative path name. 570 // This should only be used in combination with OnceMet for a given condition, 571 // otherwise it may always succeed. 572 func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation { 573 return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: false} 574 } 575 576 // NoDiagnosticWithMessage asserts that there is no diagnostic entry with the 577 // given message. 578 // 579 // This should only be used in combination with OnceMet for a given condition, 580 // otherwise it may always succeed. 581 func NoDiagnosticWithMessage(name, msg string) DiagnosticExpectation { 582 return DiagnosticExpectation{path: name, message: msg, present: false} 583 } 584 585 // GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the 586 // given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is 587 // present. 588 func (e *Env) GoSumDiagnostic(name, module string) Expectation { 589 e.T.Helper() 590 // In 1.16, go.sum diagnostics should appear on the relevant module. Earlier 591 // errors have no information and appear on the module declaration. 592 if testenv.Go1Point() >= 16 { 593 return e.DiagnosticAtRegexpWithMessage(name, module, "go.sum is out of sync") 594 } else { 595 return e.DiagnosticAtRegexpWithMessage(name, `module`, "go.sum is out of sync") 596 } 597 }