bundle.rs
⎇
Raw
1//! Feeds, export and import splitting.
2
3use pimdav::bundle::{self, Detail};
4use pimdav::object;
5
6const 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";
7
8fn uids() -> impl FnMut(&str) -> String {
9 let mut n = 0;
10 move |_| {
11 n += 1;
12 format!("new-{n}")
13 }
14}
15
16#[test]
17fn feed_merges_objects_and_deduplicates_zones() {
18 let a = format!(
19 "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"
20 );
21 // LF endings in storage become CRLF.
22 let b = format!(
23 "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",
24 BERLIN.replace("\r\n", "\n")
25 );
26 let full = bundle::calendar(&[&a, &b], Some("Work; home"), Detail::All);
27 assert!(full.starts_with("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"));
28 assert!(full.contains(r"X-WR-CALNAME:Work\; home"));
29 assert!(full.contains(r"NAME:Work\; home"));
30 let long = bundle::calendar(&[&a], Some(&"x".repeat(200)), Detail::All);
31 assert!(long.lines().all(|l| l.len() <= 75), "{long}");
32 assert!(full.contains("REFRESH-INTERVAL;VALUE=DURATION:PT1H\r\nX-PUBLISHED-TTL:PT1H\r\n"));
33 assert_eq!(full.matches("BEGIN:VTIMEZONE").count(), 1);
34 assert!(full.contains("SUMMARY:Long\r\n folded\r\n"));
35 assert!(full.contains("BEGIN:VTODO\r\nUID:c\r\n"));
36 assert!(!full.replace("\r\n", "").contains('\n'));
37 assert!(full.ends_with("END:VCALENDAR\r\n"));
38
39 let busy = bundle::calendar(&[&a, &b], None, Detail::Busy);
40 assert_eq!(busy.matches("SUMMARY:Busy").count(), 2);
41 assert!(!busy.contains("Long"));
42 assert!(!busy.contains("VALARM"));
43 assert!(!busy.contains("ATTENDEE"));
44 assert!(!busy.contains("VTODO"));
45 assert!(busy.contains("DTSTART;TZID=Europe/Berlin:20240102T100000\r\n"));
46 assert!(busy.contains("BEGIN:VTIMEZONE"));
47}
48
49fn object(components: &str) -> String {
50 format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n{components}END:VCALENDAR\r\n")
51}
52
53#[test]
54fn busy_time_hides_what_blocks_nothing() {
55 let series = object(
56 "BEGIN:VEVENT\r\nUID:series@corp.example\r\nDTSTART:20240101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\n\
57 BEGIN:VEVENT\r\nUID:series@corp.example\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240102T100000Z\r\nDTSTART:20240102T100000Z\r\nSTATUS:CANCELLED\r\nEND:VEVENT\r\n\
58 BEGIN:VEVENT\r\nUID:series@corp.example\r\nRECURRENCE-ID:20240103T100000Z\r\nDTSTART:20240103T120000Z\r\nEND:VEVENT\r\n",
59 );
60 let free = object(
61 "BEGIN:VEVENT\r\nUID:free\r\nDTSTART:20240105T100000Z\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n",
62 );
63 let busy = bundle::calendar(&[&series, &free], None, Detail::Busy);
64 // The master and the moved instance stay; the cancelled range stays as
65 // an override, so it frees its instance and the later ones.
66 assert_eq!(busy.matches("BEGIN:VEVENT").count(), 3, "{busy}");
67 assert!(!busy.contains("EXDATE"), "{busy}");
68 assert!(busy.contains("RANGE=THISANDFUTURE") && busy.contains("STATUS:CANCELLED"));
69 assert!(!busy.contains("corp.example") && !busy.contains("UID:free"));
70 let uids: Vec<&str> = busy.lines().filter(|l| l.starts_with("UID:")).collect();
71 assert_eq!(uids.len(), 3);
72 assert!(
73 uids.iter().all(|u| *u == uids[0]),
74 "master and overrides keep one UID"
75 );
76}
77
78#[test]
79fn public_feeds_reduce_private_objects() {
80 let open = object(
81 "BEGIN:VEVENT\r\nUID:open\r\nDTSTART:20240101T100000Z\r\nSUMMARY:Standup\r\nEND:VEVENT\r\n",
82 );
83 let private = object(
84 "BEGIN:VEVENT\r\nUID:doc\r\nDTSTART:20240101T120000Z\r\nRRULE:FREQ=WEEKLY\r\nSUMMARY:Doctor\r\nCLASS:PRIVATE\r\nEND:VEVENT\r\n\
85 BEGIN:VEVENT\r\nUID:doc\r\nRECURRENCE-ID:20240108T120000Z\r\nDTSTART:20240108T130000Z\r\nSUMMARY:Doctor\r\nEND:VEVENT\r\n",
86 );
87 let task = object(
88 "BEGIN:VTODO\r\nUID:t\r\nSUMMARY:Secret task\r\nCLASS:CONFIDENTIAL\r\nEND:VTODO\r\n",
89 );
90 let public = bundle::calendar(&[&open, &private, &task], None, Detail::Public);
91 assert!(public.contains("SUMMARY:Standup"));
92 assert!(
93 !public.contains("Doctor") && !public.contains("Secret task"),
94 "{public}"
95 );
96 // The whole object is reduced, so its override still matches its master.
97 assert_eq!(public.matches("SUMMARY:Busy").count(), 2);
98 let all = bundle::calendar(&[&open, &private, &task], None, Detail::All);
99 assert!(all.contains("Doctor") && all.contains("Secret task"));
100}
101
102#[test]
103fn import_splits_by_uid_with_overrides_and_zones() {
104 let file = format!(
105 "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\
106 BEGIN:VEVENT\r\nUID:m\r\nDTSTART;TZID=Europe/Berlin:20240101T100000\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n\
107 BEGIN:VEVENT\r\nUID:plain\r\nDTSTART:20240105T100000Z\r\nEND:VEVENT\r\n\
108 BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID;TZID=Europe/Berlin:20240102T100000\r\nDTSTART;TZID=Europe/Berlin:20240102T120000\r\nEND:VEVENT\r\n\
109 BEGIN:VEVENT\r\nDTSTART:20240106T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
110 );
111 let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap();
112 assert_eq!(parts.len(), 3);
113 let master = &parts[0];
114 assert_eq!(master.matches("BEGIN:VEVENT").count(), 2);
115 assert!(master.contains("RECURRENCE-ID"));
116 assert!(master.contains("TZID:Europe/Berlin"));
117 assert!(!master.contains("Unused"));
118 assert!(!master.contains("METHOD"));
119 assert!(!master.contains("X-WR-CALNAME"));
120 assert!(master.contains("PRODID:src\r\n"));
121 assert!(!parts[1].contains("VTIMEZONE"));
122 assert!(parts[2].contains("BEGIN:VEVENT\r\nUID:new-1\r\n"));
123 for p in &parts {
124 assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}");
125 }
126 // With a UID from the content, as the server makes it, a file holding
127 // everything twice gives the same objects.
128 let mut by_content = |t: &str| format!("len-{}", t.len());
129 let once = bundle::split_calendar(&file, &mut by_content, usize::MAX).unwrap();
130 let twice =
131 bundle::split_calendar(&format!("{file}{file}"), &mut by_content, usize::MAX).unwrap();
132 assert_eq!(twice, once);
133}
134
135#[test]
136fn escaped_tzid_finds_its_zone() {
137 let file = "BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:(UTC+02:00) Athens\\, Bucharest\r\nEND:VTIMEZONE\r\n\
138 BEGIN:VEVENT\r\nUID:e\r\nDTSTART;TZID=\"(UTC+02:00) Athens, Bucharest\":20130412T150000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
139 let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap();
140 assert!(parts[0].contains("BEGIN:VTIMEZONE"), "{}", parts[0]);
141 assert_eq!(
142 bundle::calendar(&[&parts[0]], None, Detail::All)
143 .matches("BEGIN:VTIMEZONE")
144 .count(),
145 1
146 );
147}
148
149#[test]
150fn import_splits_cards() {
151 let file = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:one\r\nFN:One\r\nEND:VCARD\r\n\
152 BEGIN:VCARD\nVERSION:4.0\nFN:Two\nEND:VCARD\n";
153 let cards = bundle::split_cards(file, &mut uids());
154 assert_eq!(cards.len(), 2);
155 assert_eq!(
156 cards[0],
157 "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:one\r\nFN:One\r\nEND:VCARD\r\n"
158 );
159 // Before END: vCard 4.0 wants VERSION right after BEGIN.
160 assert_eq!(
161 cards[1],
162 "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Two\r\nUID:new-1\r\nEND:VCARD\r\n"
163 );
164 for c in &cards {
165 assert!(object::vcard(c.as_bytes()).unwrap().is_some());
166 }
167 assert_eq!(bundle::cards(&[&cards[0], &cards[1]]), cards.concat());
168}
169
170#[test]
171fn quoted_tzid_keeps_its_zone() {
172 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";
173 let file = format!(
174 "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"
175 );
176 let objects = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap();
177 assert_eq!(objects.len(), 1);
178 assert!(objects[0].contains("TZID:Work\\; late: shift"));
179}
180
181#[test]
182fn calendar_names_itself() {
183 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";
184 assert_eq!(
185 bundle::calendar_meta(ics),
186 (Some("Ferien, Bayern".into()), Some("#FF2968FF".into()))
187 );
188 // RFC 7986 wins over the X- names, and a component's own NAME does not count.
189 let ics = ics.replace(
190 "VERSION:2.0\r\n",
191 "VERSION:2.0\r\nNAME:Holidays\r\nCOLOR:teal\r\n",
192 );
193 assert_eq!(
194 bundle::calendar_meta(&ics),
195 (Some("Holidays".into()), Some("teal".into()))
196 );
197 let bare = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nNAME:not me\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
198 assert_eq!(bundle::calendar_meta(bare), (None, None));
199}
200
201#[test]
202fn byte_order_mark_is_ignored() {
203 let file = format!(
204 "\u{feff}{}",
205 object("BEGIN:VEVENT\r\nUID:a\r\nEND:VEVENT\r\n")
206 );
207 assert_eq!(
208 bundle::split_calendar(&file, &mut uids(), usize::MAX)
209 .unwrap()
210 .len(),
211 1
212 );
213 assert_eq!(
214 bundle::calendar_meta("\u{feff}BEGIN:VCALENDAR\r\nNAME:N\r\nEND:VCALENDAR\r\n")
215 .0
216 .as_deref(),
217 Some("N")
218 );
219 let card = "\u{feff}BEGIN:VCARD\r\nVERSION:3.0\r\nUID:c\r\nFN:C\r\nEND:VCARD\r\n";
220 assert_eq!(bundle::split_cards(card, &mut uids()).len(), 1);
221 // A stored object keeps its mark; feeds still show it.
222 assert!(bundle::calendar(&[&file], None, Detail::All).contains("UID:a"));
223}
224
225#[test]
226fn shared_uid_across_types_splits_and_empty_uid_is_missing() {
227 let file = object(
228 "BEGIN:VEVENT\r\nUID:x\r\nDTSTART:20240101T100000Z\r\nEND:VEVENT\r\n\
229 BEGIN:VTODO\r\nUID:x\r\nSUMMARY:t\r\nEND:VTODO\r\n\
230 BEGIN:VTODO\r\nUID:x\r\nRECURRENCE-ID:20240102T100000Z\r\nEND:VTODO\r\n\
231 BEGIN:VEVENT\r\nUID:\r\nDTSTART:20240103T100000Z\r\nEND:VEVENT\r\n",
232 );
233 let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap();
234 assert_eq!(parts.len(), 3, "{parts:#?}");
235 assert!(parts[0].contains("BEGIN:VEVENT\r\nUID:x\r\n"));
236 assert_eq!(parts[1].matches("UID:new-1\r\n").count(), 2, "{}", parts[1]);
237 assert!(!parts[1].contains("UID:x"));
238 assert!(parts[2].contains("UID:new-2\r\n"));
239 assert!(!parts[2].contains("UID:\r\n"));
240 for p in &parts {
241 assert!(
242 object::calendar(p.as_bytes(), &["VEVENT", "VTODO"]).is_ok(),
243 "{p}"
244 );
245 }
246 let cards = bundle::split_cards(
247 "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:\r\nFN:C\r\nEND:VCARD\r\n",
248 &mut uids(),
249 );
250 assert_eq!(
251 cards[0],
252 "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:C\r\nUID:new-1\r\nEND:VCARD\r\n"
253 );
254}
255
256#[test]
257fn busy_time_writes_exdates_once_and_import_drops_cut_parts() {
258 let events: String = (0..2)
259 .map(|h| format!("BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T1{h}0000Z\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n"))
260 .collect();
261 let free = "BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T100000Z\r\nDTSTART:20240102T100000Z\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n";
262 let busy = bundle::calendar(&[&object(&format!("{events}{free}"))], None, Detail::Busy);
263 assert_eq!(busy.matches("EXDATE").count(), 1, "{busy}");
264
265 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";
266 let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap();
267 assert_eq!(parts.len(), 1, "{parts:?}");
268 assert!(parts[0].contains("UID:a"));
269 let cut = format!("{file}BEGIN:VALARM\r\nTRIGGER:-PT5M\r\nEND:VALARM\r\nEND:VCALENDAR\r\n");
270 assert_eq!(
271 bundle::split_calendar(&cut, &mut uids(), usize::MAX)
272 .unwrap()
273 .len(),
274 1
275 );
276 let cards = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\nEND:VCARD\r\nBEGIN:VCARD\r\nFN:B\r\n";
277 assert_eq!(bundle::split_cards(cards, &mut uids()).len(), 1);
278}
279
280#[test]
281fn a_second_master_of_a_uid_gets_an_object_of_its_own() {
282 let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\
283 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nSUMMARY:one\r\nEND:VEVENT\r\n\
284 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240201T100000Z\r\nSUMMARY:two\r\nEND:VEVENT\r\n\
285 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nSUMMARY:one\r\nEND:VEVENT\r\n\
286 END:VCALENDAR\r\n";
287 let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap();
288 assert_eq!(parts.len(), 2, "{parts:?}");
289 assert!(parts[0].contains("UID:m\r\n") && parts[0].contains("SUMMARY:one"));
290 assert!(parts[1].contains("UID:new-1\r\n") && parts[1].contains("SUMMARY:two"));
291 for p in &parts {
292 assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}");
293 }
294}
295
296#[test]
297fn an_override_follows_the_master_that_has_its_instance() {
298 let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\
299 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:A\r\nEND:VEVENT\r\n\
300 BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T100000Z\r\nDTSTART:20240102T120000Z\r\nSUMMARY:A1\r\nEND:VEVENT\r\n\
301 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240201T090000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:B\r\nEND:VEVENT\r\n\
302 BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240202T090000Z\r\nDTSTART:20240202T150000Z\r\nSUMMARY:B1\r\nEND:VEVENT\r\n\
303 END:VCALENDAR\r\n";
304 let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap();
305 assert_eq!(parts.len(), 2, "{parts:?}");
306 assert!(parts[0].contains("SUMMARY:A1") && !parts[0].contains("SUMMARY:B"));
307 assert!(parts[1].contains("UID:new-1\r\n") && parts[1].contains("SUMMARY:B1"));
308 assert!(!parts[1].contains("UID:m\r\n"), "{}", parts[1]);
309 for p in &parts {
310 assert!(object::calendar(p.as_bytes(), &["VEVENT"]).is_ok(), "{p}");
311 }
312}
313
314#[test]
315fn override_placement_reads_the_masters_own_time_zones() {
316 let file = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\
317 BEGIN:VTIMEZONE\r\nTZID:My Zone\r\nBEGIN:STANDARD\r\nDTSTART:19700101T000000\r\n\
318 TZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n\
319 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\
320 BEGIN:VEVENT\r\nUID:m\r\nDTSTART:20240101T120000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:B\r\nEND:VEVENT\r\n\
321 BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240102T180000Z\r\nDTSTART:20240102T200000Z\r\nSUMMARY:A1\r\nEND:VEVENT\r\n\
322 END:VCALENDAR\r\n";
323 let parts = bundle::split_calendar(file, &mut uids(), usize::MAX).unwrap();
324 let a = parts.iter().find(|p| p.contains("SUMMARY:A\r\n")).unwrap();
325 assert!(a.contains("SUMMARY:A1"), "{parts:?}");
326}
327
328#[test]
329fn a_free_this_and_future_override_frees_the_later_instances() {
330 let object = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\
331 BEGIN:VEVENT\r\nUID:s\r\nDTSTART:20240101T100000Z\r\nDURATION:PT1H\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEND:VEVENT\r\n\
332 BEGIN:VEVENT\r\nUID:s\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240103T100000Z\r\n\
333 DTSTART:20240103T100000Z\r\nDURATION:PT1H\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\n\
334 END:VCALENDAR\r\n";
335 let feed = bundle::calendar(&[object], None, Detail::Busy);
336 assert!(!feed.contains("EXDATE"), "{feed}");
337 let cal = pimdav::calcard::icalendar::ICalendar::parse(&feed).unwrap();
338 let range = "2024-01-01T00:00:00Z".parse().unwrap().."2024-01-10T00:00:00Z".parse().unwrap();
339 let busy = pimdav::freebusy::busy(&cal, &range, &pimdav::zone::Zone::Utc, None);
340 assert_eq!(busy.len(), 2, "{busy:?}");
341}
342
343#[test]
344fn feeds_skip_a_component_cut_off_before_its_end() {
345 let cut = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:cut\r\nSUMMARY:x\r\nEND:VCALENDAR\r\n";
346 let fine = object("BEGIN:VEVENT\r\nUID:fine\r\nDTSTART:20240101T100000Z\r\nEND:VEVENT\r\n");
347 let feed = bundle::calendar(&[cut, &fine], None, Detail::All);
348 assert!(!feed.contains("UID:cut"), "{feed}");
349 assert!(feed.contains("UID:fine"), "{feed}");
350}
351
352#[test]
353fn overrides_of_several_masters_are_placed_quickly() {
354 let mut file = String::from("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n");
355 for (summary, start) in [("A", "19700101T000000Z"), ("B", "19700101T000001Z")] {
356 file.push_str(&format!(
357 "BEGIN:VEVENT\r\nUID:m\r\nDTSTART:{start}\r\nRRULE:FREQ=SECONDLY;INTERVAL=2\r\nSUMMARY:{summary}\r\nEND:VEVENT\r\n"
358 ));
359 }
360 for n in 0..20 {
361 file.push_str(&format!(
362 "BEGIN:VEVENT\r\nUID:m\r\nRECURRENCE-ID:20240101T0000{:02}Z\r\nDTSTART:20240102T000000Z\r\nEND:VEVENT\r\n",
363 n * 2 + 1
364 ));
365 }
366 file.push_str("END:VCALENDAR\r\n");
367 let started = std::time::Instant::now();
368 let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap();
369 assert!(
370 started.elapsed().as_secs_f64() < 2.0,
371 "{:?}",
372 started.elapsed()
373 );
374 // B started last before them.
375 assert_eq!(parts.len(), 2);
376 assert_eq!(
377 parts[1].matches("RECURRENCE-ID").count(),
378 20,
379 "{}",
380 parts[1]
381 );
382}
383
384#[test]
385fn many_masters_of_one_uid_are_placed_quickly() {
386 let mut file = String::from("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n");
387 for n in 0..400 {
388 file.push_str(&format!(
389 "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",
390 n % 12 + 1
391 ));
392 }
393 for n in 0..400 {
394 file.push_str(&format!(
395 "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",
396 n % 12 + 1,
397 n % 12 + 1
398 ));
399 }
400 file.push_str("END:VCALENDAR\r\n");
401 let started = std::time::Instant::now();
402 let parts = bundle::split_calendar(&file, &mut uids(), usize::MAX).unwrap();
403 assert!(
404 started.elapsed().as_secs_f64() < 1.0,
405 "{:?}",
406 started.elapsed()
407 );
408 assert_eq!(parts.len(), 400);
409}
410
411#[test]
412fn a_busy_range_override_of_a_free_series_stays_busy() {
413 let object = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\
414 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\
415 BEGIN:VEVENT\r\nUID:s\r\nRECURRENCE-ID;RANGE=THISANDFUTURE:20240103T100000Z\r\n\
416 DTSTART:20240103T100000Z\r\nDURATION:PT1H\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\n\
417 END:VCALENDAR\r\n";
418 let feed = bundle::calendar(&[object], None, Detail::Busy);
419 let cal = pimdav::calcard::icalendar::ICalendar::parse(&feed).unwrap();
420 let range = "2024-01-01T00:00:00Z".parse().unwrap().."2024-01-10T00:00:00Z".parse().unwrap();
421 let busy = pimdav::freebusy::busy(&cal, &range, &pimdav::zone::Zone::Utc, None);
422 assert_eq!(busy.len(), 3, "{busy:?}\n{feed}");
423}
424