mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
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
190 lines
6.3 KiB
Python
190 lines
6.3 KiB
Python
"""Tests for spanning several days in usage_report: pmon timestamp filtering,
|
|
atop command bounding, the persisted last-report state, and day-segment
|
|
planning.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as _dt
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import _usage_report_atop as atop_io
|
|
import _usage_report_logs as logs
|
|
import _usage_report_pmon as pmon
|
|
from _usage_report_types import _Progress
|
|
|
|
if TYPE_CHECKING:
|
|
import pytest
|
|
|
|
|
|
# Aware timezone matching how the parser localizes naive timestamps, so
|
|
# epochs computed here line up with `_pmon_row_epoch`'s `.astimezone()`.
|
|
_LOCAL_TZ = _dt.datetime.now().astimezone().tzinfo
|
|
|
|
|
|
def _at(
|
|
year: int, month: int, day: int, hour: int = 0, minute: int = 0
|
|
) -> _dt.datetime:
|
|
"""Build an aware local datetime for tests."""
|
|
return _dt.datetime(year, month, day, hour, minute, tzinfo=_LOCAL_TZ)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# pmon timestamp helpers (parsing)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_pmon_row_epoch_parses_valid_row() -> None:
|
|
"""A well-formed pmon row yields the matching local epoch."""
|
|
row = ["20260604", "10:30:00", "0", "100", "G", "5", "1"]
|
|
|
|
assert pmon._pmon_row_epoch(row) == _at(2026, 6, 4, 10, 30).timestamp()
|
|
|
|
|
|
def test_pmon_row_epoch_returns_none_on_bad_input() -> None:
|
|
"""Malformed or short rows return None rather than raising."""
|
|
assert pmon._pmon_row_epoch([]) is None
|
|
assert pmon._pmon_row_epoch(["nope", "alsonope"]) is None
|
|
|
|
|
|
def _write_pmon(path: Path) -> None:
|
|
"""Write a tiny pmon log with two rows ten minutes apart."""
|
|
path.write_text(
|
|
"#Date Time gpu pid type sm mem enc dec jpg ofa command\n"
|
|
" 20260604 10:00:00 0 100 G 5 1 - - - - Xorg\n"
|
|
" 20260604 11:00:00 0 101 G 7 2 - - - - thorium\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def test_aggregate_pmon_without_bound_keeps_all_rows(tmp_path: Path) -> None:
|
|
"""No begin_epoch means every data row counts."""
|
|
log = tmp_path / "pmon.log"
|
|
_write_pmon(log)
|
|
|
|
_, samples = pmon.aggregate_pmon(log, _Progress(enabled=False, total_stages=1))
|
|
|
|
assert samples == 2
|
|
|
|
|
|
def test_aggregate_pmon_filters_rows_before_begin(tmp_path: Path) -> None:
|
|
"""Rows timestamped before begin_epoch are skipped."""
|
|
log = tmp_path / "pmon.log"
|
|
_write_pmon(log)
|
|
cutoff = _at(2026, 6, 4, 10, 30).timestamp()
|
|
|
|
agg, samples = pmon.aggregate_pmon(
|
|
log,
|
|
_Progress(enabled=False, total_stages=1),
|
|
begin_epoch=cutoff,
|
|
)
|
|
|
|
assert samples == 1
|
|
assert "thorium" in agg
|
|
assert "Xorg" not in agg
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# atop command bounding (parsing)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_atop_read_cmd_unbounded() -> None:
|
|
"""Without bounds the command is a plain replay."""
|
|
cmd = atop_io._atop_read_cmd(
|
|
Path("/var/log/atop/atop_20260604"), "PRC,PRM", None, None
|
|
)
|
|
|
|
assert cmd == ["atop", "-r", "/var/log/atop/atop_20260604", "-P", "PRC,PRM"]
|
|
|
|
|
|
def test_atop_read_cmd_with_begin_and_end() -> None:
|
|
"""Begin/end inject -b/-e before the -P selector."""
|
|
cmd = atop_io._atop_read_cmd(Path("/x"), "PRC", "202606041400", "202606042000")
|
|
|
|
assert cmd == [
|
|
"atop",
|
|
"-r",
|
|
"/x",
|
|
"-b",
|
|
"202606041400",
|
|
"-e",
|
|
"202606042000",
|
|
"-P",
|
|
"PRC",
|
|
]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Persisted last-report state (usage_report)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_state_round_trip(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A written timestamp reads back as an equal aware datetime."""
|
|
state = tmp_path / "state" / "last_report.json"
|
|
monkeypatch.setattr(logs, "_STATE_DIR", state.parent)
|
|
monkeypatch.setattr(logs, "_STATE_FILE", state)
|
|
when = _at(2026, 6, 2, 9, 0)
|
|
|
|
logs._write_last_generated(when)
|
|
|
|
assert logs._read_last_generated() == when
|
|
|
|
|
|
def test_state_missing_file_returns_none(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""No state file yet means "unknown", so the caller falls back to today."""
|
|
monkeypatch.setattr(logs, "_STATE_FILE", tmp_path / "absent.json")
|
|
|
|
assert logs._read_last_generated() is None
|
|
|
|
|
|
def test_state_corrupt_file_returns_none(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Corrupt or partial JSON is treated as unknown, not a crash."""
|
|
bad = tmp_path / "bad.json"
|
|
bad.write_text("{ not json", encoding="utf-8")
|
|
monkeypatch.setattr(logs, "_STATE_FILE", bad)
|
|
assert logs._read_last_generated() is None
|
|
|
|
bad.write_text("{}", encoding="utf-8") # valid JSON, missing key
|
|
assert logs._read_last_generated() is None
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Day-segment planning (usage_report)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_has_time_of_day() -> None:
|
|
"""Midnight needs no begin bound; any later time does."""
|
|
assert logs._has_time_of_day(_at(2026, 6, 4, 14, 30)) is True
|
|
assert logs._has_time_of_day(_at(2026, 6, 4, 0, 0)) is False
|
|
|
|
|
|
def test_plan_segments_single_day_midnight_unbounded() -> None:
|
|
"""A start at local midnight covers the whole first day (no -b bound)."""
|
|
segments = logs._plan_segments(_at(2026, 6, 4), _at(2026, 6, 4, 12))
|
|
|
|
assert len(segments) == 1
|
|
assert segments[0].atop_begin is None
|
|
assert segments[0].pmon_begin_epoch is None
|
|
|
|
|
|
def test_plan_segments_bounds_only_first_day() -> None:
|
|
"""A mid-day start bounds the first day only; later days are full."""
|
|
start = _at(2026, 6, 2, 14, 0)
|
|
segments = logs._plan_segments(start, _at(2026, 6, 4, 10, 0))
|
|
|
|
assert len(segments) == 3
|
|
assert segments[0].atop_begin == "20260602140000"
|
|
assert segments[0].pmon_begin_epoch == start.timestamp()
|
|
assert all(seg.atop_begin is None for seg in segments[1:])
|
|
assert segments[-1].atop_log.name == "atop_20260604"
|
|
|
|
|
|
def test_plan_segments_start_after_end_is_empty() -> None:
|
|
"""A future state file (start past end) yields no segments."""
|
|
assert logs._plan_segments(_at(2026, 6, 5), _at(2026, 6, 4)) == []
|