a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(test): direct test for break timing, skip, and long-break

+61 -31
+61 -31
src/timer/scheduler.rs
··· 1 1 use std::time::{Duration, Instant}; 2 2 3 - use super::profile::{BreakLevel, Profile}; 3 + use super::profile::Profile; 4 4 5 5 #[derive(Debug, Clone)] 6 6 pub struct ScheduledBreak { ··· 11 11 pub level_index: usize, 12 12 } 13 13 14 - /// Tracks elapsed work time and cycle counts to determine the next break. 15 - /// 16 - /// Each break level has its own independent elapsed counter so that a 17 - /// short micro-break firing does not reset progress toward longer levels. 18 - /// For example, with levels [1 min micro, 3 min regular]: 19 - /// - level_elapsed[0] resets every time the micro fires 20 - /// - level_elapsed[1] keeps accumulating and fires independently at 3 min 14 + // Tracks elapsed work time and cycle counts to determine the next break. 15 + // 16 + // Each break level has its own independent elapsed counter so that a 17 + // short micro-break firing does not reset progress toward longer levels. 18 + // For example, with levels [1 min micro, 3 min regular]: 19 + // - level_elapsed[0] resets every time the micro fires 20 + // - level_elapsed[1] keeps accumulating and fires independently at 3 min 21 21 pub struct Scheduler { 22 22 profile: Profile, 23 - /// Per-level elapsed work time. Index matches profile.levels. 23 + // Per-level elapsed work time. Index matches profile.levels. 24 24 level_elapsed: Vec<Duration>, 25 - /// How many primary-level (longest interval) cycles have completed. 25 + // How many primary-level (longest interval) cycles have completed. 26 26 primary_cycle_count: u32, 27 - /// When the last break ended (for cycle gap detection). 27 + // When the last break ended (for cycle gap detection). 28 28 last_break_ended: Option<Instant>, 29 29 } 30 30 ··· 50 50 self.primary_cycle_count = 0; 51 51 } 52 52 53 - /// Advance all level timers by `delta`. Returns a break to fire if one is due. 53 + // Advance all level timers by `delta`. Returns a break to fire if one is due. 54 54 pub fn tick(&mut self, delta: Duration) -> Option<ScheduledBreak> { 55 55 for elapsed in &mut self.level_elapsed { 56 56 *elapsed += delta; ··· 58 58 self.due_break() 59 59 } 60 60 61 - /// Force the next scheduled break to fire immediately (tray "skip to next break"). 61 + // Force the next scheduled break to fire immediately (tray "skip to next break"). 62 62 pub fn skip_to_next_break(&mut self) -> Option<ScheduledBreak> { 63 63 if let Some(idx) = self.next_break_level_index() { 64 64 self.level_elapsed[idx] = self.profile.levels[idx].work_duration; ··· 71 71 self.due_break() 72 72 } 73 73 74 - /// Called when a break finishes. Resets only the fired level's counter; 75 - /// all other levels keep their accumulated time. 74 + // Called when a break finishes. Resets only the fired level's counter; 75 + // all other levels keep their accumulated time. 76 76 pub fn record_break_completed(&mut self, fired_index: usize, was_long: bool) { 77 77 let now = Instant::now(); 78 78 ··· 89 89 let primary_idx = self.profile.levels.len().saturating_sub(1); 90 90 91 91 if fired_index == primary_idx { 92 - let gap_ok = self.last_break_ended.map_or(true, |t| { 92 + let gap_ok = self.last_break_ended.is_none_or(|t| { 93 93 self.profile 94 94 .long_break 95 95 .as_ref() 96 - .map_or(true, |lb| now.duration_since(t) <= lb.max_cycle_gap) 96 + .is_none_or(|lb| now.duration_since(t) <= lb.max_cycle_gap) 97 97 }); 98 98 if gap_ok { 99 99 self.primary_cycle_count += 1; ··· 126 126 ); 127 127 } 128 128 129 - /// Called when idle is detected (natural break). Optionally resets cycle gap. 129 + // Called when idle is detected (natural break). Optionally resets cycle gap. 130 130 pub fn record_idle_break(&mut self, idle_duration: Duration) { 131 - if let Some(lb) = &self.profile.long_break { 132 - if idle_duration >= lb.max_cycle_gap { 133 - self.primary_cycle_count = 0; 134 - } 131 + if let Some(lb) = &self.profile.long_break 132 + && idle_duration >= lb.max_cycle_gap 133 + { 134 + self.primary_cycle_count = 0; 135 135 } 136 136 for e in &mut self.level_elapsed { 137 137 *e = Duration::ZERO; ··· 139 139 self.last_break_ended = Some(Instant::now()); 140 140 } 141 141 142 - /// Seconds until the next break fires (for tray label). 142 + // Seconds until the next break fires (for tray label). 143 143 pub fn secs_until_next_break(&self) -> u64 { 144 - if let Some(lb) = &self.profile.long_break { 145 - if self.primary_cycle_count >= lb.after_cycles { 146 - let primary_idx = self.profile.levels.len() - 1; 147 - let primary_dur = self.profile.primary_level().work_duration; 148 - return primary_dur 149 - .saturating_sub(self.level_elapsed[primary_idx]) 150 - .as_secs(); 151 - } 144 + if let Some(lb) = &self.profile.long_break 145 + && self.primary_cycle_count >= lb.after_cycles 146 + { 147 + let primary_idx = self.profile.levels.len() - 1; 148 + let primary_dur = self.profile.primary_level().work_duration; 149 + return primary_dur 150 + .saturating_sub(self.level_elapsed[primary_idx]) 151 + .as_secs(); 152 152 } 153 153 if let Some(idx) = self.next_break_level_index() { 154 154 let due = self.profile.levels[idx].work_duration; ··· 349 349 assert_eq!(s.primary_cycle_count(), 3); 350 350 let b = s.tick(Duration::from_secs(180)).unwrap(); 351 351 assert!(b.is_long_break); 352 + assert_eq!(b.break_duration, Duration::from_secs(600)); 353 + } 354 + 355 + #[test] 356 + fn skip_to_next_break_targets_soonest_due_level() { 357 + let mut s = Scheduler::new(make_profile(vec![(60, 15), (180, 60)], None)); 358 + 359 + s.tick(Duration::from_secs(50)); 360 + let b = s.skip_to_next_break().expect("skip should fire a break"); 361 + 362 + assert_eq!(b.level_index, 0); 363 + assert_eq!(b.break_duration, Duration::from_secs(15)); 364 + } 365 + 366 + #[test] 367 + fn skip_to_next_break_can_force_pending_long_break() { 368 + let mut s = Scheduler::new(make_profile(vec![(60, 15), (180, 60)], Some((2, 600, 600)))); 369 + 370 + for _ in 0..2 { 371 + tick_and_complete(&mut s, 60); 372 + tick_and_complete(&mut s, 60); 373 + tick_and_complete(&mut s, 60); 374 + } 375 + 376 + let b = s 377 + .skip_to_next_break() 378 + .expect("skip should fire the pending long break"); 379 + 380 + assert!(b.is_long_break); 381 + assert_eq!(b.level_index, 1); 352 382 assert_eq!(b.break_duration, Duration::from_secs(600)); 353 383 } 354 384 }