diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-23 23:22:54 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-23 23:22:54 +0200 |
| commit | 0bfeccbed6edb106670024373e46b3891463920c (patch) | |
| tree | 49a760dc17913a7359a13e37cbfbc156c4e6e3b0 /internal/statsengine/engine_test.go | |
| parent | a8a4f82675ed2df538e9fbf95504d5674a732c52 (diff) | |
task 302: add stats engine orchestration and snapshots
Diffstat (limited to 'internal/statsengine/engine_test.go')
| -rw-r--r-- | internal/statsengine/engine_test.go | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/internal/statsengine/engine_test.go b/internal/statsengine/engine_test.go new file mode 100644 index 0000000..943fe9c --- /dev/null +++ b/internal/statsengine/engine_test.go @@ -0,0 +1,130 @@ +package statsengine + +import ( + "ior/internal/event" + "ior/internal/file" + "ior/internal/types" + "math" + "testing" + "time" +) + +type fakeClock struct { + now time.Time +} + +func (c *fakeClock) Now() time.Time { + return c.now +} + +func (c *fakeClock) Advance(d time.Duration) { + c.now = c.now.Add(d) +} + +func TestEngineIngestAndSnapshotIntegration(t *testing.T) { + clock := &fakeClock{now: time.Unix(1000, 0)} + engine := newEngineWithClock(2, clock.Now) + + engine.Ingest(newEnginePair(types.SYS_ENTER_READ, 100, types.READ_CLASSIFIED, "proc-a", 1, "/tmp/a", 100, 10, 3)) + clock.Advance(500 * time.Millisecond) + engine.Ingest(newEnginePair(types.SYS_ENTER_WRITE, -1, types.WRITE_CLASSIFIED, "proc-a", 1, "/tmp/a", 50, 20, 5)) + clock.Advance(500 * time.Millisecond) + engine.Ingest(newEnginePair(types.SYS_ENTER_COPY_FILE_RANGE, 80, types.TRANSFER_CLASSIFIED, "proc-b", 2, "/tmp/b", 20, 40, 8)) + clock.Advance(1 * time.Second) + + snap := engine.Snapshot() + if snap == nil { + t.Fatalf("expected snapshot") + } + + if snap.TotalSyscalls != 3 || snap.TotalErrors != 1 || snap.TotalBytes != 170 { + t.Fatalf("unexpected totals: syscalls=%d errors=%d bytes=%d", snap.TotalSyscalls, snap.TotalErrors, snap.TotalBytes) + } + if snap.LatencyMeanNs != (10+20+40)/3.0 { + t.Fatalf("unexpected latency mean: %v", snap.LatencyMeanNs) + } + if snap.GapMeanNs != (3+5+8)/3.0 { + t.Fatalf("unexpected gap mean: %v", snap.GapMeanNs) + } + + if math.Abs(snap.SyscallRatePerSec-1.5) > 1e-9 { + t.Fatalf("unexpected syscall rate: %v", snap.SyscallRatePerSec) + } + if math.Abs(snap.ErrorRatePerSec-0.5) > 1e-9 { + t.Fatalf("unexpected error rate: %v", snap.ErrorRatePerSec) + } + if math.Abs(snap.ReadBytesPerSec-60.0) > 1e-9 { + t.Fatalf("unexpected read bytes rate: %v", snap.ReadBytesPerSec) + } + if math.Abs(snap.WriteBytesPerSec-35.0) > 1e-9 { + t.Fatalf("unexpected write bytes rate: %v", snap.WriteBytesPerSec) + } + + if len(snap.Syscalls()) != 3 { + t.Fatalf("expected 3 syscall rows, got %d", len(snap.Syscalls())) + } + if len(snap.Files()) != 2 { + t.Fatalf("expected top 2 files due to topN=2, got %d", len(snap.Files())) + } + if len(snap.Processes()) != 2 { + t.Fatalf("expected 2 process rows, got %d", len(snap.Processes())) + } + if snap.LatencyHistogram.Total != 3 || snap.GapHistogram.Total != 3 { + t.Fatalf("unexpected histogram totals: latency=%d gap=%d", snap.LatencyHistogram.Total, snap.GapHistogram.Total) + } +} + +func TestEngineSnapshotWithNoEvents(t *testing.T) { + clock := &fakeClock{now: time.Unix(2000, 0)} + engine := newEngineWithClock(10, clock.Now) + + snap := engine.Snapshot() + if snap == nil { + t.Fatalf("expected snapshot") + } + if snap.TotalSyscalls != 0 || snap.TotalErrors != 0 || snap.TotalBytes != 0 { + t.Fatalf("expected zero totals, got %+v", snap) + } + if len(snap.Syscalls()) != 0 || len(snap.Files()) != 0 || len(snap.Processes()) != 0 { + t.Fatalf("expected empty rows in zero snapshot") + } +} + +func TestEngineTrendDetection(t *testing.T) { + if got := detectTrend(make([]float64, trendWindowSlots*2)); got.Direction != TrendStable { + t.Fatalf("expected stable for flat data, got %+v", got) + } + + series := make([]float64, trendWindowSlots*2) + for i := 0; i < trendWindowSlots; i++ { + series[i] = 10 + } + for i := trendWindowSlots; i < trendWindowSlots*2; i++ { + series[i] = 30 + } + if got := detectTrend(series); got.Direction != TrendRising { + t.Fatalf("expected rising trend, got %+v", got) + } + + for i := 0; i < trendWindowSlots; i++ { + series[i] = 40 + } + for i := trendWindowSlots; i < trendWindowSlots*2; i++ { + series[i] = 10 + } + if got := detectTrend(series); got.Direction != TrendFalling { + t.Fatalf("expected falling trend, got %+v", got) + } +} + +func newEnginePair(traceID types.TraceId, ret int64, retType uint32, comm string, pid uint32, path string, bytes uint64, duration uint64, gap uint64) *event.Pair { + return &event.Pair{ + EnterEv: &types.RetEvent{TraceId: traceID, Pid: pid}, + ExitEv: &types.RetEvent{TraceId: traceID, Pid: pid, Ret: ret, RetType: retType}, + Comm: comm, + Duration: duration, + DurationToPrev: gap, + Bytes: bytes, + File: file.NewFd(3, path, -1), + } +} |
