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}