From 38a97e00a8a1ba3a77e37579de4c14fd787e6c34 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 21 Apr 2020 23:44:31 -0400 Subject: [PATCH] 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 TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/fake/client.go | 37 ++++++++++++-- internal/lsp/regtest/env.go | 86 +++++++++++++++++++++++++++++++- internal/lsp/regtest/env_test.go | 66 ++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 internal/lsp/regtest/env_test.go diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go index 74aeff8816..66bbe63501 100644 --- a/internal/lsp/fake/client.go +++ b/internal/lsp/fake/client.go @@ -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 } diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index 486beb00c9..2bec2c5492 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -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 { diff --git a/internal/lsp/regtest/env_test.go b/internal/lsp/regtest/env_test.go new file mode 100644 index 0000000000..4bd0fe41b1 --- /dev/null +++ b/internal/lsp/regtest/env_test.go @@ -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) + } +}