Files
XplorePlane/XplorePlane.Tests/Services/DetectorFramePipelineServicePropertyTests.cs
T
2026-05-18 15:32:00 +08:00

195 lines
7.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 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();
}
});
}
}
}