using System; using System.Threading; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; using Prism.Events; using XP.Common.Logging.Interfaces; using XP.Hardware.Detector.Abstractions; using XP.Hardware.Detector.Abstractions.Events; using XplorePlane.Services.MainViewport; namespace XplorePlane.Tests.Services { /// /// FsCheck property-based tests for DetectorFramePipelineService. /// Properties 6–8 validate queue bounds and sampling correctness. /// public class DetectorFramePipelineServicePropertyTests { // ── Helpers ────────────────────────────────────────────────────────── /// /// Creates a DetectorFramePipelineService with the given capacity/sampling config. /// Uses the internal test constructor to bypass App.config reads. /// Uses a real EventAggregator so we can publish ImageCapturedEvent to drive the service. /// private static (DetectorFramePipelineService Service, EventAggregator EventAggregator) CreateService(int acquireCapacity = 16, int processCapacity = 8, int processEveryN = 1) { var mockLogger = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); var mockMainViewport = new Mock(); var eventAggregator = new EventAggregator(); var service = new DetectorFramePipelineService( eventAggregator, mockMainViewport.Object, mockLogger.Object, acquireCapacity, processCapacity, processEveryN); return (service, eventAggregator); } /// /// Publishes M frames via the EventAggregator and waits for background processing. /// private static void PublishFrames(EventAggregator eventAggregator, int frameCount, int width = 4, int height = 4) { for (int i = 0; i < frameCount; i++) { var args = new ImageCapturedEventArgs { ImageData = new ushort[width * height], Width = (uint)width, Height = (uint)height, FrameNumber = i + 1, CaptureTime = DateTime.UtcNow }; eventAggregator.GetEvent().Publish(args); } // Wait for background thread processing to complete. // The subscription runs on BackgroundThread, so we need a brief wait. Thread.Sleep(300); } // ── Property 6 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 6: 采集队列有界不变量 // Validates: Requirements 2.2 [Property(MaxTest = 20)] public Property AcquireQueueCount_NeverExceedsCapacity() { var gen = from capacity in Gen.Choose(1, 10) from frameCount in Gen.Choose(1, 100) select (capacity, frameCount); return Prop.ForAll( gen.ToArbitrary(), tuple => { var (capacity, frameCount) = tuple; var (service, eventAggregator) = CreateService(acquireCapacity: capacity); try { PublishFrames(eventAggregator, frameCount); return service.AcquireQueueCount <= service.AcquireQueueCapacity; } finally { service.Dispose(); } }); } // ── Property 7 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 7: 处理队列有界不变量 // Validates: Requirements 2.4 [Property(MaxTest = 20)] public Property ProcessQueueCount_NeverExceedsCapacity() { var gen = from capacity in Gen.Choose(1, 10) from frameCount in Gen.Choose(1, 100) select (capacity, frameCount); return Prop.ForAll( gen.ToArbitrary(), tuple => { var (capacity, frameCount) = tuple; var (service, eventAggregator) = CreateService(processCapacity: capacity); try { PublishFrames(eventAggregator, frameCount); return service.ProcessQueueCount <= service.ProcessQueueCapacity; } finally { service.Dispose(); } }); } // ── Property 8 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 8: 隔帧抽样正确性 // Validates: Requirements 2.3 // // For any N (ProcessEveryNFrames) and M frames, the number of frames entering // the process queue equals ceil(M / N). // // We use a large process queue capacity to avoid overflow dropping frames, // and count frames via ProcessFrameDequeued events. [Property(MaxTest = 20)] public Property ProcessQueueEntries_EqualsCeilMDivN() { var gen = from n in Gen.Choose(1, 10) from m in Gen.Choose(1, 50) select (n, m); return Prop.ForAll( gen.ToArbitrary(), tuple => { var (n, m) = tuple; // Use a large process queue capacity so no frames are dropped due to overflow var (service, eventAggregator) = CreateService( acquireCapacity: 200, processCapacity: 200, processEveryN: n); int dequeuedCount = 0; using var allDequeued = new SemaphoreSlim(0); int expected = (int)Math.Ceiling((double)m / n); service.ProcessFrameDequeued += (_, __) => { int count = Interlocked.Increment(ref dequeuedCount); if (count >= expected) allDequeued.Release(); }; try { PublishFrames(eventAggregator, m); // Wait up to 5 seconds for all expected frames to be dequeued bool completed = allDequeued.Wait(TimeSpan.FromSeconds(5)); // If we didn't get the expected count, give a bit more time if (!completed) Thread.Sleep(500); return dequeuedCount == expected; } finally { service.Dispose(); } }); } } }