//! 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 crate::domain::relay::types::RelayId; // NOTE: These tests will fail until MockRelayController and RelayController trait are implemented (T029, T030) // This follows TDD - write failing tests FIRST, then implement. #[tokio::test] async fn test_read_state_returns_mocked_state() { // Test: read_state() returns mocked state // // Setup: Create a mock controller and set relay 1 to On // Expected: read_state(1) should return On todo!("Implement after MockRelayController exists (T029)"); // let controller = MockRelayController::new(); // let relay_id = RelayId::new(1).unwrap(); // // // Write a known state // controller.write_state(relay_id, RelayState::On).await.unwrap(); // // // Read it back // let state = controller.read_state(relay_id).await.unwrap(); // assert_eq!(state, RelayState::On); } #[tokio::test] async fn test_write_state_updates_mocked_state() { // Test: write_state() updates mocked state // // Setup: Create a mock controller (all relays default to Off) // Action: Write relay 3 to On, then read it back // Expected: State should be On todo!("Implement after MockRelayController exists (T029)"); // let controller = MockRelayController::new(); // let relay_id = RelayId::new(3).unwrap(); // // // Initial state should be Off // let initial_state = controller.read_state(relay_id).await.unwrap(); // assert_eq!(initial_state, RelayState::Off); // // // Write On // controller.write_state(relay_id, RelayState::On).await.unwrap(); // // // Verify it changed // let updated_state = controller.read_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() returns 8 relays in known state // // Setup: Create a mock controller, set relays 1, 3, 5 to On, others Off // Action: Call read_all() // Expected: Returns Vec of 8 (RelayId, RelayState) tuples in correct state todo!("Implement after MockRelayController exists (T029)"); // let controller = MockRelayController::new(); // // // Set specific relays to On // controller.write_state(RelayId::new(1).unwrap(), RelayState::On).await.unwrap(); // controller.write_state(RelayId::new(3).unwrap(), RelayState::On).await.unwrap(); // controller.write_state(RelayId::new(5).unwrap(), RelayState::On).await.unwrap(); // // // Read all states // let all_states = controller.read_all().await.unwrap(); // // // Verify we have exactly 8 relays // assert_eq!(all_states.len(), 8); // // // Verify specific states // assert_eq!(all_states[0], (RelayId::new(1).unwrap(), RelayState::On)); // assert_eq!(all_states[1], (RelayId::new(2).unwrap(), RelayState::Off)); // assert_eq!(all_states[2], (RelayId::new(3).unwrap(), RelayState::On)); // assert_eq!(all_states[3], (RelayId::new(4).unwrap(), RelayState::Off)); // assert_eq!(all_states[4], (RelayId::new(5).unwrap(), RelayState::On)); // assert_eq!(all_states[5], (RelayId::new(6).unwrap(), RelayState::Off)); // assert_eq!(all_states[6], (RelayId::new(7).unwrap(), RelayState::Off)); // assert_eq!(all_states[7], (RelayId::new(8).unwrap(), RelayState::Off)); } #[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 todo!("Implement after MockRelayController exists (T029)"); // 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_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_state(relay_id).await.unwrap(); // assert_eq!(actual_state, expected_state, "Relay {} has incorrect state", i); // } } #[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 todo!("Implement after MockRelayController exists (T029)"); // use std::sync::Arc; // // let controller = Arc::new(MockRelayController::new()); // // // 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_state(relay_id).await.unwrap(); // let new_state = match current_state { // RelayState::On => RelayState::Off, // RelayState::Off => RelayState::On, // }; // controller_clone.write_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_state(RelayId::new(1).unwrap()).await.unwrap(); // assert!(matches!(final_state, RelayState::On | RelayState::Off)); } }