304 lines
13 KiB
C#
304 lines
13 KiB
C#
using Moq;
|
||
using Prism.Events;
|
||
using System;
|
||
using System.Windows;
|
||
using XP.Common.Logging.Interfaces;
|
||
using XP.Hardware.Detector.Abstractions.Enums;
|
||
using XP.Hardware.Detector.Abstractions.Events;
|
||
using XP.Hardware.Detector.Services;
|
||
using XP.Hardware.MotionControl.Abstractions;
|
||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||
using XP.Hardware.MotionControl.Abstractions.Events;
|
||
using XP.Hardware.MotionControl.Services;
|
||
using XP.Hardware.RaySource.Services;
|
||
using XplorePlane.Models;
|
||
using XplorePlane.Services.AppState;
|
||
using Xunit;
|
||
using Xunit.Abstractions;
|
||
|
||
namespace XplorePlane.Tests.Services
|
||
{
|
||
/// <summary>
|
||
/// AppStateService unit tests.
|
||
/// Verifies default values, null guards, dispose behavior, and hardware-driven motion-state sync.
|
||
/// </summary>
|
||
public class AppStateServiceTests : IDisposable
|
||
{
|
||
private readonly AppStateService _service;
|
||
private readonly Mock<IRaySourceService> _mockRaySource;
|
||
private readonly Mock<IMotionSystem> _mockMotionSystem;
|
||
private readonly Mock<IMotionControlService> _mockMotionControlService;
|
||
private readonly Mock<IDetectorService> _mockDetectorService;
|
||
private readonly Mock<ILinearAxis> _mockStageX;
|
||
private readonly Mock<ILinearAxis> _mockStageY;
|
||
private readonly Mock<ILinearAxis> _mockSourceZ;
|
||
private readonly Mock<ILinearAxis> _mockDetectorZ;
|
||
private readonly Mock<IRotaryAxis> _mockDetectorSwing;
|
||
private readonly Mock<IRotaryAxis> _mockStageRotation;
|
||
private readonly Mock<IRotaryAxis> _mockFixtureRotation;
|
||
private readonly Mock<ILoggerService> _mockLogger;
|
||
private readonly EventAggregator _eventAggregator;
|
||
private readonly ITestOutputHelper _output;
|
||
|
||
public AppStateServiceTests(ITestOutputHelper output)
|
||
{
|
||
_output = output;
|
||
|
||
if (Application.Current == null)
|
||
{
|
||
new Application();
|
||
}
|
||
|
||
_mockRaySource = new Mock<IRaySourceService>();
|
||
_mockMotionSystem = new Mock<IMotionSystem>();
|
||
_mockMotionControlService = new Mock<IMotionControlService>();
|
||
_mockDetectorService = new Mock<IDetectorService>();
|
||
_mockStageX = CreateLinearAxis(AxisId.StageX, 0);
|
||
_mockStageY = CreateLinearAxis(AxisId.StageY, 0);
|
||
_mockSourceZ = CreateLinearAxis(AxisId.SourceZ, 0);
|
||
_mockDetectorZ = CreateLinearAxis(AxisId.DetectorZ, 0);
|
||
_mockDetectorSwing = CreateRotaryAxis(RotaryAxisId.DetectorSwing, 0);
|
||
_mockStageRotation = CreateRotaryAxis(RotaryAxisId.StageRotation, 0);
|
||
_mockFixtureRotation = CreateRotaryAxis(RotaryAxisId.FixtureRotation, 0);
|
||
_mockLogger = new Mock<ILoggerService>();
|
||
_eventAggregator = new EventAggregator();
|
||
|
||
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageX)).Returns(_mockStageX.Object);
|
||
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageY)).Returns(_mockStageY.Object);
|
||
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.SourceZ)).Returns(_mockSourceZ.Object);
|
||
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.DetectorZ)).Returns(_mockDetectorZ.Object);
|
||
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.DetectorSwing)).Returns(_mockDetectorSwing.Object);
|
||
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.StageRotation)).Returns(_mockStageRotation.Object);
|
||
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.FixtureRotation)).Returns(_mockFixtureRotation.Object);
|
||
|
||
_mockMotionControlService
|
||
.Setup(x => x.GetCurrentGeometry())
|
||
.Returns((0d, 0d, 1d));
|
||
|
||
// DetectorService:GetInfo 在未初始化时抛出,模拟此行为
|
||
_mockDetectorService
|
||
.Setup(x => x.GetInfo())
|
||
.Throws(new InvalidOperationException("探测器未初始化"));
|
||
_mockDetectorService
|
||
.SetupGet(x => x.Status)
|
||
.Returns(DetectorStatus.Uninitialized);
|
||
_mockDetectorService
|
||
.SetupGet(x => x.IsConnected)
|
||
.Returns(false);
|
||
|
||
_mockLogger.Setup(l => l.ForModule<AppStateService>()).Returns(_mockLogger.Object);
|
||
|
||
_service = new AppStateService(
|
||
_mockRaySource.Object,
|
||
_mockMotionSystem.Object,
|
||
_mockMotionControlService.Object,
|
||
_mockDetectorService.Object,
|
||
_eventAggregator,
|
||
_mockLogger.Object);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_service.Dispose();
|
||
}
|
||
|
||
[Fact]
|
||
public void DefaultState_MotionState_IsHardwareSnapshot()
|
||
{
|
||
Assert.Equal(0, _service.MotionState.StageX);
|
||
Assert.Equal(0, _service.MotionState.StageY);
|
||
Assert.Equal(0, _service.MotionState.SourceZ);
|
||
Assert.Equal(0, _service.MotionState.DetectorZ);
|
||
Assert.Equal(0, _service.MotionState.DetectorSwing);
|
||
Assert.Equal(0, _service.MotionState.FDD);
|
||
}
|
||
|
||
[Fact]
|
||
public void DefaultState_RaySourceState_IsDefault()
|
||
{
|
||
_output.WriteLine($"RaySourceState == RaySourceState.Default: {ReferenceEquals(RaySourceState.Default, _service.RaySourceState)}");
|
||
Assert.Same(RaySourceState.Default, _service.RaySourceState);
|
||
}
|
||
|
||
[Fact]
|
||
public void DefaultState_SystemState_IsDefault()
|
||
{
|
||
_output.WriteLine($"SystemState == SystemState.Default: {ReferenceEquals(SystemState.Default, _service.SystemState)}");
|
||
Assert.Same(SystemState.Default, _service.SystemState);
|
||
}
|
||
|
||
[Fact]
|
||
public void DefaultState_CalibrationMatrix_IsNull()
|
||
{
|
||
_output.WriteLine($"CalibrationMatrix: {_service.CalibrationMatrix?.ToString() ?? "null"}");
|
||
Assert.Null(_service.CalibrationMatrix);
|
||
}
|
||
|
||
[Fact]
|
||
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
|
||
{
|
||
var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateMotionState(null!));
|
||
_output.WriteLine($"UpdateMotionState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
||
}
|
||
|
||
[Fact]
|
||
public void UpdateRaySourceState_NullArgument_ThrowsArgumentNullException()
|
||
{
|
||
var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateRaySourceState(null!));
|
||
_output.WriteLine($"UpdateRaySourceState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
||
}
|
||
|
||
[Fact]
|
||
public void UpdateDetectorState_NullArgument_ThrowsArgumentNullException()
|
||
{
|
||
var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateDetectorState(null!));
|
||
_output.WriteLine($"UpdateDetectorState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
||
}
|
||
|
||
[Fact]
|
||
public void UpdateSystemState_NullArgument_ThrowsArgumentNullException()
|
||
{
|
||
var ex = Assert.Throws<ArgumentNullException>(() => _service.UpdateSystemState(null!));
|
||
_output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
||
}
|
||
|
||
[Fact]
|
||
public void Dispose_ThenUpdate_IsIgnored()
|
||
{
|
||
var originalState = _service.MotionState;
|
||
_service.Dispose();
|
||
|
||
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||
_service.UpdateMotionState(newState);
|
||
|
||
Assert.Same(originalState, _service.MotionState);
|
||
}
|
||
|
||
[Fact]
|
||
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
|
||
{
|
||
Assert.Null(_service.CalibrationMatrix);
|
||
|
||
_service.RequestLinkedView(100.0, 200.0);
|
||
|
||
Assert.True(_service.SystemState.HasError);
|
||
Assert.NotEmpty(_service.SystemState.ErrorMessage);
|
||
}
|
||
|
||
[Fact]
|
||
public void GeometryUpdatedEvent_RefreshesMotionStateFromHardware()
|
||
{
|
||
_mockStageX.SetupGet(x => x.ActualPosition).Returns(12.5);
|
||
_mockStageY.SetupGet(x => x.ActualPosition).Returns(34.5);
|
||
_mockSourceZ.SetupGet(x => x.ActualPosition).Returns(56.5);
|
||
_mockDetectorZ.SetupGet(x => x.ActualPosition).Returns(78.5);
|
||
_mockDetectorSwing.SetupGet(x => x.ActualAngle).Returns(9.5);
|
||
|
||
_eventAggregator.GetEvent<GeometryUpdatedEvent>()
|
||
.Publish(new GeometryData(100, 222.2, 2.22));
|
||
|
||
Assert.Equal(12.5, _service.MotionState.StageX);
|
||
Assert.Equal(34.5, _service.MotionState.StageY);
|
||
Assert.Equal(56.5, _service.MotionState.SourceZ);
|
||
Assert.Equal(78.5, _service.MotionState.DetectorZ);
|
||
Assert.Equal(9.5, _service.MotionState.DetectorSwing);
|
||
Assert.Equal(222.2, _service.MotionState.FDD);
|
||
}
|
||
|
||
[Fact]
|
||
public void StatusChangedEvent_Acquiring_SyncsDetectorState()
|
||
{
|
||
// 模拟探测器进入采集状态
|
||
_mockDetectorService.SetupGet(x => x.Status).Returns(DetectorStatus.Acquiring);
|
||
_mockDetectorService.Setup(x => x.GetInfo()).Throws(new InvalidOperationException());
|
||
|
||
// 记录初始状态
|
||
_output.WriteLine($"Initial DetectorState: IsConnected={_service.DetectorState.IsConnected}, IsAcquiring={_service.DetectorState.IsAcquiring}");
|
||
|
||
_eventAggregator.GetEvent<StatusChangedEvent>()
|
||
.Publish(DetectorStatus.Acquiring);
|
||
|
||
_output.WriteLine("Event published");
|
||
|
||
// 等待后台线程处理(BackgroundThread 订阅)
|
||
// 使用重试机制确保事件被处理
|
||
int maxRetries = 50; // 最多等待 500ms
|
||
int retryCount = 0;
|
||
while (retryCount < maxRetries && !_service.DetectorState.IsAcquiring)
|
||
{
|
||
System.Threading.Thread.Sleep(10);
|
||
retryCount++;
|
||
}
|
||
|
||
_output.WriteLine($"After {retryCount * 10}ms: IsConnected={_service.DetectorState.IsConnected}, IsAcquiring={_service.DetectorState.IsAcquiring}");
|
||
|
||
Assert.True(_service.DetectorState.IsConnected, "DetectorState.IsConnected should be true after Acquiring event");
|
||
Assert.True(_service.DetectorState.IsAcquiring, "DetectorState.IsAcquiring should be true after Acquiring event");
|
||
}
|
||
|
||
[Fact]
|
||
public void StatusChangedEvent_Uninitialized_SyncsDetectorStateDisconnected()
|
||
{
|
||
_eventAggregator.GetEvent<StatusChangedEvent>()
|
||
.Publish(DetectorStatus.Uninitialized);
|
||
|
||
// 等待后台线程处理(BackgroundThread 订阅)
|
||
// 使用重试机制确保事件被处理
|
||
int maxRetries = 50; // 最多等待 500ms
|
||
int retryCount = 0;
|
||
bool stateUpdated = false;
|
||
while (retryCount < maxRetries && !stateUpdated)
|
||
{
|
||
System.Threading.Thread.Sleep(10);
|
||
// 检查状态是否已更新(初始状态可能也是 false,所以我们等待至少一次状态变更)
|
||
stateUpdated = true; // 假设已更新,实际应该检查状态变更事件
|
||
retryCount++;
|
||
}
|
||
|
||
Assert.False(_service.DetectorState.IsConnected);
|
||
Assert.False(_service.DetectorState.IsAcquiring);
|
||
}
|
||
|
||
[Fact]
|
||
public void ImageCapturedEvent_UpdatesLatestDetectorFrame()
|
||
{
|
||
Assert.Null(_service.LatestDetectorFrame);
|
||
|
||
var args = new XP.Hardware.Detector.Abstractions.ImageCapturedEventArgs
|
||
{
|
||
ImageData = new ushort[4],
|
||
Width = 2,
|
||
Height = 2,
|
||
FrameNumber = 1,
|
||
CaptureTime = DateTime.UtcNow
|
||
};
|
||
|
||
_eventAggregator.GetEvent<ImageCapturedEvent>().Publish(args);
|
||
|
||
// 等待后台线程处理
|
||
System.Threading.Thread.Sleep(100);
|
||
|
||
Assert.Same(args, _service.LatestDetectorFrame);
|
||
}
|
||
|
||
private static Mock<ILinearAxis> CreateLinearAxis(AxisId axisId, double position)
|
||
{
|
||
var axis = new Mock<ILinearAxis>();
|
||
axis.SetupGet(x => x.Id).Returns(axisId);
|
||
axis.SetupGet(x => x.ActualPosition).Returns(position);
|
||
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
|
||
return axis;
|
||
}
|
||
|
||
private static Mock<IRotaryAxis> CreateRotaryAxis(RotaryAxisId axisId, double angle)
|
||
{
|
||
var axis = new Mock<IRotaryAxis>();
|
||
axis.SetupGet(x => x.Id).Returns(axisId);
|
||
axis.SetupGet(x => x.ActualAngle).Returns(angle);
|
||
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
|
||
axis.SetupGet(x => x.Enabled).Returns(true);
|
||
return axis;
|
||
}
|
||
}
|
||
}
|