233 lines
10 KiB
C#
233 lines
10 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 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<IMainViewportService> MainViewport,
|
|
Mock<IImagePersistenceService> ImagePersistence)
|
|
CreateServiceWithMocks()
|
|
{
|
|
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, mockMainViewportService, mockImagePersistenceService);
|
|
}
|
|
|
|
/// <summary>
|
|
/// FsCheck Arbitrary for resilience test scenarios.
|
|
/// Generates (SavePositionNodes, FailureModes) where:
|
|
/// - Nodes: 2-6 SavePositionNodes with SaveImage=true
|
|
/// - FailureModes: per-position int (0=success, 1=detector null, 2=save I/O error)
|
|
/// - At least one position has a failure
|
|
/// </summary>
|
|
private static Arbitrary<(List<SavePositionNode> Nodes, List<int> FailureModes)> ResilienceScenarioArb()
|
|
{
|
|
// Generate 6 failure mode values upfront, then take posCount of them
|
|
var gen =
|
|
from posCount in Gen.Choose(2, 6)
|
|
from m0 in Gen.Choose(0, 2)
|
|
from m1 in Gen.Choose(0, 2)
|
|
from m2 in Gen.Choose(0, 2)
|
|
from m3 in Gen.Choose(0, 2)
|
|
from m4 in Gen.Choose(0, 2)
|
|
from m5 in Gen.Choose(0, 2)
|
|
let allModes = new List<int> { m0, m1, m2, m3, m4, m5 }
|
|
let modes = allModes.Take(posCount).ToList()
|
|
// Ensure at least one failure exists
|
|
let adjustedModes = modes.Any(m => m > 0)
|
|
? modes
|
|
: modes.Select((m, i) => i == 0 ? 1 : m).ToList()
|
|
let nodes = Enumerable.Range(0, posCount)
|
|
.Select(i => new SavePositionNode(
|
|
Guid.NewGuid(), i, $"Position_{i}",
|
|
MotionState.Default, SaveImage: true))
|
|
.ToList()
|
|
select (Nodes: nodes, FailureModes: adjustedModes);
|
|
|
|
return gen.ToArbitrary();
|
|
}
|
|
|
|
// Feature: cnc-multi-position-image-capture, Property 3: 采集或保存失败不中断后续执行
|
|
// **Validates: Requirements 1.5, 1.6, 3.3, 3.6**
|
|
[Property(MaxTest = 100)]
|
|
public Property AcquisitionOrSaveFailure_DoesNotInterruptSubsequentPositions()
|
|
{
|
|
return Prop.ForAll(
|
|
ResilienceScenarioArb(),
|
|
scenario =>
|
|
{
|
|
var (saveNodes, failureModes) = scenario;
|
|
int totalPositions = saveNodes.Count;
|
|
|
|
// Determine which positions have detector failures vs save failures
|
|
var detectorFailIndices = new HashSet<int>();
|
|
var saveFailIndices = new HashSet<int>();
|
|
for (int i = 0; i < totalPositions; i++)
|
|
{
|
|
switch (failureModes[i])
|
|
{
|
|
case 1: detectorFailIndices.Add(i); break;
|
|
case 2: saveFailIndices.Add(i); break;
|
|
}
|
|
}
|
|
|
|
var (service, mockMainViewport, mockImagePersistence) = CreateServiceWithMocks();
|
|
|
|
var testBitmap = CreateTestBitmap();
|
|
|
|
// ── Configure detector mock ──
|
|
// TryGetSourceImage reads LatestManualImage first, then CurrentDisplayImage.
|
|
// Use LatestManualImage to control image return per call.
|
|
int imageCallCount = 0;
|
|
mockMainViewport.SetupGet(m => m.LatestManualImage)
|
|
.Returns(() =>
|
|
{
|
|
int callNum = Interlocked.Increment(ref imageCallCount);
|
|
// Call 1 = initial runSourceImage at start of ExecuteAsync - always valid
|
|
if (callNum == 1)
|
|
return testBitmap;
|
|
// Subsequent calls: callNum-2 gives the 0-based position index
|
|
int posIdx = callNum - 2;
|
|
if (posIdx >= 0 && detectorFailIndices.Contains(posIdx))
|
|
return null; // Simulate detector failure
|
|
return testBitmap;
|
|
});
|
|
|
|
// ── Configure image persistence mock ──
|
|
mockImagePersistence.Setup(s => s.SaveImageAsync(
|
|
It.IsAny<byte[]>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<byte[], string, string, CancellationToken>((_, nodeName, __, ___) =>
|
|
{
|
|
var posIdx = saveNodes.FindIndex(n => n.Name == nodeName);
|
|
if (posIdx >= 0 && saveFailIndices.Contains(posIdx))
|
|
{
|
|
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 HashSet<Guid>();
|
|
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
|
{
|
|
// Track all nodes that received a Running report with PositionIndex
|
|
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
|
|
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,
|
|
// regardless of whether earlier positions failed.
|
|
bool allPositionsAttempted = reportedNodeIds.Count == totalPositions;
|
|
|
|
return allPositionsAttempted;
|
|
});
|
|
}
|
|
}
|
|
}
|