Skip to main content

pamoja_sim/
actuator.rs

1//! A fake actuator that records the commands it is given.
2
3use std::sync::{Arc, Mutex};
4
5use pamoja_core::{Actuator, Result};
6
7/// An actuator that records every command instead of driving hardware.
8///
9/// This stands in for a relay, a valve, or a motor in a hardware-free test: it
10/// implements the core [`Actuator`] trait and keeps an ordered log of every command
11/// applied to it, so a test can assert what a control loop decided to do. Take a
12/// [`log`](RecordingActuator::log) handle before moving the actuator into a `Node`,
13/// then read the commands back through it afterwards.
14///
15/// # Examples
16///
17/// ```
18/// use pamoja_core::Actuator;
19/// use pamoja_sim::RecordingActuator;
20///
21/// # async fn demo() -> pamoja_core::Result<()> {
22/// let mut relay = RecordingActuator::new();
23/// let log = relay.log();
24///
25/// relay.apply(true).await?;
26/// relay.apply(false).await?;
27///
28/// assert_eq!(log.commands(), vec![true, false]);
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Clone, Debug)]
33pub struct RecordingActuator<C> {
34    log: Arc<Mutex<Vec<C>>>,
35}
36
37impl<C> RecordingActuator<C> {
38    /// Creates an actuator with an empty command log.
39    ///
40    /// # Returns
41    ///
42    /// A recording actuator.
43    pub fn new() -> Self {
44        Self {
45            log: Arc::new(Mutex::new(Vec::new())),
46        }
47    }
48
49    /// Returns a handle that reads this actuator's command log.
50    ///
51    /// The handle shares the same underlying log, so commands applied after it is
52    /// taken are still visible through it.
53    ///
54    /// # Returns
55    ///
56    /// An [`ActuatorLog`] over the same recorded commands.
57    pub fn log(&self) -> ActuatorLog<C> {
58        ActuatorLog {
59            log: Arc::clone(&self.log),
60        }
61    }
62}
63
64impl<C> Default for RecordingActuator<C> {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl<C> Actuator for RecordingActuator<C> {
71    type Command = C;
72
73    async fn apply(&mut self, command: C) -> Result<()> {
74        self.log.lock().expect("actuator log lock").push(command);
75        Ok(())
76    }
77}
78
79/// A read handle over a [`RecordingActuator`]'s command log.
80#[derive(Clone, Debug)]
81pub struct ActuatorLog<C> {
82    log: Arc<Mutex<Vec<C>>>,
83}
84
85impl<C: Clone> ActuatorLog<C> {
86    /// Returns a snapshot of every command applied so far, in order.
87    ///
88    /// # Returns
89    ///
90    /// A copy of the recorded commands.
91    pub fn commands(&self) -> Vec<C> {
92        self.log.lock().expect("actuator log lock").clone()
93    }
94}
95
96impl<C> ActuatorLog<C> {
97    /// Returns how many commands have been applied.
98    ///
99    /// # Returns
100    ///
101    /// The number of recorded commands.
102    pub fn len(&self) -> usize {
103        self.log.lock().expect("actuator log lock").len()
104    }
105
106    /// Returns whether no command has been applied yet.
107    ///
108    /// # Returns
109    ///
110    /// `true` if the command log is empty.
111    pub fn is_empty(&self) -> bool {
112        self.log.lock().expect("actuator log lock").is_empty()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[tokio::test]
121    async fn it_records_each_command_in_order() {
122        let mut relay = RecordingActuator::new();
123        let log = relay.log();
124        assert!(log.is_empty());
125
126        relay.apply(true).await.unwrap();
127        relay.apply(false).await.unwrap();
128        relay.apply(true).await.unwrap();
129
130        assert_eq!(log.commands(), vec![true, false, true]);
131        assert_eq!(log.len(), 3);
132        assert!(!log.is_empty());
133    }
134
135    #[tokio::test]
136    async fn a_log_taken_early_sees_later_commands() {
137        let relay = RecordingActuator::new();
138        let log = relay.log();
139        // The actuator can be moved on (here, cloned) and still feed the same log.
140        let mut moved = relay.clone();
141        moved.apply(42u8).await.unwrap();
142        assert_eq!(log.commands(), vec![42u8]);
143    }
144}