From 3044deb89f5d21981c93f542d0c87c5319c66284 Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Tue, 12 Aug 2025 22:43:33 +0000 Subject: [PATCH] add unit tests finally --- src/main.rs | 70 ++++++++++++--------- src/tests.rs | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 30 deletions(-) create mode 100644 src/tests.rs diff --git a/src/main.rs b/src/main.rs index 44ff1a4..5744ed5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ use icalendar::{Component as _, EventLike as _}; use serde::Deserialize; use std::{io::Write as _, path::PathBuf, str::FromStr as _, time::Duration}; +#[cfg(test)] +mod tests; + #[derive(Clone, Deserialize)] struct ConfigIcal { /// Disk location to cache the ics file for debugging @@ -82,8 +85,30 @@ struct Parameters { tz: chrono_tz::Tz, } +impl Parameters { + fn new(now: DateTime) -> Result { + // Snap the cutoffs to midnight so we won't present half of a day + let midnight = chrono::NaiveTime::default(); + let output_start = (now - Duration::from_secs(86_400 * 2)) + .with_time(midnight) + .single() + .context("output_start doesn't map to a single time in our timezone")?; + let output_stop = (now + Duration::from_secs(86_400 * 45)) + .with_time(midnight) + .single() + .context("output_stop doesn't map to a single time in our timezone")?; + + Ok(Parameters { + ignore_before: now - Duration::from_secs(86_400 * 365 * 2), + output_start, + output_stop, + tz: now.timezone(), + }) + } +} + /// Similar to `icalendar::DatePerhapsTime` but doesn't allow Floating, and naive dates are stored as local midnight with an "all day" flag -#[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq)] struct DatePerhapsTime { dt: DateTime, all_day: bool, @@ -267,19 +292,20 @@ fn event_instances<'a>( struct ICal { /// The parsed ics file cal: icalendar::Calendar, - - /// The config used to download the ics file - config: ConfigIcal, } impl ICal { - fn read(config: ConfigIcal) -> Result { - let s = std::fs::read_to_string(&config.file_path)?; + fn read_from_str(s: &str) -> Result { let cal = s.parse().map_err(|s| anyhow!("parse error {s}"))?; - let cal = Self { cal, config }; + let cal = Self { cal }; Ok(cal) } + fn read_from_config(config: &ConfigIcal) -> Result { + let s = std::fs::read_to_string(&config.file_path)?; + Self::read_from_str(&s) + } + fn events(&self) -> impl Iterator { self.cal.components.iter().filter_map(|comp| { if let icalendar::CalendarComponent::Event(ev) = comp { @@ -320,42 +346,26 @@ impl ICal { #[derive(Default)] struct Data { - icals: Vec, + icals: Vec<(ICal, ConfigIcal)>, } fn read_data_from_disk(config: &Config) -> Result { let mut data = Data::default(); - for cfg in &config.icals { - let cal = ICal::read(cfg.clone())?; - data.icals.push(cal); + for config in &config.icals { + let cal = ICal::read_from_config(config)?; + data.icals.push((cal, config.clone())); } Ok(data) } fn process_data(data: &Data, now: DateTime) -> Result>> { - // Snap the cutoffs to midnight so we won't present half of a day - let midnight = chrono::NaiveTime::default(); - let output_start = (now - Duration::from_secs(86_400 * 2)) - .with_time(midnight) - .single() - .context("output_start doesn't map to a single time in our timezone")?; - let output_stop = (now + Duration::from_secs(86_400 * 45)) - .with_time(midnight) - .single() - .context("output_stop doesn't map to a single time in our timezone")?; - - let params = Parameters { - ignore_before: now - Duration::from_secs(86_400 * 365 * 2), - output_start, - output_stop, - tz: now.timezone(), - }; + let params = Parameters::new(now)?; let mut instances = vec![]; - for ical in &data.icals { + for (ical, config) in &data.icals { for ei in ical.event_instances(¶ms)? { - let ei = EventWithUrl::from_ei(&ical.config, ei)?; + let ei = EventWithUrl::from_ei(config, ei)?; instances.push(ei); } } diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..e1808ac --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,174 @@ +use super::*; + +fn chicago_time( + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, +) -> DateTime { + chrono_tz::America::Chicago + .with_ymd_and_hms(year, month, day, hour, minute, second) + .unwrap() +} + +fn dt_from_ts(ts: i64) -> DateTime { + DateTime::from_timestamp(ts, 0) + .unwrap() + .with_timezone(&chrono_tz::America::Chicago) +} + +/// Expect that parsing a calendar works +#[test] +fn calendar_from_str() -> Result<()> { + // Blank lines added for clarity + let s = r#" +BEGIN:VCALENDAR + +CALSCALE:GREGORIAN +X-WR-TIMEZONE:America/Chicago + +BEGIN:VTIMEZONE +TZID:America/Chicago +X-LIC-LOCATION:America/Chicago +BEGIN:DAYLIGHT +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +TZNAME:CDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +TZNAME:CST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE + +BEGIN:VEVENT +DTSTART;TZID=America/Chicago:20250908T180000 +DTEND;TZID=America/Chicago:20250908T200000 +UID:Redacted +RECURRENCE-ID;TZID=America/Chicago:20250901T180000 +CREATED:20241222T171032Z +LAST-MODIFIED:20250812T021726Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:zero roman mummy hatch +TRANSP:OPAQUE +END:VEVENT + +END:VCALENDAR +"#; + + let ical = ICal::read_from_str(s)?; + let now = dt_from_ts(1755000000); + let params = Parameters::new(now)?; + let instances = ical.event_instances(¶ms)?; + assert_eq!(instances.len(), 1); + + let event = &instances[0]; + let expected_time = DatePerhapsTime { + dt: chicago_time(2025, 9, 8, 18, 0, 0), + all_day: false, + }; + assert_eq!(event.dtstart, expected_time); + assert_eq!(event.ev.get_summary(), Some("zero roman mummy hatch")); + Ok(()) +} + +/// Expect that recurrent exceptions work correctly and don't duplicate events +#[test] +fn recurrence_exceptions() -> Result<()> { + let s = r#" +BEGIN:VCALENDAR + +CALSCALE:GREGORIAN +X-WR-TIMEZONE:America/Chicago + +BEGIN:VTIMEZONE +TZID:America/Chicago +X-LIC-LOCATION:America/Chicago +BEGIN:DAYLIGHT +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +TZNAME:CDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +TZNAME:CST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE + +BEGIN:VEVENT +DTSTART;TZID=America/Chicago:20250708T180000 +DTEND;TZID=America/Chicago:20250708T200000 +RRULE:FREQ=MONTHLY;BYDAY=2TU +UID:jazz repay stout steam +CLASS:PUBLIC +CREATED:20250703T113806Z +LAST-MODIFIED:20250721T232331Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:coil perm brush zippy +TRANSP:OPAQUE +END:VEVENT + +BEGIN:VEVENT +DTSTART;TZID=America/Chicago:20250814T180000 +DTEND;TZID=America/Chicago:20250814T200000 +UID:jazz repay stout steam +RECURRENCE-ID;TZID=America/Chicago:20250812T180000 +CLASS:PUBLIC +CREATED:20250703T113806Z +LAST-MODIFIED:20250721T232500Z +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:coil perm brush zippy +TRANSP:OPAQUE +END:VEVENT + +END:VCALENDAR +"#; + + let ical = ICal::read_from_str(s)?; + let params = Parameters { + ignore_before: chicago_time(2025, 1, 1, 0, 0, 0), + output_start: chicago_time(2025, 7, 1, 0, 0, 0), + output_stop: chicago_time(2025, 10, 1, 0, 0, 0), + tz: chrono_tz::America::Chicago, + }; + let instances = ical.event_instances(¶ms)?; + assert_eq!(instances.len(), 3); + + let expected_time = DatePerhapsTime { + dt: chicago_time(2025, 7, 8, 18, 0, 0), + all_day: false, + }; + assert_eq!(instances[0].dtstart, expected_time); + assert_eq!(instances[0].ev.get_summary(), Some("coil perm brush zippy")); + + let expected_time = DatePerhapsTime { + dt: chicago_time(2025, 8, 14, 18, 0, 0), + all_day: false, + }; + assert_eq!(instances[1].dtstart, expected_time); + assert_eq!(instances[1].ev.get_summary(), Some("coil perm brush zippy")); + + let expected_time = DatePerhapsTime { + dt: chicago_time(2025, 9, 9, 18, 0, 0), + all_day: false, + }; + assert_eq!(instances[2].dtstart, expected_time); + assert_eq!(instances[2].ev.get_summary(), Some("coil perm brush zippy")); + + Ok(()) +}