Files
XplorePlane/XplorePlane.Tests/Services/AppStateServiceTests.cs
T
2026-05-13 16:20:47 +08:00

304 lines
13 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 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));
// DetectorServiceGetInfo 在未初始化时抛出,模拟此行为
_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;
}
}
}