diff options
Diffstat (limited to 'internal/stats')
| -rw-r--r-- | internal/stats/stats.go | 34 | ||||
| -rw-r--r-- | internal/stats/stats_test.go | 12 |
2 files changed, 33 insertions, 13 deletions
diff --git a/internal/stats/stats.go b/internal/stats/stats.go index a5c5cf1..65d1b2d 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -29,9 +29,17 @@ const ( var windowSeconds int64 = int64(defaultWindow.Seconds()) -// nowFunc is the clock source for event timestamps and pruning cutoffs. -// Replaced in tests to control time without sleeping. -var nowFunc = time.Now +// engine carries the injectable dependencies for stats operations. The only +// dependency today is the clock source for event timestamps and pruning +// cutoffs. Production code uses defaultEngine (backed by time.Now); tests +// construct an engine with a fake clock to control time without sleeping. +type engine struct { + now func() time.Time +} + +// defaultEngine is the production engine used by the package-level Update and +// TakeSnapshot wrappers. It reads the real wall clock. +var defaultEngine = engine{now: time.Now} // SetWindow sets the sliding window used for pruning and aggregation. func SetWindow(d time.Duration) { @@ -104,8 +112,14 @@ func (s Snapshot) ScopeRPM(provider, model string) float64 { return float64(reqs) / mins } -// Update appends one event and prunes old entries under lock. +// Update appends one event and prunes old entries under lock, using the +// production clock. It delegates to engine.update. func Update(ctx context.Context, provider, model string, sentBytes, recvBytes int) error { + return defaultEngine.update(ctx, provider, model, sentBytes, recvBytes) +} + +// update appends one event and prunes old entries under lock. +func (e engine) update(ctx context.Context, provider, model string, sentBytes, recvBytes int) error { dir, err := CacheDir() if err != nil { return err @@ -121,7 +135,7 @@ func Update(ctx context.Context, provider, model string, sentBytes, recvBytes in path := filepath.Join(dir, fileName) sf := readStatsFile(path) - now := nowFunc() + now := e.now() win := Window() sf.WindowSeconds = int(win.Seconds()) sf.Events = append(sf.Events, Event{ @@ -216,9 +230,15 @@ func writeStatsFileAtomic(dir, path string, sf *File) error { } // TakeSnapshot reads the stats file and aggregates events within the stored +// window, using the production clock. It delegates to engine.takeSnapshot. +func TakeSnapshot() (Snapshot, error) { + return defaultEngine.takeSnapshot() +} + +// takeSnapshot reads the stats file and aggregates events within the stored // window (falling back to the process-level Window() if the file has none). // This is a pure read — it does not mutate global state. -func TakeSnapshot() (Snapshot, error) { +func (e engine) takeSnapshot() (Snapshot, error) { dir, err := CacheDir() if err != nil { return Snapshot{}, err @@ -239,7 +259,7 @@ func TakeSnapshot() (Snapshot, error) { if win <= 0 { win = Window() } - cutoff := nowFunc().Add(-win) + cutoff := e.now().Add(-win) snap := Snapshot{Providers: make(map[string]ProviderEntry), Window: win} for _, ev := range sf.Events { if ev.TS.Before(cutoff) { diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index fc043a5..2695c4b 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -36,20 +36,20 @@ func TestUpdate_PrunesOld_ByWindow(t *testing.T) { SetWindow(2 * time.Second) ctx := context.Background() - // Inject a fake clock so we can advance time without sleeping. + // Inject a fake clock via an engine so we can advance time without sleeping + // and without mutating package-level state. fakeNow := time.Now() - nowFunc = func() time.Time { return fakeNow } - defer func() { nowFunc = time.Now }() + eng := engine{now: func() time.Time { return fakeNow }} - if err := Update(ctx, "p", "m", 1, 1); err != nil { + if err := eng.update(ctx, "p", "m", 1, 1); err != nil { t.Fatal(err) } // Advance fake time past the 2-second window so the first event is pruned. fakeNow = fakeNow.Add(3 * time.Second) - if err := Update(ctx, "p", "m", 2, 2); err != nil { + if err := eng.update(ctx, "p", "m", 2, 2); err != nil { t.Fatal(err) } - snap, err := TakeSnapshot() + snap, err := eng.takeSnapshot() if err != nil { t.Fatal(err) } |
