mirror of
https://github.com/golang/go
synced 2024-11-18 23:14:43 -07:00
ed308ab3e7
If the user's workspace is neither in GOPATH nor a module, and there are errors in their code, send a message (with ShowMessage) only on the first load, or when the configuration changes. The previous behavior sent the message more frequently. There is a regtest, and two new Expectations for when the fake editor sees (or does not see) a ShowMessage notification. Fixes golang/go#37279 Change-Id: I076bd95105359b9310dcf97019e3559159271356 Reviewed-on: https://go-review.googlesource.com/c/tools/+/230897 Run-TryBot: Peter Weinberger <pjw@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
566 lines
16 KiB
Go
566 lines
16 KiB
Go
// 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 (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"golang.org/x/tools/internal/jsonrpc2"
|
|
"golang.org/x/tools/internal/jsonrpc2/servertest"
|
|
"golang.org/x/tools/internal/lsp/fake"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
)
|
|
|
|
// Env holds an initialized fake Editor, Workspace, and Server, which may be
|
|
// used for writing tests. It also provides adapter methods that call t.Fatal
|
|
// on any error, so that tests for the happy path may be written without
|
|
// checking errors.
|
|
type Env struct {
|
|
T *testing.T
|
|
Ctx context.Context
|
|
|
|
// Most tests should not need to access the workspace, editor, server, or
|
|
// connection, but they are available if needed.
|
|
W *fake.Workspace
|
|
E *fake.Editor
|
|
Server servertest.Connector
|
|
Conn *jsonrpc2.Conn
|
|
|
|
// mu guards the fields below, for the purpose of checking conditions on
|
|
// every change to diagnostics.
|
|
mu sync.Mutex
|
|
// For simplicity, each waiter gets a unique ID.
|
|
nextWaiterID int
|
|
state State
|
|
waiters map[int]*condition
|
|
}
|
|
|
|
// State encapsulates the server state TODO: explain more
|
|
type State struct {
|
|
// diagnostics are a map of relative path->diagnostics params
|
|
diagnostics map[string]*protocol.PublishDiagnosticsParams
|
|
logs []*protocol.LogMessageParams
|
|
showMessage []*protocol.ShowMessageParams
|
|
// outstandingWork is a map of token->work summary. All tokens are assumed to
|
|
// be string, though the spec allows for numeric tokens as well. When work
|
|
// completes, it is deleted from this map.
|
|
outstandingWork map[string]*workProgress
|
|
completedWork map[string]int
|
|
}
|
|
|
|
type workProgress struct {
|
|
title string
|
|
percent float64
|
|
}
|
|
|
|
func (s State) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("#### log messages (see RPC logs for full text):\n")
|
|
for _, msg := range s.logs {
|
|
summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message)
|
|
if len(summary) > 60 {
|
|
summary = summary[:57] + "..."
|
|
}
|
|
// Some logs are quite long, and since they should be reproduced in the RPC
|
|
// logs on any failure we include here just a short summary.
|
|
fmt.Fprint(&b, "\t"+summary+"\n")
|
|
}
|
|
b.WriteString("\n")
|
|
b.WriteString("#### diagnostics:\n")
|
|
for name, params := range s.diagnostics {
|
|
fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version))
|
|
for _, d := range params.Diagnostics {
|
|
fmt.Fprintf(&b, "\t\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message)
|
|
}
|
|
}
|
|
b.WriteString("\n")
|
|
b.WriteString("#### outstanding work:\n")
|
|
for token, state := range s.outstandingWork {
|
|
name := state.title
|
|
if name == "" {
|
|
name = fmt.Sprintf("!NO NAME(token: %s)", token)
|
|
}
|
|
fmt.Fprintf(&b, "\t%s: %.2f", name, state.percent)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// A condition is satisfied when all expectations are simultaneously
|
|
// met. At that point, the 'met' channel is closed. On any failure, err is set
|
|
// and the failed channel is closed.
|
|
type condition struct {
|
|
expectations []Expectation
|
|
verdict chan Verdict
|
|
}
|
|
|
|
// NewEnv creates a new test environment using the given workspace and gopls
|
|
// server.
|
|
func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest.Connector) *Env {
|
|
t.Helper()
|
|
conn := ts.Connect(ctx)
|
|
editor, err := fake.NewConnectedEditor(ctx, ws, conn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := &Env{
|
|
T: t,
|
|
Ctx: ctx,
|
|
W: ws,
|
|
E: editor,
|
|
Server: ts,
|
|
Conn: conn,
|
|
state: State{
|
|
diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
|
|
outstandingWork: make(map[string]*workProgress),
|
|
completedWork: make(map[string]int),
|
|
},
|
|
waiters: make(map[int]*condition),
|
|
}
|
|
env.E.Client().OnDiagnostics(env.onDiagnostics)
|
|
env.E.Client().OnLogMessage(env.onLogMessage)
|
|
env.E.Client().OnWorkDoneProgressCreate(env.onWorkDoneProgressCreate)
|
|
env.E.Client().OnProgress(env.onProgress)
|
|
env.E.Client().OnShowMessage(env.onShowMessage)
|
|
return env
|
|
}
|
|
|
|
func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
pth := e.W.URIToPath(d.URI)
|
|
e.state.diagnostics[pth] = d
|
|
e.checkConditionsLocked()
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
e.state.showMessage = append(e.state.showMessage, m)
|
|
e.checkConditionsLocked()
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
e.state.logs = append(e.state.logs, m)
|
|
e.checkConditionsLocked()
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
token := m.Token.(string)
|
|
e.state.outstandingWork[token] = &workProgress{}
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
token := m.Token.(string)
|
|
work, ok := e.state.outstandingWork[token]
|
|
if !ok {
|
|
panic(fmt.Sprintf("got progress report for unknown report %s: %v", token, m))
|
|
}
|
|
v := m.Value.(map[string]interface{})
|
|
switch kind := v["kind"]; kind {
|
|
case "begin":
|
|
work.title = v["title"].(string)
|
|
case "report":
|
|
if pct, ok := v["percentage"]; ok {
|
|
work.percent = pct.(float64)
|
|
}
|
|
case "end":
|
|
title := e.state.outstandingWork[token].title
|
|
e.state.completedWork[title] = e.state.completedWork[title] + 1
|
|
delete(e.state.outstandingWork, token)
|
|
}
|
|
e.checkConditionsLocked()
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) checkConditionsLocked() {
|
|
for id, condition := range e.waiters {
|
|
if v, _, _ := checkExpectations(e.state, condition.expectations); v != Unmet {
|
|
delete(e.waiters, id)
|
|
condition.verdict <- v
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExpectNow asserts that the current state of the editor matches the given
|
|
// expectations.
|
|
//
|
|
// It can be used together with Env.Await to allow waiting on
|
|
// simple expectations, followed by more detailed expectations tested by
|
|
// ExpectNow. For example:
|
|
//
|
|
// env.RegexpReplace("foo.go", "a", "x")
|
|
// env.Await(env.AnyDiagnosticAtCurrentVersion("foo.go"))
|
|
// env.ExpectNow(env.DiagnosticAtRegexp("foo.go", "x"))
|
|
//
|
|
// This has the advantage of not timing out if the diagnostic received for
|
|
// "foo.go" does not match the expectation: instead it fails early.
|
|
func (e *Env) ExpectNow(expectations ...Expectation) {
|
|
e.T.Helper()
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if verdict, summary, _ := checkExpectations(e.state, expectations); verdict != Met {
|
|
e.T.Fatalf("expectations unmet:\n%s\ncurrent state:\n%v", summary, e.state)
|
|
}
|
|
}
|
|
|
|
// checkExpectations reports whether s meets all expectations.
|
|
func checkExpectations(s State, expectations []Expectation) (Verdict, string, []interface{}) {
|
|
finalVerdict := Met
|
|
var metBy []interface{}
|
|
var summary strings.Builder
|
|
for _, e := range expectations {
|
|
v, mb := e.Check(s)
|
|
if v == Met {
|
|
metBy = append(metBy, mb)
|
|
}
|
|
if v > finalVerdict {
|
|
finalVerdict = v
|
|
}
|
|
summary.WriteString(fmt.Sprintf("\t%v: %s\n", v, e.Description()))
|
|
}
|
|
return finalVerdict, summary.String(), metBy
|
|
}
|
|
|
|
// 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, interface{})
|
|
// Description is a human-readable description of the expectation.
|
|
Description() string
|
|
}
|
|
|
|
// 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
|
|
)
|
|
|
|
// 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, interface{}) {
|
|
switch pre, _ := precondition.Check(s); pre {
|
|
case Unmeetable:
|
|
return Unmeetable, nil
|
|
case Met:
|
|
verdict, metBy := mustMeet.Check(s)
|
|
if verdict != Met {
|
|
return Unmeetable, metBy
|
|
}
|
|
return Met, metBy
|
|
default:
|
|
return Unmet, nil
|
|
}
|
|
}
|
|
return &SimpleExpectation{
|
|
check: check,
|
|
description: fmt.Sprintf("once %q is met, must have %q", precondition.Description(), mustMeet.Description()),
|
|
}
|
|
}
|
|
|
|
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, interface{})
|
|
description string
|
|
}
|
|
|
|
// Check invokes e.check.
|
|
func (e SimpleExpectation) Check(s State) (Verdict, interface{}) {
|
|
return e.check(s)
|
|
}
|
|
|
|
// Description returns e.descriptin.
|
|
func (e SimpleExpectation) Description() string {
|
|
return e.description
|
|
}
|
|
|
|
// 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, interface{}) {
|
|
if len(s.outstandingWork) == 0 {
|
|
return Met, nil
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
return SimpleExpectation{
|
|
check: check,
|
|
description: "no outstanding work",
|
|
}
|
|
}
|
|
|
|
// EmptyShowMessage asserts that the editor has not received a ShowMessage.
|
|
func EmptyShowMessage(title string) SimpleExpectation {
|
|
check := func(s State) (Verdict, interface{}) {
|
|
if len(s.showMessage) == 0 {
|
|
return Met, title
|
|
}
|
|
return Unmeetable, nil
|
|
}
|
|
return SimpleExpectation{
|
|
check: check,
|
|
description: "no ShowMessage received",
|
|
}
|
|
}
|
|
|
|
// SomeShowMessage asserts that the editor has received a ShowMessage.
|
|
func SomeShowMessage(title string) SimpleExpectation {
|
|
check := func(s State) (Verdict, interface{}) {
|
|
if len(s.showMessage) > 0 {
|
|
return Met, title
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
return SimpleExpectation{
|
|
check: check,
|
|
description: "received ShowMessage",
|
|
}
|
|
}
|
|
|
|
// 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, interface{}) {
|
|
if s.completedWork[title] >= atLeast {
|
|
return Met, title
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
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, interface{})
|
|
description string
|
|
}
|
|
|
|
// Check implements the Expectation interface.
|
|
func (e LogExpectation) Check(s State) (Verdict, interface{}) {
|
|
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 {
|
|
check := func(msgs []*protocol.LogMessageParams) (Verdict, interface{}) {
|
|
for _, msg := range msgs {
|
|
if msg.Type == protocol.Error {
|
|
return Unmeetable, nil
|
|
}
|
|
}
|
|
return Met, nil
|
|
}
|
|
return LogExpectation{
|
|
check: check,
|
|
description: "no errors have been logged",
|
|
}
|
|
}
|
|
|
|
// LogMatching asserts that the client has received a log message
|
|
// matching of type typ matching the regexp re.
|
|
func LogMatching(typ protocol.MessageType, re string) LogExpectation {
|
|
rec, err := regexp.Compile(re)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
check := func(msgs []*protocol.LogMessageParams) (Verdict, interface{}) {
|
|
for _, msg := range msgs {
|
|
if msg.Type == typ && rec.Match([]byte(msg.Message)) {
|
|
return Met, msg
|
|
}
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
return LogExpectation{
|
|
check: check,
|
|
description: fmt.Sprintf("log message 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 workspace-relative path to the file being asserted on.
|
|
path string
|
|
}
|
|
|
|
// Check implements the Expectation interface.
|
|
func (e DiagnosticExpectation) Check(s State) (Verdict, interface{}) {
|
|
if diags, ok := s.diagnostics[e.path]; ok && e.isMet(diags) {
|
|
return Met, diags
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
|
|
// Description implements the Expectation interface.
|
|
func (e DiagnosticExpectation) Description() string {
|
|
return fmt.Sprintf("%s: %s", e.path, e.description)
|
|
}
|
|
|
|
// EmptyDiagnostics asserts that diagnostics are empty for the
|
|
// workspace-relative path name.
|
|
func EmptyDiagnostics(name string) Expectation {
|
|
check := func(s State) (Verdict, interface{}) {
|
|
if diags, ok := s.diagnostics[name]; !ok || len(diags.Diagnostics) == 0 {
|
|
return Met, nil
|
|
}
|
|
return Unmet, nil
|
|
}
|
|
return SimpleExpectation{
|
|
check: check,
|
|
description: "empty diagnostics",
|
|
}
|
|
}
|
|
|
|
// AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for
|
|
// the current edited version of the buffer corresponding to the given
|
|
// workspace-relative pathname.
|
|
func (e *Env) AnyDiagnosticAtCurrentVersion(name string) DiagnosticExpectation {
|
|
version := e.E.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 {
|
|
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 workspace-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,
|
|
}
|
|
}
|
|
|
|
// Await waits for all expectations to simultaneously be met. It should only be
|
|
// called from the main test goroutine.
|
|
func (e *Env) Await(expectations ...Expectation) []interface{} {
|
|
e.T.Helper()
|
|
e.mu.Lock()
|
|
// Before adding the waiter, we check if the condition is currently met or
|
|
// failed to avoid a race where the condition was realized before Await was
|
|
// called.
|
|
switch verdict, summary, metBy := checkExpectations(e.state, expectations); verdict {
|
|
case Met:
|
|
e.mu.Unlock()
|
|
return metBy
|
|
case Unmeetable:
|
|
e.mu.Unlock()
|
|
e.T.Fatalf("unmeetable expectations:\n%s\nstate:\n%v", summary, e.state)
|
|
}
|
|
cond := &condition{
|
|
expectations: expectations,
|
|
verdict: make(chan Verdict),
|
|
}
|
|
e.waiters[e.nextWaiterID] = cond
|
|
e.nextWaiterID++
|
|
e.mu.Unlock()
|
|
|
|
var err error
|
|
select {
|
|
case <-e.Ctx.Done():
|
|
err = e.Ctx.Err()
|
|
case v := <-cond.verdict:
|
|
if v != Met {
|
|
err = fmt.Errorf("condition has final verdict %v", v)
|
|
}
|
|
}
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
_, summary, metBy := checkExpectations(e.state, expectations)
|
|
|
|
// Debugging an unmet expectation can be tricky, so we put some effort into
|
|
// nicely formatting the failure.
|
|
if err != nil {
|
|
e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state)
|
|
}
|
|
return metBy
|
|
}
|