#pragma warning disable xUnit1031 // FsCheck property tests require synchronous execution via GetAwaiter().GetResult()
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport;
using Xunit;
namespace XplorePlane.Tests.Services
{
///
/// Property 7: 进度报告包含正确的索引和总数
/// **Validates: Requirements 3.4**
///
/// For any CNC program with N SavePositionNodes, the CNC_Execution_Service SHALL report
/// progress N times (or fewer if cancelled), each report containing a 0-based position index
/// and the total count N, with indices strictly increasing from 0.
///
public class ProgressReportPropertyTests
{
private static (CncExecutionService Service, Mock MainViewport)
CreateServiceWithImage()
{
var mockStore = new Mock();
var mockLogger = new Mock();
var mockMainViewportService = new Mock();
var mockAppStateService = new Mock();
var mockPipelineExecutionService = new Mock();
var mockImageProcessingService = new Mock();
var mockEventAggregator = new Mock();
var mockImagePersistenceService = new Mock();
mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object);
mockEventAggregator
.Setup(ea => ea.GetEvent())
.Returns(new DetectorDisconnectedEvent());
mockStore.Setup(s => s.BeginRunAsync(
It.IsAny(),
It.IsAny()))
.Returns(Task.CompletedTask);
mockStore.Setup(s => s.AppendNodeResultAsync(
It.IsAny(),
It.IsAny>(),
It.IsAny(),
It.IsAny>()))
.Returns(Task.CompletedTask);
mockStore.Setup(s => s.CompleteRunAsync(
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Returns(Task.CompletedTask);
// Provide a valid image so that image acquisition succeeds
var dummyImage = CreateDummyBitmap();
mockMainViewportService.SetupGet(m => m.LatestManualImage).Returns((ImageSource)null);
mockMainViewportService.SetupGet(m => m.CurrentDisplayImage).Returns(dummyImage);
// Image persistence returns success
mockImagePersistenceService.Setup(s => s.SaveImageAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(new ImageSaveResult(true, @"C:\test\image.bmp", 1024));
mockImagePersistenceService.Setup(s => s.WriteSummaryAsync(
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(true);
var service = new CncExecutionService(
mockStore.Object,
mockLogger.Object,
mockMainViewportService.Object,
mockAppStateService.Object,
mockPipelineExecutionService.Object,
mockImageProcessingService.Object,
mockEventAggregator.Object,
mockImagePersistenceService.Object);
return (service, mockMainViewportService);
}
private static BitmapSource CreateDummyBitmap()
{
var stride = 4 * 4; // 4 pixels wide, 4 bytes per pixel
var pixels = new byte[stride * 4]; // 4x4 image
var bitmap = BitmapSource.Create(4, 4, 96, 96, PixelFormats.Bgra32, null, pixels, stride);
bitmap.Freeze();
return bitmap;
}
///
/// Generates a CncProgram with N SavePositionNodes (N between 1 and 10),
/// each with unique ascending indices.
///
private static Arbitrary SavePositionProgramArb()
{
var gen =
from count in Gen.Choose(1, 10)
from name in ArbMap.Default.GeneratorFor().Select(s => s.Get)
from nodes in GenSavePositionNodes(count)
select new CncProgram(
Guid.NewGuid(), name,
DateTime.UtcNow, DateTime.UtcNow,
nodes.AsReadOnly());
return gen.ToArbitrary();
}
private static Gen> GenSavePositionNodes(int count)
{
Gen> acc = Gen.Constant(new List());
for (int i = 0; i < count; i++)
{
var idx = i;
acc = from list in acc
from nodeName in ArbMap.Default.GeneratorFor().Select(s => s.Get)
let node = new SavePositionNode(
Guid.NewGuid(), idx, $"Pos_{nodeName}_{idx}",
MotionState.Default, SaveImage: false)
select new List(list) { node };
}
return acc;
}
// ── Property 7: 进度报告包含正确的索引和总数 ──────────────────────────
// Feature: cnc-multi-position-image-capture, Property 7: 进度报告包含正确的索引和总数
// Validates: Requirements 3.4
[Property(MaxTest = 100)]
public Property ProgressReports_ContainCorrectIndexAndTotal()
{
return Prop.ForAll(
SavePositionProgramArb(),
program =>
{
var (service, _) = CreateServiceWithImage();
var savePositionCount = program.Nodes.OfType().Count();
// Capture all progress reports that have PositionIndex set (Running state)
var runningReports = new List<(int PositionIndex, int TotalPositions)>();
var progress = new SynchronousProgress(p =>
{
if (p.State == NodeExecutionState.Running
&& p.PositionIndex.HasValue
&& p.TotalPositions.HasValue)
{
runningReports.Add((p.PositionIndex.Value, p.TotalPositions.Value));
}
});
service.ExecuteAsync(program, progress, CancellationToken.None)
.GetAwaiter().GetResult();
// Verify: at least N Running progress reports (one per position)
if (runningReports.Count < savePositionCount)
return false;
// Verify: all TotalPositions values equal N
if (runningReports.Any(r => r.TotalPositions != savePositionCount))
return false;
// Verify: PositionIndex values in Running reports are strictly increasing from 0
// Extract unique position indices in order of first appearance
var seenIndices = new List();
foreach (var report in runningReports)
{
if (seenIndices.Count == 0 || seenIndices.Last() != report.PositionIndex)
seenIndices.Add(report.PositionIndex);
}
// Must have exactly N distinct position indices
if (seenIndices.Count != savePositionCount)
return false;
// Indices must be 0, 1, 2, ..., N-1
for (int i = 0; i < savePositionCount; i++)
{
if (seenIndices[i] != i)
return false;
}
return true;
});
}
}
}