Skip to content

Commit d717554

Browse files
add tests
1 parent 27a76fd commit d717554

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""Tests for orjson-based serializers in reflex_base.utils.format.
2+
3+
Covers ``orjson_dumps``, ``orjson_loads``, ``orjson_dumps_socket`` and the
4+
``_replace_non_finite_floats`` walker.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import dataclasses
10+
import datetime
11+
import json
12+
from decimal import Decimal
13+
from enum import Enum
14+
from pathlib import Path
15+
from uuid import UUID
16+
17+
import pytest
18+
19+
# Skip the entire module if orjson is not installed -- the helpers have
20+
# stdlib fallbacks but these tests target the orjson code path.
21+
pytest.importorskip("orjson")
22+
23+
from reflex_base.style import Style
24+
from reflex_base.utils.format import (
25+
INF_SENTINEL,
26+
NAN_SENTINEL,
27+
NEG_INF_SENTINEL,
28+
_replace_non_finite_floats,
29+
json_dumps,
30+
orjson_dumps,
31+
orjson_dumps_socket,
32+
orjson_loads,
33+
)
34+
35+
# --- orjson_dumps + orjson_loads round-trip ---
36+
37+
38+
@pytest.mark.parametrize(
39+
"value",
40+
[
41+
None,
42+
True,
43+
False,
44+
0,
45+
-1,
46+
1.5,
47+
"hello",
48+
"",
49+
[],
50+
{},
51+
[1, 2, 3],
52+
{"a": 1, "b": [2, 3]},
53+
{"nested": {"deep": {"value": [1, 2, 3]}}},
54+
],
55+
)
56+
def test_orjson_round_trip(value):
57+
assert orjson_loads(orjson_dumps(value)) == value
58+
59+
60+
# --- socket.io kwarg compat (regression test for the bug that broke
61+
# integration tests: socket.io calls dumps(data, separators=(',',':')))
62+
63+
64+
def test_orjson_dumps_socket_accepts_separators_kwarg():
65+
out = orjson_dumps_socket({"a": 1}, separators=(",", ":"))
66+
assert out == '{"a":1}'
67+
68+
69+
def test_orjson_dumps_socket_ignores_arbitrary_kwargs():
70+
out = orjson_dumps_socket([1, 2, 3], separators=(",", ":"), sort_keys=True)
71+
assert orjson_loads(out) == [1, 2, 3]
72+
73+
74+
# --- non-finite float sentinels ---
75+
76+
77+
def test_nan_top_level():
78+
assert orjson_dumps_socket(float("nan")) == f'"{NAN_SENTINEL}"'
79+
80+
81+
def test_inf_top_level():
82+
assert orjson_dumps_socket(float("inf")) == f'"{INF_SENTINEL}"'
83+
84+
85+
def test_neg_inf_top_level():
86+
assert orjson_dumps_socket(float("-inf")) == f'"{NEG_INF_SENTINEL}"'
87+
88+
89+
def test_nan_in_list():
90+
out = orjson_dumps_socket([1.0, float("nan"), 2.0])
91+
assert orjson_loads(out) == [1.0, NAN_SENTINEL, 2.0]
92+
93+
94+
def test_nan_in_dict():
95+
out = orjson_dumps_socket({"x": float("nan"), "y": 1.0})
96+
assert orjson_loads(out) == {"x": NAN_SENTINEL, "y": 1.0}
97+
98+
99+
def test_non_finite_floats_deeply_nested():
100+
out = orjson_dumps_socket({"a": {"b": [{"c": float("nan")}, float("inf")]}})
101+
assert orjson_loads(out) == {"a": {"b": [{"c": NAN_SENTINEL}, INF_SENTINEL]}}
102+
103+
104+
def test_all_three_sentinels():
105+
out = orjson_dumps_socket([float("nan"), float("inf"), float("-inf")])
106+
assert orjson_loads(out) == [NAN_SENTINEL, INF_SENTINEL, NEG_INF_SENTINEL]
107+
108+
109+
def test_nan_inside_dataclass_field():
110+
"""Dataclass fields with NaN must still get the sentinel."""
111+
112+
@dataclasses.dataclass
113+
class Point:
114+
x: float
115+
y: float
116+
117+
out = orjson_dumps_socket({"p": Point(float("nan"), 1.0)})
118+
assert orjson_loads(out) == {"p": {"x": NAN_SENTINEL, "y": 1.0}}
119+
120+
121+
# --- copy-on-write walker behavior ---
122+
123+
124+
def test_walker_returns_unchanged_dict_as_is():
125+
obj = {"a": 1, "b": [1, 2, 3], "c": {"d": "hi"}}
126+
assert _replace_non_finite_floats(obj) is obj
127+
128+
129+
def test_walker_returns_unchanged_list_as_is():
130+
obj = [1, 2.0, "x", {"k": "v"}]
131+
assert _replace_non_finite_floats(obj) is obj
132+
133+
134+
def test_walker_returns_unchanged_tuple_as_is():
135+
obj = (1, 2.0, "x")
136+
assert _replace_non_finite_floats(obj) is obj
137+
138+
139+
def test_walker_preserves_unchanged_subtree_when_sibling_changes():
140+
inner = {"safe": 1.0, "also_safe": [1, 2]}
141+
outer = {"a": inner, "b": float("nan")}
142+
result = _replace_non_finite_floats(outer)
143+
assert result is not outer
144+
assert result["a"] is inner
145+
assert result["b"] == NAN_SENTINEL
146+
147+
148+
def test_walker_converts_modified_tuple_to_list():
149+
t = (1, float("nan"), "x")
150+
assert _replace_non_finite_floats(t) == [1, NAN_SENTINEL, "x"]
151+
152+
153+
def test_walker_passes_through_unknown_types():
154+
class Sentinel:
155+
pass
156+
157+
s = Sentinel()
158+
assert _replace_non_finite_floats(s) is s
159+
160+
161+
# --- format compatibility with the existing json_dumps ---
162+
163+
164+
@pytest.mark.parametrize(
165+
"value",
166+
[
167+
None,
168+
True,
169+
False,
170+
0,
171+
1,
172+
1.5,
173+
"hello",
174+
[1, 2, 3],
175+
{"a": 1, "b": "two"},
176+
{"nested": {"deep": [1, "two", None]}},
177+
datetime.datetime(2026, 4, 25, 10, 30, 45),
178+
datetime.datetime(2026, 4, 25, 10, 30, 45, 123456),
179+
datetime.datetime(2026, 4, 25, 10, 30, 45, tzinfo=datetime.timezone.utc),
180+
datetime.date(2026, 4, 25),
181+
datetime.time(10, 30, 45),
182+
datetime.timedelta(days=1, seconds=1, microseconds=1),
183+
Decimal("3.14"),
184+
UUID("12345678-1234-5678-1234-567812345678"),
185+
Path("/tmp/foo"),
186+
],
187+
)
188+
def test_socket_output_matches_json_dumps_for_finite_inputs(value):
189+
"""For inputs without NaN/Inf, ``orjson_dumps_socket`` must produce
190+
a payload that decodes to the same Python object as ``json_dumps``.
191+
"""
192+
socket_out = orjson_dumps_socket(value)
193+
json_out = json_dumps(value)
194+
assert json.loads(socket_out) == json.loads(json_out)
195+
196+
197+
def test_enum_value_serialized_consistently():
198+
class Color(Enum):
199+
RED = "red"
200+
BLUE = "blue"
201+
202+
assert json.loads(orjson_dumps_socket({"c": Color.RED})) == json.loads(
203+
json_dumps({"c": Color.RED})
204+
)
205+
206+
207+
def test_dict_subclass_style_serializes_equivalently():
208+
"""``Style`` is a dict subclass; both paths must agree on the output."""
209+
style = Style({"color": "red", "size": 12})
210+
assert json.loads(orjson_dumps_socket(style)) == json.loads(json_dumps(style))
211+
212+
213+
def test_datetime_uses_space_separator_not_iso_t():
214+
"""Regression: orjson natively emits 'T'-separated datetimes; we route
215+
them through ``serializers.serialize_datetime`` to keep the existing
216+
space-separated format consumed by the JS side.
217+
"""
218+
dt = datetime.datetime(2026, 4, 25, 10, 30, 45)
219+
out = orjson_dumps_socket({"dt": dt})
220+
assert orjson_loads(out) == {"dt": "2026-04-25 10:30:45"}
221+
222+
223+
# --- non-string dict keys ---
224+
225+
226+
def test_int_dict_keys_coerced_to_strings():
227+
out = orjson_dumps_socket({1: "a", 2: "b"})
228+
assert orjson_loads(out) == {"1": "a", "2": "b"}
229+
230+
231+
# --- unknown-type fallback ---
232+
233+
234+
def test_unknown_type_serializes_to_null():
235+
"""Types without a registered serializer return None from
236+
``serializers.serialize`` and end up as JSON null -- matches stdlib
237+
``json.dumps(default=serialize)`` behavior.
238+
"""
239+
240+
class Unknown:
241+
pass
242+
243+
out = orjson_dumps_socket({"x": Unknown()})
244+
assert orjson_loads(out) == {"x": None}

0 commit comments

Comments
 (0)