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 calendar_name = "example" in 107 let filter = Query.in_calendars [ calendar_name ] 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 -> 113 match Event.get_calendar_name e with id -> id = calendar_name) 114 events 115 in 116 Alcotest.(check bool) 117 (Printf.sprintf "All events should be from calendar '%s'" calendar_name) 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 calendar_names = [ "example"; "recurrence" ] in 122 let filter = Query.in_calendars calendar_names 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 = Query.in_calendars [ "non-existent-calendar" ] in 128 (match Query.query ~fs calendar_dir ~from ~to_ ~filter () with 129 | Ok events -> 130 Alcotest.(check int) 131 "Should find 0 events for non-existent calendar" 0 (List.length events) 132 | Error _ -> Alcotest.fail "Error querying events"); 133 () 134 135let test_events ~fs = 136 (* Create a test event with specific text in all fields *) 137 let create_test_event ~calendar_name ~summary ~description ~location ~start = 138 Event.create ~fs ~calendar_dir_path ~summary ~start 139 ?description:(if description = "" then None else Some description) 140 ?location:(if location = "" then None else Some location) 141 calendar_name 142 in 143 [ 144 (* Event with text in all fields *) 145 Result.get_ok 146 @@ create_test_event ~calendar_name:"search_test" ~summary:"Project Meeting" 147 ~description:"Weekly project status meeting with team" 148 ~location:"Conference Room A" 149 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date)); 150 (* Event with mixed case to test case insensitivity *) 151 Result.get_ok 152 @@ create_test_event ~calendar_name:"search_test" 153 ~summary:"IMPORTANT Meeting" 154 ~description:"Critical project review with stakeholders" 155 ~location:"Executive Suite" 156 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date)); 157 (* Event with word fragments *) 158 Result.get_ok 159 @@ create_test_event ~calendar_name:"search_test" ~summary:"Conference Call" 160 ~description:"International conference preparation" 161 ~location:"Remote Meeting Room" 162 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date)); 163 (* Event with unique text in each field *) 164 Result.get_ok 165 @@ create_test_event ~calendar_name:"search_test" 166 ~summary:"Workshop on Testing" 167 ~description:"Quality Assurance techniques and practices" 168 ~location:"Training Center" 169 ~start:(Icalendar.Params.empty, `Datetime (`Utc fixed_date)); 170 ] 171 172(* Test helper to verify if a list of events contains an event with a given summary *) 173let contains_summary events summary = 174 List.exists 175 (fun e -> String.equal (Option.get @@ Event.get_summary e) summary) 176 events 177 178let test_case_insensitive_search ~fs () = 179 (* Test lowercase query for an uppercase word *) 180 let lowercase_filter = Query.summary_contains "important" in 181 let matches = 182 List.filter 183 (fun e -> Query.matches_filter e lowercase_filter) 184 (test_events ~fs) 185 in 186 Alcotest.(check bool) 187 "Lowercase query should match uppercase text in summary" true 188 (contains_summary matches "IMPORTANT Meeting"); 189 (* Test uppercase query for a lowercase word *) 190 let uppercase_filter = Query.description_contains "WEEKLY" in 191 let matches = 192 List.filter 193 (fun e -> Query.matches_filter e uppercase_filter) 194 (test_events ~fs) 195 in 196 Alcotest.(check bool) 197 "Uppercase query should match lowercase text in description" true 198 (contains_summary matches "Project Meeting") 199 200let test_partial_word_matching ~fs () = 201 (* Test searching for part of a word *) 202 let partial_filter = Query.summary_contains "Conf" in 203 (* Should match "Conference" *) 204 let matches = 205 List.filter 206 (fun e -> Query.matches_filter e partial_filter) 207 (test_events ~fs) 208 in 209 Alcotest.(check bool) 210 "Partial query should match full word in summary" true 211 (contains_summary matches "Conference Call"); 212 (* Test another partial word in description *) 213 let partial_filter = Query.description_contains "nation" in 214 (* Should match "International" *) 215 let matches = 216 List.filter 217 (fun e -> Query.matches_filter e partial_filter) 218 (test_events ~fs) 219 in 220 Alcotest.(check bool) 221 "Partial query should match within word in description" true 222 (contains_summary matches "Conference Call"); 223 224 Alcotest.(check bool) 225 "Partial query should match within word in description" true 226 (contains_summary matches "Conference Call") 227 228let test_boolean_logic ~fs () = 229 (* Test AND filter *) 230 let and_filter = 231 Query.and_filter 232 [ Query.summary_contains "Meeting"; Query.description_contains "project" ] 233 in 234 let matches = 235 List.filter (fun e -> Query.matches_filter e and_filter) (test_events ~fs) 236 in 237 Alcotest.(check int) 238 "AND filter should match events with both terms" 2 239 (* Two events have both "Meeting" in summary and "project" in description *) 240 (List.length matches); 241 (* Test OR filter *) 242 let or_filter = 243 Query.or_filter 244 [ Query.summary_contains "Workshop"; Query.summary_contains "Conference" ] 245 in 246 let matches = 247 List.filter (fun e -> Query.matches_filter e or_filter) (test_events ~fs) 248 in 249 Alcotest.(check int) 250 "OR filter should match events with either term" 251 2 (* One event has "Workshop", one has "Conference" *) 252 (List.length matches); 253 254 (* Test NOT filter *) 255 let not_filter = Query.not_filter (Query.summary_contains "Meeting") in 256 let matches = 257 List.filter (fun e -> Query.matches_filter e not_filter) (test_events ~fs) 258 in 259 Alcotest.(check int) 260 "NOT filter should match events without the term" 261 2 (* Two events don't have "Meeting" in the summary *) 262 (List.length matches); 263 (* Test complex combination: (Meeting AND project) OR Workshop BUT NOT Conference *) 264 let complex_filter = 265 Query.and_filter 266 [ 267 Query.or_filter 268 [ 269 Query.and_filter 270 [ 271 Query.summary_contains "Meeting"; 272 Query.description_contains "project"; 273 ]; 274 Query.summary_contains "Workshop"; 275 ]; 276 Query.not_filter (Query.summary_contains "Conference"); 277 ] 278 in 279 let matches = 280 List.filter 281 (fun e -> Query.matches_filter e complex_filter) 282 (test_events ~fs) 283 in 284 Alcotest.(check int) 285 "Complex filter should match correctly" 286 3 (* Three events should match the complex criteria *) 287 (List.length matches) 288 289let test_cross_field_search ~fs () = 290 (* Search for a term that appears in multiple fields across different events *) 291 let term_filter = 292 Query.or_filter 293 [ 294 Query.summary_contains "meeting"; 295 Query.description_contains "meeting"; 296 Query.location_contains "meeting"; 297 ] 298 in 299 let matches = 300 List.filter (fun e -> Query.matches_filter e term_filter) (test_events ~fs) 301 in 302 Alcotest.(check int) 303 "Cross-field search should find all occurrences" 304 3 (* "meeting" appears in 3 events across different fields *) 305 (List.length matches); 306 (* Another test with a different term *) 307 let term_filter = 308 Query.or_filter 309 [ 310 Query.summary_contains "conference"; 311 Query.description_contains "conference"; 312 Query.location_contains "conference"; 313 ] 314 in 315 let matches = 316 List.filter (fun e -> Query.matches_filter e term_filter) (test_events ~fs) 317 in 318 Alcotest.(check int) 319 "Cross-field search should find all occurrences of 'conference'" 320 2 (* "conference" appears in 2 events across different fields *) 321 (List.length matches) 322 323let query_tests fs = 324 [ 325 ("query all events", `Quick, test_query_all ~fs); 326 ("recurrence expansion", `Quick, test_recurrence_expansion ~fs); 327 ("text search", `Quick, test_text_search ~fs); 328 ("calendar filter", `Quick, test_calendar_filter ~fs); 329 ("case insensitive search", `Quick, test_case_insensitive_search ~fs); 330 ("partial word matching", `Quick, test_partial_word_matching ~fs); 331 ("boolean logic filters", `Quick, test_boolean_logic ~fs); 332 ("cross-field searching", `Quick, test_cross_field_search ~fs); 333 ] 334 335let () = 336 Eio_main.run @@ fun env -> 337 let fs = Eio.Stdenv.fs env in 338 let _ = setup_fixed_date () in 339 Alcotest.run "Query Tests" [ ("query", query_tests fs) ]