github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/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/jhump/golang-x-tools/internal/lsp" 13 "github.com/jhump/golang-x-tools/internal/lsp/fake" 14 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 15 "github.com/jhump/golang-x-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, false) 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, mustMeets ...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 for _, mustMeet := range mustMeets { 89 verdict := mustMeet.Check(s) 90 if verdict != Met { 91 return Unmeetable 92 } 93 } 94 return Met 95 default: 96 return Unmet 97 } 98 } 99 var descriptions []string 100 for _, mustMeet := range mustMeets { 101 descriptions = append(descriptions, mustMeet.Description()) 102 } 103 return &SimpleExpectation{ 104 check: check, 105 description: fmt.Sprintf("once %q is met, must have %q", precondition.Description(), strings.Join(descriptions, "\n")), 106 } 107 } 108 109 // ReadDiagnostics is an 'expectation' that is used to read diagnostics 110 // atomically. It is intended to be used with 'OnceMet'. 111 func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation { 112 check := func(s State) Verdict { 113 diags, ok := s.diagnostics[fileName] 114 if !ok { 115 return Unmeetable 116 } 117 *into = *diags 118 return Met 119 } 120 return &SimpleExpectation{ 121 check: check, 122 description: fmt.Sprintf("read diagnostics for %q", fileName), 123 } 124 } 125 126 // NoOutstandingWork asserts that there is no work initiated using the LSP 127 // $/progress API that has not completed. 128 func NoOutstandingWork() SimpleExpectation { 129 check := func(s State) Verdict { 130 if len(s.outstandingWork) == 0 { 131 return Met 132 } 133 return Unmet 134 } 135 return SimpleExpectation{ 136 check: check, 137 description: "no outstanding work", 138 } 139 } 140 141 // NoShowMessage asserts that the editor has not received a ShowMessage. 142 func NoShowMessage() SimpleExpectation { 143 check := func(s State) Verdict { 144 if len(s.showMessage) == 0 { 145 return Met 146 } 147 return Unmeetable 148 } 149 return SimpleExpectation{ 150 check: check, 151 description: "no ShowMessage received", 152 } 153 } 154 155 // ShownMessage asserts that the editor has received a ShownMessage with the 156 // given title. 157 func ShownMessage(title string) SimpleExpectation { 158 check := func(s State) Verdict { 159 for _, m := range s.showMessage { 160 if strings.Contains(m.Message, title) { 161 return Met 162 } 163 } 164 return Unmet 165 } 166 return SimpleExpectation{ 167 check: check, 168 description: "received ShowMessage", 169 } 170 } 171 172 // ShowMessageRequest asserts that the editor has received a ShowMessageRequest 173 // with an action item that has the given title. 174 func ShowMessageRequest(title string) SimpleExpectation { 175 check := func(s State) Verdict { 176 if len(s.showMessageRequest) == 0 { 177 return Unmet 178 } 179 // Only check the most recent one. 180 m := s.showMessageRequest[len(s.showMessageRequest)-1] 181 if len(m.Actions) == 0 || len(m.Actions) > 1 { 182 return Unmet 183 } 184 if m.Actions[0].Title == title { 185 return Met 186 } 187 return Unmet 188 } 189 return SimpleExpectation{ 190 check: check, 191 description: "received ShowMessageRequest", 192 } 193 } 194 195 // DoneWithOpen expects all didOpen notifications currently sent by the editor 196 // to be completely processed. 197 func (e *Env) DoneWithOpen() Expectation { 198 opens := e.Editor.Stats().DidOpen 199 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true) 200 } 201 202 // StartedChange expects there to have been i work items started for 203 // processing didChange notifications. 204 func StartedChange(i uint64) Expectation { 205 return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), i) 206 } 207 208 // DoneWithChange expects all didChange notifications currently sent by the 209 // editor to be completely processed. 210 func (e *Env) DoneWithChange() Expectation { 211 changes := e.Editor.Stats().DidChange 212 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes, true) 213 } 214 215 // DoneWithSave expects all didSave notifications currently sent by the editor 216 // to be completely processed. 217 func (e *Env) DoneWithSave() Expectation { 218 saves := e.Editor.Stats().DidSave 219 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true) 220 } 221 222 // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications 223 // currently sent by the editor to be completely processed. 224 func (e *Env) DoneWithChangeWatchedFiles() Expectation { 225 changes := e.Editor.Stats().DidChangeWatchedFiles 226 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes, true) 227 } 228 229 // DoneWithClose expects all didClose notifications currently sent by the 230 // editor to be completely processed. 231 func (e *Env) DoneWithClose() Expectation { 232 changes := e.Editor.Stats().DidClose 233 return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes, true) 234 } 235 236 // StartedWork expect a work item to have been started >= atLeast times. 237 // 238 // See CompletedWork. 239 func StartedWork(title string, atLeast uint64) SimpleExpectation { 240 check := func(s State) Verdict { 241 if s.startedWork[title] >= atLeast { 242 return Met 243 } 244 return Unmet 245 } 246 return SimpleExpectation{ 247 check: check, 248 description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), 249 } 250 } 251 252 // CompletedWork expects a work item to have been completed >= atLeast times. 253 // 254 // Since the Progress API doesn't include any hidden metadata, we must use the 255 // progress notification title to identify the work we expect to be completed. 256 func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { 257 check := func(s State) Verdict { 258 if s.completedWork[title] == count || atLeast && s.completedWork[title] > count { 259 return Met 260 } 261 return Unmet 262 } 263 desc := fmt.Sprintf("completed work %q %v times", title, count) 264 if atLeast { 265 desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) 266 } 267 return SimpleExpectation{ 268 check: check, 269 description: desc, 270 } 271 } 272 273 // OutstandingWork expects a work item to be outstanding. The given title must 274 // be an exact match, whereas the given msg must only be contained in the work 275 // item's message. 276 func OutstandingWork(title, msg string) SimpleExpectation { 277 check := func(s State) Verdict { 278 for _, work := range s.outstandingWork { 279 if work.title == title && strings.Contains(work.msg, msg) { 280 return Met 281 } 282 } 283 return Unmet 284 } 285 return SimpleExpectation{ 286 check: check, 287 description: fmt.Sprintf("outstanding work: %s", title), 288 } 289 } 290 291 // LogExpectation is an expectation on the log messages received by the editor 292 // from gopls. 293 type LogExpectation struct { 294 check func([]*protocol.LogMessageParams) Verdict 295 description string 296 } 297 298 // Check implements the Expectation interface. 299 func (e LogExpectation) Check(s State) Verdict { 300 return e.check(s.logs) 301 } 302 303 // Description implements the Expectation interface. 304 func (e LogExpectation) Description() string { 305 return e.description 306 } 307 308 // NoErrorLogs asserts that the client has not received any log messages of 309 // error severity. 310 func NoErrorLogs() LogExpectation { 311 return NoLogMatching(protocol.Error, "") 312 } 313 314 // LogMatching asserts that the client has received a log message 315 // of type typ matching the regexp re. 316 func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) LogExpectation { 317 rec, err := regexp.Compile(re) 318 if err != nil { 319 panic(err) 320 } 321 check := func(msgs []*protocol.LogMessageParams) Verdict { 322 var found int 323 for _, msg := range msgs { 324 if msg.Type == typ && rec.Match([]byte(msg.Message)) { 325 found++ 326 } 327 } 328 // Check for an exact or "at least" match. 329 if found == count || (found >= count && atLeast) { 330 return Met 331 } 332 return Unmet 333 } 334 desc := fmt.Sprintf("log message matching %q expected %v times", re, count) 335 if atLeast { 336 desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) 337 } 338 return LogExpectation{ 339 check: check, 340 description: desc, 341 } 342 } 343 344 // NoLogMatching asserts that the client has not received a log message 345 // of type typ matching the regexp re. If re is an empty string, any log 346 // message is considered a match. 347 func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { 348 var r *regexp.Regexp 349 if re != "" { 350 var err error 351 r, err = regexp.Compile(re) 352 if err != nil { 353 panic(err) 354 } 355 } 356 check := func(msgs []*protocol.LogMessageParams) Verdict { 357 for _, msg := range msgs { 358 if msg.Type != typ { 359 continue 360 } 361 if r == nil || r.Match([]byte(msg.Message)) { 362 return Unmeetable 363 } 364 } 365 return Met 366 } 367 return LogExpectation{ 368 check: check, 369 description: fmt.Sprintf("no log message matching %q", re), 370 } 371 } 372 373 // RegistrationExpectation is an expectation on the capability registrations 374 // received by the editor from gopls. 375 type RegistrationExpectation struct { 376 check func([]*protocol.RegistrationParams) Verdict 377 description string 378 } 379 380 // Check implements the Expectation interface. 381 func (e RegistrationExpectation) Check(s State) Verdict { 382 return e.check(s.registrations) 383 } 384 385 // Description implements the Expectation interface. 386 func (e RegistrationExpectation) Description() string { 387 return e.description 388 } 389 390 // RegistrationMatching asserts that the client has received a capability 391 // registration matching the given regexp. 392 func RegistrationMatching(re string) RegistrationExpectation { 393 rec, err := regexp.Compile(re) 394 if err != nil { 395 panic(err) 396 } 397 check := func(params []*protocol.RegistrationParams) Verdict { 398 for _, p := range params { 399 for _, r := range p.Registrations { 400 if rec.Match([]byte(r.Method)) { 401 return Met 402 } 403 } 404 } 405 return Unmet 406 } 407 return RegistrationExpectation{ 408 check: check, 409 description: fmt.Sprintf("registration matching %q", re), 410 } 411 } 412 413 // UnregistrationExpectation is an expectation on the capability 414 // unregistrations received by the editor from gopls. 415 type UnregistrationExpectation struct { 416 check func([]*protocol.UnregistrationParams) Verdict 417 description string 418 } 419 420 // Check implements the Expectation interface. 421 func (e UnregistrationExpectation) Check(s State) Verdict { 422 return e.check(s.unregistrations) 423 } 424 425 // Description implements the Expectation interface. 426 func (e UnregistrationExpectation) Description() string { 427 return e.description 428 } 429 430 // UnregistrationMatching asserts that the client has received an 431 // unregistration whose ID matches the given regexp. 432 func UnregistrationMatching(re string) UnregistrationExpectation { 433 rec, err := regexp.Compile(re) 434 if err != nil { 435 panic(err) 436 } 437 check := func(params []*protocol.UnregistrationParams) Verdict { 438 for _, p := range params { 439 for _, r := range p.Unregisterations { 440 if rec.Match([]byte(r.Method)) { 441 return Met 442 } 443 } 444 } 445 return Unmet 446 } 447 return UnregistrationExpectation{ 448 check: check, 449 description: fmt.Sprintf("unregistration matching %q", re), 450 } 451 } 452 453 // A DiagnosticExpectation is a condition that must be met by the current set 454 // of diagnostics for a file. 455 type DiagnosticExpectation struct { 456 // optionally, the position of the diagnostic and the regex used to calculate it. 457 pos *fake.Pos 458 re string 459 460 // optionally, the message that the diagnostic should contain. 461 message string 462 463 // whether the expectation is that the diagnostic is present, or absent. 464 present bool 465 466 // path is the scratch workdir-relative path to the file being asserted on. 467 path string 468 } 469 470 // Check implements the Expectation interface. 471 func (e DiagnosticExpectation) Check(s State) Verdict { 472 diags, ok := s.diagnostics[e.path] 473 if !ok { 474 if !e.present { 475 return Met 476 } 477 return Unmet 478 } 479 480 found := false 481 for _, d := range diags.Diagnostics { 482 if e.pos != nil { 483 if d.Range.Start.Line != uint32(e.pos.Line) || d.Range.Start.Character != uint32(e.pos.Column) { 484 continue 485 } 486 } 487 if e.message != "" { 488 if !strings.Contains(d.Message, e.message) { 489 continue 490 } 491 } 492 found = true 493 break 494 } 495 496 if found == e.present { 497 return Met 498 } 499 return Unmet 500 } 501 502 // Description implements the Expectation interface. 503 func (e DiagnosticExpectation) Description() string { 504 desc := e.path + ":" 505 if !e.present { 506 desc += " no" 507 } 508 desc += " diagnostic" 509 if e.pos != nil { 510 desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Column) 511 if e.re != "" { 512 desc += fmt.Sprintf(" (location of %q)", e.re) 513 } 514 } 515 if e.message != "" { 516 desc += fmt.Sprintf(" with message %q", e.message) 517 } 518 return desc 519 } 520 521 // NoOutstandingDiagnostics asserts that the workspace has no outstanding 522 // diagnostic messages. 523 func NoOutstandingDiagnostics() Expectation { 524 check := func(s State) Verdict { 525 for _, diags := range s.diagnostics { 526 if len(diags.Diagnostics) > 0 { 527 return Unmet 528 } 529 } 530 return Met 531 } 532 return SimpleExpectation{ 533 check: check, 534 description: "no outstanding diagnostics", 535 } 536 } 537 538 // EmptyDiagnostics asserts that empty diagnostics are sent for the 539 // workspace-relative path name. 540 func EmptyDiagnostics(name string) Expectation { 541 check := func(s State) Verdict { 542 if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 { 543 return Met 544 } 545 return Unmet 546 } 547 return SimpleExpectation{ 548 check: check, 549 description: fmt.Sprintf("empty diagnostics for %q", name), 550 } 551 } 552 553 // EmptyOrNoDiagnostics asserts that either no diagnostics are sent for the 554 // workspace-relative path name, or empty diagnostics are sent. 555 // TODO(rFindley): this subtlety shouldn't be necessary. Gopls should always 556 // send at least one diagnostic set for open files. 557 func EmptyOrNoDiagnostics(name string) Expectation { 558 check := func(s State) Verdict { 559 if diags := s.diagnostics[name]; diags == nil || len(diags.Diagnostics) == 0 { 560 return Met 561 } 562 return Unmet 563 } 564 return SimpleExpectation{ 565 check: check, 566 description: fmt.Sprintf("empty or no diagnostics for %q", name), 567 } 568 } 569 570 // NoDiagnostics asserts that no diagnostics are sent for the 571 // workspace-relative path name. It should be used primarily in conjunction 572 // with a OnceMet, as it has to check that all outstanding diagnostics have 573 // already been delivered. 574 func NoDiagnostics(name string) Expectation { 575 check := func(s State) Verdict { 576 if _, ok := s.diagnostics[name]; !ok { 577 return Met 578 } 579 return Unmet 580 } 581 return SimpleExpectation{ 582 check: check, 583 description: "no diagnostics", 584 } 585 } 586 587 // AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for 588 // the current edited version of the buffer corresponding to the given 589 // workdir-relative pathname. 590 func (e *Env) AnyDiagnosticAtCurrentVersion(name string) Expectation { 591 version := e.Editor.BufferVersion(name) 592 check := func(s State) Verdict { 593 diags, ok := s.diagnostics[name] 594 if ok && diags.Version == int32(version) { 595 return Met 596 } 597 return Unmet 598 } 599 return SimpleExpectation{ 600 check: check, 601 description: fmt.Sprintf("any diagnostics at version %d", version), 602 } 603 } 604 605 // DiagnosticAtRegexp expects that there is a diagnostic entry at the start 606 // position matching the regexp search string re in the buffer specified by 607 // name. Note that this currently ignores the end position. 608 func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation { 609 e.T.Helper() 610 pos := e.RegexpSearch(name, re) 611 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true} 612 } 613 614 // DiagnosticAtRegexpWithMessage is like DiagnosticAtRegexp, but it also 615 // checks for the content of the diagnostic message, 616 func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpectation { 617 e.T.Helper() 618 pos := e.RegexpSearch(name, re) 619 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg} 620 } 621 622 // DiagnosticAt asserts that there is a diagnostic entry at the position 623 // specified by line and col, for the workdir-relative path name. 624 func DiagnosticAt(name string, line, col int) DiagnosticExpectation { 625 return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true} 626 } 627 628 // NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start 629 // position matching the regexp search string re in the buffer specified by 630 // name. Note that this currently ignores the end position. 631 // This should only be used in combination with OnceMet for a given condition, 632 // otherwise it may always succeed. 633 func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { 634 e.T.Helper() 635 pos := e.RegexpSearch(name, re) 636 return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false} 637 } 638 639 // NoDiagnosticAt asserts that there is no diagnostic entry at the position 640 // specified by line and col, for the workdir-relative path name. 641 // This should only be used in combination with OnceMet for a given condition, 642 // otherwise it may always succeed. 643 func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation { 644 return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: false} 645 } 646 647 // NoDiagnosticWithMessage asserts that there is no diagnostic entry with the 648 // given message. 649 // 650 // This should only be used in combination with OnceMet for a given condition, 651 // otherwise it may always succeed. 652 func NoDiagnosticWithMessage(name, msg string) DiagnosticExpectation { 653 return DiagnosticExpectation{path: name, message: msg, present: false} 654 } 655 656 // GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the 657 // given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is 658 // present. 659 func (e *Env) GoSumDiagnostic(name, module string) Expectation { 660 e.T.Helper() 661 // In 1.16, go.sum diagnostics should appear on the relevant module. Earlier 662 // errors have no information and appear on the module declaration. 663 if testenv.Go1Point() >= 16 { 664 return e.DiagnosticAtRegexpWithMessage(name, module, "go.sum is out of sync") 665 } else { 666 return e.DiagnosticAtRegexpWithMessage(name, `module`, "go.sum is out of sync") 667 } 668 }