#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 7: 进度报告包含正确的索引和总数 /// **Validates: Requirements 3.4** /// /// For any CNC program with N SavePositionNodes, the CNC_Execution_Service SHALL report /// progress N times (or fewer if cancelled), each report containing a 0-based position index /// and the total count N, with indices strictly increasing from 0. /// public class ProgressReportPropertyTests { 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(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); mockEventAggregator .Setup(ea => ea.GetEvent()) .Returns(new DetectorDisconnectedEvent()); 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 image so that image acquisition succeeds var dummyImage = CreateDummyBitmap(); mockMainViewportService.SetupGet(m => m.LatestManualImage).Returns((ImageSource)null); mockMainViewportService.SetupGet(m => m.CurrentDisplayImage).Returns(dummyImage); // Image persistence returns success 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); } private static BitmapSource CreateDummyBitmap() { var stride = 4 * 4; // 4 pixels wide, 4 bytes per pixel var pixels = new byte[stride * 4]; // 4x4 image var bitmap = BitmapSource.Create(4, 4, 96, 96, PixelFormats.Bgra32, null, pixels, stride); bitmap.Freeze(); return bitmap; } /// /// Generates a CncProgram with N SavePositionNodes (N between 1 and 10), /// each with unique ascending indices. /// private static Arbitrary SavePositionProgramArb() { var gen = from count in Gen.Choose(1, 10) from name in ArbMap.Default.GeneratorFor().Select(s => s.Get) from nodes in GenSavePositionNodes(count) select new CncProgram( Guid.NewGuid(), name, DateTime.UtcNow, DateTime.UtcNow, nodes.AsReadOnly()); return gen.ToArbitrary(); } private static Gen> GenSavePositionNodes(int count) { Gen> acc = Gen.Constant(new List()); for (int i = 0; i < count; i++) { var idx = i; acc = from list in acc from nodeName in ArbMap.Default.GeneratorFor().Select(s => s.Get) let node = new SavePositionNode( Guid.NewGuid(), idx, $"Pos_{nodeName}_{idx}", MotionState.Default, SaveImage: false) select new List(list) { node }; } return acc; } // ── Property 7: 进度报告包含正确的索引和总数 ────────────────────────── // Feature: cnc-multi-position-image-capture, Property 7: 进度报告包含正确的索引和总数 // Validates: Requirements 3.4 [Property(MaxTest = 100)] public Property ProgressReports_ContainCorrectIndexAndTotal() { return Prop.ForAll( SavePositionProgramArb(), program => { var (service, _) = CreateServiceWithImage(); var savePositionCount = program.Nodes.OfType().Count(); // Capture all progress reports that have PositionIndex set (Running state) var runningReports = new List<(int PositionIndex, int TotalPositions)>(); var progress = new SynchronousProgress(p => { if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue && p.TotalPositions.HasValue) { runningReports.Add((p.PositionIndex.Value, p.TotalPositions.Value)); } }); service.ExecuteAsync(program, progress, CancellationToken.None) .GetAwaiter().GetResult(); // Verify: at least N Running progress reports (one per position) if (runningReports.Count < savePositionCount) return false; // Verify: all TotalPositions values equal N if (runningReports.Any(r => r.TotalPositions != savePositionCount)) return false; // Verify: PositionIndex values in Running reports are strictly increasing from 0 // Extract unique position indices in order of first appearance var seenIndices = new List(); foreach (var report in runningReports) { if (seenIndices.Count == 0 || seenIndices.Last() != report.PositionIndex) seenIndices.Add(report.PositionIndex); } // Must have exactly N distinct position indices if (seenIndices.Count != savePositionCount) return false; // Indices must be 0, 1, 2, ..., N-1 for (int i = 0; i < savePositionCount; i++) { if (seenIndices[i] != i) return false; } return true; }); } } }