位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
#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<(Guid NodeId, BitmapSource SourceImage)>();
|
||||
|
||||
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) =>
|
||||
{
|
||||
// We can't easily get the NodeId from here, but we track the source image
|
||||
pipelineCalls.Add((Guid.Empty, source));
|
||||
})
|
||||
.ReturnsAsync(detectorImage);
|
||||
|
||||
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();
|
||||
|
||||
int expectedPipelineCalls = 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)
|
||||
expectedPipelineCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify: pipeline was called exactly for each SavePositionNode followed by InspectionModuleNode
|
||||
bool correctCallCount = pipelineCalls.Count == expectedPipelineCalls;
|
||||
|
||||
// Verify: each pipeline call received a non-null source image (the detector image)
|
||||
bool allReceivedImage = pipelineCalls.All(c => c.SourceImage != null);
|
||||
|
||||
// Verify: each pipeline call received the same detector image
|
||||
bool allReceivedCorrectImage = pipelineCalls.All(c =>
|
||||
ReferenceEquals(c.SourceImage, detectorImage));
|
||||
|
||||
return correctCallCount && allReceivedImage && 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(detectorImage);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user