#pragma warning disable xUnit1031 // FsCheck property tests require synchronous execution via GetAwaiter().GetResult()
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport;
using Xunit;
namespace XplorePlane.Tests.Services
{
///
/// Property 5: 多位置按 Index 升序执行
/// **Validates: Requirements 3.1**
///
/// For any CNC program containing multiple SavePositionNodes with arbitrary Index values,
/// the CNC_Execution_Service SHALL process them in strictly ascending Index order,
/// and progress reports SHALL reflect this ordering.
///
public class ExecutionOrderPropertyTests
{
///
/// Creates a CncExecutionService with all dependencies mocked.
/// The MainViewportService returns a valid BitmapSource so execution proceeds.
///
private static (CncExecutionService Service, Mock MainViewport) CreateServiceWithImage()
{
var mockStore = new Mock();
var mockLogger = new Mock();
var mockMainViewportService = new Mock();
var mockAppStateService = new Mock();
var mockPipelineExecutionService = new Mock();
var mockImageProcessingService = new Mock();
var mockEventAggregator = new Mock();
var mockImagePersistenceService = new Mock();
// Logger setup
mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object);
// EventAggregator setup - prevent NRE on constructor subscription
mockEventAggregator
.Setup(ea => ea.GetEvent())
.Returns(new DetectorDisconnectedEvent());
// InspectionResultStore setup
mockStore.Setup(s => s.BeginRunAsync(
It.IsAny(),
It.IsAny()))
.Returns(Task.CompletedTask);
mockStore.Setup(s => s.AppendNodeResultAsync(
It.IsAny(),
It.IsAny>(),
It.IsAny(),
It.IsAny>()))
.Returns(Task.CompletedTask);
mockStore.Setup(s => s.CompleteRunAsync(
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Returns(Task.CompletedTask);
// Provide a valid BitmapSource from the viewport so image acquisition succeeds
var dummyBitmap = BitmapSource.Create(
1, 1, 96, 96, PixelFormats.Gray8, null, new byte[] { 128 }, 1);
dummyBitmap.Freeze();
mockMainViewportService
.Setup(m => m.LatestManualImage)
.Returns(dummyBitmap);
// ImagePersistenceService - return success for SaveImageAsync
mockImagePersistenceService
.Setup(s => s.SaveImageAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(new ImageSaveResult(true, "C:\\test\\image.bmp", 1024));
mockImagePersistenceService
.Setup(s => s.WriteSummaryAsync(
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(true);
var service = new CncExecutionService(
mockStore.Object,
mockLogger.Object,
mockMainViewportService.Object,
mockAppStateService.Object,
mockPipelineExecutionService.Object,
mockImageProcessingService.Object,
mockEventAggregator.Object,
mockImagePersistenceService.Object);
return (service, mockMainViewportService);
}
///
/// FsCheck generator for a list of SavePositionNodes with random, unique Index values.
/// Generates 2-10 nodes with shuffled indices to ensure non-sequential ordering in the input.
///
private static Arbitrary> SavePositionNodesArb()
{
var gen =
from count in Gen.Choose(2, 10)
from indices in Gen.Shuffle(Enumerable.Range(0, 100).ToArray())
.Select(arr => arr.Take(count).ToList())
select indices.Select(idx =>
new SavePositionNode(
Guid.NewGuid(),
idx,
$"Position_{idx}",
MotionState.Default,
SaveImage: false,
ManualImagePath: ""))
.ToList();
return gen.ToArbitrary();
}
// Feature: cnc-multi-position-image-capture, Property 5: 多位置按 Index 升序执行
// **Validates: Requirements 3.1**
[Property(MaxTest = 100)]
public Property MultiPosition_ExecutedInStrictlyAscendingIndexOrder()
{
return Prop.ForAll(
SavePositionNodesArb(),
savePositionNodes =>
{
var (service, _) = CreateServiceWithImage();
// Build a CncProgram containing only the generated SavePositionNodes
// (in their original random order - the service should sort by Index)
var program = new CncProgram(
Guid.NewGuid(),
"TestProgram",
DateTime.UtcNow,
DateTime.UtcNow,
savePositionNodes.Cast().ToList().AsReadOnly());
// Capture progress reports to verify execution order
var reportedPositionIndices = new List();
var reportedNodeIds = new List();
var progress = new SynchronousProgress(p =>
{
// Capture the first Running report for each node (the initial progress report)
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
{
reportedNodeIds.Add(p.NodeId);
reportedPositionIndices.Add(p.PositionIndex.Value);
}
});
service.ExecuteAsync(program, progress, CancellationToken.None)
.GetAwaiter().GetResult();
// Build a map from NodeId to Index
var nodeIndexMap = savePositionNodes.ToDictionary(n => n.Id, n => n.Index);
// Verify 1: All nodes were executed
if (reportedNodeIds.Count != savePositionNodes.Count)
return false;
// Verify 2: Nodes were processed in strictly ascending Index order
var executedIndices = reportedNodeIds
.Select(id => nodeIndexMap[id])
.ToList();
for (int i = 1; i < executedIndices.Count; i++)
{
if (executedIndices[i] <= executedIndices[i - 1])
return false;
}
// Verify 3: Progress report PositionIndex values are strictly ascending from 0
for (int i = 0; i < reportedPositionIndices.Count; i++)
{
if (reportedPositionIndices[i] != i)
return false;
}
return true;
});
}
}
}