using System.Collections.Generic; using System.Linq; using System.Text.Json; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using XplorePlane.Models; namespace XplorePlane.Tests.Models { /// /// FsCheck property-based tests for BatchCaptureResult JSON serialization round-trip. /// Property 10: 摘要 JSON 序列化往返 /// public class SummarySerializationPropertyTests { // ── Generators ─────────────────────────────────────────────────── /// /// Generates a non-null string (FsCheck may generate nulls by default). /// private static Gen NonNullStringGen => ArbMap.Default.GeneratorFor() .Select(s => s.Get) .Or(Gen.Constant(string.Empty)); /// /// Generates a nullable string (either null or a non-empty string). /// private static Gen NullableStringGen => Gen.OneOf( Gen.Constant(null), ArbMap.Default.GeneratorFor().Select(s => s.Get)); /// /// Generates a random PositionResult with valid field values. /// private static Gen PositionResultGen => from nodeName in NonNullStringGen from nodeIndex in Gen.Choose(0, 1000) from status in Gen.Elements("Success", "Failed") from errorMessage in NullableStringGen from imagePath in NullableStringGen select new PositionResult { NodeName = nodeName, NodeIndex = nodeIndex, Status = status, ErrorMessage = errorMessage, ImagePath = imagePath }; /// /// Generates a random BatchCaptureResult with valid field values. /// private static Gen BatchCaptureResultGen => from programName in NonNullStringGen from startTime in NonNullStringGen from durationSeconds in Gen.Choose(0, 100000).Select(i => i / 100.0) from totalPositions in Gen.Choose(0, 50) from succeededPositions in Gen.Choose(0, 50) from failedPositions in Gen.Choose(0, 50) from savedImageCount in Gen.Choose(0, 100) from status in Gen.Elements("Completed", "Cancelled") from completedBeforeCancel in Gen.OneOf( Gen.Constant(null), Gen.Choose(0, 50).Select(i => (int?)i)) from notExecutedAfterCancel in Gen.OneOf( Gen.Constant(null), Gen.Choose(0, 50).Select(i => (int?)i)) from positionCount in Gen.Choose(0, 5) from positions in Gen.ListOf(PositionResultGen, positionCount) select new BatchCaptureResult { ProgramName = programName, StartTime = startTime, DurationSeconds = durationSeconds, TotalPositions = totalPositions, SucceededPositions = succeededPositions, FailedPositions = failedPositions, SavedImageCount = savedImageCount, Status = status, CompletedBeforeCancel = completedBeforeCancel, NotExecutedAfterCancel = notExecutedAfterCancel, Positions = positions.ToList() }; // ── Property 10 ───────────────────────────────────────────────── /// /// **Validates: Requirements 4.3** /// /// For any valid BatchCaptureResult object, serializing to JSON and /// deserializing back produces an object with identical field values. /// [Property(MaxTest = 100)] public Property BatchCaptureResult_JsonRoundTrip_PreservesAllFields() { return Prop.ForAll( BatchCaptureResultGen.ToArbitrary(), original => { // Serialize to JSON var json = JsonSerializer.Serialize(original); // Deserialize back var deserialized = JsonSerializer.Deserialize(json); // Verify all scalar properties bool scalarFieldsMatch = deserialized.ProgramName == original.ProgramName && deserialized.StartTime == original.StartTime && deserialized.DurationSeconds == original.DurationSeconds && deserialized.TotalPositions == original.TotalPositions && deserialized.SucceededPositions == original.SucceededPositions && deserialized.FailedPositions == original.FailedPositions && deserialized.SavedImageCount == original.SavedImageCount && deserialized.Status == original.Status; // Verify nullable int properties bool nullableFieldsMatch = deserialized.CompletedBeforeCancel == original.CompletedBeforeCancel && deserialized.NotExecutedAfterCancel == original.NotExecutedAfterCancel; // Verify list property count bool listCountMatch = deserialized.Positions.Count == original.Positions.Count; // Verify each position in the list bool positionsMatch = true; for (int i = 0; i < original.Positions.Count; i++) { var orig = original.Positions[i]; var deser = deserialized.Positions[i]; if (deser.NodeName != orig.NodeName || deser.NodeIndex != orig.NodeIndex || deser.Status != orig.Status || deser.ErrorMessage != orig.ErrorMessage || deser.ImagePath != orig.ImagePath) { positionsMatch = false; break; } } return scalarFieldsMatch && nullableFieldsMatch && listCountMatch && positionsMatch; }); } } }