双队列的打通 实时按钮的切换 补充测试用例

This commit is contained in:
zhengxuan.zhang
2026-05-06 23:25:37 +08:00
parent bd9b24beb1
commit d56caf1ab5
5 changed files with 731 additions and 0 deletions
@@ -0,0 +1,101 @@
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.MainViewport;
namespace XplorePlane.Tests.Services
{
/// <summary>
/// FsCheck property-based tests for DetectorFramePipelineService configuration correction.
/// Property 11 validates that values &lt;= 0 are corrected to 1.
/// </summary>
public class ConfigCorrectionPropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
private static DetectorFramePipelineService CreateServiceWithConfig(
int acquireCapacity,
int processCapacity,
int processEveryN)
{
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<DetectorFramePipelineService>()).Returns(mockLogger.Object);
var mockMainViewport = new Mock<IMainViewportService>();
var eventAggregator = new EventAggregator();
return new DetectorFramePipelineService(
eventAggregator,
mockMainViewport.Object,
mockLogger.Object,
acquireCapacity,
processCapacity,
processEveryN);
}
// ── Property 11 ──────────────────────────────────────────────────────
// Feature: live-image-display, Property 11: 配置值下界修正
// Validates: Requirements 6.3, 6.4
//
// For any integer config value <= 0 (ProcessEveryNFrames, AcquireQueueCapacity,
// ProcessQueueCapacity), DetectorFramePipelineService corrects it to 1.
[Property(MaxTest = 100)]
public Property InvalidConfigValues_AreCorrectedToOne()
{
// Generate values <= 0 (including 0 and negative integers)
var nonPositiveGen = Gen.Choose(int.MinValue / 2, 0);
var gen =
from acquireCapacity in nonPositiveGen
from processCapacity in nonPositiveGen
from processEveryN in nonPositiveGen
select (acquireCapacity, processCapacity, processEveryN);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (acquireCapacity, processCapacity, processEveryN) = tuple;
using var service = CreateServiceWithConfig(acquireCapacity, processCapacity, processEveryN);
// All three values must be corrected to 1
bool acquireCorrected = service.AcquireQueueCapacity == 1;
bool processCorrected = service.ProcessQueueCapacity == 1;
bool everyNCorrected = service.ProcessEveryNFrames == 1;
return acquireCorrected && processCorrected && everyNCorrected;
});
}
// Additional property: valid positive values are preserved as-is
[Property(MaxTest = 100)]
public Property ValidConfigValues_ArePreservedAsIs()
{
var positiveGen = Gen.Choose(1, 1000);
var gen =
from acquireCapacity in positiveGen
from processCapacity in positiveGen
from processEveryN in positiveGen
select (acquireCapacity, processCapacity, processEveryN);
return Prop.ForAll(
gen.ToArbitrary(),
tuple =>
{
var (acquireCapacity, processCapacity, processEveryN) = tuple;
using var service = CreateServiceWithConfig(acquireCapacity, processCapacity, processEveryN);
return service.AcquireQueueCapacity == acquireCapacity
&& service.ProcessQueueCapacity == processCapacity
&& service.ProcessEveryNFrames == processEveryN;
});
}
}
}
@@ -0,0 +1,194 @@
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
{
/// <summary>
/// FsCheck property-based tests for DetectorFramePipelineService.
/// Properties 68 validate queue bounds and sampling correctness.
/// </summary>
public class DetectorFramePipelineServicePropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
private static (DetectorFramePipelineService Service, EventAggregator EventAggregator)
CreateService(int acquireCapacity = 16, int processCapacity = 8, int processEveryN = 1)
{
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<DetectorFramePipelineService>()).Returns(mockLogger.Object);
var mockMainViewport = new Mock<IMainViewportService>();
var eventAggregator = new EventAggregator();
var service = new DetectorFramePipelineService(
eventAggregator,
mockMainViewport.Object,
mockLogger.Object,
acquireCapacity,
processCapacity,
processEveryN);
return (service, eventAggregator);
}
/// <summary>
/// Publishes M frames via the EventAggregator and waits for background processing.
/// </summary>
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<ImageCapturedEvent>().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 = 100)]
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 = 100)]
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 = 100)]
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();
}
});
}
}
}
@@ -0,0 +1,208 @@
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;
});
}
}
}