152 lines
6.6 KiB
C#
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;
|
|
});
|
|
}
|
|
}
|
|
}
|