222 lines
10 KiB
C#
222 lines
10 KiB
C#
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
using AyCode.Core.Serializers;
|
||
using AyCode.Core.Serializers.Binaries;
|
||
using AyCode.Core.Tests.TestModels;
|
||
|
||
namespace AyCode.Core.Tests.Serialization;
|
||
|
||
[TestClass]
|
||
public class AcBinarySerializerSGenRuntimeCompatibilityTests
|
||
{
|
||
private static readonly JsonSerializerOptions StjOptions = new()
|
||
{
|
||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||
};
|
||
|
||
[TestMethod]
|
||
public void SerializeWithSGen_DeserializeWithRuntime_LargeAndDeepData_MultipleOptions_RoundTrip()
|
||
{
|
||
foreach (var dataSet in GetTargetDataSets())
|
||
{
|
||
foreach (var optionFactory in GetOptionFactories())
|
||
{
|
||
var serializeOptions = optionFactory();
|
||
serializeOptions.UseGeneratedCode = true;
|
||
|
||
var deserializeOptions = optionFactory();
|
||
deserializeOptions.UseGeneratedCode = false;
|
||
|
||
var expectedJson = JsonSerializer.Serialize(dataSet.Order, StjOptions);
|
||
|
||
var bytes = AcBinarySerializer.Serialize(dataSet.Order, serializeOptions);
|
||
var roundTrip = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(bytes, deserializeOptions);
|
||
var actualJson = JsonSerializer.Serialize(roundTrip, StjOptions);
|
||
|
||
Assert.AreEqual(expectedJson, actualJson, $"STJ mismatch. Dataset={dataSet.Name}, WireMode={serializeOptions.WireMode}, BaseOptions={serializeOptions.ReferenceHandling}/{serializeOptions.UseStringInterning}");
|
||
|
||
AssertOrderEquivalent(dataSet.Order, roundTrip, $"Dataset={dataSet.Name}, WireMode={serializeOptions.WireMode}, BaseOptions={serializeOptions.ReferenceHandling}/{serializeOptions.UseStringInterning}");
|
||
}
|
||
}
|
||
}
|
||
|
||
[TestMethod]
|
||
public void SerializeWithRuntime_DeserializeWithSGen_LargeAndDeepData_MultipleOptions_RoundTrip()
|
||
{
|
||
foreach (var dataSet in GetTargetDataSets())
|
||
{
|
||
foreach (var optionFactory in GetOptionFactories())
|
||
{
|
||
var serializeOptions = optionFactory();
|
||
serializeOptions.UseGeneratedCode = false;
|
||
|
||
var deserializeOptions = optionFactory();
|
||
deserializeOptions.UseGeneratedCode = true;
|
||
|
||
var expectedJson = JsonSerializer.Serialize(dataSet.Order, StjOptions);
|
||
|
||
var bytes = AcBinarySerializer.Serialize(dataSet.Order, serializeOptions);
|
||
var roundTrip = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(bytes, deserializeOptions);
|
||
var actualJson = JsonSerializer.Serialize(roundTrip, StjOptions);
|
||
|
||
Assert.AreEqual(expectedJson, actualJson, $"STJ mismatch. Dataset={dataSet.Name}, WireMode={serializeOptions.WireMode}, BaseOptions={serializeOptions.ReferenceHandling}/{serializeOptions.UseStringInterning}");
|
||
|
||
AssertOrderEquivalent(dataSet.Order, roundTrip, $"Dataset={dataSet.Name}, WireMode={serializeOptions.WireMode}, BaseOptions={serializeOptions.ReferenceHandling}/{serializeOptions.UseStringInterning}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Regression test: SGen ↔ SGen round-trip with non-ASCII multi-byte ProductName above the
|
||
/// StringSmall threshold (utf8Len > 255 byte). Engages the StringMedium tier (marker 94,
|
||
/// fixed-width header [marker:1][charLen:16][utf8Len:16][bytes]). After ProductName in
|
||
/// TestOrderItemBase come Quantity (int) + UnitPrice (decimal) — any writer/reader byte-count
|
||
/// asymmetry in the StringMedium path surfaces as a UnitPrice corruption (DECIMAL_DRIFT) or
|
||
/// Quantity skew. The [AcStringIntern(true)] attribute on ProductName means the first occurrence
|
||
/// emits StringInternFirstMedium (marker 105) for the InternFirst tier.
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void Serialize_MediumStringUtf8_OnProductName_SGenRoundTrip()
|
||
{
|
||
// 300 chars × 2 byte (Hungarian 'á' = 2 byte UTF-8) = 600 byte UTF-8 → StringMedium (or
|
||
// StringInternFirstMedium for the first occurrence under interning).
|
||
var mediumUtf8 = new string('á', 300);
|
||
|
||
foreach (var optionFactory in GetOptionFactories())
|
||
{
|
||
var options = optionFactory();
|
||
options.UseGeneratedCode = true;
|
||
|
||
var order = BenchmarkTestDataProvider
|
||
.CreateTestDataSets()
|
||
.Cast<TestDataSet<TestOrder_All_True>>()
|
||
.First(x => x.Name.StartsWith("Small")).Order;
|
||
|
||
foreach (var item in order.Items) item.ProductName = mediumUtf8;
|
||
|
||
var bytes = AcBinarySerializer.Serialize(order, options);
|
||
var roundTrip = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(bytes, options);
|
||
|
||
AssertOrderEquivalent(order, roundTrip,
|
||
$"WireMode={options.WireMode}, Refs={options.ReferenceHandling}, Interning={options.UseStringInterning}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Regression test: SGen ↔ SGen round-trip with pure ASCII ProductName above the FixStrAscii inline
|
||
/// limit (>31 chars). Engages StringAscii (marker 167) — writer detects ASCII via
|
||
/// bytesWritten == charLength post-encode, reader byte→char widens directly without UTF-8 decode.
|
||
/// Same drift-surface as the UTF-8 variant: UnitPrice / Quantity after ProductName in TestOrderItemBase.
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void Serialize_MediumStringAscii_OnProductName_SGenRoundTrip()
|
||
{
|
||
// 500 chars × 1 byte = 500 byte ASCII → StringAscii (167) tier.
|
||
var mediumAscii = new string('X', 500);
|
||
|
||
foreach (var optionFactory in GetOptionFactories())
|
||
{
|
||
var options = optionFactory();
|
||
options.UseGeneratedCode = true;
|
||
|
||
var order = BenchmarkTestDataProvider
|
||
.CreateTestDataSets()
|
||
.Cast<TestDataSet<TestOrder_All_True>>()
|
||
.First(x => x.Name.StartsWith("Small")).Order;
|
||
|
||
foreach (var item in order.Items) item.ProductName = mediumAscii;
|
||
|
||
var bytes = AcBinarySerializer.Serialize(order, options);
|
||
var roundTrip = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(bytes, options);
|
||
|
||
AssertOrderEquivalent(order, roundTrip,
|
||
$"WireMode={options.WireMode}, Refs={options.ReferenceHandling}, Interning={options.UseStringInterning}");
|
||
}
|
||
}
|
||
|
||
private static IEnumerable<TestDataSet<TestOrder_All_True>> GetTargetDataSets()
|
||
{
|
||
// SGen↔Runtime compatibility test depends on TestOrder_All_True graphs (the AssertOrderEquivalent
|
||
// signature + JSON canonicalisation are typed for _All_True). The bare-name BenchmarkTestDataProvider
|
||
// alias closes the generic provider on _All_True — Phase 1 benchmark uses the sibling
|
||
// BenchmarkTestDataProvider_All_False alias instead.
|
||
return BenchmarkTestDataProvider
|
||
.CreateTestDataSets()
|
||
.Cast<TestDataSet<TestOrder_All_True>>()
|
||
.Where(x => x.Name.StartsWith("Large") || x.Name.StartsWith("Deep"));
|
||
}
|
||
|
||
private static IEnumerable<Func<AcBinarySerializerOptions>> GetOptionFactories()
|
||
{
|
||
yield return static () =>
|
||
{
|
||
var options = AcBinarySerializerOptions.FastMode;
|
||
options.WireMode = WireMode.Compact;
|
||
return options;
|
||
};
|
||
|
||
yield return static () =>
|
||
{
|
||
var options = AcBinarySerializerOptions.FastMode;
|
||
options.WireMode = WireMode.Fast;
|
||
return options;
|
||
};
|
||
|
||
yield return static () =>
|
||
{
|
||
var options = AcBinarySerializerOptions.Default;
|
||
options.WireMode = WireMode.Compact;
|
||
return options;
|
||
};
|
||
}
|
||
|
||
private static void AssertOrderEquivalent(TestOrder_All_True expected, TestOrder_All_True? actual, string context)
|
||
{
|
||
Assert.IsNotNull(actual, context);
|
||
Assert.AreEqual(expected.Id, actual.Id, context);
|
||
Assert.AreEqual(expected.OrderNumber, actual.OrderNumber, context);
|
||
Assert.AreEqual(expected.Status, actual.Status, context);
|
||
Assert.AreEqual(expected.Items.Count, actual.Items.Count, context);
|
||
|
||
for (var itemIndex = 0; itemIndex < expected.Items.Count; itemIndex++)
|
||
{
|
||
var expectedItem = expected.Items[itemIndex];
|
||
var actualItem = actual.Items[itemIndex];
|
||
|
||
Assert.AreEqual(expectedItem.Id, actualItem.Id, context);
|
||
Assert.AreEqual(expectedItem.ProductName, actualItem.ProductName, context);
|
||
Assert.AreEqual(expectedItem.Status, actualItem.Status, context);
|
||
Assert.AreEqual(expectedItem.Pallets.Count, actualItem.Pallets.Count, context);
|
||
|
||
for (var palletIndex = 0; palletIndex < expectedItem.Pallets.Count; palletIndex++)
|
||
{
|
||
var expectedPallet = expectedItem.Pallets[palletIndex];
|
||
var actualPallet = actualItem.Pallets[palletIndex];
|
||
|
||
Assert.AreEqual(expectedPallet.Id, actualPallet.Id, context);
|
||
Assert.AreEqual(expectedPallet.PalletCode, actualPallet.PalletCode, context);
|
||
Assert.AreEqual(expectedPallet.Measurements.Count, actualPallet.Measurements.Count, context);
|
||
|
||
for (var measurementIndex = 0; measurementIndex < expectedPallet.Measurements.Count; measurementIndex++)
|
||
{
|
||
var expectedMeasurement = expectedPallet.Measurements[measurementIndex];
|
||
var actualMeasurement = actualPallet.Measurements[measurementIndex];
|
||
|
||
Assert.AreEqual(expectedMeasurement.Id, actualMeasurement.Id, context);
|
||
Assert.AreEqual(expectedMeasurement.Name, actualMeasurement.Name, context);
|
||
Assert.AreEqual(expectedMeasurement.Points.Count, actualMeasurement.Points.Count, context);
|
||
|
||
for (var pointIndex = 0; pointIndex < expectedMeasurement.Points.Count; pointIndex++)
|
||
{
|
||
var expectedPoint = expectedMeasurement.Points[pointIndex];
|
||
var actualPoint = actualMeasurement.Points[pointIndex];
|
||
|
||
Assert.AreEqual(expectedPoint.Id, actualPoint.Id, context);
|
||
Assert.AreEqual(expectedPoint.Label, actualPoint.Label, context);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|