using System; using System.Windows.Media; using System.Windows.Media.Imaging; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; using XP.Common.Logging.Interfaces; using XplorePlane.Services.MainViewport; namespace XplorePlane.Tests.Services { /// /// FsCheck property-based tests for MainViewportService. /// Uses the real MainViewportService (no mocking of the service itself). /// public class MainViewportServicePropertyTests { // ── Helpers ────────────────────────────────────────────────────────── private static MainViewportService CreateService() { var mockLogger = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); return new MainViewportService(mockLogger.Object); } /// /// Creates a frozen BitmapSource suitable for cross-thread use in tests. /// private static BitmapSource CreateFrozenBitmap(int width = 4, int height = 4) { var pixels = new byte[width * height * 4]; var bitmap = BitmapSource.Create( width, height, 96, 96, PixelFormats.Bgra32, null, pixels, width * 4); bitmap.Freeze(); return bitmap; } /// /// Generator for DetectorFrame with a frozen BitmapSource. /// private static Gen DetectorFrameGen => from frameId in Gen.Choose(1, 100000).Select(i => (long)i) from width in Gen.Choose(1, 64) from height in Gen.Choose(1, 64) select new DetectorFrame( frameId: frameId, captureTime: DateTime.UtcNow, width: width, height: height, rawPixels: new ushort[width * height], previewImage: CreateFrozenBitmap(width, height)); // ── Property 1 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 1: 实时模式下帧更新触发显示刷新 // Validates: Requirements 3.2 [Property(MaxTest = 100)] public Property LiveDetector_RealtimeEnabled_UpdateDetectorFrame_UpdatesDisplayAndRaisesEvent() { return Prop.ForAll( DetectorFrameGen.ToArbitrary(), frame => { var service = CreateService(); // Ensure we are in LiveDetector mode with realtime enabled (default state) service.SetSourceMode(MainViewportSourceMode.LiveDetector); service.SetRealtimeDisplayEnabled(true); bool stateChangedRaised = false; service.StateChanged += (_, __) => stateChangedRaised = true; service.UpdateDetectorFrame(frame); bool imageMatches = ReferenceEquals(service.CurrentDisplayImage, frame.PreviewImage); bool eventRaised = stateChangedRaised; return imageMatches && eventRaised; }); } // ── Property 2 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 2: 实时关闭时帧更新不触发显示刷新 // Validates: Requirements 3.3 [Property(MaxTest = 100)] public Property LiveDetector_RealtimeDisabled_UpdateDetectorFrame_NoEventAndImageUnchanged() { return Prop.ForAll( DetectorFrameGen.ToArbitrary(), frame => { var service = CreateService(); // Set up: LiveDetector mode, realtime disabled service.SetSourceMode(MainViewportSourceMode.LiveDetector); service.SetRealtimeDisplayEnabled(false); // Capture the display image before the update var imageBefore = service.CurrentDisplayImage; bool stateChangedRaised = false; service.StateChanged += (_, __) => stateChangedRaised = true; service.UpdateDetectorFrame(frame); bool noEvent = !stateChangedRaised; bool imageUnchanged = ReferenceEquals(service.CurrentDisplayImage, imageBefore); return noEvent && imageUnchanged; }); } // ── Property 3 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 3: ManualImage 模式下探测器帧不覆盖显示图像 // Validates: Requirements 3.4 [Property(MaxTest = 100)] public Property ManualImage_MultipleUpdateDetectorFrame_DisplayAlwaysManualImage() { var gen = from frameCount in Gen.Choose(1, 10) from frames in Gen.ListOf(DetectorFrameGen, frameCount) select frames; return Prop.ForAll( gen.ToArbitrary(), frames => { var service = CreateService(); // Set a manual image first var manualImage = CreateFrozenBitmap(8, 8); service.SetManualImage(manualImage, "test.png"); // Verify we are in ManualImage mode if (service.CurrentSourceMode != MainViewportSourceMode.ManualImage) return false; // Call UpdateDetectorFrame multiple times foreach (var frame in (System.Collections.Generic.IEnumerable)frames) { service.UpdateDetectorFrame(frame); } // CurrentDisplayImage must still be the manual image return ReferenceEquals(service.CurrentDisplayImage, manualImage); }); } // ── Property 4 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 4: CNC 运行时 SetManualImage 被忽略 // Validates: Requirements 3.10 [Property(MaxTest = 100)] public Property CncRunning_SetManualImage_SourceModeRemainsLiveDetector() { return Prop.ForAll( DetectorFrameGen.ToArbitrary(), frame => { var service = CreateService(); // Start CNC running service.SetCncRunning(true); // Attempt to set a manual image var manualImage = CreateFrozenBitmap(4, 4); service.SetManualImage(manualImage, "manual.png"); // CurrentSourceMode must remain LiveDetector return service.CurrentSourceMode == MainViewportSourceMode.LiveDetector; }); } // ── Property 5 ─────────────────────────────────────────────────────── // Feature: live-image-display, Property 5: CNC 运行时无法关闭实时刷新 // Validates: Requirements 3.11 [Property(MaxTest = 100)] public Property CncRunning_SetRealtimeDisplayEnabledFalse_IsRealtimeDisplayEnabledRemainsTrue() { // No generator needed — fixed input return Prop.ForAll( Gen.Constant(true).ToArbitrary(), _ => { var service = CreateService(); // Ensure realtime is enabled first service.SetRealtimeDisplayEnabled(true); // Start CNC running service.SetCncRunning(true); // Attempt to disable realtime display service.SetRealtimeDisplayEnabled(false); // IsRealtimeDisplayEnabled must remain true return service.IsRealtimeDisplayEnabled; }); } } }