diff --git a/wizard-work/beam/test_json_canonical.py b/wizard-work/beam/test_json_canonical.py index 4bec93b..2f42851 100644 --- a/wizard-work/beam/test_json_canonical.py +++ b/wizard-work/beam/test_json_canonical.py @@ -1,39 +1,70 @@ -import sys, os, json +#!/usr/bin/env python3 +"""Cells for json_canonical.py: sorted keys, compact separators, UTF-8 pass-through, +the rejection surface, and round-trip. + +Seven cells, one per behaviour the module claims in its docstring. They are +`unittest.TestCase` methods, so `unittest discover` collects them and the lane's +CHECK counts them; there is no module-level verdict printed at import. + +The module path resolves from this file's own directory, never a host path. +""" +import json +import os +import sys +import unittest + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from json_canonical import canonical, canonical_bytes, CanonicalError +from json_canonical import canonical, canonical_bytes, CanonicalError # noqa: E402 -def test_basic(): - assert canonical({"b":2,"a":1}) == '{"a":1,"b":2}' - assert canonical([3,1,2]) == '[3,1,2]' - assert canonical(True) == 'true' - assert canonical(None) == 'null' - assert canonical("x") == '"x"' -def test_nested(): - assert canonical({"z":{"y":1,"x":2},"a":[1,2]}) == '{"a":[1,2],"z":{"x":2,"y":1}}' +class JsonCanonicalTest(unittest.TestCase): + # cell 1: sorted object keys, list order preserved, the three scalars + def test_basic(self): + self.assertEqual(canonical({"b": 2, "a": 1}), '{"a":1,"b":2}') + self.assertEqual(canonical([3, 1, 2]), "[3,1,2]") + self.assertEqual(canonical(True), "true") + self.assertEqual(canonical(False), "false") + self.assertEqual(canonical(None), "null") + self.assertEqual(canonical("x"), '"x"') -def test_unicode(): - assert canonical({"é":1}) == '{"é":1}' + # cell 2: sorting reaches nested objects and nothing is reordered inside lists + def test_nested(self): + self.assertEqual(canonical({"z": {"y": 1, "x": 2}, "a": [1, 2]}), + '{"a":[1,2],"z":{"x":2,"y":1}}') -def test_reject_nan(): - try: - canonical(float('nan')) - raise AssertionError("expected error") - except CanonicalError: - pass + # cell 3: ensure_ascii=False -- non-ASCII survives as UTF-8, not \u escapes + def test_unicode(self): + self.assertEqual(canonical({"é": "ü"}), '{"é":"ü"}') + self.assertEqual(canonical_bytes({"é": "ü"}), '{"é":"ü"}'.encode("utf-8")) + self.assertNotIn("\\u", canonical({"é": "ü"})) -def test_reject_nonstr_key(): - try: - canonical({1:2}) - raise AssertionError("expected error") - except CanonicalError: - pass + # cell 4: every non-finite float is refused, not serialised as NaN/Infinity + def test_reject_nan(self): + for bad in (float("nan"), float("inf"), float("-inf")): + with self.assertRaises(CanonicalError): + canonical(bad) + with self.assertRaises(CanonicalError): + canonical({"k": [bad]}) -def test_bytes(): - assert canonical_bytes({"a":1}) == b'{"a":1}' + # cell 5: a non-string key is refused wherever it sits, including nested + def test_reject_nonstr_key(self): + for bad in ({1: 2}, {None: 2}, {1.0: 2}, {"a": {2: 3}}): + with self.assertRaises(CanonicalError): + canonical(bad) -def test_roundtrip(): - o = {"a":[1,2,3],"b":{"c":True}} - assert json.loads(canonical(o)) == o + # cell 6: canonical_bytes is bytes, encoded once, byte-identical to the str form + def test_bytes(self): + out = canonical_bytes({"a": 1}) + self.assertIsInstance(out, bytes) + self.assertEqual(out, b'{"a":1}') + self.assertEqual(canonical_bytes([1.5, "é"]), canonical([1.5, "é"]).encode("utf-8")) -print("ALL PASS") + # cell 7: the output parses back to the input value + def test_roundtrip(self): + obj = {"a": [1, 2, 3], "b": {"c": True, "d": None}, "é": "ü", "f": 1.5} + self.assertEqual(json.loads(canonical(obj)), obj) + self.assertEqual(json.loads(canonical_bytes(obj).decode("utf-8")), obj) + + +if __name__ == "__main__": + unittest.main()