testsAndMisc/linux_configuration/tests/test_usage_report_aggregate.py
Krzysztof kuhy Rudnicki b5dbe1e67a
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
usage_report: split _usage_report_parsing.py 425 -> 243 + 206
Lifts the atop subprocess machinery into _usage_report_atop.py: running
atop, streaming its parseable output, building/caching the atop_agg C
helper, and reading its TSV. What stays is record parsing, epoch windows
and per-day merging.

Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 16 functions against HEAD reports 16 identical, 0
mismatches.

_usage_report_render.py imported _run from the parsing module; it now
imports from the module that owns it. That is the same re-export barrel
the last split shipped and had to fix in a follow-up - caught in the same
commit this time. usage_report.py needed no edit at all.

Both modules hold 100% line+branch (94/28 and 130/34, 0 missed). 179
tests before and after; the five new test files collect 8+6+4+14+5 = 37,
which is last commit's 36 plus the one re-homed out of
test_usage_report_modes.py, where it referenced a constant that had
moved out from under it.

The two test files added last commit were themselves over cap (373, 272)
so they are split five ways here - the DropZone lesson from earlier
today, where a lint fix silently added an over-cap file.

Ran the program: a real report from today's logs, through the native
atop_agg path, which is precisely the code this commit moved.

Over-cap 39 -> 38.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 12:12:11 +02:00

112 lines
3.6 KiB
Python

"""Tests for aggregate_atop, usage_report's pure-Python fallback parser used
when the native atop_agg helper is unavailable.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import _usage_report_parsing as parsing
from _usage_report_types import _Progress
if TYPE_CHECKING:
import pytest
def _progress() -> _Progress:
"""A progress reporter that never draws, so stderr stays clean."""
return _Progress(enabled=False, total_stages=1)
# --------------------------------------------------------------------------- #
# aggregate_atop (pure-Python fallback)
# --------------------------------------------------------------------------- #
def test_aggregate_atop_parses_prc_and_prm(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""With no native helper, the Python parser folds PRC and PRM records."""
log = tmp_path / "atop.log"
log.write_bytes(b"x" * 100)
lines = [
"# a comment",
"RESET",
"SEP",
"",
"PRC host 1000 d t 600 42 (python) S 100 500 20 a b c",
"PRC host 1600 d t 600 42 (python) S 100 900 40 a b c",
"PRM host 1000 d t 600 42 (python) S 4096 9999 2048 a b c",
"PRC short row",
]
monkeypatch.setattr(parsing, "_atop_agg_binary", lambda: None)
monkeypatch.setattr(parsing, "_iter_atop_lines", lambda *_a, **_k: iter(lines))
agg, window = parsing.aggregate_atop(log, _progress())
# utime+stime deltas: (900+40) - (500+20) = 420.
assert agg["python"].cpu_ticks == 420
assert agg["python"].peak_rss_kb == 2048
assert window.distinct_samples == 2
assert window.interval_s == 600
def test_aggregate_atop_ignores_unparseable_epoch(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A PRC row with a non-numeric epoch still parses, minus the timestamp."""
log = tmp_path / "atop.log"
log.write_bytes(b"x" * 10)
lines = ["PRC host notanepoch d t 600 42 (python) S 100 500 20 a b c"]
monkeypatch.setattr(parsing, "_atop_agg_binary", lambda: None)
monkeypatch.setattr(parsing, "_iter_atop_lines", lambda *_a, **_k: iter(lines))
agg, window = parsing.aggregate_atop(log, _progress())
assert agg["python"].pid_count == 1
assert window.distinct_samples == 0
def test_aggregate_atop_delegates_to_native_when_available(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""When the C helper builds, the native path is used instead."""
log = tmp_path / "atop.log"
log.write_bytes(b"x")
monkeypatch.setattr(parsing, "_atop_agg_binary", lambda: Path("/bin/atop_agg"))
called: list[Path] = []
def fake_native(
log_arg: Path,
_progress_arg: _Progress,
binary: Path,
_begin: str | None = None,
_end: str | None = None,
) -> tuple[dict[str, object], object]:
called.append(binary)
return {}, parsing._Window()
monkeypatch.setattr(parsing, "_aggregate_atop_native", fake_native)
parsing.aggregate_atop(log, _progress())
assert called == [Path("/bin/atop_agg")]
def test_aggregate_atop_skips_whitespace_only_rows(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A row of only whitespace splits to nothing and is skipped."""
log = tmp_path / "atop.log"
log.write_bytes(b"x")
monkeypatch.setattr(parsing, "_atop_agg_binary", lambda: None)
monkeypatch.setattr(parsing, "_iter_atop_lines", lambda *_a, **_k: iter([" "]))
agg, window = parsing.aggregate_atop(log, _progress())
assert agg == {}
assert window.distinct_samples == 0