195 lines
7.6 KiB
C#
195 lines
7.6 KiB
C#
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 6–8 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 = 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();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|