Add real Modbus TCP communication through ModbusRelayController: - T025a: Connection setup with Arc<Mutex<Context>> and configurable timeout - T025b: read_coils_with_timeout() helper wrapping tokio::time::timeout - T025c: write_single_coil_with_timeout() with nested Result handling - T025d: RelayController::read_relay_state() using timeout helper - T025e: RelayController::write_relay_state() with state conversion - Additional: Complete RelayController trait with all required methods - Domain support: RelayId::to_modbus_address(), RelayState conversion helpers Implements hexagonal architecture with infrastructure layer properly depending on domain types. Includes structured logging at key operations. TDD phase: green (implementation following test stubs from T023-T024) Ref: T025a-T025e (specs/001-modbus-relay-control/tasks.md)
675 lines
26 KiB
Rust
675 lines
26 KiB
Rust
//! Tests for `ModbusRelayController`
|
|
//!
|
|
//! These tests cover T025a through T025e of the implementation plan.
|
|
//! Note: These tests require mocking or a test Modbus server since they test
|
|
//! real TCP connections and Modbus protocol interactions.
|
|
|
|
use super::*;
|
|
|
|
#[cfg(test)]
|
|
mod t025a_connection_setup_tests {
|
|
use super::*;
|
|
|
|
/// T025a Test 1: `new()` with valid config connects successfully
|
|
///
|
|
/// This test verifies that `ModbusRelayController::new()` can establish
|
|
/// a connection to a valid Modbus TCP server.
|
|
///
|
|
/// NOTE: This test requires a running Modbus TCP server at 127.0.0.1:5020
|
|
/// or should be modified to use a mock server. Mark with #[ignore] if no server available.
|
|
#[tokio::test]
|
|
#[ignore = "Requires running Modbus TCP server"]
|
|
async fn test_new_with_valid_config_connects_successfully() {
|
|
// Arrange: Use localhost test server
|
|
let host = "127.0.0.1";
|
|
let port = 5020; // Test Modbus TCP port
|
|
let slave_id = 1;
|
|
let timeout_secs = 5;
|
|
|
|
// Act: Attempt to create controller
|
|
let result = ModbusRelayController::new(host, port, slave_id, timeout_secs).await;
|
|
|
|
// Assert: Connection should succeed
|
|
assert!(
|
|
result.is_ok(),
|
|
"Expected successful connection to test Modbus server, got error: {:?}",
|
|
result.err()
|
|
);
|
|
}
|
|
|
|
/// T025a Test 2: `new()` with invalid host returns `ConnectionError`
|
|
///
|
|
/// This test verifies that `ModbusRelayController::new()` returns a proper
|
|
/// `ConnectionError` when given an invalid hostname.
|
|
#[tokio::test]
|
|
async fn test_new_with_invalid_host_returns_connection_error() {
|
|
// Arrange: Use invalid host format
|
|
let host = "not a valid host!!!";
|
|
let port = 502;
|
|
let slave_id = 1;
|
|
let timeout_secs = 5;
|
|
|
|
// Act: Attempt to create controller
|
|
let result = ModbusRelayController::new(host, port, slave_id, timeout_secs).await;
|
|
|
|
// Assert: Should return ConnectionError
|
|
assert!(result.is_err(), "Expected ConnectionError for invalid host");
|
|
|
|
let error = result.unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
error,
|
|
crate::domain::relay::controller::ControllerError::ConnectionError(_)
|
|
),
|
|
"Expected ControllerError::ConnectionError, got {error:?}"
|
|
);
|
|
}
|
|
|
|
/// T025a Test 3: `new()` with unreachable port returns `ConnectionError`
|
|
///
|
|
/// This test verifies that attempting to connect to a closed/unreachable port
|
|
/// results in a `ConnectionError`. Uses localhost port 1 (closed) for instant
|
|
/// "connection refused" error without waiting for TCP timeout.
|
|
#[tokio::test]
|
|
async fn test_new_with_unreachable_host_returns_connection_error() {
|
|
// Arrange: Use localhost with a closed port (port 1 is typically closed)
|
|
// This gives instant "connection refused" instead of waiting for TCP timeout
|
|
let host = "127.0.0.1";
|
|
let port = 1; // Closed port for instant connection failure
|
|
let slave_id = 1;
|
|
let timeout_secs = 1;
|
|
|
|
// Act: Attempt to create controller
|
|
let result = ModbusRelayController::new(host, port, slave_id, timeout_secs).await;
|
|
|
|
// Assert: Should return ConnectionError
|
|
assert!(
|
|
result.is_err(),
|
|
"Expected ConnectionError for unreachable host"
|
|
);
|
|
}
|
|
|
|
/// T025a Test 4: `new()` stores correct `timeout_duration`
|
|
///
|
|
/// This test verifies that the `timeout_secs` parameter is correctly
|
|
/// stored in the controller instance.
|
|
///
|
|
/// NOTE: This test requires access to internal state or a working connection
|
|
/// to verify timeout behavior. Mark with #[ignore] if no server available.
|
|
#[tokio::test]
|
|
#[ignore = "Requires running Modbus TCP server or refactoring to expose timeout"]
|
|
async fn test_new_stores_correct_timeout_duration() {
|
|
// Arrange
|
|
let host = "127.0.0.1";
|
|
let port = 5020;
|
|
let slave_id = 1;
|
|
let timeout_secs = 10;
|
|
|
|
// Act
|
|
let controller = ModbusRelayController::new(host, port, slave_id, timeout_secs)
|
|
.await
|
|
.expect("Failed to create controller");
|
|
|
|
// Assert: Verify timeout is stored correctly
|
|
// Note: This requires either:
|
|
// 1. A getter method for timeout_duration, or
|
|
// 2. Testing timeout behavior by triggering an actual timeout, or
|
|
// 3. Refactoring to make timeout_duration accessible for testing
|
|
//
|
|
// For now, this is a placeholder that documents the requirement
|
|
drop(controller);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod t025b_read_coils_timeout_tests {
|
|
// Note: These tests require access to private method read_coils_with_timeout()
|
|
// Options:
|
|
// 1. Make the method pub(crate) for testing
|
|
// 2. Test indirectly through public RelayController methods
|
|
// 3. Use a test-only feature flag to expose for testing
|
|
//
|
|
// For now, we'll test through the public interface (read_relay_state)
|
|
|
|
use super::*;
|
|
use crate::domain::relay::{
|
|
controller::{ControllerError, RelayController},
|
|
types::RelayId,
|
|
};
|
|
|
|
/// T025b Test 1: `read_coils_with_timeout()` returns coil values on success
|
|
///
|
|
/// This test verifies that reading coils succeeds when the Modbus server
|
|
/// responds correctly.
|
|
///
|
|
/// NOTE: Tests through `read_relay_state()` public interface
|
|
#[tokio::test]
|
|
#[ignore = "Requires running Modbus TCP server with known state"]
|
|
async fn test_read_coils_returns_coil_values_on_success() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Read relay state (internally calls read_coils_with_timeout)
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should succeed with a valid state
|
|
assert!(
|
|
result.is_ok(),
|
|
"Expected successful coil read, got error: {:?}",
|
|
result.err()
|
|
);
|
|
}
|
|
|
|
/// T025b Test 2: `read_coils_with_timeout()` returns Timeout error when operation exceeds timeout
|
|
///
|
|
/// This test verifies that the timeout mechanism works correctly.
|
|
///
|
|
/// NOTE: Requires either a slow/non-responsive Modbus server or mocking
|
|
#[tokio::test]
|
|
#[ignore = "Requires slow/non-responsive Modbus server or mocking"]
|
|
async fn test_read_coils_returns_timeout_on_slow_response() {
|
|
// Arrange: Connect with very short timeout
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 1)
|
|
.await
|
|
.expect("Failed to connect");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Attempt to read (server should be configured to delay response)
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should return Timeout error
|
|
assert!(result.is_err(), "Expected timeout error");
|
|
|
|
if let Err(ControllerError::Timeout(secs)) = result {
|
|
assert_eq!(secs, 1, "Timeout duration should match configured value");
|
|
} else {
|
|
panic!("Expected ControllerError::Timeout, got {result:?}");
|
|
}
|
|
}
|
|
|
|
/// T025b Test 3: `read_coils_with_timeout()` returns `ConnectionError` on `io::Error`
|
|
///
|
|
/// This test verifies that IO errors are properly wrapped as `ConnectionError`.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server that drops connections"]
|
|
async fn test_read_coils_returns_connection_error_on_io_error() {
|
|
// Arrange: Connect to server that will drop connection
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect");
|
|
|
|
// Server should be configured to drop connection after initial connect
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Attempt to read after connection is dropped
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should return ConnectionError
|
|
assert!(result.is_err(), "Expected connection error");
|
|
assert!(
|
|
matches!(result, Err(ControllerError::ConnectionError(_))),
|
|
"Expected ConnectionError, got {result:?}"
|
|
);
|
|
}
|
|
|
|
/// T025b Test 4: `read_coils_with_timeout()` returns `ModbusException` on protocol error
|
|
///
|
|
/// This test verifies that Modbus protocol exceptions are properly handled.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server that returns exception codes"]
|
|
async fn test_read_coils_returns_modbus_exception_on_protocol_error() {
|
|
// Arrange: Connect to server configured to return Modbus exception
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect");
|
|
|
|
// Server should be configured to return exception for this relay ID
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Attempt to read (should trigger Modbus exception)
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should return ModbusException error
|
|
assert!(result.is_err(), "Expected Modbus exception");
|
|
assert!(
|
|
matches!(result, Err(ControllerError::ModbusException(_))),
|
|
"Expected ModbusException, got {result:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod t025c_write_single_coil_timeout_tests {
|
|
use super::*;
|
|
use crate::domain::relay::{
|
|
controller::{ControllerError, RelayController},
|
|
types::{RelayId, RelayState},
|
|
};
|
|
|
|
/// T025c Test 1: `write_single_coil_with_timeout()` succeeds for valid write
|
|
///
|
|
/// This test verifies that writing to a coil succeeds when the Modbus server
|
|
/// responds correctly.
|
|
///
|
|
/// NOTE: Tests through `write_relay_state()` public interface
|
|
#[tokio::test]
|
|
#[ignore = "Requires running Modbus TCP server"]
|
|
async fn test_write_single_coil_succeeds_for_valid_write() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
let state = RelayState::On;
|
|
|
|
// Act: Write relay state (internally calls write_single_coil_with_timeout)
|
|
let result = controller.write_relay_state(relay_id, state).await;
|
|
|
|
// Assert: Should succeed
|
|
assert!(
|
|
result.is_ok(),
|
|
"Expected successful coil write, got error: {:?}",
|
|
result.err()
|
|
);
|
|
}
|
|
|
|
/// T025c Test 2: `write_single_coil_with_timeout()` returns Timeout on slow device
|
|
///
|
|
/// This test verifies that write operations properly timeout.
|
|
#[tokio::test]
|
|
#[ignore = "Requires slow/non-responsive Modbus server"]
|
|
async fn test_write_single_coil_returns_timeout_on_slow_device() {
|
|
// Arrange: Connect with very short timeout
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 1)
|
|
.await
|
|
.expect("Failed to connect");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
let state = RelayState::On;
|
|
|
|
// Act: Attempt to write (server should be configured to delay response)
|
|
let result = controller.write_relay_state(relay_id, state).await;
|
|
|
|
// Assert: Should return Timeout error
|
|
assert!(result.is_err(), "Expected timeout error");
|
|
|
|
if let Err(ControllerError::Timeout(secs)) = result {
|
|
assert_eq!(secs, 1, "Timeout duration should match configured value");
|
|
} else {
|
|
panic!("Expected ControllerError::Timeout, got {result:?}");
|
|
}
|
|
}
|
|
|
|
/// T025c Test 3: `write_single_coil_with_timeout()` returns appropriate error on failure
|
|
///
|
|
/// This test verifies that various write failures are properly handled.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server configured for error testing"]
|
|
async fn test_write_single_coil_returns_error_on_failure() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
let state = RelayState::On;
|
|
|
|
// Act: Attempt to write (server should be configured to return error)
|
|
let result = controller.write_relay_state(relay_id, state).await;
|
|
|
|
// Assert: Should return an error (ConnectionError or ModbusException)
|
|
assert!(result.is_err(), "Expected error from write operation");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod t025d_read_relay_state_tests {
|
|
use super::*;
|
|
use crate::domain::relay::{
|
|
controller::RelayController,
|
|
types::{RelayId, RelayState},
|
|
};
|
|
|
|
/// T025d Test 1: `read_relay_state(RelayId(1))` returns On when coil is true
|
|
///
|
|
/// This test verifies that a true coil value is correctly converted to `RelayState::On`.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server with relay 1 set to On"]
|
|
async fn test_read_state_returns_on_when_coil_is_true() {
|
|
// Arrange: Connect to test server with relay 1 in On state
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Read relay state
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should return On state
|
|
assert!(result.is_ok(), "Failed to read relay state");
|
|
assert_eq!(result.unwrap(), RelayState::On, "Expected relay to be On");
|
|
}
|
|
|
|
/// T025d Test 2: `read_relay_state(RelayId(1))` returns Off when coil is false
|
|
///
|
|
/// This test verifies that a false coil value is correctly converted to `RelayState::Off`.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server with relay 1 set to Off"]
|
|
async fn test_read_state_returns_off_when_coil_is_false() {
|
|
// Arrange: Connect to test server with relay 1 in Off state
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Read relay state
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should return Off state
|
|
assert!(result.is_ok(), "Failed to read relay state");
|
|
assert_eq!(result.unwrap(), RelayState::Off, "Expected relay to be Off");
|
|
}
|
|
|
|
/// T025d Test 3: `read_relay_state()` propagates `ControllerError` from helper
|
|
///
|
|
/// This test verifies that errors from `read_coils_with_timeout` are properly propagated.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server configured to return errors"]
|
|
async fn test_read_state_propagates_controller_error() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Server should be configured to return error for this relay
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act: Attempt to read relay state
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should propagate error
|
|
assert!(
|
|
result.is_err(),
|
|
"Expected error to be propagated from helper"
|
|
);
|
|
}
|
|
|
|
/// T025d Test 4: `read_relay_state()` correctly maps `RelayId` to `ModbusAddress`
|
|
///
|
|
/// This test verifies that relay IDs are correctly converted to 0-based Modbus addresses.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server with specific relay states"]
|
|
async fn test_read_state_correctly_maps_relay_id_to_modbus_address() {
|
|
// Arrange: Connect to test server with known relay states
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Test multiple relays to verify address mapping (RelayId 1-8 → Modbus 0-7)
|
|
for relay_num in 1..=8 {
|
|
let relay_id = RelayId::new(relay_num).expect("Valid relay ID");
|
|
|
|
// Act: Read relay state
|
|
let result = controller.read_relay_state(relay_id).await;
|
|
|
|
// Assert: Should succeed for all valid relay IDs
|
|
assert!(result.is_ok(), "Failed to read relay {relay_num} state");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod t025e_write_relay_state_tests {
|
|
use super::*;
|
|
use crate::domain::relay::{
|
|
controller::RelayController,
|
|
types::{RelayId, RelayState},
|
|
};
|
|
|
|
/// T025e Test 1: `write_relay_state(RelayId(1)`, `RelayState::On`) writes true to coil
|
|
///
|
|
/// This test verifies that `RelayState::On` is correctly converted to a true coil value.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server that can verify written values"]
|
|
async fn test_write_state_on_writes_true_to_coil() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
let state = RelayState::On;
|
|
|
|
// Act: Write On state
|
|
let write_result = controller.write_relay_state(relay_id, state).await;
|
|
|
|
// Assert: Write should succeed
|
|
assert!(
|
|
write_result.is_ok(),
|
|
"Failed to write relay state: {:?}",
|
|
write_result.err()
|
|
);
|
|
|
|
// Verify by reading back
|
|
let read_result = controller.read_relay_state(relay_id).await;
|
|
assert!(read_result.is_ok(), "Failed to read back relay state");
|
|
assert_eq!(
|
|
read_result.unwrap(),
|
|
RelayState::On,
|
|
"Relay should be On after writing On state"
|
|
);
|
|
}
|
|
|
|
/// T025e Test 2: `write_relay_state(RelayId(1)`, `RelayState::Off`) writes false to coil
|
|
///
|
|
/// This test verifies that `RelayState::Off` is correctly converted to a false coil value.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server that can verify written values"]
|
|
async fn test_write_state_off_writes_false_to_coil() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
let state = RelayState::Off;
|
|
|
|
// Act: Write Off state
|
|
let write_result = controller.write_relay_state(relay_id, state).await;
|
|
|
|
// Assert: Write should succeed
|
|
assert!(
|
|
write_result.is_ok(),
|
|
"Failed to write relay state: {:?}",
|
|
write_result.err()
|
|
);
|
|
|
|
// Verify by reading back
|
|
let read_result = controller.read_relay_state(relay_id).await;
|
|
assert!(read_result.is_ok(), "Failed to read back relay state");
|
|
assert_eq!(
|
|
read_result.unwrap(),
|
|
RelayState::Off,
|
|
"Relay should be Off after writing Off state"
|
|
);
|
|
}
|
|
|
|
/// T025e Test 3: `write_relay_state()` correctly maps `RelayId` to `ModbusAddress`
|
|
///
|
|
/// This test verifies that relay IDs are correctly converted when writing.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_state_correctly_maps_relay_id_to_modbus_address() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Test writing to all relay IDs to verify address mapping
|
|
for relay_num in 1..=8 {
|
|
let relay_id = RelayId::new(relay_num).expect("Valid relay ID");
|
|
|
|
// Act: Write to relay
|
|
let result = controller.write_relay_state(relay_id, RelayState::On).await;
|
|
|
|
// Assert: Should succeed for all valid relay IDs
|
|
assert!(
|
|
result.is_ok(),
|
|
"Failed to write to relay {}: {:?}",
|
|
relay_num,
|
|
result.err()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// T025e Test 4: `write_relay_state()` can toggle relays between On and Off
|
|
///
|
|
/// This test verifies that relays can be toggled multiple times.
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_state_can_toggle_relay_multiple_times() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
let relay_id = RelayId::new(1).expect("Valid relay ID");
|
|
|
|
// Act & Assert: Toggle relay multiple times
|
|
for expected_state in [
|
|
RelayState::On,
|
|
RelayState::Off,
|
|
RelayState::On,
|
|
RelayState::Off,
|
|
] {
|
|
let write_result = controller.write_relay_state(relay_id, expected_state).await;
|
|
assert!(
|
|
write_result.is_ok(),
|
|
"Failed to write state {expected_state:?}"
|
|
);
|
|
|
|
let read_result = controller.read_relay_state(relay_id).await;
|
|
assert!(read_result.is_ok(), "Failed to read state back");
|
|
assert_eq!(
|
|
read_result.unwrap(),
|
|
expected_state,
|
|
"Relay state should match written value"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod write_all_states_validation_tests {
|
|
use super::*;
|
|
|
|
/// Test: `write_all_states()` returns `InvalidInput` when given 0 states
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_all_states_with_empty_vector_returns_invalid_input() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Act: Attempt to write with empty vector
|
|
let result = controller.write_all_states(vec![]).await;
|
|
|
|
// Assert: Should return InvalidInput error
|
|
assert!(result.is_err(), "Expected error for empty states vector");
|
|
assert!(
|
|
matches!(result.unwrap_err(), ControllerError::InvalidInput(_)),
|
|
"Expected InvalidInput error variant"
|
|
);
|
|
}
|
|
|
|
/// Test: `write_all_states()` returns `InvalidInput` when given 7 states (too few)
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_all_states_with_7_states_returns_invalid_input() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Act: Attempt to write with 7 states (missing one)
|
|
let states = vec![RelayState::On; 7];
|
|
let result = controller.write_all_states(states).await;
|
|
|
|
// Assert: Should return InvalidInput error with descriptive message
|
|
assert!(result.is_err(), "Expected error for 7 states");
|
|
match result.unwrap_err() {
|
|
ControllerError::InvalidInput(msg) => {
|
|
assert!(
|
|
msg.contains("Expected 8"),
|
|
"Error message should mention expected count"
|
|
);
|
|
assert!(
|
|
msg.contains('7'),
|
|
"Error message should mention actual count"
|
|
);
|
|
}
|
|
other => panic!("Expected InvalidInput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// Test: `write_all_states()` returns `InvalidInput` when given 9 states (too many)
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_all_states_with_9_states_returns_invalid_input() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Act: Attempt to write with 9 states (one extra)
|
|
let states = vec![RelayState::Off; 9];
|
|
let result = controller.write_all_states(states).await;
|
|
|
|
// Assert: Should return InvalidInput error with descriptive message
|
|
assert!(result.is_err(), "Expected error for 9 states");
|
|
match result.unwrap_err() {
|
|
ControllerError::InvalidInput(msg) => {
|
|
assert!(
|
|
msg.contains("Expected 8"),
|
|
"Error message should mention expected count"
|
|
);
|
|
assert!(
|
|
msg.contains('9'),
|
|
"Error message should mention actual count"
|
|
);
|
|
}
|
|
other => panic!("Expected InvalidInput, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// Test: `write_all_states()` succeeds with exactly 8 states
|
|
#[tokio::test]
|
|
#[ignore = "Requires Modbus server"]
|
|
async fn test_write_all_states_with_8_states_succeeds() {
|
|
// Arrange: Connect to test server
|
|
let controller = ModbusRelayController::new("127.0.0.1", 5020, 1, 5)
|
|
.await
|
|
.expect("Failed to connect to test server");
|
|
|
|
// Act: Write with exactly 8 states
|
|
let states = vec![RelayState::On; 8];
|
|
let result = controller.write_all_states(states).await;
|
|
|
|
// Assert: Should succeed
|
|
assert!(
|
|
result.is_ok(),
|
|
"Expected success for 8 states, got: {:?}",
|
|
result.err()
|
|
);
|
|
}
|
|
}
|