Files
2026-07-13 12:24:33 +08:00

109 lines
3.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for lmcache.v1.utils.json_utils.
Covers make_json_safe (recursive sanitization) and safe_asdict
(dataclass -> JSON-safe dict) across primitive, container,
nested, non-serializable, and invalid-input scenarios.
"""
# Standard
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# Third Party
import pytest
# First Party
from lmcache.v1.utils.json_utils import make_json_safe, safe_asdict
# ---------------------------------------------------------------- #
# make_json_safe
# ---------------------------------------------------------------- #
class TestMakeJsonSafe:
@pytest.mark.parametrize(
"value",
["s", 1, 1.5, True, False, None],
)
def test_primitives_unchanged(self, value: Any):
"""JSON-native primitives are returned unchanged."""
assert make_json_safe(value) == value
assert type(make_json_safe(value)) is type(value)
def test_dict_recursively_sanitized(self):
obj = {"a": 1, "b": Path("/tmp/x"), "c": {"d": (1, 2)}}
result = make_json_safe(obj)
assert result == {"a": 1, "b": "/tmp/x", "c": {"d": [1, 2]}}
def test_tuple_becomes_list(self):
assert make_json_safe((1, 2, 3)) == [1, 2, 3]
def test_list_recursively_sanitized(self):
assert make_json_safe([1, Path("/a"), [Path("/b")]]) == [
1,
"/a",
["/b"],
]
def test_non_serializable_falls_back_to_str(self):
class Opaque:
def __str__(self) -> str:
return "opaque-repr"
assert make_json_safe(Opaque()) == "opaque-repr"
def test_nested_mixed_structures(self):
obj = {"items": [{"p": Path("/x")}, (Path("/y"),)]}
assert make_json_safe(obj) == {
"items": [{"p": "/x"}, ["/y"]],
}
# ---------------------------------------------------------------- #
# safe_asdict
# ---------------------------------------------------------------- #
@dataclass
class _FakeCfg:
name: str = "n"
port: int = 8080
path: Path = Path("/tmp/cfg")
nested: dict = field(default_factory=lambda: {"k": Path("/v")})
@dataclass
class _EmptyCfg:
pass
class TestSafeAsdict:
def test_dataclass_converted_with_non_serializable_fields(self):
result = safe_asdict(_FakeCfg())
assert result == {
"name": "n",
"port": 8080,
"path": "/tmp/cfg",
"nested": {"k": "/v"},
}
def test_empty_dataclass(self):
assert safe_asdict(_EmptyCfg()) == {}
def test_non_dataclass_raises(self):
with pytest.raises(TypeError, match="Expected a dataclass"):
safe_asdict({"not": "a dataclass"})
def test_dataclass_class_rejected(self):
"""A dataclass *type* (not instance) must be rejected."""
with pytest.raises(TypeError, match="Expected a dataclass"):
safe_asdict(_FakeCfg)
@pytest.mark.parametrize("bad", [None, 42, "str", [1, 2]])
def test_primitive_inputs_rejected(self, bad: Any):
with pytest.raises(TypeError, match="Expected a dataclass"):
safe_asdict(bad)