手动数据源、存图、流程计算

This commit is contained in:
zhengxuan.zhang
2026-05-15 15:29:53 +08:00
parent f07d891346
commit 49c6785682
9 changed files with 800 additions and 419 deletions
@@ -47,12 +47,9 @@ namespace XplorePlane.Tests.Services
// ── 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()
CreateServiceWithMocks()
{
var mockStore = new Mock<IInspectionResultStore>();
var mockLogger = new Mock<ILoggerService>();
@@ -103,114 +100,90 @@ namespace XplorePlane.Tests.Services
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;
return (service, mockMainViewportService, mockImagePersistenceService);
}
/// <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).
/// 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 Gen<(HashSet<int> DetectorFailIndices, HashSet<int> SaveFailIndices)> FailureIndicesGen(int totalPositions)
private static Arbitrary<(List<SavePositionNode> Nodes, List<int> FailureModes)> ResilienceScenarioArb()
{
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);
// 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();
}
// ── 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>
// 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()
{
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 =>
ResilienceScenarioArb(),
scenario =>
{
var (saveNodes, detectorFailIndices, saveFailIndices) = tuple;
var (saveNodes, failureModes) = scenario;
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)
// 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++)
{
int nodeIndex = saveNodes.IndexOf(node);
if (detectorFailIndices.Contains(nodeIndex))
imageSequence.Enqueue(null); // Detector fails
else
imageSequence.Enqueue(testBitmap); // Detector succeeds
switch (failureModes[i])
{
case 1: detectorFailIndices.Add(i); break;
case 2: saveFailIndices.Add(i); break;
}
}
// Also need an initial call for runSourceImage at the start of ExecuteAsync
mockMainViewport.Setup(m => m.CurrentDisplayImage)
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(() =>
{
if (imageSequence.Count > 0)
return imageSequence.Dequeue();
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 ──
// For positions where save should fail, throw IOException.
// For others, return success.
mockImagePersistence.Setup(s => s.SaveImageAsync(
It.IsAny<byte[]>(),
It.IsAny<string>(),
@@ -218,11 +191,9 @@ namespace XplorePlane.Tests.Services
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"));
}
@@ -237,15 +208,12 @@ namespace XplorePlane.Tests.Services
saveNodes.Cast<CncNode>().ToList().AsReadOnly());
// ── Track progress reports ──
var reportedNodeIds = new List<Guid>();
var reportedNodeIds = new HashSet<Guid>();
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
{
// Count Running reports for SavePositionNodes (first report per position)
// Track all nodes that received a Running report with PositionIndex
if (p.State == NodeExecutionState.Running && p.PositionIndex.HasValue)
{
if (!reportedNodeIds.Contains(p.NodeId))
reportedNodeIds.Add(p.NodeId);
}
reportedNodeIds.Add(p.NodeId);
});
// ── Execute ──
@@ -253,7 +221,8 @@ namespace XplorePlane.Tests.Services
.GetAwaiter().GetResult();
// ── Verify: ALL positions were attempted ──
// Each SavePositionNode should have received at least one Running progress report
// Each SavePositionNode should have received at least one Running progress report,
// regardless of whether earlier positions failed.
bool allPositionsAttempted = reportedNodeIds.Count == totalPositions;
return allPositionsAttempted;