AyCode.Core/AyCode.Core.Tests/Serialization/AcBinarySerializerSGenRunti...

222 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 &gt; 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 (&gt;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);
}
}
}
}
}
}