789 lines
27 KiB
C#
789 lines
27 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)0, result[0]); // BinaryTypeCode.Null = 0
|
|
}
|
|
|
|
[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]
|
|
[DataRow(DateTimeKind.Unspecified)]
|
|
[DataRow(DateTimeKind.Utc)]
|
|
[DataRow(DateTimeKind.Local)]
|
|
public void Serialize_DateTime_PreservesKind(DateTimeKind kind)
|
|
{
|
|
var value = new DateTime(2024, 12, 25, 10, 30, 45, kind);
|
|
var binary = AcBinarySerializer.Serialize(value);
|
|
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
|
|
|
Assert.AreEqual(value.Ticks, result.Ticks);
|
|
Assert.AreEqual(value.Kind, result.Kind);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// REGRESSION TEST: Comprehensive string interning edge cases.
|
|
///
|
|
/// Production bug pattern: "Invalid interned string index: X. Interned strings count: Y"
|
|
///
|
|
/// Root causes identified:
|
|
/// 1. Property names not being registered in intern table during deserialization
|
|
/// 2. String values with same length but different content
|
|
/// 3. Nested objects creating complex interning order
|
|
/// 4. Collections of objects with repeated property names
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization()
|
|
{
|
|
// This test verifies that property names (>= 4 chars) are properly
|
|
// registered in the intern table during deserialization.
|
|
// The serializer registers them via WriteString, so deserializer must too.
|
|
|
|
var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames
|
|
{
|
|
FirstProperty = $"Value1_{i}",
|
|
SecondProperty = $"Value2_{i}",
|
|
ThirdProperty = $"Value3_{i}",
|
|
FourthProperty = $"Value4_{i}"
|
|
}).ToList();
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestClassWithLongPropertyNames>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(10, result.Count);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
Assert.AreEqual($"Value1_{i}", result[i].FirstProperty);
|
|
Assert.AreEqual($"Value2_{i}", result[i].SecondProperty);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_MixedShortAndLongStrings_HandledCorrectly()
|
|
{
|
|
// Short strings (< 4 chars) are NOT interned
|
|
// Long strings (>= 4 chars) ARE interned
|
|
// This creates different traversal patterns
|
|
|
|
var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings
|
|
{
|
|
Id = i,
|
|
ShortName = $"A{i % 3}", // 2-3 chars, NOT interned
|
|
LongName = $"LongName_{i % 5}", // > 4 chars, interned
|
|
Description = $"Description_value_{i % 7}", // > 4 chars, interned
|
|
Tag = i % 2 == 0 ? "AB" : "XY" // 2 chars, NOT interned
|
|
}).ToList();
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestClassWithMixedStrings>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(20, result.Count);
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
Assert.AreEqual(i, result[i].Id);
|
|
Assert.AreEqual($"A{i % 3}", result[i].ShortName);
|
|
Assert.AreEqual($"LongName_{i % 5}", result[i].LongName);
|
|
Assert.AreEqual($"Description_value_{i % 7}", result[i].Description);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder()
|
|
{
|
|
// Complex nested structure where property names and values
|
|
// are interleaved in a specific order
|
|
|
|
var root = new TestNestedStructure
|
|
{
|
|
RootName = "RootObject",
|
|
Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1
|
|
{
|
|
Level1Name = $"Level1_{i}",
|
|
Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2
|
|
{
|
|
Level2Name = $"Level2_{i}_{j}",
|
|
Value = $"Value_{i * 3 + j}"
|
|
}).ToList()
|
|
}).ToList()
|
|
};
|
|
|
|
var binary = root.ToBinary();
|
|
var result = binary.BinaryTo<TestNestedStructure>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual("RootObject", result.RootName);
|
|
Assert.AreEqual(5, result.Level1Items.Count);
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name);
|
|
Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count);
|
|
|
|
for (int j = 0; j < 3; j++)
|
|
{
|
|
Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences()
|
|
{
|
|
// When the same string value appears multiple times,
|
|
// the serializer writes StringInterned reference instead of the full string.
|
|
// The deserializer must look up the correct index.
|
|
|
|
var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues
|
|
{
|
|
Id = i,
|
|
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
|
Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC",
|
|
Priority = i % 2 == 0 ? "High" : "Low_Priority_Value"
|
|
}).ToList();
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestClassWithRepeatedValues>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(50, result.Count);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
Assert.AreEqual(i, result[i].Id);
|
|
var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed";
|
|
Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}");
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup()
|
|
{
|
|
// First create many unique strings (all get registered)
|
|
// Then repeat some of them (use StringInterned references)
|
|
// This tests the index calculation
|
|
|
|
var items = new List<TestClassWithNameValue>();
|
|
|
|
// First 30 items with unique names (all registered as new)
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
items.Add(new TestClassWithNameValue
|
|
{
|
|
Name = $"UniqueName_{i:D4}",
|
|
Value = $"UniqueValue_{i:D4}"
|
|
});
|
|
}
|
|
|
|
// Next 20 items reuse names from first batch (should use StringInterned)
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
items.Add(new TestClassWithNameValue
|
|
{
|
|
Name = $"UniqueName_{i % 10:D4}", // Reuse first 10 names
|
|
Value = $"UniqueValue_{(i + 10) % 30:D4}" // Reuse different values
|
|
});
|
|
}
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestClassWithNameValue>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(50, result.Count);
|
|
|
|
// Verify first batch
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}");
|
|
Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}");
|
|
}
|
|
|
|
// Verify second batch (reused strings)
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}");
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable()
|
|
{
|
|
// Empty strings use StringEmpty type code
|
|
// Null strings use Null type code
|
|
// Neither should affect intern table indices
|
|
|
|
var items = new List<TestClassWithNullableStrings>();
|
|
|
|
for (int i = 0; i < 25; i++)
|
|
{
|
|
items.Add(new TestClassWithNullableStrings
|
|
{
|
|
Id = i,
|
|
RequiredName = $"Required_{i:D3}",
|
|
OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}",
|
|
Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null
|
|
});
|
|
}
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestClassWithNullableStrings>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(25, result.Count);
|
|
|
|
for (int i = 0; i < 25; i++)
|
|
{
|
|
Assert.AreEqual(i, result[i].Id);
|
|
Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName);
|
|
|
|
if (i % 3 == 0)
|
|
{
|
|
Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null");
|
|
}
|
|
else if (i % 3 == 1)
|
|
{
|
|
// Empty string may deserialize as either "" or null depending on implementation
|
|
Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName),
|
|
$"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'");
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName,
|
|
$"OptionalName at index {i} mismatch");
|
|
}
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_ProductionLikeCustomerDto_RoundTrip()
|
|
{
|
|
// Simulate the CustomerDto structure that causes production issues
|
|
// Key characteristics:
|
|
// - Many string properties (FirstName, LastName, Email, Company, etc.)
|
|
// - GenericAttributes list with repeated Key values
|
|
// - List of items with common status/category values
|
|
|
|
var customers = Enumerable.Range(0, 25).Select(i => new TestCustomerLikeDto
|
|
{
|
|
Id = i,
|
|
FirstName = $"FirstName_{i % 10}", // 10 unique values
|
|
LastName = $"LastName_{i % 8}", // 8 unique values
|
|
Email = $"user{i}@example.com", // All unique
|
|
Company = $"Company_{i % 5}", // 5 unique values
|
|
Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing",
|
|
Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest",
|
|
Status = i % 2 == 0 ? "Active" : "Inactive",
|
|
Attributes = new List<TestGenericAttribute>
|
|
{
|
|
new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" },
|
|
new() { Key = "Priority", Value = (i % 5).ToString() },
|
|
new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" },
|
|
new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" }
|
|
}
|
|
}).ToList();
|
|
|
|
var binary = customers.ToBinary();
|
|
var result = binary.BinaryTo<List<TestCustomerLikeDto>>();
|
|
|
|
Assert.IsNotNull(result, "Result should not be null - deserialization failed");
|
|
Assert.AreEqual(25, result.Count);
|
|
|
|
for (int i = 0; i < 25; i++)
|
|
{
|
|
Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}");
|
|
Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}");
|
|
Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}");
|
|
Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}");
|
|
Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}");
|
|
|
|
Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key);
|
|
Assert.AreEqual("Priority", result[i].Attributes[1].Key);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly()
|
|
{
|
|
// Large dataset (100+ items) with high string reuse ratio
|
|
// This is the scenario that triggers production bugs
|
|
|
|
const int itemCount = 150;
|
|
var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto
|
|
{
|
|
Id = i,
|
|
// Property names are reused 150 times (once per object)
|
|
CategoryCode = $"CAT_{i % 10:D2}", // 10 unique values, 15x reuse each
|
|
StatusCode = $"STATUS_{i % 5:D2}", // 5 unique values, 30x reuse each
|
|
TypeCode = $"TYPE_{i % 3:D2}", // 3 unique values, 50x reuse each
|
|
PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", // 2 values, 75x each
|
|
UniqueField = $"UNIQUE_{i:D4}" // All unique, no reuse
|
|
}).ToList();
|
|
|
|
var binary = items.ToBinary();
|
|
var result = binary.BinaryTo<List<TestHighReuseDto>>();
|
|
|
|
Assert.IsNotNull(result, "Result should not be null");
|
|
Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items");
|
|
|
|
// Verify every item
|
|
for (int i = 0; i < itemCount; i++)
|
|
{
|
|
Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}");
|
|
Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}");
|
|
Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}");
|
|
Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}");
|
|
Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}");
|
|
}
|
|
}
|
|
|
|
#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; } = "";
|
|
}
|
|
|
|
// New test models for string interning edge cases
|
|
|
|
private class TestClassWithLongPropertyNames
|
|
{
|
|
public string FirstProperty { get; set; } = "";
|
|
public string SecondProperty { get; set; } = "";
|
|
public string ThirdProperty { get; set; } = "";
|
|
public string FourthProperty { get; set; } = "";
|
|
}
|
|
|
|
private class TestClassWithMixedStrings
|
|
{
|
|
public int Id { get; set; }
|
|
public string ShortName { get; set; } = ""; // < 4 chars
|
|
public string LongName { get; set; } = ""; // >= 4 chars
|
|
public string Description { get; set; } = ""; // >= 4 chars
|
|
public string Tag { get; set; } = ""; // < 4 chars
|
|
}
|
|
|
|
private class TestNestedStructure
|
|
{
|
|
public string RootName { get; set; } = "";
|
|
public List<TestLevel1> Level1Items { get; set; } = new();
|
|
}
|
|
|
|
private class TestLevel1
|
|
{
|
|
public string Level1Name { get; set; } = "";
|
|
public List<TestLevel2> Level2Items { get; set; } = new();
|
|
}
|
|
|
|
private class TestLevel2
|
|
{
|
|
public string Level2Name { get; set; } = "";
|
|
public string Value { get; set; } = "";
|
|
}
|
|
|
|
private class TestClassWithRepeatedValues
|
|
{
|
|
public int Id { get; set; }
|
|
public string Status { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public string Priority { get; set; } = "";
|
|
}
|
|
|
|
private class TestClassWithNameValue
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Value { get; set; } = "";
|
|
}
|
|
|
|
private class TestClassWithNullableStrings
|
|
{
|
|
public int Id { get; set; }
|
|
public string RequiredName { get; set; } = "";
|
|
public string? OptionalName { get; set; }
|
|
public string? Description { get; set; }
|
|
}
|
|
|
|
private class TestCustomerLikeDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string FirstName { get; set; } = "";
|
|
public string LastName { get; set; } = "";
|
|
public string Email { get; set; } = "";
|
|
public string Company { get; set; } = "";
|
|
public string Department { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public string Status { get; set; } = "";
|
|
public List<TestGenericAttribute> Attributes { get; set; } = new();
|
|
}
|
|
|
|
private class TestGenericAttribute
|
|
{
|
|
public string Key { get; set; } = "";
|
|
public string Value { get; set; } = "";
|
|
}
|
|
|
|
private class TestHighReuseDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string CategoryCode { get; set; } = "";
|
|
public string StatusCode { get; set; } = "";
|
|
public string TypeCode { get; set; } = "";
|
|
public string PriorityCode { get; set; } = "";
|
|
public string UniqueField { 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
|
|
}
|