golang.org/x/tools/gopls@v0.15.3/internal/test/integration/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 integration 6 7 import ( 8 "fmt" 9 "regexp" 10 "sort" 11 "strings" 12 13 "github.com/google/go-cmp/cmp" 14 "golang.org/x/tools/gopls/internal/protocol" 15 "golang.org/x/tools/gopls/internal/server" 16 ) 17 18 var ( 19 // InitialWorkspaceLoad is an expectation that the workspace initial load has 20 // completed. It is verified via workdone reporting. 21 InitialWorkspaceLoad = CompletedWork(server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad), 1, false) 22 ) 23 24 // A Verdict is the result of checking an expectation against the current 25 // editor state. 26 type Verdict int 27 28 // Order matters for the following constants: verdicts are sorted in order of 29 // decisiveness. 30 const ( 31 // Met indicates that an expectation is satisfied by the current state. 32 Met Verdict = iota 33 // Unmet indicates that an expectation is not currently met, but could be met 34 // in the future. 35 Unmet 36 // Unmeetable indicates that an expectation cannot be satisfied in the 37 // future. 38 Unmeetable 39 ) 40 41 func (v Verdict) String() string { 42 switch v { 43 case Met: 44 return "Met" 45 case Unmet: 46 return "Unmet" 47 case Unmeetable: 48 return "Unmeetable" 49 } 50 return fmt.Sprintf("unrecognized verdict %d", v) 51 } 52 53 // An Expectation is an expected property of the state of the LSP client. 54 // The Check function reports whether the property is met. 55 // 56 // Expectations are combinators. By composing them, tests may express 57 // complex expectations in terms of simpler ones. 58 // 59 // TODO(rfindley): as expectations are combined, it becomes harder to identify 60 // why they failed. A better signature for Check would be 61 // 62 // func(State) (Verdict, string) 63 // 64 // returning a reason for the verdict that can be composed similarly to 65 // descriptions. 66 type Expectation struct { 67 Check func(State) Verdict 68 69 // Description holds a noun-phrase identifying what the expectation checks. 70 // 71 // TODO(rfindley): revisit existing descriptions to ensure they compose nicely. 72 Description string 73 } 74 75 // OnceMet returns an Expectation that, once the precondition is met, asserts 76 // that mustMeet is met. 77 func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { 78 check := func(s State) Verdict { 79 switch pre := precondition.Check(s); pre { 80 case Unmeetable: 81 return Unmeetable 82 case Met: 83 for _, mustMeet := range mustMeets { 84 verdict := mustMeet.Check(s) 85 if verdict != Met { 86 return Unmeetable 87 } 88 } 89 return Met 90 default: 91 return Unmet 92 } 93 } 94 description := describeExpectations(mustMeets...) 95 return Expectation{ 96 Check: check, 97 Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), 98 } 99 } 100 101 func describeExpectations(expectations ...Expectation) string { 102 var descriptions []string 103 for _, e := range expectations { 104 descriptions = append(descriptions, e.Description) 105 } 106 return strings.Join(descriptions, "\n") 107 } 108 109 // Not inverts the sense of an expectation: a met expectation is unmet, and an 110 // unmet expectation is met. 111 func Not(e Expectation) Expectation { 112 check := func(s State) Verdict { 113 switch v := e.Check(s); v { 114 case Met: 115 return Unmet 116 case Unmet, Unmeetable: 117 return Met 118 default: 119 panic(fmt.Sprintf("unexpected verdict %v", v)) 120 } 121 } 122 description := describeExpectations(e) 123 return Expectation{ 124 Check: check, 125 Description: fmt.Sprintf("not: %s", description), 126 } 127 } 128 129 // AnyOf returns an expectation that is satisfied when any of the given 130 // expectations is met. 131 func AnyOf(anyOf ...Expectation) Expectation { 132 check := func(s State) Verdict { 133 for _, e := range anyOf { 134 verdict := e.Check(s) 135 if verdict == Met { 136 return Met 137 } 138 } 139 return Unmet 140 } 141 description := describeExpectations(anyOf...) 142 return Expectation{ 143 Check: check, 144 Description: fmt.Sprintf("Any of:\n%s", description), 145 } 146 } 147 148 // AllOf expects that all given expectations are met. 149 // 150 // TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf 151 // and AllOf) is that we lose the information of *why* they failed: the Awaiter 152 // is not smart enough to look inside. 153 // 154 // Refactor the API such that the Check function is responsible for explaining 155 // why an expectation failed. This should allow us to significantly improve 156 // test output: we won't need to summarize state at all, as the verdict 157 // explanation itself should describe clearly why the expectation not met. 158 func AllOf(allOf ...Expectation) Expectation { 159 check := func(s State) Verdict { 160 verdict := Met 161 for _, e := range allOf { 162 if v := e.Check(s); v > verdict { 163 verdict = v 164 } 165 } 166 return verdict 167 } 168 description := describeExpectations(allOf...) 169 return Expectation{ 170 Check: check, 171 Description: fmt.Sprintf("All of:\n%s", description), 172 } 173 } 174 175 // ReadDiagnostics is an Expectation that stores the current diagnostics for 176 // fileName in into, whenever it is evaluated. 177 // 178 // It can be used in combination with OnceMet or AfterChange to capture the 179 // state of diagnostics when other expectations are satisfied. 180 func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { 181 check := func(s State) Verdict { 182 diags, ok := s.diagnostics[fileName] 183 if !ok { 184 return Unmeetable 185 } 186 *into = *diags 187 return Met 188 } 189 return Expectation{ 190 Check: check, 191 Description: fmt.Sprintf("read diagnostics for %q", fileName), 192 } 193 } 194 195 // ReadAllDiagnostics is an expectation that stores all published diagnostics 196 // into the provided map, whenever it is evaluated. 197 // 198 // It can be used in combination with OnceMet or AfterChange to capture the 199 // state of diagnostics when other expectations are satisfied. 200 func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { 201 check := func(s State) Verdict { 202 allDiags := make(map[string]*protocol.PublishDiagnosticsParams) 203 for name, diags := range s.diagnostics { 204 allDiags[name] = diags 205 } 206 *into = allDiags 207 return Met 208 } 209 return Expectation{ 210 Check: check, 211 Description: "read all diagnostics", 212 } 213 } 214 215 // ShownDocument asserts that the client has received a 216 // ShowDocumentRequest for the given URI. 217 func ShownDocument(uri protocol.URI) Expectation { 218 check := func(s State) Verdict { 219 for _, params := range s.showDocument { 220 if params.URI == uri { 221 return Met 222 } 223 } 224 return Unmet 225 } 226 return Expectation{ 227 Check: check, 228 Description: fmt.Sprintf("received window/showDocument for URI %s", uri), 229 } 230 } 231 232 // NoShownMessage asserts that the editor has not received a ShowMessage. 233 func NoShownMessage(subString string) Expectation { 234 check := func(s State) Verdict { 235 for _, m := range s.showMessage { 236 if strings.Contains(m.Message, subString) { 237 return Unmeetable 238 } 239 } 240 return Met 241 } 242 return Expectation{ 243 Check: check, 244 Description: fmt.Sprintf("no ShowMessage received containing %q", subString), 245 } 246 } 247 248 // ShownMessage asserts that the editor has received a ShowMessageRequest 249 // containing the given substring. 250 func ShownMessage(containing string) Expectation { 251 check := func(s State) Verdict { 252 for _, m := range s.showMessage { 253 if strings.Contains(m.Message, containing) { 254 return Met 255 } 256 } 257 return Unmet 258 } 259 return Expectation{ 260 Check: check, 261 Description: fmt.Sprintf("received window/showMessage containing %q", containing), 262 } 263 } 264 265 // ShownMessageRequest asserts that the editor has received a 266 // ShowMessageRequest with message matching the given regular expression. 267 func ShownMessageRequest(messageRegexp string) Expectation { 268 msgRE := regexp.MustCompile(messageRegexp) 269 check := func(s State) Verdict { 270 if len(s.showMessageRequest) == 0 { 271 return Unmet 272 } 273 for _, m := range s.showMessageRequest { 274 if msgRE.MatchString(m.Message) { 275 return Met 276 } 277 } 278 return Unmet 279 } 280 return Expectation{ 281 Check: check, 282 Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp), 283 } 284 } 285 286 // DoneDiagnosingChanges expects that diagnostics are complete from common 287 // change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, 288 // and didClose. 289 // 290 // This can be used when multiple notifications may have been sent, such as 291 // when a didChange is immediately followed by a didSave. It is insufficient to 292 // simply await NoOutstandingWork, because the LSP client has no control over 293 // when the server starts processing a notification. Therefore, we must keep 294 // track of 295 func (e *Env) DoneDiagnosingChanges() Expectation { 296 stats := e.Editor.Stats() 297 statsBySource := map[server.ModificationSource]uint64{ 298 server.FromDidOpen: stats.DidOpen, 299 server.FromDidChange: stats.DidChange, 300 server.FromDidSave: stats.DidSave, 301 server.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, 302 server.FromDidClose: stats.DidClose, 303 server.FromDidChangeConfiguration: stats.DidChangeConfiguration, 304 } 305 306 var expected []server.ModificationSource 307 for k, v := range statsBySource { 308 if v > 0 { 309 expected = append(expected, k) 310 } 311 } 312 313 // Sort for stability. 314 sort.Slice(expected, func(i, j int) bool { 315 return expected[i] < expected[j] 316 }) 317 318 var all []Expectation 319 for _, source := range expected { 320 all = append(all, CompletedWork(server.DiagnosticWorkTitle(source), statsBySource[source], true)) 321 } 322 323 return AllOf(all...) 324 } 325 326 // AfterChange expects that the given expectations will be met after all 327 // state-changing notifications have been processed by the server. 328 // 329 // It awaits the completion of all anticipated work before checking the given 330 // expectations. 331 func (e *Env) AfterChange(expectations ...Expectation) { 332 e.T.Helper() 333 e.OnceMet( 334 e.DoneDiagnosingChanges(), 335 expectations..., 336 ) 337 } 338 339 // DoneWithOpen expects all didOpen notifications currently sent by the editor 340 // to be completely processed. 341 func (e *Env) DoneWithOpen() Expectation { 342 opens := e.Editor.Stats().DidOpen 343 return CompletedWork(server.DiagnosticWorkTitle(server.FromDidOpen), opens, true) 344 } 345 346 // StartedChange expects that the server has at least started processing all 347 // didChange notifications sent from the client. 348 func (e *Env) StartedChange() Expectation { 349 changes := e.Editor.Stats().DidChange 350 return StartedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes) 351 } 352 353 // DoneWithChange expects all didChange notifications currently sent by the 354 // editor to be completely processed. 355 func (e *Env) DoneWithChange() Expectation { 356 changes := e.Editor.Stats().DidChange 357 return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes, true) 358 } 359 360 // DoneWithSave expects all didSave notifications currently sent by the editor 361 // to be completely processed. 362 func (e *Env) DoneWithSave() Expectation { 363 saves := e.Editor.Stats().DidSave 364 return CompletedWork(server.DiagnosticWorkTitle(server.FromDidSave), saves, true) 365 } 366 367 // StartedChangeWatchedFiles expects that the server has at least started 368 // processing all didChangeWatchedFiles notifications sent from the client. 369 func (e *Env) StartedChangeWatchedFiles() Expectation { 370 changes := e.Editor.Stats().DidChangeWatchedFiles 371 return StartedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes) 372 } 373 374 // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications 375 // currently sent by the editor to be completely processed. 376 func (e *Env) DoneWithChangeWatchedFiles() Expectation { 377 changes := e.Editor.Stats().DidChangeWatchedFiles 378 return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes, true) 379 } 380 381 // DoneWithClose expects all didClose notifications currently sent by the 382 // editor to be completely processed. 383 func (e *Env) DoneWithClose() Expectation { 384 changes := e.Editor.Stats().DidClose 385 return CompletedWork(server.DiagnosticWorkTitle(server.FromDidClose), changes, true) 386 } 387 388 // StartedWork expect a work item to have been started >= atLeast times. 389 // 390 // See CompletedWork. 391 func StartedWork(title string, atLeast uint64) Expectation { 392 check := func(s State) Verdict { 393 if s.startedWork()[title] >= atLeast { 394 return Met 395 } 396 return Unmet 397 } 398 return Expectation{ 399 Check: check, 400 Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), 401 } 402 } 403 404 // CompletedWork expects a work item to have been completed >= atLeast times. 405 // 406 // Since the Progress API doesn't include any hidden metadata, we must use the 407 // progress notification title to identify the work we expect to be completed. 408 func CompletedWork(title string, count uint64, atLeast bool) Expectation { 409 check := func(s State) Verdict { 410 completed := s.completedWork() 411 if completed[title] == count || atLeast && completed[title] > count { 412 return Met 413 } 414 return Unmet 415 } 416 desc := fmt.Sprintf("completed work %q %v times", title, count) 417 if atLeast { 418 desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) 419 } 420 return Expectation{ 421 Check: check, 422 Description: desc, 423 } 424 } 425 426 type WorkStatus struct { 427 // Last seen message from either `begin` or `report` progress. 428 Msg string 429 // Message sent with `end` progress message. 430 EndMsg string 431 } 432 433 // CompletedProgress expects that workDone progress is complete for the given 434 // progress token. When non-nil WorkStatus is provided, it will be filled 435 // when the expectation is met. 436 // 437 // If the token is not a progress token that the client has seen, this 438 // expectation is Unmeetable. 439 func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation { 440 check := func(s State) Verdict { 441 work, ok := s.work[token] 442 if !ok { 443 return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result 444 } 445 if work.complete { 446 if into != nil { 447 into.Msg = work.msg 448 into.EndMsg = work.endMsg 449 } 450 return Met 451 } 452 return Unmet 453 } 454 desc := fmt.Sprintf("completed work for token %v", token) 455 return Expectation{ 456 Check: check, 457 Description: desc, 458 } 459 } 460 461 // OutstandingWork expects a work item to be outstanding. The given title must 462 // be an exact match, whereas the given msg must only be contained in the work 463 // item's message. 464 func OutstandingWork(title, msg string) Expectation { 465 check := func(s State) Verdict { 466 for _, work := range s.work { 467 if work.complete { 468 continue 469 } 470 if work.title == title && strings.Contains(work.msg, msg) { 471 return Met 472 } 473 } 474 return Unmet 475 } 476 return Expectation{ 477 Check: check, 478 Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), 479 } 480 } 481 482 // NoOutstandingWork asserts that there is no work initiated using the LSP 483 // $/progress API that has not completed. 484 // 485 // If non-nil, the ignore func is used to ignore certain work items for the 486 // purpose of this check. 487 // 488 // TODO(rfindley): consider refactoring to treat outstanding work the same way 489 // we treat diagnostics: with an algebra of filters. 490 func NoOutstandingWork(ignore func(title, msg string) bool) Expectation { 491 check := func(s State) Verdict { 492 for _, w := range s.work { 493 if w.complete { 494 continue 495 } 496 if w.title == "" { 497 // A token that has been created but not yet used. 498 // 499 // TODO(rfindley): this should be separated in the data model: until 500 // the "begin" notification, work should not be in progress. 501 continue 502 } 503 if ignore != nil && ignore(w.title, w.msg) { 504 continue 505 } 506 return Unmet 507 } 508 return Met 509 } 510 return Expectation{ 511 Check: check, 512 Description: "no outstanding work", 513 } 514 } 515 516 // IgnoreTelemetryPromptWork may be used in conjunction with NoOutStandingWork 517 // to ignore the telemetry prompt. 518 func IgnoreTelemetryPromptWork(title, msg string) bool { 519 return title == server.TelemetryPromptWorkTitle 520 } 521 522 // NoErrorLogs asserts that the client has not received any log messages of 523 // error severity. 524 func NoErrorLogs() Expectation { 525 return NoLogMatching(protocol.Error, "") 526 } 527 528 // LogMatching asserts that the client has received a log message 529 // of type typ matching the regexp re a certain number of times. 530 // 531 // The count argument specifies the expected number of matching logs. If 532 // atLeast is set, this is a lower bound, otherwise there must be exactly count 533 // matching logs. 534 // 535 // Logs are asynchronous to other LSP messages, so this expectation should not 536 // be used with combinators such as OnceMet or AfterChange that assert on 537 // ordering with respect to other operations. 538 func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { 539 rec, err := regexp.Compile(re) 540 if err != nil { 541 panic(err) 542 } 543 check := func(state State) Verdict { 544 var found int 545 for _, msg := range state.logs { 546 if msg.Type == typ && rec.Match([]byte(msg.Message)) { 547 found++ 548 } 549 } 550 // Check for an exact or "at least" match. 551 if found == count || (found >= count && atLeast) { 552 return Met 553 } 554 // If we require an exact count, and have received more than expected, the 555 // expectation can never be met. 556 if found > count && !atLeast { 557 return Unmeetable 558 } 559 return Unmet 560 } 561 desc := fmt.Sprintf("log message matching %q expected %v times", re, count) 562 if atLeast { 563 desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) 564 } 565 return Expectation{ 566 Check: check, 567 Description: desc, 568 } 569 } 570 571 // NoLogMatching asserts that the client has not received a log message 572 // of type typ matching the regexp re. If re is an empty string, any log 573 // message is considered a match. 574 func NoLogMatching(typ protocol.MessageType, re string) Expectation { 575 var r *regexp.Regexp 576 if re != "" { 577 var err error 578 r, err = regexp.Compile(re) 579 if err != nil { 580 panic(err) 581 } 582 } 583 check := func(state State) Verdict { 584 for _, msg := range state.logs { 585 if msg.Type != typ { 586 continue 587 } 588 if r == nil || r.Match([]byte(msg.Message)) { 589 return Unmeetable 590 } 591 } 592 return Met 593 } 594 return Expectation{ 595 Check: check, 596 Description: fmt.Sprintf("no log message matching %q", re), 597 } 598 } 599 600 // FileWatchMatching expects that a file registration matches re. 601 func FileWatchMatching(re string) Expectation { 602 return Expectation{ 603 Check: checkFileWatch(re, Met, Unmet), 604 Description: fmt.Sprintf("file watch matching %q", re), 605 } 606 } 607 608 // NoFileWatchMatching expects that no file registration matches re. 609 func NoFileWatchMatching(re string) Expectation { 610 return Expectation{ 611 Check: checkFileWatch(re, Unmet, Met), 612 Description: fmt.Sprintf("no file watch matching %q", re), 613 } 614 } 615 616 func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { 617 rec := regexp.MustCompile(re) 618 return func(s State) Verdict { 619 r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] 620 watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) 621 for _, watcher := range watchers { 622 pattern := jsonProperty(watcher, "globPattern").(string) 623 if rec.MatchString(pattern) { 624 return onMatch 625 } 626 } 627 return onNoMatch 628 } 629 } 630 631 // jsonProperty extracts a value from a path of JSON property names, assuming 632 // the default encoding/json unmarshaling to the empty interface (i.e.: that 633 // JSON objects are unmarshalled as map[string]interface{}) 634 // 635 // For example, if obj is unmarshalled from the following json: 636 // 637 // { 638 // "foo": { "bar": 3 } 639 // } 640 // 641 // Then jsonProperty(obj, "foo", "bar") will be 3. 642 func jsonProperty(obj interface{}, path ...string) interface{} { 643 if len(path) == 0 || obj == nil { 644 return obj 645 } 646 m := obj.(map[string]interface{}) 647 return jsonProperty(m[path[0]], path[1:]...) 648 } 649 650 // Diagnostics asserts that there is at least one diagnostic matching the given 651 // filters. 652 func Diagnostics(filters ...DiagnosticFilter) Expectation { 653 check := func(s State) Verdict { 654 diags := flattenDiagnostics(s) 655 for _, filter := range filters { 656 var filtered []flatDiagnostic 657 for _, d := range diags { 658 if filter.check(d.name, d.diag) { 659 filtered = append(filtered, d) 660 } 661 } 662 if len(filtered) == 0 { 663 // TODO(rfindley): if/when expectations describe their own failure, we 664 // can provide more useful information here as to which filter caused 665 // the failure. 666 return Unmet 667 } 668 diags = filtered 669 } 670 return Met 671 } 672 var descs []string 673 for _, filter := range filters { 674 descs = append(descs, filter.desc) 675 } 676 return Expectation{ 677 Check: check, 678 Description: "any diagnostics " + strings.Join(descs, ", "), 679 } 680 } 681 682 // NoDiagnostics asserts that there are no diagnostics matching the given 683 // filters. Notably, if no filters are supplied this assertion checks that 684 // there are no diagnostics at all, for any file. 685 func NoDiagnostics(filters ...DiagnosticFilter) Expectation { 686 check := func(s State) Verdict { 687 diags := flattenDiagnostics(s) 688 for _, filter := range filters { 689 var filtered []flatDiagnostic 690 for _, d := range diags { 691 if filter.check(d.name, d.diag) { 692 filtered = append(filtered, d) 693 } 694 } 695 diags = filtered 696 } 697 if len(diags) > 0 { 698 return Unmet 699 } 700 return Met 701 } 702 var descs []string 703 for _, filter := range filters { 704 descs = append(descs, filter.desc) 705 } 706 return Expectation{ 707 Check: check, 708 Description: "no diagnostics " + strings.Join(descs, ", "), 709 } 710 } 711 712 type flatDiagnostic struct { 713 name string 714 diag protocol.Diagnostic 715 } 716 717 func flattenDiagnostics(state State) []flatDiagnostic { 718 var result []flatDiagnostic 719 for name, diags := range state.diagnostics { 720 for _, diag := range diags.Diagnostics { 721 result = append(result, flatDiagnostic{name, diag}) 722 } 723 } 724 return result 725 } 726 727 // -- Diagnostic filters -- 728 729 // A DiagnosticFilter filters the set of diagnostics, for assertion with 730 // Diagnostics or NoDiagnostics. 731 type DiagnosticFilter struct { 732 desc string 733 check func(name string, _ protocol.Diagnostic) bool 734 } 735 736 // ForFile filters to diagnostics matching the sandbox-relative file name. 737 func ForFile(name string) DiagnosticFilter { 738 return DiagnosticFilter{ 739 desc: fmt.Sprintf("for file %q", name), 740 check: func(diagName string, _ protocol.Diagnostic) bool { 741 return diagName == name 742 }, 743 } 744 } 745 746 // FromSource filters to diagnostics matching the given diagnostics source. 747 func FromSource(source string) DiagnosticFilter { 748 return DiagnosticFilter{ 749 desc: fmt.Sprintf("with source %q", source), 750 check: func(_ string, d protocol.Diagnostic) bool { 751 return d.Source == source 752 }, 753 } 754 } 755 756 // AtRegexp filters to diagnostics in the file with sandbox-relative path name, 757 // at the first position matching the given regexp pattern. 758 // 759 // TODO(rfindley): pass in the editor to expectations, so that they may depend 760 // on editor state and AtRegexp can be a function rather than a method. 761 func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { 762 loc := e.RegexpSearch(name, pattern) 763 return DiagnosticFilter{ 764 desc: fmt.Sprintf("at the first position (%v) matching %#q in %q", loc.Range.Start, pattern, name), 765 check: func(diagName string, d protocol.Diagnostic) bool { 766 return diagName == name && d.Range.Start == loc.Range.Start 767 }, 768 } 769 } 770 771 // AtPosition filters to diagnostics at location name:line:character, for a 772 // sandbox-relative path name. 773 // 774 // Line and character are 0-based, and character measures UTF-16 codes. 775 // 776 // Note: prefer the more readable AtRegexp. 777 func AtPosition(name string, line, character uint32) DiagnosticFilter { 778 pos := protocol.Position{Line: line, Character: character} 779 return DiagnosticFilter{ 780 desc: fmt.Sprintf("at %s:%d:%d", name, line, character), 781 check: func(diagName string, d protocol.Diagnostic) bool { 782 return diagName == name && d.Range.Start == pos 783 }, 784 } 785 } 786 787 // WithMessage filters to diagnostics whose message contains the given 788 // substring. 789 func WithMessage(substring string) DiagnosticFilter { 790 return DiagnosticFilter{ 791 desc: fmt.Sprintf("with message containing %q", substring), 792 check: func(_ string, d protocol.Diagnostic) bool { 793 return strings.Contains(d.Message, substring) 794 }, 795 } 796 } 797 798 // WithSeverityTags filters to diagnostics whose severity and tags match 799 // the given expectation. 800 func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter { 801 return DiagnosticFilter{ 802 desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags), 803 check: func(_ string, d protocol.Diagnostic) bool { 804 return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags) 805 }, 806 } 807 }