"""Tests for the refactored logreport, run against BOTH the legacy and the refactored module. The characterization tests are shared: one base class, two concrete subclasses, one per implementation. If a refactor changes any byte of output, the refactored subclass fails while the legacy one keeps passing, which is exactly the signal you want. The pure-function tests at the bottom exist only for the new code. Run: python -m unittest refactor-with-an-agent-characterization-tests_test.py Python 3.13, standard library only. """ from __future__ import annotations import importlib.util import io import os import sys import unittest HERE = os.path.dirname(os.path.abspath(__file__)) def load(name: str, filename: str): """Import a module from a hyphenated filename. Registering it in sys.modules first matters: dataclasses with postponed annotations look the module up there.""" spec = importlib.util.spec_from_file_location(name, os.path.join(HERE, filename)) mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod legacy = load("logreport_legacy", "add-tests-to-a-legacy-script-with-an-agent.py") new = load("logreport_new", "refactor-with-an-agent-characterization-tests.py") LINES = [ "2026-09-05 14:03:11 ERROR auth: token expired user=42\n", "2026-09-05 14:03:12 INFO auth: refreshed user=42\n", "garbage line without a timestamp\n", "2026-09-05 14:04:00 INFO web: GET /health 200\n", "2026-09-05 14:05:30 WARN db: slow query 1.2s\n", ] EXPECTED_FIVE = ("WARN skipping line 3: 'garbage line without a timestamp'\n" "lines: 5 (skipped 1)\n" " INFO 2\n" " ERROR 1\n" " WARN 1\n" "first: 05/09/26 14:03:11\n" "last: 05/09/26 14:05:30\n") class Characterization: """Mixin: subclasses define report(lines) -> text. (Not named run: TestCase.run exists.)""" def test_report_for_five_lines_is_exactly_this(self): self.assertEqual(self.report(LINES), EXPECTED_FIVE) def test_empty_input_prints_only_the_line_count(self): self.assertEqual(self.report([]), "lines: 0 (skipped 0)\n") def test_levels_sort_by_count_then_name(self): out = self.report(["2026-01-01 00:00:00 B x\n", "2026-01-01 00:00:00 A x\n", "2026-01-01 00:00:00 A x\n", "2026-01-01 00:00:00 C x\n"]) self.assertEqual(out.splitlines()[1:4], [" A 2", " B 1", " C 1"]) def test_level_match_is_case_sensitive(self): out = self.report(["2026-01-01 00:00:00 error x\n", "2026-01-01 00:00:00 ERROR x\n"]) self.assertIn(" ERROR 1\n", out) self.assertIn(" error 1\n", out) def test_malformed_stamps_are_skipped_with_a_warning(self): cases = { "2026-9-5 14:03:11 INFO x\n": "WARN skipping line 1: '2026-9-5 14:03:11 INFO x'", "2026-09-05 14:03 INFO x\n": "WARN skipping line 1: '2026-09-05 14:03 INFO x'", "2026-09-05 14:03:11 INFO\n": "WARN skipping line 1: '2026-09-05 14:03:11 INFO'", } for line, expected in cases.items(): with self.subTest(line=line): self.assertEqual(self.report([line]).splitlines()[0], expected) def test_last_seen_is_wrong_across_a_month_boundary_documented_bug(self): out = self.report(["2026-08-31 23:59:59 INFO x\n", "2026-09-01 00:00:01 INFO y\n"]) self.assertIn("first: 01/09/26 00:00:01\n", out) self.assertIn("last: 31/08/26 23:59:59\n", out) class LegacyBehaviour(Characterization, unittest.TestCase): @staticmethod def report(lines): out = io.StringIO() legacy.run(lines, out) return out.getvalue() class RefactoredBehaviour(Characterization, unittest.TestCase): @staticmethod def report(lines): return new.run(lines) class PureFunctions(unittest.TestCase): """Only possible against the refactored module: no globals, no streams.""" def test_parse_line_returns_entry(self): self.assertEqual(new.parse_line("2026-09-05 14:03:11 ERROR auth: x\n"), new.Entry(stamp="05/09/26 14:03:11", level="ERROR")) def test_parse_line_returns_none_for_garbage(self): self.assertIsNone(new.parse_line("nope\n")) def test_summarise_counts_without_rendering(self): s = new.summarise(LINES) self.assertEqual((s.total, s.bad), (5, 1)) self.assertEqual(s.counts, {"ERROR": 1, "INFO": 2, "WARN": 1}) self.assertEqual(len(s.warnings), 1) def test_render_of_empty_summary(self): self.assertEqual(new.render(new.Summary()), "lines: 0 (skipped 0)\n") def test_summarise_is_pure_across_calls(self): new.summarise(LINES) self.assertEqual(new.summarise([]).total, 0) if __name__ == "__main__": unittest.main()