1use std::time::{Duration, Instant};
13
14use chrono::Duration as ChronoDuration;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum TimerType {
21 Warning,
23 DeadMan,
25}
26
27#[derive(Debug, Clone, PartialEq)]
32pub struct Timer {
33 timer_type: TimerType,
35 start: Instant,
37 duration: Duration,
39}
40
41impl Timer {
42 pub fn new(timer_type: TimerType, duration: Duration) -> Self {
44 Timer {
45 timer_type,
46 start: Instant::now(),
47 duration,
48 }
49 }
50
51 pub fn get_type(&self) -> TimerType {
54 self.timer_type
55 }
56
57 pub fn elapsed(&self) -> Duration {
59 Instant::now().duration_since(self.start)
60 }
61
62 pub fn remaining_percent(&self) -> u16 {
64 let elapsed = self.start.elapsed().as_secs();
65 let total = self.duration.as_secs();
66 if elapsed >= total {
67 return 0;
68 }
69 let remaining = total.saturating_sub(elapsed);
70 (remaining as f64 / total as f64 * 100.0) as u16
71 }
72
73 pub fn label(&self) -> String {
75 let elapsed = self.start.elapsed();
76 if elapsed >= self.duration {
77 return "0 second(s)".to_string();
78 }
79
80 let remaining = self
81 .duration
82 .checked_sub(elapsed)
83 .unwrap_or(Duration::from_secs(0));
84 let remaining_chrono = ChronoDuration::try_seconds(remaining.as_secs() as i64)
85 .unwrap_or(ChronoDuration::zero());
86 format_duration(remaining_chrono)
87 }
88
89 pub fn update(&mut self, elapsed: Duration, dead_man_duration: u64) {
92 if self.timer_type == TimerType::Warning && elapsed >= self.duration {
93 self.timer_type = TimerType::DeadMan;
94 self.start = Instant::now();
96 self.duration = Duration::from_secs(dead_man_duration);
97 }
98 }
99
100 pub fn expired(&self) -> bool {
102 self.start.elapsed() >= self.duration
103 }
104
105 pub fn reset(&mut self, config: &crate::config::Config) {
110 match self.get_type() {
111 TimerType::Warning => {
112 self.start = Instant::now();
113 }
114 TimerType::DeadMan => {
115 self.timer_type = TimerType::Warning;
116 self.start = Instant::now();
117 self.duration = Duration::from_secs(config.timer_warning);
118 }
119 }
120 }
121}
122
123fn format_duration(duration: ChronoDuration) -> String {
125 let days = duration.num_days();
126 let hours = duration.num_hours() % 24;
127 let minutes = duration.num_minutes() % 60;
128 let seconds = duration.num_seconds() % 60;
129
130 let mut parts = vec![];
131
132 if days > 0 {
133 parts.push(format!("{days} day(s)"));
134 }
135 if hours > 0 {
136 parts.push(format!("{hours} hour(s)"));
137 }
138 if minutes > 0 {
139 parts.push(format!("{minutes} minute(s)"));
140 }
141 if seconds > 0 || parts.is_empty() {
142 parts.push(format!("{} second(s)", seconds + 1));
143 }
144
145 parts.join(", ")
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::config::Config;
152 use std::thread::sleep;
153
154 fn get_test_config() -> Config {
155 Config {
156 username: "user@example.com".to_string(),
157 password: "password".to_string(),
158 smtp_server: "smtp.example.com".to_string(),
159 smtp_port: 587,
160 message: "This is a test message".to_string(),
161 message_warning: "This is a test warning message".to_string(),
162 subject: "Test Subject".to_string(),
163 subject_warning: "Test Warning Subject".to_string(),
164 to: "recipient@example.com".to_string(),
165 from: "sender@example.com".to_string(),
166 attachment: None,
167 timer_warning: 60,
168 timer_dead_man: 120,
169 web_password: "password".to_string(),
170 cookie_exp_days: 7,
171 log_level: None,
172 }
173 }
174
175 #[test]
176 fn timer_creation() {
177 let warning_timer = Timer::new(TimerType::Warning, Duration::from_secs(60));
178 assert_eq!(warning_timer.get_type(), TimerType::Warning);
179 assert!(warning_timer.duration == Duration::from_secs(60));
180 }
181
182 #[test]
183 fn timer_elapsed_less_than_duration() {
184 let timer = Timer::new(TimerType::Warning, Duration::from_secs(60));
185 assert!(timer.elapsed() < Duration::from_secs(60));
186 }
187
188 #[test]
189 fn timer_update_to_dead_man() {
190 let mut timer = Timer::new(TimerType::Warning, Duration::from_secs(1));
191 timer.update(Duration::from_secs(2), 3600);
193 assert_eq!(timer.get_type(), TimerType::DeadMan);
194 assert_eq!(timer.duration, Duration::from_secs(3600));
195 assert!(!timer.expired());
196 }
197
198 #[test]
199 fn timer_expiration() {
200 let timer = Timer::new(TimerType::Warning, Duration::from_secs(1));
201 sleep(Duration::from_secs(2));
203 assert!(timer.expired());
204 }
205
206 #[test]
207 fn format_seconds_only() {
208 let duration = ChronoDuration::try_seconds(45).unwrap();
209 assert_eq!(format_duration(duration), "46 second(s)");
210 }
211
212 #[test]
213 fn format_minutes_and_seconds() {
214 let duration =
215 ChronoDuration::try_minutes(5).unwrap() + ChronoDuration::try_seconds(30).unwrap();
216 assert_eq!(format_duration(duration), "5 minute(s), 31 second(s)");
217 }
218
219 #[test]
220 fn format_hours_minutes_and_seconds() {
221 let duration = ChronoDuration::try_hours(2).unwrap()
222 + ChronoDuration::try_minutes(15).unwrap()
223 + ChronoDuration::try_seconds(10).unwrap();
224 assert_eq!(
225 format_duration(duration),
226 "2 hour(s), 15 minute(s), 11 second(s)"
227 );
228 }
229
230 #[test]
231 fn format_days_hours_minutes() {
232 let duration = ChronoDuration::try_days(1).unwrap()
233 + ChronoDuration::try_hours(3).unwrap()
234 + ChronoDuration::try_minutes(45).unwrap();
235 assert_eq!(
236 format_duration(duration),
237 "1 day(s), 3 hour(s), 45 minute(s)"
238 );
239 }
240
241 #[test]
242 fn format_days_only() {
243 let duration = ChronoDuration::try_days(4).unwrap();
244 assert_eq!(format_duration(duration), "4 day(s)");
245 }
246
247 #[test]
248 fn format_large_duration() {
249 let duration = ChronoDuration::try_days(7).unwrap()
250 + ChronoDuration::try_hours(23).unwrap()
251 + ChronoDuration::try_minutes(59).unwrap()
252 + ChronoDuration::try_seconds(59).unwrap();
253 assert_eq!(
254 format_duration(duration),
255 "7 day(s), 23 hour(s), 59 minute(s), 60 second(s)"
256 );
257 }
258
259 #[test]
260 fn reset_warning_timer_resets_start_time() {
261 let config = get_test_config();
262
263 let mut timer = Timer::new(
264 TimerType::Warning,
265 Duration::from_secs(config.timer_warning),
266 );
267 let original_start = timer.start;
268 sleep(Duration::from_millis(100));
270 timer.reset(&config);
271 assert!(timer.start > original_start);
272 assert_eq!(timer.duration, Duration::from_secs(config.timer_warning));
273 assert_eq!(timer.get_type(), TimerType::Warning);
274 }
275
276 #[test]
277 fn reset_dead_man_timer_promotes_to_warning_and_resets() {
278 let config = get_test_config();
279
280 let mut timer = Timer::new(
281 TimerType::DeadMan,
282 Duration::from_secs(config.timer_dead_man),
283 );
284 sleep(Duration::from_millis(100));
286 timer.reset(&config);
287 assert_eq!(timer.get_type(), TimerType::Warning);
288 assert_eq!(timer.duration, Duration::from_secs(config.timer_warning));
289 }
290}