blob: b1b49a4bd2f4855b7c7f68aa685bebddbc182b25 [file] [log] [blame]
# 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.
"""
Cross-language xlang tests for Python <-> Java/Rust/Go/etc.
This module is invoked by PythonXlangTest.java and other language xlang tests.
The test cases follow the same pattern as test_cross_language.rs (Rust).
Data file path is passed via DATA_FILE environment variable.
"""
import enum
import logging
import os
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set
import pyfory
from pyfory.meta.meta_compressor import NoOpMetaCompressor
def debug_print(*params):
"""Print params if debug is needed."""
if os.environ.get("ENABLE_FORY_DEBUG_OUTPUT") == "1":
print(*params)
def get_data_file() -> str:
"""Get the data file path from environment variable."""
return os.environ["DATA_FILE"]
# ============================================================================
# Test Data Classes - Must match XlangTestBase.java definitions
# ============================================================================
class Color(enum.Enum):
Green = 0
Red = 1
Blue = 2
White = 3
@dataclass
class Item:
name: str = ""
@dataclass
class SimpleStruct:
f1: Dict[pyfory.int32, pyfory.float64] = None
f2: pyfory.int32 = 0
f3: Item = None
f4: str = ""
f5: Color = None
f6: List[str] = None
f7: pyfory.int32 = 0
f8: pyfory.int32 = 0
last: pyfory.int32 = 0
@dataclass
class VersionCheckStruct:
f1: pyfory.int32 = 0
f2: Optional[str] = None
f3: pyfory.float64 = 0.0
@dataclass
class Dog:
age: pyfory.int32 = 0
name: Optional[str] = None
@dataclass
class Cat:
age: pyfory.int32 = 0
lives: pyfory.int32 = 0
@dataclass
class AnimalListHolder:
animals: List[Any] = None
@dataclass
class AnimalMapHolder:
animal_map: Dict[Optional[str], Any] = None
@dataclass
class MyStruct:
id: pyfory.int32 = 0
@dataclass
class MyExt:
id: pyfory.int32 = 0
class MyExtSerializer(pyfory.serializer.Serializer):
def write(self, buffer, value):
self.xwrite(buffer, value)
def read(self, buffer):
return self.xread(buffer)
def xwrite(self, buffer, value):
buffer.write_varint32(value.id)
def xread(self, buffer):
obj = MyExt()
obj.id = buffer.read_varint32()
return obj
@dataclass
class MyWrapper:
color: Color = None
my_struct: MyStruct = None
my_ext: MyExt = None
@dataclass
class EmptyWrapper:
pass
@dataclass
class EmptyStruct:
pass
@dataclass
class OneStringFieldStruct:
f1: Optional[str] = None
@dataclass
class TwoStringFieldStruct:
f1: str = ""
f2: str = ""
class TestEnum(enum.Enum):
VALUE_A = 0
VALUE_B = 1
VALUE_C = 2
@dataclass
class OneEnumFieldStruct:
f1: TestEnum = None
@dataclass
class TwoEnumFieldStruct:
f1: TestEnum = None
f2: TestEnum = None
# ============================================================================
# Nullable Field Test Types
# ============================================================================
@dataclass
class NullableComprehensiveSchemaConsistent:
"""
Comprehensive struct for testing nullable fields in SCHEMA_CONSISTENT mode.
Fields are organized as:
- Base non-nullable fields: byte, short, int, long, float, double, bool, string, list, set, map
- Nullable fields (first half - boxed numeric types): Integer, Long, Float
- Nullable fields (second half): Double, Boolean, String, List, Set, Map
"""
# Base non-nullable primitive fields
byte_field: pyfory.int8 = 0
short_field: pyfory.int16 = 0
int_field: pyfory.int32 = 0
long_field: pyfory.int64 = 0
float_field: pyfory.float32 = 0.0
double_field: pyfory.float64 = 0.0
bool_field: bool = False
# Base non-nullable reference fields
string_field: str = ""
list_field: List[str] = None
set_field: Set[str] = None
map_field: Dict[str, str] = None
# Nullable fields - first half (boxed types)
nullable_int: Optional[pyfory.int32] = None
nullable_long: Optional[pyfory.int64] = None
nullable_float: Optional[pyfory.float32] = None
# Nullable fields - second half
nullable_double: Optional[pyfory.float64] = None
nullable_bool: Optional[bool] = None
nullable_string: Optional[str] = None
nullable_list: Optional[List[str]] = None
nullable_set: Optional[Set[str]] = None
nullable_map: Optional[Dict[str, str]] = None
# ============================================================================
# Reference Tracking Test Types
# ============================================================================
@dataclass
class RefInnerSchemaConsistent:
"""Inner struct for reference tracking test (SCHEMA_CONSISTENT mode)."""
id: pyfory.int32 = 0
name: str = ""
@dataclass
class RefOuterSchemaConsistent:
"""Outer struct with two fields pointing to the same inner object (SCHEMA_CONSISTENT mode)."""
inner1: Optional[RefInnerSchemaConsistent] = pyfory.field(default=None, ref=True, nullable=True)
inner2: Optional[RefInnerSchemaConsistent] = pyfory.field(default=None, ref=True, nullable=True)
@dataclass
class RefInnerCompatible:
"""Inner struct for reference tracking test (COMPATIBLE mode)."""
id: pyfory.int32 = 0
name: str = ""
@dataclass
class RefOuterCompatible:
"""Outer struct with two fields pointing to the same inner object (COMPATIBLE mode)."""
inner1: Optional[RefInnerCompatible] = pyfory.field(default=None, ref=True, nullable=True)
inner2: Optional[RefInnerCompatible] = pyfory.field(default=None, ref=True, nullable=True)
@dataclass
class NullableComprehensiveCompatible:
"""
Cross-language schema evolution test struct for COMPATIBLE mode.
All fields are Optional in Python to properly handle both null and non-null values from Java:
- Group 1: Non-nullable in Java (always has values)
- Group 2: Nullable in Java (@ForyField(nullable=true)) - can be null
Python uses Optional for all fields so it can correctly receive and re-serialize
values from Java, whether they are null or non-null.
"""
# Group 1: Nullable in Python (Optional), Non-nullable in Java
# Primitive fields
byte_field: Optional[pyfory.int8] = None
short_field: Optional[pyfory.int16] = None
int_field: Optional[pyfory.int32] = None
long_field: Optional[pyfory.int64] = None
float_field: Optional[pyfory.float32] = None
double_field: Optional[pyfory.float64] = None
bool_field: Optional[bool] = None
# Boxed fields - also nullable in Python
boxed_int: Optional[pyfory.int32] = None
boxed_long: Optional[pyfory.int64] = None
boxed_float: Optional[pyfory.float32] = None
boxed_double: Optional[pyfory.float64] = None
boxed_bool: Optional[bool] = None
# Reference fields - also nullable in Python
string_field: Optional[str] = None
list_field: Optional[List[str]] = None
set_field: Optional[Set[str]] = None
map_field: Optional[Dict[str, str]] = None
# Group 2: Also Nullable in Python (must match Java's nullable annotation)
# When Java sends null for these fields, Python must be able to receive and re-serialize None.
# Boxed types - use Optional to handle None from Java
nullable_int1: Optional[pyfory.int32] = None
nullable_long1: Optional[pyfory.int64] = None
nullable_float1: Optional[pyfory.float32] = None
nullable_double1: Optional[pyfory.float64] = None
nullable_bool1: Optional[bool] = None
# Reference types - also Optional
nullable_string2: Optional[str] = None
nullable_list2: Optional[List[str]] = None
nullable_set2: Optional[Set[str]] = None
nullable_map2: Optional[Dict[str, str]] = None
# ============================================================================
# Test Functions - Each function handles read -> verify -> write back
# ============================================================================
def test_string_serializer():
"""Test string serialization with various encodings."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
buffer = pyfory.Buffer(data_bytes)
fory = pyfory.Fory(xlang=True, compatible=True)
test_strings = [
# Latin1
"ab",
"Rust123",
"Çüéâäàåçêëèïî",
# UTF16
"こんにちは",
"Привет",
"𝄞🎵🎶",
# UTF8
"Hello, 世界",
]
for expected in test_strings:
value = fory.deserialize(buffer)
assert value == expected, f"string mismatch: {value} != {expected}"
# Write strings back
new_buffer = pyfory.Buffer.allocate(512)
for s in test_strings:
fory.serialize(s, buffer=new_buffer)
with open(data_file, "wb") as f:
f.write(new_buffer.get_bytes(0, new_buffer.writer_index))
def test_simple_struct():
"""Test simple struct serialization."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(Color, type_id=101)
fory.register_type(Item, type_id=102)
fory.register_type(SimpleStruct, type_id=103)
expected = SimpleStruct(
f1={1: 1.0, 2: 2.0},
f2=39,
f3=Item(name="item"),
f4="f4",
f5=Color.White,
f6=["f6"],
f7=40,
f8=41,
last=42,
)
debug_print(f"Java bytes length: {len(data_bytes)}")
debug_print(f"Java bytes (first 50): {data_bytes[:50].hex()}")
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
debug_print(f"Python bytes length: {len(new_bytes)}")
debug_print(f"Python bytes (first 50): {new_bytes[:50].hex()}")
debug_print(f"Bytes match: {data_bytes == new_bytes}")
new_value = fory.deserialize(new_bytes)
assert new_value == expected, f"new_value: {new_value},\n expected: {expected}"
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_named_simple_struct():
"""Test named simple struct serialization."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(Color, namespace="demo", typename="color")
fory.register_type(Item, namespace="demo", typename="item")
fory.register_type(SimpleStruct, namespace="demo", typename="simple_struct")
expected = SimpleStruct(
f1={1: 1.0, 2: 2.0},
f2=39,
f3=Item(name="item"),
f4="f4",
f5=Color.White,
f6=["f6"],
f7=40,
f8=41,
last=42,
)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
assert fory.deserialize(new_bytes) == expected
with open(data_file, "wb") as f:
f.write(new_bytes)
def _test_skip_custom(fory1, fory2):
"""Helper for skip custom type tests."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
obj = fory1.deserialize(data_bytes)
assert obj == EmptyWrapper(), f"Expected EmptyWrapper, got {obj}"
wrapper = MyWrapper(color=Color.White, my_struct=MyStruct(id=42), my_ext=MyExt(id=43))
new_bytes = fory2.serialize(wrapper)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_skip_id_custom():
"""Test skipping custom types registered by ID."""
fory1 = pyfory.Fory(xlang=True, compatible=True)
fory1.register_type(MyExt, type_id=103, serializer=MyExtSerializer(fory1, MyExt))
fory1.register_type(EmptyWrapper, type_id=104)
fory2 = pyfory.Fory(xlang=True, compatible=True)
fory2.register_type(Color, type_id=101)
fory2.register_type(MyStruct, type_id=102)
fory2.register_type(MyExt, type_id=103, serializer=MyExtSerializer(fory2, MyExt))
fory2.register_type(MyWrapper, type_id=104)
_test_skip_custom(fory1, fory2)
def test_skip_name_custom():
"""Test skipping custom types registered by name."""
fory1 = pyfory.Fory(xlang=True, compatible=True)
fory1.register_type(MyExt, typename="my_ext", serializer=MyExtSerializer(fory1, MyExt))
fory1.register_type(EmptyWrapper, typename="my_wrapper")
fory2 = pyfory.Fory(xlang=True, compatible=True)
fory2.register_type(Color, typename="color")
fory2.register_type(MyStruct, typename="my_struct")
fory2.register_type(MyExt, typename="my_ext", serializer=MyExtSerializer(fory2, MyExt))
fory2.register_type(MyWrapper, typename="my_wrapper")
_test_skip_custom(fory1, fory2)
def test_consistent_named():
"""Test consistent mode with named types."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
buffer = pyfory.Buffer(data_bytes)
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(Color, typename="color")
fory.register_type(MyStruct, typename="my_struct")
fory.register_type(MyExt, typename="my_ext", serializer=MyExtSerializer(fory, MyExt))
color = Color.White
my_struct = MyStruct(id=42)
my_ext = MyExt(id=43)
for _ in range(3):
assert fory.deserialize(buffer) == color
for _ in range(3):
assert fory.deserialize(buffer) == my_struct
for _ in range(3):
assert fory.deserialize(buffer) == my_ext
new_buffer = pyfory.Buffer.allocate(256)
for _ in range(3):
fory.serialize(color, buffer=new_buffer)
for _ in range(3):
fory.serialize(my_struct, buffer=new_buffer)
for _ in range(3):
fory.serialize(my_ext, buffer=new_buffer)
with open(data_file, "wb") as f:
f.write(new_buffer.get_bytes(0, new_buffer.writer_index))
def test_struct_version_check():
"""Test struct version check."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(VersionCheckStruct, type_id=201)
expected = VersionCheckStruct(f1=10, f2="test", f3=3.2)
obj = fory.deserialize(data_bytes)
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
assert fory.deserialize(new_bytes) == expected
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_polymorphic_list():
"""Test polymorphic list serialization."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
buffer = pyfory.Buffer(data_bytes)
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(Dog, type_id=302)
fory.register_type(Cat, type_id=303)
fory.register_type(AnimalListHolder, type_id=304)
# Part 1: Read List<Animal> with polymorphic elements
animals = fory.deserialize(buffer)
assert len(animals) == 2
dog = animals[0]
assert isinstance(dog, Dog)
assert dog.age == 3
assert dog.name == "Buddy"
cat = animals[1]
assert isinstance(cat, Cat)
assert cat.age == 5
assert cat.lives == 9
# Part 2: Read AnimalListHolder
holder = fory.deserialize(buffer)
assert len(holder.animals) == 2
dog2 = holder.animals[0]
assert isinstance(dog2, Dog)
assert dog2.name == "Rex"
cat2 = holder.animals[1]
assert isinstance(cat2, Cat)
assert cat2.lives == 7
# Write back
new_buffer = pyfory.Buffer.allocate(256)
fory.serialize(animals, buffer=new_buffer)
fory.serialize(holder, buffer=new_buffer)
with open(data_file, "wb") as f:
f.write(new_buffer.get_bytes(0, new_buffer.writer_index))
def test_polymorphic_map():
"""Test polymorphic map serialization."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
buffer = pyfory.Buffer(data_bytes)
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(Dog, type_id=302)
fory.register_type(Cat, type_id=303)
fory.register_type(AnimalMapHolder, type_id=305)
# Part 1: Read Map<String, Animal> with polymorphic values
animal_map = fory.deserialize(buffer)
assert len(animal_map) == 2
dog1 = animal_map.get("dog1")
assert isinstance(dog1, Dog)
assert dog1.name == "Rex"
assert dog1.age == 2
cat1 = animal_map.get("cat1")
assert isinstance(cat1, Cat)
assert cat1.lives == 9
assert cat1.age == 4
# Part 2: Read AnimalMapHolder
holder = fory.deserialize(buffer)
assert len(holder.animal_map) == 2
my_dog = holder.animal_map.get("myDog")
assert isinstance(my_dog, Dog)
assert my_dog.name == "Fido"
my_cat = holder.animal_map.get("myCat")
assert isinstance(my_cat, Cat)
assert my_cat.lives == 8
# Write back
new_buffer = pyfory.Buffer.allocate(256)
fory.serialize(animal_map, buffer=new_buffer)
fory.serialize(holder, buffer=new_buffer)
with open(data_file, "wb") as f:
f.write(new_buffer.get_bytes(0, new_buffer.writer_index))
def test_one_string_field_schema():
"""Test one string field struct with schema consistent mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(OneStringFieldStruct, type_id=200)
expected = OneStringFieldStruct(f1="hello")
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_one_string_field_compatible():
"""Test one string field struct with compatible mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(OneStringFieldStruct, type_id=200)
expected = OneStringFieldStruct(f1="hello")
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_two_string_field_compatible():
"""Test two string field struct with compatible mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TwoStringFieldStruct, type_id=201)
expected = TwoStringFieldStruct(f1="first", f2="second")
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_schema_evolution_compatible():
"""Test schema evolution: deserialize TwoStringFieldStruct as EmptyStruct."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Deserialize TwoStringFieldStruct as EmptyStruct (should skip all fields)
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(EmptyStruct, type_id=200)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized as EmptyStruct: {obj}")
assert isinstance(obj, EmptyStruct), f"Expected EmptyStruct, got {type(obj)}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_schema_evolution_compatible_reverse():
"""Test schema evolution: deserialize OneStringFieldStruct as TwoStringFieldStruct."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Deserialize OneStringFieldStruct as TwoStringFieldStruct
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TwoStringFieldStruct, type_id=200)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized as TwoStringFieldStruct: {obj}")
assert isinstance(obj, TwoStringFieldStruct), f"Expected TwoStringFieldStruct, got {type(obj)}"
assert obj.f1 == "only_one", f"Expected f1='only_one', got f1='{obj.f1}'"
# f2 should be None (missing field)
assert obj.f2 is None or obj.f2 == "", f"Expected f2=None or empty, got f2='{obj.f2}'"
# Set f2 to empty string for serialization (match Go behavior)
if obj.f2 is None:
obj.f2 = ""
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_one_enum_field_schema():
"""Test one enum field struct with schema consistent mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(TestEnum, type_id=210)
fory.register_type(OneEnumFieldStruct, type_id=211)
expected = OneEnumFieldStruct(f1=TestEnum.VALUE_B)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_one_enum_field_compatible():
"""Test one enum field struct with compatible mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TestEnum, type_id=210)
fory.register_type(OneEnumFieldStruct, type_id=211)
expected = OneEnumFieldStruct(f1=TestEnum.VALUE_A)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_two_enum_field_compatible():
"""Test two enum field struct with compatible mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TestEnum, type_id=210)
fory.register_type(TwoEnumFieldStruct, type_id=212)
expected = TwoEnumFieldStruct(f1=TestEnum.VALUE_A, f2=TestEnum.VALUE_C)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
assert obj == expected, f"Mismatch: {obj} != {expected}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_enum_schema_evolution_compatible():
"""Test enum schema evolution: deserialize TwoEnumFieldStruct as EmptyStruct."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Deserialize TwoEnumFieldStruct as EmptyStruct (should skip all fields)
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TestEnum, type_id=210)
fory.register_type(EmptyStruct, type_id=211)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized as EmptyStruct: {obj}")
assert isinstance(obj, EmptyStruct), f"Expected EmptyStruct, got {type(obj)}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_enum_schema_evolution_compatible_reverse():
"""Test enum schema evolution: deserialize OneEnumFieldStruct as TwoEnumFieldStruct."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Deserialize OneEnumFieldStruct as TwoEnumFieldStruct
fory = pyfory.Fory(xlang=True, compatible=True)
fory.register_type(TestEnum, type_id=210)
fory.register_type(TwoEnumFieldStruct, type_id=211)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized as TwoEnumFieldStruct: {obj}")
assert isinstance(obj, TwoEnumFieldStruct), f"Expected TwoEnumFieldStruct, got {type(obj)}"
assert obj.f1 == TestEnum.VALUE_C, f"Expected f1=VALUE_C, got f1={obj.f1}"
# f2 should be None (missing field due to schema evolution)
f2_value = getattr(obj, "f2", None)
assert f2_value is None, f"Expected f2=None, got f2={f2_value}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
# ============================================================================
# Nullable Field Tests
# ============================================================================
def test_nullable_field_schema_consistent_not_null():
"""Test nullable fields with non-null values in schema consistent mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(NullableComprehensiveSchemaConsistent, type_id=401)
expected = NullableComprehensiveSchemaConsistent(
# Base non-nullable primitive fields
byte_field=1,
short_field=2,
int_field=42,
long_field=123456789,
float_field=1.5,
double_field=2.5,
bool_field=True,
# Base non-nullable reference fields
string_field="hello",
list_field=["a", "b", "c"],
set_field={"x", "y"},
map_field={"key1": "value1", "key2": "value2"},
# Nullable fields - first half (boxed types) - all have values
nullable_int=100,
nullable_long=200,
nullable_float=1.5,
# Nullable fields - second half - all have values
nullable_double=2.5,
nullable_bool=False,
nullable_string="nullable_value",
nullable_list=["p", "q"],
nullable_set={"m", "n"},
nullable_map={"nk1": "nv1"},
)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify base primitive fields
assert obj.byte_field == expected.byte_field, f"byte_field: {obj.byte_field} != {expected.byte_field}"
assert obj.short_field == expected.short_field, f"short_field: {obj.short_field} != {expected.short_field}"
assert obj.int_field == expected.int_field, f"int_field: {obj.int_field} != {expected.int_field}"
assert obj.long_field == expected.long_field, f"long_field: {obj.long_field} != {expected.long_field}"
assert abs(obj.float_field - expected.float_field) < 0.01, f"float_field: {obj.float_field} != {expected.float_field}"
assert abs(obj.double_field - expected.double_field) < 0.000001, f"double_field: {obj.double_field} != {expected.double_field}"
assert obj.bool_field == expected.bool_field, f"bool_field: {obj.bool_field} != {expected.bool_field}"
# Verify base reference fields
assert obj.string_field == expected.string_field, f"string_field: {obj.string_field} != {expected.string_field}"
assert obj.list_field == expected.list_field, f"list_field: {obj.list_field} != {expected.list_field}"
assert obj.set_field == expected.set_field, f"set_field: {obj.set_field} != {expected.set_field}"
assert obj.map_field == expected.map_field, f"map_field: {obj.map_field} != {expected.map_field}"
# Verify nullable fields - first half (boxed types)
assert obj.nullable_int == expected.nullable_int, f"nullable_int: {obj.nullable_int} != {expected.nullable_int}"
assert obj.nullable_long == expected.nullable_long, f"nullable_long: {obj.nullable_long} != {expected.nullable_long}"
assert abs(obj.nullable_float - expected.nullable_float) < 0.01, f"nullable_float: {obj.nullable_float} != {expected.nullable_float}"
# Verify nullable fields - second half
assert abs(obj.nullable_double - expected.nullable_double) < 0.01, f"nullable_double: {obj.nullable_double} != {expected.nullable_double}"
assert obj.nullable_bool == expected.nullable_bool, f"nullable_bool: {obj.nullable_bool} != {expected.nullable_bool}"
assert obj.nullable_string == expected.nullable_string, f"nullable_string: {obj.nullable_string} != {expected.nullable_string}"
assert obj.nullable_list == expected.nullable_list, f"nullable_list: {obj.nullable_list} != {expected.nullable_list}"
assert obj.nullable_set == expected.nullable_set, f"nullable_set: {obj.nullable_set} != {expected.nullable_set}"
assert obj.nullable_map == expected.nullable_map, f"nullable_map: {obj.nullable_map} != {expected.nullable_map}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_nullable_field_schema_consistent_null():
"""Test nullable fields with null values in schema consistent mode."""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False)
fory.register_type(NullableComprehensiveSchemaConsistent, type_id=401)
expected = NullableComprehensiveSchemaConsistent(
# Base non-nullable primitive fields - must have values
byte_field=1,
short_field=2,
int_field=42,
long_field=123456789,
float_field=1.5,
double_field=2.5,
bool_field=True,
# Base non-nullable reference fields - must have values
string_field="hello",
list_field=["a", "b", "c"],
set_field={"x", "y"},
map_field={"key1": "value1", "key2": "value2"},
# Nullable fields - first half (boxed types) - all null
nullable_int=None,
nullable_long=None,
nullable_float=None,
# Nullable fields - second half - all null
nullable_double=None,
nullable_bool=None,
nullable_string=None,
nullable_list=None,
nullable_set=None,
nullable_map=None,
)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify base primitive fields
assert obj.byte_field == expected.byte_field, f"byte_field: {obj.byte_field} != {expected.byte_field}"
assert obj.short_field == expected.short_field, f"short_field: {obj.short_field} != {expected.short_field}"
assert obj.int_field == expected.int_field, f"int_field: {obj.int_field} != {expected.int_field}"
assert obj.long_field == expected.long_field, f"long_field: {obj.long_field} != {expected.long_field}"
assert abs(obj.float_field - expected.float_field) < 0.01, f"float_field: {obj.float_field} != {expected.float_field}"
assert abs(obj.double_field - expected.double_field) < 0.000001, f"double_field: {obj.double_field} != {expected.double_field}"
assert obj.bool_field == expected.bool_field, f"bool_field: {obj.bool_field} != {expected.bool_field}"
# Verify base reference fields
assert obj.string_field == expected.string_field, f"string_field: {obj.string_field} != {expected.string_field}"
assert obj.list_field == expected.list_field, f"list_field: {obj.list_field} != {expected.list_field}"
assert obj.set_field == expected.set_field, f"set_field: {obj.set_field} != {expected.set_field}"
assert obj.map_field == expected.map_field, f"map_field: {obj.map_field} != {expected.map_field}"
# Verify nullable fields - first half (boxed types) - all null
assert obj.nullable_int is None, f"nullable_int: {obj.nullable_int} != None"
assert obj.nullable_long is None, f"nullable_long: {obj.nullable_long} != None"
assert obj.nullable_float is None, f"nullable_float: {obj.nullable_float} != None"
# Verify nullable fields - second half - all null
assert obj.nullable_double is None, f"nullable_double: {obj.nullable_double} != None"
assert obj.nullable_bool is None, f"nullable_bool: {obj.nullable_bool} != None"
assert obj.nullable_string is None, f"nullable_string: {obj.nullable_string} != None"
assert obj.nullable_list is None, f"nullable_list: {obj.nullable_list} != None"
assert obj.nullable_set is None, f"nullable_set: {obj.nullable_set} != None"
assert obj.nullable_map is None, f"nullable_map: {obj.nullable_map} != None"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_nullable_field_compatible_not_null():
"""
Test cross-language schema evolution - all fields have values.
Java sends: Group 1 (non-nullable) + Group 2 (nullable with values)
Python reads: Group 1 (nullable/Optional) + Group 2 (non-nullable)
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Use NoOpMetaCompressor to match Java's test configuration
fory = pyfory.Fory(xlang=True, compatible=True, meta_compressor=NoOpMetaCompressor())
fory.register_type(NullableComprehensiveCompatible, type_id=402)
expected = NullableComprehensiveCompatible(
# Group 1: Nullable in Python (read from Java's non-nullable)
byte_field=1,
short_field=2,
int_field=42,
long_field=123456789,
float_field=1.5,
double_field=2.5,
bool_field=True,
boxed_int=10,
boxed_long=20,
boxed_float=1.1,
boxed_double=2.2,
boxed_bool=True,
string_field="hello",
list_field=["a", "b", "c"],
set_field={"x", "y"},
map_field={"key1": "value1", "key2": "value2"},
# Group 2: Non-nullable in Python (read from Java's nullable with values)
nullable_int1=100,
nullable_long1=200,
nullable_float1=1.5,
nullable_double1=2.5,
nullable_bool1=False,
nullable_string2="nullable_value",
nullable_list2=["p", "q"],
nullable_set2={"m", "n"},
nullable_map2={"nk1": "nv1"},
)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify Group 1: Nullable in Python (read from Java's non-nullable)
assert obj.byte_field == expected.byte_field, f"byte_field: {obj.byte_field} != {expected.byte_field}"
assert obj.short_field == expected.short_field, f"short_field: {obj.short_field} != {expected.short_field}"
assert obj.int_field == expected.int_field, f"int_field: {obj.int_field} != {expected.int_field}"
assert obj.long_field == expected.long_field, f"long_field: {obj.long_field} != {expected.long_field}"
assert abs(obj.float_field - expected.float_field) < 0.01, f"float_field: {obj.float_field} != {expected.float_field}"
assert abs(obj.double_field - expected.double_field) < 0.000001, f"double_field: {obj.double_field} != {expected.double_field}"
assert obj.bool_field == expected.bool_field, f"bool_field: {obj.bool_field} != {expected.bool_field}"
assert obj.boxed_int == expected.boxed_int, f"boxed_int: {obj.boxed_int} != {expected.boxed_int}"
assert obj.boxed_long == expected.boxed_long, f"boxed_long: {obj.boxed_long} != {expected.boxed_long}"
assert abs(obj.boxed_float - expected.boxed_float) < 0.01, f"boxed_float: {obj.boxed_float} != {expected.boxed_float}"
assert abs(obj.boxed_double - expected.boxed_double) < 0.01, f"boxed_double: {obj.boxed_double} != {expected.boxed_double}"
assert obj.boxed_bool == expected.boxed_bool, f"boxed_bool: {obj.boxed_bool} != {expected.boxed_bool}"
assert obj.string_field == expected.string_field, f"string_field: {obj.string_field} != {expected.string_field}"
assert obj.list_field == expected.list_field, f"list_field: {obj.list_field} != {expected.list_field}"
assert obj.set_field == expected.set_field, f"set_field: {obj.set_field} != {expected.set_field}"
assert obj.map_field == expected.map_field, f"map_field: {obj.map_field} != {expected.map_field}"
# Verify Group 2: Non-nullable in Python (read from Java's nullable with values)
assert obj.nullable_int1 == expected.nullable_int1, f"nullable_int1: {obj.nullable_int1} != {expected.nullable_int1}"
assert obj.nullable_long1 == expected.nullable_long1, f"nullable_long1: {obj.nullable_long1} != {expected.nullable_long1}"
assert abs(obj.nullable_float1 - expected.nullable_float1) < 0.01, f"nullable_float1: {obj.nullable_float1} != {expected.nullable_float1}"
assert abs(obj.nullable_double1 - expected.nullable_double1) < 0.01, f"nullable_double1: {obj.nullable_double1} != {expected.nullable_double1}"
assert obj.nullable_bool1 == expected.nullable_bool1, f"nullable_bool1: {obj.nullable_bool1} != {expected.nullable_bool1}"
assert obj.nullable_string2 == expected.nullable_string2, f"nullable_string2: {obj.nullable_string2} != {expected.nullable_string2}"
assert obj.nullable_list2 == expected.nullable_list2, f"nullable_list2: {obj.nullable_list2} != {expected.nullable_list2}"
assert obj.nullable_set2 == expected.nullable_set2, f"nullable_set2: {obj.nullable_set2} != {expected.nullable_set2}"
assert obj.nullable_map2 == expected.nullable_map2, f"nullable_map2: {obj.nullable_map2} != {expected.nullable_map2}"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_nullable_field_compatible_null():
"""
Test cross-language schema evolution - nullable fields are null.
Java sends: Group 1 (non-nullable with values) + Group 2 (nullable with null)
Python reads: Group 1 (nullable/Optional) + Group 2 (non-nullable -> defaults)
When Java sends null for Group 2 fields, Python's non-nullable fields receive
default values (0 for numbers, False for bool, empty/None for collections/strings).
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
# Use NoOpMetaCompressor to match Java's test configuration
fory = pyfory.Fory(xlang=True, compatible=True, meta_compressor=NoOpMetaCompressor())
fory.register_type(NullableComprehensiveCompatible, type_id=402)
expected = NullableComprehensiveCompatible(
# Group 1: Nullable in Python (read from Java's non-nullable)
byte_field=1,
short_field=2,
int_field=42,
long_field=123456789,
float_field=1.5,
double_field=2.5,
bool_field=True,
boxed_int=10,
boxed_long=20,
boxed_float=1.1,
boxed_double=2.2,
boxed_bool=True,
string_field="hello",
list_field=["a", "b", "c"],
set_field={"x", "y"},
map_field={"key1": "value1", "key2": "value2"},
# Group 2: Java sends null, Python receives null (like C++)
# Python properly preserves null values from the wire format
nullable_int1=None,
nullable_long1=None,
nullable_float1=None,
nullable_double1=None,
nullable_bool1=None,
nullable_string2=None,
nullable_list2=None,
nullable_set2=None,
nullable_map2=None,
)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify Group 1: Nullable in Python (read from Java's non-nullable)
assert obj.byte_field == expected.byte_field, f"byte_field: {obj.byte_field} != {expected.byte_field}"
assert obj.short_field == expected.short_field, f"short_field: {obj.short_field} != {expected.short_field}"
assert obj.int_field == expected.int_field, f"int_field: {obj.int_field} != {expected.int_field}"
assert obj.long_field == expected.long_field, f"long_field: {obj.long_field} != {expected.long_field}"
assert abs(obj.float_field - expected.float_field) < 0.01, f"float_field: {obj.float_field} != {expected.float_field}"
assert abs(obj.double_field - expected.double_field) < 0.000001, f"double_field: {obj.double_field} != {expected.double_field}"
assert obj.bool_field == expected.bool_field, f"bool_field: {obj.bool_field} != {expected.bool_field}"
assert obj.boxed_int == expected.boxed_int, f"boxed_int: {obj.boxed_int} != {expected.boxed_int}"
assert obj.boxed_long == expected.boxed_long, f"boxed_long: {obj.boxed_long} != {expected.boxed_long}"
assert abs(obj.boxed_float - expected.boxed_float) < 0.01, f"boxed_float: {obj.boxed_float} != {expected.boxed_float}"
assert abs(obj.boxed_double - expected.boxed_double) < 0.01, f"boxed_double: {obj.boxed_double} != {expected.boxed_double}"
assert obj.boxed_bool == expected.boxed_bool, f"boxed_bool: {obj.boxed_bool} != {expected.boxed_bool}"
assert obj.string_field == expected.string_field, f"string_field: {obj.string_field} != {expected.string_field}"
assert obj.list_field == expected.list_field, f"list_field: {obj.list_field} != {expected.list_field}"
assert obj.set_field == expected.set_field, f"set_field: {obj.set_field} != {expected.set_field}"
assert obj.map_field == expected.map_field, f"map_field: {obj.map_field} != {expected.map_field}"
# Verify Group 2: Java sent null, Python receives null (like C++ with std::optional)
assert obj.nullable_int1 is None, f"nullable_int1: {obj.nullable_int1} != None"
assert obj.nullable_long1 is None, f"nullable_long1: {obj.nullable_long1} != None"
assert obj.nullable_float1 is None, f"nullable_float1: {obj.nullable_float1} != None"
assert obj.nullable_double1 is None, f"nullable_double1: {obj.nullable_double1} != None"
assert obj.nullable_bool1 is None, f"nullable_bool1: {obj.nullable_bool1} != None"
assert obj.nullable_string2 is None, f"nullable_string2: {obj.nullable_string2} != None"
assert obj.nullable_list2 is None, f"nullable_list2: {obj.nullable_list2} != None"
assert obj.nullable_set2 is None, f"nullable_set2: {obj.nullable_set2} != None"
assert obj.nullable_map2 is None, f"nullable_map2: {obj.nullable_map2} != None"
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
# ============================================================================
# Reference Tracking Tests
# ============================================================================
def test_ref_schema_consistent():
"""
Test cross-language reference tracking in SCHEMA_CONSISTENT mode (compatible=false).
This test verifies that when Java serializes an object where two fields point to
the same instance, Python can properly deserialize it and both fields will reference
the same object. When re-serializing, the reference relationship should be preserved.
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False, ref=True)
fory.register_type(RefInnerSchemaConsistent, type_id=501)
fory.register_type(RefOuterSchemaConsistent, type_id=502)
outer = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {outer}")
# Both inner1 and inner2 should have values
assert outer.inner1 is not None, "inner1 should not be None"
assert outer.inner2 is not None, "inner2 should not be None"
# Both should have the same values (they reference the same object in Java)
assert outer.inner1.id == 42, f"inner1.id should be 42, got {outer.inner1.id}"
assert outer.inner1.name == "shared_inner", f"inner1.name should be 'shared_inner', got {outer.inner1.name}"
assert outer.inner1 == outer.inner2, "inner1 and inner2 should be equal (same reference)"
# In Python, after deserialization with reference tracking, inner1 and inner2
# should point to the same object (identity check)
assert outer.inner1 is outer.inner2, "inner1 and inner2 should be the same object (reference identity)"
# Re-serialize and write back
new_bytes = fory.serialize(outer)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_ref_compatible():
"""
Test cross-language reference tracking in COMPATIBLE mode (compatible=true).
This test verifies reference tracking works correctly with schema evolution support.
The inner object is shared between two fields, and this relationship should be
preserved through serialization/deserialization.
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True, ref=True)
fory.register_type(RefInnerCompatible, type_id=503)
fory.register_type(RefOuterCompatible, type_id=504)
outer = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {outer}")
# Both inner1 and inner2 should have values
assert outer.inner1 is not None, "inner1 should not be None"
assert outer.inner2 is not None, "inner2 should not be None"
# Both should have the same values (they reference the same object in Java)
assert outer.inner1.id == 99, f"inner1.id should be 99, got {outer.inner1.id}"
assert outer.inner1.name == "compatible_shared", f"inner1.name should be 'compatible_shared', got {outer.inner1.name}"
assert outer.inner1 == outer.inner2, "inner1 and inner2 should be equal (same reference)"
# In Python, after deserialization with reference tracking, inner1 and inner2
# should point to the same object (identity check)
assert outer.inner1 is outer.inner2, "inner1 and inner2 should be the same object (reference identity)"
# Re-serialize and write back
new_bytes = fory.serialize(outer)
with open(data_file, "wb") as f:
f.write(new_bytes)
# ============================================================================
# Circular Reference Test Types
# ============================================================================
@dataclass
class CircularRefStruct:
"""
Struct for circular reference tests.
Contains a self-referencing field and a name field.
The 'self_ref' field points back to the same object, creating a circular reference.
Matches Java CircularRefStruct (type id 601 for schema consistent, 602 for compatible)
"""
name: str = ""
self_ref: Optional["CircularRefStruct"] = pyfory.field(default=None, ref=True, nullable=True)
# ============================================================================
# Circular Reference Tests
# ============================================================================
def test_circular_ref_schema_consistent():
"""
Test circular reference in SCHEMA_CONSISTENT mode (compatible=false).
Creates a struct where the 'self_ref' field points back to the same object.
Verifies that after serialization/deserialization across languages,
the circular reference is preserved.
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=False, ref=True)
fory.register_type(CircularRefStruct, type_id=601)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify the struct has the expected name
assert obj.name == "circular_test", f"name should be 'circular_test', got {obj.name}"
# Verify circular reference is preserved (self_ref points to itself)
assert obj.self_ref is not None, "self_ref should not be None"
assert obj.self_ref is obj, "self_ref should point to the same object (circular reference)"
debug_print("Circular reference verified: obj.self_ref is obj")
# Re-serialize and write back
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
def test_circular_ref_compatible():
"""
Test circular reference in COMPATIBLE mode (compatible=true).
Creates a struct where the 'self_ref' field points back to the same object.
Verifies that circular references work with schema evolution support.
"""
data_file = get_data_file()
with open(data_file, "rb") as f:
data_bytes = f.read()
fory = pyfory.Fory(xlang=True, compatible=True, ref=True)
fory.register_type(CircularRefStruct, type_id=602)
obj = fory.deserialize(data_bytes)
debug_print(f"Deserialized: {obj}")
# Verify the struct has the expected name
assert obj.name == "compatible_circular", f"name should be 'compatible_circular', got {obj.name}"
# Verify circular reference is preserved (self_ref points to itself)
assert obj.self_ref is not None, "self_ref should not be None"
assert obj.self_ref is obj, "self_ref should point to the same object (circular reference)"
debug_print("Circular reference verified: obj.self_ref is obj")
# Re-serialize and write back
new_bytes = fory.serialize(obj)
with open(data_file, "wb") as f:
f.write(new_bytes)
if __name__ == "__main__":
"""
This file is executed by PythonXlangTest.java and other cross-language tests.
The test case name is passed as the first argument.
"""
import sys
print(f"Execute {sys.argv}")
try:
args = sys.argv[1:]
assert len(args) > 0, "Test case name required"
test_name = args[0]
func = getattr(sys.modules[__name__], test_name)
if not func:
raise Exception(f"Unknown test case: {test_name}")
func(*args[1:])
except BaseException as e:
logging.exception("Execute %s failed with %s", args, e)
raise