//! Feeds, export and import splitting. use pimdav::bundle::{self, Detail}; use pimdav::object; const BERLIN: &str = "BEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:STANDARD\r\nDTSTART:19701025T030000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n"; fn uids() -> impl FnMut(&str) -> String { let mut n = 0; move |_| { n += 1; format!("new-{n}") } } #[test] fn feed_merges_objects_and_deduplicates_zones() { let a = format!( "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:x\r\n{BERLIN}BEGIN:VEVENT\r\nUID:a\r\nDTSTART;TZID=Europe/Berlin:20240101T100000\r\nSUMMARY:Long\r\n folded\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT5M\r\nEND:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" ); // LF endings in storage become CRLF. let b = format!( "BEGIN:VCALENDAR\nVERSION:2.0\n{}BEGIN:VEVENT\nUID:b\nDTSTART;TZID=Europe/Berlin:20240102T100000\nATTENDEE:mailto:x@y\nEND:VEVENT\nBEGIN:VTODO\nUID:c\nEND:VTODO\nEND:VCALENDAR\n", BERLIN.replace("\r\n", "\n") ); let full = bundle::calendar(&[&a, &b], Some("Work; home"), Detail::All); assert!(full.starts_with("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n")); assert!(full.contains(r"X-WR-CALNAME:Work\; home")); assert!(full.contains(r"NAME:Work\; home")); let long = bundle::calendar(&[&a], Some(&"x".repeat(200)), Detail::All); assert!(long.lines().all(|l| l.len() <= 75), "{long}"); assert!(full.contains("REFRESH-INTERVAL;VALUE=DURATION:PT1H\r\nX-PUBLISHED-TTL:PT1H\r\n")); assert_eq!(full.matches("BEGIN:VTIMEZONE").count(), 1); assert!(full.contains("SUMMARY:Long\r\n folded\r\n")); assert!(full.contains("BEGIN:VTODO\r\nUID:c\r\n")); assert!(!full.replace("\r\n", "").contains('\n')); assert!(full.ends_with("END:VCALENDAR\r\n")); let busy = bundle::calendar(&[&a, &b], None, Detail::Busy); assert_eq!(busy.matches("SUMMARY:Busy").count(), 2); assert!(!busy.contains("Long")); assert!(!busy.contains("VALARM")); assert!(!busy.contains("ATTENDEE")); assert!(!busy.contains("VTODO")); assert!(busy.contains("DTSTART;TZID=Europe/Berlin:20240102T100000\r\n")); assert!(busy.contains("BEGIN:VTIMEZONE")); } fn object(components: &str) -> String { format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n{components}END:VCALENDAR\r\n") } #[test] fn busy_time_hides_what_blocks_nothing() { let series = object( "BEGIN:VEVENT\r\nUID:series@corp.example\r\nDTSTART:20240101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:series@corp.example\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240102T100000Z\r\nDTSTART:20240102T100000Z\r\nSTATUS:CANCELLED\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:series@corp.example\r\nRECURRENCE-ID:20240103T100000Z\r\nDTSTART:20240103T120000Z\r\nEND:VEVENT\r\n", ); let free = object( "BEGIN:VEVENT\r\nUID:free\r\nDTSTART:20240105T100000Z\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n", ); let busy = bundle::calendar(&[&series, &free], None, Detail::Busy); // The master and the moved instance stay; the cancelled range stays as // an override, so it frees its instance and the later ones. assert_eq!(busy.matches("BEGIN:VEVENT").count(), 3, "{busy}"); assert!(!busy.contains("EXDATE"), "{busy}"); assert!(busy.contains("RANGE=THISANDFUTURE") && busy.contains("STATUS:CANCELLED")); assert!(!busy.contains("corp.example") && !busy.contains("UID:free")); let uids: Vec<&str> = busy.lines().filter(|l| l.starts_with("UID:")).collect(); assert_eq!(uids.len(), 3); assert!( uids.iter().all(|u| *u == uids[0]), "master and overrides keep one UID" ); } #[test] fn public_feeds_reduce_private_objects() { let open = object( "BEGIN:VEVENT\r\nUID:open\r\nDTSTART:20240101T100000Z\r\nSUMMARY:Standup\r\nEND:VEVENT\r\n", ); let private = object( "BEGIN:VEVENT\r\nUID:doc\r\nDTSTART:20240101T120000Z\r\nRRULE:FREQ=WEEKLY\r\nSUMMARY:Doctor\r\nCLASS:PRIVATE\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:doc\r\nRECURRENCE-ID:20240108T120000Z\r\nDTSTART:20240108T130000Z\r\nSUMMARY:Doctor\r\nEND:VEVENT\r\n", ); let task = object( "BEGIN:VTODO\r\nUID:t\r\nSUMMARY:Secret task\r\nCLASS:CONFIDENTIAL\r\nEND:VTODO\r\n", ); let public = bundle::calendar(&[&open, &private, &task], None, Detail::Public); assert!(public.contains("SUMMARY:Standup")); assert!( !public.contains("Doctor") && !public.contains("Secret task"), "{public}" ); // The whole object is reduced, so its override still matches its master. assert_eq!(public.matches("SUMMARY:Busy").count(), 2); let all = bundle::calendar(&[&open, &private, &task], None, Detail::All); assert!(all.contains("Doctor") && all.contains("Secret task")); } #[test] fn import_splits_by_uid_with_overrides_and_zones() { let file = format!( "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:src\r\nMETHOD:PUBLISH\r\nX-WR-CALNAME:Old\r\n{BERLIN}BEGIN:VTIMEZONE\r\nTZID:Unused\r\nEND:VTIMEZONE\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART;TZID=Europe/Berlin:20240101T100000\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:plain\r\nDTSTART:20240105T100000Z\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID;TZID=Europe/Berlin:20240102T100000\r\nDTSTART;TZID=Europe/Berlin:20240102T120000\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nDTSTART:20240106T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" ); let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap(); assert_eq!(parts.len(), 3); let master = &parts[0]; assert_eq!(master.matches("BEGIN:VEVENT").count(), 2); assert!(master.contains("RECURRENCE-ID")); assert!(master.contains("TZID:Europe/Berlin")); assert!(!master.contains("Unused")); assert!(!master.contains("METHOD")); assert!(!master.contains("X-WR-CALNAME")); assert!(master.contains("PRODID:src\r\n")); assert!(!parts[1].contains("VTIMEZONE")); assert!(parts[2].contains("BEGIN:VEVENT\r\nUID:new-1\r\n")); for p in &parts { assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}"); } // With a UID from the content, as the server makes it, a file holding // everything twice gives the same objects. let mut by_content = |t: &str| format!("len-{}", t.len()); let once = bundle::split_calendar(&file, &mut by_content, usize::MAX).unwrap(); let twice = bundle::split_calendar(&format!("{file}{file}"), &mut by_content, usize::MAX).unwrap(); assert_eq!(twice, once); } #[test] fn escaped_tzid_finds_its_zone() { let file = "BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:(UTC+02:00) Athens\\, Bucharest\r\nEND:VTIMEZONE\r\n\ BEGIN:VEVENT\r\nUID:e\r\nDTSTART;TZID=\"(UTC+02:00) Athens, Bucharest\":20130412T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap(); assert!(parts[0].contains("BEGIN:VTIMEZONE"), "{}", parts[0]); assert_eq!( bundle::calendar(&[&parts[0]], None, Detail::All) .matches("BEGIN:VTIMEZONE") .count(), 1 ); } #[test] fn import_splits_cards() { let file = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:one\r\nFN:One\r\nEND:VCARD\r\n\ BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD\n"; let cards = bundle::split_cards(file, &mut uids()); assert_eq!(cards.len(), 2); assert_eq!( cards[0], "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:one\r\nFN:One\r\nEND:VCARD\r\n" ); // Before END: vCard 4.0 wants VERSION right after BEGIN. assert_eq!( cards[1], "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Two\r\nUID:new-1\r\nEND:VCARD\r\n" ); for c in &cards { assert!(object::vcard(c.as_bytes()).unwrap().is_some()); } assert_eq!(bundle::cards(&[&cards[0], &cards[1]]), cards.concat()); } #[test] fn quoted_tzid_keeps_its_zone() { let zone = "BEGIN:VTIMEZONE\r\nTZID:Work\\; late: shift\r\nBEGIN:STANDARD\r\nDTSTART:19700101T000000\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n"; let file = format!( "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n{zone}BEGIN:VEVENT\r\nUID:a\r\nDTSTART;TZID=\"Work; late: shift\":20240101T100000\r\nRECURRENCE-ID;TZID=\"Work; late: shift\":20240101T100000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" ); let objects = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap(); assert_eq!(objects.len(), 1); assert!(objects[0].contains("TZID:Work\\; late: shift")); } #[test] fn calendar_names_itself() { let ics = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nX-WR-CALNAME:Ferien\\, Bayern\r\nX-APPLE-CALENDAR-COLOR:#FF2968FF\r\nBEGIN:VEVENT\r\nUID:a\r\nSUMMARY:x\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; assert_eq!( bundle::calendar_meta(ics), (Some("Ferien, Bayern".into()), Some("#FF2968FF".into())) ); // RFC 7986 wins over the X- names, and a component's own NAME does not count. let ics = ics.replace( "VERSION:2.0\r\n", "VERSION:2.0\r\nNAME:Holidays\r\nCOLOR:teal\r\n", ); assert_eq!( bundle::calendar_meta(&ics), (Some("Holidays".into()), Some("teal".into())) ); let bare = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nNAME:not me\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; assert_eq!(bundle::calendar_meta(bare), (None, None)); } #[test] fn byte_order_mark_is_ignored() { let file = format!( "\u{feff}{}", object("BEGIN:VEVENT\r\nUID:a\r\nEND:VEVENT\r\n") ); assert_eq!( bundle::split_calendar(&file, &mut uids(), usize::MAX) .unwrap() .len(), 1 ); assert_eq!( bundle::calendar_meta("\u{feff}BEGIN:VCALENDAR\r\nNAME:N\r\nEND:VCALENDAR\r\n") .0 .as_deref(), Some("N") ); let card = "\u{feff}BEGIN:VCARD\r\nVERSION:3.0\r\nUID:c\r\nFN:C\r\nEND:VCARD\r\n"; assert_eq!(bundle::split_cards(card, &mut uids()).len(), 1); // A stored object keeps its mark; feeds still show it. assert!(bundle::calendar(&[&file], None, Detail::All).contains("UID:a")); } #[test] fn shared_uid_across_types_splits_and_empty_uid_is_missing() { let file = object( "BEGIN:VEVENT\r\nUID:x\r\nDTSTART:20240101T100000Z\r\nEND:VEVENT\r\n\ BEGIN:VTODO\r\nUID:x\r\nSUMMARY:t\r\nEND:VTODO\r\n\ BEGIN:VTODO\r\nUID:x\r\nRECURRENCE-ID:20240102T100000Z\r\nEND:VTODO\r\n\ BEGIN:VEVENT\r\nUID:\r\nDTSTART:20240103T100000Z\r\nEND:VEVENT\r\n", ); let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap(); assert_eq!(parts.len(), 3, "{parts:#?}"); assert!(parts[0].contains("BEGIN:VEVENT\r\nUID:x\r\n")); assert_eq!(parts[1].matches("UID:new-1\r\n").count(), 2, "{}", parts[1]); assert!(!parts[1].contains("UID:x")); assert!(parts[2].contains("UID:new-2\r\n")); assert!(!parts[2].contains("UID:\r\n")); for p in &parts { assert!( object::calendar(p.as_bytes(), &["VEVENT", "VTODO"]).is_ok(), "{p}" ); } let cards = bundle::split_cards( "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:\r\nFN:C\r\nEND:VCARD\r\n", &mut uids(), ); assert_eq!( cards[0], "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:C\r\nUID:new-1\r\nEND:VCARD\r\n" ); } #[test] fn busy_time_writes_exdates_once_and_import_drops_cut_parts() { let events: String = (0..2) .map(|h| format!("BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T1{h}0000Z\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n")) .collect(); let free = "BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T100000Z\r\nDTSTART:20240102T100000Z\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n"; let busy = bundle::calendar(&[&object(&format!("{events}{free}"))], None, Detail::Busy); assert_eq!(busy.matches("EXDATE").count(), 1, "{busy}"); let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:a\r\nDTSTART:20240101T100000Z\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:b\r\nSUMMARY:x\r\n"; let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap(); assert_eq!(parts.len(), 1, "{parts:?}"); assert!(parts[0].contains("UID:a")); let cut = format!("{file}BEGIN:VALARM\r\nTRIGGER:-PT5M\r\nEND:VALARM\r\nEND:VCALENDAR\r\n"); assert_eq!( bundle::split_calendar(&cut, &mut uids(), usize::MAX) .unwrap() .len(), 1 ); let cards = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\nEND:VCARD\r\nBEGIN:VCARD\r\nFN:B\r\n"; assert_eq!(bundle::split_cards(cards, &mut uids()).len(), 1); } #[test] fn a_second_master_of_a_uid_gets_an_object_of_its_own() { let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nSUMMARY:one\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240201T100000Z\r\nSUMMARY:two\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nSUMMARY:one\r\nEND:VEVENT\r\n\ END:VCALENDAR\r\n"; let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap(); assert_eq!(parts.len(), 2, "{parts:?}"); assert!(parts[0].contains("UID:m\r\n") && parts[0].contains("SUMMARY:one")); assert!(parts[1].contains("UID:new-1\r\n") && parts[1].contains("SUMMARY:two")); for p in &parts { assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}"); } } #[test] fn an_override_follows_the_master_that_has_its_instance() { let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:A\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T100000Z\r\nDTSTART:20240102T120000Z\r\nSUMMARY:A1\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240201T090000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:B\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240202T090000Z\r\nDTSTART:20240202T150000Z\r\nSUMMARY:B1\r\nEND:VEVENT\r\n\ END:VCALENDAR\r\n"; let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap(); assert_eq!(parts.len(), 2, "{parts:?}"); assert!(parts[0].contains("SUMMARY:A1") && !parts[0].contains("SUMMARY:B")); assert!(parts[1].contains("UID:new-1\r\n") && parts[1].contains("SUMMARY:B1")); assert!(!parts[1].contains("UID:m\r\n"), "{}", parts[1]); for p in &parts { assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}"); } } #[test] fn override_placement_reads_the_masters_own_time_zones() { let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\ BEGIN:VTIMEZONE\r\nTZID:My Zone\r\nBEGIN:STANDARD\r\nDTSTART:19700101T000000\r\n\ TZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART;TZID=My Zone:20240101T080000\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:A\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T120000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:B\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T180000Z\r\nDTSTART:20240102T200000Z\r\nSUMMARY:A1\r\nEND:VEVENT\r\n\ END:VCALENDAR\r\n"; let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap(); let a = parts.iter().find(|p| p.contains("SUMMARY:A\r\n")).unwrap(); assert!(a.contains("SUMMARY:A1"), "{parts:?}"); } #[test] fn a_free_this_and_future_override_frees_the_later_instances() { let object = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\ BEGIN:VEVENT\r\nUID:s\r\nDTSTART:20240101T100000Z\r\nDURATION:PT1H\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:s\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240103T100000Z\r\n\ DTSTART:20240103T100000Z\r\nDURATION:PT1H\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n\ END:VCALENDAR\r\n"; let feed = bundle::calendar(&[object], None, Detail::Busy); assert!(!feed.contains("EXDATE"), "{feed}"); let cal = pimdav::calcard::icalendar::ICalendar::parse(&feed).unwrap(); let range = "2024-01-01T00:00:00Z".parse().unwrap().."2024-01-10T00:00:00Z".parse().unwrap(); let busy = pimdav::freebusy::busy(&cal, &range, &pimdav::zone::Zone::Utc, None); assert_eq!(busy.len(), 2, "{busy:?}"); } #[test] fn feeds_skip_a_component_cut_off_before_its_end() { let cut = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:cut\r\nSUMMARY:x\r\nEND:VCALENDAR\r\n"; let fine = object("BEGIN:VEVENT\r\nUID:fine\r\nDTSTART:20240101T100000Z\r\nEND:VEVENT\r\n"); let feed = bundle::calendar(&[cut, &fine], None, Detail::All); assert!(!feed.contains("UID:cut"), "{feed}"); assert!(feed.contains("UID:fine"), "{feed}"); } #[test] fn overrides_of_several_masters_are_placed_quickly() { let mut file = String::from("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"); for (summary, start) in [("A", "19700101T000000Z"), ("B", "19700101T000001Z")] { file.push_str(&format!( "BEGIN:VEVENT\r\nUID:m\r\nDTSTART:{start}\r\nRRULE:FREQ=SECONDLY;INTERVAL=2\r\nSUMMARY:{summary}\r\nEND:VEVENT\r\n" )); } for n in 0..20 { file.push_str(&format!( "BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240101T0000{:02}Z\r\nDTSTART:20240102T000000Z\r\nEND:VEVENT\r\n", n * 2 + 1 )); } file.push_str("END:VCALENDAR\r\n"); let started = std::time::Instant::now(); let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap(); assert!( started.elapsed().as_secs_f64() < 2.0, "{:?}", started.elapsed() ); // B started last before them. assert_eq!(parts.len(), 2); assert_eq!( parts[1].matches("RECURRENCE-ID").count(), 20, "{}", parts[1] ); } #[test] fn many_masters_of_one_uid_are_placed_quickly() { let mut file = String::from("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"); for n in 0..400 { file.push_str(&format!( "BEGIN:VEVENT\r\nUID:m\r\nDTSTART:2024{:02}01T100000Z\r\nRRULE:FREQ=DAILY;COUNT=2\r\nSUMMARY:S{n}\r\nEND:VEVENT\r\n", n % 12 + 1 )); } for n in 0..400 { file.push_str(&format!( "BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:2024{:02}02T100000Z\r\nDTSTART:2024{:02}02T120000Z\r\nSUMMARY:O{n}\r\nEND:VEVENT\r\n", n % 12 + 1, n % 12 + 1 )); } file.push_str("END:VCALENDAR\r\n"); let started = std::time::Instant::now(); let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap(); assert!( started.elapsed().as_secs_f64() < 1.0, "{:?}", started.elapsed() ); assert_eq!(parts.len(), 400); } #[test] fn a_busy_range_override_of_a_free_series_stays_busy() { let object = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\ BEGIN:VEVENT\r\nUID:s\r\nDTSTART:20240101T100000Z\r\nDURATION:PT1H\r\nRRULE:FREQ=DAILY;COUNT=5\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n\ BEGIN:VEVENT\r\nUID:s\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240103T100000Z\r\n\ DTSTART:20240103T100000Z\r\nDURATION:PT1H\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\n\ END:VCALENDAR\r\n"; let feed = bundle::calendar(&[object], None, Detail::Busy); let cal = pimdav::calcard::icalendar::ICalendar::parse(&feed).unwrap(); let range = "2024-01-01T00:00:00Z".parse().unwrap().."2024-01-10T00:00:00Z".parse().unwrap(); let busy = pimdav::freebusy::busy(&cal, &range, &pimdav::zone::Zone::Utc, None); assert_eq!(busy.len(), 3, "{busy:?}\n{feed}"); }