"""Tests for the perf-stat collector — parser logic in isolation (no actual perf invocation, since perf needs CAP_SYS_ADMIN and hardware counters that the test runner can't assume).""" from __future__ import annotations import json from pathlib import Path import pytest from collectors import perf_qemu def test_parse_event_line_extracts_fields() -> None: line = '{"interval":0.100123,"counter-value":"1234567","unit":"","event":"cycles"}' evt = perf_qemu.parse_perf_event_line(line) assert evt is not None assert evt["event"] == "cycles" assert evt["interval"] == 0.100123 assert evt["counter-value"] == "1234567" def test_parse_event_line_skips_non_json() -> None: assert perf_qemu.parse_perf_event_line("") is None assert perf_qemu.parse_perf_event_line("garbage") is None assert perf_qemu.parse_perf_event_line("# Performance counter stats") is None def test_coerce_int_handles_perf_quirks() -> None: assert perf_qemu._coerce_int("1234567") == 1234567 assert perf_qemu._coerce_int("1,234,567") == 1234567 assert perf_qemu._coerce_int("") is None assert perf_qemu._coerce_int("") is None assert perf_qemu._coerce_int("") is None assert perf_qemu._coerce_int(None) is None assert perf_qemu._coerce_int(42) == 42 def test_build_row_computes_ipc_and_miss_rate() -> None: agg = { "cycles": 1_000_000_000, "instructions": 660_000_000, "cache-references": 1_000_000, "cache-misses": 50_000, "branches": 100_000_000, "branch-misses": 5_000_000, "page-faults": 12, "context-switches": 20, } row = perf_qemu._build_row(t_mono_origin_ns=0, interval_s=0.1, agg=agg) assert row["source"] == "host_perf" assert row["available_in_deployment"] is False assert row["cycles"] == 1_000_000_000 assert row["instructions"] == 660_000_000 assert pytest.approx(row["ipc"], abs=1e-9) == 0.66 assert pytest.approx(row["cache_miss_rate"], abs=1e-9) == 0.05 assert row["interval_s"] == 0.1 def test_build_row_handles_missing_counters() -> None: """If perf can't enable cache-misses on this hardware, the row should still be valid — just with None for the missing fields.""" agg = {"cycles": 100, "instructions": 50} row = perf_qemu._build_row(t_mono_origin_ns=0, interval_s=0.1, agg=agg) assert row["cycles"] == 100 assert row["cache_misses"] is None assert row["cache_miss_rate"] is None assert pytest.approx(row["ipc"], abs=1e-9) == 0.5 def test_run_loop_returns_zero_when_perf_missing(tmp_path: Path, monkeypatch) -> None: monkeypatch.setattr(perf_qemu, "perf_available", lambda: False) import threading rows = perf_qemu.run_loop( pid=1, output_path=tmp_path / "telemetry-perf.jsonl", t_mono_origin_ns=0, interval_ms=100, stop_event=threading.Event(), ) assert rows == 0