344 lines
15 KiB
C#
344 lines
15 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 XplorePlane.ViewModels;
|
|
using Xunit;
|
|
|
|
namespace XplorePlane.Tests.Services
|
|
{
|
|
/// <summary>
|
|
/// Property 6: 采集图像传递给紧邻的 InspectionModuleNode
|
|
/// Feature: cnc-multi-position-image-capture
|
|
/// **Validates: Requirements 3.2**
|
|
///
|
|
/// For any CNC program where a SavePositionNode is immediately followed (by Index order)
|
|
/// by an InspectionModuleNode, the image acquired at that SavePositionNode SHALL be passed
|
|
/// as the source image to the InspectionModuleNode's pipeline execution.
|
|
/// </summary>
|
|
public class ImagePassingPropertyTests
|
|
{
|
|
// ── Helper: Create a 1x1 BitmapSource for testing ──────────────────────
|
|
|
|
private static BitmapSource CreateTestBitmap()
|
|
{
|
|
var bitmap = new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgr32, null);
|
|
bitmap.Freeze();
|
|
return bitmap;
|
|
}
|
|
|
|
// ── Generator: CNC programs with SavePositionNodes optionally followed by InspectionModuleNodes ──
|
|
|
|
private static Gen<string> NonEmptyStringGen =>
|
|
ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get);
|
|
|
|
private static Gen<PipelineModel> PipelineGen =>
|
|
from name in NonEmptyStringGen
|
|
select new PipelineModel
|
|
{
|
|
Name = name,
|
|
Nodes = new List<PipelineNodeModel>
|
|
{
|
|
new PipelineNodeModel { OperatorKey = "test_op", Order = 0, IsEnabled = true }
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Generates a CNC program where some SavePositionNodes are followed by InspectionModuleNodes
|
|
/// and some are not. Returns the program along with metadata about which pairs exist.
|
|
/// </summary>
|
|
private static Gen<CncProgram> CncProgramWithMixedFollowersGen()
|
|
{
|
|
return from saveCount in Gen.Choose(1, 5)
|
|
from followFlags in Gen.ListOf<bool>(ArbMap.Default.GeneratorFor<bool>(), saveCount)
|
|
from programName in NonEmptyStringGen
|
|
let nodes = BuildMixedNodes(saveCount, followFlags)
|
|
select new CncProgram(
|
|
Guid.NewGuid(),
|
|
programName,
|
|
DateTime.UtcNow,
|
|
DateTime.UtcNow,
|
|
nodes.AsReadOnly());
|
|
}
|
|
|
|
private static List<CncNode> BuildMixedNodes(int saveCount, IReadOnlyList<bool> followFlags)
|
|
{
|
|
var nodes = new List<CncNode>();
|
|
int index = 0;
|
|
|
|
for (int i = 0; i < saveCount; i++)
|
|
{
|
|
// Add a SavePositionNode
|
|
var spNode = new SavePositionNode(
|
|
Guid.NewGuid(),
|
|
index,
|
|
$"Position_{i}",
|
|
MotionState.Default,
|
|
SaveImage: false,
|
|
ManualImagePath: "");
|
|
nodes.Add(spNode);
|
|
index++;
|
|
|
|
// Optionally follow with an InspectionModuleNode
|
|
if (i < followFlags.Count && followFlags[i])
|
|
{
|
|
var pipeline = new PipelineModel
|
|
{
|
|
Name = $"Pipeline_{i}",
|
|
Nodes = new List<PipelineNodeModel>
|
|
{
|
|
new PipelineNodeModel { OperatorKey = "test_op", Order = 0, IsEnabled = true }
|
|
}
|
|
};
|
|
var inspNode = new InspectionModuleNode(
|
|
Guid.NewGuid(),
|
|
index,
|
|
$"Inspect_{i}",
|
|
pipeline);
|
|
nodes.Add(inspNode);
|
|
index++;
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
// ── Service Factory ──────────────────────────────────────────────────────
|
|
|
|
private static (
|
|
CncExecutionService Service,
|
|
Mock<IPipelineExecutionService> PipelineExec,
|
|
Mock<IMainViewportService> MainViewport,
|
|
Mock<IImagePersistenceService> ImagePersistence,
|
|
Mock<ILoggerService> Logger)
|
|
CreateServiceWithCapture(BitmapSource detectorImage)
|
|
{
|
|
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());
|
|
|
|
// Return the detector image from the viewport service (simulates detector acquisition)
|
|
mockMainViewportService
|
|
.Setup(m => m.CurrentDisplayImage)
|
|
.Returns(detectorImage);
|
|
|
|
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);
|
|
|
|
mockImagePersistenceService
|
|
.Setup(s => s.SaveImageAsync(
|
|
It.IsAny<byte[]>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ImageSaveResult(true, "test.bmp", 100));
|
|
|
|
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, mockPipelineExecutionService, mockMainViewportService, mockImagePersistenceService, mockLogger);
|
|
}
|
|
|
|
// ── Property 6: 采集图像传递给紧邻的 InspectionModuleNode ──────────────
|
|
|
|
// Feature: cnc-multi-position-image-capture, Property 6: 采集图像传递给紧邻的 InspectionModuleNode
|
|
// Validates: Requirements 3.2
|
|
[Property(MaxTest = 100)]
|
|
public Property AcquiredImage_PassedToFollowingInspectionModuleNode()
|
|
{
|
|
return Prop.ForAll(
|
|
CncProgramWithMixedFollowersGen().ToArbitrary(),
|
|
program =>
|
|
{
|
|
var detectorImage = CreateTestBitmap();
|
|
var (service, mockPipelineExec, _, _, _) = CreateServiceWithCapture(detectorImage);
|
|
|
|
// Track all pipeline execution calls with their source images
|
|
var pipelineCalls = new List<BitmapSource>();
|
|
|
|
mockPipelineExec
|
|
.Setup(p => p.ExecutePipelineAsync(
|
|
It.IsAny<IEnumerable<PipelineNodeViewModel>>(),
|
|
It.IsAny<BitmapSource>(),
|
|
It.IsAny<IProgress<PipelineProgress>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<PipelineNodeViewModel>, BitmapSource, IProgress<PipelineProgress>, CancellationToken>(
|
|
(nodes, source, progress, ct) =>
|
|
{
|
|
pipelineCalls.Add(source);
|
|
})
|
|
.ReturnsAsync(new PipelineExecutionResult(detectorImage, null));
|
|
|
|
service.ExecuteAsync(program, null, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
// Determine expected behavior from the program structure
|
|
var allNodesOrdered = program.Nodes.OrderBy(n => n.Index).ToList();
|
|
var savePositionNodes = allNodesOrdered.OfType<SavePositionNode>().ToList();
|
|
|
|
// Count SavePositionNodes that are immediately followed by InspectionModuleNode
|
|
int savePositionFollowedByInspection = 0;
|
|
foreach (var sp in savePositionNodes)
|
|
{
|
|
int spOrderIndex = allNodesOrdered.FindIndex(n => n.Id == sp.Id);
|
|
if (spOrderIndex >= 0 && spOrderIndex + 1 < allNodesOrdered.Count)
|
|
{
|
|
var nextNode = allNodesOrdered[spOrderIndex + 1];
|
|
if (nextNode is InspectionModuleNode)
|
|
savePositionFollowedByInspection++;
|
|
}
|
|
}
|
|
|
|
// Property verification:
|
|
// 1. When a SavePositionNode is followed by InspectionModuleNode, pipeline MUST be called
|
|
// at least once with the detector image (from the multi-position loop)
|
|
// 2. All pipeline calls must receive the detector image as source
|
|
// (since all images come from the same detector mock)
|
|
|
|
// If there are SavePositionNodes followed by InspectionModuleNodes,
|
|
// the pipeline must have been called at least that many times
|
|
bool pipelineCalledForFollowedNodes = pipelineCalls.Count >= savePositionFollowedByInspection;
|
|
|
|
// Every pipeline call must have received the detector image
|
|
bool allReceivedCorrectImage = pipelineCalls.All(img =>
|
|
ReferenceEquals(img, detectorImage));
|
|
|
|
return pipelineCalledForFollowedNodes && allReceivedCorrectImage;
|
|
});
|
|
}
|
|
|
|
// ── Property 6 (negative case): SavePositionNode NOT followed by InspectionModuleNode ──
|
|
|
|
// Feature: cnc-multi-position-image-capture, Property 6 (negative): No pipeline execution for non-followed SavePositionNodes
|
|
// Validates: Requirements 3.2
|
|
[Property(MaxTest = 100)]
|
|
public Property NoPipelineExecution_WhenNoFollowingInspectionModuleNode()
|
|
{
|
|
// Generate programs where NO SavePositionNode is followed by an InspectionModuleNode
|
|
var gen = from saveCount in Gen.Choose(1, 5)
|
|
from programName in NonEmptyStringGen
|
|
let nodes = BuildNodesWithoutFollowingInspection(saveCount)
|
|
select new CncProgram(
|
|
Guid.NewGuid(),
|
|
programName,
|
|
DateTime.UtcNow,
|
|
DateTime.UtcNow,
|
|
nodes.AsReadOnly());
|
|
|
|
return Prop.ForAll(
|
|
gen.ToArbitrary(),
|
|
program =>
|
|
{
|
|
var detectorImage = CreateTestBitmap();
|
|
var (service, mockPipelineExec, _, _, _) = CreateServiceWithCapture(detectorImage);
|
|
|
|
int pipelineCallCount = 0;
|
|
mockPipelineExec
|
|
.Setup(p => p.ExecutePipelineAsync(
|
|
It.IsAny<IEnumerable<PipelineNodeViewModel>>(),
|
|
It.IsAny<BitmapSource>(),
|
|
It.IsAny<IProgress<PipelineProgress>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<PipelineNodeViewModel>, BitmapSource, IProgress<PipelineProgress>, CancellationToken>(
|
|
(_, _, _, _) => Interlocked.Increment(ref pipelineCallCount))
|
|
.ReturnsAsync(new PipelineExecutionResult(detectorImage, null));
|
|
|
|
service.ExecuteAsync(program, null, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
// No pipeline execution should occur for SavePositionNodes
|
|
// (InspectionModuleNodes in the general loop may still trigger pipeline,
|
|
// but since we only have SavePositionNodes and non-inspection nodes, count should be 0)
|
|
return pipelineCallCount == 0;
|
|
});
|
|
}
|
|
|
|
private static List<CncNode> BuildNodesWithoutFollowingInspection(int saveCount)
|
|
{
|
|
var nodes = new List<CncNode>();
|
|
int index = 0;
|
|
|
|
for (int i = 0; i < saveCount; i++)
|
|
{
|
|
// Add a SavePositionNode
|
|
nodes.Add(new SavePositionNode(
|
|
Guid.NewGuid(),
|
|
index,
|
|
$"Position_{i}",
|
|
MotionState.Default,
|
|
SaveImage: false,
|
|
ManualImagePath: ""));
|
|
index++;
|
|
|
|
// Follow with a non-inspection node (e.g., WaitDelayNode with 0ms)
|
|
nodes.Add(new WaitDelayNode(
|
|
Guid.NewGuid(),
|
|
index,
|
|
$"Wait_{i}",
|
|
0));
|
|
index++;
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
}
|
|
}
|