Files
XplorePlane/XplorePlane.Tests/Services/CncProgramServiceTests.cs
T
zhengxuan.zhang b95941cb50 feat: CNC 编排新增硬件控制能力(位置硬件还原 + 四类动作节点)
在 CNC 编排层补齐 P0 硬件控制能力,使检测编排可覆盖设备测试与校准流程。

位置节点硬件状态还原(核心):
- SavePositionNode 新增可选探测器采集参数快照,兼容旧 .xp 文件
- 执行到位后按序还原射线源电压/电流与探测器参数(先电压后电流)
- 射线未开弹窗提醒并暂停;还原失败弹窗提示并暂停不继续;服务不可用降级跳过
- 新增 ICncInteractionService 封装弹窗交互,便于单元测试

四类硬件动作节点:
- 射线源控制:开/关/设参/暖机/训机,安全时序 + 稳定等待
- 探测器校正:暗场/增益/坏像素/自动
- 安全门控制:开门/关门,复用内置联锁
- 几何设置:按 FDD 或放大倍率反算定位

模型、编辑器与工具栏接线:
- CncNodeType 追加四枚举值 + 四类节点 record + 多态判别符 + 值域校验
- 编辑器新增插入命令、类型判定、图标与工具栏入口
- IDetectorService 预留当前采集参数读取接口(暂返回 null,待硬件层完善)

系统设置:
- 新增数据存储根目录与日志目录配置及浏览按钮
- 修复保存时重复打开配置文件导致的配置冲突异常

测试:
- 新增 33 个单元测试,覆盖序列化往返、旧文件兼容、节点分派/降级/失败、
  位置还原全路径与混合程序执行
2026-07-03 14:09:24 +08:00

