From 4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 18 Jun 2026 07:45:37 +0300 Subject: ik0 replace test seams with dependency injection --- internal/stats/stats.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) (limited to 'internal/stats/stats.go') 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) { -- cgit v1.2.3