#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; }); } } }