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