From 84a2ea0f027db2114bf4d2205eeedd95b520641d Mon Sep 17 00:00:00 2001 From: Dibbayajyoti Date: Tue, 27 Oct 2026 13:26:14 +0530 Subject: [PATCH] test: read recorded requests under the recording server's lock The recording server appends under a mutex, but the reqs() accessors of the logs --watch tests read the slice with no lock. A watch stops by cancelling its context, which can leave a poll inside the handler; that append has no happens-before edge to the later read, so -race reported a data race intermittently (TestLogsWatchPlainPagesBackToCloseAGap, ~1 in 50 runs). Product code is not involved: logsWatchPlain is single-goroutine, and the TUI hands each poll its own copy of the known-keys map. Share the mutex with a recordedSoFar reader used by both accessors. --- internal/cmd/helpers_test.go | 22 ++++++++++++++++------ internal/cmd/logs_watch_test.go | 4 ++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/cmd/helpers_test.go b/internal/cmd/helpers_test.go index 341bef5..6049fed 100644 --- a/internal/cmd/helpers_test.go +++ b/internal/cmd/helpers_test.go @@ -91,18 +91,28 @@ type recordedRequest struct { Body []byte } +// recordedMu orders the servers' appends against recordedSoFar. A reader that +// runs while a cancelled request is still inside its handler (a stopped watch) +// has no other happens-before edge to that append. +var recordedMu sync.Mutex + +// recordedSoFar is *reqs for a reader that may overlap a handler still running. +// Appends only write past len, so the elements of the returned slice are stable. +func recordedSoFar(reqs *[]recordedRequest) []recordedRequest { + recordedMu.Lock() + defer recordedMu.Unlock() + return *reqs +} + func newRecordingServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *[]recordedRequest) { t.Helper() - var ( - reqs []recordedRequest - mu sync.Mutex - ) + var reqs []recordedRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var b bytes.Buffer _, _ = b.ReadFrom(r.Body) - mu.Lock() + recordedMu.Lock() reqs = append(reqs, recordedRequest{r.Method, r.URL.Path, r.URL.RawQuery, r.Header.Clone(), b.Bytes()}) - mu.Unlock() + recordedMu.Unlock() r.Body = http.NoBody handler(w, r) })) diff --git a/internal/cmd/logs_watch_test.go b/internal/cmd/logs_watch_test.go index d3cb430..f1b1e20 100644 --- a/internal/cmd/logs_watch_test.go +++ b/internal/cmd/logs_watch_test.go @@ -39,7 +39,7 @@ func logsScripted(t *testing.T, bodies ...string) (string, func() []recordedRequ i := min(int(n.Add(1))-1, len(bodies)-1) _, _ = w.Write([]byte(bodies[i])) }) - return srv.URL, func() []recordedRequest { return *reqs } + return srv.URL, func() []recordedRequest { return recordedSoFar(reqs) } } var ( @@ -319,7 +319,7 @@ func logsTUIServer(t *testing.T) (string, func() []recordedRequest) { } _, _ = w.Write([]byte(logsList(false, e))) }) - return srv.URL, func() []recordedRequest { return *reqs } + return srv.URL, func() []recordedRequest { return recordedSoFar(reqs) } } func TestLogsWatchFullScreenQuitsOnQ(t *testing.T) {