209 lines
8.7 KiB
C#
209 lines
8.7 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// FsCheck property-based tests for MainViewportService.
|
|
/// Uses the real MainViewportService (no mocking of the service itself).
|
|
/// </summary>
|
|
public class MainViewportServicePropertyTests
|
|
{
|
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
private static MainViewportService CreateService()
|
|
{
|
|
var mockLogger = new Mock<ILoggerService>();
|
|
mockLogger.Setup(l => l.ForModule<MainViewportService>()).Returns(mockLogger.Object);
|
|
return new MainViewportService(mockLogger.Object);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a frozen BitmapSource suitable for cross-thread use in tests.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generator for DetectorFrame with a frozen BitmapSource.
|
|
/// </summary>
|
|
private static Gen<DetectorFrame> 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<DetectorFrame>(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<DetectorFrame>)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;
|
|
});
|
|
}
|
|
}
|
|
}
|