blob: 9d9f023d92aac24fe4eaf76190ba07ecbeb0861b [file] [log] [blame]
// 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)
},
}
}