forgejo-runner-optimiser/internal/summary/accumulator_test.go
Waldemar Kindler 7201a527d8
All checks were successful
ci / build (push) Successful in 1m39s
feat(collector): Summaries metrics at the end of the process
2026-02-04 16:21:17 +01:00

335 lines
9.2 KiB
Go

// 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)
}
}