# SPDX-License-Identifier: MIT """Local regression tests. Run beside task_report.py; no network or paid API.""" import importlib.util import json from pathlib import Path import subprocess import sys import tempfile import unittest SOURCE = Path(__file__).with_name("task_report.py") spec = importlib.util.spec_from_file_location("task_report", SOURCE) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) class TaskReportTests(unittest.TestCase): def run_cli(self, raw): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "tasks.json" path.write_bytes(raw) return subprocess.run([sys.executable, str(SOURCE), str(path)], capture_output=True, timeout=5) def check_invalid(self, raw): result = self.run_cli(raw) self.assertEqual(result.returncode, 2) self.assertEqual(result.stdout, b"") self.assertEqual(result.stderr, b"error: invalid or unreadable task file\n") def test_valid_mixed(self): raw = json.dumps([{"title": "Read", "status": "open"}, {"title": "Test", "status": "closed"}]).encode() result = self.run_cli(raw) self.assertEqual(result.returncode, 0) self.assertEqual(result.stderr, b"") self.assertEqual(json.loads(result.stdout), {"records": 2, "by_status": {"open": 1, "closed": 1}}) def test_empty_list(self): self.assertEqual(module.summarise([]), {"records": 0, "by_status": {"closed": 0, "open": 0}}) def test_invalid_syntax(self): self.check_invalid(b"[") def test_wrong_outer_type(self): self.check_invalid(b'{}') def test_invalid_last_record_no_partial_output(self): self.check_invalid(b'[{"title":"a","status":"open"},{"title":"b","status":"pending"}]') def test_non_object_record(self): self.check_invalid(b'[1]') def test_missing_and_extra_fields(self): for raw in [b'[{"title":"a"}]', b'[{"title":"a","status":"open","id":1}]']: with self.subTest(raw=raw): self.check_invalid(raw) def test_wrong_status_types(self): for value in [True, 1, None, [], {}]: with self.subTest(value=value): self.check_invalid(json.dumps([{"title": "a", "status": value}]).encode()) def test_title_rules(self): for value in ["", " \t", 1, None, "a" * 101]: with self.subTest(value=value): self.check_invalid(json.dumps([{"title": value, "status": "open"}]).encode()) self.assertEqual(module.summarise([{"title": "a" * 100, "status": "open"}])["records"], 1) def test_unicode_title(self): raw = json.dumps([{"title": "Café", "status": "open"}], ensure_ascii=False).encode() self.assertEqual(self.run_cli(raw).returncode, 0) def test_status_is_case_sensitive(self): self.check_invalid(b'[{"title":"a","status":"Open"}]') def test_duplicate_object_keys(self): self.check_invalid(b'[{"title":"a","status":"closed","status":"open"}]') def test_invalid_utf8(self): self.check_invalid(b'\xff') def test_bom_rejected(self): self.check_invalid(b'\xef\xbb\xbf[]') def test_nonstandard_constants(self): for raw in [b'[NaN]', b'[Infinity]', b'[-Infinity]']: with self.subTest(raw=raw): self.check_invalid(raw) def test_exact_byte_boundary_and_one_more(self): result = self.run_cli(b'[]' + b' ' * (module.MAX_BYTES - 2)) self.assertEqual(result.returncode, 0) self.check_invalid(b'[]' + b' ' * (module.MAX_BYTES - 1)) def test_record_limit(self): raw = json.dumps([{"title": "a", "status": "open"}] * module.MAX_RECORDS).encode() self.assertLessEqual(len(raw), module.MAX_BYTES) self.assertEqual(self.run_cli(raw).returncode, 0) self.check_invalid(json.dumps([{"title": "a", "status": "open"}] * (module.MAX_RECORDS + 1)).encode()) def test_excessive_nesting(self): self.check_invalid(b'[' * 2000 + b']' * 2000) def test_missing_file(self): with tempfile.TemporaryDirectory() as folder: result = subprocess.run([sys.executable, str(SOURCE), str(Path(folder) / "missing.json")], capture_output=True, timeout=5) self.assertEqual(result.returncode, 2) self.assertEqual(result.stdout, b"") self.assertEqual(result.stderr, b"error: invalid or unreadable task file\n") def test_help_and_missing_argument(self): help_result = subprocess.run([sys.executable, str(SOURCE), "--help"], capture_output=True, timeout=5) self.assertEqual(help_result.returncode, 0) self.assertIn(b"usage:", help_result.stdout) missing = subprocess.run([sys.executable, str(SOURCE)], capture_output=True, timeout=5) self.assertEqual(missing.returncode, 2) self.assertEqual(missing.stdout, b"") if __name__ == "__main__": unittest.main()