1use crate::app;
13use crate::config::Config;
14use crate::error::TimerError;
15
16use chrono::Duration as ChronoDuration;
17use serde::{Deserialize, Serialize};
18use std::fs::{self, File};
19use std::io::Write;
20use std::path::PathBuf;
21use std::sync::{OnceLock, mpsc};
22use std::thread;
23use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
24use tracing::{error, trace, warn};
25
26static PERSIST_SENDER: OnceLock<mpsc::Sender<State>> = OnceLock::new();
27
28const DEFAULT_DEBOUNCE_MS: u64 = 300;
29
30#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
34pub enum TimerType {
35 #[default]
37 Warning,
38 DeadMan,
40}
41
42#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
46pub struct State {
47 timer_type: TimerType,
49 last_modified: u64,
51}
52
53#[derive(Debug, Clone, PartialEq)]
58pub struct Timer {
59 timer_type: TimerType,
61 start: Instant,
63 duration: Duration,
65}
66
67impl Timer {
68 pub fn new(config: &Config) -> Result<Self, TimerError> {
70 let timer = load_or_initialize(config)?;
71
72 Ok(timer)
73 }
74
75 pub fn get_type(&self) -> TimerType {
78 self.timer_type
79 }
80
81 pub fn elapsed(&self) -> Duration {
83 Instant::now().duration_since(self.start)
84 }
85
86 pub fn remaining_chrono(&self) -> ChronoDuration {
88 let elapsed = self.elapsed();
89 if elapsed < self.duration {
90 let remaining = self.duration.saturating_sub(elapsed);
91 return ChronoDuration::try_seconds(remaining.as_secs() as i64)
92 .unwrap_or(ChronoDuration::zero());
93 }
94
95 ChronoDuration::zero()
96 }
97
98 pub fn remaining_percent(&self) -> u16 {
100 let remaining_chrono = self.remaining_chrono();
101
102 if remaining_chrono > ChronoDuration::zero() {
103 return (remaining_chrono.num_seconds() as f64 / self.duration.as_secs() as f64 * 100.0)
104 as u16;
105 }
106
107 0
108 }
109
110 pub fn label(&self) -> String {
112 let remaining_chrono = self.remaining_chrono();
113 if remaining_chrono > ChronoDuration::zero() {
114 return format_duration(remaining_chrono);
115 }
116
117 "0 second(s)".to_string()
118 }
119
120 pub fn update(&mut self, elapsed: Duration, dead_man_duration: u64) -> Result<(), TimerError> {
123 if self.timer_type == TimerType::Warning && elapsed >= self.duration {
124 self.timer_type = TimerType::DeadMan;
125 self.start = Instant::now();
127 self.duration = Duration::from_secs(dead_man_duration);
128
129 let state = state_from_timer(self)?;
130 persist_state_non_blocking(state)?;
131 }
132
133 Ok(())
134 }
135
136 pub fn expired(&self) -> bool {
138 self.elapsed() >= self.duration
139 }
140
141 pub fn reset(&mut self, config: &crate::config::Config) -> Result<(), TimerError> {
146 let (timer, state) = default_timer_and_state(config)?;
147 self.timer_type = timer.timer_type;
148 self.start = timer.start;
149 self.duration = timer.duration;
150
151 persist_state_non_blocking(state)?;
152
153 Ok(())
154 }
155}
156
157fn format_duration(duration: ChronoDuration) -> String {
159 let days = duration.num_days();
160 let hours = duration.num_hours() % 24;
161 let minutes = duration.num_minutes() % 60;
162 let seconds = duration.num_seconds() % 60;
163
164 let mut parts = vec![];
165
166 if days > 0 {
167 parts.push(format!("{days} day(s)"));
168 }
169 if hours > 0 {
170 parts.push(format!("{hours} hour(s)"));
171 }
172 if minutes > 0 {
173 parts.push(format!("{minutes} minute(s)"));
174 }
175 if seconds > 0 || parts.is_empty() {
176 parts.push(format!("{} second(s)", seconds));
177 }
178
179 parts.join(", ")
180}
181
182pub fn load_or_initialize(config: &Config) -> Result<Timer, TimerError> {
191 let file_path = file_path()?;
192
193 let (timer, state) = match fs::read_to_string(&file_path) {
195 Ok(state_str) => {
196 match toml::from_str::<State>(state_str.as_str()) {
197 Ok(state) => {
198 let wall_elapsed = wall_elapsed(state.last_modified);
200 let now_mono = Instant::now();
204 let start = match now_mono.checked_sub(wall_elapsed) {
205 Some(s) => s,
206 None => now_mono, };
208 let timer = Timer {
209 timer_type: state.timer_type,
210 start,
211 duration: match state.timer_type {
212 TimerType::Warning => Duration::from_secs(config.timer_warning),
213 TimerType::DeadMan => Duration::from_secs(config.timer_dead_man),
214 },
215 };
216 (timer, state)
217 }
218 Err(e) => {
219 warn!(
221 error = ?e,
222 path = %file_path.display(),
223 "persisted file parse error: using defaults"
224 );
225 default_timer_and_state(config)?
226 }
227 }
228 }
229 Err(_) => {
230 trace!(
232 path = %file_path.display(),
233 "no persisted file found: using defaults"
234 );
235 default_timer_and_state(config)?
236 }
237 };
238
239 let _ = PERSIST_SENDER.get_or_init(|| spawn_persistence_worker(Some(state.clone())));
241
242 persist_state_blocking(state)?;
245
246 Ok(timer)
247}
248
249fn default_timer_and_state(config: &Config) -> Result<(Timer, State), TimerError> {
250 let timer = Timer {
251 timer_type: TimerType::Warning,
252 start: Instant::now(),
253 duration: Duration::from_secs(config.timer_warning),
254 };
255 let state = state_from_timer(&timer)?;
256
257 Ok((timer, state))
258}
259
260fn file_name() -> &'static str {
262 "state.toml"
263}
264
265pub fn file_path() -> Result<PathBuf, TimerError> {
272 let path = app::file_path(file_name())?;
273
274 Ok(path)
275}
276
277pub fn system_time_epoch() -> Result<Duration, TimerError> {
279 let now_wall = SystemTime::now()
280 .duration_since(UNIX_EPOCH)
281 .map_err(TimerError::SystemTime)?;
282
283 Ok(now_wall)
284}
285
286fn wall_elapsed(last_modified: u64) -> Duration {
291 let persisted_last_modified = Duration::from_secs(last_modified);
292 let now_wall = system_time_epoch().unwrap_or(Duration::from_secs(0));
293
294 if now_wall > persisted_last_modified {
295 now_wall - persisted_last_modified
296 } else {
297 Duration::from_secs(0)
298 }
299}
300
301fn state_from_timer(timer: &Timer) -> Result<State, TimerError> {
302 let state = State {
303 timer_type: timer.timer_type,
304 last_modified: system_time_epoch()?.as_secs(),
305 };
306
307 Ok(state)
308}
309
310pub fn persist_state_blocking(state: State) -> Result<(), TimerError> {
315 let path = file_path()?;
316 if let Err(e) = persist_state_to_path(&path, &state) {
317 error!(error = ?e, path = %path.display(), "persist new state failed");
318 return Err(e);
319 }
320
321 Ok(())
322}
323
324fn persist_state_non_blocking(state: State) -> Result<(), TimerError> {
330 let sender = PERSIST_SENDER
331 .get_or_init(|| spawn_persistence_worker(Some(state.clone())))
332 .clone();
333 if let Err(e) = sender.send(state) {
334 error!(error = ?e, "failed to enqueue persistence of state; background worker may have stopped");
335 }
336 Ok(())
337}
338
339fn spawn_persistence_worker(state: Option<State>) -> mpsc::Sender<State> {
340 let (tx, rx) = mpsc::channel::<State>();
341
342 thread::spawn(move || {
343 let mut last_written: Option<State> = state;
344 let debounce = Duration::from_millis(DEFAULT_DEBOUNCE_MS);
345
346 while let Ok(mut snapshot) = rx.recv() {
347 while let Ok(next) = rx.try_recv() {
349 snapshot = next;
350 }
351
352 let start = Instant::now();
354 while start.elapsed() < debounce {
355 if let Ok(next) = rx.try_recv() {
356 snapshot = next;
357 } else {
358 thread::sleep(Duration::from_millis(10));
360 }
361 }
362
363 if last_written.as_ref() == Some(&snapshot) {
365 trace!("skipping persist state: identical to last written");
366 continue;
367 }
368
369 if let Ok(path) = file_path() {
370 if let Err(e) = persist_state_to_path(&path, &snapshot) {
371 error!(error = ?e, path = %path.display(), "persist new state failed");
372 } else {
373 trace!(path = %path.display(), "persisted new state");
374 last_written = Some(snapshot);
375 }
376 }
377 }
378 });
379
380 tx
381}
382
383fn persist_state_to_path(path: &PathBuf, state: &State) -> Result<(), TimerError> {
384 let state_str = toml::to_string(state)?;
385 write_atomic(path, state_str.as_bytes())
386}
387
388fn write_atomic(path: &PathBuf, data: &[u8]) -> Result<(), TimerError> {
389 let tmp_path = path.with_extension("tmp");
390 {
391 let mut tmp = File::create(&tmp_path)?;
392 tmp.write_all(data)?;
393 tmp.sync_all()?;
394 }
395 fs::rename(&tmp_path, path)?;
396 Ok(())
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::config::{self, Config};
403 use std::fs::{self, File};
404 use std::io::Write;
405 use std::ops::Sub;
406 use std::path::Path;
407 use std::thread::sleep;
408
409 fn get_test_config() -> Config {
410 Config {
411 username: "user@example.com".to_string(),
412 password: "password".to_string(),
413 smtp_server: "smtp.example.com".to_string(),
414 smtp_port: 587,
415 smtp_check_timeout: Some(5),
416 message: "This is a test message".to_string(),
417 message_warning: "This is a test warning message".to_string(),
418 subject: "Test Subject".to_string(),
419 subject_warning: "Test Warning Subject".to_string(),
420 to: "recipient@example.com".to_string(),
421 from: "sender@example.com".to_string(),
422 attachment: None,
423 timer_warning: 60,
424 timer_dead_man: 120,
425 web_password: "password".to_string(),
426 cookie_exp_days: 7,
427 log_level: None,
428 }
429 }
430
431 struct TestGuard;
432 impl TestGuard {
433 fn new(s: &State) -> Self {
434 let file_path = file_path().expect("setup: failed file_path()");
437
438 if let Some(parent) = file_path.parent() {
440 fs::create_dir_all(parent).expect("setup: failed to create dir");
441 }
442 let mut file = File::create(file_path).expect("setup: failed to create file");
443 let s_str = toml::to_string(s).expect("setup: failed to convert data");
444 file.write_all(s_str.as_bytes())
445 .expect("setup: failed to write data");
446 file.sync_all()
447 .expect("setup: failed to ensure file written to disk");
448
449 TestGuard
450 }
451 }
452 impl Drop for TestGuard {
453 fn drop(&mut self) {
454 let file_path = file_path().expect("teardown: failed file_path()");
456 cleanup_test_dir_parent(file_path.as_path());
457 }
458 }
459
460 fn cleanup_test_dir(dir: &Path) {
462 if let Some(parent) = dir.parent() {
463 let _ = fs::remove_dir_all(parent);
464 }
465 }
466
467 fn cleanup_test_dir_parent(dir: &Path) {
469 if let Some(parent) = dir.parent() {
470 cleanup_test_dir(parent)
471 }
472 }
473
474 fn load_state_from_path(path: &PathBuf) -> State {
476 let state_str = fs::read_to_string(path).expect("helper: error reading state data");
477 let state: State = toml::from_str(&state_str).expect("helper: error parsing state data");
478 state
479 }
480
481 #[test]
482 fn file_path_in_test_mode() {
483 let result = file_path();
485 assert!(result.is_ok());
486
487 let expected = format!("{}_test", app::name());
488 let result = result.unwrap();
489 assert!(result.to_string_lossy().contains(expected.as_str()));
490
491 let expected = Path::new(app::name()).join(file_name());
493 assert!(
494 result
495 .to_string_lossy()
496 .contains(expected.to_string_lossy().as_ref())
497 );
498
499 cleanup_test_dir_parent(&result);
501 }
502
503 #[test]
504 fn generates_system_time_epoch() {
505 let result = system_time_epoch();
506 assert!(result.is_ok());
507
508 let expected_range_low = 1767225600; let expected_range_high = 4102444800; let result = result.unwrap().as_secs();
511 assert!(
512 result >= expected_range_low && result <= expected_range_high,
513 "expected: between {:?} (2026-01-01 00:00:00) and {:?} (2100-01-01 00:00:00) (got: {:?})",
514 expected_range_low,
515 expected_range_high,
516 result
517 );
518 }
519
520 #[test]
521 fn update_persisted_state_ok() {
522 let state = State {
524 timer_type: TimerType::Warning,
525 last_modified: 1,
526 };
527
528 let result = persist_state_blocking(state.clone());
529 assert!(result.is_ok());
530
531 let test_path = file_path().unwrap();
532 let loaded_state = load_state_from_path(&test_path);
533 assert_eq!(loaded_state, state);
535
536 cleanup_test_dir_parent(&test_path);
538 }
539
540 #[test]
541 fn state_guard_ok() {
542 let state = State {
547 timer_type: TimerType::DeadMan,
548 last_modified: 2,
549 };
550 let _guard = TestGuard::new(&state);
551
552 let test_path = file_path().unwrap();
554 let loaded_state = load_state_from_path(&test_path);
555 assert_eq!(loaded_state, state);
556 }
557
558 #[test]
559 fn load_or_initialize_with_existing_file() {
560 let config = Config::default();
561
562 let existing_state_1 = State {
564 timer_type: TimerType::Warning,
565 last_modified: 3,
566 };
567 let existing_state_2 = State {
568 timer_type: TimerType::DeadMan,
569 last_modified: 4,
570 };
571
572 let _guard_1 = TestGuard::new(&existing_state_1);
573
574 let timer = load_or_initialize(&config).unwrap();
576
577 assert_eq!(timer.timer_type, existing_state_1.timer_type);
579
580 let _guard_2 = TestGuard::new(&existing_state_2);
581
582 let timer = load_or_initialize(&config).unwrap();
584
585 assert_eq!(timer.timer_type, existing_state_2.timer_type);
587 }
588
589 #[test]
590 fn load_or_initialize_with_no_existing_file() {
591 let config = Config::default();
593
594 let timer = load_or_initialize(&config).unwrap();
596
597 let expected = TimerType::Warning;
598 let result = timer.timer_type;
599 assert!(
600 result == expected,
601 "expected: '{:?}' got: '{:?}')",
602 expected,
603 result
604 );
605
606 let expected = Duration::from_secs(config.timer_warning);
607 let result = timer.duration;
608 assert!(
609 result == expected,
610 "expected: '{:?}' got: '{:?}')",
611 expected,
612 result
613 );
614
615 let test_path = file_path().unwrap();
617 cleanup_test_dir_parent(&test_path);
618 }
619
620 #[test]
621 fn timer_remaining_chrono_with_state_in_past() {
622 let default_config = config::load_or_initialize().expect("failed to load default config");
623
624 let now_wall = system_time_epoch()
625 .expect("failed to get current time")
626 .as_secs();
627
628 let _guard = TestGuard::new(&State {
630 timer_type: TimerType::Warning,
631 last_modified: now_wall - 10000,
632 });
633
634 let timer = Timer::new(&default_config).expect("failed to create new timer");
635
636 let tolerance_secs = 5;
637 let expected_range_high =
638 chrono::TimeDelta::new((default_config.timer_warning - 10000) as i64, 0)
639 .expect("failed creating time delta");
640 let expected_range_low = expected_range_high
641 .sub(chrono::TimeDelta::new(tolerance_secs, 0).expect("failed creating time delta"));
642 let result = timer.remaining_chrono();
643 assert!(
645 result >= expected_range_low && result <= expected_range_high,
646 "expected: timer.remaining_chrono() between {:?} and {:?} (got: {:?})",
647 expected_range_low,
648 expected_range_high,
649 result
650 );
651 }
652
653 #[test]
654 fn timer_remaining_chrono_with_state_in_future() {
655 let default_config = config::load_or_initialize().expect("failed to load default config");
656
657 let now_wall = system_time_epoch()
658 .expect("failed to get current time")
659 .as_secs();
660
661 let _guard = TestGuard::new(&State {
664 timer_type: TimerType::Warning,
665 last_modified: now_wall + 1000,
666 });
667
668 let timer = Timer::new(&default_config).expect("failed to create new timer");
669
670 let tolerance_secs = 5;
671 let expected_range_high = chrono::TimeDelta::new((default_config.timer_warning) as i64, 0)
672 .expect("failed creating time delta");
673 let expected_range_low = expected_range_high
674 .sub(chrono::TimeDelta::new(tolerance_secs, 0).expect("failed creating time delta"));
675 let result = timer.remaining_chrono();
677 assert!(
678 result >= expected_range_low && result <= expected_range_high,
679 "expected: timer.remaining_chrono() between {:?} and {:?} (got: {:?})",
680 expected_range_low,
681 expected_range_high,
682 result
683 );
684 }
685
686 #[test]
687 fn timer_remaining_chrono_with_state() {
688 let mut config = get_test_config();
689
690 let now_wall = system_time_epoch()
691 .expect("failed to get current time")
692 .as_secs();
693
694 let _guard = TestGuard::new(&State {
696 timer_type: TimerType::Warning,
697 last_modified: now_wall,
698 });
699
700 let mut timer = Timer::new(&config).expect("failed to create new timer");
701
702 config.timer_warning = 2;
703 timer.reset(&config).expect("failed to reset timer");
705
706 let expected = chrono::TimeDelta::new(2, 0).expect("failed creating time delta");
707 let result = timer.remaining_chrono();
708 assert!(
709 result <= expected,
710 "expected: timer.remaining_chrono() < {:?} (got: {:?})",
711 expected,
712 result
713 );
714
715 sleep(Duration::from_secs(2));
716
717 let expected = chrono::TimeDelta::new(1, 0).expect("failed creating time delta");
718 let result = timer.remaining_chrono();
719 assert!(
720 result <= expected,
721 "expected: timer.remaining_chrono() < {:?} (got: {:?})",
722 expected,
723 result
724 );
725 }
726
727 #[test]
728 fn timer_elapsed_less_than_duration() {
729 let mut config = get_test_config();
730
731 let now_wall = system_time_epoch()
732 .expect("failed to get current time")
733 .as_secs();
734
735 let _guard = TestGuard::new(&State {
737 timer_type: TimerType::Warning,
738 last_modified: now_wall,
739 });
740
741 let mut timer = Timer::new(&config).expect("failed to create new timer");
742
743 config.timer_warning = 60;
744 timer.reset(&config).expect("failed to reset timer");
746
747 let expected = Duration::from_secs(2);
748 let result = timer.elapsed();
749 assert!(
750 result < expected,
751 "expected: timer.elapsed() < {:?} (got: {:?})",
752 expected,
753 result
754 );
755
756 sleep(Duration::from_secs_f64(1.5));
757
758 let expected = Duration::from_secs(1);
759 let result = timer.elapsed();
760 assert!(
761 result > expected,
762 "expected: timer.elapsed() > {:?} (got: {:?})",
763 expected,
764 result
765 );
766 }
767
768 #[test]
769 fn timer_update_to_dead_man() {
770 let mut config = get_test_config();
771
772 let now_wall = system_time_epoch()
773 .expect("failed to get current time")
774 .as_secs();
775
776 let _guard = TestGuard::new(&State {
778 timer_type: TimerType::Warning,
779 last_modified: now_wall,
780 });
781
782 let mut timer = Timer::new(&config).expect("failed to create new timer");
783
784 config.timer_warning = 1;
785 timer.reset(&config).expect("failed to reset timer");
787
788 let expected = TimerType::Warning;
790 let result = timer.timer_type;
791 assert!(
792 result == expected,
793 "expected: '{:?}' got: '{:?}')",
794 expected,
795 result
796 );
797
798 let expected = Duration::from_secs(config.timer_warning);
800 let result = timer.duration;
801 assert!(
802 result == expected,
803 "expected: '{:?}' got: '{:?}')",
804 expected,
805 result
806 );
807
808 timer
810 .update(Duration::from_secs(2), 3600)
811 .expect("Failed to update timer");
812
813 let expected = TimerType::DeadMan;
814 let result = timer.get_type();
815 assert!(
816 result == expected,
817 "expected: '{:?}' got: '{:?}')",
818 expected,
819 result
820 );
821
822 let expected = Duration::from_secs(3600);
823 let result = timer.duration;
824 assert!(
825 result == expected,
826 "expected: '{:?}' got: '{:?}')",
827 expected,
828 result
829 );
830
831 let expected = false;
832 let result = timer.expired();
833 assert!(
834 result == expected,
835 "expected: '{:?}' got: '{:?}')",
836 expected,
837 result
838 );
839 }
840
841 #[test]
842 fn timer_expiration() {
843 let mut config = get_test_config();
844
845 let now_wall = system_time_epoch()
846 .expect("failed to get current time")
847 .as_secs();
848
849 let _guard = TestGuard::new(&State {
851 timer_type: TimerType::Warning,
852 last_modified: now_wall,
853 });
854
855 let mut timer = Timer::new(&config).expect("failed to create new timer");
856
857 config.timer_warning = 1;
858 timer.reset(&config).expect("failed to reset timer");
860
861 let expected = TimerType::Warning;
863 let result = timer.timer_type;
864 assert!(
865 result == expected,
866 "expected: '{:?}' got: '{:?}')",
867 expected,
868 result
869 );
870
871 let expected = Duration::from_secs(config.timer_warning);
873 let result = timer.duration;
874 assert!(
875 result == expected,
876 "expected: '{:?}' got: '{:?}')",
877 expected,
878 result
879 );
880
881 timer
883 .update(Duration::from_secs(2), 1)
884 .expect("Failed to update timer");
885
886 sleep(Duration::from_secs(2));
888
889 let expected = true;
890 let result = timer.expired();
891 assert!(
892 result == expected,
893 "expected: '{:?}' got: '{:?}')",
894 expected,
895 result
896 );
897 }
898
899 #[test]
900 fn timer_remaining_percent() {
901 let mut config = get_test_config();
902
903 let now_wall = system_time_epoch()
904 .expect("failed to get current time")
905 .as_secs();
906
907 let _guard = TestGuard::new(&State {
909 timer_type: TimerType::Warning,
910 last_modified: now_wall,
911 });
912
913 let mut timer = Timer::new(&config).expect("failed to create new timer");
914
915 config.timer_warning = 2;
916 timer.reset(&config).expect("failed to reset timer");
918
919 let expected = 0;
920 let result = timer.remaining_percent();
921 assert!(
922 result > expected,
923 "expected: timer.remaining_percent() > {:?} (got: {:?})",
924 expected,
925 result
926 );
927
928 sleep(Duration::from_secs(2));
930
931 let result = timer.remaining_percent();
932 assert!(
933 result == expected,
934 "expected: timer.remaining_percent() == {:?} (got: {:?})",
935 expected,
936 result
937 );
938 }
939
940 #[test]
941 fn timer_label() {
942 let mut config = get_test_config();
943
944 let now_wall = system_time_epoch()
945 .expect("failed to get current time")
946 .as_secs();
947
948 let _guard = TestGuard::new(&State {
950 timer_type: TimerType::Warning,
951 last_modified: now_wall,
952 });
953
954 let mut timer = Timer::new(&config).expect("failed to create new timer");
955
956 config.timer_warning = 2;
957 timer.reset(&config).expect("failed to reset timer");
959
960 let expected = "0 second(s)";
961 let result = timer.label();
962 assert!(
963 result != expected,
964 "expected: timer.label() != {:?} (got: {:?})",
965 expected,
966 result
967 );
968
969 sleep(Duration::from_secs(2));
971
972 let result = timer.label();
973 assert!(
974 result == expected,
975 "expected: timer.label() == {:?} (got: {:?})",
976 expected,
977 result
978 );
979 }
980
981 #[test]
982 fn format_seconds_only() {
983 let duration = ChronoDuration::try_seconds(45).unwrap();
984 assert_eq!(format_duration(duration), "45 second(s)");
985 }
986
987 #[test]
988 fn format_minutes_and_seconds() {
989 let duration =
990 ChronoDuration::try_minutes(5).unwrap() + ChronoDuration::try_seconds(30).unwrap();
991 assert_eq!(format_duration(duration), "5 minute(s), 30 second(s)");
992 }
993
994 #[test]
995 fn format_hours_minutes_and_seconds() {
996 let duration = ChronoDuration::try_hours(2).unwrap()
997 + ChronoDuration::try_minutes(15).unwrap()
998 + ChronoDuration::try_seconds(10).unwrap();
999 assert_eq!(
1000 format_duration(duration),
1001 "2 hour(s), 15 minute(s), 10 second(s)"
1002 );
1003 }
1004
1005 #[test]
1006 fn format_days_hours_minutes() {
1007 let duration = ChronoDuration::try_days(1).unwrap()
1008 + ChronoDuration::try_hours(3).unwrap()
1009 + ChronoDuration::try_minutes(45).unwrap();
1010 assert_eq!(
1011 format_duration(duration),
1012 "1 day(s), 3 hour(s), 45 minute(s)"
1013 );
1014 }
1015
1016 #[test]
1017 fn format_days_only() {
1018 let duration = ChronoDuration::try_days(4).unwrap();
1019 assert_eq!(format_duration(duration), "4 day(s)");
1020 }
1021
1022 #[test]
1023 fn format_large_duration() {
1024 let duration = ChronoDuration::try_days(7).unwrap()
1025 + ChronoDuration::try_hours(23).unwrap()
1026 + ChronoDuration::try_minutes(59).unwrap()
1027 + ChronoDuration::try_seconds(59).unwrap();
1028 assert_eq!(
1029 format_duration(duration),
1030 "7 day(s), 23 hour(s), 59 minute(s), 59 second(s)"
1031 );
1032 }
1033
1034 #[test]
1035 fn reset_warning_timer_resets_start_time() {
1036 let config = get_test_config();
1037
1038 let now_wall = system_time_epoch()
1039 .expect("failed to get current time")
1040 .as_secs();
1041
1042 let _guard = TestGuard::new(&State {
1044 timer_type: TimerType::Warning,
1045 last_modified: now_wall - 60,
1046 });
1047
1048 let mut timer = Timer::new(&config).expect("failed to create new timer");
1049
1050 let original_start = timer.start;
1051
1052 timer.reset(&config).expect("failed to reset timer");
1053
1054 let expected = original_start;
1055 let result = timer.start;
1056 assert!(
1057 result > expected,
1058 "expected: timer.start > {:?} (got: {:?})",
1059 expected,
1060 result
1061 );
1062
1063 let expected = Duration::from_secs(config.timer_warning);
1064 let result = timer.duration;
1065 assert!(
1066 result == expected,
1067 "expected: timer.duration == {:?} (got: {:?})",
1068 expected,
1069 result
1070 );
1071
1072 let expected = TimerType::Warning;
1073 let result = timer.get_type();
1074 assert!(
1075 result == expected,
1076 "expected: timer.get_type() == {:?} (got: {:?})",
1077 expected,
1078 result
1079 );
1080 }
1081
1082 #[test]
1083 fn reset_dead_man_timer_promotes_to_warning_and_resets() {
1084 let config = get_test_config();
1085
1086 let _guard = TestGuard::new(&State {
1088 timer_type: TimerType::DeadMan,
1089 last_modified: system_time_epoch()
1090 .expect("failed to get current time")
1091 .as_secs(),
1092 });
1093
1094 let mut timer = Timer::new(&config).expect("failed to create new timer");
1095
1096 timer.reset(&config).expect("failed to reset timer");
1097
1098 let expected = Duration::from_secs(config.timer_warning);
1099 let result = timer.duration;
1100 assert!(
1101 result == expected,
1102 "expected: timer.duration == {:?} (got: {:?})",
1103 expected,
1104 result
1105 );
1106
1107 let expected = TimerType::Warning;
1108 let result = timer.get_type();
1109 assert!(
1110 result == expected,
1111 "expected: timer.get_type() == {:?} (got: {:?})",
1112 expected,
1113 result
1114 );
1115 }
1116}