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

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
@@ -13,6 +13,7 @@ using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
@@ -26,26 +27,19 @@ namespace XplorePlane.Tests.Services
{
/// <summary>
/// Property 9: 运行摘要字段完整性与一致性
/// Validates: Requirements 4.1, 4.2
///
/// For any completed (non-cancelled) batch execution with known position results,
/// the generated BatchCaptureResult SHALL satisfy:
/// SucceededPositions + FailedPositions == TotalPositions,
/// and a position is marked "Success" if and only if its image capture completed
/// AND its inspection pipeline (if present) executed without error.
/// **Validates: Requirements 4.1, 4.2**
/// </summary>
public class SummaryCorrectnessPropertyTests
{
/// <summary>
/// Creates a CncExecutionService with mocks configured for multi-position testing.
/// Returns the service and the mock for IImagePersistenceService to capture WriteSummaryAsync calls.
/// The failurePattern parameter controls which positions fail image acquisition (detector returns null).
/// The pipelineFailurePattern controls which positions fail pipeline execution.
/// </summary>
private static BitmapSource CreateFrozenBitmap()
{
var bitmap = BitmapSource.Create(1, 1, 96, 96, PixelFormats.Bgra32, null, new byte[4], 4);
bitmap.Freeze();
return bitmap;
}
private static (CncExecutionService Service, Mock<IImagePersistenceService> ImagePersistence)
CreateServiceWithFailurePatterns(
bool[] imageAcquisitionFailures,
bool[] pipelineFailures)
CreateServiceWithFailurePattern(List<bool> imageAcquisitionFailures)
{
var mockStore = new Mock<IInspectionResultStore>();
var mockLogger = new Mock<ILoggerService>();
@@ -57,324 +51,163 @@ namespace XplorePlane.Tests.Services
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);
// Track which position index is being processed to control failure patterns
int callIndex = 0;
// Control image acquisition: when imageAcquisitionFailures[i] is true,
// the detector returns null for that position (causing failure).
// We use LatestManualImage to provide/deny images.
var frozenBitmap = CreateFrozenBitmap();
int imageCallCount = 0;
mockMainViewportService
.Setup(m => m.LatestManualImage)
.SetupGet(m => m.LatestManualImage)
.Returns(() =>
{
// Return null to simulate detector failure for positions marked to fail
return null;
});
mockMainViewportService
.Setup(m => m.CurrentDisplayImage)
.Returns(() => null);
// Use AppStateService.LatestDetectorFrame to control image availability per position
// We'll use a sequence-based approach via callback
var positionCallCount = 0;
mockAppStateService
.Setup(a => a.LatestDetectorFrame)
.Returns(() =>
{
int currentPos = positionCallCount++;
if (currentPos < imageAcquisitionFailures.Length && imageAcquisitionFailures[currentPos])
{
// Return null frame to simulate acquisition failure
int currentPos = imageCallCount++;
if (currentPos < imageAcquisitionFailures.Count && imageAcquisitionFailures[currentPos])
return null;
}
// Return a valid frame
return new DetectorFrame
{
ImageData = new ushort[4],
Width = 2,
Height = 2
};
return frozenBitmap;
});
mockMainViewportService
.SetupGet(m => m.CurrentDisplayImage)
.Returns((ImageSource)null);
mockAppStateService
.SetupGet(a => a.LatestDetectorFrame)
.Returns((ImageCapturedEventArgs)null);
// Control pipeline execution: throw exception for positions marked to fail
var pipelineCallCount = 0;
mockPipelineExecutionService
.Setup(p => p.ExecutePipelineAsync(
It.IsAny<IEnumerable<PipelineNodeViewModel>>(),
It.IsAny<BitmapSource>(),
It.IsAny<CancellationToken>()))
.Returns<IEnumerable<PipelineNodeViewModel>, BitmapSource, CancellationToken>((nodes, img, ct) =>
{
int currentPipelinePos = pipelineCallCount++;
if (currentPipelinePos < pipelineFailures.Length && pipelineFailures[currentPipelinePos])
{
throw new Exception("Simulated pipeline failure");
}
return Task.FromResult(img);
});
// Image persistence always succeeds
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));
It.IsAny<byte[]>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ImageSaveResult(true, @"C:\test\image.bmp", 1024));
mockImagePersistenceService
.Setup(s => s.WriteSummaryAsync(
It.IsAny<BatchCaptureResult>(),
It.IsAny<string>(),
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);
mockStore.Object, mockLogger.Object,
mockMainViewportService.Object, mockAppStateService.Object,
mockPipelineExecutionService.Object, mockImageProcessingService.Object,
mockEventAggregator.Object, mockImagePersistenceService.Object);
return (service, mockImagePersistenceService);
}
/// <summary>
/// Generates a list of SavePositionNodes with optional trailing InspectionModuleNodes.
/// </summary>
private static Gen<(List<CncNode> Nodes, bool[] HasInspection)> GenSavePositionProgram(int count)
{
return from hasInspections in Gen.ListOf(count, ArbMap.Default.GeneratorFor<bool>())
let nodes = BuildNodes(count, hasInspections.ToArray())
select (nodes, hasInspections.ToArray());
}
private static Gen<List<SavePositionNode>> SavePositionNodesGen =>
from count in Gen.Choose(1, 8)
select Enumerable.Range(0, count)
.Select(i => new SavePositionNode(
Guid.NewGuid(), i, $"Position_{i}",
MotionState.Default, SaveImage: true))
.ToList();
private static List<CncNode> BuildNodes(int positionCount, bool[] hasInspection)
{
var nodes = new List<CncNode>();
int index = 0;
for (int i = 0; i < positionCount; i++)
{
var spNode = new SavePositionNode(
Guid.NewGuid(),
index,
$"Position_{i}",
MotionState.Default,
SaveImage: true,
ManualImagePath: "");
nodes.Add(spNode);
index++;
if (i < hasInspection.Length && hasInspection[i])
{
var inspNode = new InspectionModuleNode(
Guid.NewGuid(),
index,
$"Inspect_{i}",
new PipelineModel { Name = $"Pipeline_{i}" });
nodes.Add(inspNode);
index++;
}
}
return nodes;
}
// ── Property 9: 运行摘要字段完整性与一致性 ──────────────────────────────
/// <summary>
/// **Validates: Requirements 4.1, 4.2**
///
/// For any completed (non-cancelled) batch execution:
/// SucceededPositions + FailedPositions == TotalPositions == N
/// </summary>
// Feature: cnc-multi-position-image-capture, Property 9: 运行摘要字段完整性与一致性
// **Validates: Requirements 4.1, 4.2**
[Property(MaxTest = 100)]
public Property Summary_SucceededPlusFailedEqualsTotalPositions()
{
// Generate 1-8 positions with random failure patterns
var gen =
from posCount in Gen.Choose(1, 8)
from imageFailures in Gen.ListOf(posCount, Gen.Elements(true, false))
from pipelineFailures in Gen.ListOf(posCount, Gen.Elements(true, false))
from hasInspections in Gen.ListOf(posCount, ArbMap.Default.GeneratorFor<bool>())
select new
{
PositionCount = posCount,
ImageFailures = imageFailures.ToArray(),
PipelineFailures = pipelineFailures.ToArray(),
HasInspections = hasInspections.ToArray()
};
return Prop.ForAll(
gen.ToArbitrary(),
testCase =>
SavePositionNodesGen.ToArbitrary(),
nodes =>
{
var nodes = BuildNodes(testCase.PositionCount, testCase.HasInspections);
var random = new Random(nodes.GetHashCode());
var failures = nodes.Select(_ => random.Next(2) == 0).ToList();
int totalPositions = nodes.Count;
var program = new CncProgram(
Guid.NewGuid(),
"TestProgram",
DateTime.UtcNow,
DateTime.UtcNow,
nodes.AsReadOnly());
Guid.NewGuid(), "TestProgram",
DateTime.UtcNow, DateTime.UtcNow,
nodes.Cast<CncNode>().ToList().AsReadOnly());
// Build pipeline failure pattern: only positions with inspections can have pipeline failures
// Map pipeline failures to only those positions that have an inspection node following them
var effectivePipelineFailures = new bool[testCase.PositionCount];
for (int i = 0; i < testCase.PositionCount; i++)
{
effectivePipelineFailures[i] = testCase.HasInspections[i] && testCase.PipelineFailures[i];
}
var (service, mockImagePersistence) = CreateServiceWithFailurePattern(failures);
var (service, mockImagePersistence) = CreateServiceWithFailurePatterns(
testCase.ImageFailures,
effectivePipelineFailures);
// Capture the BatchCaptureResult passed to WriteSummaryAsync
BatchCaptureResult capturedResult = null;
mockImagePersistence
.Setup(s => s.WriteSummaryAsync(
It.IsAny<BatchCaptureResult>(),
It.IsAny<string>(),
It.IsAny<BatchCaptureResult>(), It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Callback<BatchCaptureResult, string, CancellationToken>((result, _, _) =>
{
capturedResult = result;
})
.Callback<BatchCaptureResult, string, CancellationToken>((r, _, _) => capturedResult = r)
.ReturnsAsync(true);
service.ExecuteAsync(program, null, CancellationToken.None)
.GetAwaiter().GetResult();
// Verify invariants
if (capturedResult == null)
return false;
if (capturedResult == null) return false;
// Invariant 1: SucceededPositions + FailedPositions == TotalPositions
bool sumCorrect = capturedResult.SucceededPositions + capturedResult.FailedPositions
== capturedResult.TotalPositions;
// Invariant 2: TotalPositions == N (number of SavePositionNodes)
bool totalCorrect = capturedResult.TotalPositions == testCase.PositionCount;
// Invariant 3: Status == "Completed" (not cancelled)
bool totalCorrect = capturedResult.TotalPositions == totalPositions;
bool statusCorrect = capturedResult.Status == "Completed";
// Invariant 4: Each position's Status matches whether it succeeded or failed
bool positionsCorrect = capturedResult.Positions.Count == testCase.PositionCount;
if (positionsCorrect)
bool positionsCountCorrect = capturedResult.Positions.Count == totalPositions;
bool aggregatesMatch = false;
if (positionsCountCorrect)
{
int succeededCount = capturedResult.Positions.Count(p => p.Status == "Success");
int failedCount = capturedResult.Positions.Count(p => p.Status == "Failed");
positionsCorrect = succeededCount == capturedResult.SucceededPositions
&& failedCount == capturedResult.FailedPositions;
int succeeded = capturedResult.Positions.Count(p => p.Status == "Success");
int failed = capturedResult.Positions.Count(p => p.Status == "Failed");
aggregatesMatch = succeeded == capturedResult.SucceededPositions
&& failed == capturedResult.FailedPositions;
}
return sumCorrect && totalCorrect && statusCorrect && positionsCorrect;
return sumCorrect && totalCorrect && statusCorrect
&& positionsCountCorrect && aggregatesMatch;
});
}
/// <summary>
/// **Validates: Requirements 4.1, 4.2**
///
/// A position is marked "Success" if and only if its image capture completed
/// AND its inspection pipeline (if present) executed without error.
/// A position with image acquisition failure must be "Failed".
/// </summary>
// Feature: cnc-multi-position-image-capture, Property 9: 运行摘要字段完整性与一致性
// **Validates: Requirements 4.1, 4.2**
[Property(MaxTest = 100)]
public Property Summary_PositionStatusReflectsActualOutcome()
public Property Summary_PositionStatusReflectsImageAcquisitionOutcome()
{
var gen =
from posCount in Gen.Choose(1, 6)
from imageFailures in Gen.ListOf(posCount, Gen.Elements(true, false))
from pipelineFailures in Gen.ListOf(posCount, Gen.Elements(true, false))
from hasInspections in Gen.ListOf(posCount, ArbMap.Default.GeneratorFor<bool>())
select new
{
PositionCount = posCount,
ImageFailures = imageFailures.ToArray(),
PipelineFailures = pipelineFailures.ToArray(),
HasInspections = hasInspections.ToArray()
};
return Prop.ForAll(
gen.ToArbitrary(),
testCase =>
SavePositionNodesGen.ToArbitrary(),
nodes =>
{
var nodes = BuildNodes(testCase.PositionCount, testCase.HasInspections);
var random = new Random(nodes.Sum(n => n.Index) * 17 + nodes.Count);
var failures = nodes.Select(_ => random.Next(2) == 0).ToList();
int totalPositions = nodes.Count;
var program = new CncProgram(
Guid.NewGuid(),
"TestProgram",
DateTime.UtcNow,
DateTime.UtcNow,
nodes.AsReadOnly());
Guid.NewGuid(), "TestProgram",
DateTime.UtcNow, DateTime.UtcNow,
nodes.Cast<CncNode>().ToList().AsReadOnly());
var effectivePipelineFailures = new bool[testCase.PositionCount];
for (int i = 0; i < testCase.PositionCount; i++)
{
effectivePipelineFailures[i] = testCase.HasInspections[i] && testCase.PipelineFailures[i];
}
var (service, mockImagePersistence) = CreateServiceWithFailurePatterns(
testCase.ImageFailures,
effectivePipelineFailures);
var (service, mockImagePersistence) = CreateServiceWithFailurePattern(failures);
BatchCaptureResult capturedResult = null;
mockImagePersistence
.Setup(s => s.WriteSummaryAsync(
It.IsAny<BatchCaptureResult>(),
It.IsAny<string>(),
It.IsAny<BatchCaptureResult>(), It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Callback<BatchCaptureResult, string, CancellationToken>((result, _, _) =>
{
capturedResult = result;
})
.Callback<BatchCaptureResult, string, CancellationToken>((r, _, _) => capturedResult = r)
.ReturnsAsync(true);
service.ExecuteAsync(program, null, CancellationToken.None)
.GetAwaiter().GetResult();
if (capturedResult == null || capturedResult.Positions.Count != testCase.PositionCount)
if (capturedResult == null || capturedResult.Positions.Count != totalPositions)
return false;
// Verify each position's status matches expected outcome
for (int i = 0; i < testCase.PositionCount; i++)
for (int i = 0; i < totalPositions; i++)
{
var posResult = capturedResult.Positions[i];
bool imageAcquired = !testCase.ImageFailures[i];
bool pipelineFailed = effectivePipelineFailures[i];
// A position is "Success" iff image capture completed AND pipeline (if present) succeeded
bool expectedSuccess = imageAcquired && !pipelineFailed;
string expectedStatus = expectedSuccess ? "Success" : "Failed";
if (posResult.Status != expectedStatus)
return false;
bool imageAcquired = !failures[i];
string expectedStatus = imageAcquired ? "Success" : "Failed";
if (posResult.Status != expectedStatus) return false;
}
return true;