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

1"""Round-trip tests for json_processor serialization and deserialization. 

2 

3This module tests the complete serialize-deserialize cycle for various 

4object types including primitives, containers, enums, and custom objects. 

5""" 

6import json 

7import pytest 

8 

9from enum import Enum 

10 

11from mixinforge.json_processor import ( 

12 _to_serializable_dict, 

13 _from_serializable_dict, 

14 dumpjs, 

15 loadjs, 

16 _Markers, 

17) 

18 

19 

20class Color(Enum): 

21 RED = 1 

22 GREEN = 2 

23 BLUE = 3 

24 

25 

26class DictOnly: 

27 def __init__(self): 

28 self.x = 10 

29 self.y = "hi" 

30 

31 

32class SlotsOnly: 

33 __slots__ = ("a", "b") 

34 

35 def __init__(self, a=1, b=2): 

36 self.a = a 

37 self.b = b 

38 

39 

40class BaseSlots: 

41 __slots__ = ("base",) 

42 

43 def __init__(self): 

44 self.base = "base" 

45 

46 

47class Hybrid(BaseSlots): 

48 __slots__ = ("__dict__", "s") 

49 

50 def __init__(self): 

51 super().__init__() 

52 self.s = 42 

53 self.d = "present in __dict__" 

54 

55 

56class BadStateTuple(BaseSlots): 

57 def __getstate__(self): 

58 return 1, 2, 3 

59 

60 

61class GetParams: 

62 def __init__(self, a=3, b="z"): 

63 self.a = a 

64 self.b = b 

65 

66 def get_params(self): 

67 return {"a": self.a, "b": self.b} 

68 

69 

70class GetState: 

71 def __init__(self, v): 

72 self._v = v 

73 

74 def __getstate__(self): 

75 return {"v": self._v} 

76 

77 def __setstate__(self, state): 

78 self._v = state["v"] 

79 

80 

81class SlottedGetstateDict: 

82 __slots__ = ("val",) 

83 

84 def __init__(self, val): 

85 self.val = val 

86 

87 def __getstate__(self): 

88 return {"val": self.val} 

89 

90 

91class StateNoSetState: 

92 def __init__(self, a, b): 

93 self.a = a 

94 self.b = b 

95 

96 def __getstate__(self): 

97 return {"a": 111, "b": 222} 

98 

99 

100class WithWeakref: 

101 __slots__ = ("a", "__weakref__") 

102 

103 def __init__(self, a=1): 

104 self.a = a 

105 

106 

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" 

114 

115 

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 

122 

123 

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" 

130 

131 ser = dumpjs(obj) 

132 back = loadjs(ser) 

133 

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" 

139 

140 

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 

147 

148 

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 } 

156 

157 with pytest.raises(TypeError, match="Tuple state length .* does not match"): 

158 _from_serializable_dict(serialized) 

159 

160 

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 

167 

168 state_tuple = ({"d": 2}, {"base": "b", "s": 1}) 

169 

170 serialized_payload = { 

171 _Markers.MODULE: __name__, 

172 _Markers.CLASS: "Hybrid", 

173 _Markers.STATE: _to_serializable_dict(state_tuple), 

174 } 

175 

176 reconstructed = _from_serializable_dict(serialized_payload) 

177 

178 assert isinstance(reconstructed, Hybrid) 

179 assert reconstructed.base == "b" 

180 assert reconstructed.s == 1 

181 assert reconstructed.d == 2 

182 

183 

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) 

205 

206 

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} 

220 

221 

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" 

228 

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 

234 

235 

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 

243 

244 

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)} 

251 

252 

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) 

264 

265 

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) 

278 

279 

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 

289 

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" 

297 

298 

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")