475 lines
19 KiB
C#
475 lines
19 KiB
C#
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Serializers.Binaries;
|
|
using System.Reflection;
|
|
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
|
|
|
|
namespace AyCode.Core.Tests.Serialization;
|
|
|
|
/// <summary>
|
|
/// Diagnostic tests to help debug serialization issues.
|
|
/// </summary>
|
|
[TestClass]
|
|
public class AcBinarySerializerDiagnosticTests
|
|
{
|
|
/// <summary>
|
|
/// Diagnostic test to understand the exact binary structure.
|
|
/// This test outputs the binary bytes to help debug production issues.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_StockTaking_BinaryStructure()
|
|
{
|
|
var stockTaking = new TestStockTakingWithInheritance
|
|
{
|
|
Id = 1,
|
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = false,
|
|
Creator = 6,
|
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
|
StockTakingItems = null
|
|
};
|
|
|
|
var binary = stockTaking.ToBinary();
|
|
|
|
var hexDump = string.Join(" ", binary.Select(b => b.ToString("X2")));
|
|
Console.WriteLine($"Binary length: {binary.Length}");
|
|
Console.WriteLine($"Binary hex: {hexDump}");
|
|
|
|
for (int i = 0; i < binary.Length; i++)
|
|
{
|
|
if (binary[i] == 214)
|
|
{
|
|
Console.WriteLine($"Found 0xD6 at position {i}");
|
|
}
|
|
}
|
|
|
|
var result = binary.BinaryTo<TestStockTakingWithInheritance>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(6, result.Creator);
|
|
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test with nested list to ensure proper stream positioning.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_StockTaking_WithNestedItems()
|
|
{
|
|
var stockTaking = new TestStockTakingWithInheritance
|
|
{
|
|
Id = 1,
|
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = false,
|
|
Creator = 6,
|
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
|
StockTakingItems = new List<TestStockTakingItemWithInheritance>
|
|
{
|
|
new()
|
|
{
|
|
Id = 10,
|
|
StockTakingId = 1,
|
|
ProductId = 100,
|
|
IsMeasured = true,
|
|
OriginalStockQuantity = 50,
|
|
MeasuredStockQuantity = 48,
|
|
Created = new DateTime(2025, 1, 24, 14, 0, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 14, 30, 0, DateTimeKind.Utc),
|
|
StockTakingItemPallets = null
|
|
}
|
|
}
|
|
};
|
|
|
|
var binary = stockTaking.ToBinary();
|
|
Console.WriteLine($"Binary length with 1 item: {binary.Length}");
|
|
|
|
var result = binary.BinaryTo<TestStockTakingWithInheritance>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(6, result.Creator, "Creator should be 6");
|
|
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
|
|
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
|
|
Assert.IsNotNull(result.StockTakingItems);
|
|
Assert.AreEqual(1, result.StockTakingItems.Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// CRITICAL TEST: Verify property order is consistent.
|
|
/// This test checks that the reflection-based property order matches
|
|
/// what's expected for serialization/deserialization.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_PropertyOrder_InheritanceHierarchy()
|
|
{
|
|
var type = typeof(SimStockTaking);
|
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
Console.WriteLine($"Properties of {type.Name} (count: {props.Length}):");
|
|
for (int i = 0; i < props.Length; i++)
|
|
{
|
|
var prop = props[i];
|
|
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name} (declared in: {prop.DeclaringType?.Name})");
|
|
}
|
|
|
|
// The exact order may vary by platform!
|
|
// Log it so we can compare server vs client
|
|
Assert.IsTrue(props.Length >= 7, "Should have at least 7 properties");
|
|
|
|
// Check that all expected properties exist
|
|
var propNames = props.Select(p => p.Name).ToHashSet();
|
|
Assert.IsTrue(propNames.Contains("Id"), "Should have Id");
|
|
Assert.IsTrue(propNames.Contains("StartDateTime"), "Should have StartDateTime");
|
|
Assert.IsTrue(propNames.Contains("IsClosed"), "Should have IsClosed");
|
|
Assert.IsTrue(propNames.Contains("Creator"), "Should have Creator");
|
|
Assert.IsTrue(propNames.Contains("Created"), "Should have Created");
|
|
Assert.IsTrue(propNames.Contains("Modified"), "Should have Modified");
|
|
Assert.IsTrue(propNames.Contains("StockTakingItems"), "Should have StockTakingItems");
|
|
}
|
|
|
|
/// <summary>
|
|
/// CRITICAL REGRESSION TEST: Simulates exact production hierarchy.
|
|
/// StockTaking : MgStockTaking<StockTakingItem> : MgEntityBase : BaseEntity
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_SimStockTaking_RoundTrip()
|
|
{
|
|
var stockTaking = new SimStockTaking
|
|
{
|
|
Id = 1,
|
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = false,
|
|
Creator = 6, // The exact value from production error
|
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
|
StockTakingItems = null // loadRelations = false means no items
|
|
};
|
|
|
|
var binary = stockTaking.ToBinary();
|
|
|
|
// Log the property names in the header
|
|
Console.WriteLine($"Binary length: {binary.Length}");
|
|
|
|
var result = binary.BinaryTo<SimStockTaking>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(1, result.Id, "Id should be 1");
|
|
Assert.AreEqual(6, result.Creator, "Creator should be 6 - this is where the bug occurs!");
|
|
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
|
|
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
|
|
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
|
|
Assert.IsFalse(result.IsClosed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test List of SimStockTaking - exact production scenario.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_ListOfSimStockTaking_RoundTrip()
|
|
{
|
|
var stockTakings = Enumerable.Range(1, 3).Select(i => new SimStockTaking
|
|
{
|
|
Id = i,
|
|
StartDateTime = DateTime.UtcNow.AddDays(-i),
|
|
IsClosed = i % 2 == 0,
|
|
Creator = i,
|
|
Created = DateTime.UtcNow.AddDays(-i),
|
|
Modified = DateTime.UtcNow,
|
|
StockTakingItems = null
|
|
}).ToList();
|
|
|
|
var binary = stockTakings.ToBinary();
|
|
var result = binary.BinaryTo<List<SimStockTaking>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(3, result.Count);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var original = stockTakings[i];
|
|
var deserialized = result[i];
|
|
|
|
Assert.AreEqual(original.Id, deserialized.Id, $"[{i}] Id mismatch");
|
|
Assert.AreEqual(original.Creator, deserialized.Creator, $"[{i}] Creator mismatch");
|
|
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks,
|
|
$"[{i}] Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Diagnostic: Check what PropertyType the reflection returns for generic type parameter.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_GenericProperty_ReflectionType()
|
|
{
|
|
var parentType = typeof(ConcreteParent);
|
|
var itemsProp = parentType.GetProperty("Items");
|
|
|
|
Assert.IsNotNull(itemsProp);
|
|
|
|
var propType = itemsProp.PropertyType;
|
|
Console.WriteLine($"PropertyType: {propType}");
|
|
Console.WriteLine($"PropertyType.FullName: {propType.FullName}");
|
|
Console.WriteLine($"IsGenericType: {propType.IsGenericType}");
|
|
|
|
if (propType.IsGenericType)
|
|
{
|
|
var args = propType.GetGenericArguments();
|
|
Console.WriteLine($"GenericArguments.Length: {args.Length}");
|
|
foreach (var arg in args)
|
|
{
|
|
Console.WriteLine($" GenericArgument: {arg.FullName}");
|
|
}
|
|
}
|
|
|
|
Assert.IsTrue(propType.IsGenericType);
|
|
var elementType = propType.GetGenericArguments()[0];
|
|
Assert.AreEqual(typeof(GenericItemImpl), elementType,
|
|
"Element type should be GenericItemImpl, not IGenericItem");
|
|
}
|
|
|
|
/// <summary>
|
|
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
|
|
/// This test verifies what happens when:
|
|
/// 1. Metadata header registers ALL properties including StockTakingItems
|
|
/// 2. Body SKIPS StockTakingItems because it's null
|
|
/// 3. Deserializer reads the body and must correctly map indices
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
|
|
{
|
|
var stockTaking = new SimStockTaking
|
|
{
|
|
Id = 1,
|
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = false,
|
|
Creator = 6, // The exact value from production error (becomes TinyInt 0xD6)
|
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
|
StockTakingItems = null // THIS IS THE KEY - loadRelations = false
|
|
};
|
|
|
|
var binary = stockTaking.ToBinary();
|
|
|
|
// Log the binary structure
|
|
Console.WriteLine($"Binary length: {binary.Length}");
|
|
|
|
// Parse the header manually to understand structure
|
|
var pos = 0;
|
|
var version = binary[pos++];
|
|
Console.WriteLine($"Version: {version}");
|
|
|
|
var marker = binary[pos++];
|
|
Console.WriteLine($"Marker: 0x{marker:X2}");
|
|
|
|
// Read property count from metadata header
|
|
if ((marker & 0x10) != 0) // HasMetadata flag
|
|
{
|
|
var propCount = binary[pos++];
|
|
Console.WriteLine($"\n=== METADATA HEADER ===");
|
|
Console.WriteLine($"Property count in header: {propCount}");
|
|
|
|
for (int i = 0; i < propCount; i++)
|
|
{
|
|
var strLen = binary[pos++];
|
|
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
|
|
pos += strLen;
|
|
Console.WriteLine($" Header property [{i}]: '{propName}'");
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
|
|
|
|
// The body should start with Object marker (0x19)
|
|
var bodyStart = pos;
|
|
var objectMarker = binary[pos++];
|
|
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
|
|
|
|
// Read ref ID (if reference handling is enabled)
|
|
// VarInt: if top bit is set, continue reading
|
|
var refIdByte = binary[pos];
|
|
int refId;
|
|
if ((refIdByte & 0x80) == 0)
|
|
{
|
|
refId = refIdByte;
|
|
pos++;
|
|
}
|
|
else
|
|
{
|
|
// Multi-byte VarInt - simplified parsing
|
|
refId = -1;
|
|
pos += 2; // Skip for now
|
|
}
|
|
Console.WriteLine($"RefId: {refId}");
|
|
|
|
// Read property count in body
|
|
var bodyPropCount = binary[pos++];
|
|
Console.WriteLine($"Property count in body: {bodyPropCount}");
|
|
|
|
Console.WriteLine($"\n=== BODY PROPERTIES ===");
|
|
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
|
|
{
|
|
var propIndex = binary[pos++];
|
|
Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}");
|
|
|
|
// Skip the value (simplified - just log)
|
|
var valueType = binary[pos];
|
|
if (valueType == 0x14) // DateTime
|
|
{
|
|
Console.WriteLine($" -> DateTime (9 bytes)");
|
|
pos += 10; // type + 9 bytes
|
|
}
|
|
else if (valueType >= 0xD0 && valueType <= 0xE7) // TinyInt
|
|
{
|
|
var tinyValue = valueType - 0xD0;
|
|
Console.WriteLine($" -> TinyInt value: {tinyValue}");
|
|
pos += 1;
|
|
}
|
|
else if (valueType == 0x03) // False
|
|
{
|
|
Console.WriteLine($" -> Boolean: false");
|
|
pos += 1;
|
|
}
|
|
else if (valueType == 0x02) // True
|
|
{
|
|
Console.WriteLine($" -> Boolean: true");
|
|
pos += 1;
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($" -> Unknown type: 0x{valueType:X2}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find where 0xD6 (Creator = 6) appears in the body
|
|
Console.WriteLine($"\n=== 0xD6 OCCURRENCES ===");
|
|
for (int i = bodyStart; i < binary.Length; i++)
|
|
{
|
|
if (binary[i] == 0xD6)
|
|
{
|
|
Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}");
|
|
}
|
|
}
|
|
|
|
// Deserialize and verify
|
|
var result = binary.BinaryTo<SimStockTaking>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(1, result.Id, "Id should be 1");
|
|
Assert.AreEqual(6, result.Creator,
|
|
$"Creator should be 6. Got: {result.Creator}. " +
|
|
$"If this fails with a very large number, it means DateTime bytes were interpreted as int!");
|
|
Assert.AreEqual(stockTaking.Created, result.Created,
|
|
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}. " +
|
|
$"If Created has wrong value, deserializer read wrong bytes!");
|
|
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
|
|
Assert.IsFalse(result.IsClosed);
|
|
Assert.IsNull(result.StockTakingItems, "StockTakingItems should remain null");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to verify property order consistency between serializer and deserializer.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_VerifyPropertyOrderConsistency()
|
|
{
|
|
// Get serializer's property order
|
|
var serializerType = typeof(AcBinarySerializer);
|
|
var metadataCacheField = serializerType.GetField("TypeMetadataCache",
|
|
BindingFlags.NonPublic | BindingFlags.Static);
|
|
|
|
// Clear cache to force fresh metadata creation
|
|
// (This helps ensure we're testing the actual order)
|
|
|
|
var type = typeof(SimStockTaking);
|
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
|
.ToArray();
|
|
|
|
Console.WriteLine($"Properties of {type.Name} (reflection order):");
|
|
for (int i = 0; i < props.Length; i++)
|
|
{
|
|
var prop = props[i];
|
|
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name}");
|
|
}
|
|
|
|
// Verify Creator comes BEFORE Created in the reflection order
|
|
var creatorIndex = Array.FindIndex(props, p => p.Name == "Creator");
|
|
var createdIndex = Array.FindIndex(props, p => p.Name == "Created");
|
|
var stockTakingItemsIndex = Array.FindIndex(props, p => p.Name == "StockTakingItems");
|
|
|
|
Console.WriteLine($"\nKey indices:");
|
|
Console.WriteLine($" StockTakingItems: {stockTakingItemsIndex}");
|
|
Console.WriteLine($" Creator: {creatorIndex}");
|
|
Console.WriteLine($" Created: {createdIndex}");
|
|
|
|
// The bug scenario: if StockTakingItems is skipped during serialization,
|
|
// but the deserializer still expects it at the original index position,
|
|
// then Creator (index 3) would be read when expecting StockTakingItems (index 2)
|
|
// and Created (index 4) would be read when expecting Creator (index 3)
|
|
|
|
Assert.IsTrue(stockTakingItemsIndex >= 0, "StockTakingItems should exist");
|
|
Assert.IsTrue(creatorIndex >= 0, "Creator should exist");
|
|
Assert.IsTrue(createdIndex >= 0, "Created should exist");
|
|
|
|
// In the class definition order:
|
|
// StockTakingItems comes BEFORE Creator and Created
|
|
Assert.IsTrue(stockTakingItemsIndex < creatorIndex,
|
|
"StockTakingItems should come before Creator");
|
|
Assert.IsTrue(creatorIndex < createdIndex,
|
|
"Creator should come before Created");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test multiple StockTakings with null StockTakingItems - exact production scenario.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Diagnostic_MultipleStockTakings_NullItems()
|
|
{
|
|
var stockTakings = new List<SimStockTaking>
|
|
{
|
|
new()
|
|
{
|
|
Id = 1,
|
|
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = false,
|
|
Creator = 6,
|
|
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
|
|
StockTakingItems = null
|
|
},
|
|
new()
|
|
{
|
|
Id = 2,
|
|
StartDateTime = new DateTime(2025, 1, 23, 9, 0, 0, DateTimeKind.Utc),
|
|
IsClosed = true,
|
|
Creator = 12,
|
|
Created = new DateTime(2025, 1, 23, 14, 0, 0, DateTimeKind.Utc),
|
|
Modified = new DateTime(2025, 1, 23, 15, 30, 0, DateTimeKind.Utc),
|
|
StockTakingItems = null
|
|
}
|
|
};
|
|
|
|
var binary = stockTakings.ToBinary();
|
|
Console.WriteLine($"Binary length for 2 StockTakings: {binary.Length}");
|
|
|
|
var result = binary.BinaryTo<List<SimStockTaking>>();
|
|
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(2, result.Count);
|
|
|
|
// First item
|
|
Assert.AreEqual(1, result[0].Id);
|
|
Assert.AreEqual(6, result[0].Creator, "First item Creator should be 6");
|
|
Assert.AreEqual(stockTakings[0].Created, result[0].Created,
|
|
$"First item Created mismatch. Expected: {stockTakings[0].Created}, Got: {result[0].Created}");
|
|
|
|
// Second item
|
|
Assert.AreEqual(2, result[1].Id);
|
|
Assert.AreEqual(12, result[1].Creator, "Second item Creator should be 12");
|
|
Assert.AreEqual(stockTakings[1].Created, result[1].Created,
|
|
$"Second item Created mismatch. Expected: {stockTakings[1].Created}, Got: {result[1].Created}");
|
|
}
|
|
}
|