位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
#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 8: 取消令牌中止后续位置执行
|
||||
/// **Validates: Requirements 3.5**
|
||||
///
|
||||
/// For any CNC program being executed, when a CancellationToken is triggered,
|
||||
/// no SavePositionNode with Index greater than the currently executing node SHALL be processed.
|
||||
/// </summary>
|
||||
public class CancellationPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a CncExecutionService with all dependencies mocked.
|
||||
/// The SaveImageAsync mock triggers cancellation after K positions have been processed.
|
||||
/// </summary>
|
||||
private static (CncExecutionService Service, Mock<IImagePersistenceService> ImagePersistence, List<string> ExecutedNodeNames)
|
||||
CreateServiceWithCancellation(CancellationTokenSource cts, int cancelAfterK)
|
||||
{
|
||||
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);
|
||||
|
||||
// Track which nodes have their images saved (i.e., which positions were executed)
|
||||
var executedNodeNames = new List<string>();
|
||||
int saveCallCount = 0;
|
||||
|
||||
// ImagePersistenceService - cancel after K positions have been saved
|
||||
mockImagePersistenceService
|
||||
.Setup(s => s.SaveImageAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<byte[], string, string, CancellationToken>((_, nodeName, __, ___) =>
|
||||
{
|
||||
executedNodeNames.Add(nodeName);
|
||||
int currentCount = Interlocked.Increment(ref saveCallCount);
|
||||
// Cancel after K positions have been processed
|
||||
if (currentCount >= cancelAfterK)
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
return Task.FromResult(new ImageSaveResult(true, $"C:\\test\\{nodeName}.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, mockImagePersistenceService, executedNodeNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FsCheck generator for a cancellation test scenario:
|
||||
/// - N SavePositionNodes (N between 2 and 10)
|
||||
/// - A cancellation point K (1 <= K < N), meaning cancel after K positions are processed
|
||||
/// </summary>
|
||||
private static Arbitrary<(List<SavePositionNode> Nodes, int CancelAfterK)> CancellationScenarioArb()
|
||||
{
|
||||
var gen =
|
||||
from count in Gen.Choose(2, 10)
|
||||
from cancelAfterK in Gen.Choose(1, count - 1)
|
||||
from indices in Gen.Shuffle(Enumerable.Range(0, count).ToArray())
|
||||
.Select(arr => arr.ToList())
|
||||
select (
|
||||
Nodes: indices.Select(idx =>
|
||||
new SavePositionNode(
|
||||
Guid.NewGuid(),
|
||||
idx,
|
||||
$"Position_{idx}",
|
||||
MotionState.Default,
|
||||
SaveImage: true,
|
||||
ManualImagePath: ""))
|
||||
.ToList(),
|
||||
CancelAfterK: cancelAfterK
|
||||
);
|
||||
|
||||
return gen.ToArbitrary();
|
||||
}
|
||||
|
||||
// Feature: cnc-multi-position-image-capture, Property 8: 取消令牌中止后续位置执行
|
||||
// **Validates: Requirements 3.5**
|
||||
[Property(MaxTest = 100)]
|
||||
public Property Cancellation_StopsSubsequentPositionExecution()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
CancellationScenarioArb(),
|
||||
scenario =>
|
||||
{
|
||||
var (nodes, cancelAfterK) = scenario;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var (service, _, executedNodeNames) = CreateServiceWithCancellation(cts, cancelAfterK);
|
||||
|
||||
// Build a CncProgram containing the SavePositionNodes
|
||||
// (in their original random order - the service sorts by Index)
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(),
|
||||
"TestProgram",
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
nodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
// Track progress reports to verify which positions were executed
|
||||
var executedPositionIndices = new List<int>();
|
||||
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
||||
{
|
||||
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
|
||||
{
|
||||
executedPositionIndices.Add(p.PositionIndex.Value);
|
||||
}
|
||||
});
|
||||
|
||||
service.ExecuteAsync(program, progress, cts.Token)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// The expected sorted order of nodes by Index
|
||||
var sortedNodes = nodes.OrderBy(n => n.Index).ToList();
|
||||
|
||||
// Verify: No more than cancelAfterK positions should have been executed.
|
||||
// The cancellation is triggered after K SaveImageAsync calls complete,
|
||||
// so at most K positions should have their images saved.
|
||||
// The (K+1)th position might start (Running reported) but its SaveImageAsync
|
||||
// should not be called because cancellation is checked at loop start.
|
||||
bool noExcessiveSaves = executedNodeNames.Count <= cancelAfterK;
|
||||
|
||||
// Verify: Positions after the cancellation point should NOT be executed.
|
||||
// The service checks cancellation at the start of each loop iteration,
|
||||
// so after K positions complete and cancellation is triggered,
|
||||
// subsequent positions should not receive Running state.
|
||||
bool noSubsequentExecution = executedPositionIndices.Count <= cancelAfterK;
|
||||
|
||||
// Verify: The executed positions should be the first K positions in sorted order
|
||||
bool correctPositionsExecuted = true;
|
||||
for (int i = 0; i < executedNodeNames.Count && i < cancelAfterK; i++)
|
||||
{
|
||||
if (executedNodeNames[i] != sortedNodes[i].Name)
|
||||
{
|
||||
correctPositionsExecuted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return noExcessiveSaves && noSubsequentExecution && correctPositionsExecuted;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user