add unit tests finally
This commit is contained in:
parent
a4e4c89e15
commit
3044deb89f
2 changed files with 214 additions and 30 deletions
70
src/main.rs
70
src/main.rs
|
@ -6,6 +6,9 @@ use icalendar::{Component as _, EventLike as _};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{io::Write as _, path::PathBuf, str::FromStr as _, time::Duration};
|
use std::{io::Write as _, path::PathBuf, str::FromStr as _, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
struct ConfigIcal {
|
struct ConfigIcal {
|
||||||
/// Disk location to cache the ics file for debugging
|
/// Disk location to cache the ics file for debugging
|
||||||
|
@ -82,8 +85,30 @@ struct Parameters {
|
||||||
tz: chrono_tz::Tz,
|
tz: chrono_tz::Tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Parameters {
|
||||||
|
fn new(now: DateTime<chrono_tz::Tz>) -> Result<Self> {
|
||||||
|
// 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
|
/// 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 {
|
struct DatePerhapsTime {
|
||||||
dt: DateTime<chrono_tz::Tz>,
|
dt: DateTime<chrono_tz::Tz>,
|
||||||
all_day: bool,
|
all_day: bool,
|
||||||
|
@ -267,19 +292,20 @@ fn event_instances<'a>(
|
||||||
struct ICal {
|
struct ICal {
|
||||||
/// The parsed ics file
|
/// The parsed ics file
|
||||||
cal: icalendar::Calendar,
|
cal: icalendar::Calendar,
|
||||||
|
|
||||||
/// The config used to download the ics file
|
|
||||||
config: ConfigIcal,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ICal {
|
impl ICal {
|
||||||
fn read(config: ConfigIcal) -> Result<Self> {
|
fn read_from_str(s: &str) -> Result<Self> {
|
||||||
let s = std::fs::read_to_string(&config.file_path)?;
|
|
||||||
let cal = s.parse().map_err(|s| anyhow!("parse error {s}"))?;
|
let cal = s.parse().map_err(|s| anyhow!("parse error {s}"))?;
|
||||||
let cal = Self { cal, config };
|
let cal = Self { cal };
|
||||||
Ok(cal)
|
Ok(cal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_from_config(config: &ConfigIcal) -> Result<Self> {
|
||||||
|
let s = std::fs::read_to_string(&config.file_path)?;
|
||||||
|
Self::read_from_str(&s)
|
||||||
|
}
|
||||||
|
|
||||||
fn events(&self) -> impl Iterator<Item = &icalendar::Event> {
|
fn events(&self) -> impl Iterator<Item = &icalendar::Event> {
|
||||||
self.cal.components.iter().filter_map(|comp| {
|
self.cal.components.iter().filter_map(|comp| {
|
||||||
if let icalendar::CalendarComponent::Event(ev) = comp {
|
if let icalendar::CalendarComponent::Event(ev) = comp {
|
||||||
|
@ -320,42 +346,26 @@ impl ICal {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Data {
|
struct Data {
|
||||||
icals: Vec<ICal>,
|
icals: Vec<(ICal, ConfigIcal)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_data_from_disk(config: &Config) -> Result<Data> {
|
fn read_data_from_disk(config: &Config) -> Result<Data> {
|
||||||
let mut data = Data::default();
|
let mut data = Data::default();
|
||||||
for cfg in &config.icals {
|
for config in &config.icals {
|
||||||
let cal = ICal::read(cfg.clone())?;
|
let cal = ICal::read_from_config(config)?;
|
||||||
data.icals.push(cal);
|
data.icals.push((cal, config.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_data(data: &Data, now: DateTime<chrono_tz::Tz>) -> Result<Vec<EventWithUrl<'_>>> {
|
fn process_data(data: &Data, now: DateTime<chrono_tz::Tz>) -> Result<Vec<EventWithUrl<'_>>> {
|
||||||
// Snap the cutoffs to midnight so we won't present half of a day
|
let params = Parameters::new(now)?;
|
||||||
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 mut instances = vec![];
|
let mut instances = vec![];
|
||||||
for ical in &data.icals {
|
for (ical, config) in &data.icals {
|
||||||
for ei in ical.event_instances(¶ms)? {
|
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);
|
instances.push(ei);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
174
src/tests.rs
Normal file
174
src/tests.rs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn chicago_time(
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
day: u32,
|
||||||
|
hour: u32,
|
||||||
|
minute: u32,
|
||||||
|
second: u32,
|
||||||
|
) -> DateTime<chrono_tz::Tz> {
|
||||||
|
chrono_tz::America::Chicago
|
||||||
|
.with_ymd_and_hms(year, month, day, hour, minute, second)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dt_from_ts(ts: i64) -> DateTime<chrono_tz::Tz> {
|
||||||
|
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(())
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue