From 07f35978d7264445cb9c32a6f6ca29ffd1afe446 Mon Sep 17 00:00:00 2001 From: Ben Lovy Date: Thu, 19 Sep 2024 00:38:04 -0400 Subject: [PATCH] typecheck --- Cargo.toml | 2 +- README.md | 8 +- examples/basic.rs | 8 +- examples/readme.rs | 8 +- src/error.rs | 19 ++--- src/job.rs | 204 +++++++++++++++++++++------------------------ src/lib.rs | 12 +-- src/scheduler.rs | 165 ++++++++++++++++++------------------ src/time.rs | 92 ++++++++++---------- 9 files changed, 246 insertions(+), 272 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 01fcb7d..17f9a19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ default = [] ffi = [] [dependencies] -chrono = "0.4" +jiff = "0.1" libc = "0.2" rand = "0.8" regex = "1.5" diff --git a/README.md b/README.md index 3e3165f..b5aaefb 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ Documentation can be found on [docs.rs](https://docs.rs/skedge). This library uses the Builder pattern to define jobs. Instantiate a fresh `Scheduler`, then use the `every()` and `every_single()` functions to begin defining a job. Finalize configuration by calling `Job::run()` to add the new job to the scheduler. The `Scheduler::run_pending()` method is used to fire any jobs that have arrived at their next scheduled run time. Currently, precision can only be specified to the second, no smaller. ```rust -use chrono::Local; +use jiff::Zoned; use skedge::{every, Scheduler}; use std::thread::sleep; use std::time::Duration; fn greet(name: &str) { - let now = Local::now().to_rfc2822(); + let now = Zoned::now(); println!("Hello {name}, it's {now}!"); } @@ -29,10 +29,10 @@ fn main() -> Result<(), Box> { every(2) .to(8)? .seconds()? - .until(Local::now() + chrono::Duration::seconds(30))? + .until(Zoned::now() + Duration::from_secs(30))? .run_one_arg(&mut schedule, greet, "Cool Person")?; - let now = Local::now(); + let now = Zoned::now(); println!("Starting at {now}"); loop { if let Err(e) = schedule.run_pending() { diff --git a/examples/basic.rs b/examples/basic.rs index 5fd842e..38ac08b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,12 +1,12 @@ // Some more varied usage examples. -use chrono::Local; +use jiff::{ToSpan, Zoned}; use skedge::{every, every_single, Scheduler}; use std::thread::sleep; use std::time::Duration; fn job() { - let now = Local::now().to_rfc2822(); + let now = Zoned::now(); println!("Hello, it's {now}!"); } @@ -41,7 +41,7 @@ fn main() -> Result<(), Box> { every(2) .to(8)? .seconds()? - .until(Local::now() + chrono::Duration::days(5))? + .until(Zoned::now().checked_add(5.seconds()).unwrap())? .run_six_args( &mut schedule, flirt, @@ -53,7 +53,7 @@ fn main() -> Result<(), Box> { "foraged chanterelle croque monsieur", )?; - let now = Local::now().to_rfc3339(); + let now = Zoned::now(); println!("Starting at {now}"); loop { if let Err(e) = schedule.run_pending() { diff --git a/examples/readme.rs b/examples/readme.rs index 38756f2..2fdf4f3 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -1,12 +1,12 @@ // This is the exact code from the README.md example -use chrono::Local; +use jiff::{ToSpan, Zoned}; use skedge::{every, Scheduler}; use std::thread::sleep; use std::time::Duration; fn greet(name: &str) { - let now = Local::now().to_rfc2822(); + let now = Zoned::now(); println!("Hello {name}, it's {now}!"); } @@ -16,10 +16,10 @@ fn main() -> Result<(), Box> { every(2) .to(8)? .seconds()? - .until(Local::now() + chrono::Duration::seconds(30))? + .until(Zoned::now().checked_add(30.seconds()).unwrap())? .run_one_arg(&mut schedule, greet, "Cool Person")?; - let now = Local::now(); + let now = Zoned::now(); println!("Starting at {now}"); loop { if let Err(e) = schedule.run_pending() { diff --git a/src/error.rs b/src/error.rs index b9699b8..a9004d4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ //! This module defines the error type and Result alias. use crate::Unit; -use chrono::Weekday; +use jiff::civil::Weekday; use thiserror::Error; #[derive(Debug, PartialEq, Error)] @@ -17,15 +17,15 @@ pub enum Error { #[error("Invalid unit (valid units are `days`, `hours`, and `minutes`)")] InvalidUnit, #[error("Invalid hour ({0} is not between 0 and 23)")] - InvalidHour(u32), + InvalidHour(i8), #[error("Invalid time format for daily job (valid format is HH:MM(:SS)?)")] InvalidDailyAtStr, #[error("Invalid time format for hourly job (valid format is (MM)?:SS)")] InvalidHourlyAtStr, #[error("Invalid time format for minutely job (valid format is :SS)")] InvalidMinuteAtStr, - #[error("Invalid hms values for NaiveTime ({0},{1},{2})")] - InvalidNaiveTime(u32, u32, u32), + #[error("Invalid hms values for civil::Time ({0},{1},{2})")] + InvalidCivilTime(i8, i8, i8), #[error("Invalid string format for until()")] InvalidUntilStr, #[error("Cannot schedule a job to run until a time in the past")] @@ -42,9 +42,9 @@ pub enum Error { StartDayError, #[error("{0}")] ParseInt(#[from] std::num::ParseIntError), - #[error("Scheduling jobs on {0} is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported")] + #[error("Scheduling jobs on {0:?} is only allowed for weekly jobs. Using specific days on a job scheduled to run every 2 or more weeks is not supported")] Weekday(Weekday), - #[error("Cannot schedule {0} job, already scheduled for {1}")] + #[error("Cannot schedule {0:?} job, already scheduled for {1:?}")] WeekdayCollision(Weekday, Weekday), #[error("Invalid unit without specifying start day")] UnspecifiedStartDay, @@ -56,15 +56,10 @@ pub(crate) fn unit_error(intended: Unit, existing: Unit) -> Error { } /// Construct a new invalid hour error. -pub(crate) fn invalid_hour_error(hour: u32) -> Error { +pub(crate) fn invalid_hour_error(hour: i8) -> Error { Error::InvalidHour(hour) } -/// Concstruct a new invalid HMS error. -pub(crate) fn invalid_hms_error(hour: u32, minute: u32, second: u32) -> Error { - Error::InvalidNaiveTime(hour, minute, second) -} - /// Construct a new Interval error. pub(crate) fn interval_error(interval: Unit) -> Error { Error::Interval(interval) diff --git a/src/job.rs b/src/job.rs index 8830a3a..8b2c107 100644 --- a/src/job.rs +++ b/src/job.rs @@ -1,6 +1,6 @@ //! A Job is a piece of work that can be configured and added to the scheduler -use chrono::{prelude::*, Datelike, Duration, Timelike}; +use jiff::{civil, Span, Zoned}; use rand::prelude::*; use regex::Regex; use std::{ @@ -14,9 +14,9 @@ use tracing::debug; #[cfg(feature = "ffi")] use crate::callable::ffi::ExternUnitToUnit; use crate::{ - interval_error, invalid_hms_error, invalid_hour_error, unit_error, weekday_collision_error, - weekday_error, Callable, Error, FiveToUnit, FourToUnit, OneToUnit, Real, Result, Scheduler, - SixToUnit, ThreeToUnit, Timekeeper, Timestamp, TwoToUnit, Unit, UnitToUnit, + interval_error, invalid_hour_error, unit_error, weekday_collision_error, weekday_error, + Callable, Error, FiveToUnit, FourToUnit, OneToUnit, Result, Scheduler, SixToUnit, ThreeToUnit, + Timekeeper, TwoToUnit, Unit, UnitToUnit, }; /// A Tag is used to categorize a job. @@ -66,19 +66,17 @@ pub struct Job { /// Unit of time described by intervals unit: Option, /// Optional set time at which this job runs - at_time: Option, + at_time: Option, /// Timestamp of last run - last_run: Option, + last_run: Option, /// Timestamp of next run - pub(crate) next_run: Option, + pub(crate) next_run: Option, /// Time delta between runs - period: Option, + period: Option, /// Specific day of the week to start on - start_day: Option, + start_day: Option, /// Optional time of final run - pub(crate) cancel_after: Option, - /// Interface to current time - clock: Option>, + pub(crate) cancel_after: Option, // Track number of times run, for testing #[cfg(test)] pub(crate) call_count: u64, @@ -99,50 +97,11 @@ impl Job { period: None, start_day: None, cancel_after: None, - clock: Some(Box::::default()), #[cfg(test)] call_count: 0, } } - #[cfg(test)] - /// Build a job with a fake timer - #[must_use] - pub fn with_mock_time(interval: Interval, clock: crate::time::mock::Mock) -> Self { - Self { - interval, - latest: None, - job: None, - tags: HashSet::new(), - unit: None, - at_time: None, - last_run: None, - next_run: None, - period: None, - start_day: None, - cancel_after: None, - clock: Some(Box::new(clock)), - #[cfg(test)] - call_count: 0, - } - } - - #[cfg(test)] - /// Add a duration to the clock - /// - /// # Panics - /// - /// Panics if job has no associated clock. - pub fn add_duration(&mut self, duration: Duration) { - self.clock.as_mut().unwrap().add_duration(duration); - } - - /// Helper function to get the current time - fn now(&self) -> Timestamp { - // unwrap is safe, there will always be one - self.clock.as_ref().unwrap().now() - } - /// Tag the job with one or more unique identifiers pub fn tag(&mut self, tags: &[&str]) { for &t in tags { @@ -186,6 +145,7 @@ impl Job { /// /// Returns an error if passed an invalid or nonsensical date string. pub fn at(mut self, time_str: &str) -> Result { + // FIXME - can this whole fun just use jiff? use Unit::{Day, Hour, Minute, Week, Year}; // Validate time unit @@ -243,10 +203,7 @@ impl Job { } // Store timestamp and return - self.at_time = Some( - NaiveTime::from_hms_opt(hour, minute, second) - .ok_or(invalid_hms_error(hour, minute, second))?, - ); + self.at_time = Some(civil::time(hour, minute, second, 0)); Ok(self) } @@ -297,9 +254,11 @@ impl Job { /// # Errors /// /// Returns an error if the `until_time` is before the current time. - pub fn until(mut self, until_time: Timestamp) -> Result { - if until_time < self.now() { - return Err(Error::InvalidUntilTime); + pub fn until(mut self, until_time: Zoned) -> Result { + if let Some(ref last_run) = self.last_run { + if until_time < *last_run { + return Err(Error::InvalidUntilTime); + } } self.cancel_after = Some(until_time); Ok(self) @@ -323,9 +282,10 @@ impl Job { /// # Errors /// /// Returns an error if unable to schedule the run. + // FIXME this also goes on scheduler? pub fn run(mut self, scheduler: &mut Scheduler, job: fn() -> ()) -> Result<()> { self.job = Some(Box::new(UnitToUnit::new("job", job))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -340,7 +300,7 @@ impl Job { job: extern "C" fn() -> (), ) -> Result<()> { self.job = Some(Box::new(ExternUnitToUnit::new("job", job))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -375,7 +335,7 @@ impl Job { T: 'static + Clone, { self.job = Some(Box::new(OneToUnit::new("job_one_arg", job, arg))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -433,7 +393,7 @@ impl Job { arg_one, arg_two, ))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -479,7 +439,7 @@ impl Job { arg_two, arg_three, ))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -530,7 +490,7 @@ impl Job { arg_three, arg_four, ))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -586,7 +546,7 @@ impl Job { arg_four, arg_five, ))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } @@ -654,14 +614,15 @@ impl Job { arg_five, arg_six, ))); - self.schedule_next_run()?; + self.schedule_next_run(&scheduler.now())?; scheduler.add_job(self); Ok(()) } /// Check whether this job should be run now - pub(crate) fn should_run(&self) -> bool { - self.next_run.is_some() && self.now() >= self.next_run.unwrap() + // FIXME I think this belongs on Scheduler + pub(crate) fn should_run(&self, now: &Zoned) -> bool { + self.next_run.is_some() && now >= self.next_run.as_ref().unwrap() } /// Run this job and immediately reschedule it, returning true. If job should cancel, return false. @@ -674,8 +635,9 @@ impl Job { /// /// Returns an error if unable to schedule the run. // FIXME: if we support return values from job fns, this fn should return that. - pub fn execute(&mut self) -> Result { - if self.is_overdue(self.now()) { + // FIXME: I think this also belongs on scheduler + pub fn execute(&mut self, now: &Zoned) -> Result { + if self.is_overdue(now) { debug!("Deadline already reached, cancelling job {self}"); return Ok(false); } @@ -691,10 +653,10 @@ impl Job { { self.call_count += 1; } - self.last_run = Some(self.now()); - self.schedule_next_run()?; + self.last_run = Some(now.clone()); + self.schedule_next_run(now)?; - if self.is_overdue(self.now()) { + if self.is_overdue(now) { debug!("Execution went over deadline, cancelling job {self}",); return Ok(false); } @@ -837,7 +799,7 @@ impl Job { /// # Errors /// /// Returns an error if this assignment is incompatible with the current configuration. - fn set_weekday_mode(mut self, weekday: Weekday) -> Result { + fn set_weekday_mode(mut self, weekday: civil::Weekday) -> Result { if self.interval != 1 { Err(weekday_error(weekday)) } else if let Some(w) = self.start_day { @@ -853,7 +815,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn monday(self) -> Result { - self.set_weekday_mode(Weekday::Mon) + self.set_weekday_mode(civil::Weekday::Monday) } /// Set weekly mode on Tuesday @@ -861,7 +823,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn tuesday(self) -> Result { - self.set_weekday_mode(Weekday::Tue) + self.set_weekday_mode(civil::Weekday::Tuesday) } /// Set weekly mode on Wednesday @@ -869,7 +831,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn wednesday(self) -> Result { - self.set_weekday_mode(Weekday::Wed) + self.set_weekday_mode(civil::Weekday::Wednesday) } /// Set weekly mode on Thursday @@ -877,7 +839,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn thursday(self) -> Result { - self.set_weekday_mode(Weekday::Thu) + self.set_weekday_mode(civil::Weekday::Thursday) } /// Set weekly mode on Friday @@ -885,7 +847,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn friday(self) -> Result { - self.set_weekday_mode(Weekday::Fri) + self.set_weekday_mode(civil::Weekday::Friday) } /// Set weekly mode on Saturday @@ -893,7 +855,7 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn saturday(self) -> Result { - self.set_weekday_mode(Weekday::Sat) + self.set_weekday_mode(civil::Weekday::Saturday) } /// Set weekly mode on Sunday @@ -901,11 +863,11 @@ impl Job { /// /// Returns an error if this assignment is incompatible with the current configuration. pub fn sunday(self) -> Result { - self.set_weekday_mode(Weekday::Sun) + self.set_weekday_mode(civil::Weekday::Sunday) } /// Compute the timestamp for the next run - fn schedule_next_run(&mut self) -> Result<()> { + fn schedule_next_run(&mut self, now: &Zoned) -> Result<()> { // If "latest" is set, find the actual interval for this run, otherwise just used stored val let interval = match self.latest { Some(v) => { @@ -920,7 +882,7 @@ impl Job { // Calculate period (Duration) let period = self.unit()?.duration(interval); self.period = Some(period); - self.next_run = Some(self.now() + period); + self.next_run = Some(now + period); // Handle start day for weekly jobs if let Some(w) = self.start_day { @@ -929,14 +891,15 @@ impl Job { return Err(Error::StartDayError); } - let weekday_num = w.num_days_from_monday(); + let weekday_num = w.to_monday_zero_offset(); let mut days_ahead = i64::from(weekday_num) - i64::from( self.next_run + .as_ref() .ok_or(Error::NextRunUnreachable)? - .date_naive() + .date() .weekday() - .num_days_from_monday(), + .to_monday_zero_offset(), ); // Check if the weekday already happened this week, advance a week if so @@ -945,8 +908,11 @@ impl Job { } self.next_run = Some( - self.next_run()? + Unit::Day.duration(u32::try_from(days_ahead).unwrap()) - - self.period()?, + self.next_run()? + .checked_add(Unit::Day.duration(u32::try_from(days_ahead).unwrap())) + .unwrap() + .checked_sub(&self.period()?) + .unwrap(), ); } @@ -974,32 +940,45 @@ impl Job { } else { next_run.minute() }; - let naive_time = NaiveTime::from_hms_opt(hour, minute, second) - .ok_or(invalid_hms_error(hour, minute, second))?; - let naive_date = next_run.date_naive(); - let local_datetime = Local - .from_local_datetime(&naive_date.and_time(naive_time)) + let naive_time = civil::time(hour, minute, second, 0); + let naive_date = next_run.date(); + let tz = next_run.time_zone(); + let local_datetime = civil::DateTime::from_parts(naive_date, naive_time) + .to_zoned(tz.clone()) .unwrap(); self.next_run = Some(local_datetime); // Make sure job gets run TODAY or THIS HOUR // Accounting for jobs take long enough that they finish in the next period - if self.last_run.is_none() || (self.next_run()? - self.last_run()?) > self.period()? { - let now = self.now(); + if self.last_run.is_none() + || self + .next_run()? + .since(&self.last_run()?) + .unwrap() + .compare(self.period()?) + .unwrap() == std::cmp::Ordering::Greater + { if self.unit == Some(Day) && self.at_time.unwrap() > now.time() && self.interval == 1 { - self.next_run = Some(self.next_run.unwrap() - Day.duration(1)); + // FIXME all of this should be jiffier + self.next_run = Some( + self.next_run + .as_ref() + .unwrap() + .checked_sub(Day.duration(1)) + .unwrap(), + ); } else if self.unit == Some(Hour) && (self.at_time.unwrap().minute() > now.minute() || self.at_time.unwrap().minute() == now.minute() && self.at_time.unwrap().second() > now.second()) { - self.next_run = Some(self.next_run()? - Hour.duration(1)); + self.next_run = Some(self.next_run()?.checked_sub(Hour.duration(1)).unwrap()); } else if self.unit == Some(Minute) && self.at_time.unwrap().second() > now.second() { - self.next_run = Some(self.next_run()? - Minute.duration(1)); + self.next_run = Some(self.next_run()?.checked_sub(Minute.duration(1)).unwrap()); } } } @@ -1007,9 +986,9 @@ impl Job { // Check if at_time on given day should fire today or next week if self.start_day.is_some() && self.at_time.is_some() { // unwraps are safe, we already set them in this function - let next = self.next_run.unwrap(); // safe, we already set it - if (next - self.now()).num_days() >= 7 { - self.next_run = Some(next - self.period.unwrap()); + let next = self.next_run.as_ref().unwrap(); // safe, we already set it + if now.until(next).unwrap().get_days() >= 7 { + self.next_run = Some(next.checked_sub(self.period.unwrap()).unwrap()); } } @@ -1017,19 +996,19 @@ impl Job { } /// Check if given time is after the `cancel_after` time - fn is_overdue(&self, when: Timestamp) -> bool { - self.cancel_after.is_some() && when > self.cancel_after.unwrap() + fn is_overdue(&self, when: &Zoned) -> bool { + self.cancel_after.is_some() && when > self.cancel_after.as_ref().unwrap() } - pub(crate) fn last_run(&self) -> Result { - self.last_run.ok_or(Error::LastRunUnreachable) + pub(crate) fn last_run(&self) -> Result { + self.last_run.clone().ok_or(Error::LastRunUnreachable) } - pub(crate) fn next_run(&self) -> Result { - self.next_run.ok_or(Error::NextRunUnreachable) + pub(crate) fn next_run(&self) -> Result { + self.next_run.clone().ok_or(Error::NextRunUnreachable) } - pub(crate) fn period(&self) -> Result { + pub(crate) fn period(&self) -> Result { self.period.ok_or(Error::PeriodUnreachable) } @@ -1186,8 +1165,13 @@ mod tests { let mut job = every_single(); let expected = "Attempted to use a start day for a unit other than `weeks`".to_string(); job.unit = Some(Unit::Day); - job.start_day = Some(Weekday::Wed); - assert_eq!(job.schedule_next_run().unwrap_err().to_string(), expected); + job.start_day = Some(civil::Weekday::Wednesday); + assert_eq!( + job.schedule_next_run(&Zoned::now()) + .unwrap_err() + .to_string(), + expected + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index b730307..b5f6b51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! Define a work function: //! ```rust //! fn job() { -//! println!("Hello, it's {}!", chrono::Local::now().to_rfc2822()); +//! println!("Hello, it's {}!", jiff::Zoned::now()); //! } //! ``` //! You can use up to six arguments: @@ -18,11 +18,11 @@ //! Instantiate a `Scheduler` and schedule jobs: //! ```rust //! # use skedge::{Scheduler, every, every_single}; -//! # use chrono::Local; +//! # use jiff::Zoned; //! # use std::time::Duration; //! # use std::thread::sleep; //! # fn job() { -//! # println!("Hello, it's {}!", Local::now()); +//! # println!("Hello, it's {}!", Zoned::now()); //! # } //! # fn greet(name: &str) { //! # println!("Hello, {}!", name); @@ -41,8 +41,8 @@ //! every(2) //! .to(8)? //! .seconds()? -//! .until(Local::now() + chrono::Duration::seconds(30))? -//! .run_one_arg(&mut schedule, greet, "Good-Looking")?; +//! .until(Zoned::now() + Duration::from_secs(30))? +//! .run_one_arg(&mut schedule, greet, "Cool Person")?; //! # Ok(()) //! # } //! ``` @@ -73,7 +73,7 @@ use callable::{ pub use error::*; pub use job::{every, every_single, Interval, Job, Tag}; pub use scheduler::Scheduler; -use time::{Real, Timekeeper, Timestamp, Unit}; +use time::{Clock, Timekeeper, Unit}; #[cfg(feature = "ffi")] mod ffi; diff --git a/src/scheduler.rs b/src/scheduler.rs index 02b1590..30bc8b7 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -1,15 +1,16 @@ //! The scheduler is responsible for managing all scheduled jobs. -use crate::{time::Real, Job, Result, Tag, Timekeeper, Timestamp}; +use crate::{Clock, Job, Result, Tag, Timekeeper}; +use jiff::Zoned; use tracing::debug; /// A Scheduler creates jobs, tracks recorded jobs, and executes jobs. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Scheduler { /// The currently scheduled lob list jobs: Vec, /// Interface to current time - clock: Option>, + clock: Clock, } impl Scheduler { @@ -22,20 +23,10 @@ impl Scheduler { /// Instantiate with mocked time #[cfg(test)] fn with_mock_time(clock: crate::time::mock::Mock) -> Self { - let mut ret = Self::new(); - ret.clock = Some(Box::new(clock)); - ret - } - - /// Advance all clocks by a certain duration - #[cfg(test)] - fn bump_times(&mut self, duration: chrono::Duration) -> Result<()> { - self.clock.as_mut().unwrap().add_duration(duration); - for job in &mut self.jobs { - job.add_duration(duration); + Self { + clock: Clock::Mock(clock), + ..Default::default() } - self.run_pending()?; - Ok(()) } /// Add a new job to the list @@ -62,9 +53,10 @@ impl Scheduler { //let mut jobs_to_run: Vec<&Job> = self.jobs.iter().filter(|el| el.should_run()).collect(); self.jobs.sort(); let mut to_remove = Vec::new(); + let now = self.now(); for (idx, job) in self.jobs.iter_mut().enumerate() { - if job.should_run() { - let keep_going = job.execute()?; + if job.should_run(&now) { + let keep_going = job.execute(&now)?; if !keep_going { debug!("Cancelling job {job}"); to_remove.push(idx); @@ -85,8 +77,9 @@ impl Scheduler { pub fn run_all(&mut self, delay_seconds: u64) { let num_jobs = self.jobs.len(); debug!("Running all {num_jobs} jobs with {delay_seconds}s delay"); + let now = self.now(); for job in &mut self.jobs { - if let Err(e) = job.execute() { + if let Err(e) = job.execute(&now) { eprintln!("Error: {e}"); } std::thread::sleep(std::time::Duration::from_secs(delay_seconds)); @@ -159,12 +152,12 @@ impl Scheduler { /// /// Would panic if it can't call `min()` on an array that we know has at least one element. #[must_use] - pub fn next_run(&self) -> Option { + pub fn next_run(&self) -> Option { if self.jobs.is_empty() { None } else { // unwrap is safe, we know there's at least one job - self.jobs.iter().min().unwrap().next_run + self.jobs.iter().min().unwrap().next_run.clone() } } @@ -182,7 +175,7 @@ impl Scheduler { /// ``` #[must_use] pub fn idle_seconds(&self) -> Option { - Some((self.next_run()? - self.now()).num_seconds()) + Some(self.next_run()?.until(&self.now()).unwrap().get_seconds()) } /// Get the most recently added job, for testing @@ -195,23 +188,14 @@ impl Scheduler { } } -impl Default for Scheduler { - fn default() -> Self { - Self { - jobs: Vec::new(), - clock: Some(Box::new(Real)), - } - } -} - impl Timekeeper for Scheduler { - fn now(&self) -> Timestamp { - self.clock.as_ref().unwrap().now() + fn now(&self) -> Zoned { + self.clock.now() } #[cfg(test)] - fn add_duration(&mut self, duration: chrono::Duration) { - self.clock.as_mut().unwrap().add_duration(duration) + fn add_duration(&mut self, duration: impl Into) { + self.clock.add_duration(duration) } } @@ -222,22 +206,18 @@ mod tests { use super::*; use crate::{ error::Result, + every, every_single, time::mock::{Mock, START}, - Interval, }; - use chrono::{prelude::*, Duration, Timelike}; + use jiff::{civil, ToSpan}; use pretty_assertions::assert_eq; /// Overshadow scheduler, `every()` and `every_single()` to use our clock instead - fn setup() -> (Scheduler, impl Fn(Interval) -> Job, impl Fn() -> Job) { + fn setup() -> Scheduler { let clock = Mock::default(); let scheduler = Scheduler::with_mock_time(clock); - let every = move |interval: Interval| -> Job { Job::with_mock_time(interval, clock) }; - - let every_single = move || -> Job { Job::with_mock_time(1, clock) }; - - (scheduler, every, every_single) + scheduler } /// Empty mock job @@ -245,7 +225,7 @@ mod tests { #[test] fn test_two_jobs() -> Result<()> { - let (mut scheduler, every, every_single) = setup(); + let mut scheduler = setup(); assert_eq!(scheduler.idle_seconds(), None); @@ -254,31 +234,37 @@ mod tests { every_single().minute()?.run(&mut scheduler, job)?; assert_eq!(scheduler.idle_seconds(), Some(17)); - assert_eq!(scheduler.next_run(), Some(*START + Duration::seconds(17))); + assert_eq!( + scheduler.next_run(), + Some(START.checked_add(17.seconds()).unwrap()) + ); - scheduler.bump_times(Duration::seconds(17))?; + scheduler.add_duration(17.seconds()); assert_eq!( scheduler.next_run(), - Some(*START + Duration::seconds(17 * 2)) + Some(START.checked_add((17 * 2).seconds()).unwrap()) ); - scheduler.bump_times(Duration::seconds(17))?; + scheduler.add_duration(17.seconds()); assert_eq!( scheduler.next_run(), - Some(*START + Duration::seconds(17 * 3)) + Some(START.checked_add((17 * 3).seconds()).unwrap()) ); // This time, we should hit the minute mark next, not the next 17 second mark - scheduler.bump_times(Duration::seconds(17))?; + scheduler.add_duration(17.seconds()); assert_eq!(scheduler.idle_seconds(), Some(9)); - assert_eq!(scheduler.next_run(), Some(*START + Duration::minutes(1))); + assert_eq!( + scheduler.next_run(), + Some(START.checked_add(1.minutes()).unwrap()) + ); // Afterwards, back to the 17 second job - scheduler.bump_times(Duration::seconds(9))?; + scheduler.add_duration(9.seconds()); assert_eq!(scheduler.idle_seconds(), Some(8)); assert_eq!( scheduler.next_run(), - Some(*START + Duration::seconds(17 * 4)) + Some(START.checked_add((17 * 4).seconds()).unwrap()) ); Ok(()) @@ -286,7 +272,7 @@ mod tests { #[test] fn test_time_range() -> Result<()> { - let (mut scheduler, every, _) = setup(); + let mut scheduler = setup(); // Set up 100 jobs, store the minute of the next run let num_jobs = 100; @@ -298,6 +284,7 @@ mod tests { .most_recent_job() .unwrap() .next_run + .as_ref() .unwrap() .minute(), ); @@ -328,7 +315,7 @@ mod tests { #[test] fn test_at_time() -> Result<()> { - let (mut scheduler, _, every_single) = setup(); + let mut scheduler = setup(); every_single() .day()? @@ -339,6 +326,7 @@ mod tests { .most_recent_job() .unwrap() .next_run + .as_ref() .unwrap() .hour(), 10 @@ -348,6 +336,7 @@ mod tests { .most_recent_job() .unwrap() .next_run + .as_ref() .unwrap() .minute(), 30 @@ -357,6 +346,7 @@ mod tests { .most_recent_job() .unwrap() .next_run + .as_ref() .unwrap() .second(), 50 @@ -367,7 +357,7 @@ mod tests { #[test] fn test_clear_scheduler() -> Result<()> { - let (mut scheduler, _, every_single) = setup(); + let mut scheduler = setup(); every_single().day()?.run(&mut scheduler, job)?; every_single().minute()?.run(&mut scheduler, job)?; @@ -380,44 +370,49 @@ mod tests { #[test] fn test_until_time() -> Result<()> { - let (mut scheduler, every, every_single) = setup(); + let mut scheduler = setup(); // Make sure it stores a deadline - let deadline = Local - .with_ymd_and_hms(3000, 1, 1, 12, 0, 0) - .single() - .expect("valid time"); + let deadline = civil::date(3000, 1, 1) + .at(12, 0, 0, 0) + .intz("America/New_York") + .unwrap(); every_single() .day()? - .until(deadline)? + .until(deadline.clone())? .run(&mut scheduler, job)?; assert_eq!( - scheduler.most_recent_job().unwrap().cancel_after.unwrap(), + scheduler + .most_recent_job() + .unwrap() + .cancel_after + .clone() + .unwrap(), deadline ); // Make sure it cancels a job after next_run passes the deadline scheduler.clear(None); - let deadline = Local - .with_ymd_and_hms(2021, 1, 1, 12, 0, 10) - .single() - .expect("valid time"); + let deadline = civil::date(2021, 1, 1) + .at(12, 0, 0, 0) + .intz("America/New_York") + .unwrap(); every(5) .seconds()? .until(deadline)? .run(&mut scheduler, job)?; assert_eq!(scheduler.most_recent_job().unwrap().call_count, 0); - scheduler.bump_times(Duration::seconds(5))?; + scheduler.add_duration(5.seconds()); scheduler.run_pending()?; assert_eq!(scheduler.most_recent_job().unwrap().call_count, 1); assert_eq!(scheduler.jobs.len(), 1); - scheduler.bump_times(Duration::seconds(5))?; + scheduler.add_duration(5.seconds()); assert_eq!(scheduler.jobs.len(), 1); assert_eq!(scheduler.most_recent_job().unwrap().call_count, 2); scheduler.run_pending()?; - scheduler.bump_times(Duration::seconds(5))?; + scheduler.add_duration(5.seconds()); scheduler.run_pending()?; // TODO - how to test to ensure the job did not run? assert_eq!(scheduler.jobs.len(), 0); @@ -425,12 +420,12 @@ mod tests { // Make sure it cancels a job if current execution passes the deadline scheduler.clear(None); - let deadline = *START; + let deadline = START.clone(); every(5) .seconds()? .until(deadline)? .run(&mut scheduler, job)?; - scheduler.bump_times(Duration::seconds(40))?; + scheduler.add_duration(5.seconds()); scheduler.run_pending()?; // TODO - how to test to ensure the job did not run? assert_eq!(scheduler.jobs.len(), 0); @@ -440,7 +435,7 @@ mod tests { #[test] fn test_weekday_at_time() -> Result<()> { - let (mut scheduler, _, every_single) = setup(); + let mut scheduler = setup(); every_single() .wednesday()? @@ -448,12 +443,12 @@ mod tests { .run(&mut scheduler, job)?; let j = scheduler.most_recent_job().unwrap(); - assert_eq!(j.next_run.unwrap().year(), 2021); - assert_eq!(j.next_run.unwrap().month(), 1); - assert_eq!(j.next_run.unwrap().day(), 6); - assert_eq!(j.next_run.unwrap().hour(), 22); - assert_eq!(j.next_run.unwrap().minute(), 38); - assert_eq!(j.next_run.unwrap().second(), 10); + assert_eq!(j.next_run.as_ref().unwrap().year(), 2021); + assert_eq!(j.next_run.as_ref().unwrap().month(), 1); + assert_eq!(j.next_run.as_ref().unwrap().day(), 6); + assert_eq!(j.next_run.as_ref().unwrap().hour(), 22); + assert_eq!(j.next_run.as_ref().unwrap().minute(), 38); + assert_eq!(j.next_run.as_ref().unwrap().second(), 10); scheduler.clear(None); @@ -463,12 +458,12 @@ mod tests { .run(&mut scheduler, job)?; let j = scheduler.most_recent_job().unwrap(); - assert_eq!(j.next_run.unwrap().year(), 2021); - assert_eq!(j.next_run.unwrap().month(), 1); - assert_eq!(j.next_run.unwrap().day(), 6); - assert_eq!(j.next_run.unwrap().hour(), 22); - assert_eq!(j.next_run.unwrap().minute(), 39); - assert_eq!(j.next_run.unwrap().second(), 0); + assert_eq!(j.next_run.as_ref().unwrap().year(), 2021); + assert_eq!(j.next_run.as_ref().unwrap().month(), 1); + assert_eq!(j.next_run.as_ref().unwrap().day(), 6); + assert_eq!(j.next_run.as_ref().unwrap().hour(), 22); + assert_eq!(j.next_run.as_ref().unwrap().minute(), 39); + assert_eq!(j.next_run.as_ref().unwrap().second(), 0); Ok(()) } diff --git a/src/time.rs b/src/time.rs index 7403a22..a82f6ec 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,37 +1,39 @@ //! For mocking purposes, access to the current time is controlled directed through this struct. -use chrono::{prelude::*, Duration}; +use jiff::{Span, ToSpan, Zoned}; use std::fmt; -/// Timestamps are in the users local timezone -pub type Timestamp = DateTime; - pub(crate) trait Timekeeper: std::fmt::Debug { /// Return the current time - fn now(&self) -> Timestamp; + fn now(&self) -> Zoned; /// Add a specific duration for testing purposes #[cfg(test)] - fn add_duration(&mut self, duration: Duration); + fn add_duration(&mut self, duration: impl Into); } -impl PartialEq for dyn Timekeeper { - fn eq(&self, other: &Self) -> bool { - self.now() - other.now() < Duration::milliseconds(10) - } +#[derive(Debug, Default)] +pub(crate) enum Clock { + #[default] + Real, + #[cfg(test)] + Mock(mock::Mock), } -impl Eq for dyn Timekeeper {} - -#[derive(Debug, Default, Clone, Copy)] -pub struct Real; - -impl Timekeeper for Real { - fn now(&self) -> Timestamp { - Local::now() +impl Timekeeper for Clock { + fn now(&self) -> Zoned { + match self { + Clock::Real => Zoned::now(), + #[cfg(test)] + Clock::Mock(mock) => mock.now(), + } } + #[cfg(test)] - fn add_duration(&mut self, _duration: Duration) { - unreachable!() // unneeded + fn add_duration(&mut self, duration: impl Into) { + match self { + Clock::Real => unreachable!(), + Clock::Mock(mock) => mock.add_duration(duration), + } } } @@ -48,18 +50,18 @@ pub enum Unit { } impl Unit { - /// Get a `chrono::Duration` from an interval based on time unit - pub fn duration(self, interval: u32) -> Duration { + /// Get a [`jiff::SignedDuration`] from an interval based on time unit. + pub fn duration(self, interval: u32) -> Span { use Unit::{Day, Hour, Minute, Month, Second, Week, Year}; let interval = i64::from(interval); match self { - Second => Duration::seconds(interval), - Minute => Duration::minutes(interval), - Hour => Duration::hours(interval), - Day => Duration::days(interval), - Week => Duration::weeks(interval), - Month => Duration::weeks(interval * 4), - Year => Duration::weeks(interval * 52), + Second => interval.seconds(), + Minute => interval.minutes(), + Hour => interval.hours(), + Day => interval.days(), + Week => interval.weeks(), + Month => interval.months(), + Year => interval.years(), } } } @@ -82,40 +84,38 @@ impl fmt::Display for Unit { #[cfg(test)] pub mod mock { - use super::{Local, TimeZone, Timekeeper, Timestamp}; - /// Default starting time - pub static START: std::sync::LazyLock = std::sync::LazyLock::new(|| { - Local - .with_ymd_and_hms(2021, 1, 1, 12, 0, 0) - .single() - .expect("valid date") - }); + use super::Timekeeper; + use jiff::{Zoned, ZonedArithmetic}; + use std::sync::LazyLock; + + pub(crate) static START: LazyLock = + LazyLock::new(|| "2024-01-01:22:00:00[America/New_York]".parse().unwrap()); /// Mock the datetime for predictable results. - #[derive(Debug, Clone, Copy)] + #[derive(Debug)] pub struct Mock { - stamp: Timestamp, + instant: Zoned, } impl Mock { - pub fn new(stamp: Timestamp) -> Self { - Self { stamp } + pub fn new(stamp: Zoned) -> Self { + Self { instant: stamp } } } impl Default for Mock { fn default() -> Self { - Self::new(*START) + Self::new(START.clone()) } } impl Timekeeper for Mock { - fn now(&self) -> Timestamp { - self.stamp + fn now(&self) -> Zoned { + self.instant.clone() } - fn add_duration(&mut self, duration: chrono::Duration) { - self.stamp += duration; + fn add_duration(&mut self, duration: impl Into) { + let _ = self.instant.checked_add(duration); } } }