from __future__ import annotations from datetime import datetime, timezone import pytest from app.monitoring.resources import ResourceMonitor def write_proc_snapshot( proc_root, *, cpu_line: str, core_lines: tuple[str, ...], process_ticks: tuple[int, int], rss_pages: int = 100, mem_total_kib: int = 100_000, mem_available_kib: int = 40_000, ) -> None: self_dir = proc_root / "self" self_dir.mkdir(parents=True, exist_ok=True) (proc_root / "stat").write_text( "\n".join((cpu_line, *core_lines, "")), encoding="ascii", ) utime, stime = process_ticks process_stat = f"1 (matrix app) S 1 2 3 4 5 6 7 8 9 10 {utime} {stime} 0 0\n" (self_dir / "stat").write_text(process_stat, encoding="ascii") (self_dir / "statm").write_text(f"1000 {rss_pages} 0 0 0 0 0\n", encoding="ascii") (proc_root / "meminfo").write_text( f"MemTotal: {mem_total_kib} kB\n" f"MemAvailable: {mem_available_kib} kB\n", encoding="ascii", ) def test_resource_monitor_uses_delta_cpu_and_current_memory(tmp_path): proc_root = tmp_path / "proc" sampled_at = datetime(2026, 7, 21, 8, 0, tzinfo=timezone.utc) write_proc_snapshot( proc_root, cpu_line="cpu 100 0 100 800 0 0 0 0 0 0", core_lines=( "cpu0 25 0 25 200 0 0 0 0 0 0", "cpu1 25 0 25 200 0 0 0 0 0 0", "cpu2 25 0 25 200 0 0 0 0 0 0", "cpu3 25 0 25 200 0 0 0 0 0 0", ), process_ticks=(20, 5), ) monitor = ResourceMonitor(proc_root=proc_root, page_size=4096, now=lambda: sampled_at) first = monitor.sample_now() assert first["status"] == "ok" assert first["sampled_at"] == "2026-07-21T08:00:00.000Z" assert first["cpu"] == { "application_percent": None, "total_percent": None, "logical_cpu_count": 4, "application_core_equivalent": None, "cores_percent": [None, None, None, None], } assert first["memory"]["application_bytes"] == 409_600 assert first["memory"]["application_percent"] == pytest.approx(0.4) assert first["memory"]["total_percent"] == pytest.approx(60.0) write_proc_snapshot( proc_root, cpu_line="cpu 130 0 120 850 0 0 0 0 0 0", core_lines=( "cpu0 35 0 30 210 0 0 0 0 0 0", "cpu1 35 0 30 210 0 0 0 0 0 0", "cpu2 30 0 30 215 0 0 0 0 0 0", "cpu3 30 0 30 215 0 0 0 0 0 0", ), process_ticks=(24, 6), ) second = monitor.sample_now() assert second["cpu"]["application_percent"] == pytest.approx(5.0) assert second["cpu"]["application_core_equivalent"] == pytest.approx(0.2) assert second["cpu"]["total_percent"] == pytest.approx(50.0) assert second["cpu"]["cores_percent"] == pytest.approx([60.0, 60.0, 40.0, 40.0]) assert second["error_code"] is None def test_resource_monitor_reports_application_using_more_than_one_core(tmp_path): proc_root = tmp_path / "proc" first_cores = tuple(f"cpu{index} 100 0 0 0" for index in range(4)) second_cores = tuple(f"cpu{index} 200 0 0 0" for index in range(4)) write_proc_snapshot( proc_root, cpu_line="cpu 400 0 0 0", core_lines=first_cores, process_ticks=(20, 0), ) monitor = ResourceMonitor(proc_root=proc_root, page_size=4096) monitor.sample_now() write_proc_snapshot( proc_root, cpu_line="cpu 800 0 0 0", core_lines=second_cores, process_ticks=(170, 0), ) status = monitor.sample_now() assert status["cpu"]["application_percent"] == pytest.approx(37.5) assert status["cpu"]["application_core_equivalent"] == pytest.approx(1.5) assert status["cpu"]["cores_percent"] == pytest.approx([100.0] * 4) def test_resource_monitor_discards_cpu_delta_when_core_set_changes(tmp_path): proc_root = tmp_path / "proc" write_proc_snapshot( proc_root, cpu_line="cpu 200 0 0 0", core_lines=("cpu0 100 0 0 0", "cpu1 100 0 0 0"), process_ticks=(10, 0), ) monitor = ResourceMonitor(proc_root=proc_root, page_size=4096) monitor.sample_now() write_proc_snapshot( proc_root, cpu_line="cpu 600 0 0 0", core_lines=tuple(f"cpu{index} 150 0 0 0" for index in range(4)), process_ticks=(50, 0), ) status = monitor.sample_now() assert status["cpu"]["logical_cpu_count"] == 4 assert status["cpu"]["application_percent"] is None assert status["cpu"]["application_core_equivalent"] is None assert status["cpu"]["total_percent"] is None assert status["cpu"]["cores_percent"] == [None, None, None, None] def test_resource_monitor_clamps_percentages_and_returns_defensive_copies(tmp_path): proc_root = tmp_path / "proc" write_proc_snapshot( proc_root, cpu_line="cpu 100 0 0 0 0 0 0 0 0 0", core_lines=tuple(f"cpu{index} 25 0 0 0" for index in range(4)), process_ticks=(0, 0), rss_pages=50_000, mem_total_kib=100_000, mem_available_kib=0, ) monitor = ResourceMonitor(proc_root=proc_root, page_size=4096) monitor.sample_now() write_proc_snapshot( proc_root, cpu_line="cpu 200 0 0 0 0 0 0 0 0 0", core_lines=tuple(f"cpu{index} 50 0 0 0" for index in range(4)), process_ticks=(300, 0), rss_pages=50_000, mem_total_kib=100_000, mem_available_kib=0, ) status = monitor.sample_now() status["cpu"]["total_percent"] = -1 status["cpu"]["cores_percent"][0] = -1 status["memory"]["application_percent"] = -1 cached = monitor.get_status() assert cached["cpu"]["application_percent"] == 100.0 assert cached["cpu"]["application_core_equivalent"] == 4.0 assert cached["cpu"]["total_percent"] == 100.0 assert cached["cpu"]["cores_percent"] == [100.0] * 4 assert cached["memory"]["application_percent"] == 100.0 assert cached["memory"]["total_percent"] == 100.0 def test_resource_monitor_failure_is_isolated_and_does_not_create_files(tmp_path): proc_root = tmp_path / "missing-proc" monitor = ResourceMonitor(proc_root=proc_root, poll_interval=0.01, page_size=4096) status = monitor.sample_now() monitor.start() monitor.close() assert status["status"] == "error" assert status["error_code"] == "proc_read_error" assert status["cpu"] == { "application_percent": None, "total_percent": None, "logical_cpu_count": None, "application_core_equivalent": None, "cores_percent": [], } assert status["memory"] == { "application_bytes": None, "application_percent": None, "total_percent": None, } assert not proc_root.exists()