//! Differential test of `expand` over calcard's fixture corpus. //! //! `corpus/oracle.tsv` holds the instances recurring-ical-events computes //! (see `corpus/oracle.py`). `corpus/deviations.tsv` overrides it where the //! oracle fails or where this crate deliberately differs, with the reason. //! On failure the test prints deviation lines for the files that differ. //! //! `PIMDAV_CORPUS_FULL=` writes every start to that file, in the format //! of `oracle.py --full`, for review. use std::collections::HashMap; use std::fmt::Write as _; use std::path::PathBuf; use chrono::{DateTime, Utc}; use pimdav::calcard::icalendar::ICalendarComponentType; use pimdav::calcard::{Entry, Parser}; use pimdav::expand::expand; use pimdav::zone::Zone; /// Keep equal to the pinned calcard version in Cargo.toml. const CALCARD: &str = "calcard-0.3.14"; fn fnv(data: &[u8]) -> String { let mut h: u64 = 0xcbf29ce484222325; for b in data { h = (h ^ u64::from(*b)).wrapping_mul(0x100000001b3); } format!("{h:016x}") } fn fixtures() -> PathBuf { let home = std::env::var_os("CARGO_HOME") .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(std::env::var_os("HOME").unwrap()).join(".cargo")); std::fs::read_dir(home.join("registry/src")) .expect("no cargo registry") .map(|e| e.unwrap().path().join(CALCARD).join("resources/ical")) .find(|p| p.is_dir()) .expect("calcard sources not in the cargo registry") } /// `name -> (columns...)` of a tab-separated file. fn table(name: &str) -> HashMap> { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/corpus") .join(name); std::fs::read_to_string(path) .unwrap() .lines() .filter(|l| !l.is_empty() && !l.starts_with('#')) .map(|l| { let mut cols = l.split('\t').map(str::to_string); (cols.next().unwrap(), cols.collect()) }) .collect() } fn starts(input: &str) -> Vec { let lo = DateTime::::from_timestamp(0, 0).unwrap(); let hi = DateTime::::from_timestamp(2_208_988_800, 0).unwrap(); let mut parser = Parser::new(input); let mut out = Vec::new(); loop { match parser.entry() { Entry::ICalendar(cal) => { let expansion = expand(&cal, lo..hi, Zone::Utc); out.extend( expansion .instances .iter() .filter(|i| { cal.components[i.component].component_type == ICalendarComponentType::VEvent && i.start >= lo && i.start < hi }) .map(|i| i.start.timestamp()), ); } Entry::Eof => break, _ => {} } } out.sort_unstable(); out } #[test] fn corpus_matches_oracle() { let dir = fixtures(); let oracle = table("oracle.tsv"); let deviations = table("deviations.tsv"); let mut names: Vec<_> = std::fs::read_dir(&dir) .unwrap() .map(|e| e.unwrap().file_name().into_string().unwrap()) .filter(|n| n.ends_with(".ics")) .collect(); names.sort(); assert_eq!( names.len(), oracle.len(), "fixture set changed, rerun oracle.py" ); let mut full = String::new(); let mut failures = String::new(); for name in &names { let raw = std::fs::read(dir.join(name)).unwrap(); let o = &oracle[name]; assert_eq!(o[0], fnv(&raw), "{name} changed, rerun oracle.py"); let input = String::from_utf8(raw.clone()) .unwrap_or_else(|_| raw.iter().map(|&b| b as char).collect()); let starts = starts(&input); let list = starts .iter() .map(i64::to_string) .collect::>() .join(","); let (count, hash) = (starts.len(), fnv(list.as_bytes())); writeln!(full, "{name}\t{}\tok\t{count}\t{list}", o[0]).unwrap(); let expected = match deviations.get(name) { Some(d) => Some((d[0].as_str(), d[1].as_str())), None if o[1] == "ok" => Some((o[2].as_str(), o[3].as_str())), None => None, }; if expected != Some((count.to_string().as_str(), hash.as_str())) { let why = deviations.get(name).map_or("?", |d| d[2].as_str()); writeln!(failures, "{name}\t{count}\t{hash}\t{why}").unwrap(); } } if let Some(path) = std::env::var_os("PIMDAV_CORPUS_FULL") { std::fs::write(path, full).unwrap(); } assert!(failures.is_empty(), "differs from expectation:\n{failures}"); }