internal/lsp/regtest: track outstanding work using the progress API

In preparation for later changes, add support for tracking outstanding
work in the lsp regtests. This simply threads through progress
notifications and tracks their state in regtest.Env.state. A new
Expectation is added to assert that there is no outstanding work, but
this is as-yet unused.

A unit test is added for Env to check that we're handling work progress
reports correctly after Marshaling/Unmarshaling, since we're not yet
exercising this code path in actual regtests.

Change-Id: I104caf25cfd49340f13d086314f5aef2b8f3bd3b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/229320
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Rob Findley 2020-04-21 23:44:31 -04:00 committed by Robert Findley
parent 2723c5de0d
commit 38a97e00a8
3 changed files with 183 additions and 6 deletions

View File

@ -10,13 +10,16 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
)
// Client is an adapter that converts a *Client into an LSP Client.
// Client is an adapter that converts an *Editor into an LSP Client. It mosly
// delegates functionality to hooks that can be configured by tests.
type Client struct {
*Editor
// Hooks for testing. Add additional hooks here as needed for testing.
onLogMessage func(context.Context, *protocol.LogMessageParams) error
onDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error
onLogMessage func(context.Context, *protocol.LogMessageParams) error
onDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error
onWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error
onProgress func(context.Context, *protocol.ProgressParams) error
}
// OnLogMessage sets the hook to run when the editor receives a log message.
@ -34,6 +37,18 @@ func (c *Client) OnDiagnostics(hook func(context.Context, *protocol.PublishDiagn
c.mu.Unlock()
}
func (c *Client) OnWorkDoneProgressCreate(hook func(context.Context, *protocol.WorkDoneProgressCreateParams) error) {
c.mu.Lock()
c.onWorkDoneProgressCreate = hook
c.mu.Unlock()
}
func (c *Client) OnProgress(hook func(context.Context, *protocol.ProgressParams) error) {
c.mu.Lock()
c.onProgress = hook
c.mu.Unlock()
}
func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error {
c.mu.Lock()
c.lastMessage = params
@ -97,11 +112,23 @@ func (c *Client) UnregisterCapability(context.Context, *protocol.UnregistrationP
return nil
}
func (c *Client) Progress(context.Context, *protocol.ProgressParams) error {
func (c *Client) Progress(ctx context.Context, params *protocol.ProgressParams) error {
c.mu.Lock()
onProgress := c.onProgress
c.mu.Unlock()
if onProgress != nil {
return onProgress(ctx, params)
}
return nil
}
func (c *Client) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
func (c *Client) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error {
c.mu.Lock()
onCreate := c.onWorkDoneProgressCreate
c.mu.Unlock()
if onCreate != nil {
return onCreate(ctx, params)
}
return nil
}

View File

@ -346,6 +346,15 @@ type State struct {
// diagnostics are a map of relative path->diagnostics params
diagnostics map[string]*protocol.PublishDiagnosticsParams
logs []*protocol.LogMessageParams
// 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
}
type workProgress struct {
title string
percent float64
}
func (s State) String() string {
@ -368,6 +377,15 @@ func (s State) String() string {
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()
}
@ -396,12 +414,15 @@ func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest
Server: ts,
Conn: conn,
state: State{
diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
outstandingWork: make(map[string]*workProgress),
},
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)
return env
}
@ -423,6 +444,38 @@ func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) erro
return nil
}
func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error {
e.mu.Lock()
defer e.mu.Unlock()
// panic if we don't have a string token.
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", token))
}
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":
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 {
@ -512,6 +565,37 @@ func (v Verdict) String() string {
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
}
// 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",
}
}
// LogExpectation is an expectation on the log messages received by the editor
// from gopls.
type LogExpectation struct {

View File

@ -0,0 +1,66 @@
// 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"
"encoding/json"
"testing"
"golang.org/x/tools/internal/lsp/protocol"
)
func TestProgressUpdating(t *testing.T) {
e := &Env{
state: State{
outstandingWork: make(map[string]*workProgress),
},
}
ctx := context.Background()
if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{
Token: "foo",
}); err != nil {
t.Fatal(err)
}
if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{
Token: "bar",
}); err != nil {
t.Fatal(err)
}
updates := []struct {
token string
value interface{}
}{
{"foo", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "foo work"}},
{"bar", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "bar work"}},
{"foo", protocol.WorkDoneProgressEnd{Kind: "end"}},
{"bar", protocol.WorkDoneProgressReport{Kind: "report", Percentage: 42}},
}
for _, update := range updates {
params := &protocol.ProgressParams{
Token: update.token,
Value: update.value,
}
data, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}
var unmarshaled protocol.ProgressParams
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatal(err)
}
if err := e.onProgress(ctx, &unmarshaled); err != nil {
t.Fatal(err)
}
}
if _, ok := e.state.outstandingWork["foo"]; ok {
t.Error("got work entry for \"foo\", want none")
}
got := *e.state.outstandingWork["bar"]
want := workProgress{title: "bar work", percent: 42}
if got != want {
t.Errorf("work progress for \"bar\": %v, want %v", got, want)
}
}