main
stats_test.go
Eric Bower
·
2026-08-16
1package main
2
3import (
4 "path/filepath"
5 "testing"
6 "time"
7)
8
9func TestStats_EMACalculation(t *testing.T) {
10 // Formula: EMA_new = 0.3 * actual + 0.7 * EMA_prev
11 // If EMA_prev == 0, EMA_new = actual
12 stats := &RepoStats{
13 Repo: "testrepo",
14 Tasks: map[string]*TaskStats{
15 "fmt": {
16 AvgDurationMs: 1000,
17 LastDurationMs: 1000,
18 LastStatus: "success",
19 LastExitCode: 0,
20 SeenCount: 1,
21 },
22 },
23 }
24
25 // Update with a new run of 2000ms
26 updateTaskStats(stats, "fmt", 2000*time.Millisecond, 0)
27 task := stats.Tasks["fmt"]
28 if task.SeenCount != 2 {
29 t.Errorf("expected seen count 2, got %d", task.SeenCount)
30 }
31 // 0.3 * 2000 + 0.7 * 1000 = 600 + 700 = 1300
32 if task.AvgDurationMs != 1300 {
33 t.Errorf("expected AvgDurationMs 1300, got %d", task.AvgDurationMs)
34 }
35 if task.LastDurationMs != 2000 {
36 t.Errorf("expected LastDurationMs 2000, got %d", task.LastDurationMs)
37 }
38 if task.LastStatus != "success" {
39 t.Errorf("expected LastStatus success, got %s", task.LastStatus)
40 }
41 if task.FailureStreak != 0 {
42 t.Errorf("expected FailureStreak 0, got %d", task.FailureStreak)
43 }
44
45 // Update with new task (not seen before)
46 updateTaskStats(stats, "lint", 4000*time.Millisecond, 1)
47 lintTask := stats.Tasks["lint"]
48 if lintTask.AvgDurationMs != 4000 {
49 t.Errorf("expected initial AvgDurationMs 4000, got %d", lintTask.AvgDurationMs)
50 }
51 if lintTask.LastStatus != "failed" {
52 t.Errorf("expected LastStatus failed, got %s", lintTask.LastStatus)
53 }
54 if lintTask.FailureStreak != 1 {
55 t.Errorf("expected FailureStreak 1, got %d", lintTask.FailureStreak)
56 }
57
58 // Fail again -> streak increments
59 updateTaskStats(stats, "lint", 4100*time.Millisecond, 1)
60 lintTask = stats.Tasks["lint"]
61 if lintTask.FailureStreak != 2 {
62 t.Errorf("expected FailureStreak 2, got %d", lintTask.FailureStreak)
63 }
64
65 // Succeed -> streak resets to 0
66 updateTaskStats(stats, "lint", 3900*time.Millisecond, 0)
67 lintTask = stats.Tasks["lint"]
68 if lintTask.FailureStreak != 0 {
69 t.Errorf("expected FailureStreak reset to 0, got %d", lintTask.FailureStreak)
70 }
71 if lintTask.LastStatus != "success" {
72 t.Errorf("expected LastStatus success, got %s", lintTask.LastStatus)
73 }
74}
75
76func TestStats_StorageAndPath(t *testing.T) {
77 tempCache := t.TempDir()
78 t.Setenv("XDG_CACHE_HOME", tempCache)
79
80 repo := "my-project"
81 expectedPath := filepath.Join(tempCache, "pici", repo, "stats.json")
82 path := getStatsPath(repo)
83 if path != expectedPath {
84 t.Errorf("expected stats path %s, got %s", expectedPath, path)
85 }
86
87 // Test Save & Load
88 stats := &RepoStats{
89 Repo: repo,
90 LastRunAt: time.Now().UTC().Format(time.RFC3339),
91 LastOverallStatus: "success",
92 RunsCount: 5,
93 AvgWallDurationMs: 12000,
94 Tasks: map[string]*TaskStats{
95 "build": {
96 AvgDurationMs: 6000,
97 LastDurationMs: 6100,
98 LastStatus: "success",
99 LastExitCode: 0,
100 SeenCount: 5,
101 },
102 },
103 }
104
105 if err := saveRepoStats(stats); err != nil {
106 t.Fatalf("failed to save stats: %v", err)
107 }
108
109 loaded, err := loadRepoStats(repo)
110 if err != nil {
111 t.Fatalf("failed to load stats: %v", err)
112 }
113 if loaded.Repo != repo || loaded.RunsCount != 5 || loaded.AvgWallDurationMs != 12000 {
114 t.Errorf("loaded stats mismatch: %+v", loaded)
115 }
116 if task, ok := loaded.Tasks["build"]; !ok || task.AvgDurationMs != 6000 {
117 t.Errorf("loaded task stats mismatch: %+v", task)
118 }
119}
120
121func TestStats_OutcomeTransitions(t *testing.T) {
122 // 1. Fixed transition (failed in prev run, succeeded in current run)
123 t1 := &TaskStats{
124 LastStatus: "failed",
125 LastExitCode: 1,
126 }
127 if trans := computeOutcomeTransition(t1, 0); trans != "fixed" {
128 t.Errorf("expected 'fixed', got '%s'", trans)
129 }
130
131 // 2. Still failing transition (failed in prev run, failed in current run)
132 if trans := computeOutcomeTransition(t1, 1); trans != "still_failing" {
133 t.Errorf("expected 'still_failing', got '%s'", trans)
134 }
135
136 // 3. New failure transition (succeeded in prev run, failed in current run)
137 t2 := &TaskStats{
138 LastStatus: "success",
139 LastExitCode: 0,
140 }
141 if trans := computeOutcomeTransition(t2, 2); trans != "new_failure" {
142 t.Errorf("expected 'new_failure', got '%s'", trans)
143 }
144
145 // 4. Clean success (succeeded in prev run, succeeded in current run)
146 if trans := computeOutcomeTransition(t2, 0); trans != "" {
147 t.Errorf("expected '', got '%s'", trans)
148 }
149
150 // 5. First time seen (nil prev)
151 if trans := computeOutcomeTransition(nil, 0); trans != "" {
152 t.Errorf("expected '', got '%s'", trans)
153 }
154 if trans := computeOutcomeTransition(nil, 1); trans != "new_failure" {
155 t.Errorf("expected 'new_failure', got '%s'", trans)
156 }
157}