testsAndMisc/linux_configuration/tests/test_usage_report_run_since.py
Krzysztof kuhy Rudnicki ff1a1b6a0e usage_report: split usage_report.py 426 -> 101 + 195 + 117 + 68
The entry point now holds only the parser, mode dispatch and main. Three
modules take one concern each: _usage_report_logs (where the logs are,
the state file, per-day segment planning), _usage_report_emit (preflight,
stdout, clipboard), _usage_report_run (the two run paths and the
aggregation they drive).

Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 22 top-level defs and classes reports 22 identical, 0
mismatches.

All four modules hold 100% line+branch. 204 tests before and after. Both
CLI modes were run against the machine's real logs, which is the check
the unit tests cannot make: they stub the collaborators, so only a real
run proves the new import graph resolves.

Two lessons, recorded because the next target is a 498-line file:

This cut was too big. Two names had to be relocated after their first
placement created an import cycle, and four modules' worth of monkeypatch
targets needed re-pointing. A smaller first slice would have been cheaper.

Patching a moved function must target the module whose namespace resolves
it - the caller's, not the one it was moved to. Getting that wrong on
_run_since made main() call the real function, which read the actual atop
logs and hung the suite rather than failing.

Three import bugs (Path and _fmt_h left under TYPE_CHECKING but used at
runtime, dataclass missing where _Aggregates landed) were caught
immediately by the coverage from the previous commit.

Over-cap 38 -> 37.

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

156 lines
5.9 KiB
Python

"""Tests for usage_report's since-last-report run: the atop guard, the normal
report path, the no-data path, and when the saved timestamp advances.
"""
from __future__ import annotations
import datetime as _dt
import _usage_report_run as run
from _usage_report_types import _Window
import pytest
import usage_report
def _args(**overrides: object) -> object:
"""Build a parsed-args namespace with the parser's own defaults."""
args = usage_report._build_parser().parse_args([])
for key, value in overrides.items():
setattr(args, key, value)
return args
def _aggregates(*, days_with_data: int = 1, seconds: int = 600) -> object:
"""Build an _Aggregates with a window that looks like real coverage."""
return run._Aggregates(
cpu={},
gpu={},
window=_Window(distinct_samples=2, interval_s=600, seconds=seconds),
gpu_samples=0,
days_with_data=days_with_data,
)
# --------------------------------------------------------------------------- #
# _run_since
# --------------------------------------------------------------------------- #
def test_run_since_exits_without_atop(monkeypatch: pytest.MonkeyPatch) -> None:
"""The multi-day path also refuses to run without atop installed."""
monkeypatch.setattr(run.shutil, "which", lambda _n: None)
with pytest.raises(SystemExit):
run._run_since(_args(), _dt.datetime.now().astimezone())
def test_run_since_reports_and_advances_state(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A normal run emits a report and moves the saved timestamp forward."""
now = _dt.datetime.now().astimezone()
monkeypatch.setattr(run.shutil, "which", lambda _n: "/usr/bin/atop")
monkeypatch.setattr(run, "_resolve_start", lambda *_a: now)
monkeypatch.setattr(run, "_plan_segments", lambda *_a: [])
monkeypatch.setattr(run, "_aggregate_segments", lambda *_a: _aggregates())
monkeypatch.setattr(run, "_log_descriptions", lambda _s: ("a", "p"))
monkeypatch.setattr(run, "_render_report", lambda *_a, **_k: "report")
emitted: list[str] = []
monkeypatch.setattr(run, "_emit", lambda _a, r: emitted.append(r))
advanced: list[object] = []
monkeypatch.setattr(run, "_write_last_generated", advanced.append)
rc = run._run_since(_args(quiet=True), now)
assert rc == 0
assert emitted == ["report"]
assert advanced == [now]
def test_run_since_with_no_data_still_advances_state(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Gap days produce a stderr note, no report, but the state still moves."""
now = _dt.datetime.now().astimezone()
monkeypatch.setattr(run.shutil, "which", lambda _n: "/usr/bin/atop")
monkeypatch.setattr(run, "_resolve_start", lambda *_a: now)
monkeypatch.setattr(run, "_plan_segments", lambda *_a: [])
monkeypatch.setattr(
run,
"_aggregate_segments",
lambda *_a: _aggregates(days_with_data=0),
)
emitted: list[str] = []
monkeypatch.setattr(run, "_emit", lambda _a, r: emitted.append(r))
advanced: list[object] = []
monkeypatch.setattr(run, "_write_last_generated", advanced.append)
rc = run._run_since(_args(quiet=True), now)
assert rc == 0
assert emitted == []
assert advanced == [now]
assert "nothing to report" in capsys.readouterr().err
def test_run_since_with_no_data_leaves_state_when_suppressed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""--no-update-state keeps the saved timestamp even on the no-data path."""
now = _dt.datetime.now().astimezone()
monkeypatch.setattr(run.shutil, "which", lambda _n: "/usr/bin/atop")
monkeypatch.setattr(run, "_resolve_start", lambda *_a: now)
monkeypatch.setattr(run, "_plan_segments", lambda *_a: [])
monkeypatch.setattr(
run,
"_aggregate_segments",
lambda *_a: _aggregates(days_with_data=0),
)
advanced: list[object] = []
monkeypatch.setattr(run, "_write_last_generated", advanced.append)
run._run_since(_args(quiet=True, no_update_state=True), now)
assert advanced == []
def test_run_since_defaults_an_empty_window_to_a_day(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A window with no measured seconds is treated as a full day."""
now = _dt.datetime.now().astimezone()
aggs = _aggregates(seconds=0)
monkeypatch.setattr(run.shutil, "which", lambda _n: "/usr/bin/atop")
monkeypatch.setattr(run, "_resolve_start", lambda *_a: now)
monkeypatch.setattr(run, "_plan_segments", lambda *_a: [])
monkeypatch.setattr(run, "_aggregate_segments", lambda *_a: aggs)
monkeypatch.setattr(run, "_log_descriptions", lambda _s: ("a", "p"))
monkeypatch.setattr(run, "_render_report", lambda *_a, **_k: "report")
monkeypatch.setattr(run, "_emit", lambda *_a: None)
monkeypatch.setattr(run, "_write_last_generated", lambda *_a: None)
run._run_since(_args(quiet=True), now)
assert aggs.window.seconds == run._SEC_PER_DAY
def test_run_since_leaves_state_when_suppressed_after_a_report(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""--no-update-state keeps the timestamp even when a report was emitted."""
now = _dt.datetime.now().astimezone()
monkeypatch.setattr(run.shutil, "which", lambda _n: "/usr/bin/atop")
monkeypatch.setattr(run, "_resolve_start", lambda *_a: now)
monkeypatch.setattr(run, "_plan_segments", lambda *_a: [])
monkeypatch.setattr(run, "_aggregate_segments", lambda *_a: _aggregates())
monkeypatch.setattr(run, "_log_descriptions", lambda _s: ("a", "p"))
monkeypatch.setattr(run, "_render_report", lambda *_a, **_k: "report")
emitted: list[str] = []
monkeypatch.setattr(run, "_emit", lambda _a, r: emitted.append(r))
advanced: list[object] = []
monkeypatch.setattr(run, "_write_last_generated", advanced.append)
run._run_since(_args(quiet=True, no_update_state=True), now)
assert emitted == ["report"]
assert advanced == []