netsleuth/tests/test_history.py
Andrew Pimenov 245533f904 add Prometheus/CSV export, auto-diff vs previous run, and --trend
New metrics.py: collect_metrics() does the per-section extraction a flat
dict walk can't (latency needs label/host/method, TLS label/host, path hops
target/ttl/ip/asn), producing a flat Metric list that render_prometheus()
and render_csv() each format in about 15 lines. Simpler than treating them
as two separate renderers. FORMAT_EXTENSIONS gained "prom"/"csv" as the
single wiring point both parse_formats() and Output._normalize_formats()
already key off of.

New history.py finds a previous JSON report for the same ASN by reusing
the exact filename convention exporter.report_filename() already writes,
so the lookup key can't drift from what's on disk. Wired into diagnose()
before write_report() - a compact vs-previous-run summary
(compare.py::render_diff_brief(), new) prints automatically whenever this
run writes JSON and an earlier one exists for the same network; silent on
every other case (first run, prior run wrote no JSON prints one note
instead of guessing). New --no-auto-diff escapes it per run,
history.auto_diff in config disables it entirely.

New trend.py aggregates N saved JSON reports into per-label sparkline
series via --trend N - read-only, and it doesn't touch the network to find
"this network's" history either: history.latest_key() just reads it off
the newest report filename already on disk.

Growing FORMAT_EXTENSIONS to 5 entries broke three pre-existing tests that
hardcoded the old 3-format set; fixed in this commit.

Live-tested: prom/csv artifacts render correctly, auto-diff fired on a
second run, --trend 3 produced readable output, --compare unaffected.

Part of plans/zesty-zooming-wilkinson.md (gitignored).
2026-08-12 22:10:21 +03:00

68 lines
2.5 KiB
Python

from __future__ import annotations
from netsleuth.history import find_previous, latest_key, matching_reports, report_key
def test_report_key_prefers_the_explicit_target():
assert report_key({"target": "AS64500"}, {}) == "AS64500"
def test_report_key_falls_back_to_egress_asn():
report = {"ip_geo": {"data": {"egress_v4": {"asn": "AS64500"}}}}
assert report_key({}, report) == "AS64500"
def test_matching_reports_orders_newest_first():
names = [
"report_AS64500_20260101T000000Z.json",
"report_AS64500_20260103T000000Z.json",
"report_AS64500_20260102T000000Z.json",
]
assert matching_reports(names, "AS64500") == [
"report_AS64500_20260103T000000Z.json",
"report_AS64500_20260102T000000Z.json",
"report_AS64500_20260101T000000Z.json",
]
def test_matching_reports_excludes_the_current_file():
names = ["report_AS64500_20260101T000000Z.json", "report_AS64500_20260102T000000Z.json"]
assert matching_reports(names, "AS64500", exclude="report_AS64500_20260102T000000Z.json") == [
"report_AS64500_20260101T000000Z.json"
]
def test_matching_reports_does_not_cross_contaminate_different_keys():
names = ["report_AS1_20260101T000000Z.json", "report_AS13335_20260101T000000Z.json"]
assert matching_reports(names, "AS1") == ["report_AS1_20260101T000000Z.json"]
def test_matching_reports_ignores_non_json_files():
names = ["report_AS64500_20260101T000000Z.md", "report_AS64500_20260101T000000Z.json"]
assert matching_reports(names, "AS64500") == ["report_AS64500_20260101T000000Z.json"]
def test_find_previous_on_an_empty_directory(tmp_path):
assert find_previous(tmp_path, "AS64500") == []
def test_find_previous_on_a_nonexistent_directory(tmp_path):
assert find_previous(tmp_path / "nope", "AS64500") == []
def test_find_previous_respects_limit(tmp_path):
for ts in ("20260101T000000Z", "20260102T000000Z", "20260103T000000Z"):
(tmp_path / f"report_AS64500_{ts}.json").write_text("{}", encoding="utf-8")
result = find_previous(tmp_path, "AS64500", limit=2)
assert len(result) == 2
assert result[0].name.endswith("20260103T000000Z.json")
def test_latest_key_picks_the_newest_report_across_any_key(tmp_path):
(tmp_path / "report_AS1_20260101T000000Z.json").write_text("{}", encoding="utf-8")
(tmp_path / "report_AS2_20260102T000000Z.json").write_text("{}", encoding="utf-8")
assert latest_key(tmp_path) == "AS2"
def test_latest_key_on_an_empty_directory(tmp_path):
assert latest_key(tmp_path) is None