| // 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 regtest |
| |
| import ( |
| "fmt" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/lsp" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| ) |
| |
| var ( |
| // InitialWorkspaceLoad is an expectation that the workspace initial load has |
| // completed. It is verified via workdone reporting. |
| InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1, false) |
| ) |
| |
| // A Verdict is the result of checking an expectation against the current |
| // editor state. |
| type Verdict int |
| |
| // Order matters for the following constants: verdicts are sorted in order of |
| // decisiveness. |
| const ( |
| // Met indicates that an expectation is satisfied by the current state. |
| Met Verdict = iota |
| // Unmet indicates that an expectation is not currently met, but could be met |
| // in the future. |
| Unmet |
| // Unmeetable indicates that an expectation cannot be satisfied in the |
| // future. |
| Unmeetable |
| ) |
| |
| func (v Verdict) String() string { |
| switch v { |
| case Met: |
| return "Met" |
| case Unmet: |
| return "Unmet" |
| case Unmeetable: |
| return "Unmeetable" |
| } |
| return fmt.Sprintf("unrecognized verdict %d", v) |
| } |
| |
| // An Expectation is an expected property of the state of the LSP client. |
| // The Check function reports whether the property is met. |
| // |
| // Expectations are combinators. By composing them, tests may express |
| // complex expectations in terms of simpler ones. |
| // |
| // TODO(rfindley): as expectations are combined, it becomes harder to identify |
| // why they failed. A better signature for Check would be |
| // |
| // func(State) (Verdict, string) |
| // |
| // returning a reason for the verdict that can be composed similarly to |
| // descriptions. |
| type Expectation struct { |
| Check func(State) Verdict |
| |
| // Description holds a noun-phrase identifying what the expectation checks. |
| // |
| // TODO(rfindley): revisit existing descriptions to ensure they compose nicely. |
| Description string |
| } |
| |
| // OnceMet returns an Expectation that, once the precondition is met, asserts |
| // that mustMeet is met. |
| func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { |
| check := func(s State) Verdict { |
| switch pre := precondition.Check(s); pre { |
| case Unmeetable: |
| return Unmeetable |
| case Met: |
| for _, mustMeet := range mustMeets { |
| verdict := mustMeet.Check(s) |
| if verdict != Met { |
| return Unmeetable |
| } |
| } |
| return Met |
| default: |
| return Unmet |
| } |
| } |
| description := describeExpectations(mustMeets...) |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), |
| } |
| } |
| |
| func describeExpectations(expectations ...Expectation) string { |
| var descriptions []string |
| for _, e := range expectations { |
| descriptions = append(descriptions, e.Description) |
| } |
| return strings.Join(descriptions, "\n") |
| } |
| |
| // AnyOf returns an expectation that is satisfied when any of the given |
| // expectations is met. |
| func AnyOf(anyOf ...Expectation) Expectation { |
| check := func(s State) Verdict { |
| for _, e := range anyOf { |
| verdict := e.Check(s) |
| if verdict == Met { |
| return Met |
| } |
| } |
| return Unmet |
| } |
| description := describeExpectations(anyOf...) |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("Any of:\n%s", description), |
| } |
| } |
| |
| // AllOf expects that all given expectations are met. |
| // |
| // TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf |
| // and AllOf) is that we lose the information of *why* they failed: the Awaiter |
| // is not smart enough to look inside. |
| // |
| // Refactor the API such that the Check function is responsible for explaining |
| // why an expectation failed. This should allow us to significantly improve |
| // test output: we won't need to summarize state at all, as the verdict |
| // explanation itself should describe clearly why the expectation not met. |
| func AllOf(allOf ...Expectation) Expectation { |
| check := func(s State) Verdict { |
| verdict := Met |
| for _, e := range allOf { |
| if v := e.Check(s); v > verdict { |
| verdict = v |
| } |
| } |
| return verdict |
| } |
| description := describeExpectations(allOf...) |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("All of:\n%s", description), |
| } |
| } |
| |
| // ReadDiagnostics is an Expectation that stores the current diagnostics for |
| // fileName in into, whenever it is evaluated. |
| // |
| // It can be used in combination with OnceMet or AfterChange to capture the |
| // state of diagnostics when other expectations are satisfied. |
| func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { |
| check := func(s State) Verdict { |
| diags, ok := s.diagnostics[fileName] |
| if !ok { |
| return Unmeetable |
| } |
| *into = *diags |
| return Met |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("read diagnostics for %q", fileName), |
| } |
| } |
| |
| // NoOutstandingWork asserts that there is no work initiated using the LSP |
| // $/progress API that has not completed. |
| func NoOutstandingWork() Expectation { |
| check := func(s State) Verdict { |
| if len(s.outstandingWork()) == 0 { |
| return Met |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: "no outstanding work", |
| } |
| } |
| |
| // NoShownMessage asserts that the editor has not received a ShowMessage. |
| func NoShownMessage(subString string) Expectation { |
| check := func(s State) Verdict { |
| for _, m := range s.showMessage { |
| if strings.Contains(m.Message, subString) { |
| return Unmeetable |
| } |
| } |
| return Met |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("no ShowMessage received containing %q", subString), |
| } |
| } |
| |
| // ShownMessage asserts that the editor has received a ShowMessageRequest |
| // containing the given substring. |
| func ShownMessage(containing string) Expectation { |
| check := func(s State) Verdict { |
| for _, m := range s.showMessage { |
| if strings.Contains(m.Message, containing) { |
| return Met |
| } |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: "received ShowMessage", |
| } |
| } |
| |
| // ShowMessageRequest asserts that the editor has received a ShowMessageRequest |
| // with an action item that has the given title. |
| func ShowMessageRequest(title string) Expectation { |
| check := func(s State) Verdict { |
| if len(s.showMessageRequest) == 0 { |
| return Unmet |
| } |
| // Only check the most recent one. |
| m := s.showMessageRequest[len(s.showMessageRequest)-1] |
| if len(m.Actions) == 0 || len(m.Actions) > 1 { |
| return Unmet |
| } |
| if m.Actions[0].Title == title { |
| return Met |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: "received ShowMessageRequest", |
| } |
| } |
| |
| // DoneDiagnosingChanges expects that diagnostics are complete from common |
| // change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, |
| // and didClose. |
| // |
| // This can be used when multiple notifications may have been sent, such as |
| // when a didChange is immediately followed by a didSave. It is insufficient to |
| // simply await NoOutstandingWork, because the LSP client has no control over |
| // when the server starts processing a notification. Therefore, we must keep |
| // track of |
| func (e *Env) DoneDiagnosingChanges() Expectation { |
| stats := e.Editor.Stats() |
| statsBySource := map[lsp.ModificationSource]uint64{ |
| lsp.FromDidOpen: stats.DidOpen, |
| lsp.FromDidChange: stats.DidChange, |
| lsp.FromDidSave: stats.DidSave, |
| lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, |
| lsp.FromDidClose: stats.DidClose, |
| } |
| |
| var expected []lsp.ModificationSource |
| for k, v := range statsBySource { |
| if v > 0 { |
| expected = append(expected, k) |
| } |
| } |
| |
| // Sort for stability. |
| sort.Slice(expected, func(i, j int) bool { |
| return expected[i] < expected[j] |
| }) |
| |
| var all []Expectation |
| for _, source := range expected { |
| all = append(all, CompletedWork(lsp.DiagnosticWorkTitle(source), statsBySource[source], true)) |
| } |
| |
| return AllOf(all...) |
| } |
| |
| // AfterChange expects that the given expectations will be met after all |
| // state-changing notifications have been processed by the server. |
| // |
| // It awaits the completion of all anticipated work before checking the given |
| // expectations. |
| func (e *Env) AfterChange(expectations ...Expectation) { |
| e.T.Helper() |
| e.OnceMet( |
| e.DoneDiagnosingChanges(), |
| expectations..., |
| ) |
| } |
| |
| // DoneWithOpen expects all didOpen notifications currently sent by the editor |
| // to be completely processed. |
| func (e *Env) DoneWithOpen() Expectation { |
| opens := e.Editor.Stats().DidOpen |
| return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true) |
| } |
| |
| // StartedChange expects that the server has at least started processing all |
| // didChange notifications sent from the client. |
| func (e *Env) StartedChange() Expectation { |
| changes := e.Editor.Stats().DidChange |
| return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) |
| } |
| |
| // DoneWithChange expects all didChange notifications currently sent by the |
| // editor to be completely processed. |
| func (e *Env) DoneWithChange() Expectation { |
| changes := e.Editor.Stats().DidChange |
| return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes, true) |
| } |
| |
| // DoneWithSave expects all didSave notifications currently sent by the editor |
| // to be completely processed. |
| func (e *Env) DoneWithSave() Expectation { |
| saves := e.Editor.Stats().DidSave |
| return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true) |
| } |
| |
| // StartedChangeWatchedFiles expects that the server has at least started |
| // processing all didChangeWatchedFiles notifications sent from the client. |
| func (e *Env) StartedChangeWatchedFiles() Expectation { |
| changes := e.Editor.Stats().DidChangeWatchedFiles |
| return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes) |
| } |
| |
| // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications |
| // currently sent by the editor to be completely processed. |
| func (e *Env) DoneWithChangeWatchedFiles() Expectation { |
| changes := e.Editor.Stats().DidChangeWatchedFiles |
| return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes, true) |
| } |
| |
| // DoneWithClose expects all didClose notifications currently sent by the |
| // editor to be completely processed. |
| func (e *Env) DoneWithClose() Expectation { |
| changes := e.Editor.Stats().DidClose |
| return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes, true) |
| } |
| |
| // StartedWork expect a work item to have been started >= atLeast times. |
| // |
| // See CompletedWork. |
| func StartedWork(title string, atLeast uint64) Expectation { |
| check := func(s State) Verdict { |
| if s.startedWork()[title] >= atLeast { |
| return Met |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), |
| } |
| } |
| |
| // CompletedWork expects a work item to have been completed >= atLeast times. |
| // |
| // Since the Progress API doesn't include any hidden metadata, we must use the |
| // progress notification title to identify the work we expect to be completed. |
| func CompletedWork(title string, count uint64, atLeast bool) Expectation { |
| check := func(s State) Verdict { |
| completed := s.completedWork() |
| if completed[title] == count || atLeast && completed[title] > count { |
| return Met |
| } |
| return Unmet |
| } |
| desc := fmt.Sprintf("completed work %q %v times", title, count) |
| if atLeast { |
| desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) |
| } |
| return Expectation{ |
| Check: check, |
| Description: desc, |
| } |
| } |
| |
| type WorkStatus struct { |
| // Last seen message from either `begin` or `report` progress. |
| Msg string |
| // Message sent with `end` progress message. |
| EndMsg string |
| } |
| |
| // CompletedProgress expects that workDone progress is complete for the given |
| // progress token. When non-nil WorkStatus is provided, it will be filled |
| // when the expectation is met. |
| // |
| // If the token is not a progress token that the client has seen, this |
| // expectation is Unmeetable. |
| func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation { |
| check := func(s State) Verdict { |
| work, ok := s.work[token] |
| if !ok { |
| return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result |
| } |
| if work.complete { |
| if into != nil { |
| into.Msg = work.msg |
| into.EndMsg = work.endMsg |
| } |
| return Met |
| } |
| return Unmet |
| } |
| desc := fmt.Sprintf("completed work for token %v", token) |
| return Expectation{ |
| Check: check, |
| Description: desc, |
| } |
| } |
| |
| // OutstandingWork expects a work item to be outstanding. The given title must |
| // be an exact match, whereas the given msg must only be contained in the work |
| // item's message. |
| func OutstandingWork(title, msg string) Expectation { |
| check := func(s State) Verdict { |
| for _, work := range s.work { |
| if work.complete { |
| continue |
| } |
| if work.title == title && strings.Contains(work.msg, msg) { |
| return Met |
| } |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), |
| } |
| } |
| |
| // NoErrorLogs asserts that the client has not received any log messages of |
| // error severity. |
| func NoErrorLogs() Expectation { |
| return NoLogMatching(protocol.Error, "") |
| } |
| |
| // LogMatching asserts that the client has received a log message |
| // of type typ matching the regexp re a certain number of times. |
| // |
| // The count argument specifies the expected number of matching logs. If |
| // atLeast is set, this is a lower bound, otherwise there must be exactly cound |
| // matching logs. |
| func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { |
| rec, err := regexp.Compile(re) |
| if err != nil { |
| panic(err) |
| } |
| check := func(state State) Verdict { |
| var found int |
| for _, msg := range state.logs { |
| if msg.Type == typ && rec.Match([]byte(msg.Message)) { |
| found++ |
| } |
| } |
| // Check for an exact or "at least" match. |
| if found == count || (found >= count && atLeast) { |
| return Met |
| } |
| return Unmet |
| } |
| desc := fmt.Sprintf("log message matching %q expected %v times", re, count) |
| if atLeast { |
| desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) |
| } |
| return Expectation{ |
| Check: check, |
| Description: desc, |
| } |
| } |
| |
| // NoLogMatching asserts that the client has not received a log message |
| // of type typ matching the regexp re. If re is an empty string, any log |
| // message is considered a match. |
| func NoLogMatching(typ protocol.MessageType, re string) Expectation { |
| var r *regexp.Regexp |
| if re != "" { |
| var err error |
| r, err = regexp.Compile(re) |
| if err != nil { |
| panic(err) |
| } |
| } |
| check := func(state State) Verdict { |
| for _, msg := range state.logs { |
| if msg.Type != typ { |
| continue |
| } |
| if r == nil || r.Match([]byte(msg.Message)) { |
| return Unmeetable |
| } |
| } |
| return Met |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("no log message matching %q", re), |
| } |
| } |
| |
| // FileWatchMatching expects that a file registration matches re. |
| func FileWatchMatching(re string) Expectation { |
| return Expectation{ |
| Check: checkFileWatch(re, Met, Unmet), |
| Description: fmt.Sprintf("file watch matching %q", re), |
| } |
| } |
| |
| // NoFileWatchMatching expects that no file registration matches re. |
| func NoFileWatchMatching(re string) Expectation { |
| return Expectation{ |
| Check: checkFileWatch(re, Unmet, Met), |
| Description: fmt.Sprintf("no file watch matching %q", re), |
| } |
| } |
| |
| func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { |
| rec := regexp.MustCompile(re) |
| return func(s State) Verdict { |
| r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] |
| watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) |
| for _, watcher := range watchers { |
| pattern := jsonProperty(watcher, "globPattern").(string) |
| if rec.MatchString(pattern) { |
| return onMatch |
| } |
| } |
| return onNoMatch |
| } |
| } |
| |
| // jsonProperty extracts a value from a path of JSON property names, assuming |
| // the default encoding/json unmarshaling to the empty interface (i.e.: that |
| // JSON objects are unmarshalled as map[string]interface{}) |
| // |
| // For example, if obj is unmarshalled from the following json: |
| // |
| // { |
| // "foo": { "bar": 3 } |
| // } |
| // |
| // Then jsonProperty(obj, "foo", "bar") will be 3. |
| func jsonProperty(obj interface{}, path ...string) interface{} { |
| if len(path) == 0 || obj == nil { |
| return obj |
| } |
| m := obj.(map[string]interface{}) |
| return jsonProperty(m[path[0]], path[1:]...) |
| } |
| |
| // RegistrationMatching asserts that the client has received a capability |
| // registration matching the given regexp. |
| // |
| // TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited. |
| // |
| // Deprecated: use (No)FileWatchMatching |
| func RegistrationMatching(re string) Expectation { |
| rec := regexp.MustCompile(re) |
| check := func(s State) Verdict { |
| for _, p := range s.registrations { |
| for _, r := range p.Registrations { |
| if rec.Match([]byte(r.Method)) { |
| return Met |
| } |
| } |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("registration matching %q", re), |
| } |
| } |
| |
| // UnregistrationMatching asserts that the client has received an |
| // unregistration whose ID matches the given regexp. |
| func UnregistrationMatching(re string) Expectation { |
| rec := regexp.MustCompile(re) |
| check := func(s State) Verdict { |
| for _, p := range s.unregistrations { |
| for _, r := range p.Unregisterations { |
| if rec.Match([]byte(r.Method)) { |
| return Met |
| } |
| } |
| } |
| return Unmet |
| } |
| return Expectation{ |
| Check: check, |
| Description: fmt.Sprintf("unregistration matching %q", re), |
| } |
| } |
| |
| // Diagnostics asserts that there is at least one diagnostic matching the given |
| // filters. |
| func Diagnostics(filters ...DiagnosticFilter) Expectation { |
| check := func(s State) Verdict { |
| diags := flattenDiagnostics(s) |
| for _, filter := range filters { |
| var filtered []flatDiagnostic |
| for _, d := range diags { |
| if filter.check(d.name, d.diag) { |
| filtered = append(filtered, d) |
| } |
| } |
| if len(filtered) == 0 { |
| // TODO(rfindley): if/when expectations describe their own failure, we |
| // can provide more useful information here as to which filter caused |
| // the failure. |
| return Unmet |
| } |
| diags = filtered |
| } |
| return Met |
| } |
| var descs []string |
| for _, filter := range filters { |
| descs = append(descs, filter.desc) |
| } |
| return Expectation{ |
| Check: check, |
| Description: "any diagnostics " + strings.Join(descs, ", "), |
| } |
| } |
| |
| // NoDiagnostics asserts that there are no diagnostics matching the given |
| // filters. Notably, if no filters are supplied this assertion checks that |
| // there are no diagnostics at all, for any file. |
| func NoDiagnostics(filters ...DiagnosticFilter) Expectation { |
| check := func(s State) Verdict { |
| diags := flattenDiagnostics(s) |
| for _, filter := range filters { |
| var filtered []flatDiagnostic |
| for _, d := range diags { |
| if filter.check(d.name, d.diag) { |
| filtered = append(filtered, d) |
| } |
| } |
| diags = filtered |
| } |
| if len(diags) > 0 { |
| return Unmet |
| } |
| return Met |
| } |
| var descs []string |
| for _, filter := range filters { |
| descs = append(descs, filter.desc) |
| } |
| return Expectation{ |
| Check: check, |
| Description: "no diagnostics " + strings.Join(descs, ", "), |
| } |
| } |
| |
| type flatDiagnostic struct { |
| name string |
| diag protocol.Diagnostic |
| } |
| |
| func flattenDiagnostics(state State) []flatDiagnostic { |
| var result []flatDiagnostic |
| for name, diags := range state.diagnostics { |
| for _, diag := range diags.Diagnostics { |
| result = append(result, flatDiagnostic{name, diag}) |
| } |
| } |
| return result |
| } |
| |
| // -- Diagnostic filters -- |
| |
| // A DiagnosticFilter filters the set of diagnostics, for assertion with |
| // Diagnostics or NoDiagnostics. |
| type DiagnosticFilter struct { |
| desc string |
| check func(name string, _ protocol.Diagnostic) bool |
| } |
| |
| // ForFile filters to diagnostics matching the sandbox-relative file name. |
| func ForFile(name string) DiagnosticFilter { |
| return DiagnosticFilter{ |
| desc: fmt.Sprintf("for file %q", name), |
| check: func(diagName string, _ protocol.Diagnostic) bool { |
| return diagName == name |
| }, |
| } |
| } |
| |
| // FromSource filters to diagnostics matching the given diagnostics source. |
| func FromSource(source string) DiagnosticFilter { |
| return DiagnosticFilter{ |
| desc: fmt.Sprintf("with source %q", source), |
| check: func(_ string, d protocol.Diagnostic) bool { |
| return d.Source == source |
| }, |
| } |
| } |
| |
| // AtRegexp filters to diagnostics in the file with sandbox-relative path name, |
| // at the first position matching the given regexp pattern. |
| // |
| // TODO(rfindley): pass in the editor to expectations, so that they may depend |
| // on editor state and AtRegexp can be a function rather than a method. |
| func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { |
| loc := e.RegexpSearch(name, pattern) |
| return DiagnosticFilter{ |
| desc: fmt.Sprintf("at the first position matching %#q in %q", pattern, name), |
| check: func(diagName string, d protocol.Diagnostic) bool { |
| return diagName == name && d.Range.Start == loc.Range.Start |
| }, |
| } |
| } |
| |
| // AtPosition filters to diagnostics at location name:line:character, for a |
| // sandbox-relative path name. |
| // |
| // Line and character are 0-based, and character measures UTF-16 codes. |
| // |
| // Note: prefer the more readable AtRegexp. |
| func AtPosition(name string, line, character uint32) DiagnosticFilter { |
| pos := protocol.Position{Line: line, Character: character} |
| return DiagnosticFilter{ |
| desc: fmt.Sprintf("at %s:%d:%d", name, line, character), |
| check: func(diagName string, d protocol.Diagnostic) bool { |
| return diagName == name && d.Range.Start == pos |
| }, |
| } |
| } |
| |
| // WithMessage filters to diagnostics whose message contains the given |
| // substring. |
| func WithMessage(substring string) DiagnosticFilter { |
| return DiagnosticFilter{ |
| desc: fmt.Sprintf("with message containing %q", substring), |
| check: func(_ string, d protocol.Diagnostic) bool { |
| return strings.Contains(d.Message, substring) |
| }, |
| } |
| } |