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) ]