Skip to content

Commit e2f3b25

Browse files
committed
gopls/internal/bug: record bug reports in file cache
This change causes bug.Report to save a copy of the bug report in the file cache, and the 'gopls stats' command to report all such bug reports associated with the same executable. The bug.Notify mechanism was made synchronous (and renamed bug.Handle) so that the handler has time to record the report before the process terminates. The handler is registered early during filecache initialization. The test uses an undocumented environment variable, TEST_GOPLS_BUG, which causes the stats command (!) to simply report a bug and return. Change-Id: I8b84ad3b8f05386844f00faf5728106495b56294 Reviewed-on: https://go-review.googlesource.com/c/tools/+/488795 Run-TryBot: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent ab24b7b commit e2f3b25

File tree

7 files changed

+141
-47
lines changed

7 files changed

+141
-47
lines changed

gopls/internal/bug/bug.go

+18-18
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var PanicOnBugs = false
2727
var (
2828
mu sync.Mutex
2929
exemplars map[string]Bug
30-
waiters []chan<- Bug
30+
handlers []func(Bug)
3131
)
3232

3333
// A Bug represents an unexpected event or broken invariant. They are used for
@@ -80,31 +80,31 @@ func report(description string) {
8080
}
8181

8282
mu.Lock()
83-
defer mu.Unlock()
84-
85-
if exemplars == nil {
86-
exemplars = make(map[string]Bug)
87-
}
88-
8983
if _, ok := exemplars[key]; !ok {
84+
if exemplars == nil {
85+
exemplars = make(map[string]Bug)
86+
}
9087
exemplars[key] = bug // capture one exemplar per key
9188
}
92-
93-
for _, waiter := range waiters {
94-
waiter <- bug
89+
hh := handlers
90+
handlers = nil
91+
mu.Unlock()
92+
93+
// Call the handlers outside the critical section since a
94+
// handler may itself fail and call bug.Report. Since handlers
95+
// are one-shot, the inner call should be trivial.
96+
for _, handle := range hh {
97+
handle(bug)
9598
}
96-
waiters = nil
9799
}
98100

