手动数据源、存图、流程计算
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user