Files
sta/backend/tests/modbus_hardware_test.rs
Lucien Cartier-Tilet ce186095fa feat(application): HealthMonitor service and hardware integration test
Add HealthMonitor service for tracking system health status with
comprehensive state transition logic and thread-safe operations.
Includes 16 unit tests covering all functionality including concurrent
access scenarios.

Add optional Modbus hardware integration tests with 7 test cases for
real device testing. Tests are marked as ignored and can be run with

running 21 tests
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure ... FAILED
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false ... FAILED
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error ... FAILED
test infrastructure::modbus::client::tests::t025a_connection_setup_tests::test_new_with_valid_config_connects_successfully ... ok
test infrastructure::modbus::client::tests::t025a_connection_setup_tests::test_new_stores_correct_timeout_duration ... ok
test infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_coil_values_on_success ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_9_states_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_empty_vector_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_can_toggle_relay_multiple_times ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_8_states_succeeds ... ok
test infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_succeeds_for_valid_write ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil ... FAILED
test infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_correctly_maps_relay_id_to_modbus_address ... ok
test infrastructure::modbus::client::tests::write_all_states_validation_tests::test_write_all_states_with_7_states_returns_invalid_input ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_on_writes_true_to_coil ... ok
test infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_correctly_maps_relay_id_to_modbus_address ... ok

failures:

---- infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure stdout ----

thread 'infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure' (1157113) panicked at backend/src/infrastructure/modbus/client_test.rs:320:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device stdout ----

thread 'infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device' (1157114) panicked at backend/src/infrastructure/modbus/client_test.rs:293:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response' (1157112) panicked at backend/src/infrastructure/modbus/client_test.rs:176:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error' (1157111) panicked at backend/src/infrastructure/modbus/client_test.rs:227:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true' (1157119) panicked at backend/src/infrastructure/modbus/client_test.rs:354:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error' (1157117) panicked at backend/src/infrastructure/modbus/client_test.rs:396:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false stdout ----

thread 'infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false' (1157118) panicked at backend/src/infrastructure/modbus/client_test.rs:375:14:
Failed to connect to test server: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error stdout ----

thread 'infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error' (1157110) panicked at backend/src/infrastructure/modbus/client_test.rs:202:14:
Failed to connect: ConnectionError("Connection refused (os error 111)")

---- infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil stdout ----

thread 'infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil' (1157122) panicked at backend/src/infrastructure/modbus/client_test.rs:508:9:
assertion `left == right` failed: Relay should be Off after writing Off state
  left: On
 right: Off


failures:
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_connection_error_on_io_error
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_modbus_exception_on_protocol_error
    infrastructure::modbus::client::tests::t025b_read_coils_timeout_tests::test_read_coils_returns_timeout_on_slow_response
    infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_error_on_failure
    infrastructure::modbus::client::tests::t025c_write_single_coil_timeout_tests::test_write_single_coil_returns_timeout_on_slow_device
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_propagates_controller_error
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_off_when_coil_is_false
    infrastructure::modbus::client::tests::t025d_read_relay_state_tests::test_read_state_returns_on_when_coil_is_true
    infrastructure::modbus::client::tests::t025e_write_relay_state_tests::test_write_state_off_writes_false_to_coil

test result: FAILED. 12 passed; 9 failed; 0 ignored; 0 measured; 128 filtered out; finished in 3.27s.

Ref: T034, T039, T040 (specs/001-modbus-relay-control/tasks.org)
2026-01-22 00:57:11 +01:00

254 lines
8.3 KiB
Rust

