mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21: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
180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
"""Tests for usage_report's command-line surface: argument parsing, the
|
|
single-day and since-last-report entry points, and main's dispatch between
|
|
them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as _dt
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import _usage_report_run as run
|
|
from _usage_report_types import _Progress, _Window
|
|
import usage_report
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
import pytest
|
|
|
|
|
|
def _progress() -> _Progress:
|
|
"""A progress reporter that never draws, so stderr stays clean."""
|
|
return _Progress(enabled=False, total_stages=1)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# _build_parser
|
|
# --------------------------------------------------------------------------- #
|
|
def test_parser_defaults() -> None:
|
|
"""With no flags every option takes its documented default."""
|
|
args = usage_report._build_parser().parse_args([])
|
|
|
|
assert args.date is None
|
|
assert args.since is None
|
|
assert args.top == usage_report._DEFAULT_TOP
|
|
assert args.atop_log is None
|
|
assert args.pmon_log is None
|
|
assert args.no_clipboard is False
|
|
assert args.no_update_state is False
|
|
assert args.quiet is False
|
|
|
|
|
|
def test_parser_reads_every_flag() -> None:
|
|
"""Each flag lands on the namespace with the right type."""
|
|
args = usage_report._build_parser().parse_args(
|
|
[
|
|
"--date",
|
|
"20260817",
|
|
"--since",
|
|
"20260801",
|
|
"--top",
|
|
"20",
|
|
"--atop-log",
|
|
"/nonexistent/a",
|
|
"--pmon-log",
|
|
"/nonexistent/p",
|
|
"--no-clipboard",
|
|
"--no-update-state",
|
|
"--quiet",
|
|
],
|
|
)
|
|
|
|
assert args.date == "20260817"
|
|
assert args.since == "20260801"
|
|
assert args.top == 20
|
|
assert args.atop_log == Path("/nonexistent/a")
|
|
assert args.pmon_log == Path("/nonexistent/p")
|
|
assert args.no_clipboard is True
|
|
assert args.no_update_state is True
|
|
assert args.quiet is True
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# main dispatch
|
|
# --------------------------------------------------------------------------- #
|
|
def test_main_routes_single_day(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A --date run goes to the single-day path."""
|
|
seen: list[str] = []
|
|
|
|
def record(label: str) -> Callable[..., int]:
|
|
def run(*_a: object) -> int:
|
|
seen.append(label)
|
|
return 0
|
|
|
|
return run
|
|
|
|
monkeypatch.setattr(usage_report, "_run_single_day", record("single"))
|
|
monkeypatch.setattr(usage_report, "_run_since", record("since"))
|
|
|
|
assert usage_report.main(["--date", "20260817"]) == 0
|
|
assert seen == ["single"]
|
|
|
|
|
|
def test_main_routes_since_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""With no day-selecting flag the since-last-report path runs."""
|
|
seen: list[str] = []
|
|
|
|
def record(label: str) -> Callable[..., int]:
|
|
def run(*_a: object) -> int:
|
|
seen.append(label)
|
|
return 0
|
|
|
|
return run
|
|
|
|
monkeypatch.setattr(usage_report, "_run_single_day", record("single"))
|
|
monkeypatch.setattr(usage_report, "_run_since", record("since"))
|
|
|
|
assert usage_report.main([]) == 0
|
|
assert seen == ["since"]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# _run_single_day
|
|
# --------------------------------------------------------------------------- #
|
|
def test_run_single_day_emits_a_report(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The single-day path renders once and writes the result out."""
|
|
monkeypatch.setattr(run, "_preflight", lambda _log: None)
|
|
monkeypatch.setattr(
|
|
run,
|
|
"_resolve_logs",
|
|
lambda _date: (Path("/nonexistent/atop"), Path("/nonexistent/pmon")),
|
|
)
|
|
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: k["period_line"])
|
|
emitted: list[str] = []
|
|
monkeypatch.setattr(run, "_emit", lambda _args, r: emitted.append(r))
|
|
|
|
rc = run._run_single_day(
|
|
_args(date="20260817", quiet=True),
|
|
_dt.datetime.now().astimezone(),
|
|
)
|
|
|
|
assert rc == 0
|
|
assert emitted == ["- **Reporting period**: 20260817 (single day)"]
|
|
|
|
|
|
def test_run_single_day_defaults_an_empty_window_to_a_day(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A window with no measured seconds is treated as a full day."""
|
|
aggs = _aggregates(seconds=0)
|
|
monkeypatch.setattr(run, "_preflight", lambda _log: None)
|
|
monkeypatch.setattr(
|
|
run,
|
|
"_resolve_logs",
|
|
lambda _date: (Path("/nonexistent/atop"), Path("/nonexistent/pmon")),
|
|
)
|
|
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)
|
|
|
|
run._run_single_day(
|
|
_args(date="20260817", quiet=True),
|
|
_dt.datetime.now().astimezone(),
|
|
)
|
|
|
|
assert aggs.window.seconds == run._SEC_PER_DAY
|