You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
525 lines
21 KiB
C#
525 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
using Moq;
|
|
using Haoliang.Core.Services;
|
|
using Haoliang.Data.Repositories;
|
|
using Haoliang.Models.Models.Device;
|
|
using Haoliang.Models.Models.System;
|
|
|
|
namespace Haoliang.Tests.Services
|
|
{
|
|
public class DeviceStateMachineTests
|
|
{
|
|
private readonly Mock<IDeviceRepository> _mockDeviceRepository;
|
|
private readonly Mock<IDeviceCollectionService> _mockCollectionService;
|
|
private readonly Mock<IAlarmService> _mockAlarmService;
|
|
private readonly Mock<ICacheService> _mockCacheService;
|
|
private readonly DeviceStateMachine _deviceStateMachine;
|
|
|
|
public DeviceStateMachineTests()
|
|
{
|
|
_mockDeviceRepository = new Mock<IDeviceRepository>();
|
|
_mockCollectionService = new Mock<IDeviceCollectionService>();
|
|
_mockAlarmService = new Mock<IAlarmService>();
|
|
_mockCacheService = new Mock<ICacheService>();
|
|
|
|
_deviceStateMachine = new DeviceStateMachine(
|
|
_mockDeviceRepository.Object,
|
|
_mockCollectionService.Object,
|
|
_mockAlarmService.Object,
|
|
_mockCacheService.Object
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCurrentState_DeviceInCache_ReturnsState()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var expectedState = DeviceState.Running;
|
|
|
|
// Setup device context in cache
|
|
var deviceContext = new DeviceStateContext
|
|
{
|
|
CurrentState = expectedState,
|
|
PreviousState = DeviceState.Idle,
|
|
StateChangedAt = DateTime.UtcNow
|
|
};
|
|
|
|
// This would require mocking the internal dictionary
|
|
// For this test, we'll simulate the initial state
|
|
await _deviceStateMachine.StartAsync(CancellationToken.None);
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, expectedState, "Test setup");
|
|
|
|
// Act
|
|
var result = _deviceStateMachine.GetCurrentState(deviceId);
|
|
|
|
// Assert
|
|
Assert.Equal(expectedState, result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanTransitionAsync_ValidTransition_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var fromState = DeviceState.Idle;
|
|
var targetState = DeviceState.Running;
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanTransitionAsync_InvalidTransition_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var fromState = DeviceState.Running;
|
|
var targetState = DeviceState.Offline;
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransitionToStateAsync_ValidTransition_TransitionsState()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var fromState = DeviceState.Idle;
|
|
var targetState = DeviceState.Running;
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device", Status = DeviceStatus.Idle };
|
|
var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Idle, Runtime = TimeSpan.Zero };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
.ReturnsAsync(currentStatus);
|
|
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(fromState, result.FromState);
|
|
Assert.Equal(targetState, result.ToState);
|
|
Assert.Equal($"Successfully transitioned from {fromState} to {targetState}", result.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransitionToStateAsync_InvalidTransition_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var fromState = DeviceState.Running;
|
|
var targetState = DeviceState.Offline;
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Contains($"Cannot transition from {fromState} to {targetState}", result.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TriggerEventAsync_ValidEvent_TransitionsState()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var deviceEvent = DeviceEvent.Start;
|
|
var targetState = DeviceState.Running;
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device", EnableProduction = true };
|
|
var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
.ReturnsAsync(currentStatus);
|
|
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal(DeviceState.Idle, result.FromState);
|
|
Assert.Equal(targetState, result.ToState);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TriggerEventAsync_InvalidEvent_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var deviceEvent = DeviceEvent.Stop; // Cannot stop when not running
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Contains($"No event handler configured for {deviceEvent} in state DeviceState.Idle", result.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForceStateAsync_ValidForce_TransitionsState()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var targetState = DeviceState.Running;
|
|
var reason = "Emergency override";
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.ForceStateAsync(deviceId, targetState, reason);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Equal($"Forced state transition to {targetState}: {reason}", result.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateStateAsync_ValidState_ReturnsValid()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var device = new CNCDevice {
|
|
Id = deviceId,
|
|
Name = "Test Device",
|
|
Status = DeviceStatus.Idle,
|
|
EnableProduction = true
|
|
};
|
|
var currentStatus = new DeviceCurrentStatus {
|
|
DeviceId = deviceId,
|
|
Status = DeviceStatus.Idle,
|
|
Runtime = TimeSpan.FromHours(1)
|
|
};
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
.ReturnsAsync(currentStatus);
|
|
|
|
// Initialize device state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
|
|
// Assert
|
|
Assert.True(result.IsValid);
|
|
Assert.Equal(DeviceState.Idle, result.CurrentState);
|
|
Assert.Empty(result.Issues);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateStateAsync_StateMismatch_ReturnsInvalid()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var device = new CNCDevice {
|
|
Id = deviceId,
|
|
Name = "Test Device",
|
|
Status = DeviceStatus.Running, // Status doesn't match state
|
|
EnableProduction = true
|
|
};
|
|
var currentStatus = new DeviceCurrentStatus {
|
|
DeviceId = deviceId,
|
|
Status = DeviceStatus.Running,
|
|
Runtime = TimeSpan.FromHours(1)
|
|
};
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
.ReturnsAsync(currentStatus);
|
|
|
|
// Initialize device state as Idle but status is Running
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
|
|
// Assert
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains("State mismatch", result.Issues.First());
|
|
}
|
|
|
|
[Fact]
|
|
public void RegisterStateHandler_ValidHandler_RegistersHandler()
|
|
{
|
|
// Arrange
|
|
var handlerMock = new Mock<Func<int, DeviceState, DeviceState, Task>>();
|
|
|
|
// Act
|
|
_deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
|
|
// Assert
|
|
// This test would need to verify the handler was registered
|
|
// For now, we'll just ensure it doesn't throw
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnregisterStateHandler_ValidHandler_UnregistersHandler()
|
|
{
|
|
// Arrange
|
|
var handlerMock = new Mock<Func<int, DeviceState, DeviceState, Task>>();
|
|
|
|
// Act
|
|
_deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
_deviceStateMachine.UnregisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
|
|
// Assert
|
|
// This test would need to verify the handler was unregistered
|
|
// For now, we'll just ensure it doesn't throw
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStateHistoryAsync_DeviceWithHistory_ReturnsHistory()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var history = new List<DeviceStateHistory>
|
|
{
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) },
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) }
|
|
};
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null))
|
|
.ReturnsAsync(history);
|
|
|
|
// Initialize current state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.GetStateHistoryAsync(deviceId);
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.Count); // History entries + current state
|
|
Assert.Equal(DeviceState.Idle, result[0].State);
|
|
Assert.Equal(DeviceState.Running, result[1].State);
|
|
Assert.Equal(DeviceState.Stopped, result[2].State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStateStatisticsAsync_DeviceWithHistory_ReturnsStatistics()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var history = new List<DeviceStateHistory>
|
|
{
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) },
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) }
|
|
};
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null))
|
|
.ReturnsAsync(history);
|
|
|
|
// Initialize current state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(deviceId, result.DeviceId);
|
|
Assert.True(result.StateTransitions > 0);
|
|
Assert.True(result.StateDurations.ContainsKey(DeviceState.Idle));
|
|
Assert.True(result.StateDurations.ContainsKey(DeviceState.Running));
|
|
Assert.True(result.StateDurations.ContainsKey(DeviceState.Stopped));
|
|
Assert.True(result.StateCounts.ContainsKey(DeviceState.Idle));
|
|
Assert.True(result.StateCounts.ContainsKey(DeviceState.Running));
|
|
Assert.True(result.StateCounts.ContainsKey(DeviceState.Stopped));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStateStatisticsAsync_WithDateFilter_ReturnsFilteredStatistics()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var filterStartDate = DateTime.Now.AddDays(-1);
|
|
var filterEndDate = DateTime.Now;
|
|
|
|
var history = new List<DeviceStateHistory>
|
|
{
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddDays(-2) }, // Outside filter
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-12) }, // Within filter
|
|
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Stopped, ChangedAt = DateTime.Now.AddHours(-6) } // Within filter
|
|
};
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, filterStartDate, filterEndDate))
|
|
.ReturnsAsync(history);
|
|
|
|
// Initialize current state
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
|
|
// Act
|
|
var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId, filterStartDate, filterEndDate);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(deviceId, result.DeviceId);
|
|
Assert.Equal(filterStartDate, result.PeriodStart);
|
|
Assert.Equal(filterEndDate, result.PeriodEnd);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TranslateStatusToDeviceState_ValidStatus_ReturnsState()
|
|
{
|
|
// Arrange
|
|
var currentStatus = new DeviceCurrentStatus { Status = DeviceStatus.Running };
|
|
|
|
// Act
|
|
// This method would need to be made public or tested through another method
|
|
// For now, we'll test the translation logic through a state transition
|
|
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
.ReturnsAsync(device);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1))
|
|
.ReturnsAsync(currentStatus);
|
|
|
|
// Initialize device state and check if it matches the status
|
|
await _deviceStateMachine.ForceStateAsync(1, DeviceState.Unknown, "Test setup");
|
|
var result = _deviceStateMachine.GetCurrentState(1);
|
|
|
|
// Assert
|
|
// The state should be Running based on the status
|
|
Assert.Equal(DeviceState.Unknown, result); // Initially unknown until transition
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TranslateStateToDeviceStatus_ValidState_ReturnsStatus()
|
|
{
|
|
// Arrange
|
|
var state = DeviceState.Running;
|
|
|
|
// Act
|
|
// This would test the reverse translation
|
|
// We can test it by verifying the device status is updated correctly
|
|
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
.ReturnsAsync(device);
|
|
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Transition to Running state and check if device status is updated
|
|
await _deviceStateMachine.TransitionToStateAsync(1, state);
|
|
|
|
// Assert
|
|
// The device status should be updated to Running
|
|
Assert.Equal(DeviceStatus.Running, device.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HandleInvalidStateAsync_InvalidState_TransitionsToSafeState()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var validationResult = new DeviceValidationResult
|
|
{
|
|
DeviceId = deviceId,
|
|
IsValid = false,
|
|
Issues = new List<string> { "State mismatch" },
|
|
CurrentState = DeviceState.Error
|
|
};
|
|
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
.ReturnsAsync(device);
|
|
|
|
// Act
|
|
// This method is private, so we test through ValidateStateAsync which calls it
|
|
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Error, "Test setup");
|
|
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
|
|
// The state machine should handle the invalid state
|
|
// We expect the state to remain Error since it's already a safe state for errors
|
|
Assert.Equal(DeviceState.Error, _deviceStateMachine.GetCurrentState(deviceId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StartAsync_InitializesDeviceStates()
|
|
{
|
|
// Arrange
|
|
var devices = new List<CNCDevice>
|
|
{
|
|
new CNCDevice { Id = 1, Name = "Device 1" },
|
|
new CNCDevice { Id = 2, Name = "Device 2" }
|
|
};
|
|
|
|
var device1Status = new DeviceCurrentStatus { DeviceId = 1, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero };
|
|
var device2Status = new DeviceCurrentStatus { DeviceId = 2, Status = DeviceStatus.Running, Runtime = TimeSpan.FromHours(1) };
|
|
|
|
_mockDeviceRepository.Setup(repo => repo.GetAllDevicesAsync())
|
|
.ReturnsAsync(devices);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1))
|
|
.ReturnsAsync(device1Status);
|
|
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(2))
|
|
.ReturnsAsync(device2Status);
|
|
|
|
// Act
|
|
await _deviceStateMachine.StartAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
// Allow time for async initialization
|
|
await Task.Delay(100);
|
|
|
|
// Check if states are initialized based on device status
|
|
Assert.Equal(DeviceState.Online, _deviceStateMachine.GetCurrentState(1));
|
|
Assert.Equal(DeviceState.Running, _deviceStateMachine.GetCurrentState(2));
|
|
}
|
|
}
|
|
} |