blob: 4326afce3fbb0d99e71ec0ade5cf5a3e21265dde [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.
from dataclasses import dataclass
import datetime
from typing import Dict, Any, List, Set, Optional
import os
import pytest
import typing
import pyfory
from pyfory import Fory
from pyfory.error import TypeUnregisteredError
from pyfory.struct import DataClassSerializer
def ser_de(fory, obj):
binary = fory.serialize(obj)
return fory.deserialize(binary)
@dataclass
class SimpleObject:
f1: Optional[Dict[pyfory.int32, pyfory.float64]] = None
@dataclass
class ComplexObject:
f1: Optional[Any] = None
f2: Optional[Any] = None
f3: pyfory.int8 = 0
f4: pyfory.int16 = 0
f5: pyfory.int32 = 0
f6: pyfory.int64 = 0
f7: pyfory.float32 = 0
f8: pyfory.float64 = 0
f9: Optional[List[pyfory.int16]] = None
f10: Optional[Dict[pyfory.int32, pyfory.float64]] = None
def test_struct():
fory = Fory(xlang=True, ref=True)
fory.register_type(SimpleObject, typename="SimpleObject")
fory.register_type(ComplexObject, typename="example.ComplexObject")
o = SimpleObject(f1={1: 1.0 / 3})
assert ser_de(fory, o) == o
o = ComplexObject(
f1="str",
f2={"k1": -1, "k2": [1, 2]},
f3=2**7 - 1,
f4=2**15 - 1,
f5=2**31 - 1,
f6=2**63 - 1,
f7=1.0 / 2,
f8=2.0 / 3,
f9=[1, 2],
f10={1: 1.0 / 3, 100: 2 / 7.0},
)
assert ser_de(fory, o) == o
with pytest.raises(AssertionError):
assert ser_de(fory, ComplexObject(f7=1.0 / 3)) == ComplexObject(f7=1.0 / 3)
with pytest.raises(OverflowError):
assert ser_de(fory, ComplexObject(f3=2**8)) == ComplexObject(f3=2**8)
with pytest.raises(OverflowError):
assert ser_de(fory, ComplexObject(f4=2**16)) == ComplexObject(f4=2**16)
with pytest.raises(OverflowError):
assert ser_de(fory, ComplexObject(f5=2**32)) == ComplexObject(f5=2**32)
with pytest.raises(OverflowError):
assert ser_de(fory, ComplexObject(f6=2**64)) == ComplexObject(f6=2**64)
@dataclass
class SuperClass1:
f1: Optional[Any] = None
f2: pyfory.int8 = 0
@dataclass
class ChildClass1(SuperClass1):
f3: Optional[Dict[str, pyfory.float64]] = None
def test_strict():
fory = Fory(xlang=False, ref=True)
obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3})
with pytest.raises(TypeUnregisteredError):
fory.serialize(obj)
def test_inheritance():
type_hints = typing.get_type_hints(ChildClass1)
print(type_hints)
assert type_hints.keys() == {"f1", "f2", "f3"}
fory = Fory(xlang=False, ref=True, strict=False)
obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3})
assert ser_de(fory, obj) == obj
assert type(fory.type_resolver.get_serializer(ChildClass1)) is pyfory.DataClassSerializer
@dataclass
class DataClassObject:
f_int: int
f_float: float
f_str: str
f_bool: bool
f_list: List[int]
f_dict: Dict[str, float]
f_any: Optional[Any]
f_complex: Optional[ComplexObject] = None
@classmethod
def create(cls):
return cls(
f_int=42,
f_float=3.14159,
f_str="test_codegen",
f_bool=True,
f_list=[1, 2, 3],
f_dict={"key": 1.5},
f_any="any_data",
f_complex=None,
)
def test_sort_fields():
@dataclass
class TestClass:
f1: pyfory.int32
f2: List[pyfory.int16]
f3: Dict[str, pyfory.float64]
f4: str
f5: pyfory.float32
f6: bytes
f7: bool
f8: Any
f9: Dict[pyfory.int32, pyfory.float64]
f10: List[str]
f11: pyfory.int8
f12: pyfory.int64
f13: pyfory.float64
f14: Set[pyfory.int32]
f15: datetime.datetime
fory = Fory(xlang=True, ref=True)
serializer = DataClassSerializer(fory, TestClass, xlang=True)
# Sorting order:
# 1. Non-compressed primitives (compress=0) by -size, then name:
# float64(8), float32(4), bool(1), int8(1) => f13, f5, f11, f7
# (f11 < f7 alphabetically since '1' < '7')
# 2. Compressed primitives (compress=1) by -size, then name:
# int64(8), int32(4) => f12, f1
# 3. Internal types by type_id, then name: str, datetime, bytes => f4, f15, f6
# 4. Collection types by type_id, then name: list => f10, f2
# 5. Set types by type_id, then name: set => f14
# 6. Map types by type_id, then name: dict => f3, f9
# 7. Other types (polymorphic/any) by name: any => f8
assert serializer._field_names == ["f13", "f5", "f11", "f7", "f12", "f1", "f4", "f15", "f6", "f10", "f2", "f14", "f3", "f9", "f8"]
def test_data_class_serializer_xlang():
fory = Fory(xlang=True, ref=True)
fory.register_type(ComplexObject, typename="example.ComplexObject")
fory.register_type(DataClassObject, typename="example.TestDataClassObject")
complex_data = ComplexObject(
f1="nested_str",
f5=100,
f8=3.14,
f10={10: 1.0, 20: 2.0},
)
obj_original = DataClassObject(
f_int=123,
f_float=45.67,
f_str="hello xlang",
f_bool=True,
f_list=[1, 2, 3, 4, 5],
f_dict={"a": 1.1, "b": 2.2},
f_any="any_value",
f_complex=complex_data,
)
obj_deserialized = ser_de(fory, obj_original)
assert obj_deserialized == obj_original
assert obj_deserialized.f_int == obj_original.f_int
assert obj_deserialized.f_float == obj_original.f_float
assert obj_deserialized.f_str == obj_original.f_str
assert obj_deserialized.f_bool == obj_original.f_bool
assert obj_deserialized.f_list == obj_original.f_list
assert obj_deserialized.f_dict == obj_original.f_dict
assert obj_deserialized.f_any == obj_original.f_any
assert obj_deserialized.f_complex == obj_original.f_complex
assert type(fory.type_resolver.get_serializer(DataClassObject)) is pyfory.DataClassSerializer
# Ensure it's using xlang mode indirectly, by checking no JIT methods if possible,
# or by ensuring it was registered with _register_xtype which now uses DataClassSerializer(xlang=True)
# For now, the registration path check is implicit via Language.XLANG usage.
# We can also check if the hash is non-zero if it was computed,
# or if the _serializers attribute exists.
serializer_instance = fory.type_resolver.get_serializer(DataClassObject)
assert hasattr(serializer_instance, "_serializers") # xlang mode creates this
assert serializer_instance._xlang is True
# Test with None for a complex field
obj_with_none_complex = DataClassObject(
f_int=789,
f_float=12.34,
f_str="another string",
f_bool=False,
f_list=[10, 20],
f_dict={"x": 7.7, "y": 8.8},
f_any=None,
f_complex=None,
)
obj_deserialized_none = ser_de(fory, obj_with_none_complex)
assert obj_deserialized_none == obj_with_none_complex
def test_data_class_serializer_xlang_codegen():
"""Test that DataClassSerializer generates xwrite/xread methods correctly in xlang mode."""
fory = Fory(xlang=True, ref=True)
# Register types first
fory.register_type(ComplexObject, typename="example.ComplexObject")
fory.register_type(DataClassObject, typename="example.TestDataClassObject")
# trigger lazy serializer replace
fory.serialize(DataClassObject.create())
# Get the serializer that was created during registration
serializer = fory.type_resolver.get_serializer(DataClassObject)
# Check that the generated methods exist
assert hasattr(serializer, "_generated_xwrite_method"), "Generated xwrite method should exist"
assert hasattr(serializer, "_generated_xread_method"), "Generated xread method should exist"
assert hasattr(serializer, "_xwrite_method_code"), "Generated xwrite method code should exist"
assert hasattr(serializer, "_xread_method_code"), "Generated xread method code should exist"
# Check that the serializer is in xlang mode
assert serializer._xlang is True
assert hasattr(serializer, "_serializers")
assert len(serializer._serializers) == len(serializer._field_names)
# Test that the generated methods work correctly through the normal serialization flow
test_obj = DataClassObject(
f_int=42,
f_float=3.14159,
f_str="test_codegen",
f_bool=True,
f_list=[1, 2, 3],
f_dict={"key": 1.5},
f_any="any_data",
f_complex=None,
)
# Test serialization and deserialization using the normal fory flow
# This will use the generated methods internally
binary = fory.serialize(test_obj)
deserialized_obj = fory.deserialize(binary)
# Verify the results
assert deserialized_obj.f_int == test_obj.f_int
assert deserialized_obj.f_float == test_obj.f_float
assert deserialized_obj.f_str == test_obj.f_str
assert deserialized_obj.f_bool == test_obj.f_bool
assert deserialized_obj.f_list == test_obj.f_list
assert deserialized_obj.f_dict == test_obj.f_dict
assert deserialized_obj.f_any == test_obj.f_any
assert deserialized_obj.f_complex == test_obj.f_complex
def test_data_class_serializer_xlang_codegen_with_jit():
"""Test that DataClassSerializer JIT compilation works correctly when enabled."""
# Save the original environment variable
original_jit_setting = os.environ.get("ENABLE_FORY_PYTHON_JIT")
try:
# Enable JIT
os.environ["ENABLE_FORY_PYTHON_JIT"] = "True"
# Import after setting environment variable to ensure it takes effect
import importlib
import pyfory.serializer
importlib.reload(pyfory.serializer)
fory = Fory(xlang=True, ref=True)
# Register types first
fory.register_type(ComplexObject, typename="example.ComplexObject")
fory.register_type(DataClassObject, typename="example.TestDataClassObject")
# Get the serializer that was created during registration
serializer = fory.type_resolver.get_serializer(DataClassObject)
# Check that JIT methods are assigned when JIT is enabled
# The methods should be the generated functions, not the original instance methods
assert callable(serializer.xwrite)
assert callable(serializer.xread)
# Test that the JIT-compiled methods work through normal serialization
test_obj = DataClassObject(
f_int=123,
f_float=45.67,
f_str="jit_test",
f_bool=False,
f_list=[10, 20, 30],
f_dict={"jit": 2.5},
f_any={"nested": "data"},
f_complex=None,
)
# Use normal serialization flow which will use the JIT-compiled methods internally
binary = fory.serialize(test_obj)
deserialized_obj = fory.deserialize(binary)
assert deserialized_obj.f_int == test_obj.f_int
assert deserialized_obj.f_float == test_obj.f_float
assert deserialized_obj.f_str == test_obj.f_str
assert deserialized_obj.f_bool == test_obj.f_bool
assert deserialized_obj.f_list == test_obj.f_list
assert deserialized_obj.f_dict == test_obj.f_dict
assert deserialized_obj.f_any == test_obj.f_any
assert deserialized_obj.f_complex == test_obj.f_complex
finally:
# Restore original environment variable
if original_jit_setting is None:
os.environ.pop("ENABLE_FORY_PYTHON_JIT", None)
else:
os.environ["ENABLE_FORY_PYTHON_JIT"] = original_jit_setting
# Reload to restore the original state
importlib.reload(pyfory.serializer)
def test_data_class_serializer_xlang_codegen_generated_code():
"""Test that the generated code contains expected elements."""
fory = Fory(xlang=True, ref=True)
# Register types first
fory.register_type(ComplexObject, typename="example.ComplexObject")
fory.register_type(DataClassObject, typename="example.TestDataClassObject")
# trigger lazy serializer replace
fory.serialize(DataClassObject.create())
# Get the serializer that was created during registration
serializer = fory.type_resolver.get_serializer(DataClassObject)
# Check that generated code exists and contains expected elements
xwrite_code = serializer._xwrite_method_code
xread_code = serializer._xread_method_code
assert isinstance(xwrite_code, str)
assert isinstance(xread_code, str)
# Check that xwrite code contains expected elements
assert "def xwrite_" in xwrite_code
assert "buffer.write_int32" in xwrite_code # Hash writing
assert "fory.xwrite_ref" in xwrite_code # Field serialization
# Check that xread code contains expected elements
assert "def xread_" in xread_code
assert "buffer.read_int32" in xread_code # Hash reading
assert "fory.xread_ref" in xread_code # Field deserialization
assert "TypeNotCompatibleError" in xread_code # Hash validation
# Check that field names are referenced in the code
for field_name in serializer._field_names:
# Field names should appear in the generated code
assert field_name in xwrite_code or field_name in xread_code
def test_data_class_serializer_xlang_vs_non_xlang():
"""Test that xlang and non-xlang modes produce different serializers."""
fory_xlang = Fory(xlang=True, ref=True)
fory_python = Fory(xlang=False, ref=True, strict=False)
# Register types for xlang
fory_xlang.register_type(ComplexObject, typename="example.ComplexObject")
fory_xlang.register_type(DataClassObject, typename="example.TestDataClassObject")
# trigger lazy serializer replace
fory_xlang.serialize(DataClassObject.create())
# For Python mode, we can create the serializer directly since it doesn't require registration
serializer_xlang = fory_xlang.type_resolver.get_serializer(DataClassObject)
serializer_python = DataClassSerializer(fory_python, DataClassObject, xlang=False)
# xlang serializer should have xlang-specific attributes
assert serializer_xlang._xlang is True
assert hasattr(serializer_xlang, "_serializers")
assert hasattr(serializer_xlang, "_generated_xwrite_method")
assert hasattr(serializer_xlang, "_generated_xread_method")
# non-xlang serializer should have different attributes
assert serializer_python._xlang is False
assert hasattr(serializer_python, "_generated_write_method")
assert hasattr(serializer_python, "_generated_read_method")
# They should have different method implementations
assert serializer_xlang._generated_xwrite_method != serializer_python._generated_write_method
assert serializer_xlang._generated_xread_method != serializer_python._generated_read_method
@dataclass
class OptionalFieldsObject:
f1: Optional[int] = None
f2: Optional[str] = None
f3: Optional[List[int]] = None
f4: int = 0
f5: str = ""
@pytest.mark.parametrize("xlang", [False, True])
@pytest.mark.parametrize("compatible", [False, True])
def test_optional_fields(xlang, compatible):
fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False)
if xlang:
fory.register_type(OptionalFieldsObject, typename="example.OptionalFieldsObject")
obj_with_none = OptionalFieldsObject(f1=None, f2=None, f3=None, f4=42, f5="test")
result = ser_de(fory, obj_with_none)
assert result.f1 is None
assert result.f2 is None
assert result.f3 is None
assert result.f4 == 42
assert result.f5 == "test"
obj_with_values = OptionalFieldsObject(f1=100, f2="hello", f3=[1, 2, 3], f4=42, f5="test")
result = ser_de(fory, obj_with_values)
assert result.f1 == 100
assert result.f2 == "hello"
assert result.f3 == [1, 2, 3]
assert result.f4 == 42
assert result.f5 == "test"
obj_mixed = OptionalFieldsObject(f1=100, f2=None, f3=[1, 2, 3], f4=42, f5="test")
result = ser_de(fory, obj_mixed)
assert result.f1 == 100
assert result.f2 is None
assert result.f3 == [1, 2, 3]
assert result.f4 == 42
assert result.f5 == "test"
@dataclass
class NestedOptionalObject:
f1: Optional[ComplexObject] = None
f2: Optional[Dict[str, int]] = None
f3: str = ""
@pytest.mark.parametrize("xlang", [False, True])
@pytest.mark.parametrize("compatible", [False, True])
def test_nested_optional_fields(xlang, compatible):
fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False)
if xlang:
fory.register_type(ComplexObject, typename="example.ComplexObject")
fory.register_type(NestedOptionalObject, typename="example.NestedOptionalObject")
obj_with_none = NestedOptionalObject(f1=None, f2=None, f3="test")
result = ser_de(fory, obj_with_none)
assert result.f1 is None
assert result.f2 is None
assert result.f3 == "test"
complex_obj = ComplexObject(f1="nested", f5=100, f8=3.14)
obj_with_values = NestedOptionalObject(f1=complex_obj, f2={"a": 1, "b": 2}, f3="test")
result = ser_de(fory, obj_with_values)
assert result.f1.f1 == "nested"
assert result.f1.f5 == 100
assert result.f2 == {"a": 1, "b": 2}
assert result.f3 == "test"
@dataclass
class OptionalV1:
f1: Optional[int] = None
f2: str = ""
f3: Optional[List[int]] = None
@dataclass
class OptionalV2:
f1: Optional[int] = None
f2: str = ""
f3: Optional[List[int]] = None
f4: Optional[str] = None
@dataclass
class OptionalV3:
f1: Optional[int] = None
f2: str = ""
@dataclass
class CompatibleV1:
f1: int = 0
f2: str = ""
f3: float = 0.0
@dataclass
class CompatibleV2:
f1: int = 0
f2: str = ""
f3: float = 0.0
f4: bool = False
@dataclass
class CompatibleV3:
f1: int = 0
f2: str = ""
@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_add_field(xlang):
"""Test that adding a field with default value works in compatible mode."""
fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v1.register_type(CompatibleV1, typename="example.Compatible")
fory_v2.register_type(CompatibleV2, typename="example.Compatible")
# V1 object serialized
v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14)
v1_binary = fory_v1.serialize(v1_obj)
# V2 can read V1 data, new field gets default value
v2_result = fory_v2.deserialize(v1_binary)
assert v2_result.f1 == 100
assert v2_result.f2 == "test"
assert v2_result.f3 == 3.14
assert v2_result.f4 is False # Default value
@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_remove_field(xlang):
"""Test that removing a field works in compatible mode."""
fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v3 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v2.register_type(CompatibleV2, typename="example.Compatible")
fory_v3.register_type(CompatibleV3, typename="example.Compatible")
# V2 object with all fields
v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True)
v2_binary = fory_v2.serialize(v2_obj)
# V3 can read V2 data, extra fields are ignored
v3_result = fory_v3.deserialize(v2_binary)
assert v3_result.f1 == 200
assert v3_result.f2 == "hello"
# f3 and f4 from V2 are ignored
@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_bidirectional(xlang):
"""Test bidirectional compatible serialization."""
fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v1.register_type(CompatibleV1, typename="example.Compatible")
fory_v2.register_type(CompatibleV2, typename="example.Compatible")
# V1 -> V2
v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14)
v1_binary = fory_v1.serialize(v1_obj)
v2_result = fory_v2.deserialize(v1_binary)
assert v2_result.f1 == 100
assert v2_result.f2 == "test"
assert v2_result.f3 == 3.14
assert v2_result.f4 is False
# V2 -> V1
v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True)
v2_binary = fory_v2.serialize(v2_obj)
v1_result = fory_v1.deserialize(v2_binary)
assert v1_result.f1 == 200
assert v1_result.f2 == "hello"
assert v1_result.f3 == 2.71
@dataclass
class CompatibleWithOptional:
f1: Optional[int] = None
f2: str = ""
f3: Optional[List[int]] = None
@dataclass
class CompatibleWithOptionalV2:
f1: Optional[int] = None
f2: str = ""
f3: Optional[List[int]] = None
f4: Optional[str] = None
@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_with_optional_fields(xlang):
"""Test compatible mode with optional fields."""
fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v1.register_type(CompatibleWithOptional, typename="example.CompatibleOptional")
fory_v2.register_type(CompatibleWithOptionalV2, typename="example.CompatibleOptional")
# V1 with None values
v1_obj = CompatibleWithOptional(f1=None, f2="test", f3=None)
v1_binary = fory_v1.serialize(v1_obj)
v2_result = fory_v2.deserialize(v1_binary)
assert v2_result.f1 is None
assert v2_result.f2 == "test"
assert v2_result.f3 is None
assert v2_result.f4 is None
# V1 with values
v1_obj2 = CompatibleWithOptional(f1=100, f2="test", f3=[1, 2, 3])
v1_binary2 = fory_v1.serialize(v1_obj2)
v2_result2 = fory_v2.deserialize(v1_binary2)
assert v2_result2.f1 == 100
assert v2_result2.f2 == "test"
assert v2_result2.f3 == [1, 2, 3]
assert v2_result2.f4 is None
@dataclass
class CompatibleAllTypes:
f_int: int = 0
f_str: str = ""
f_float: float = 0.0
f_bool: bool = False
f_list: Optional[List[int]] = None
f_dict: Optional[Dict[str, int]] = None
@dataclass
class CompatibleAllTypesV2:
f_int: int = 0
f_str: str = ""
f_float: float = 0.0
f_bool: bool = False
f_list: Optional[List[int]] = None
f_dict: Optional[Dict[str, int]] = None
f_new: str = "default"
@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_all_basic_types(xlang):
"""Test compatible mode with all basic types."""
fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
fory_v1.register_type(CompatibleAllTypes, typename="example.CompatibleAllTypes")
fory_v2.register_type(CompatibleAllTypesV2, typename="example.CompatibleAllTypes")
v1_obj = CompatibleAllTypes(f_int=42, f_str="hello", f_float=3.14, f_bool=True, f_list=[1, 2, 3], f_dict={"a": 1, "b": 2})
v1_binary = fory_v1.serialize(v1_obj)
v2_result = fory_v2.deserialize(v1_binary)
assert v2_result.f_int == 42
assert v2_result.f_str == "hello"
assert v2_result.f_float == 3.14
assert v2_result.f_bool is True
assert v2_result.f_list == [1, 2, 3]
assert v2_result.f_dict == {"a": 1, "b": 2}
assert v2_result.f_new == "default"
def test_optional_compatible_mode_evolution():
fory_v1 = Fory(xlang=True, ref=True, compatible=True)
fory_v2 = Fory(xlang=True, ref=True, compatible=True)
fory_v3 = Fory(xlang=True, ref=True, compatible=True)
fory_v1.register_type(OptionalV1, typename="example.OptionalVersioned")
fory_v2.register_type(OptionalV2, typename="example.OptionalVersioned")
fory_v3.register_type(OptionalV3, typename="example.OptionalVersioned")
v1_obj = OptionalV1(f1=100, f2="test", f3=[1, 2, 3])
v1_binary = fory_v1.serialize(v1_obj)
v2_result = fory_v2.deserialize(v1_binary)
assert v2_result.f1 == 100
assert v2_result.f2 == "test"
assert v2_result.f3 == [1, 2, 3]
assert v2_result.f4 is None
v1_obj_with_none = OptionalV1(f1=None, f2="test", f3=None)
v1_binary_with_none = fory_v1.serialize(v1_obj_with_none)
v2_result_with_none = fory_v2.deserialize(v1_binary_with_none)
assert v2_result_with_none.f1 is None
assert v2_result_with_none.f2 == "test"
assert v2_result_with_none.f3 is None
assert v2_result_with_none.f4 is None
v2_obj = OptionalV2(f1=200, f2="test2", f3=[4, 5], f4="extra")
v2_binary = fory_v2.serialize(v2_obj)
v3_result = fory_v3.deserialize(v2_binary)
assert v3_result.f1 == 200
assert v3_result.f2 == "test2"
v2_obj_partial_none = OptionalV2(f1=None, f2="test2", f3=None, f4=None)
v2_binary_partial_none = fory_v2.serialize(v2_obj_partial_none)
v3_result_partial_none = fory_v3.deserialize(v2_binary_partial_none)
assert v3_result_partial_none.f1 is None
assert v3_result_partial_none.f2 == "test2"
v3_obj = OptionalV3(f1=300, f2="test3")
v3_binary = fory_v3.serialize(v3_obj)
v1_result = fory_v1.deserialize(v3_binary)
assert v1_result.f1 == 300
assert v1_result.f2 == "test3"
assert v1_result.f3 is None