test: lock-protect fakeSender so -race CI passes
CI / Lint (pull_request) Successful in 31s
CI / Build (linux/amd64) (pull_request) Successful in 20s
CI / Build (linux/arm64) (pull_request) Successful in 19s
CI / Test (linux/amd64) (pull_request) Successful in 1m27s
CI / Build (windows/amd64) (pull_request) Successful in 1m34s

The CI runs go test with -race; the agent runner has two pump goroutines
(pumpStdout + pumpStderr) writing through the sender concurrently, and
the unprotected fakeSender slice append raced. The cancel_test had a
local 'safeSender' workaround for the same issue; promote that mutex
onto fakeSender itself so every test in the package is race-clean
without per-test variants.

- fakeSender grows mu sync.Mutex; Send takes/releases. New snapshot()
  helper for tests that want a stable copy.
- cancel_test drops its local safeSender + sync import; uses fakeSender.

Verified: go test -race ./... passes across all packages.
This commit is contained in:
2026-05-04 18:01:35 +01:00
parent e4031d26fa
commit 28d5043eb0
2 changed files with 29 additions and 26 deletions
+5 -24
View File
@@ -3,35 +3,16 @@ package runner
import (
"context"
"strings"
"sync"
"testing"
"time"
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)
// safeSender is a thread-safe variant of fakeSender. The cancel test
// has the runner goroutine sending envelopes while the test goroutine
// is reading the slice, so we need a mutex.
type safeSender struct {
mu sync.Mutex
envs []api.Envelope
}
func (s *safeSender) Send(e api.Envelope) error {
s.mu.Lock()
s.envs = append(s.envs, e)
s.mu.Unlock()
return nil
}
func (s *safeSender) snapshot() []api.Envelope {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]api.Envelope, len(s.envs))
copy(out, s.envs)
return out
}
// (fakeSender is defined in runner_test.go; it's already lock-protected
// because the runner's stdout + stderr pump goroutines call Send
// concurrently. The original local 'safeSender' here was a workaround
// from before fakeSender itself grew the mutex.)
// TestRunBackupCanceledMidRunReportsCanceled spawns a backup against
// a fake restic that sleeps for 30 seconds, cancels the context after
@@ -48,7 +29,7 @@ func TestRunBackupCanceledMidRunReportsCanceled(t *testing.T) {
// SIGKILL fallback path firing — slower and noisier.
bin := setupScript(t, `exec sleep 30`)
tx := &safeSender{}
tx := &fakeSender{}
r := New(Config{ResticBin: bin}, tx, 0)
ctx, cancel := context.WithCancel(context.Background())