#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 XplorePlane.ViewModels; using Xunit; namespace XplorePlane.Tests.Services { /// /// Property 6: 采集图像传递给紧邻的 InspectionModuleNode /// Feature: cnc-multi-position-image-capture /// **Validates: Requirements 3.2** /// /// For any CNC program where a SavePositionNode is immediately followed (by Index order) /// by an InspectionModuleNode, the image acquired at that SavePositionNode SHALL be passed /// as the source image to the InspectionModuleNode's pipeline execution. /// public class ImagePassingPropertyTests { // ── Helper: Create a 1x1 BitmapSource for testing ────────────────────── private static BitmapSource CreateTestBitmap() { var bitmap = new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgr32, null); bitmap.Freeze(); return bitmap; } // ── Generator: CNC programs with SavePositionNodes optionally followed by InspectionModuleNodes ── private static Gen NonEmptyStringGen => ArbMap.Default.GeneratorFor().Select(s => s.Get); private static Gen PipelineGen => from name in NonEmptyStringGen select new PipelineModel { Name = name, Nodes = new List { new PipelineNodeModel { OperatorKey = "test_op", Order = 0, IsEnabled = true } } }; /// /// Generates a CNC program where some SavePositionNodes are followed by InspectionModuleNodes /// and some are not. Returns the program along with metadata about which pairs exist. /// private static Gen CncProgramWithMixedFollowersGen() { return from saveCount in Gen.Choose(1, 5) from followFlags in Gen.ListOf(ArbMap.Default.GeneratorFor(), saveCount) from programName in NonEmptyStringGen let nodes = BuildMixedNodes(saveCount, followFlags) select new CncProgram( Guid.NewGuid(), programName, DateTime.UtcNow, DateTime.UtcNow, nodes.AsReadOnly()); } private static List BuildMixedNodes(int saveCount, IReadOnlyList followFlags) { var nodes = new List(); int index = 0; for (int i = 0; i < saveCount; i++) { // Add a SavePositionNode var spNode = new SavePositionNode( Guid.NewGuid(), index, $"Position_{i}", MotionState.Default, SaveImage: false, ManualImagePath: ""); nodes.Add(spNode); index++; // Optionally follow with an InspectionModuleNode if (i < followFlags.Count && followFlags[i]) { var pipeline = new PipelineModel { Name = $"Pipeline_{i}", Nodes = new List { new PipelineNodeModel { OperatorKey = "test_op", Order = 0, IsEnabled = true } } }; var inspNode = new InspectionModuleNode( Guid.NewGuid(), index, $"Inspect_{i}", pipeline); nodes.Add(inspNode); index++; } } return nodes; } // ── Service Factory ────────────────────────────────────────────────────── private static ( CncExecutionService Service, Mock PipelineExec, Mock MainViewport, Mock ImagePersistence, Mock Logger) CreateServiceWithCapture(BitmapSource detectorImage) { 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()); // Return the detector image from the viewport service (simulates detector acquisition) mockMainViewportService .Setup(m => m.CurrentDisplayImage) .Returns(detectorImage); 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); mockImagePersistenceService .Setup(s => s.SaveImageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ImageSaveResult(true, "test.bmp", 100)); 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, mockPipelineExecutionService, mockMainViewportService, mockImagePersistenceService, mockLogger); } // ── Property 6: 采集图像传递给紧邻的 InspectionModuleNode ────────────── // Feature: cnc-multi-position-image-capture, Property 6: 采集图像传递给紧邻的 InspectionModuleNode // Validates: Requirements 3.2 [Property(MaxTest = 100)] public Property AcquiredImage_PassedToFollowingInspectionModuleNode() { return Prop.ForAll( CncProgramWithMixedFollowersGen().ToArbitrary(), program => { var detectorImage = CreateTestBitmap(); var (service, mockPipelineExec, _, _, _) = CreateServiceWithCapture(detectorImage); // Track all pipeline execution calls with their source images var pipelineCalls = new List(); mockPipelineExec .Setup(p => p.ExecutePipelineAsync( It.IsAny>(), It.IsAny(), It.IsAny>(), It.IsAny())) .Callback, BitmapSource, IProgress, CancellationToken>( (nodes, source, progress, ct) => { pipelineCalls.Add(source); }) .ReturnsAsync(new PipelineExecutionResult(detectorImage, null)); service.ExecuteAsync(program, null, CancellationToken.None) .GetAwaiter().GetResult(); // Determine expected behavior from the program structure var allNodesOrdered = program.Nodes.OrderBy(n => n.Index).ToList(); var savePositionNodes = allNodesOrdered.OfType().ToList(); // Count SavePositionNodes that are immediately followed by InspectionModuleNode int savePositionFollowedByInspection = 0; foreach (var sp in savePositionNodes) { int spOrderIndex = allNodesOrdered.FindIndex(n => n.Id == sp.Id); if (spOrderIndex >= 0 && spOrderIndex + 1 < allNodesOrdered.Count) { var nextNode = allNodesOrdered[spOrderIndex + 1]; if (nextNode is InspectionModuleNode) savePositionFollowedByInspection++; } } // Property verification: // 1. When a SavePositionNode is followed by InspectionModuleNode, pipeline MUST be called // at least once with the detector image (from the multi-position loop) // 2. All pipeline calls must receive the detector image as source // (since all images come from the same detector mock) // If there are SavePositionNodes followed by InspectionModuleNodes, // the pipeline must have been called at least that many times bool pipelineCalledForFollowedNodes = pipelineCalls.Count >= savePositionFollowedByInspection; // Every pipeline call must have received the detector image bool allReceivedCorrectImage = pipelineCalls.All(img => ReferenceEquals(img, detectorImage)); return pipelineCalledForFollowedNodes && allReceivedCorrectImage; }); } // ── Property 6 (negative case): SavePositionNode NOT followed by InspectionModuleNode ── // Feature: cnc-multi-position-image-capture, Property 6 (negative): No pipeline execution for non-followed SavePositionNodes // Validates: Requirements 3.2 [Property(MaxTest = 100)] public Property NoPipelineExecution_WhenNoFollowingInspectionModuleNode() { // Generate programs where NO SavePositionNode is followed by an InspectionModuleNode var gen = from saveCount in Gen.Choose(1, 5) from programName in NonEmptyStringGen let nodes = BuildNodesWithoutFollowingInspection(saveCount) select new CncProgram( Guid.NewGuid(), programName, DateTime.UtcNow, DateTime.UtcNow, nodes.AsReadOnly()); return Prop.ForAll( gen.ToArbitrary(), program => { var detectorImage = CreateTestBitmap(); var (service, mockPipelineExec, _, _, _) = CreateServiceWithCapture(detectorImage); int pipelineCallCount = 0; mockPipelineExec .Setup(p => p.ExecutePipelineAsync( It.IsAny>(), It.IsAny(), It.IsAny>(), It.IsAny())) .Callback, BitmapSource, IProgress, CancellationToken>( (_, _, _, _) => Interlocked.Increment(ref pipelineCallCount)) .ReturnsAsync(new PipelineExecutionResult(detectorImage, null)); service.ExecuteAsync(program, null, CancellationToken.None) .GetAwaiter().GetResult(); // No pipeline execution should occur for SavePositionNodes // (InspectionModuleNodes in the general loop may still trigger pipeline, // but since we only have SavePositionNodes and non-inspection nodes, count should be 0) return pipelineCallCount == 0; }); } private static List BuildNodesWithoutFollowingInspection(int saveCount) { var nodes = new List(); int index = 0; for (int i = 0; i < saveCount; i++) { // Add a SavePositionNode nodes.Add(new SavePositionNode( Guid.NewGuid(), index, $"Position_{i}", MotionState.Default, SaveImage: false, ManualImagePath: "")); index++; // Follow with a non-inspection node (e.g., WaitDelayNode with 0ms) nodes.Add(new WaitDelayNode( Guid.NewGuid(), index, $"Wait_{i}", 0)); index++; } return nodes; } } }