Files
XplorePlane/XplorePlane.Tests/Models/SummarySerializationPropertyTests.cs
T

152 lines
6.6 KiB
C#

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
{
/// <summary>
/// FsCheck property-based tests for BatchCaptureResult JSON serialization round-trip.
/// Property 10: 摘要 JSON 序列化往返
/// </summary>
public class SummarySerializationPropertyTests
{
// ── Generators ───────────────────────────────────────────────────
/// <summary>
/// Generates a non-null string (FsCheck may generate nulls by default).
/// </summary>
private static Gen<string> NonNullStringGen =>
ArbMap.Default.GeneratorFor<NonEmptyString>()
.Select(s => s.Get)
.Or(Gen.Constant(string.Empty));
/// <summary>
/// Generates a nullable string (either null or a non-empty string).
/// </summary>
private static Gen<string> NullableStringGen =>
Gen.OneOf(
Gen.Constant<string>(null),
ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get));
/// <summary>
/// Generates a random PositionResult with valid field values.
/// </summary>
private static Gen<PositionResult> 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
};
/// <summary>
/// Generates a random BatchCaptureResult with valid field values.
/// </summary>
private static Gen<BatchCaptureResult> 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<int?>(null),
Gen.Choose(0, 50).Select(i => (int?)i))
from notExecutedAfterCancel in Gen.OneOf(
Gen.Constant<int?>(null),
Gen.Choose(0, 50).Select(i => (int?)i))
from positionCount in Gen.Choose(0, 5)
from positions in Gen.ListOf<PositionResult>(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 ─────────────────────────────────────────────────
/// <summary>
/// **Validates: Requirements 4.3**
///
/// For any valid BatchCaptureResult object, serializing to JSON and
/// deserializing back produces an object with identical field values.
/// </summary>
[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<BatchCaptureResult>(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;
});
}
}
}