summaryrefslogtreecommitdiff
path: root/internal/stats/stats.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
commit4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch)
tree2dc708cbb95975d34084eb5afd376871caa13e27 /internal/stats/stats.go
parentece7dcfd232b780f5650326c8ac2379ca70387d4 (diff)
ik0 replace test seams with dependency injection
Diffstat (limited to 'internal/stats/stats.go')
-rw-r--r--internal/stats/stats.go34
1 files changed, 27 insertions, 7 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) {