//! Mock relay controller for testing without hardware. //! //! This module provides a mock implementation of the relay controller //! that stores state in memory, enabling testing without physical Modbus hardware. use std::{ collections::HashMap, sync::{Arc, Mutex, MutexGuard}, }; use async_trait::async_trait; use crate::domain::relay::{ controller::{ControllerError, RelayController}, types::{RelayId, RelayState}, }; /// Mock relay controller for testing without physical Modbus hardware. /// /// This implementation stores relay states in memory using a thread-safe /// `Arc>`, enabling concurrent access in tests. It provides /// optional timeout simulation for testing error handling scenarios. #[derive(Debug, Clone)] pub struct MockRelayController { states: Arc>>, firmware_version: Option, simulate_timeout: bool, } impl MockRelayController { /// Creates a new mock relay controller with default configuration. /// /// The controller initializes with: /// - Empty relay state map (no relays configured) /// - Firmware version set to "v2.00" /// - Timeout simulation disabled #[must_use] pub fn new() -> Self { Self::default() } /// Enables timeout simulation for testing error handling. /// /// When enabled, all operations will simulate a 4-second delay followed /// by a timeout error. This is useful for testing timeout handling in /// application code. #[must_use] pub const fn with_timeout_simulation(mut self) -> Self { self.simulate_timeout = true; self } async fn maybe_timeout(&self) -> Result<(), ControllerError> { if self.simulate_timeout { tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; Err(ControllerError::Timeout(3)) } else { Ok(()) } } fn states(&self) -> Result>, ControllerError> { self.states .lock() .map_err(|e| ControllerError::ModbusException(e.to_string())) } } impl Default for MockRelayController { fn default() -> Self { let hashmap: HashMap = HashMap::new(); Self { states: Arc::new(Mutex::new(hashmap)), firmware_version: Some("v2.00".to_string()), simulate_timeout: false, } } } #[async_trait] impl RelayController for MockRelayController { async fn read_relay_state(&self, id: RelayId) -> Result { self.maybe_timeout().await?; let states = self.states()?; states.get(&id).map_or_else( || Err(ControllerError::InvalidRelayId(id.as_u8())), |state| Ok(*state), ) } async fn write_relay_state( &self, id: RelayId, state: RelayState, ) -> Result<(), ControllerError> { self.maybe_timeout().await?; self.states()?.insert(id, state); Ok(()) } async fn read_all_states(&self) -> Result, ControllerError> { self.maybe_timeout().await?; let mut vec: Vec<(RelayId, RelayState)> = self .states()? .iter() .map(|(id, state)| (*id, *state)) .collect(); vec.sort_by_key(|v| v.0); Ok(vec.iter().map(|v| v.1).collect()) } async fn write_all_states(&self, states: Vec) -> Result<(), ControllerError> { self.maybe_timeout().await?; let mut keys: Vec = self.states()?.keys().copied().collect(); keys.sort(); for update in keys.iter().zip(states) { self.states()?.insert(*update.0, update.1); } Ok(()) } async fn check_connection(&self) -> Result<(), ControllerError> { Ok(()) } async fn get_firmware_version(&self) -> Result, ControllerError> { Ok(self.firmware_version.clone()) } } #[cfg(test)] mod tests { use super::MockRelayController; use crate::domain::relay::types::RelayId; #[tokio::test] async fn test_read_state_returns_mocked_state() { // Test: read_relay_state() returns mocked state // // Setup: Create a mock controller and set relay 1 to On // Expected: read_relay_state(1) should return On use crate::domain::relay::{controller::RelayController, types::RelayState}; let controller = MockRelayController::new(); let relay_id = RelayId::new(1).unwrap(); // Write a known state controller .write_relay_state(relay_id, RelayState::On) .await .unwrap(); // Read it back let state = controller.read_relay_state(relay_id).await.unwrap(); assert_eq!(state, RelayState::On); } #[tokio::test] async fn test_write_state_updates_mocked_state() { // Test: write_relay_state() updates mocked state // // Setup: Create a mock controller with relay 3 initialized to Off // Action: Write relay 3 to On, then read it back // Expected: State should be On use crate::domain::relay::{controller::RelayController, types::RelayState}; let controller = MockRelayController::new(); let relay_id = RelayId::new(3).unwrap(); // Initialize relay 3 to Off controller .write_relay_state(relay_id, RelayState::Off) .await .unwrap(); // Verify initial state let initial_state = controller.read_relay_state(relay_id).await.unwrap(); assert_eq!(initial_state, RelayState::Off); // Write On controller .write_relay_state(relay_id, RelayState::On) .await .unwrap(); // Verify it changed let updated_state = controller.read_relay_state(relay_id).await.unwrap(); assert_eq!(updated_state, RelayState::On); } #[tokio::test] async fn test_read_all_returns_8_relays_in_known_state() { // Test: read_all_states() returns 8 relays in known state // // Setup: Create a mock controller, initialize all 8 relays, set relays 1, 3, 5 to On, others Off // Action: Call read_all_states() // Expected: Returns Vec of 8 RelayState values in correct order use crate::domain::relay::{controller::RelayController, types::RelayState}; let controller = MockRelayController::new(); // Initialize all 8 relays to Off first for i in 1..=8 { controller .write_relay_state(RelayId::new(i).unwrap(), RelayState::Off) .await .unwrap(); } // Set specific relays to On controller .write_relay_state(RelayId::new(1).unwrap(), RelayState::On) .await .unwrap(); controller .write_relay_state(RelayId::new(3).unwrap(), RelayState::On) .await .unwrap(); controller .write_relay_state(RelayId::new(5).unwrap(), RelayState::On) .await .unwrap(); // Read all states let all_states = controller.read_all_states().await.unwrap(); // Verify we have exactly 8 relays assert_eq!(all_states.len(), 8); // Verify specific states (indexed 0-7, corresponding to relays 1-8) assert_eq!(all_states[0], RelayState::On); // Relay 1 assert_eq!(all_states[1], RelayState::Off); // Relay 2 assert_eq!(all_states[2], RelayState::On); // Relay 3 assert_eq!(all_states[3], RelayState::Off); // Relay 4 assert_eq!(all_states[4], RelayState::On); // Relay 5 assert_eq!(all_states[5], RelayState::Off); // Relay 6 assert_eq!(all_states[6], RelayState::Off); // Relay 7 assert_eq!(all_states[7], RelayState::Off); // Relay 8 } #[tokio::test] async fn test_write_state_for_all_8_relays() { // Test: Can write state to all 8 relays independently // // Setup: Create a mock controller // Action: Write different states to each relay // Expected: Each relay maintains its own independent state use crate::domain::relay::{controller::RelayController, types::RelayState}; let controller = MockRelayController::new(); // Write alternating states (On, Off, On, Off, ...) for i in 1..=8 { let relay_id = RelayId::new(i).unwrap(); let state = if i % 2 == 1 { RelayState::On } else { RelayState::Off }; controller.write_relay_state(relay_id, state).await.unwrap(); } // Verify each relay has correct state for i in 1..=8 { let relay_id = RelayId::new(i).unwrap(); let expected_state = if i % 2 == 1 { RelayState::On } else { RelayState::Off }; let actual_state = controller.read_relay_state(relay_id).await.unwrap(); assert_eq!( actual_state, expected_state, "Relay {i} has incorrect state", ); } } #[tokio::test] async fn test_read_state_with_invalid_relay_id() { // Test: read_state() with out-of-range relay ID fails gracefully // // Note: RelayId::new() will already fail for invalid IDs (0 or 9+), // so this test verifies the type system prevents invalid relay IDs // at construction time (type-driven design) // Verify RelayId construction fails for invalid IDs assert!(RelayId::new(0).is_err(), "RelayId::new(0) should fail"); assert!(RelayId::new(9).is_err(), "RelayId::new(9) should fail"); // If we somehow get an invalid relay ID through (which shouldn't be possible), // the controller should handle it gracefully (tested in T029 implementation) } #[tokio::test] async fn test_concurrent_access_is_safe() { // Test: MockRelayController is thread-safe (uses Arc>) // // Setup: Create mock controller, spawn multiple tasks that read/write // Expected: No data races, all operations complete successfully use std::sync::Arc; use crate::domain::relay::{controller::RelayController, types::RelayState}; let controller = Arc::new(MockRelayController::new()); let relay_id = RelayId::new(1).unwrap(); // Initialize relay 1 to Off controller .write_relay_state(relay_id, RelayState::Off) .await .unwrap(); // Spawn 10 tasks that toggle relay 1 let mut handles = vec![]; for _ in 0..10 { let controller_clone = Arc::clone(&controller); let handle = tokio::spawn(async move { let relay_id = RelayId::new(1).unwrap(); let current_state = controller_clone.read_relay_state(relay_id).await.unwrap(); let new_state = match current_state { RelayState::On => RelayState::Off, RelayState::Off => RelayState::On, }; controller_clone .write_relay_state(relay_id, new_state) .await .unwrap(); }); handles.push(handle); } // Wait for all tasks to complete for handle in handles { handle.await.unwrap(); } // Controller should still be in valid state (either On or Off) let final_state = controller.read_relay_state(relay_id).await.unwrap(); assert!(matches!(final_state, RelayState::On | RelayState::Off)); } }