302 lines
13 KiB
C#
302 lines
13 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 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.Select(n => (CncNode)n).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.Select(n => (CncNode)n).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.Select(n => (CncNode)n).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;
|
|
});
|
|
}
|
|
}
|
|
}
|