blob: ac0c9ee5383d1186e62fe3a162b3b8e7a53913ae (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
module Ics = Colitur_render.Emit_ics
let out_2027 () = Ics.year (Test_view.view_of 2027)
let lines s =
String.split_on_char '\n' s
|> List.map (fun l -> if l <> "" && l.[String.length l - 1] = '\r' then String.sub l 0 (String.length l - 1) else l)
|> List.filter (fun l -> l <> "")
let count_prefix p ls =
List.length (List.filter (fun l -> String.length l >= String.length p && String.sub l 0 (String.length p) = p) ls)
let test_envelope () =
let ls = lines (out_2027 ()) in
Alcotest.(check string) "first line" "BEGIN:VCALENDAR" (List.hd ls);
Alcotest.(check string) "last line" "END:VCALENDAR" (List.nth ls (List.length ls - 1));
Alcotest.(check int) "version" 1 (count_prefix "VERSION:2.0" ls);
Alcotest.(check int) "prodid" 1 (count_prefix "PRODID:" ls);
Alcotest.(check int) "365 events" 365 (count_prefix "BEGIN:VEVENT" ls)
(* RFC 5545 section 3.6.1: for an all-day event DTEND is EXCLUSIVE. Getting
this wrong shows every event a day short in every client. *)
let test_dtend_is_exclusive () =
let ls = lines (out_2027 ()) in
let starts = List.filter_map (fun l ->
if count_prefix "DTSTART;VALUE=DATE:" [l] = 1 then
Some (String.sub l (String.length "DTSTART;VALUE=DATE:") 8) else None) ls in
let ends = List.filter_map (fun l ->
if count_prefix "DTEND;VALUE=DATE:" [l] = 1 then
Some (String.sub l (String.length "DTEND;VALUE=DATE:") 8) else None) ls in
Alcotest.(check int) "same count" (List.length starts) (List.length ends);
Alcotest.(check string) "1 Jan starts" "20270101" (List.hd starts);
Alcotest.(check string) "1 Jan ends on the 2nd" "20270102" (List.hd ends);
Alcotest.(check string) "31 Dec ends on 1 Jan next" "20280101"
(List.nth ends (List.length ends - 1))
let test_uids_unique_and_shaped () =
let ls = lines (out_2027 ()) in
let uids = List.filter_map (fun l ->
if count_prefix "UID:" [l] = 1 then Some (String.sub l 4 (String.length l - 4)) else None) ls in
Alcotest.(check int) "365 uids" 365 (List.length uids);
let sorted = List.sort_uniq String.compare uids in
Alcotest.(check int) "all unique" 365 (List.length sorted);
Alcotest.(check string) "shape" "20270101-ef@colitur" (List.hd uids)
(* The rule that punishes silently, months later, in someone else's phone:
regenerating a feed must not duplicate every event. *)
let test_output_is_byte_stable_across_runs () =
Alcotest.(check string) "two runs identical" (out_2027 ()) (out_2027 ())
let test_every_line_folded_and_crlf () =
let raw = out_2027 () in
List.iter
(fun l ->
if String.length l > 75 then Alcotest.failf "line of %d octets: %s" (String.length l) l)
(lines raw);
(* Every physical line ends CRLF (RFC 5545 section 3.1). *)
let n = String.length raw in
let rec check i =
if i >= n then ()
else if raw.[i] = '\n' then (
if i = 0 || raw.[i - 1] <> '\r' then Alcotest.failf "bare LF at offset %d" i;
check (i + 1))
else check (i + 1)
in
check 0
let test_no_rrule () =
Alcotest.(check int) "no RRULE" 0 (count_prefix "RRULE" (lines (out_2027 ())))
let test_text_escaped () =
let ls = lines (Ics.year (Test_view.view_of 2035)) in
(* 2035-04-03 is St Joseph, whose English name contains a comma. *)
let joseph = List.find (fun l -> count_prefix "SUMMARY:St. Joseph" [l] = 1) ls in
Alcotest.(check bool) "comma escaped" true
(let re = "\\," in
let n = String.length re in
let rec f i = i + n <= String.length joseph && (String.sub joseph i n = re || f (i + 1)) in
f 0)
let test_dtstamp_is_a_parameter () =
let a = Ics.year ~dtstamp:"20200101T000000Z" (Test_view.view_of 2027) in
let b = Ics.year ~dtstamp:"20210101T000000Z" (Test_view.view_of 2027) in
Alcotest.(check bool) "differs with the parameter" true (a <> b);
(* DTSTAMP is a per-VEVENT property (RFC 5545 section 3.8.7.2), not a
calendar-level one -- every one of the 365 events carries the same
default value, not just one line in the whole feed. *)
Alcotest.(check int) "default is deterministic" 365
(count_prefix "DTSTAMP:20270101T000000Z" (lines (out_2027 ())))
let suite =
( "Emit/ics",
[ Alcotest.test_case "envelope" `Quick test_envelope;
Alcotest.test_case "DTEND exclusive" `Quick test_dtend_is_exclusive;
Alcotest.test_case "uids unique and shaped" `Quick test_uids_unique_and_shaped;
Alcotest.test_case "byte-stable across runs" `Quick test_output_is_byte_stable_across_runs;
Alcotest.test_case "folded and CRLF" `Quick test_every_line_folded_and_crlf;
Alcotest.test_case "no RRULE" `Quick test_no_rrule;
Alcotest.test_case "text escaped" `Quick test_text_escaped;
Alcotest.test_case "DTSTAMP is a parameter" `Quick test_dtstamp_is_a_parameter ] )
|