// Integration tests for Modbus hardware
// These tests require physical Modbus relay device to be connected
// Run with: cargo test -- --ignored
use std::time::Duration;
#[cfg(test)]
mod tests {
use super::*;
use sta::domain::relay::controller::RelayController;
use sta::domain::relay::types::{RelayId, RelayState};
use sta::infrastructure::modbus::client::ModbusRelayController;
static HOST: &str = "192.168.1.200";
static PORT: u16 = 502;
static SLAVE_ID: u8 = 1;
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_modbus_connection() {
// This test verifies we can connect to the actual Modbus device
// Configured with settings from settings/base.yaml
let timeout_secs = 5;
let _controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
// If we got here, connection was successful
println!("✓ Successfully connected to Modbus device");
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_read_relay_states() {
let timeout_secs = 5;
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
// Test reading individual relay states
for relay_id in 1..=8 {
let relay_id = RelayId::new(relay_id).unwrap();
let state = controller
.read_relay_state(relay_id)
.await
.expect("Failed to read relay state");
println!("Relay {}: {:?}", relay_id.as_u8(), state);
}
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_read_all_relays() {
let timeout_secs = 5;
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
let relays = controller
.read_all_states()
.await
.expect("Failed to read all relay states");
assert_eq!(relays.len(), 8, "Should have exactly 8 relays");
for (i, state) in relays.iter().enumerate() {
let relay_id = i + 1;
println!("Relay {}: {:?}", relay_id, state);
}
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_write_relay_state() {
let timeout_secs = 5;
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
let relay_id = RelayId::new(1).unwrap();
// Turn relay on
controller
.write_relay_state(relay_id, RelayState::On)
.await
.expect("Failed to write relay state");
// Verify it's on
let state = controller
.read_relay_state(relay_id)
.await
.expect("Failed to read relay state");
assert_eq!(state, RelayState::On, "Relay should be ON");
// Turn relay off
controller
.write_relay_state(relay_id, RelayState::Off)
.await
.expect("Failed to write relay state");
// Verify it's off
let state = controller
.read_relay_state(relay_id)
.await
.expect("Failed to read relay state");
assert_eq!(state, RelayState::Off, "Relay should be OFF");
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_write_all_relays() {
let timeout_secs = 5;
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
// Turn all relays on
let all_on_states = vec![RelayState::On; 8];
controller
.write_all_states(all_on_states)
.await
.expect("Failed to write all relay states");
// Verify all are on
let relays = controller
.read_all_states()
.await
.expect("Failed to read all relay states");
for state in &relays {
assert_eq!(*state, RelayState::On, "All relays should be ON");
}
// Turn all relays off
let all_off_states = vec![RelayState::Off; 8];
controller
.write_all_states(all_off_states)
.await
.expect("Failed to write all relay states");
// Verify all are off
let relays = controller
.read_all_states()
.await
.expect("Failed to read all relay states");
for state in &relays {
assert_eq!(*state, RelayState::Off, "All relays should be OFF");
}
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_timeout_handling() {
let timeout_secs = 1; // Short timeout for testing
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
// This test verifies that timeout works correctly
// We'll try to read a relay state with a very short timeout
let relay_id = RelayId::new(1).unwrap();
// The operation should either succeed quickly or timeout
let result = tokio::time::timeout(
Duration::from_secs(2),
controller.read_relay_state(relay_id),
)
.await;
match result {
Ok(Ok(state)) => {
println!("✓ Operation completed within timeout: {:?}", state);
}
Ok(Err(e)) => {
println!("✓ Operation failed (expected): {}", e);
}
Err(_) => {
println!("✓ Operation timed out (expected)");
}
}
}
#[tokio::test]
#[ignore = "Requires physical Modbus device"]
async fn test_concurrent_access() {
let timeout_secs = 5;
let _controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect to Modbus device");
// Test concurrent access to the controller
// We'll test a few relays concurrently using tokio::join!
// Note: We can't clone the controller, so we'll just test sequential access
// This is still valuable for testing the controller works with multiple relays
let relay_id1 = RelayId::new(1).unwrap();
let relay_id2 = RelayId::new(2).unwrap();
let relay_id3 = RelayId::new(3).unwrap();
let relay_id4 = RelayId::new(4).unwrap();
let task1 = tokio::spawn(async move {
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect");
controller.read_relay_state(relay_id1).await
});
let task2 = tokio::spawn(async move {
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect");
controller.read_relay_state(relay_id2).await
});
let task3 = tokio::spawn(async move {
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect");
controller.read_relay_state(relay_id3).await
});
let task4 = tokio::spawn(async move {
let controller = ModbusRelayController::new(HOST, PORT, SLAVE_ID, timeout_secs)
.await
.expect("Failed to connect");
controller.read_relay_state(relay_id4).await
});
let (result1, result2, result3, result4) = tokio::join!(task1, task2, task3, task4);
// Process results
if let Ok(Ok(state)) = result1 {
println!("Relay 1: {:?}", state);
}
if let Ok(Ok(state)) = result2 {
println!("Relay 2: {:?}", state);
}
if let Ok(Ok(state)) = result3 {
println!("Relay 3: {:?}", state);
}
if let Ok(Ok(state)) = result4 {
println!("Relay 4: {:?}", state);
}
}
}