205 lines
8.6 KiB
C#
205 lines
8.6 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// Property 5: 多位置按 Index 升序执行
|
|
/// **Validates: Requirements 3.1**
|
|
///
|
|
/// For any CNC program containing multiple SavePositionNodes with arbitrary Index values,
|
|
/// the CNC_Execution_Service SHALL process them in strictly ascending Index order,
|
|
/// and progress reports SHALL reflect this ordering.
|
|
/// </summary>
|
|
public class ExecutionOrderPropertyTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a CncExecutionService with all dependencies mocked.
|
|
/// The MainViewportService returns a valid BitmapSource so execution proceeds.
|
|
/// </summary>
|
|
private static (CncExecutionService Service, Mock<IMainViewportService> MainViewport) CreateServiceWithImage()
|
|
{
|
|
var mockStore = new Mock<IInspectionResultStore>();
|
|
var mockLogger = new Mock<ILoggerService>();
|
|
var mockMainViewportService = new Mock<IMainViewportService>();
|
|
var mockAppStateService = new Mock<IAppStateService>();
|
|
var mockPipelineExecutionService = new Mock<IPipelineExecutionService>();
|
|
var mockImageProcessingService = new Mock<IImageProcessingService>();
|
|
var mockEventAggregator = new Mock<IEventAggregator>();
|
|
var mockImagePersistenceService = new Mock<IImagePersistenceService>();
|
|
|
|
// Logger setup
|
|
mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object);
|
|
|
|
// EventAggregator setup - prevent NRE on constructor subscription
|
|
mockEventAggregator
|
|
.Setup(ea => ea.GetEvent<DetectorDisconnectedEvent>())
|
|
.Returns(new DetectorDisconnectedEvent());
|
|
|
|
// InspectionResultStore setup
|
|
mockStore.Setup(s => s.BeginRunAsync(
|
|
It.IsAny<InspectionRunRecord>(),
|
|
It.IsAny<InspectionAssetWriteRequest>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
mockStore.Setup(s => s.AppendNodeResultAsync(
|
|
It.IsAny<InspectionNodeResult>(),
|
|
It.IsAny<IEnumerable<InspectionMetricResult>>(),
|
|
It.IsAny<PipelineExecutionSnapshot>(),
|
|
It.IsAny<IEnumerable<InspectionAssetWriteRequest>>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
mockStore.Setup(s => s.CompleteRunAsync(
|
|
It.IsAny<Guid>(),
|
|
It.IsAny<bool?>(),
|
|
It.IsAny<DateTime?>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Provide a valid BitmapSource from the viewport so image acquisition succeeds
|
|
var dummyBitmap = BitmapSource.Create(
|
|
1, 1, 96, 96, PixelFormats.Gray8, null, new byte[] { 128 }, 1);
|
|
dummyBitmap.Freeze();
|
|
mockMainViewportService
|
|
.Setup(m => m.LatestManualImage)
|
|
.Returns(dummyBitmap);
|
|
|
|
// ImagePersistenceService - return success for SaveImageAsync
|
|
mockImagePersistenceService
|
|
.Setup(s => s.SaveImageAsync(
|
|
It.IsAny<byte[]>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ImageSaveResult(true, "C:\\test\\image.bmp", 1024));
|
|
|
|
mockImagePersistenceService
|
|
.Setup(s => s.WriteSummaryAsync(
|
|
It.IsAny<BatchCaptureResult>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// FsCheck generator for a list of SavePositionNodes with random, unique Index values.
|
|
/// Generates 2-10 nodes with shuffled indices to ensure non-sequential ordering in the input.
|
|
/// </summary>
|
|
private static Arbitrary<List<SavePositionNode>> SavePositionNodesArb()
|
|
{
|
|
var gen =
|
|
from count in Gen.Choose(2, 10)
|
|
from indices in Gen.Shuffle(Enumerable.Range(0, 100).ToArray())
|
|
.Select(arr => arr.Take(count).ToList())
|
|
select indices.Select(idx =>
|
|
new SavePositionNode(
|
|
Guid.NewGuid(),
|
|
idx,
|
|
$"Position_{idx}",
|
|
MotionState.Default,
|
|
SaveImage: false,
|
|
ManualImagePath: ""))
|
|
.ToList();
|
|
|
|
return gen.ToArbitrary();
|
|
}
|
|
|
|
// Feature: cnc-multi-position-image-capture, Property 5: 多位置按 Index 升序执行
|
|
// **Validates: Requirements 3.1**
|
|
[Property(MaxTest = 100)]
|
|
public Property MultiPosition_ExecutedInStrictlyAscendingIndexOrder()
|
|
{
|
|
return Prop.ForAll(
|
|
SavePositionNodesArb(),
|
|
savePositionNodes =>
|
|
{
|
|
var (service, _) = CreateServiceWithImage();
|
|
|
|
// Build a CncProgram containing only the generated SavePositionNodes
|
|
// (in their original random order - the service should sort by Index)
|
|
var program = new CncProgram(
|
|
Guid.NewGuid(),
|
|
"TestProgram",
|
|
DateTime.UtcNow,
|
|
DateTime.UtcNow,
|
|
savePositionNodes.Cast<CncNode>().ToList().AsReadOnly());
|
|
|
|
// Capture progress reports to verify execution order
|
|
var reportedPositionIndices = new List<int>();
|
|
var reportedNodeIds = new List<Guid>();
|
|
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
|
{
|
|
// Capture the first Running report for each node (the initial progress report)
|
|
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
|
|
{
|
|
reportedNodeIds.Add(p.NodeId);
|
|
reportedPositionIndices.Add(p.PositionIndex.Value);
|
|
}
|
|
});
|
|
|
|
service.ExecuteAsync(program, progress, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
// Build a map from NodeId to Index
|
|
var nodeIndexMap = savePositionNodes.ToDictionary(n => n.Id, n => n.Index);
|
|
|
|
// Verify 1: All nodes were executed
|
|
if (reportedNodeIds.Count != savePositionNodes.Count)
|
|
return false;
|
|
|
|
// Verify 2: Nodes were processed in strictly ascending Index order
|
|
var executedIndices = reportedNodeIds
|
|
.Select(id => nodeIndexMap[id])
|
|
.ToList();
|
|
|
|
for (int i = 1; i < executedIndices.Count; i++)
|
|
{
|
|
if (executedIndices[i] <= executedIndices[i - 1])
|
|
return false;
|
|
}
|
|
|
|
// Verify 3: Progress report PositionIndex values are strictly ascending from 0
|
|
for (int i = 0; i < reportedPositionIndices.Count; i++)
|
|
{
|
|
if (reportedPositionIndices[i] != i)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
}
|