aquarium_control/utilities/
database_ping_trait.rs

1use crate::database::pingable::Pingable;
2use crate::food::feed::Feed;
3use crate::mineral::balling::Balling;
4use crate::permission::schedule_check::ScheduleCheck;
5use crate::recorder::data_logger::DataLogger;
6use crate::thermal::heating::Heating;
7use crate::water::refill::Refill;
8use std::time::{Duration, Instant};
9
10pub trait DatabasePingTrait {
11    /// Gets the interval at which the resource should be pinged.
12    fn get_ping_interval(&self) -> Duration;
13
14    /// Gets the timestamp of the last successful ping.
15    fn get_last_ping_instant(&self) -> Instant;
16
17    /// Updates the timestamp of the last successful ping.
18    fn update_last_ping_instant(&mut self);
19
20    /// Pings the database.
21    fn check_timing_and_ping_database(
22        &mut self,
23        database_interface: &mut (impl Pingable + ?Sized),
24    ) {
25        if self.get_last_ping_instant().elapsed() > self.get_ping_interval() {
26            database_interface.ping();
27            self.update_last_ping_instant();
28        }
29    }
30}
31
32impl DatabasePingTrait for DataLogger {
33    fn get_ping_interval(&self) -> Duration {
34        self.database_ping_interval
35    }
36
37    fn get_last_ping_instant(&self) -> Instant {
38        self.last_ping_instant
39    }
40
41    fn update_last_ping_instant(&mut self) {
42        self.last_ping_instant = Instant::now();
43    }
44}
45
46impl DatabasePingTrait for Feed {
47    fn get_ping_interval(&self) -> Duration {
48        self.database_ping_interval
49    }
50
51    fn get_last_ping_instant(&self) -> Instant {
52        self.last_ping_instant
53    }
54
55    fn update_last_ping_instant(&mut self) {
56        self.last_ping_instant = Instant::now();
57    }
58}
59
60impl DatabasePingTrait for Refill {
61    fn get_ping_interval(&self) -> Duration {
62        self.database_ping_interval
63    }
64
65    fn get_last_ping_instant(&self) -> Instant {
66        self.last_ping_instant
67    }
68
69    fn update_last_ping_instant(&mut self) {
70        self.last_ping_instant = Instant::now();
71    }
72}
73
74impl DatabasePingTrait for Heating {
75    fn get_ping_interval(&self) -> Duration {
76        self.database_ping_interval
77    }
78
79    fn get_last_ping_instant(&self) -> Instant {
80        self.last_ping_instant
81    }
82
83    fn update_last_ping_instant(&mut self) {
84        self.last_ping_instant = Instant::now();
85    }
86}
87
88impl DatabasePingTrait for Balling {
89    fn get_ping_interval(&self) -> Duration {
90        self.database_ping_interval
91    }
92
93    fn get_last_ping_instant(&self) -> Instant {
94        self.last_ping_instant
95    }
96
97    fn update_last_ping_instant(&mut self) {
98        self.last_ping_instant = Instant::now();
99    }
100}
101
102impl DatabasePingTrait for ScheduleCheck {
103    fn get_ping_interval(&self) -> Duration {
104        self.database_ping_interval
105    }
106
107    fn get_last_ping_instant(&self) -> Instant {
108        self.last_ping_instant
109    }
110
111    fn update_last_ping_instant(&mut self) {
112        self.last_ping_instant = Instant::now();
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    // A mock struct to act as the object that can be pinged.
121    // It implements the `Pingable` trait and tracks if its `ping` method was called.
122    struct MockPingable {
123        ping_was_called: bool,
124    }
125
126    impl MockPingable {
127        fn new() -> Self {
128            Self {
129                ping_was_called: false,
130            }
131        }
132    }
133
134    impl Pingable for MockPingable {
135        fn ping(&mut self) {
136            self.ping_was_called = true;
137        }
138    }
139
140    // A mock struct to implement the `DatabasePingTrait`.
141    // This allows us to test the trait's default method in isolation.
142    struct MockPinger {
143        database_ping_interval: Duration,
144        last_ping_instant: Instant,
145    }
146
147    impl DatabasePingTrait for MockPinger {
148        fn get_ping_interval(&self) -> Duration {
149            self.database_ping_interval
150        }
151
152        fn get_last_ping_instant(&self) -> Instant {
153            self.last_ping_instant
154        }
155
156        fn update_last_ping_instant(&mut self) {
157            self.last_ping_instant = Instant::now();
158        }
159    }
160
161    #[test]
162    fn test_ping_not_needed_when_interval_not_elapsed() {
163        // Arrange: Set up a pinger with a long interval and a recent last_ping time.
164        let mut pinger = MockPinger {
165            database_ping_interval: Duration::from_secs(10),
166            last_ping_instant: Instant::now(),
167        };
168        let mut pingable = MockPingable::new();
169
170        // Act: Check if a ping is needed.
171        pinger.check_timing_and_ping_database(&mut pingable);
172
173        // Assert: The pingable object's ping method should NOT have been called.
174        assert!(
175            !pingable.ping_was_called,
176            "Ping should not have been called as the interval has not elapsed."
177        );
178    }
179
180    #[test]
181    fn test_ping_is_needed_when_interval_has_elapsed() {
182        // Arrange: Set up a pinger with a short interval and a last_ping time in the past.
183        let interval = Duration::from_millis(10);
184        let mut pinger = MockPinger {
185            database_ping_interval: interval,
186            last_ping_instant: Instant::now() - (interval + Duration::from_millis(5)),
187        };
188        let mut pingable = MockPingable::new();
189
190        // Act: Check if a ping is needed.
191        pinger.check_timing_and_ping_database(&mut pingable);
192
193        // Assert: The pingable object's ping method SHOULD have been called.
194        assert!(
195            pingable.ping_was_called,
196            "Ping should have been called because the interval has elapsed."
197        );
198    }
199
200    #[test]
201    fn test_ping_timer_is_reset_after_pinging() {
202        // Arrange: Set up a pinger where the interval has elapsed.
203        let interval = Duration::from_millis(10);
204        let mut pinger = MockPinger {
205            database_ping_interval: interval,
206            last_ping_instant: Instant::now() - (interval + Duration::from_millis(5)),
207        };
208        let mut pingable = MockPingable::new();
209
210        // Act 1: The first check should trigger a ping.
211        pinger.check_timing_and_ping_database(&mut pingable);
212
213        // Assert 1: The ping was triggered.
214        assert!(
215            pingable.ping_was_called,
216            "Ping should have been called the first time."
217        );
218
219        // Arrange 2: Reset the mock's flag for the next check.
220        pingable.ping_was_called = false;
221
222        // Act 2: Immediately check again. The timer should have been reset, so no ping is needed.
223        pinger.check_timing_and_ping_database(&mut pingable);
224
225        // Assert 2: The ping was NOT triggered a second time.
226        assert!(
227            !pingable.ping_was_called,
228            "Ping should not be called again immediately, as the timer should have been reset."
229        );
230    }
231}