// ABOUTME: Tests for the summary accumulator that tracks metrics across a run. // ABOUTME: Validates stats computation (peak/avg/P95), process peak tracking, and edge cases. package summary import ( "testing" "time" "edp.buildth.ing/DevFW-CICD/forgejo-runner-resource-collector/internal/metrics" ) func TestAccumulator_NoSamples(t *testing.T) { acc := NewAccumulator(5) result := acc.Summarize() if result != nil { t.Errorf("expected nil summary for no samples, got %+v", result) } } func TestAccumulator_SingleSample(t *testing.T) { acc := NewAccumulator(5) acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), CPU: metrics.CPUMetrics{TotalPercent: 42.5}, Memory: metrics.MemoryMetrics{ UsedBytes: 1000, UsedPercent: 50.0, }, }) s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // With a single sample, peak=avg=p95 if s.CPUTotal.Peak != 42.5 { t.Errorf("CPU peak: got %f, want 42.5", s.CPUTotal.Peak) } if s.CPUTotal.Avg != 42.5 { t.Errorf("CPU avg: got %f, want 42.5", s.CPUTotal.Avg) } if s.CPUTotal.P95 != 42.5 { t.Errorf("CPU p95: got %f, want 42.5", s.CPUTotal.P95) } if s.MemUsedBytes.Peak != 1000 { t.Errorf("MemUsedBytes peak: got %f, want 1000", s.MemUsedBytes.Peak) } if s.MemUsedPercent.Peak != 50.0 { t.Errorf("MemUsedPercent peak: got %f, want 50.0", s.MemUsedPercent.Peak) } } func TestAccumulator_Stats(t *testing.T) { acc := NewAccumulator(5) cpuValues := []float64{10, 20, 30, 40, 50} for i, v := range cpuValues { acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC), CPU: metrics.CPUMetrics{TotalPercent: v}, Memory: metrics.MemoryMetrics{ UsedBytes: uint64(v * 100), UsedPercent: v, }, }) } s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // Peak = max = 50 if s.CPUTotal.Peak != 50 { t.Errorf("CPU peak: got %f, want 50", s.CPUTotal.Peak) } // Avg = (10+20+30+40+50)/5 = 30 if s.CPUTotal.Avg != 30 { t.Errorf("CPU avg: got %f, want 30", s.CPUTotal.Avg) } // P95: sorted=[10,20,30,40,50], index=int(4*0.95)=int(3.8)=3, value=40 if s.CPUTotal.P95 != 40 { t.Errorf("CPU p95: got %f, want 40", s.CPUTotal.P95) } } func TestAccumulator_P95_LargerDataset(t *testing.T) { acc := NewAccumulator(5) // 20 values: 1, 2, 3, ..., 20 for i := 1; i <= 20; i++ { acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC), CPU: metrics.CPUMetrics{TotalPercent: float64(i)}, Memory: metrics.MemoryMetrics{}, }) } s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // P95: sorted=[1..20], index=int(19*0.95)=int(18.05)=18, value=19 if s.CPUTotal.P95 != 19 { t.Errorf("CPU p95: got %f, want 19", s.CPUTotal.P95) } // Avg = (1+2+...+20)/20 = 210/20 = 10.5 if s.CPUTotal.Avg != 10.5 { t.Errorf("CPU avg: got %f, want 10.5", s.CPUTotal.Avg) } } func TestAccumulator_MemoryStats(t *testing.T) { acc := NewAccumulator(5) memBytes := []uint64{1000, 2000, 3000, 4000, 5000} memPercent := []float64{10, 20, 30, 40, 50} for i := range memBytes { acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC), CPU: metrics.CPUMetrics{TotalPercent: 0}, Memory: metrics.MemoryMetrics{ UsedBytes: memBytes[i], UsedPercent: memPercent[i], }, }) } s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // MemUsedBytes: peak=5000, avg=3000, p95=4000 if s.MemUsedBytes.Peak != 5000 { t.Errorf("MemUsedBytes peak: got %f, want 5000", s.MemUsedBytes.Peak) } if s.MemUsedBytes.Avg != 3000 { t.Errorf("MemUsedBytes avg: got %f, want 3000", s.MemUsedBytes.Avg) } if s.MemUsedBytes.P95 != 4000 { t.Errorf("MemUsedBytes p95: got %f, want 4000", s.MemUsedBytes.P95) } // MemUsedPercent: peak=50, avg=30, p95=40 if s.MemUsedPercent.Peak != 50 { t.Errorf("MemUsedPercent peak: got %f, want 50", s.MemUsedPercent.Peak) } if s.MemUsedPercent.Avg != 30 { t.Errorf("MemUsedPercent avg: got %f, want 30", s.MemUsedPercent.Avg) } if s.MemUsedPercent.P95 != 40 { t.Errorf("MemUsedPercent p95: got %f, want 40", s.MemUsedPercent.P95) } } func TestAccumulator_ProcessPeaks(t *testing.T) { acc := NewAccumulator(5) // Same PID across two samples; peaks should be retained acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, TopCPU: []metrics.ProcessMetrics{ {PID: 1, Name: "a", CPUPercent: 10, MemRSS: 100}, }, TopMemory: []metrics.ProcessMetrics{ {PID: 1, Name: "a", CPUPercent: 10, MemRSS: 100}, }, }) acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, 1, 0, time.UTC), CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, TopCPU: []metrics.ProcessMetrics{ {PID: 1, Name: "a", CPUPercent: 20, MemRSS: 50}, }, TopMemory: []metrics.ProcessMetrics{ {PID: 1, Name: "a", CPUPercent: 5, MemRSS: 200}, }, }) s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // Should find PID 1 with peak CPU=20, peak mem=200 found := false for _, p := range s.TopCPUProcesses { if p.PID == 1 { found = true if p.PeakCPU != 20 { t.Errorf("PeakCPU: got %f, want 20", p.PeakCPU) } if p.PeakMem != 200 { t.Errorf("PeakMem: got %d, want 200", p.PeakMem) } } } if !found { t.Error("PID 1 not found in TopCPUProcesses") } } func TestAccumulator_ProcessPeaks_TopN(t *testing.T) { acc := NewAccumulator(2) // Only top 2 acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, TopCPU: []metrics.ProcessMetrics{ {PID: 1, Name: "low", CPUPercent: 10, MemRSS: 100}, {PID: 2, Name: "mid", CPUPercent: 50, MemRSS: 500}, {PID: 3, Name: "high", CPUPercent: 90, MemRSS: 900}, }, TopMemory: []metrics.ProcessMetrics{ {PID: 1, Name: "low", CPUPercent: 10, MemRSS: 100}, {PID: 2, Name: "mid", CPUPercent: 50, MemRSS: 500}, {PID: 3, Name: "high", CPUPercent: 90, MemRSS: 900}, }, }) s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // TopCPUProcesses should have at most 2 entries, sorted by PeakCPU descending if len(s.TopCPUProcesses) != 2 { t.Fatalf("TopCPUProcesses length: got %d, want 2", len(s.TopCPUProcesses)) } if s.TopCPUProcesses[0].PeakCPU != 90 { t.Errorf("TopCPU[0] PeakCPU: got %f, want 90", s.TopCPUProcesses[0].PeakCPU) } if s.TopCPUProcesses[1].PeakCPU != 50 { t.Errorf("TopCPU[1] PeakCPU: got %f, want 50", s.TopCPUProcesses[1].PeakCPU) } // TopMemProcesses should have at most 2 entries, sorted by PeakMem descending if len(s.TopMemProcesses) != 2 { t.Fatalf("TopMemProcesses length: got %d, want 2", len(s.TopMemProcesses)) } if s.TopMemProcesses[0].PeakMem != 900 { t.Errorf("TopMem[0] PeakMem: got %d, want 900", s.TopMemProcesses[0].PeakMem) } if s.TopMemProcesses[1].PeakMem != 500 { t.Errorf("TopMem[1] PeakMem: got %d, want 500", s.TopMemProcesses[1].PeakMem) } } func TestAccumulator_ProcessPeaks_Dedup(t *testing.T) { acc := NewAccumulator(5) // A process appears in both TopCPU and TopMemory acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, TopCPU: []metrics.ProcessMetrics{ {PID: 1, Name: "proc", CPUPercent: 80, MemRSS: 100}, }, TopMemory: []metrics.ProcessMetrics{ {PID: 1, Name: "proc", CPUPercent: 30, MemRSS: 500}, }, }) s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } // The internal process map should have merged the peaks // PeakCPU should be 80 (from TopCPU), PeakMem should be 500 (from TopMemory) for _, p := range s.TopCPUProcesses { if p.PID == 1 { if p.PeakCPU != 80 { t.Errorf("PeakCPU: got %f, want 80", p.PeakCPU) } if p.PeakMem != 500 { t.Errorf("PeakMem: got %d, want 500", p.PeakMem) } } } } func TestAccumulator_SampleCount(t *testing.T) { acc := NewAccumulator(5) if acc.SampleCount() != 0 { t.Errorf("initial SampleCount: got %d, want 0", acc.SampleCount()) } for i := 0; i < 3; i++ { acc.Add(&metrics.SystemMetrics{ Timestamp: time.Date(2025, 1, 1, 0, 0, i, 0, time.UTC), CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, }) } if acc.SampleCount() != 3 { t.Errorf("SampleCount after 3 adds: got %d, want 3", acc.SampleCount()) } } func TestAccumulator_Duration(t *testing.T) { acc := NewAccumulator(5) start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2025, 1, 1, 0, 1, 0, 0, time.UTC) // 60 seconds later acc.Add(&metrics.SystemMetrics{ Timestamp: start, CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, }) acc.Add(&metrics.SystemMetrics{ Timestamp: end, CPU: metrics.CPUMetrics{}, Memory: metrics.MemoryMetrics{}, }) s := acc.Summarize() if s == nil { t.Fatal("expected non-nil summary") } if !s.StartTime.Equal(start) { t.Errorf("StartTime: got %v, want %v", s.StartTime, start) } if s.DurationSeconds != 60 { t.Errorf("DurationSeconds: got %f, want 60", s.DurationSeconds) } }