Coverage for Users / vladimirpavlov / PycharmProjects / parameterizable / tests / test_json_processor_round_trips.py: 98%
170 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-01 16:37 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-01 16:37 -0600
1"""Round-trip tests for json_processor serialization and deserialization.
3This module tests the complete serialize-deserialize cycle for various
4object types including primitives, containers, enums, and custom objects.
5"""
6import json
7import pytest
9from enum import Enum
11from mixinforge.json_processor import (
12 _to_serializable_dict,
13 _from_serializable_dict,
14 dumpjs,
15 loadjs,
16 _Markers,
17)
20class Color(Enum):
21 RED = 1
22 GREEN = 2
23 BLUE = 3
26class DictOnly:
27 def __init__(self):
28 self.x = 10
29 self.y = "hi"
32class SlotsOnly:
33 __slots__ = ("a", "b")
35 def __init__(self, a=1, b=2):
36 self.a = a
37 self.b = b
40class BaseSlots:
41 __slots__ = ("base",)
43 def __init__(self):
44 self.base = "base"
47class Hybrid(BaseSlots):
48 __slots__ = ("__dict__", "s")
50 def __init__(self):
51 super().__init__()
52 self.s = 42
53 self.d = "present in __dict__"
56class BadStateTuple(BaseSlots):
57 def __getstate__(self):
58 return 1, 2, 3
61class GetParams:
62 def __init__(self, a=3, b="z"):
63 self.a = a
64 self.b = b
66 def get_params(self):
67 return {"a": self.a, "b": self.b}
70class GetState:
71 def __init__(self, v):
72 self._v = v
74 def __getstate__(self):
75 return {"v": self._v}
77 def __setstate__(self, state):
78 self._v = state["v"]
81class SlottedGetstateDict:
82 __slots__ = ("val",)
84 def __init__(self, val):
85 self.val = val
87 def __getstate__(self):
88 return {"val": self.val}
91class StateNoSetState:
92 def __init__(self, a, b):
93 self.a = a
94 self.b = b
96 def __getstate__(self):
97 return {"a": 111, "b": 222}
100class WithWeakref:
101 __slots__ = ("a", "__weakref__")
103 def __init__(self, a=1):
104 self.a = a
107def test_round_trip_dict_only():
108 obj = DictOnly()
109 ser = dumpjs(obj)
110 back = loadjs(ser)
111 assert isinstance(back, DictOnly)
112 assert back.x == 10
113 assert back.y == "hi"
116def test_round_trip_slotted_with_getstate_dict():
117 obj = SlottedGetstateDict(101)
118 ser = dumpjs(obj)
119 back = loadjs(ser)
120 assert isinstance(back, SlottedGetstateDict)
121 assert back.val == 101
124def test_round_trip_hybrid_slots_and_dict():
125 obj = Hybrid()
126 obj.base = "new base"
127 obj.s = 99
128 obj.d = "new dict val"
129 obj.extra = "another dict val"
131 ser = dumpjs(obj)
132 back = loadjs(ser)
134 assert isinstance(back, Hybrid)
135 assert back.base == "new base"
136 assert back.s == 99
137 assert back.d == "new dict val"
138 assert back.extra == "another dict val"
141def test_round_trip_with_weakref():
142 obj = WithWeakref(a=10)
143 ser = dumpjs(obj)
144 back = loadjs(ser)
145 assert isinstance(back, WithWeakref)
146 assert back.a == 10
149def test_recreate_from_malformed_tuple_state_raises():
150 obj = BadStateTuple()
151 serialized = {
152 _Markers.MODULE: __name__,
153 _Markers.CLASS: "BadStateTuple",
154 _Markers.STATE: _to_serializable_dict(obj.__getstate__()),
155 }
157 with pytest.raises(TypeError, match="Tuple state length .* does not match"):
158 _from_serializable_dict(serialized)
161def test_recreate_from_cpython_getstate_tuple_format():
162 """Verify reconstruction from CPython's (dict, slots) tuple state format."""
163 obj = Hybrid()
164 obj.base = "b"
165 obj.s = 1
166 obj.d = 2
168 state_tuple = ({"d": 2}, {"base": "b", "s": 1})
170 serialized_payload = {
171 _Markers.MODULE: __name__,
172 _Markers.CLASS: "Hybrid",
173 _Markers.STATE: _to_serializable_dict(state_tuple),
174 }
176 reconstructed = _from_serializable_dict(serialized_payload)
178 assert isinstance(reconstructed, Hybrid)
179 assert reconstructed.base == "b"
180 assert reconstructed.s == 1
181 assert reconstructed.d == 2
184@pytest.mark.parametrize(
185 "obj, equals",
186 [
187 (None, lambda a, b: a is b),
188 (True, lambda a, b: a is b),
189 (0, lambda a, b: a == b),
190 (3.14, lambda a, b: a == b),
191 ("abc", lambda a, b: a == b),
192 ([1, 2, 3], lambda a, b: a == b),
193 ((1, 2, 3), lambda a, b: a == b),
194 ({1, 2, 3}, lambda a, b: a == b),
195 ({"a": 1, "b": (2, 3)}, lambda a, b: a == b),
196 ({1: "a", 2: "b"}, lambda a, b: a == b),
197 ({"a": 1, 2: "b"}, lambda a, b: a == b),
198 (Color.RED, lambda a, b: a is b),
199 ],
200)
201def test_round_trip_matrix(obj, equals):
202 ser = _to_serializable_dict(obj)
203 back = _from_serializable_dict(ser)
204 assert equals(obj, back)
207def test_from_serializable_round_trip_complex():
208 complex_obj = {
209 (1, 2): {"a": [1, 2, {3, 4}]},
210 "enum": Color.RED,
211 "obj": GetParams(8, "q"),
212 }
213 ser = _to_serializable_dict(complex_obj)
214 de = _from_serializable_dict(ser)
215 assert (1, 2) in de
216 assert de["enum"] is Color.RED
217 assert isinstance(de["obj"], GetParams)
218 assert de["obj"].a == 8 and de["obj"].b == "q"
219 assert de[(1, 2)]["a"][2] == {3, 4}
222def test_round_trip_custom_objects_both_variants():
223 gp = GetParams(7, "z")
224 gp_ser = _to_serializable_dict(gp)
225 gp_back = _from_serializable_dict(gp_ser)
226 assert isinstance(gp_back, GetParams)
227 assert gp_back.a == 7 and gp_back.b == "z"
229 gs = GetState(123)
230 gs_ser = _to_serializable_dict(gs)
231 gs_back = _from_serializable_dict(gs_ser)
232 assert isinstance(gs_back, GetState)
233 assert gs_back._v == 123
236def test_round_trip_slots_only():
237 obj = SlotsOnly(a=10, b=20)
238 ser = dumpjs(obj)
239 back = loadjs(ser)
240 assert isinstance(back, SlotsOnly)
241 assert back.a == 10
242 assert back.b == 20
245def test_mixed_key_dict_uses_DICT_marker_and_round_trips():
246 data = {"a": 1, 2: (3, 4)}
247 ser = _to_serializable_dict(data)
248 assert isinstance(ser, dict) and _Markers.DICT in ser
249 back = _from_serializable_dict(ser)
250 assert back == {"a": 1, 2: (3, 4)}
253@pytest.mark.parametrize(
254 "payload",
255 [
256 {_Markers.TUPLE: [1, 2], "extra": 1},
257 {_Markers.SET: [1, 2], "extra": 1},
258 {_Markers.DICT: [], "extra": 1},
259 ],
260)
261def test_from_serializable_marker_exclusive_key_violation(payload):
262 with pytest.raises(TypeError):
263 _from_serializable_dict(payload)
266@pytest.mark.parametrize(
267 "payload",
268 [
269 {_Markers.TUPLE: 123},
270 {_Markers.SET: 123},
271 {_Markers.DICT: 123},
272 {_Markers.DICT: [[1, 2, 3]]},
273 ],
274)
275def test_from_serializable_marker_value_type_violation(payload):
276 with pytest.raises(TypeError):
277 _from_serializable_dict(payload)
280def test_dumps_and_loads_round_trip_json_string_with_kwargs():
281 obj = {
282 "msg": "hello",
283 "nums": (1, 2, 3),
284 "enum": Color.GREEN,
285 "inner": GetParams(2, "ok"),
286 }
287 s = dumpjs(obj, indent=2, sort_keys=True)
288 assert isinstance(s, str) and "\n" in s and " " in s
290 loaded = loadjs(s)
291 assert loaded["msg"] == "hello"
292 assert loaded["nums"] == (1, 2, 3)
293 assert loaded["enum"] is Color.GREEN
294 assert isinstance(loaded["inner"], GetParams)
295 assert loaded["inner"].a == 2
296 assert loaded["inner"].b == "ok"
299def test_loads_forbids_object_hook_and_invalid_json():
300 with pytest.raises(ValueError):
301 loadjs("{}", object_hook=lambda d: d)
302 with pytest.raises(json.JSONDecodeError):
303 loadjs("not a json")