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
{
///
/// FsCheck property-based tests for DetectorFramePipelineService.
/// Properties 6–8 validate queue bounds and sampling correctness.
///
public class DetectorFramePipelineServicePropertyTests
{
// ── Helpers ──────────────────────────────────────────────────────────
///
/// 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.
///
private static (DetectorFramePipelineService Service, EventAggregator EventAggregator)
CreateService(int acquireCapacity = 16, int processCapacity = 8, int processEveryN = 1)
{
var mockLogger = new Mock();
mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object);
var mockMainViewport = new Mock();
var eventAggregator = new EventAggregator();
var service = new DetectorFramePipelineService(
eventAggregator,
mockMainViewport.Object,
mockLogger.Object,
acquireCapacity,
processCapacity,
processEveryN);
return (service, eventAggregator);
}
///
/// Publishes M frames via the EventAggregator and waits for background processing.
///
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().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();
}
});
}
}
}