package app import ( "encoding/json" "os" "path/filepath" "testing" "time" ) func TestMetricsTrackerDisabledByEmptyDir(t *testing.T) { m, err := newMetricsTracker("") if err != nil { t.Fatalf("newMetricsTracker(\"\") err: %v", err) } if m != nil { t.Fatalf("expected nil tracker for empty dir, got %v", m) } } func TestMetricsTrackerRecordsAndWrites(t *testing.T) { dir := t.TempDir() m, err := newMetricsTracker(dir) if err != nil { t.Fatalf("newMetricsTracker: %v", err) } if m == nil { t.Fatal("expected non-nil tracker") } m.recordPTYOut(2*time.Millisecond, 1024) m.recordPTYOut(5*time.Millisecond, 4096) m.recordRender(800 * time.Microsecond) m.recordStdout(300*time.Microsecond, 1100) m.recordEmuWrite(150 * time.Microsecond) m.recordEmuTitle(0, true) m.recordEmuTitle(20*time.Microsecond, false) m.recordSidebar(100*time.Microsecond, true) m.recordSidebar(900*time.Microsecond, false) m.recordTabbar(50*time.Microsecond, true) m.recordStatus(40*time.Microsecond, true) m.recordSnapshot(2 * time.Millisecond) m.recordTickerFire(false) m.recordTickerFire(true) m.recordPTYOutDrop() m.close() // metrics.json should exist and parse, and reflect what we recorded. raw, err := os.ReadFile(filepath.Join(dir, "metrics.json")) if err != nil { t.Fatalf("read metrics.json: %v", err) } var snap metricsSnapshot if err := json.Unmarshal(raw, &snap); err != nil { t.Fatalf("parse metrics.json: %v", err) } if snap.PTYChunks != 2 { t.Errorf("PTYChunks = %d, want 2", snap.PTYChunks) } if snap.PTYBytes != 5120 { t.Errorf("PTYBytes = %d, want 5120", snap.PTYBytes) } if snap.OnPTYOutMaxNs != (5 * time.Millisecond).Nanoseconds() { t.Errorf("OnPTYOutMaxNs = %d, want %d", snap.OnPTYOutMaxNs, (5 * time.Millisecond).Nanoseconds()) } if snap.SidebarDraws != 2 { t.Errorf("SidebarDraws = %d, want 2", snap.SidebarDraws) } if snap.SidebarCacheHits != 1 { t.Errorf("SidebarCacheHits = %d, want 1", snap.SidebarCacheHits) } if snap.SidebarCacheHitRate != 0.5 { t.Errorf("SidebarCacheHitRate = %v, want 0.5", snap.SidebarCacheHitRate) } if snap.EmuTitleCalls != 1 || snap.EmuTitleSkips != 1 { t.Errorf("emu title accounting: calls=%d skips=%d, want 1/1", snap.EmuTitleCalls, snap.EmuTitleSkips) } if snap.TickerFires != 2 || snap.TickerIdleFires != 1 { t.Errorf("ticker accounting: fires=%d idle=%d, want 2/1", snap.TickerFires, snap.TickerIdleFires) } if snap.OnPTYOutDrops != 1 { t.Errorf("OnPTYOutDrops = %d, want 1", snap.OnPTYOutDrops) } // summary.txt should also be present and non-empty. info, err := os.Stat(filepath.Join(dir, "summary.txt")) if err != nil { t.Fatalf("stat summary.txt: %v", err) } if info.Size() == 0 { t.Fatal("summary.txt is empty") } } func TestMetricsTrackerNilSafe(t *testing.T) { // Every record* method must be safe to call on a nil receiver // because the hot paths use that to avoid an enabled-check. var m *metricsTracker m.recordPTYOut(time.Millisecond, 100) m.recordPTYOutDrop() m.recordRender(time.Microsecond) m.recordStdout(time.Microsecond, 50) m.recordEmuWrite(time.Microsecond) m.recordEmuTitle(time.Microsecond, false) m.recordEmuTitle(0, true) m.recordSidebar(time.Microsecond, true) m.recordTabbar(time.Microsecond, false) m.recordStatus(time.Microsecond, true) m.recordSnapshot(time.Microsecond) m.recordTickerFire(true) m.close() }