diff options
Diffstat (limited to 'gopls/internal/lsp/regtest/expectation.go')
-rw-r--r-- | gopls/internal/lsp/regtest/expectation.go | 769 |
1 files changed, 769 insertions, 0 deletions
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go new file mode 100644 index 000000000..9d9f023d9 --- /dev/null +++ b/gopls/internal/lsp/regtest/expectation.go @@ -0,0 +1,769 @@ +// 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), + } +} + +// ReadAllDiagnostics is an expectation that stores all published diagnostics +// into the provided map, 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 ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { + check := func(s State) Verdict { + allDiags := make(map[string]*protocol.PublishDiagnosticsParams) + for name, diags := range s.diagnostics { + allDiags[name] = diags + } + *into = allDiags + return Met + } + return Expectation{ + Check: check, + Description: "read all diagnostics", + } +} + +// 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 count +// 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) + }, + } +} |