425 lines
13 KiB
C#
425 lines
13 KiB
C#
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Tests.TestModels;
|
|
|
|
namespace AyCode.Core.Tests.Serialization;
|
|
|
|
[TestClass]
|
|
public class AcBinarySerializerTests
|
|
{
|
|
#region Basic Serialization Tests
|
|
|
|
[TestMethod]
|
|
public void Serialize_Null_ReturnsSingleNullByte()
|
|
{
|
|
var result = AcBinarySerializer.Serialize<object?>(null);
|
|
Assert.AreEqual(1, result.Length);
|
|
Assert.AreEqual((byte)32, result[0]); // BinaryTypeCode.Null = 32
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Int32_RoundTrip()
|
|
{
|
|
var value = 12345;
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<int>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Int64_RoundTrip()
|
|
{
|
|
var value = 123456789012345L;
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<long>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Double_RoundTrip()
|
|
{
|
|
var value = 3.14159265358979;
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<double>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_String_RoundTrip()
|
|
{
|
|
var value = "Hello, Binary World!";
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<string>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Boolean_RoundTrip()
|
|
{
|
|
var trueResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(true));
|
|
var falseResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(false));
|
|
Assert.IsTrue(trueResult);
|
|
Assert.IsFalse(falseResult);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_DateTime_RoundTrip()
|
|
{
|
|
var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc);
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Guid_RoundTrip()
|
|
{
|
|
var value = Guid.NewGuid();
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<Guid>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Decimal_RoundTrip()
|
|
{
|
|
var value = 123456.789012m;
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<decimal>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_TimeSpan_RoundTrip()
|
|
{
|
|
var value = TimeSpan.FromHours(2.5);
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<TimeSpan>(binary);
|
|
Assert.AreEqual(value, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_DateTimeOffset_RoundTrip()
|
|
{
|
|
var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2));
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<DateTimeOffset>(binary);
|
|
|
|
// Compare UTC ticks and offset separately since we store UTC ticks
|
|
Assert.AreEqual(value.UtcTicks, result.UtcTicks);
|
|
Assert.AreEqual(value.Offset, result.Offset);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Object Serialization Tests
|
|
|
|
[TestMethod]
|
|
public void Serialize_SimpleObject_RoundTrip()
|
|
{
|
|
var obj = new TestSimpleClass
|
|
{
|
|
Id = 42,
|
|
Name = "Test Object",
|
|
Value = 3.14,
|
|
IsActive = true
|
|
};
|
|
|
|
var binary = obj.ToBinary();
|
|
var result = binary.BinaryTo<TestSimpleClass>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(obj.Id, result.Id);
|
|
Assert.AreEqual(obj.Name, result.Name);
|
|
Assert.AreEqual(obj.Value, result.Value);
|
|
Assert.AreEqual(obj.IsActive, result.IsActive);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_NestedObject_RoundTrip()
|
|
{
|
|
var obj = new TestNestedClass
|
|
{
|
|
Id = 1,
|
|
Name = "Parent",
|
|
Child = new TestSimpleClass
|
|
{
|
|
Id = 2,
|
|
Name = "Child",
|
|
Value = 2.5,
|
|
IsActive = true
|
|
}
|
|
};
|
|
|
|
var binary = obj.ToBinary();
|
|
var result = binary.BinaryTo<TestNestedClass>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(obj.Id, result.Id);
|
|
Assert.AreEqual(obj.Name, result.Name);
|
|
Assert.IsNotNull(result.Child);
|
|
Assert.AreEqual(obj.Child.Id, result.Child.Id);
|
|
Assert.AreEqual(obj.Child.Name, result.Child.Name);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_List_RoundTrip()
|
|
{
|
|
var list = new List<int> { 1, 2, 3, 4, 5 };
|
|
var binary = list.ToBinary();
|
|
var result = binary.BinaryTo<List<int>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
CollectionAssert.AreEqual(list, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_ObjectWithList_RoundTrip()
|
|
{
|
|
var obj = new TestClassWithList
|
|
{
|
|
Id = 1,
|
|
Items = new List<string> { "Item1", "Item2", "Item3" }
|
|
};
|
|
|
|
var binary = obj.ToBinary();
|
|
var result = binary.BinaryTo<TestClassWithList>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(obj.Id, result.Id);
|
|
Assert.IsNotNull(result.Items);
|
|
CollectionAssert.AreEqual(obj.Items, result.Items);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_Dictionary_RoundTrip()
|
|
{
|
|
var dict = new Dictionary<string, int>
|
|
{
|
|
["one"] = 1,
|
|
["two"] = 2,
|
|
["three"] = 3
|
|
};
|
|
|
|
var binary = dict.ToBinary();
|
|
var result = binary.BinaryTo<Dictionary<string, int>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(dict.Count, result.Count);
|
|
foreach (var kvp in dict)
|
|
{
|
|
Assert.IsTrue(result.ContainsKey(kvp.Key));
|
|
Assert.AreEqual(kvp.Value, result[kvp.Key]);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Populate Tests
|
|
|
|
[TestMethod]
|
|
public void Populate_UpdatesExistingObject()
|
|
{
|
|
var target = new TestSimpleClass { Id = 0, Name = "Original" };
|
|
var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 };
|
|
|
|
var binary = source.ToBinary();
|
|
binary.BinaryTo(target);
|
|
|
|
Assert.AreEqual(42, target.Id);
|
|
Assert.AreEqual("Updated", target.Name);
|
|
Assert.AreEqual(3.14, target.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PopulateMerge_MergesNestedObjects()
|
|
{
|
|
var target = new TestNestedClass
|
|
{
|
|
Id = 1,
|
|
Name = "Original",
|
|
Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 }
|
|
};
|
|
|
|
var source = new TestNestedClass
|
|
{
|
|
Id = 2,
|
|
Name = "Updated",
|
|
Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 }
|
|
};
|
|
|
|
var binary = source.ToBinary();
|
|
binary.BinaryToMerge(target);
|
|
|
|
Assert.AreEqual(2, target.Id);
|
|
Assert.AreEqual("Updated", target.Name);
|
|
Assert.IsNotNull(target.Child);
|
|
// Child object should be merged, not replaced
|
|
Assert.AreEqual(20, target.Child.Id);
|
|
Assert.AreEqual("UpdatedChild", target.Child.Name);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region String Interning Tests
|
|
|
|
[TestMethod]
|
|
public void Serialize_RepeatedStrings_UsesInterning()
|
|
{
|
|
var obj = new TestClassWithRepeatedStrings
|
|
{
|
|
Field1 = "Repeated",
|
|
Field2 = "Repeated",
|
|
Field3 = "Repeated",
|
|
Field4 = "Unique"
|
|
};
|
|
|
|
var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default);
|
|
var binaryWithoutInterning = AcBinarySerializer.Serialize(obj,
|
|
new AcBinarySerializerOptions { UseStringInterning = false });
|
|
|
|
// With interning should be smaller
|
|
Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length,
|
|
$"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}");
|
|
|
|
// Both should deserialize correctly
|
|
var result1 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithInterning);
|
|
var result2 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(binaryWithoutInterning);
|
|
|
|
Assert.AreEqual(obj.Field1, result1!.Field1);
|
|
Assert.AreEqual(obj.Field1, result2!.Field1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Size Comparison Tests
|
|
|
|
[TestMethod]
|
|
public void Serialize_IsSmallerThanJson()
|
|
{
|
|
var obj = TestDataFactory.CreateBenchmarkOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3);
|
|
|
|
var jsonBytes = System.Text.Encoding.UTF8.GetBytes(obj.ToJson());
|
|
var binaryBytes = obj.ToBinary();
|
|
|
|
Console.WriteLine($"JSON size: {jsonBytes.Length} bytes");
|
|
Console.WriteLine($"Binary size: {binaryBytes.Length} bytes");
|
|
Console.WriteLine($"Ratio: {(double)binaryBytes.Length / jsonBytes.Length:P2}");
|
|
|
|
Assert.IsTrue(binaryBytes.Length < jsonBytes.Length,
|
|
$"Binary ({binaryBytes.Length}) should be smaller than JSON ({jsonBytes.Length})");
|
|
|
|
// Verify roundtrip works
|
|
var result = binaryBytes.BinaryTo<TestOrder>();
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(obj.Id, result.Id);
|
|
Assert.AreEqual(obj.Items.Count, result.Items.Count);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extension Method Tests
|
|
|
|
[TestMethod]
|
|
public void BinaryCloneTo_CreatesDeepCopy()
|
|
{
|
|
var original = new TestNestedClass
|
|
{
|
|
Id = 1,
|
|
Name = "Original",
|
|
Child = new TestSimpleClass { Id = 2, Name = "Child" }
|
|
};
|
|
|
|
var clone = original.BinaryCloneTo();
|
|
|
|
Assert.IsNotNull(clone);
|
|
Assert.AreNotSame(original, clone);
|
|
Assert.AreNotSame(original.Child, clone.Child);
|
|
Assert.AreEqual(original.Id, clone.Id);
|
|
Assert.AreEqual(original.Child.Id, clone.Child!.Id);
|
|
|
|
// Modify clone, original should be unchanged
|
|
clone.Id = 999;
|
|
clone.Child.Id = 888;
|
|
Assert.AreEqual(1, original.Id);
|
|
Assert.AreEqual(2, original.Child.Id);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Models
|
|
|
|
private class TestSimpleClass
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = "";
|
|
public double Value { get; set; }
|
|
public bool IsActive { get; set; }
|
|
}
|
|
|
|
private class TestNestedClass
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = "";
|
|
public TestSimpleClass? Child { get; set; }
|
|
}
|
|
|
|
private class TestClassWithList
|
|
{
|
|
public int Id { get; set; }
|
|
public List<string> Items { get; set; } = new();
|
|
}
|
|
|
|
private class TestClassWithRepeatedStrings
|
|
{
|
|
public string Field1 { get; set; } = "";
|
|
public string Field2 { get; set; } = "";
|
|
public string Field3 { get; set; } = "";
|
|
public string Field4 { get; set; } = "";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Benchmark Order Tests
|
|
|
|
[TestMethod]
|
|
public void Serialize_BenchmarkOrder_RoundTrip()
|
|
{
|
|
// This is the exact same data that causes stack overflow in benchmarks
|
|
var order = TestDataFactory.CreateBenchmarkOrder(
|
|
itemCount: 3,
|
|
palletsPerItem: 2,
|
|
measurementsPerPallet: 2,
|
|
pointsPerMeasurement: 5);
|
|
|
|
// Should not throw stack overflow
|
|
var binary = AcBinarySerializer.Serialize(order);
|
|
Assert.IsTrue(binary.Length > 0, "Binary data should not be empty");
|
|
|
|
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(order.Id, result.Id);
|
|
Assert.AreEqual(order.OrderNumber, result.OrderNumber);
|
|
Assert.AreEqual(order.Items.Count, result.Items.Count);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Serialize_BenchmarkOrder_SmallData_RoundTrip()
|
|
{
|
|
// Smaller test to isolate the issue
|
|
var order = TestDataFactory.CreateBenchmarkOrder(
|
|
itemCount: 1,
|
|
palletsPerItem: 1,
|
|
measurementsPerPallet: 1,
|
|
pointsPerMeasurement: 1);
|
|
|
|
var binary = AcBinarySerializer.Serialize(order);
|
|
Assert.IsTrue(binary.Length > 0);
|
|
|
|
var result = AcBinaryDeserializer.Deserialize<TestOrder>(binary);
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(order.Id, result.Id);
|
|
}
|
|
|
|
#endregion
|
|
}
|