blob: 697363f0af690a1812418847468d9cfcbe8c03e9 [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"
"strings"
"golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/protocol"
)
// An Expectation asserts that the state of the editor at a point in time
// matches an expected condition. This is used for signaling in tests when
// certain conditions in the editor are met.
type Expectation interface {
// Check determines whether the state of the editor satisfies the
// expectation, returning the results that met the condition.
Check(State) Verdict
// Description is a human-readable description of the expectation.
Description() string
}
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)
)
// 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)
}
// SimpleExpectation holds an arbitrary check func, and implements the Expectation interface.
type SimpleExpectation struct {
check func(State) Verdict
description string
}
// Check invokes e.check.
func (e SimpleExpectation) Check(s State) Verdict {
return e.check(s)
}
// Description returns e.descriptin.
func (e SimpleExpectation) Description() string {
return e.description
}
// OnceMet returns an Expectation that, once the precondition is met, asserts
// that mustMeet is met.
func OnceMet(precondition Expectation, mustMeet Expectation) *SimpleExpectation {
check := func(s State) Verdict {
switch pre := precondition.Check(s); pre {
case Unmeetable:
return Unmeetable
case Met:
verdict := mustMeet.Check(s)
if verdict != Met {
return Unmeetable
}
return Met
default:
return Unmet
}
}
return &SimpleExpectation{
check: check,
description: fmt.Sprintf("once %q is met, must have %q", precondition.Description(), mustMeet.Description()),
}
}
// ReadDiagnostics is an 'expectation' that is used to read diagnostics
// atomically. It is intended to be used with 'OnceMet'.
func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation {
check := func(s State) Verdict {
diags, ok := s.diagnostics[fileName]
if !ok {
return Unmeetable
}
*into = *diags
return Met
}
return &SimpleExpectation{
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() SimpleExpectation {
check := func(s State) Verdict {
if len(s.outstandingWork) == 0 {
return Met
}
return Unmet
}
return SimpleExpectation{
check: check,
description: "no outstanding work",
}
}
// NoShowMessage asserts that the editor has not received a ShowMessage.
func NoShowMessage() SimpleExpectation {
check := func(s State) Verdict {
if len(s.showMessage) == 0 {
return Met
}
return Unmeetable
}
return SimpleExpectation{
check: check,
description: "no ShowMessage received",
}
}
// ShownMessage asserts that the editor has received a ShownMessage with the
// given title.
func ShownMessage(title string) SimpleExpectation {
check := func(s State) Verdict {
for _, m := range s.showMessage {
if strings.Contains(m.Message, title) {
return Met
}
}
return Unmet
}
return SimpleExpectation{
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) SimpleExpectation {
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 SimpleExpectation{
check: check,
description: "received ShowMessageRequest",
}
}
// 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, atLeast int) SimpleExpectation {
check := func(s State) Verdict {
if s.completedWork[title] >= atLeast {
return Met
}
return Unmet
}
return SimpleExpectation{
check: check,
description: fmt.Sprintf("completed work %q at least %d time(s)", title, atLeast),
}
}
// LogExpectation is an expectation on the log messages received by the editor
// from gopls.
type LogExpectation struct {
check func([]*protocol.LogMessageParams) Verdict
description string
}
// Check implements the Expectation interface.
func (e LogExpectation) Check(s State) Verdict {
return e.check(s.logs)
}
// Description implements the Expectation interface.
func (e LogExpectation) Description() string {
return e.description
}
// NoErrorLogs asserts that the client has not received any log messages of
// error severity.
func NoErrorLogs() LogExpectation {
return NoLogMatching(protocol.Error, "")
}
// LogMatching asserts that the client has received a log message
// of type typ matching the regexp re.
func LogMatching(typ protocol.MessageType, re string, count int) LogExpectation {
rec, err := regexp.Compile(re)
if err != nil {
panic(err)
}
check := func(msgs []*protocol.LogMessageParams) Verdict {
var found int
for _, msg := range msgs {
if msg.Type == typ && rec.Match([]byte(msg.Message)) {
found++
}
}
if found == count {
return Met
}
return Unmet
}
return LogExpectation{
check: check,
description: fmt.Sprintf("log message matching %q", re),
}
}
// 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) LogExpectation {
var r *regexp.Regexp
if re != "" {
var err error
r, err = regexp.Compile(re)
if err != nil {
panic(err)
}
}
check := func(msgs []*protocol.LogMessageParams) Verdict {
for _, msg := range msgs {
if msg.Type != typ {
continue
}
if r == nil || r.Match([]byte(msg.Message)) {
return Unmeetable
}
}
return Met
}
return LogExpectation{
check: check,
description: fmt.Sprintf("no log message matching %q", re),
}
}
// RegistrationExpectation is an expectation on the capability registrations
// received by the editor from gopls.
type RegistrationExpectation struct {
check func([]*protocol.RegistrationParams) Verdict
description string
}
// Check implements the Expectation interface.
func (e RegistrationExpectation) Check(s State) Verdict {
return e.check(s.registrations)
}
// Description implements the Expectation interface.
func (e RegistrationExpectation) Description() string {
return e.description
}
// RegistrationMatching asserts that the client has received a capability
// registration matching the given regexp.
func RegistrationMatching(re string) RegistrationExpectation {
rec, err := regexp.Compile(re)
if err != nil {
panic(err)
}
check := func(params []*protocol.RegistrationParams) Verdict {
for _, p := range params {
for _, r := range p.Registrations {
if rec.Match([]byte(r.Method)) {
return Met
}
}
}
return Unmet
}
return RegistrationExpectation{
check: check,
description: fmt.Sprintf("registration matching %q", re),
}
}
// UnregistrationExpectation is an expectation on the capability
// unregistrations received by the editor from gopls.
type UnregistrationExpectation struct {
check func([]*protocol.UnregistrationParams) Verdict
description string
}
// Check implements the Expectation interface.
func (e UnregistrationExpectation) Check(s State) Verdict {
return e.check(s.unregistrations)
}
// Description implements the Expectation interface.
func (e UnregistrationExpectation) Description() string {
return e.description
}
// UnregistrationMatching asserts that the client has received an
// unregistration whose ID matches the given regexp.
func UnregistrationMatching(re string) UnregistrationExpectation {
rec, err := regexp.Compile(re)
if err != nil {
panic(err)
}
check := func(params []*protocol.UnregistrationParams) Verdict {
for _, p := range params {
for _, r := range p.Unregisterations {
if rec.Match([]byte(r.Method)) {
return Met
}
}
}
return Unmet
}
return UnregistrationExpectation{
check: check,
description: fmt.Sprintf("unregistration matching %q", re),
}
}
// A DiagnosticExpectation is a condition that must be met by the current set
// of diagnostics for a file.
type DiagnosticExpectation struct {
// IsMet determines whether the diagnostics for this file version satisfy our
// expectation.
isMet func(*protocol.PublishDiagnosticsParams) bool
// Description is a human-readable description of the diagnostic expectation.
description string
// Path is the scratch workdir-relative path to the file being asserted on.
path string
}
// Check implements the Expectation interface.
func (e DiagnosticExpectation) Check(s State) Verdict {
if diags, ok := s.diagnostics[e.path]; ok && e.isMet(diags) {
return Met
}
return Unmet
}
// Description implements the Expectation interface.
func (e DiagnosticExpectation) Description() string {
return fmt.Sprintf("%s: %s", e.path, e.description)
}
// EmptyDiagnostics asserts that empty diagnostics are sent for the
// workspace-relative path name.
func EmptyDiagnostics(name string) Expectation {
check := func(s State) Verdict {
if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 {
return Met
}
return Unmet
}
return SimpleExpectation{
check: check,
description: "empty diagnostics",
}
}
// NoDiagnostics asserts that no diagnostics are sent for the
// workspace-relative path name. It should be used primarily in conjunction
// with a OnceMet, as it has to check that all outstanding diagnostics have
// already been delivered.
func NoDiagnostics(name string) Expectation {
check := func(s State) Verdict {
if _, ok := s.diagnostics[name]; !ok {
return Met
}
return Unmet
}
return SimpleExpectation{
check: check,
description: "no diagnostics",
}
}
// AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for
// the current edited version of the buffer corresponding to the given
// workdir-relative pathname.
func (e *Env) AnyDiagnosticAtCurrentVersion(name string) DiagnosticExpectation {
version := e.Editor.BufferVersion(name)
isMet := func(diags *protocol.PublishDiagnosticsParams) bool {
return int(diags.Version) == version
}
return DiagnosticExpectation{
isMet: isMet,
description: fmt.Sprintf("any diagnostics at version %d", version),
path: name,
}
}
// DiagnosticAtRegexp expects that there is a diagnostic entry at the start
// position matching the regexp search string re in the buffer specified by
// name. Note that this currently ignores the end position.
func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation {
e.T.Helper()
pos := e.RegexpSearch(name, re)
expectation := DiagnosticAt(name, pos.Line, pos.Column)
expectation.description += fmt.Sprintf(" (location of %q)", re)
return expectation
}
// DiagnosticAt asserts that there is a diagnostic entry at the position
// specified by line and col, for the workdir-relative path name.
func DiagnosticAt(name string, line, col int) DiagnosticExpectation {
isMet := func(diags *protocol.PublishDiagnosticsParams) bool {
for _, d := range diags.Diagnostics {
if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) {
return true
}
}
return false
}
return DiagnosticExpectation{
isMet: isMet,
description: fmt.Sprintf("diagnostic at {line:%d, column:%d}", line, col),
path: name,
}
}
// NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start
// position matching the regexp search string re in the buffer specified by
// name. Note that this currently ignores the end position.
// This should only be used in combination with OnceMet for a given condition,
// otherwise it may always succeed.
func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation {
e.T.Helper()
pos := e.RegexpSearch(name, re)
expectation := NoDiagnosticAt(name, pos.Line, pos.Column)
expectation.description += fmt.Sprintf(" (location of %q)", re)
return expectation
}
// NoDiagnosticAt asserts that there is no diagnostic entry at the position
// specified by line and col, for the workdir-relative path name.
// This should only be used in combination with OnceMet for a given condition,
// otherwise it may always succeed.
func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation {
isMet := func(diags *protocol.PublishDiagnosticsParams) bool {
for _, d := range diags.Diagnostics {
if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) {
return false
}
}
return true
}
return DiagnosticExpectation{
isMet: isMet,
description: fmt.Sprintf("no diagnostic at {line:%d, column:%d}", line, col),
path: name,
}
}