testsAndMisc/linux_configuration/tests/test_usage_report_segments.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

113 lines
3.7 KiB
Python

"""Tests for usage_report's per-day segment aggregation, the log-description
strings, and the deprecated _compute_window shim.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import _usage_report_run as run
from _usage_report_types import _Progress, _Window
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)
# --------------------------------------------------------------------------- #
# _compute_window (deprecated shim; no callers remain in-tree)
# --------------------------------------------------------------------------- #
def test_compute_window_returns_the_aggregate_window(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The shim forwards to aggregate_atop and hands back its window."""
window = _Window(distinct_samples=3, interval_s=600, seconds=1200)
monkeypatch.setattr(run, "aggregate_atop", lambda *_a: ({}, window))
result = run._compute_window(Path("/nonexistent/atop"), _progress())
assert result is window
assert result.seconds == 1200
def test_compute_window_defaults_an_empty_window_to_a_day(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A window with no measured seconds is widened to a full day."""
window = _Window(distinct_samples=1)
monkeypatch.setattr(run, "aggregate_atop", lambda *_a: ({}, window))
assert (
run._compute_window(Path("/nonexistent/atop"), _progress()).seconds
== run._SEC_PER_DAY
)
# --------------------------------------------------------------------------- #
# _aggregate_segments / _log_descriptions
# --------------------------------------------------------------------------- #
def test_aggregate_segments_skips_missing_atop_logs(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A gap day contributes no atop data but its pmon log is still read."""
present = tmp_path / "atop-present"
present.write_text("x", encoding="utf-8")
segments = [
run._Segment(present, tmp_path / "pmon-a"),
run._Segment(tmp_path / "atop-absent", tmp_path / "pmon-b"),
]
monkeypatch.setattr(
run,
"aggregate_atop",
lambda *_a: ({}, _Window(distinct_samples=2, seconds=600)),
)
pmon_calls: list[Path] = []
def fake_pmon(log: Path, *_a: object) -> tuple[dict[str, object], int]:
pmon_calls.append(log)
return {}, 5
monkeypatch.setattr(run, "aggregate_pmon", fake_pmon)
aggs = run._aggregate_segments(segments, _progress())
assert aggs.days_with_data == 1
assert aggs.gpu_samples == 10
assert len(pmon_calls) == 2
def test_log_descriptions_names_only_existing_logs(tmp_path: Path) -> None:
"""Descriptions cover the logs that are actually on disk."""
atop = tmp_path / "atop"
atop.write_text("x", encoding="utf-8")
segments = [run._Segment(atop, tmp_path / "pmon-absent")]
atop_desc, pmon_desc = run._log_descriptions(segments)
assert str(atop) in atop_desc
assert "none" in pmon_desc.lower() or pmon_desc
def test_aggregate_segments_ignores_a_log_with_no_samples(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""A present atop log that yields no samples does not count as a data day."""
present = tmp_path / "atop-empty"
present.write_text("x", encoding="utf-8")
monkeypatch.setattr(run, "aggregate_atop", lambda *_a: ({}, _Window()))
monkeypatch.setattr(run, "aggregate_pmon", lambda *_a: ({}, 0))
aggs = run._aggregate_segments(
[run._Segment(present, tmp_path / "pmon")],
_progress(),
)
assert aggs.days_with_data == 0