位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
#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 3: 采集或保存失败不中断后续执行
|
||||
/// **Validates: Requirements 1.5, 1.6, 3.3, 3.6**
|
||||
///
|
||||
/// For any CNC program with multiple SavePositionNodes, if image acquisition fails
|
||||
/// (detector returns null) or image persistence fails (I/O error) at any position,
|
||||
/// all subsequent positions SHALL still be attempted for execution.
|
||||
/// </summary>
|
||||
public class ExecutionResiliencePropertyTests
|
||||
{
|
||||
// ── Helper: Create a frozen BitmapSource for testing ──
|
||||
private static BitmapSource CreateTestBitmap()
|
||||
{
|
||||
var stride = 4;
|
||||
var pixels = new byte[stride * 1];
|
||||
var bitmap = BitmapSource.Create(1, 1, 96, 96, PixelFormats.Bgra32, null, pixels, stride);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// ── Helper: Create service with full mock access ──
|
||||
private static (
|
||||
CncExecutionService Service,
|
||||
Mock<IInspectionResultStore> Store,
|
||||
Mock<ILoggerService> Logger,
|
||||
Mock<IMainViewportService> MainViewport,
|
||||
Mock<IAppStateService> AppState,
|
||||
Mock<IImagePersistenceService> ImagePersistence)
|
||||
CreateServiceWithImagePersistence()
|
||||
{
|
||||
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);
|
||||
|
||||
// Default: WriteSummaryAsync succeeds
|
||||
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, mockStore, mockLogger, mockMainViewportService, mockAppStateService, mockImagePersistenceService);
|
||||
}
|
||||
|
||||
// ── Generator: Produce N SavePositionNodes with unique ascending indices ──
|
||||
private static Gen<List<SavePositionNode>> SavePositionNodesGen(int minCount, int maxCount)
|
||||
{
|
||||
return
|
||||
from count in Gen.Choose(minCount, maxCount)
|
||||
from nodes in GenSavePositionNodes(count)
|
||||
select nodes;
|
||||
}
|
||||
|
||||
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
|
||||
from saveImage in Gen.Elements(true, false)
|
||||
select new List<SavePositionNode>(list)
|
||||
{
|
||||
new SavePositionNode(
|
||||
Guid.NewGuid(), idx, $"Pos_{idx}",
|
||||
MotionState.Default, SaveImage: saveImage)
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generator for a set of failure indices (positions where acquisition or save will fail).
|
||||
/// Ensures at least one failure position and that failures don't cover ALL positions
|
||||
/// (so we can verify subsequent positions still execute).
|
||||
/// </summary>
|
||||
private static Gen<(HashSet<int> DetectorFailIndices, HashSet<int> SaveFailIndices)> FailureIndicesGen(int totalPositions)
|
||||
{
|
||||
return
|
||||
from failCount in Gen.Choose(1, Math.Max(1, totalPositions - 1))
|
||||
from failIndices in Gen.Shuffle(Enumerable.Range(0, totalPositions).ToArray())
|
||||
.Select(arr => arr.Take(failCount).ToList())
|
||||
from splitPoint in Gen.Choose(0, failCount)
|
||||
let detectorFails = new HashSet<int>(failIndices.Take(splitPoint))
|
||||
let saveFails = new HashSet<int>(failIndices.Skip(splitPoint))
|
||||
select (detectorFails, saveFails);
|
||||
}
|
||||
|
||||
// ── Property Test ──
|
||||
|
||||
/// <summary>
|
||||
/// Feature: cnc-multi-position-image-capture, Property 3: 采集或保存失败不中断后续执行
|
||||
/// **Validates: Requirements 1.5, 1.6, 3.3, 3.6**
|
||||
///
|
||||
/// For any CNC program with N >= 2 SavePositionNodes, if image acquisition fails
|
||||
/// (detector returns null) or image persistence fails (I/O error) at randomly selected
|
||||
/// positions, ALL N positions SHALL still be attempted (progress reported for each).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property AcquisitionOrSaveFailure_DoesNotInterruptSubsequentPositions()
|
||||
{
|
||||
var gen =
|
||||
from nodes in SavePositionNodesGen(2, 8)
|
||||
from failures in FailureIndicesGen(nodes.Count)
|
||||
select (nodes, failures.DetectorFailIndices, failures.SaveFailIndices);
|
||||
|
||||
return Prop.ForAll(
|
||||
gen.ToArbitrary(),
|
||||
tuple =>
|
||||
{
|
||||
var (saveNodes, detectorFailIndices, saveFailIndices) = tuple;
|
||||
int totalPositions = saveNodes.Count;
|
||||
|
||||
var (service, _, _, mockMainViewport, _, mockImagePersistence) =
|
||||
CreateServiceWithImagePersistence();
|
||||
|
||||
// ── Configure detector mock ──
|
||||
// For positions where detector should fail, return null.
|
||||
// For others, return a valid image.
|
||||
var testBitmap = CreateTestBitmap();
|
||||
|
||||
// TryGetSourceImage reads from LatestManualImage first, then CurrentDisplayImage.
|
||||
// We use CurrentDisplayImage to control per-position behavior.
|
||||
mockMainViewport.SetupGet(m => m.LatestManualImage)
|
||||
.Returns((System.Windows.Media.ImageSource)null);
|
||||
|
||||
// Use a callback-based setup to return null or valid image based on position
|
||||
var imageSequence = new Queue<BitmapSource>();
|
||||
foreach (var node in saveNodes)
|
||||
{
|
||||
int nodeIndex = saveNodes.IndexOf(node);
|
||||
if (detectorFailIndices.Contains(nodeIndex))
|
||||
imageSequence.Enqueue(null); // Detector fails
|
||||
else
|
||||
imageSequence.Enqueue(testBitmap); // Detector succeeds
|
||||
}
|
||||
|
||||
// Also need an initial call for runSourceImage at the start of ExecuteAsync
|
||||
mockMainViewport.Setup(m => m.CurrentDisplayImage)
|
||||
.Returns(() =>
|
||||
{
|
||||
if (imageSequence.Count > 0)
|
||||
return imageSequence.Dequeue();
|
||||
return testBitmap;
|
||||
});
|
||||
|
||||
// ── Configure image persistence mock ──
|
||||
// For positions where save should fail, throw IOException.
|
||||
// For others, return success.
|
||||
mockImagePersistence.Setup(s => s.SaveImageAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<byte[], string, string, CancellationToken>((_, nodeName, __, ___) =>
|
||||
{
|
||||
// Find the position index by node name
|
||||
var posIdx = saveNodes.FindIndex(n => n.Name == nodeName);
|
||||
if (posIdx >= 0 && saveFailIndices.Contains(posIdx))
|
||||
{
|
||||
// Simulate I/O failure
|
||||
return Task.FromResult(new ImageSaveResult(
|
||||
false, string.Empty, 0, "Simulated I/O error"));
|
||||
}
|
||||
return Task.FromResult(new ImageSaveResult(
|
||||
true, $"C:\\Images\\{nodeName}.bmp", 1024, null));
|
||||
});
|
||||
|
||||
// ── Build CNC program ──
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(), "TestProgram",
|
||||
DateTime.UtcNow, DateTime.UtcNow,
|
||||
saveNodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
// ── Track progress reports ──
|
||||
var reportedNodeIds = new List<Guid>();
|
||||
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
||||
{
|
||||
// Count Running reports for SavePositionNodes (first report per position)
|
||||
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
|
||||
{
|
||||
if (!reportedNodeIds.Contains(p.NodeId))
|
||||
reportedNodeIds.Add(p.NodeId);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Execute ──
|
||||
service.ExecuteAsync(program, progress, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// ── Verify: ALL positions were attempted ──
|
||||
// Each SavePositionNode should have received at least one Running progress report
|
||||
bool allPositionsAttempted = reportedNodeIds.Count == totalPositions;
|
||||
|
||||
return allPositionsAttempted;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user