"""_json 抽象层测试。 验证 orjson/stdlib 双后端的输出语义一致。由于 orjson 是可选依赖, 本测试在两种环境下均应通过(orjson 已安装或未安装)。 """ from __future__ import annotations import io from pathlib import Path import pytest from pyflowx._json import JSONDecodeError, dump, dumps, load, loads class TestDumpsLoads: """dumps/loads 往返测试。""" def test_dumps_basic_dict(self) -> None: s = dumps({"a": 1, "b": [1, 2, 3]}) assert loads(s) == {"a": 1, "b": [1, 2, 3]} def test_dumps_returns_str(self) -> None: s = dumps({"x": 1}) assert isinstance(s, str) def test_dumps_none(self) -> None: assert dumps(None) == "null" def test_dumps_bool(self) -> None: assert dumps(True) == "true" assert dumps(False) == "false" def test_dumps_int(self) -> None: assert dumps(42) == "42" def test_dumps_float(self) -> None: s = dumps(3.14) assert loads(s) == 3.14 def test_dumps_unicode(self) -> None: """ensure_ascii=False:中文字符不应被转义为 \\uXXXX。""" s = dumps({"msg": "条件不满足"}) assert "条件不满足" in s assert "\\u" not in s def test_dumps_indent(self) -> None: """indent=2 应产生多行格式。""" s = dumps({"a": 1}, indent=2) assert "\n" in s def test_dumps_indent_none_compact(self) -> None: """indent=None 应产生紧凑单行。""" s = dumps({"a": 1}, indent=None) assert "\n" not in s def test_dumps_default_callable(self) -> None: """default 参数应处理不可序列化的类型。""" class Custom: def __str__(self) -> str: return "custom-value" s = dumps({"obj": Custom()}, default=str) assert loads(s) == {"obj": "custom-value"} def test_dumps_non_serialisable_raises(self) -> None: """不可序列化的值(无 default)应抛 TypeError。""" with pytest.raises(TypeError): _ = dumps(object()) def test_loads_bytes(self) -> None: """loads 应接受 bytes 输入。""" assert loads(b'{"x": 1}') == {"x": 1} def test_loads_str(self) -> None: """loads 应接受 str 输入。""" assert loads('{"x": 1}') == {"x": 1} def test_loads_invalid_raises(self) -> None: with pytest.raises(JSONDecodeError): _ = loads("not valid json") class TestDumpLoad: """dump/load 文件句柄往返测试。""" def test_dump_load_round_trip(self, tmp_path: Path) -> None: path = tmp_path / "test.json" data = {"a": 1, "b": [1, 2, 3], "c": "中文"} with open(path, "w", encoding="utf-8") as fh: dump(data, fh, ensure_ascii=False, indent=2) with open(path, encoding="utf-8") as fh: assert load(fh) == data def test_dump_indent(self, tmp_path: Path) -> None: path = tmp_path / "test.json" with open(path, "w", encoding="utf-8") as fh: dump({"a": 1}, fh, indent=2) text = path.read_text(encoding="utf-8") assert "\n" in text def test_dump_compact(self, tmp_path: Path) -> None: path = tmp_path / "test.json" with open(path, "w", encoding="utf-8") as fh: dump({"a": 1}, fh, indent=None) text = path.read_text(encoding="utf-8") assert "\n" not in text def test_dump_default_callable(self, tmp_path: Path) -> None: path = tmp_path / "test.json" with open(path, "w", encoding="utf-8") as fh: dump({"p": Path("/tmp/x")}, fh, default=str) with open(path, encoding="utf-8") as fh: assert load(fh) == {"p": "/tmp/x"} class TestStringIO: """通过 StringIO 验证 dump/load 与 dumps/loads 一致。""" def test_dump_load_stringio(self) -> None: buf = io.StringIO() dump({"a": 1, "b": [1, 2]}, buf, ensure_ascii=False) buf.seek(0) assert load(buf) == {"a": 1, "b": [1, 2]}