位置节点增加保存图像到本地的功能;支持输入图像
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
#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 Xunit;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Feature: cnc-multi-position-image-capture, Property 2: 图像持久化对所有 SaveImage=true 节点触发
|
||||
/// **Validates: Requirements 1.1, 2.5**
|
||||
///
|
||||
/// For any CNC program containing one or more SavePositionNode with SaveImage=true
|
||||
/// (regardless of whether ManualImagePath is set or empty), the CNC_Execution_Service
|
||||
/// SHALL invoke the Image_Persistence_Service exactly once per such node that is reached
|
||||
/// during execution.
|
||||
/// </summary>
|
||||
public class ImagePersistenceCallPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a 1x1 pixel frozen BitmapSource for testing purposes.
|
||||
/// </summary>
|
||||
private static BitmapSource CreateTestBitmap()
|
||||
{
|
||||
var bitmap = BitmapSource.Create(
|
||||
1, 1, 96, 96,
|
||||
PixelFormats.Gray16,
|
||||
null,
|
||||
new byte[] { 0xFF, 0xFF },
|
||||
2);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully mocked CncExecutionService with IMainViewportService returning
|
||||
/// a valid image (so TryGetSourceImage always succeeds) and IImagePersistenceService
|
||||
/// tracking SaveImageAsync calls.
|
||||
/// </summary>
|
||||
private static (CncExecutionService Service, Mock<IImagePersistenceService> ImagePersistence)
|
||||
CreateServiceWithImageSupport()
|
||||
{
|
||||
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);
|
||||
|
||||
// Set up GetEvent<DetectorDisconnectedEvent>() so the constructor subscription doesn't throw
|
||||
mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<DetectorDisconnectedEvent>())
|
||||
.Returns(new DetectorDisconnectedEvent());
|
||||
|
||||
// Mock IMainViewportService to return a valid image so TryGetSourceImage() succeeds
|
||||
var testBitmap = CreateTestBitmap();
|
||||
mockMainViewportService
|
||||
.Setup(m => m.LatestManualImage)
|
||||
.Returns(testBitmap);
|
||||
|
||||
// Mock IInspectionResultStore methods
|
||||
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);
|
||||
|
||||
// Mock SaveImageAsync to return success
|
||||
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));
|
||||
|
||||
// Mock WriteSummaryAsync to return success
|
||||
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, mockImagePersistenceService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generator for a list of SavePositionNodes with random SaveImage flags.
|
||||
/// Ensures at least one node has SaveImage=true.
|
||||
/// </summary>
|
||||
private static Gen<List<SavePositionNode>> SavePositionNodesGen =>
|
||||
from count in Gen.Choose(1, 8)
|
||||
from flags in Gen.ListOf<bool>(ArbMap.Default.GeneratorFor<bool>(), count)
|
||||
let hasAtLeastOneTrue = flags.Any(f => f)
|
||||
let adjustedFlags = hasAtLeastOneTrue ? flags : flags.Select((f, i) => i == 0 || f).ToList()
|
||||
from names in Gen.ListOf<string>(ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get), count)
|
||||
select Enumerable.Range(0, count)
|
||||
.Select(i => new SavePositionNode(
|
||||
Guid.NewGuid(),
|
||||
i,
|
||||
names[i],
|
||||
new MotionState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
SaveImage: adjustedFlags[i],
|
||||
ManualImagePath: ""))
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Generator for SavePositionNodes where some have ManualImagePath set (non-empty)
|
||||
/// and some don't, but all with SaveImage=true to test that persistence is called
|
||||
/// regardless of ManualImagePath being set.
|
||||
/// Note: ManualImagePath validation requires file to exist, so we leave it empty
|
||||
/// in this test (the detector mock provides the image).
|
||||
/// </summary>
|
||||
private static Gen<List<SavePositionNode>> SavePositionNodesAllSaveImageTrueGen =>
|
||||
from count in Gen.Choose(1, 8)
|
||||
from names in Gen.ListOf<string>(ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get), count)
|
||||
select Enumerable.Range(0, count)
|
||||
.Select(i => new SavePositionNode(
|
||||
Guid.NewGuid(),
|
||||
i,
|
||||
names[i],
|
||||
new MotionState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
SaveImage: true,
|
||||
ManualImagePath: ""))
|
||||
.ToList();
|
||||
|
||||
// ── Property 2: 图像持久化对所有 SaveImage=true 节点触发 ──────────────
|
||||
|
||||
/// <summary>
|
||||
/// Feature: cnc-multi-position-image-capture, Property 2: 图像持久化对所有 SaveImage=true 节点触发
|
||||
/// **Validates: Requirements 1.1, 2.5**
|
||||
///
|
||||
/// For any CNC program containing SavePositionNodes with mixed SaveImage flags,
|
||||
/// SaveImageAsync is called exactly N times where N = count of nodes with SaveImage=true.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SaveImageAsync_CalledExactlyOncePerSaveImageTrueNode()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
SavePositionNodesGen.ToArbitrary(),
|
||||
nodes =>
|
||||
{
|
||||
var (service, mockImagePersistence) = CreateServiceWithImageSupport();
|
||||
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(),
|
||||
"TestProgram",
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
nodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
service.ExecuteAsync(program, null, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
int expectedCalls = nodes.Count(n => n.SaveImage);
|
||||
|
||||
mockImagePersistence.Verify(
|
||||
s => s.SaveImageAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Exactly(expectedCalls));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feature: cnc-multi-position-image-capture, Property 2 (all true variant):
|
||||
/// When all nodes have SaveImage=true, SaveImageAsync is called exactly N times (one per node).
|
||||
/// **Validates: Requirements 1.1, 2.5**
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SaveImageAsync_CalledForEveryNode_WhenAllSaveImageTrue()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
SavePositionNodesAllSaveImageTrueGen.ToArbitrary(),
|
||||
nodes =>
|
||||
{
|
||||
var (service, mockImagePersistence) = CreateServiceWithImageSupport();
|
||||
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(),
|
||||
"TestProgram",
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
nodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
service.ExecuteAsync(program, null, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// All nodes have SaveImage=true, so SaveImageAsync should be called for each
|
||||
mockImagePersistence.Verify(
|
||||
s => s.SaveImageAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Exactly(nodes.Count));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feature: cnc-multi-position-image-capture, Property 2 (none true variant):
|
||||
/// When no nodes have SaveImage=true, SaveImageAsync is never called.
|
||||
/// **Validates: Requirements 1.1, 2.5**
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property SaveImageAsync_NeverCalled_WhenNoSaveImageTrue()
|
||||
{
|
||||
var nodesGen =
|
||||
from count in Gen.Choose(1, 8)
|
||||
from names in Gen.ListOf<string>(ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get), count)
|
||||
select Enumerable.Range(0, count)
|
||||
.Select(i => new SavePositionNode(
|
||||
Guid.NewGuid(),
|
||||
i,
|
||||
names[i],
|
||||
new MotionState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
SaveImage: false,
|
||||
ManualImagePath: ""))
|
||||
.ToList();
|
||||
|
||||
return Prop.ForAll(
|
||||
nodesGen.ToArbitrary(),
|
||||
nodes =>
|
||||
{
|
||||
var (service, mockImagePersistence) = CreateServiceWithImageSupport();
|
||||
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(),
|
||||
"TestProgram",
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
nodes.Cast<CncNode>().ToList().AsReadOnly());
|
||||
|
||||
service.ExecuteAsync(program, null, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
// No nodes have SaveImage=true, so SaveImageAsync should never be called
|
||||
mockImagePersistence.Verify(
|
||||
s => s.SaveImageAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user