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::{mpsc, OnceLock};
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!(result
494 .to_string_lossy()
495 .contains(expected.to_string_lossy().as_ref()));
496
497 cleanup_test_dir_parent(&result);
499 }
500
501 #[test]
502 fn generates_system_time_epoch() {
503 let result = system_time_epoch();
504 assert!(result.is_ok());
505
506 let expected_range_low = 1767225600; let expected_range_high = 4102444800; let result = result.unwrap().as_secs();
509 assert!(
510 result >= expected_range_low && result <= expected_range_high,
511 "expected: between {:?} (2026-01-01 00:00:00) and {:?} (2100-01-01 00:00:00) (got: {:?})",
512 expected_range_low,
513 expected_range_high,
514 result
515 );
516 }
517
518 #[test]
519 fn update_persisted_state_ok() {
520 let state = State {
522 timer_type: TimerType::Warning,
523 last_modified: 1,
524 };
525
526 let result = persist_state_blocking(state.clone());
527 assert!(result.is_ok());
528
529 let test_path = file_path().unwrap();
530 let loaded_state = load_state_from_path(&test_path);
531 assert_eq!(loaded_state, state);
533
534 cleanup_test_dir_parent(&test_path);
536 }
537
538 #[test]
539 fn state_guard_ok() {
540 let state = State {
545 timer_type: TimerType::DeadMan,
546 last_modified: 2,
547 };
548 let _guard = TestGuard::new(&state);
549
550 let test_path = file_path().unwrap();
552 let loaded_state = load_state_from_path(&test_path);
553 assert_eq!(loaded_state, state);
554 }
555
556 #[test]
557 fn load_or_initialize_with_existing_file() {
558 let config = Config::default();
559
560 let existing_state_1 = State {
562 timer_type: TimerType::Warning,
563 last_modified: 3,
564 };
565 let existing_state_2 = State {
566 timer_type: TimerType::DeadMan,
567 last_modified: 4,
568 };
569
570 let _guard_1 = TestGuard::new(&existing_state_1);
571
572 let timer = load_or_initialize(&config).unwrap();
574
575 assert_eq!(timer.timer_type, existing_state_1.timer_type);
577
578 let _guard_2 = TestGuard::new(&existing_state_2);
579
580 let timer = load_or_initialize(&config).unwrap();
582
583 assert_eq!(timer.timer_type, existing_state_2.timer_type);
585 }
586
587 #[test]
588 fn load_or_initialize_with_no_existing_file() {
589 let config = Config::default();
591
592 let timer = load_or_initialize(&config).unwrap();
594
595 let expected = TimerType::Warning;
596 let result = timer.timer_type;
597 assert!(
598 result == expected,
599 "expected: '{:?}' got: '{:?}')",
600 expected,
601 result
602 );
603
604 let expected = Duration::from_secs(config.timer_warning);
605 let result = timer.duration;
606 assert!(
607 result == expected,
608 "expected: '{:?}' got: '{:?}')",
609 expected,
610 result
611 );
612
613 let test_path = file_path().unwrap();
615 cleanup_test_dir_parent(&test_path);
616 }
617
618 #[test]
619 fn timer_remaining_chrono_with_state_in_past() {
620 let default_config = config::load_or_initialize().expect("failed to load default config");
621
622 let now_wall = system_time_epoch()
623 .expect("failed to get current time")
624 .as_secs();
625
626 let _guard = TestGuard::new(&State {
628 timer_type: TimerType::Warning,
629 last_modified: now_wall - 10000,
630 });
631
632 let timer = Timer::new(&default_config).expect("failed to create new timer");
633
634 let tolerance_secs = 5;
635 let expected_range_high =
636 chrono::TimeDelta::new((default_config.timer_warning - 10000) as i64, 0)
637 .expect("failed creating time delta");
638 let expected_range_low = expected_range_high
639 .sub(chrono::TimeDelta::new(tolerance_secs, 0).expect("failed creating time delta"));
640 let result = timer.remaining_chrono();
641 assert!(
643 result >= expected_range_low && result <= expected_range_high,
644 "expected: timer.remaining_chrono() between {:?} and {:?} (got: {:?})",
645 expected_range_low,
646 expected_range_high,
647 result
648 );
649 }
650
651 #[test]
652 fn timer_remaining_chrono_with_state_in_future() {
653 let default_config = config::load_or_initialize().expect("failed to load default config");
654
655 let now_wall = system_time_epoch()
656 .expect("failed to get current time")
657 .as_secs();
658
659 let _guard = TestGuard::new(&State {
662 timer_type: TimerType::Warning,
663 last_modified: now_wall + 1000,
664 });
665
666 let timer = Timer::new(&default_config).expect("failed to create new timer");
667
668 let tolerance_secs = 5;
669 let expected_range_high = chrono::TimeDelta::new((default_config.timer_warning) as i64, 0)
670 .expect("failed creating time delta");
671 let expected_range_low = expected_range_high
672 .sub(chrono::TimeDelta::new(tolerance_secs, 0).expect("failed creating time delta"));
673 let result = timer.remaining_chrono();
675 assert!(
676 result >= expected_range_low && result <= expected_range_high,
677 "expected: timer.remaining_chrono() between {:?} and {:?} (got: {:?})",
678 expected_range_low,
679 expected_range_high,
680 result
681 );
682 }
683
684 #[test]
685 fn timer_remaining_chrono_with_state() {
686 let mut config = get_test_config();
687
688 let now_wall = system_time_epoch()
689 .expect("failed to get current time")
690 .as_secs();
691
692 let _guard = TestGuard::new(&State {
694 timer_type: TimerType::Warning,
695 last_modified: now_wall,
696 });
697
698 let mut timer = Timer::new(&config).expect("failed to create new timer");
699
700 config.timer_warning = 2;
701 timer.reset(&config).expect("failed to reset timer");
703
704 let expected = chrono::TimeDelta::new(2, 0).expect("failed creating time delta");
705 let result = timer.remaining_chrono();
706 assert!(
707 result <= expected,
708 "expected: timer.remaining_chrono() < {:?} (got: {:?})",
709 expected,
710 result
711 );
712
713 sleep(Duration::from_secs(2));
714
715 let expected = chrono::TimeDelta::new(1, 0).expect("failed creating time delta");
716 let result = timer.remaining_chrono();
717 assert!(
718 result <= expected,
719 "expected: timer.remaining_chrono() < {:?} (got: {:?})",
720 expected,
721 result
722 );
723 }
724
725 #[test]
726 fn timer_elapsed_less_than_duration() {
727 let mut config = get_test_config();
728
729 let now_wall = system_time_epoch()
730 .expect("failed to get current time")
731 .as_secs();
732
733 let _guard = TestGuard::new(&State {
735 timer_type: TimerType::Warning,
736 last_modified: now_wall,
737 });
738
739 let mut timer = Timer::new(&config).expect("failed to create new timer");
740
741 config.timer_warning = 60;
742 timer.reset(&config).expect("failed to reset timer");
744
745 let expected = Duration::from_secs(2);
746 let result = timer.elapsed();
747 assert!(
748 result < expected,
749 "expected: timer.elapsed() < {:?} (got: {:?})",
750 expected,
751 result
752 );
753
754 sleep(Duration::from_secs_f64(1.5));
755
756 let expected = Duration::from_secs(1);
757 let result = timer.elapsed();
758 assert!(
759 result > expected,
760 "expected: timer.elapsed() > {:?} (got: {:?})",
761 expected,
762 result
763 );
764 }
765
766 #[test]
767 fn timer_update_to_dead_man() {
768 let mut config = get_test_config();
769
770 let now_wall = system_time_epoch()
771 .expect("failed to get current time")
772 .as_secs();
773
774 let _guard = TestGuard::new(&State {
776 timer_type: TimerType::Warning,
777 last_modified: now_wall,
778 });
779
780 let mut timer = Timer::new(&config).expect("failed to create new timer");
781
782 config.timer_warning = 1;
783 timer.reset(&config).expect("failed to reset timer");
785
786 let expected = TimerType::Warning;
788 let result = timer.timer_type;
789 assert!(
790 result == expected,
791 "expected: '{:?}' got: '{:?}')",
792 expected,
793 result
794 );
795
796 let expected = Duration::from_secs(config.timer_warning);
798 let result = timer.duration;
799 assert!(
800 result == expected,
801 "expected: '{:?}' got: '{:?}')",
802 expected,
803 result
804 );
805
806 timer
808 .update(Duration::from_secs(2), 3600)
809 .expect("Failed to update timer");
810
811 let expected = TimerType::DeadMan;
812 let result = timer.get_type();
813 assert!(
814 result == expected,
815 "expected: '{:?}' got: '{:?}')",
816 expected,
817 result
818 );
819
820 let expected = Duration::from_secs(3600);
821 let result = timer.duration;
822 assert!(
823 result == expected,
824 "expected: '{:?}' got: '{:?}')",
825 expected,
826 result
827 );
828
829 let expected = false;
830 let result = timer.expired();
831 assert!(
832 result == expected,
833 "expected: '{:?}' got: '{:?}')",
834 expected,
835 result
836 );
837 }
838
839 #[test]
840 fn timer_expiration() {
841 let mut config = get_test_config();
842
843 let now_wall = system_time_epoch()
844 .expect("failed to get current time")
845 .as_secs();
846
847 let _guard = TestGuard::new(&State {
849 timer_type: TimerType::Warning,
850 last_modified: now_wall,
851 });
852
853 let mut timer = Timer::new(&config).expect("failed to create new timer");
854
855 config.timer_warning = 1;
856 timer.reset(&config).expect("failed to reset timer");
858
859 let expected = TimerType::Warning;
861 let result = timer.timer_type;
862 assert!(
863 result == expected,
864 "expected: '{:?}' got: '{:?}')",
865 expected,
866 result
867 );
868
869 let expected = Duration::from_secs(config.timer_warning);
871 let result = timer.duration;
872 assert!(
873 result == expected,
874 "expected: '{:?}' got: '{:?}')",
875 expected,
876 result
877 );
878
879 timer
881 .update(Duration::from_secs(2), 1)
882 .expect("Failed to update timer");
883
884 sleep(Duration::from_secs(2));
886
887 let expected = true;
888 let result = timer.expired();
889 assert!(
890 result == expected,
891 "expected: '{:?}' got: '{:?}')",
892 expected,
893 result
894 );
895 }
896
897 #[test]
898 fn timer_remaining_percent() {
899 let mut config = get_test_config();
900
901 let now_wall = system_time_epoch()
902 .expect("failed to get current time")
903 .as_secs();
904
905 let _guard = TestGuard::new(&State {
907 timer_type: TimerType::Warning,
908 last_modified: now_wall,
909 });
910
911 let mut timer = Timer::new(&config).expect("failed to create new timer");
912
913 config.timer_warning = 2;
914 timer.reset(&config).expect("failed to reset timer");
916
917 let expected = 0;
918 let result = timer.remaining_percent();
919 assert!(
920 result > expected,
921 "expected: timer.remaining_percent() > {:?} (got: {:?})",
922 expected,
923 result
924 );
925
926 sleep(Duration::from_secs(2));
928
929 let result = timer.remaining_percent();
930 assert!(
931 result == expected,
932 "expected: timer.remaining_percent() == {:?} (got: {:?})",
933 expected,
934 result
935 );
936 }
937
938 #[test]
939 fn timer_label() {
940 let mut config = get_test_config();
941
942 let now_wall = system_time_epoch()
943 .expect("failed to get current time")
944 .as_secs();
945
946 let _guard = TestGuard::new(&State {
948 timer_type: TimerType::Warning,
949 last_modified: now_wall,
950 });
951
952 let mut timer = Timer::new(&config).expect("failed to create new timer");
953
954 config.timer_warning = 2;
955 timer.reset(&config).expect("failed to reset timer");
957
958 let expected = "0 second(s)";
959 let result = timer.label();
960 assert!(
961 result != expected,
962 "expected: timer.label() != {:?} (got: {:?})",
963 expected,
964 result
965 );
966
967 sleep(Duration::from_secs(2));
969
970 let result = timer.label();
971 assert!(
972 result == expected,
973 "expected: timer.label() == {:?} (got: {:?})",
974 expected,
975 result
976 );
977 }
978
979 #[test]
980 fn format_seconds_only() {
981 let duration = ChronoDuration::try_seconds(45).unwrap();
982 assert_eq!(format_duration(duration), "45 second(s)");
983 }
984
985 #[test]
986 fn format_minutes_and_seconds() {
987 let duration =
988 ChronoDuration::try_minutes(5).unwrap() + ChronoDuration::try_seconds(30).unwrap();
989 assert_eq!(format_duration(duration), "5 minute(s), 30 second(s)");
990 }
991
992 #[test]
993 fn format_hours_minutes_and_seconds() {
994 let duration = ChronoDuration::try_hours(2).unwrap()
995 + ChronoDuration::try_minutes(15).unwrap()
996 + ChronoDuration::try_seconds(10).unwrap();
997 assert_eq!(
998 format_duration(duration),
999 "2 hour(s), 15 minute(s), 10 second(s)"
1000 );
1001 }
1002
1003 #[test]
1004 fn format_days_hours_minutes() {
1005 let duration = ChronoDuration::try_days(1).unwrap()
1006 + ChronoDuration::try_hours(3).unwrap()
1007 + ChronoDuration::try_minutes(45).unwrap();
1008 assert_eq!(
1009 format_duration(duration),
1010 "1 day(s), 3 hour(s), 45 minute(s)"
1011 );
1012 }
1013
1014 #[test]
1015 fn format_days_only() {
1016 let duration = ChronoDuration::try_days(4).unwrap();
1017 assert_eq!(format_duration(duration), "4 day(s)");
1018 }
1019
1020 #[test]
1021 fn format_large_duration() {
1022 let duration = ChronoDuration::try_days(7).unwrap()
1023 + ChronoDuration::try_hours(23).unwrap()
1024 + ChronoDuration::try_minutes(59).unwrap()
1025 + ChronoDuration::try_seconds(59).unwrap();
1026 assert_eq!(
1027 format_duration(duration),
1028 "7 day(s), 23 hour(s), 59 minute(s), 59 second(s)"
1029 );
1030 }
1031
1032 #[test]
1033 fn reset_warning_timer_resets_start_time() {
1034 let config = get_test_config();
1035
1036 let now_wall = system_time_epoch()
1037 .expect("failed to get current time")
1038 .as_secs();
1039
1040 let _guard = TestGuard::new(&State {
1042 timer_type: TimerType::Warning,
1043 last_modified: now_wall - 60,
1044 });
1045
1046 let mut timer = Timer::new(&config).expect("failed to create new timer");
1047
1048 let original_start = timer.start;
1049
1050 timer.reset(&config).expect("failed to reset timer");
1051
1052 let expected = original_start;
1053 let result = timer.start;
1054 assert!(
1055 result > expected,
1056 "expected: timer.start > {:?} (got: {:?})",
1057 expected,
1058 result
1059 );
1060
1061 let expected = Duration::from_secs(config.timer_warning);
1062 let result = timer.duration;
1063 assert!(
1064 result == expected,
1065 "expected: timer.duration == {:?} (got: {:?})",
1066 expected,
1067 result
1068 );
1069
1070 let expected = TimerType::Warning;
1071 let result = timer.get_type();
1072 assert!(
1073 result == expected,
1074 "expected: timer.get_type() == {:?} (got: {:?})",
1075 expected,
1076 result
1077 );
1078 }
1079
1080 #[test]
1081 fn reset_dead_man_timer_promotes_to_warning_and_resets() {
1082 let config = get_test_config();
1083
1084 let _guard = TestGuard::new(&State {
1086 timer_type: TimerType::DeadMan,
1087 last_modified: system_time_epoch()
1088 .expect("failed to get current time")
1089 .as_secs(),
1090 });
1091
1092 let mut timer = Timer::new(&config).expect("failed to create new timer");
1093
1094 timer.reset(&config).expect("failed to reset timer");
1095
1096 let expected = Duration::from_secs(config.timer_warning);
1097 let result = timer.duration;
1098 assert!(
1099 result == expected,
1100 "expected: timer.duration == {:?} (got: {:?})",
1101 expected,
1102 result
1103 );
1104
1105 let expected = TimerType::Warning;
1106 let result = timer.get_type();
1107 assert!(
1108 result == expected,
1109 "expected: timer.get_type() == {:?} (got: {:?})",
1110 expected,
1111 result
1112 );
1113 }
1114}