Command-line and Emacs Calendar Client
1(* Test the Query module *)
2
3open Caledonia_lib
4
5(* Setup a fixed date for testing *)
6let fixed_date = Option.get @@ Ptime.of_date_time ((2025, 3, 27), ((0, 0, 0), 0))
7
8let setup_fixed_date () =
9 (Date.get_today := fun ?tz:_ () -> fixed_date);
10 fixed_date
11
12let calendar_dir_path = Filename.concat (Sys.getcwd ()) "calendar"
13
14let test_query_all ~fs () =
15 let calendar_dir =
16 Result.get_ok @@ Calendar_dir.create ~fs calendar_dir_path
17 in
18 let from =
19 Some (Option.get @@ Ptime.of_date_time ((2025, 01, 01), ((0, 0, 0), 0)))
20 in
21 let to_ = Option.get @@ Ptime.of_date_time ((2026, 01, 01), ((0, 0, 0), 0)) in
22 let events = Result.get_ok @@ Calendar_dir.get_events ~fs calendar_dir in
23 let events = Event.query events ~from ~to_ () in
24 Alcotest.(check int) "Should find events" 791 (List.length events);
25 let test_event =
26 List.find_opt
27 (fun event -> Option.get @@ Event.get_summary event = "Test Event")
28 events
29 in
30 Alcotest.(check bool) "Should find Test Event" true (test_event <> None)
31
32let test_recurrence_expansion ~fs () =
33 let calendar_dir =
34 Result.get_ok @@ Calendar_dir.create ~fs calendar_dir_path
35 in
36 let from =
37 Some (Option.get @@ Ptime.of_date_time ((2025, 3, 1), ((0, 0, 0), 0)))
38 in
39 let to_ =
40 Option.get @@ Ptime.of_date_time ((2025, 5, 31), ((23, 59, 59), 0))
41 in
42 let events = Result.get_ok @@ Calendar_dir.get_events ~fs calendar_dir in
43 let events = Event.query events ~from ~to_ () in
44 let recurring_events =
45 List.filter
46 (fun event -> Option.get @@ Event.get_summary event = "Recurring Event")
47 events
48 in
49 Alcotest.(check bool)
50 "Should find multiple recurring event events" true
51 (List.length recurring_events > 1)
52
53let test_text_search ~fs () =
54 let calendar_dir =
55 Result.get_ok @@ Calendar_dir.create ~fs calendar_dir_path
56 in
57 let filter = Event.summary_contains "Test" in
58 let from =
59 Some (Option.get @@ Ptime.of_date_time ((2025, 01, 01), ((0, 0, 0), 0)))
60 in
61 let to_ = Option.get @@ Ptime.of_date_time ((2026, 01, 01), ((0, 0, 0), 0)) in
62 let events = Result.get_ok @@ Calendar_dir.get_events ~fs calendar_dir in
63 (let events = Event.query events ~from ~to_ ~filter () in
64 Alcotest.(check int)
65 "Should find event with 'Test' in summary" 2 (List.length events));
66 let filter = Event.location_contains "Weekly" in
67 (let events = Event.query events ~from ~to_ ~filter () in
68 Alcotest.(check int)
69 "Should find event with 'Weekly' in location" 10 (List.length events));
70 let filter =
71 Event.and_filter
72 [ Event.summary_contains "Test"; Event.description_contains "test" ]
73 in
74 (let events = Event.query events ~from ~to_ ~filter () in
75 Alcotest.(check int)
76 "Should find events matching combined and criteria" 2 (List.length events));
77 let filter =
78 Event.or_filter
79 [ Event.summary_contains "Test"; Event.location_contains "Weekly" ]
80 in
81 (let events = Event.query events ~from ~to_ ~filter () in
82 Alcotest.(check int)
83 "Should find events matching combined or criteria" 12 (List.length events));
84 ()
85
86let test_calendar_filter ~fs () =
87 let calendar_dir =
88 Result.get_ok @@ Calendar_dir.create ~fs calendar_dir_path
89 in
90 let from =
91 Some (Option.get @@ Ptime.of_date_time ((2025, 01, 01), ((0, 0, 0), 0)))
92 in
93 let to_ = Option.get @@ Ptime.of_date_time ((2026, 01, 01), ((0, 0, 0), 0)) in
94 let calendar_name = "example" in
95 let filter = Event.in_calendars [ calendar_name ] in
96 let events = Result.get_ok @@ Calendar_dir.get_events ~fs calendar_dir in
97 (let events = Event.query events ~from ~to_ ~filter () in
98 let all_match_calendar =
99 List.for_all
100 (fun e ->
101 match Event.get_calendar_name e with id -> id = calendar_name)
102 events
103 in
104 Alcotest.(check bool)
105 (Printf.sprintf "All events should be from calendar '%s'" calendar_name)
106 true all_match_calendar;
107 Alcotest.(check int) "Should find events" 2 (List.length events));
108 let calendar_names = [ "example"; "recurrence" ] in
109 let filter = Event.in_calendars calendar_names in
110 (let events = Event.query events ~from ~to_ ~filter () in
111 Alcotest.(check int) "Should find events" 791 (List.length events));
112 let filter = Event.in_calendars [ "non-existent-calendar" ] in
113 (let events = Event.query events ~from ~to_ ~filter () in
114 Alcotest.(check int)
115 "Should find 0 events for non-existent calendar" 0 (List.length events));
116 ()
117
118let test_events ~fs =
119 (* Create a test event with specific text in all fields *)
120 let create_test_event ~calendar_name ~summary ~description ~location ~start =
121 Event.create ~fs ~calendar_dir_path ~summary ~start
122 ?description:(if description = "" then None else Some description)
123 ?location:(if location = "" then None else Some location)
124 calendar_name
125 in
126 [
127 (* Event with text in all fields *)
128 Result.get_ok
129 @@ create_test_event ~calendar_name:"search_test" ~summary:"Project Meeting"
130 ~description:"Weekly project status meeting with team"
131 ~location:"Conference Room A"
132 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
133 (* Event with mixed case to test case insensitivity *)
134 Result.get_ok
135 @@ create_test_event ~calendar_name:"search_test"
136 ~summary:"IMPORTANT Meeting"
137 ~description:"Critical project review with stakeholders"
138 ~location:"Executive Suite"
139 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
140 (* Event with word fragments *)
141 Result.get_ok
142 @@ create_test_event ~calendar_name:"search_test" ~summary:"Conference Call"
143 ~description:"International conference preparation"
144 ~location:"Remote Meeting Room"
145 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
146 (* Event with unique text in each field *)
147 Result.get_ok
148 @@ create_test_event ~calendar_name:"search_test"
149 ~summary:"Workshop on Testing"
150 ~description:"Quality Assurance techniques and practices"
151 ~location:"Training Center"
152 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date));
153 ]
154
155(* Test helper to verify if a list of events contains an event with a given summary *)
156let contains_summary events summary =
157 List.exists
158 (fun e -> String.equal (Option.get @@ Event.get_summary e) summary)
159 events
160
161let test_case_insensitive_search ~fs () =
162 (* Test lowercase query for an uppercase word *)
163 let lowercase_filter = Event.summary_contains "important" in
164 let matches =
165 List.filter
166 (fun e -> Event.matches_filter e lowercase_filter)
167 (test_events ~fs)
168 in
169 Alcotest.(check bool)
170 "Lowercase query should match uppercase text in summary" true
171 (contains_summary matches "IMPORTANT Meeting");
172 (* Test uppercase query for a lowercase word *)
173 let uppercase_filter = Event.description_contains "WEEKLY" in
174 let matches =
175 List.filter
176 (fun e -> Event.matches_filter e uppercase_filter)
177 (test_events ~fs)
178 in
179 Alcotest.(check bool)
180 "Uppercase query should match lowercase text in description" true
181 (contains_summary matches "Project Meeting")
182
183let test_partial_word_matching ~fs () =
184 (* Test searching for part of a word *)
185 let partial_filter = Event.summary_contains "Conf" in
186 (* Should match "Conference" *)
187 let matches =
188 List.filter
189 (fun e -> Event.matches_filter e partial_filter)
190 (test_events ~fs)
191 in
192 Alcotest.(check bool)
193 "Partial query should match full word in summary" true
194 (contains_summary matches "Conference Call");
195 (* Test another partial word in description *)
196 let partial_filter = Event.description_contains "nation" in
197 (* Should match "International" *)
198 let matches =
199 List.filter
200 (fun e -> Event.matches_filter e partial_filter)
201 (test_events ~fs)
202 in
203 Alcotest.(check bool)
204 "Partial query should match within word in description" true
205 (contains_summary matches "Conference Call");
206
207 Alcotest.(check bool)
208 "Partial query should match within word in description" true
209 (contains_summary matches "Conference Call")
210
211let test_boolean_logic ~fs () =
212 (* Test AND filter *)
213 let and_filter =
214 Event.and_filter
215 [ Event.summary_contains "Meeting"; Event.description_contains "project" ]
216 in
217 let matches =
218 List.filter (fun e -> Event.matches_filter e and_filter) (test_events ~fs)
219 in
220 Alcotest.(check int)
221 "AND filter should match events with both terms" 2
222 (* Two events have both "Meeting" in summary and "project" in description *)
223 (List.length matches);
224 (* Test OR filter *)
225 let or_filter =
226 Event.or_filter
227 [ Event.summary_contains "Workshop"; Event.summary_contains "Conference" ]
228 in
229 let matches =
230 List.filter (fun e -> Event.matches_filter e or_filter) (test_events ~fs)
231 in
232 Alcotest.(check int)
233 "OR filter should match events with either term"
234 2 (* One event has "Workshop", one has "Conference" *)
235 (List.length matches);
236
237 (* Test NOT filter *)
238 let not_filter = Event.not_filter (Event.summary_contains "Meeting") in
239 let matches =
240 List.filter (fun e -> Event.matches_filter e not_filter) (test_events ~fs)
241 in
242 Alcotest.(check int)
243 "NOT filter should match events without the term"
244 2 (* Two events don't have "Meeting" in the summary *)
245 (List.length matches);
246 (* Test complex combination: (Meeting AND project) OR Workshop BUT NOT Conference *)
247 let complex_filter =
248 Event.and_filter
249 [
250 Event.or_filter
251 [
252 Event.and_filter
253 [
254 Event.summary_contains "Meeting";
255 Event.description_contains "project";
256 ];
257 Event.summary_contains "Workshop";
258 ];
259 Event.not_filter (Event.summary_contains "Conference");
260 ]
261 in
262 let matches =
263 List.filter
264 (fun e -> Event.matches_filter e complex_filter)
265 (test_events ~fs)
266 in
267 Alcotest.(check int)
268 "Complex filter should match correctly"
269 3 (* Three events should match the complex criteria *)
270 (List.length matches)
271
272let test_cross_field_search ~fs () =
273 (* Search for a term that appears in multiple fields across different events *)
274 let term_filter =
275 Event.or_filter
276 [
277 Event.summary_contains "meeting";
278 Event.description_contains "meeting";
279 Event.location_contains "meeting";
280 ]
281 in
282 let matches =
283 List.filter (fun e -> Event.matches_filter e term_filter) (test_events ~fs)
284 in
285 Alcotest.(check int)
286 "Cross-field search should find all occurrences"
287 3 (* "meeting" appears in 3 events across different fields *)
288 (List.length matches);
289 (* Another test with a different term *)
290 let term_filter =
291 Event.or_filter
292 [
293 Event.summary_contains "conference";
294 Event.description_contains "conference";
295 Event.location_contains "conference";
296 ]
297 in
298 let matches =
299 List.filter (fun e -> Event.matches_filter e term_filter) (test_events ~fs)
300 in
301 Alcotest.(check int)
302 "Cross-field search should find all occurrences of 'conference'"
303 2 (* "conference" appears in 2 events across different fields *)
304 (List.length matches)
305
306let query_tests fs =
307 [
308 ("query all events", `Quick, test_query_all ~fs);
309 ("recurrence expansion", `Quick, test_recurrence_expansion ~fs);
310 ("text search", `Quick, test_text_search ~fs);
311 ("calendar filter", `Quick, test_calendar_filter ~fs);
312 ("case insensitive search", `Quick, test_case_insensitive_search ~fs);
313 ("partial word matching", `Quick, test_partial_word_matching ~fs);
314 ("boolean logic filters", `Quick, test_boolean_logic ~fs);
315 ("cross-field searching", `Quick, test_cross_field_search ~fs);
316 ]
317
318let () =
319 Eio_main.run @@ fun env ->
320 let fs = Eio.Stdenv.fs env in
321 let _ = setup_fixed_date () in
322 Alcotest.run "Query Tests" [ ("query", query_tests fs) ]