210 lines
8.8 KiB
C#
210 lines
8.8 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>
|
|
/// Property 7: 进度报告包含正确的索引和总数
|
|
/// **Validates: Requirements 3.4**
|
|
///
|
|
/// For any CNC program with N SavePositionNodes, the CNC_Execution_Service SHALL report
|
|
/// progress N times (or fewer if cancelled), each report containing a 0-based position index
|
|
/// and the total count N, with indices strictly increasing from 0.
|
|
/// </summary>
|
|
public class ProgressReportPropertyTests
|
|
{
|
|
private static (CncExecutionService Service, Mock<IMainViewportService> MainViewport)
|
|
CreateServiceWithImage()
|
|
{
|
|
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());
|
|
|
|
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);
|
|
|
|
// Provide a valid image so that image acquisition succeeds
|
|
var dummyImage = CreateDummyBitmap();
|
|
mockMainViewportService.SetupGet(m => m.LatestManualImage).Returns((ImageSource)null);
|
|
mockMainViewportService.SetupGet(m => m.CurrentDisplayImage).Returns(dummyImage);
|
|
|
|
// Image persistence returns 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));
|
|
|
|
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, mockMainViewportService);
|
|
}
|
|
|
|
private static BitmapSource CreateDummyBitmap()
|
|
{
|
|
var stride = 4 * 4; // 4 pixels wide, 4 bytes per pixel
|
|
var pixels = new byte[stride * 4]; // 4x4 image
|
|
var bitmap = BitmapSource.Create(4, 4, 96, 96, PixelFormats.Bgra32, null, pixels, stride);
|
|
bitmap.Freeze();
|
|
return bitmap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a CncProgram with N SavePositionNodes (N between 1 and 10),
|
|
/// each with unique ascending indices.
|
|
/// </summary>
|
|
private static Arbitrary<CncProgram> SavePositionProgramArb()
|
|
{
|
|
var gen =
|
|
from count in Gen.Choose(1, 10)
|
|
from name in ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get)
|
|
from nodes in GenSavePositionNodes(count)
|
|
select new CncProgram(
|
|
Guid.NewGuid(), name,
|
|
DateTime.UtcNow, DateTime.UtcNow,
|
|
nodes.AsReadOnly());
|
|
return gen.ToArbitrary();
|
|
}
|
|
|
|
private static Gen<List<CncNode>> GenSavePositionNodes(int count)
|
|
{
|
|
Gen<List<CncNode>> acc = Gen.Constant(new List<CncNode>());
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var idx = i;
|
|
acc = from list in acc
|
|
from nodeName in ArbMap.Default.GeneratorFor<NonEmptyString>().Select(s => s.Get)
|
|
let node = new SavePositionNode(
|
|
Guid.NewGuid(), idx, $"Pos_{nodeName}_{idx}",
|
|
MotionState.Default, SaveImage: false)
|
|
select new List<CncNode>(list) { node };
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
// ── Property 7: 进度报告包含正确的索引和总数 ──────────────────────────
|
|
|
|
// Feature: cnc-multi-position-image-capture, Property 7: 进度报告包含正确的索引和总数
|
|
// Validates: Requirements 3.4
|
|
[Property(MaxTest = 100)]
|
|
public Property ProgressReports_ContainCorrectIndexAndTotal()
|
|
{
|
|
return Prop.ForAll(
|
|
SavePositionProgramArb(),
|
|
program =>
|
|
{
|
|
var (service, _) = CreateServiceWithImage();
|
|
|
|
var savePositionCount = program.Nodes.OfType<SavePositionNode>().Count();
|
|
|
|
// Capture all progress reports that have PositionIndex set (Running state)
|
|
var runningReports = new List<(int PositionIndex, int TotalPositions)>();
|
|
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
|
|
{
|
|
if (p.State == NodeExecutionState.Running
|
|
&& p.PositionIndex.HasValue
|
|
&& p.TotalPositions.HasValue)
|
|
{
|
|
runningReports.Add((p.PositionIndex.Value, p.TotalPositions.Value));
|
|
}
|
|
});
|
|
|
|
service.ExecuteAsync(program, progress, CancellationToken.None)
|
|
.GetAwaiter().GetResult();
|
|
|
|
// Verify: at least N Running progress reports (one per position)
|
|
if (runningReports.Count < savePositionCount)
|
|
return false;
|
|
|
|
// Verify: all TotalPositions values equal N
|
|
if (runningReports.Any(r => r.TotalPositions != savePositionCount))
|
|
return false;
|
|
|
|
// Verify: PositionIndex values in Running reports are strictly increasing from 0
|
|
// Extract unique position indices in order of first appearance
|
|
var seenIndices = new List<int>();
|
|
foreach (var report in runningReports)
|
|
{
|
|
if (seenIndices.Count == 0 || seenIndices.Last() != report.PositionIndex)
|
|
seenIndices.Add(report.PositionIndex);
|
|
}
|
|
|
|
// Must have exactly N distinct position indices
|
|
if (seenIndices.Count != savePositionCount)
|
|
return false;
|
|
|
|
// Indices must be 0, 1, 2, ..., N-1
|
|
for (int i = 0; i < savePositionCount; i++)
|
|
{
|
|
if (seenIndices[i] != i)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
}
|