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(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(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}"); } } } /// /// 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. /// [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>() .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(bytes, options); AssertOrderEquivalent(order, roundTrip, $"WireMode={options.WireMode}, Refs={options.ReferenceHandling}, Interning={options.UseStringInterning}"); } } /// /// 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. /// [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>() .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(bytes, options); AssertOrderEquivalent(order, roundTrip, $"WireMode={options.WireMode}, Refs={options.ReferenceHandling}, Interning={options.UseStringInterning}"); } } private static IEnumerable> 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>() .Where(x => x.Name.StartsWith("Large") || x.Name.StartsWith("Deep")); } private static IEnumerable> 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); } } } } } }