位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
#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>
|
||||
/// FsCheck property-based tests for cancellation summary correctness.
|
||||
/// Property 11: 取消时摘要包含正确的完成/未执行计数
|
||||
/// **Validates: Requirements 4.5**
|
||||
/// </summary>
|
||||
public class CancellationSummaryPropertyTests
|
||||
{
|
||||
// ── Helper: Create a frozen BitmapSource for mocking ──────────────
|
||||
|
||||
private static BitmapSource CreateFrozenBitmap()
|
||||
{
|
||||
var bitmap = BitmapSource.Create(1, 1, 96, 96, PixelFormats.Bgra32, null, new byte[4], 4);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// ── Helper: Create CncExecutionService with mocks ────────────────
|
||||
|
||||
private static (CncExecutionService Service, Mock<IImagePersistenceService> ImagePersistence)
|
||||
CreateServiceWithImagePersistence(BitmapSource sourceImage)
|
||||
{
|
||||
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>();
|
||||
|
||||
mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object);
|
||||
|
||||
mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<DetectorDisconnectedEvent>())
|
||||
.Returns(new DetectorDisconnectedEvent());
|
||||
|
||||
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 source image so positions can succeed
|
||||
mockMainViewportService.SetupGet(m => m.LatestManualImage).Returns(sourceImage);
|
||||
mockMainViewportService.SetupGet(m => m.CurrentDisplayImage).Returns(sourceImage);
|
||||
|
||||
// SaveImageAsync returns success
|
||||
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));
|
||||
|
||||
// WriteSummaryAsync returns success
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Generator: N SavePositionNodes with cancellation point K ─────
|
||||
|
||||
private static Gen<(List<SavePositionNode> Nodes, int CancelAfterK)> SavePositionNodesWithCancelGen =>
|
||||
from n in Gen.Choose(2, 10)
|
||||
from k in Gen.Choose(0, n - 1)
|
||||
from nodes in GenSavePositionNodes(n)
|
||||
select (nodes, k);
|
||||
|
||||
private static Gen<List<SavePositionNode>> GenSavePositionNodes(int count)
|
||||
{
|
||||
Gen<List<SavePositionNode>> acc = Gen.Constant(new List<SavePositionNode>());
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var idx = i;
|
||||
acc = from list in acc
|
||||
let node = new SavePositionNode(
|
||||
Guid.NewGuid(), idx, $"Position_{idx}",
|
||||
MotionState.Default, SaveImage: true)
|
||||
select new List<SavePositionNode>(list) { node };
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// ── Property 11: 取消时摘要包含正确的完成/未执行计数 ─────────────
|
||||
|
||||
/// <summary>
|
||||
/// **Validates: Requirements 4.5**
|
||||
///
|
||||
/// For any CNC program execution that is cancelled at position K (0-based)
|
||||
/// out of N total positions, the summary SHALL have Status="Cancelled",
|
||||
/// CompletedBeforeCancel=K, and NotExecutedAfterCancel=N-K,
|
||||
/// where CompletedBeforeCancel + NotExecutedAfterCancel == TotalPositions.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property CancellationSummary_HasCorrectCompletedAndNotExecutedCounts()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
SavePositionNodesWithCancelGen.ToArbitrary(),
|
||||
tuple =>
|
||||
{
|
||||
var (nodes, cancelAfterK) = tuple;
|
||||
int totalPositions = nodes.Count;
|
||||
|
||||
var sourceImage = CreateFrozenBitmap();
|
||||
var (service, mockImagePersistence) = CreateServiceWithImagePersistence(sourceImage);
|
||||
|
||||
// Create a CTS that we will cancel after K positions are processed
|
||||
using var cts = new CancellationTokenSource();
|
||||
int processedCount = 0;
|
||||
|
||||
// Use progress callback to trigger cancellation after K positions complete
|
||||
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
||||
{
|
||||
// Count positions that have completed (Succeeded or Failed state)
|
||||
if (p.State == NodeExecutionState.Succeeded || p.State == NodeExecutionState.Failed)
|
||||
{
|
||||
if (p.PositionIndex.HasValue)
|
||||
{
|
||||
processedCount++;
|
||||
if (processedCount >= cancelAfterK)
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(), "TestProgram",
|
||||
DateTime.UtcNow, DateTime.UtcNow,
|
||||
nodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
service.ExecuteAsync(program, progress, cts.Token)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// Capture the BatchCaptureResult passed to WriteSummaryAsync
|
||||
BatchCaptureResult capturedResult = null;
|
||||
mockImagePersistence.Verify(
|
||||
s => s.WriteSummaryAsync(
|
||||
It.IsAny<BatchCaptureResult>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
|
||||
// Extract the captured argument
|
||||
var invocation = mockImagePersistence.Invocations
|
||||
.First(i => i.Method.Name == nameof(IImagePersistenceService.WriteSummaryAsync));
|
||||
capturedResult = (BatchCaptureResult)invocation.Arguments[0];
|
||||
|
||||
// Verify the cancellation summary properties
|
||||
bool statusIsCancelled = capturedResult.Status == "Cancelled";
|
||||
bool completedBeforeCancelIsK = capturedResult.CompletedBeforeCancel == cancelAfterK;
|
||||
bool notExecutedIsCorrect = capturedResult.NotExecutedAfterCancel == totalPositions - cancelAfterK;
|
||||
bool sumEqualsTotal =
|
||||
capturedResult.CompletedBeforeCancel + capturedResult.NotExecutedAfterCancel == totalPositions;
|
||||
bool totalPositionsCorrect = capturedResult.TotalPositions == totalPositions;
|
||||
|
||||
return statusIsCancelled
|
||||
&& completedBeforeCancelIsK
|
||||
&& notExecutedIsCorrect
|
||||
&& sumEqualsTotal
|
||||
&& totalPositionsCorrect;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user