99-
// Notify returns a channel that will be sent the next bug to occur on the
100-
// server. This channel only ever receives one bug.
101-
func Notify() <-chan Bug {
101+
// Handle adds a handler function that will be called with the next
102+
// bug to occur on the server. The handler only ever receives one bug.
103+
// It is called synchronously, and should return in a timely manner.
104+
func Handle(h func(Bug)) {
102105
mu.Lock()
103106
defer mu.Unlock()
104-
105-
ch := make(chan Bug, 1) // 1-buffered so that bug reporting is non-blocking
106-
waiters = append(waiters, ch)
107-
return ch
107+
handlers = append(handlers, h)
108108
}
109109

110110
// List returns a slice of bug exemplars -- the first bugs to occur at each

gopls/internal/bug/bug_test.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func resetForTesting() {
1313
exemplars = nil
14-
waiters = nil
14+
handlers = nil
1515
}
1616

1717
func TestListBugs(t *testing.T) {
@@ -44,19 +44,21 @@ func wantBugs(t *testing.T, want ...string) {
4444
}
4545
}
4646

47-
func TestBugNotification(t *testing.T) {
47+
func TestBugHandler(t *testing.T) {
4848
defer resetForTesting()
4949

5050
Report("unseen")
5151

52-
notify1 := Notify()
53-
notify2 := Notify()
52+
// Both handlers are called, in order of registration, only once.
53+
var got string
54+
Handle(func(b Bug) { got += "1:" + b.Description })
55+
Handle(func(b Bug) { got += "2:" + b.Description })
5456

5557
Report("seen")
5658

57-
for _, got := range []Bug{<-notify1, <-notify2} {
58-
if got, want := got.Description, "seen"; got != want {
59-
t.Errorf("Saw bug %q, want %q", got, want)
60-
}
59+
Report("again")
60+
61+
if want := "1:seen2:seen"; got != want {
62+
t.Errorf("got %q, want %q", got, want)
6163
}
6264
}

gopls/internal/lsp/cmd/stats.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"golang.org/x/tools/gopls/internal/lsp"
2222
"golang.org/x/tools/gopls/internal/lsp/command"
2323
"golang.org/x/tools/gopls/internal/lsp/debug"
24+
"golang.org/x/tools/gopls/internal/lsp/filecache"
2425
"golang.org/x/tools/gopls/internal/lsp/protocol"
2526
"golang.org/x/tools/gopls/internal/lsp/source"
2627
)
@@ -47,6 +48,15 @@ Example:
4748
}
4849

4950
func (s *stats) Run(ctx context.Context, args ...string) error {
51+
52+
// This undocumented environment variable allows
53+
// the cmd integration test to trigger a call to bug.Report.
54+
if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" {
55+
filecache.Start() // effect: register bug handler
56+
goplsbug.Report(msg)
57+
return nil
58+
}
59+
5060
if s.app.Remote != "" {
5161
// stats does not work with -remote.
5262
// Other sessions on the daemon may interfere with results.
@@ -126,12 +136,15 @@ func (s *stats) Run(ctx context.Context, args ...string) error {
126136
}
127137
defer conn.terminate(ctx)
128138

129-
// bug.List only reports bugs that have been encountered in the current
130-
// process, so only list bugs after the initial workspace load has completed.
131-
//
132-
// TODO(rfindley): persist bugs to the gopls cache, so that they can be
133-
// interrogated.
134-
stats.Bugs = goplsbug.List()
139+
// Gather bug reports produced by any process using
140+
// this executable and persisted in the cache.
141+
stats.BugReports = []string{} // non-nil for JSON
142+
do("Gathering bug reports", func() error {
143+
for _, report := range filecache.BugReports() {
144+
stats.BugReports = append(stats.BugReports, string(report))
145+
}
146+
return nil
147+
})
135148

136149
if _, err := do("Querying memstats", func() error {
137150
memStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
@@ -184,7 +197,7 @@ type GoplsStats struct {
184197
GoVersion string
185198
GoplsVersion string
186199
InitialWorkspaceLoadDuration string // in time.Duration string form
187-
Bugs []goplsbug.Bug
200+
BugReports []string
188201
MemStats command.MemStatsResult
189202
WorkspaceStats command.WorkspaceStatsResult
190203
DirStats dirStats

gopls/internal/lsp/cmd/test/cmdtest.go

-6
This file was deleted.

gopls/internal/lsp/cmd/test/integration_test.go

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2023 The Go Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
4+
5+
// Package cmdtest contains the test suite for the command line behavior of gopls.
46
package cmdtest
57

68
// This file defines integration tests of each gopls subcommand that
@@ -28,6 +30,7 @@ import (
2830
"context"
2931
"encoding/json"
3032
"fmt"
33+
"math/rand"
3134
"os"
3235
"path/filepath"
3336
"regexp"
@@ -694,6 +697,15 @@ package b
694697
package foo
695698
`)
696699

700+
// Trigger a bug report with a distinctive string
701+
// and check that it was durably recorded.
702+
oops := fmt.Sprintf("oops-%d", rand.Int())
703+
{
704+
env := []string{"TEST_GOPLS_BUG=" + oops}
705+
res := goplsWithEnv(t, tree, env, "stats")
706+
res.checkExit(true)
707+
}
708+
697709
res := gopls(t, tree, "stats")
698710
res.checkExit(true)
699711

@@ -728,6 +740,22 @@ package foo
728740
t.Errorf("stats.%s = %d, want %d", check.field, check.got, check.want)
729741
}
730742
}
743+
744+
// Check that we got a BugReport with the expected message.
745+
{
746+
got := fmt.Sprint(stats.BugReports)
747+
wants := []string{
748+
"cmd/stats.go", // File containing call to bug.Report
749+
oops, // Description
750+
}
751+
for _, want := range wants {
752+
if !strings.Contains(got, want) {
753+
t.Errorf("BugReports does not contain %q. Got:<<%s>>", want, got)
754+
break
755+
}
756+
}
757+
}
758+
731759
}
732760

733761
// TestFix tests the 'fix' subcommand (../suggested_fix.go).
@@ -814,7 +842,12 @@ func TestMain(m *testing.M) {
814842

815843
// This function is a stand-in for gopls.main in ../../../../main.go.
816844
func goplsMain() {
817-
bug.PanicOnBugs = true // (not in the production command)
845+
// Panic on bugs (unlike the production gopls command),
846+
// except in tests that inject calls to bug.Report.
847+
if os.Getenv("TEST_GOPLS_BUG") == "" {
848+
bug.PanicOnBugs = true
849+
}
850+
818851
tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
819852
}
820853

@@ -844,6 +877,10 @@ func writeTree(t *testing.T, archive string) string {
844877

845878
// gopls executes gopls in a child process.
846879
func gopls(t *testing.T, dir string, args ...string) *result {
880+
return goplsWithEnv(t, dir, nil, args...)
881+
}
882+
883+
func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *result {
847884
testenv.NeedsTool(t, "go")
848885

849886
// Catch inadvertent use of dir=".", which would make
@@ -857,6 +894,7 @@ func gopls(t *testing.T, dir string, args ...string) *result {
857894
"ENTRYPOINT=goplsMain",
858895
fmt.Sprintf("%s=true", cmd.DebugSuggestedFixEnvVar),
859896
)
897+
goplsCmd.Env = append(goplsCmd.Env, env...)
860898
goplsCmd.Dir = dir
861899
goplsCmd.Stdout = new(bytes.Buffer)
862900
goplsCmd.Stderr = new(bytes.Buffer)

gopls/internal/lsp/filecache/filecache.go

+47
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import (
2424
"bytes"
2525
"crypto/sha256"
2626
"encoding/binary"
27+
"encoding/hex"
2728
"errors"
2829
"fmt"
2930
"hash/crc32"
3031
"io"
32+
"io/fs"
3133
"log"
3234
"os"
3335
"path/filepath"
@@ -420,3 +422,48 @@ func gc(goplsDir string) {
420422
}
421423
}
422424
}
425+
426+
const bugKind = "bug" // reserved kind for gopls bug reports
427+
428+
func init() {
429+
// Register a handler to durably record this process's first
430+
// assertion failure in the cache so that we can ask users to
431+
// share this information via the stats command.
432+
bug.Handle(func(bug bug.Bug) {
433+
// Wait for cache init (bugs in tests happen early).
434+
_, _ = getCacheDir()
435+
436+
value := []byte(fmt.Sprintf("%s: %+v", time.Now().Format(time.RFC3339), bug))
437+
key := sha256.Sum256(value)
438+
_ = Set(bugKind, key, value)
439+
})
440+
}
441+
442+
// BugReports returns a new unordered array of the contents
443+
// of all cached bug reports produced by this executable.
444+
func BugReports() [][]byte {
445+
dir, err := getCacheDir()
446+
if err != nil {
447+
return nil // ignore initialization errors
448+
}
449+
var result [][]byte
450+
_ = filepath.Walk(filepath.Join(dir, bugKind),
451+
func(path string, info fs.FileInfo, err error) error {
452+
if err != nil {
453+
return nil // ignore readdir/stat errors
454+
}
455+
if !info.IsDir() {
456+
var key [32]byte
457+
n, err := hex.Decode(key[:], []byte(filepath.Base(path)))
458+
if err != nil || n != len(key) {
459+
return nil // ignore malformed file names
460+
}
461+
content, err := Get(bugKind, key)
462+
if err == nil { // ignore read errors
463+
result = append(result, content)
464+
}
465+
}
466+
return nil
467+
})
468+
return result
469+
}

gopls/internal/lsp/general.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ
6363

6464
if options.ShowBugReports {
6565
// Report the next bug that occurs on the server.
66-
bugCh := bug.Notify()
67-
go func() {
68-
b := <-bugCh
66+
bug.Handle(func(b bug.Bug) {
6967
msg := &protocol.ShowMessageParams{
7068
Type: protocol.Error,
7169
Message: fmt.Sprintf("A bug occurred on the server: %s\nLocation:%s", b.Description, b.Key),
7270
}
73-
if err := s.eventuallyShowMessage(context.Background(), msg); err != nil {
74-
log.Printf("error showing bug: %v", err)
75-
}
76-
}()
71+
go func() {
72+
if err := s.eventuallyShowMessage(context.Background(), msg); err != nil {
73+
log.Printf("error showing bug: %v", err)
74+
}
75+
}()
76+
})
7777
}
7878

7979
folders := params.WorkspaceFolders

0 commit comments

Comments
 (0)