feat(domain): implement RelayController trait and error handling
Add RelayController async trait (T030) defining interface for Modbus relay operations with methods for read/write state, bulk operations, connection checks, and firmware queries. Implement ControllerError enum (T031) with variants for connection failures, timeouts, Modbus exceptions, and invalid relay IDs. Provide MockRelayController (T029) in-memory implementation using Arc<Mutex<HashMap>> for thread-safe state storage with timeout simulation for error handling tests. Add RelayLabelRepository trait abstraction. Ref: T029 T030 T031 (specs/001-modbus-relay-control/tasks.md)
This commit is contained in:
@@ -3,6 +3,131 @@
|
||||
//! 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<Mutex<HashMap>>`, enabling concurrent access in tests. It provides
|
||||
/// optional timeout simulation for testing error handling scenarios.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockRelayController {
|
||||
states: Arc<Mutex<HashMap<RelayId, RelayState>>>,
|
||||
firmware_version: Option<String>,
|
||||
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<MutexGuard<'_, HashMap<RelayId, RelayState>>, ControllerError> {
|
||||
self.states
|
||||
.lock()
|
||||
.map_err(|e| ControllerError::ModbusException(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockRelayController {
|
||||
fn default() -> Self {
|
||||
let hashmap: HashMap<RelayId, RelayState> = 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<RelayState, ControllerError> {
|
||||
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<Vec<RelayState>, 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<RelayState>) -> Result<(), ControllerError> {
|
||||
self.maybe_timeout().await?;
|
||||
let mut keys: Vec<RelayId> = 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<Option<String>, ControllerError> {
|
||||
Ok(self.firmware_version.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::relay::types::RelayId;
|
||||
|
||||
Reference in New Issue
Block a user