tests: a test that sets time.Local does it through testenv.Zone, which first waits out the stand-ins' first runs — their os.Stat reads time.Local, and TestDsh -race alone raced it every run

testenv.StandIns (06420a71) starts each stand-in once in the background on
macOS and records it with os.Stat, which reads time.Local. internal/
sessions' inZone, internal/usage's TestDayFilterDST and the root package's
sessions CLI setup assigned time.Local while those runs could still be
under way. `go test -race -run '^TestDsh$' ./internal/sessions` reported
the race 6 of 6 runs on main (stats_test.go:14 against program.go:140).

testenv.Zone(t, loc) waits on the same WaitGroup RemovePrograms waits on,
sets time.Local and restores it in a Cleanup; the three helpers use it.
The root setup registers Zone before its own Cleanup, so the zone still
goes back after sessions.Reset's index write, as before.

After: the same command 0 races in 6 runs; -race -count=20 of
Dsh|DST over sessions and usage, -race -count=3 of the root Sessions
tests, -race of sessions/testenv/usage/root, go vet, gofmt pass. Tests only.
This commit is contained in:
yetone committed 2026-10-07 06:17:56 +08:00
1 parent 2c2137beb7
commit 395c4bc51e
4 files changed
+22 -9

No files matched your search

+3 -3
View File
@@ -6,13 +6,13 @@ import (
"strconv"
"testing"
"time"
"github.com/yetone/magpie/internal/testenv"
)
// inZone runs the test with the local time zone at a fixed offset.
func inZone(t *testing.T, hours int) {
old := time.Local
time.Local = time.FixedZone("test", hours*3600)
t.Cleanup(func() { time.Local = old })
testenv.Zone(t, time.FixedZone("test", hours*3600))
}
// ccMsg is a line of a Claude Code reply at a time.
+11
View File
@@ -23,6 +23,7 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/yetone/magpie/internal/agentenv"
)
@@ -216,6 +217,16 @@ func StandIns(bin string, names []string) error {
return nil
}
// Zone sets time.Local for the test, and puts it back after. It first
// waits out the stand-ins' first runs: they os.Stat what ran, which reads
// time.Local, so a test that swaps it while they are under way races them.
func Zone(t testing.TB, loc *time.Location) {
warming.Wait()
old := time.Local
time.Local = loc
t.Cleanup(func() { time.Local = old })
}
// warming is the stand-ins' first runs under way.
var warming sync.WaitGroup
+3 -3
View File
@@ -5,6 +5,8 @@ import (
"reflect"
"testing"
"time"
"github.com/yetone/magpie/internal/testenv"
)
func TestRequestPageDay(t *testing.T) {
@@ -63,9 +65,7 @@ func TestDayFilterDST(t *testing.T) {
if err != nil {
t.Fatal(err)
}
old := time.Local
time.Local = loc
t.Cleanup(func() { time.Local = old })
testenv.Zone(t, loc)
for _, day := range []string{"2026-03-08", "2026-11-01"} {
start, err := time.ParseInLocation(time.DateOnly, day, loc)
if err != nil {
+5 -3
View File
@@ -14,6 +14,7 @@ import (
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/sessions"
"github.com/yetone/magpie/internal/settings"
"github.com/yetone/magpie/internal/testenv"
)
// sessionsHome is a sandbox HOME with the sessions package's fixtures as
@@ -40,8 +41,9 @@ func sessionsHome(t *testing.T) time.Time {
}
t.Setenv(env, dir)
}
oldZone, oldPrice := time.Local, sessions.PriceOf
time.Local = time.UTC
// put back after the Reset below, whose index write reads the zone
testenv.Zone(t, time.UTC)
oldPrice := sessions.PriceOf
sessions.PriceOf = func(_ settings.Settings, m string) (catalog.Price, bool) {
switch m {
case "claude-opus-5-5":
@@ -55,7 +57,7 @@ func sessionsHome(t *testing.T) time.Time {
// the index is written behind the page: let that write finish before
// the zone it reads (a stat of the file it writes) goes back
sessions.Reset()
time.Local, sessions.PriceOf = oldZone, oldPrice
sessions.PriceOf = oldPrice
})
sessions.Reset()
return time.Date(2026, 9, 28, 12, 0, 0, 0, time.UTC)