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