mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:07:03 +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
170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
"""Tests for usage_report run modes and field parsing: start resolution,
|
|
single-day vs multi-day dispatch, the report period fragments, the PRC
|
|
HZ-field regression, and native-helper selection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as _dt
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import _usage_report_logs as logs
|
|
import _usage_report_parsing as parsing
|
|
import _usage_report_run as run
|
|
import usage_report
|
|
|
|
if TYPE_CHECKING:
|
|
from _usage_report_types import _PidCpu
|
|
import pytest
|
|
|
|
|
|
# Aware timezone matching how the parser localizes naive timestamps, so epochs
|
|
# computed here line up with `_pmon_row_epoch`'s `.astimezone()` conversion.
|
|
_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)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Start resolution and mode dispatch (usage_report)
|
|
# --------------------------------------------------------------------------- #
|
|
def _args(**overrides: object) -> argparse.Namespace:
|
|
"""Build a Namespace with the usage_report CLI defaults."""
|
|
base: dict[str, object] = {
|
|
"date": None,
|
|
"since": None,
|
|
"atop_log": None,
|
|
"pmon_log": None,
|
|
}
|
|
base.update(overrides)
|
|
return argparse.Namespace(**base)
|
|
|
|
|
|
def test_resolve_start_prefers_since(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""--since wins over any saved state and starts at local midnight."""
|
|
monkeypatch.setattr(run, "_read_last_generated", lambda: _at(2026, 1, 1))
|
|
start = run._resolve_start(_args(since="20260604"), _at(2026, 6, 4, 12))
|
|
|
|
assert start.date() == _dt.date(2026, 6, 4)
|
|
assert (start.hour, start.minute) == (0, 0)
|
|
|
|
|
|
def test_resolve_start_uses_last_report(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Without --since, the saved last-report timestamp is the start."""
|
|
last = _at(2026, 6, 2, 9, 0)
|
|
monkeypatch.setattr(run, "_read_last_generated", lambda: last)
|
|
|
|
assert run._resolve_start(_args(), _at(2026, 6, 4, 12)) == last
|
|
|
|
|
|
def test_resolve_start_first_run_is_today_midnight(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""First-ever run (no state) covers today from local midnight."""
|
|
monkeypatch.setattr(run, "_read_last_generated", lambda: None)
|
|
now = _at(2026, 6, 4, 12, 30)
|
|
|
|
assert run._resolve_start(_args(), now) == _at(2026, 6, 4, 0, 0)
|
|
|
|
|
|
def test_is_single_day_mode() -> None:
|
|
"""Pinning a date or explicit log path selects single-day mode."""
|
|
assert usage_report._is_single_day_mode(_args(date="20260604")) is True
|
|
assert usage_report._is_single_day_mode(_args(atop_log=Path("/x"))) is True
|
|
assert usage_report._is_single_day_mode(_args(pmon_log=Path("/x"))) is True
|
|
assert usage_report._is_single_day_mode(_args()) is False
|
|
|
|
|
|
def test_should_advance_state_only_for_default_run() -> None:
|
|
"""Only a plain since-last-report run re-baselines the saved timestamp."""
|
|
assert run._should_advance_state(_args(no_update_state=False)) is True
|
|
assert run._should_advance_state(_args(no_update_state=True)) is False
|
|
# --since is an ad-hoc query and must never advance state.
|
|
assert (
|
|
run._should_advance_state(
|
|
_args(since="20260510", no_update_state=False),
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Report fragments (usage_report)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_period_line_contains_both_bounds() -> None:
|
|
"""The period bullet shows start, end, and the span."""
|
|
line = run._period_line(_at(2026, 6, 2, 9), _at(2026, 6, 4, 9))
|
|
|
|
assert "2026-06-02T09:00:00" in line
|
|
assert "2026-06-04T09:00:00" in line
|
|
assert "→" in line
|
|
|
|
|
|
def test_describe_logs_counts() -> None:
|
|
"""Log description switches between none / single / multiple wording."""
|
|
assert "none found" in logs._describe_logs([], "atop -r")
|
|
assert logs._describe_logs(
|
|
[Path("/var/log/atop/atop_20260604")], "atop -r"
|
|
).startswith(
|
|
"`/var/log/atop/atop_20260604`",
|
|
)
|
|
many = logs._describe_logs(
|
|
[Path("/v/atop_20260601"), Path("/v/atop_20260604")],
|
|
"atop -r",
|
|
)
|
|
assert "2 daily logs" in many
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# PRC field parsing — HZ-field regression (parsing)
|
|
# --------------------------------------------------------------------------- #
|
|
def test_parse_prc_does_not_charge_hz_as_cpu() -> None:
|
|
"""atop emits `... pid (name) state HZ utime stime`; the HZ column must be
|
|
skipped, never summed as CPU.
|
|
|
|
Regression for the off-by-one that read HZ (100) as utime, which inflated
|
|
every process's CPU-seconds to its record/PID count (xset showing 67h).
|
|
"""
|
|
pid_cpu: dict[int, _PidCpu] = {}
|
|
# 6 generic fields, pid, (name), state, HZ=100, utime=7, stime=3, + tail.
|
|
line = "PRC host 1000 2026/06/04 12:00:00 600 4242 (xset) E 100 7 3 0 0 0"
|
|
|
|
parsing._parse_prc(line.split(), pid_cpu)
|
|
|
|
entry = pid_cpu[4242]
|
|
assert entry.name == "xset"
|
|
assert entry.delta_ticks == 10 # utime+stime, never the HZ constant (100)
|
|
|
|
|
|
def test_parse_prc_skips_hz_with_multiword_name() -> None:
|
|
"""The HZ skip stays aligned when the name spans several tokens."""
|
|
pid_cpu: dict[int, _PidCpu] = {}
|
|
line = "PRC h 1000 d t 600 99 (Web Content) S 100 40 2 0 0"
|
|
|
|
parsing._parse_prc(line.split(), pid_cpu)
|
|
|
|
assert pid_cpu[99].name == "Web Content"
|
|
assert pid_cpu[99].delta_ticks == 42 # 40+2, HZ(100) skipped
|
|
|
|
|
|
def test_parse_prc_too_short_is_ignored() -> None:
|
|
"""A truncated PRC record (missing stime) is skipped, not a crash."""
|
|
pid_cpu: dict[int, _PidCpu] = {}
|
|
# Tokens run out at utime — no stime at after+3, so the record is dropped.
|
|
line = "PRC h 1000 d t 600 7 (x) S 100 5"
|
|
|
|
parsing._parse_prc(line.split(), pid_cpu)
|
|
|
|
assert pid_cpu == {}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Native helper selection (parsing)
|