216 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using Moq;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Services;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.Permission;
using Xunit;
namespace XplorePlane.Tests.Services
{
public class CncProgramServiceTests
{
[Fact]
public void SerializeDeserialize_SavePosition_PreservesSaveImage()
{
var appState = new Mock<IAppStateService>();
appState.SetupGet(s => s.MotionState).Returns(MotionState.Default);
appState.SetupGet(s => s.RaySourceState).Returns(RaySourceState.Default);
appState.SetupGet(s => s.DetectorState).Returns(DetectorState.Default);
var raySource = new Mock<IRaySourceService>();
var logger = new Mock<ILoggerService>();
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
var permissionService = new Mock<IPermissionService>();
permissionService.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
var service = new CncProgramService(appState.Object, raySource.Object, logger.Object, permissionService.Object);
var program = new CncProgram(
Guid.NewGuid(),
"Program",
DateTime.UtcNow,
DateTime.UtcNow,
new List<CncNode>
{
new SavePositionNode(Guid.NewGuid(), 0, "保存位置_0", MotionState.Default, SaveImage: true)
}.AsReadOnly());
var json = service.Serialize(program);
var deserialized = service.Deserialize(json);
var savePosition = Assert.IsType<SavePositionNode>(Assert.Single(deserialized.Nodes));
Assert.True(savePosition.SaveImage);
}
private static CncProgramService CreateService()
{
var appState = new Mock<IAppStateService>();
appState.SetupGet(s => s.MotionState).Returns(MotionState.Default);
appState.SetupGet(s => s.RaySourceState).Returns(RaySourceState.Default);
appState.SetupGet(s => s.DetectorState).Returns(DetectorState.Default);
var raySource = new Mock<IRaySourceService>();
var logger = new Mock<ILoggerService>();
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
var permissionService = new Mock<IPermissionService>();
permissionService.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
return new CncProgramService(appState.Object, raySource.Object, logger.Object, permissionService.Object);
}
private static CncProgram Roundtrip(CncProgramService service, params CncNode[] nodes)
{
var program = new CncProgram(
Guid.NewGuid(), "Program", DateTime.UtcNow, DateTime.UtcNow,
new List<CncNode>(nodes).AsReadOnly());
var json = service.Serialize(program);
return service.Deserialize(json);
}
[Fact]
public void SerializeDeserialize_RaySourceControlNode_PreservesAllFields()
{
var service = CreateService();
var node = new RaySourceControlNode(Guid.NewGuid(), 0, "射线源控制_0",
RaySourceAction.TurnOn, Voltage: 120, Current: 200, WaitForStable: true, StableTimeoutMs: 8000);
var result = Assert.IsType<RaySourceControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(RaySourceAction.TurnOn, result.Action);
Assert.Equal(120, result.Voltage);
Assert.Equal(200, result.Current);
Assert.True(result.WaitForStable);
Assert.Equal(8000, result.StableTimeoutMs);
}
[Fact]
public void SerializeDeserialize_DetectorCorrectionNode_PreservesAllFields()
{
var service = CreateService();
var node = new DetectorCorrectionNode(Guid.NewGuid(), 0, "探测器校正_0",
DetectorCorrectionType.Dark, FrameCount: 20);
var result = Assert.IsType<DetectorCorrectionNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(DetectorCorrectionType.Dark, result.CorrectionType);
Assert.Equal(20, result.FrameCount);
}
[Fact]
public void SerializeDeserialize_DoorControlNode_PreservesAllFields()
{
var service = CreateService();
var node = new DoorControlNode(Guid.NewGuid(), 0, "安全门_0",
DoorAction.Open, WaitForComplete: true, TimeoutMs: 12000);
var result = Assert.IsType<DoorControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(DoorAction.Open, result.Action);
Assert.True(result.WaitForComplete);
Assert.Equal(12000, result.TimeoutMs);
}
[Fact]
public void SerializeDeserialize_GeometryNode_PreservesAllFields()
{
var service = CreateService();
var node = new GeometryNode(Guid.NewGuid(), 0, "几何_0",
GeometryMode.ByMagnification, Fod: 50.5, Fdd: 0, Magnification: 4.0, WaitForSettled: true);
var result = Assert.IsType<GeometryNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(GeometryMode.ByMagnification, result.Mode);
Assert.Equal(50.5, result.Fod);
Assert.Equal(4.0, result.Magnification);
Assert.True(result.WaitForSettled);
}
[Fact]
public void SerializeDeserialize_SavePositionWithDetectorAcq_PreservesSnapshot()
{
var service = CreateService();
var node = new SavePositionNode(Guid.NewGuid(), 0, "检测位置_0", MotionState.Default,
RaySourceState: new RaySourceState(true, 100, 150),
SaveImage: true,
DetectorAcq: new DetectorAcquisitionSnapshot(BinningIndex: 1, Pga: 3, FrameRate: 15m));
var result = Assert.IsType<SavePositionNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.NotNull(result.DetectorAcq);
Assert.Equal(1, result.DetectorAcq.BinningIndex);
Assert.Equal(3, result.DetectorAcq.Pga);
Assert.Equal(15m, result.DetectorAcq.FrameRate);
Assert.Equal(100, result.RaySourceState.Voltage);
Assert.Equal(150, result.RaySourceState.Power);
}
[Fact]
public void Deserialize_LegacySavePositionWithoutDetectorAcq_LoadsWithNullSnapshot()
{
var service = CreateService();
// 模拟旧 .xpSavePosition 节点不含 DetectorAcq 字段
var legacyJson = @"{
""Id"": ""11111111-1111-1111-1111-111111111111"",
""Name"": ""LegacyProgram"",
""CreatedAt"": ""2024-01-01T00:00:00Z"",
""UpdatedAt"": ""2024-01-01T00:00:00Z"",
""Nodes"": [
{
""$type"": ""SavePosition"",
""Id"": ""22222222-2222-2222-2222-222222222222"",
""Index"": 0,
""Name"": ""检测位置_0"",
""MotionState"": null,
""SaveImage"": true
}
]
}";
var deserialized = service.Deserialize(legacyJson);
var savePosition = Assert.IsType<SavePositionNode>(Assert.Single(deserialized.Nodes));
Assert.Null(savePosition.DetectorAcq);
Assert.True(savePosition.SaveImage);
}
[Fact]
public void Deserialize_DetectorCorrectionFrameCountOutOfRange_IsClamped()
{
var service = CreateService();
// FrameCount 超上限(9999)应被截断到 1000
var node = new DetectorCorrectionNode(Guid.NewGuid(), 0, "校正", DetectorCorrectionType.Dark, FrameCount: 9999);
var result = Assert.IsType<DetectorCorrectionNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(1000, result.FrameCount);
}
[Fact]
public void Deserialize_RaySourceStableTimeoutOutOfRange_IsClamped()
{
var service = CreateService();
// StableTimeoutMs 超上限(999999)应被截断到 600000
var node = new RaySourceControlNode(Guid.NewGuid(), 0, "射线源", RaySourceAction.TurnOn, 120, 200, WaitForStable: true, StableTimeoutMs: 999999);
var result = Assert.IsType<RaySourceControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
Assert.Equal(600000, result.StableTimeoutMs);
}
[Fact]
public void SerializeDeserialize_MixedHardwareProgram_PreservesAllNodes()
{
var service = CreateService();
var geo = new GeometryNode(Guid.NewGuid(), 0, "几何", GeometryMode.ByFdd, 50, 200);
var pos = new SavePositionNode(Guid.NewGuid(), 1, "位置", MotionState.Default,
RaySourceState: new RaySourceState(true, 100, 150),
DetectorAcq: new DetectorAcquisitionSnapshot(1, 2, 15m));
var corr = new DetectorCorrectionNode(Guid.NewGuid(), 2, "校正", DetectorCorrectionType.Auto, 10);
var rt = Roundtrip(service, geo, pos, corr);
Assert.Equal(3, rt.Nodes.Count);
Assert.IsType<GeometryNode>(rt.Nodes[0]);
var posRt = Assert.IsType<SavePositionNode>(rt.Nodes[1]);
Assert.IsType<DetectorCorrectionNode>(rt.Nodes[2]);
Assert.NotNull(posRt.DetectorAcq);
Assert.Equal(15m, posRt.DetectorAcq.FrameRate);
}
}
}