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