blob: 8d4a86816250c6e73d1875ae9993445e486c2544 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for JSON graph serialization/deserialization roundtrips."""
from __future__ import annotations
import json
from typing import Any, Callable
import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi.serialization import from_json_graph_str, to_json_graph_str
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _roundtrip(obj: Any) -> Any:
"""Serialize then deserialize and return the result."""
return from_json_graph_str(to_json_graph_str(obj))
def _assert_roundtrip_eq(obj: Any, cmp: Callable[..., Any] | None = None) -> None:
"""Assert that roundtrip preserves the value."""
result = _roundtrip(obj)
if cmp is not None:
cmp(result)
elif isinstance(obj, float):
assert isinstance(result, float)
assert result == pytest.approx(obj)
elif obj is None:
assert result is None
else:
_assert_any_equal(result, obj)
def _assert_any_equal(a: Any, b: Any) -> None:
"""Recursively compare two tvm_ffi values for equality."""
if isinstance(b, tvm_ffi.Array):
assert len(a) == len(b)
for x, y in zip(a, b):
_assert_any_equal(x, y)
elif isinstance(b, (tvm_ffi.Map, tvm_ffi.Dict)):
assert len(a) == len(b)
for k in b:
_assert_any_equal(a[k], b[k])
elif isinstance(b, tvm_ffi.Shape):
assert list(a) == list(b)
elif isinstance(b, str):
# tvm_ffi String inherits from str
assert str(a) == str(b)
else:
assert a == b
# ---------------------------------------------------------------------------
# Primitive types
# ---------------------------------------------------------------------------
class TestNone:
"""Roundtrip tests for None."""
def test_none(self) -> None:
"""None roundtrips to None."""
assert _roundtrip(None) is None
class TestBool:
"""Roundtrip tests for bool values."""
def test_true(self) -> None:
"""True roundtrips to True."""
assert _roundtrip(True) is True
def test_false(self) -> None:
"""False roundtrips to False."""
assert _roundtrip(False) is False
class TestInt:
"""Roundtrip tests for integer values."""
def test_zero(self) -> None:
"""Zero roundtrips correctly."""
result = _roundtrip(0)
assert result == 0
def test_positive(self) -> None:
"""Positive int roundtrips correctly."""
result = _roundtrip(42)
assert result == 42
def test_negative(self) -> None:
"""Negative int roundtrips correctly."""
result = _roundtrip(-1)
assert result == -1
def test_large_positive(self) -> None:
"""Large positive int roundtrips correctly."""
result = _roundtrip(10**15)
assert result == 10**15
def test_large_negative(self) -> None:
"""Large negative int roundtrips correctly."""
result = _roundtrip(-(10**15))
assert result == -(10**15)
class TestFloat:
"""Roundtrip tests for float values."""
def test_zero(self) -> None:
"""Float zero roundtrips correctly."""
result = _roundtrip(0.0)
assert result == 0.0
def test_positive(self) -> None:
"""Positive float roundtrips correctly."""
result = _roundtrip(3.14159)
assert result == pytest.approx(3.14159)
def test_negative(self) -> None:
"""Negative float roundtrips correctly."""
result = _roundtrip(-2.718)
assert result == pytest.approx(-2.718)
def test_very_small(self) -> None:
"""Very small float roundtrips correctly."""
result = _roundtrip(1e-300)
assert result == pytest.approx(1e-300)
def test_very_large(self) -> None:
"""Very large float roundtrips correctly."""
result = _roundtrip(1e300)
assert result == pytest.approx(1e300)
# ---------------------------------------------------------------------------
# String types
# ---------------------------------------------------------------------------
class TestString:
"""Roundtrip tests for ffi.String values."""
def test_empty(self) -> None:
"""Empty string roundtrips correctly."""
_assert_roundtrip_eq(tvm_ffi.convert(""))
def test_short(self) -> None:
"""Short string roundtrips correctly."""
_assert_roundtrip_eq(tvm_ffi.convert("hello"))
def test_long(self) -> None:
"""Long string roundtrips correctly."""
_assert_roundtrip_eq(tvm_ffi.convert("x" * 1000))
def test_special_chars(self) -> None:
"""String with special characters roundtrips correctly."""
_assert_roundtrip_eq(tvm_ffi.convert('hello\nworld\t"quotes"'))
def test_unicode(self) -> None:
"""Unicode string roundtrips correctly."""
_assert_roundtrip_eq(tvm_ffi.convert("hello 世界"))
# ---------------------------------------------------------------------------
# DataType
# ---------------------------------------------------------------------------
class TestDataType:
"""Roundtrip tests for DLDataType values."""
def test_int32(self) -> None:
"""int32 dtype roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.dtype("int32"))
result = from_json_graph_str(s)
assert str(result) == "int32"
def test_float64(self) -> None:
"""float64 dtype roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.dtype("float64"))
result = from_json_graph_str(s)
assert str(result) == "float64"
def test_bool(self) -> None:
"""Bool dtype roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.dtype("bool"))
result = from_json_graph_str(s)
assert str(result) == "bool"
def test_vector(self) -> None:
"""Vector dtype roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.dtype("float32x4"))
result = from_json_graph_str(s)
assert str(result) == "float32x4"
# ---------------------------------------------------------------------------
# Device
# ---------------------------------------------------------------------------
class TestDevice:
"""Roundtrip tests for DLDevice values."""
def test_cpu(self) -> None:
"""CPU device roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.Device("cpu", 0))
result = from_json_graph_str(s)
assert result.dlpack_device_type() == tvm_ffi.Device("cpu", 0).dlpack_device_type()
assert result.index == 0
def test_cuda(self) -> None:
"""CUDA device roundtrips correctly."""
s = to_json_graph_str(tvm_ffi.Device("cuda", 1))
result = from_json_graph_str(s)
assert result.dlpack_device_type() == tvm_ffi.Device("cuda", 1).dlpack_device_type()
assert result.index == 1
# ---------------------------------------------------------------------------
# Containers
# ---------------------------------------------------------------------------
class TestArray:
"""Roundtrip tests for ffi.Array containers."""
def test_empty(self) -> None:
"""Empty array roundtrips correctly."""
arr = tvm_ffi.convert([])
_assert_roundtrip_eq(arr)
def test_single_element(self) -> None:
"""Single-element array roundtrips correctly."""
arr = tvm_ffi.convert([42])
result = _roundtrip(arr)
assert len(result) == 1
assert result[0] == 42
def test_multiple_elements(self) -> None:
"""Multi-element array roundtrips correctly."""
arr = tvm_ffi.convert([1, 2, 3])
result = _roundtrip(arr)
assert len(result) == 3
assert list(result) == [1, 2, 3]
def test_mixed_types(self) -> None:
"""Array with mixed types roundtrips correctly."""
arr = tvm_ffi.convert([42, "hello", True, None])
result = _roundtrip(arr)
assert len(result) == 4
assert result[0] == 42
assert result[1] == "hello"
assert result[2] is True
assert result[3] is None
def test_nested_arrays(self) -> None:
"""Nested arrays roundtrip correctly."""
inner1 = tvm_ffi.convert([1, 2])
inner2 = tvm_ffi.convert([3])
outer = tvm_ffi.convert([inner1, inner2])
result = _roundtrip(outer)
assert len(result) == 2
assert list(result[0]) == [1, 2]
assert list(result[1]) == [3]
def test_duplicated_elements(self) -> None:
"""Array with duplicated elements roundtrips correctly."""
arr = tvm_ffi.convert([42, 42, 42])
result = _roundtrip(arr)
assert len(result) == 3
assert all(x == 42 for x in result)
class TestMap:
"""Roundtrip tests for ffi.Map containers."""
def test_empty(self) -> None:
"""Empty map roundtrips correctly."""
m = tvm_ffi.convert({})
_assert_roundtrip_eq(m)
def test_single_entry(self) -> None:
"""Single-entry map roundtrips correctly."""
m = tvm_ffi.convert({"key": 42})
result = _roundtrip(m)
assert len(result) == 1
assert result["key"] == 42
def test_multiple_entries(self) -> None:
"""Multi-entry map roundtrips correctly."""
m = tvm_ffi.convert({"a": 1, "b": 2, "c": 3})
result = _roundtrip(m)
assert len(result) == 3
assert result["a"] == 1
assert result["b"] == 2
assert result["c"] == 3
def test_mixed_value_types(self) -> None:
"""Map with mixed value types roundtrips correctly."""
m = tvm_ffi.convert({"int": 42, "str": "hello", "bool": True, "none": None})
result = _roundtrip(m)
assert result["int"] == 42
assert result["str"] == "hello"
assert result["bool"] is True
assert result["none"] is None
def test_nested_map(self) -> None:
"""Nested maps roundtrip correctly."""
inner = tvm_ffi.convert({"x": 1})
outer = tvm_ffi.convert({"inner": inner})
result = _roundtrip(outer)
assert result["inner"]["x"] == 1
def test_map_with_array_value(self) -> None:
"""Map with array values roundtrips correctly."""
arr = tvm_ffi.convert([10, 20])
m = tvm_ffi.convert({"nums": arr})
result = _roundtrip(m)
assert list(result["nums"]) == [10, 20]
class TestDict:
"""Roundtrip tests for ffi.Dict containers."""
def test_empty(self) -> None:
"""Empty dict roundtrips correctly."""
d = tvm_ffi.Dict({})
_assert_roundtrip_eq(d)
def test_single_entry(self) -> None:
"""Single-entry dict roundtrips correctly."""
d = tvm_ffi.Dict({"key": 42})
result = _roundtrip(d)
assert len(result) == 1
assert result["key"] == 42
def test_multiple_entries(self) -> None:
"""Multi-entry dict roundtrips correctly."""
d = tvm_ffi.Dict({"a": 1, "b": 2, "c": 3})
result = _roundtrip(d)
assert len(result) == 3
assert result["a"] == 1
assert result["b"] == 2
assert result["c"] == 3
def test_mixed_value_types(self) -> None:
"""Dict with mixed value types roundtrips correctly."""
d = tvm_ffi.Dict({"int": 42, "str": "hello", "bool": True, "none": None})
result = _roundtrip(d)
assert result["int"] == 42
assert result["str"] == "hello"
assert result["bool"] is True
assert result["none"] is None
def test_nested_dict(self) -> None:
"""Nested dicts roundtrip correctly."""
inner = tvm_ffi.Dict({"x": 1})
outer = tvm_ffi.Dict({"inner": inner})
result = _roundtrip(outer)
assert result["inner"]["x"] == 1
def test_dict_with_array_value(self) -> None:
"""Dict with array values roundtrips correctly."""
arr = tvm_ffi.convert([10, 20])
d = tvm_ffi.Dict({"nums": arr})
result = _roundtrip(d)
assert list(result["nums"]) == [10, 20]
class TestShape:
"""Roundtrip tests for ffi.Shape containers."""
def test_empty(self) -> None:
"""Empty shape roundtrips correctly."""
shape = tvm_ffi.Shape(())
_assert_roundtrip_eq(shape)
def test_1d(self) -> None:
"""1D shape roundtrips correctly."""
shape = tvm_ffi.Shape((10,))
result = _roundtrip(shape)
assert list(result) == [10]
def test_nd(self) -> None:
"""N-D shape roundtrips correctly."""
shape = tvm_ffi.Shape((1, 2, 3, 4))
result = _roundtrip(shape)
assert list(result) == [1, 2, 3, 4]
# ---------------------------------------------------------------------------
# Objects with reflection
# ---------------------------------------------------------------------------
class TestObjectSerialization:
"""Roundtrip tests for objects with reflection metadata."""
def test_int_pair_roundtrip(self) -> None:
"""TestIntPair has refl::init and POD int64 fields."""
pair = tvm_ffi.testing.TestIntPair(3, 7)
s = to_json_graph_str(pair)
result = from_json_graph_str(s)
assert result.a == 3
assert result.b == 7
def test_int_pair_zero_values(self) -> None:
"""TestIntPair with zero values roundtrips correctly."""
pair = tvm_ffi.testing.TestIntPair(0, 0)
result = _roundtrip(pair)
assert result.a == 0
assert result.b == 0
def test_int_pair_negative_values(self) -> None:
"""TestIntPair with negative values roundtrips correctly."""
pair = tvm_ffi.testing.TestIntPair(-100, -200)
result = _roundtrip(pair)
assert result.a == -100
assert result.b == -200
def test_int_pair_large_values(self) -> None:
"""TestIntPair with large values roundtrips correctly."""
pair = tvm_ffi.testing.TestIntPair(10**15, -(10**15))
result = _roundtrip(pair)
assert result.a == 10**15
assert result.b == -(10**15)
# ---------------------------------------------------------------------------
# JSON structure verification
# ---------------------------------------------------------------------------
class TestJSONStructure:
"""Tests verifying the internal JSON graph structure."""
def test_null_json_structure(self) -> None:
"""None produces a single node with type 'None'."""
s = to_json_graph_str(None)
parsed = json.loads(s)
assert parsed["root_index"] == 0
assert len(parsed["nodes"]) == 1
assert parsed["nodes"][0]["type"] == "None"
def test_bool_json_structure(self) -> None:
"""Bool produces a node with type 'bool'."""
s = to_json_graph_str(True)
parsed = json.loads(s)
assert parsed["nodes"][0]["type"] == "bool"
assert parsed["nodes"][0]["data"] is True
def test_int_json_structure(self) -> None:
"""Int produces a node with type 'int'."""
s = to_json_graph_str(42)
parsed = json.loads(s)
assert parsed["nodes"][0]["type"] == "int"
assert parsed["nodes"][0]["data"] == 42
def test_float_json_structure(self) -> None:
"""Float produces a node with type 'float'."""
s = to_json_graph_str(3.14)
parsed = json.loads(s)
assert parsed["nodes"][0]["type"] == "float"
assert parsed["nodes"][0]["data"] == pytest.approx(3.14)
def test_string_json_structure(self) -> None:
"""String produces a node with type 'ffi.String'."""
s = to_json_graph_str(tvm_ffi.convert("hello"))
parsed = json.loads(s)
assert parsed["nodes"][parsed["root_index"]]["type"] == "ffi.String"
assert parsed["nodes"][parsed["root_index"]]["data"] == "hello"
def test_array_json_structure(self) -> None:
"""Array data contains node index references."""
s = to_json_graph_str(tvm_ffi.convert([1, 2]))
parsed = json.loads(s)
root = parsed["nodes"][parsed["root_index"]]
assert root["type"] == "ffi.Array"
# data should be list of node indices
assert isinstance(root["data"], list)
assert len(root["data"]) == 2
def test_map_json_structure(self) -> None:
"""Map data contains flattened key-value node index pairs."""
s = to_json_graph_str(tvm_ffi.convert({"a": 1}))
parsed = json.loads(s)
root = parsed["nodes"][parsed["root_index"]]
assert root["type"] == "ffi.Map"
assert isinstance(root["data"], list)
# key-value pairs flattened: [key_idx, val_idx]
assert len(root["data"]) == 2
def test_dict_json_structure(self) -> None:
"""Dict data contains flattened key-value node index pairs."""
s = to_json_graph_str(tvm_ffi.Dict({"a": 1}))
parsed = json.loads(s)
root = parsed["nodes"][parsed["root_index"]]
assert root["type"] == "ffi.Dict"
assert isinstance(root["data"], list)
# key-value pairs flattened: [key_idx, val_idx]
assert len(root["data"]) == 2
def test_node_dedup(self) -> None:
"""Duplicate values should share the same node index."""
s = to_json_graph_str(tvm_ffi.convert([42, 42, 42]))
parsed = json.loads(s)
root = parsed["nodes"][parsed["root_index"]]
# all three elements should reference the same node
assert root["data"][0] == root["data"][1] == root["data"][2]
def test_object_pod_fields_are_inlined(self) -> None:
"""POD fields (int, bool, float) are inlined directly via field_static_type_index."""
pair = tvm_ffi.testing.TestIntPair(3, 7)
s = to_json_graph_str(pair)
parsed = json.loads(s)
root = parsed["nodes"][parsed["root_index"]]
assert root["type"] == "testing.TestIntPair"
# fields 'a' and 'b' should be inlined as direct int values (not node indices)
assert root["data"]["a"] == 3
assert root["data"]["b"] == 7
# ---------------------------------------------------------------------------
# Metadata
# ---------------------------------------------------------------------------
class TestMetadata:
"""Tests for optional metadata in serialized output."""
def test_with_metadata(self) -> None:
"""Metadata dict appears in serialized JSON when provided."""
s = to_json_graph_str(42, {"version": "1.0"})
parsed = json.loads(s)
assert "metadata" in parsed
assert parsed["metadata"]["version"] == "1.0"
def test_without_metadata(self) -> None:
"""Metadata key is absent when not provided."""
s = to_json_graph_str(42)
parsed = json.loads(s)
assert "metadata" not in parsed
# ---------------------------------------------------------------------------
# Error cases
# ---------------------------------------------------------------------------
class TestErrors:
"""Tests for error handling in deserialization."""
def test_invalid_json_string(self) -> None:
"""Invalid JSON string raises an error."""
with pytest.raises(Exception):
from_json_graph_str("not valid json")
def test_empty_json_string(self) -> None:
"""Empty string raises an error."""
with pytest.raises(Exception):
from_json_graph_str("")
def test_missing_root_index(self) -> None:
"""JSON without root_index raises an error."""
with pytest.raises(Exception):
from_json_graph_str('{"nodes": []}')
def test_missing_nodes(self) -> None:
"""JSON without nodes raises an error."""
with pytest.raises(Exception):
from_json_graph_str('{"root_index": 0}')