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) + } +}