GPS Exchange Format library/CLI in OCaml
1(** Alcotest suite comparing Unix and Eio implementations *) 2 3open Alcotest 4 5let test_data_dir = "test/data" 6 7let test_files = [ 8 "simple_waypoints.gpx"; 9 "detailed_waypoints.gpx"; 10 "simple_route.gpx"; 11 "simple_track.gpx"; 12 "multi_segment_track.gpx"; 13 "comprehensive.gpx"; 14 "minimal.gpx"; 15 "edge_cases.gpx"; 16] 17 18(** Helper to compare GPX documents *) 19let compare_gpx_basic gpx1 gpx2 = 20 let open Gpx in 21 gpx1.creator = gpx2.creator && 22 List.length gpx1.waypoints = List.length gpx2.waypoints && 23 List.length gpx1.routes = List.length gpx2.routes && 24 List.length gpx1.tracks = List.length gpx2.tracks 25 26(** Test Unix implementation can read all test files *) 27let test_unix_parsing filename () = 28 let path = Filename.concat test_data_dir filename in 29 match Gpx_unix.read path with 30 | Ok gpx -> 31 let validation = Gpx.validate_gpx gpx in 32 check bool "GPX is valid" true validation.is_valid; 33 check bool "Has some content" true ( 34 List.length gpx.waypoints > 0 || 35 List.length gpx.routes > 0 || 36 List.length gpx.tracks > 0 37 ) 38 | Error err -> 39 failf "Unix parsing failed for %s: %s" filename 40 (match err with 41 | Gpx.Invalid_xml s -> "Invalid XML: " ^ s 42 | Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s 43 | Gpx.Missing_required_attribute (elem, attr) -> 44 Printf.sprintf "Missing attribute %s in %s" attr elem 45 | Gpx.Missing_required_element s -> "Missing element: " ^ s 46 | Gpx.Validation_error s -> "Validation error: " ^ s 47 | Gpx.Xml_error s -> "XML error: " ^ s 48 | Gpx.IO_error s -> "I/O error: " ^ s) 49 50(** Test Eio implementation can read all test files *) 51let test_eio_parsing filename () = 52 Eio_main.run @@ fun env -> 53 let fs = Eio.Stdenv.fs env in 54 let path = Filename.concat test_data_dir filename in 55 try 56 let gpx = Gpx_eio.read ~fs path in 57 let validation = Gpx.validate_gpx gpx in 58 check bool "GPX is valid" true validation.is_valid; 59 check bool "Has some content" true ( 60 List.length gpx.waypoints > 0 || 61 List.length gpx.routes > 0 || 62 List.length gpx.tracks > 0 63 ) 64 with 65 | Gpx.Gpx_error err -> 66 failf "Eio parsing failed for %s: %s" filename 67 (match err with 68 | Gpx.Invalid_xml s -> "Invalid XML: " ^ s 69 | Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s 70 | Gpx.Missing_required_attribute (elem, attr) -> 71 Printf.sprintf "Missing attribute %s in %s" attr elem 72 | Gpx.Missing_required_element s -> "Missing element: " ^ s 73 | Gpx.Validation_error s -> "Validation error: " ^ s 74 | Gpx.Xml_error s -> "XML error: " ^ s 75 | Gpx.IO_error s -> "I/O error: " ^ s) 76 77(** Test Unix and Eio implementations produce equivalent results *) 78let test_unix_eio_equivalence filename () = 79 let path = Filename.concat test_data_dir filename in 80 81 (* Parse with Unix *) 82 let unix_result = Gpx_unix.read path in 83 84 (* Parse with Eio *) 85 let eio_result = 86 try 87 Eio_main.run @@ fun env -> 88 let fs = Eio.Stdenv.fs env in 89 Ok (Gpx_eio.read ~fs path) 90 with 91 | Gpx.Gpx_error err -> Error err 92 in 93 94 match unix_result, eio_result with 95 | Ok gpx_unix, Ok gpx_eio -> 96 check bool "Unix and Eio produce equivalent results" true 97 (compare_gpx_basic gpx_unix gpx_eio); 98 check string "Creators match" gpx_unix.creator gpx_eio.creator; 99 check int "Waypoint counts match" 100 (List.length gpx_unix.waypoints) (List.length gpx_eio.waypoints); 101 check int "Route counts match" 102 (List.length gpx_unix.routes) (List.length gpx_eio.routes); 103 check int "Track counts match" 104 (List.length gpx_unix.tracks) (List.length gpx_eio.tracks) 105 | Error _, Error _ -> 106 (* Both failed - that's consistent *) 107 check bool "Both Unix and Eio failed consistently" true true 108 | Ok _, Error _ -> 109 failf "Unix succeeded but Eio failed for %s" filename 110 | Error _, Ok _ -> 111 failf "Eio succeeded but Unix failed for %s" filename 112 113(** Test write-read round-trip with Unix *) 114let test_unix_round_trip filename () = 115 let path = Filename.concat test_data_dir filename in 116 match Gpx_unix.read path with 117 | Ok gpx_original -> 118 (* Write to temporary string *) 119 (match Gpx.write_string gpx_original with 120 | Ok xml_string -> 121 (* Parse the written string *) 122 (match Gpx.parse_string xml_string with 123 | Ok gpx_roundtrip -> 124 check bool "Round-trip preserves basic structure" true 125 (compare_gpx_basic gpx_original gpx_roundtrip); 126 check string "Creator preserved" 127 gpx_original.creator gpx_roundtrip.creator 128 | Error _ -> 129 failf "Round-trip parse failed for %s" filename) 130 | Error _ -> 131 failf "Round-trip write failed for %s" filename) 132 | Error _ -> 133 failf "Initial read failed for %s" filename 134 135(** Test write-read round-trip with Eio *) 136let test_eio_round_trip filename () = 137 Eio_main.run @@ fun env -> 138 let fs = Eio.Stdenv.fs env in 139 let path = Filename.concat test_data_dir filename in 140 try 141 let gpx_original = Gpx_eio.read ~fs path in 142 (* Write to temporary string via GPX core *) 143 match Gpx.write_string gpx_original with 144 | Ok xml_string -> 145 (* Parse the written string *) 146 (match Gpx.parse_string xml_string with 147 | Ok gpx_roundtrip -> 148 check bool "Round-trip preserves basic structure" true 149 (compare_gpx_basic gpx_original gpx_roundtrip); 150 check string "Creator preserved" 151 gpx_original.creator gpx_roundtrip.creator 152 | Error _ -> 153 failf "Round-trip parse failed for %s" filename) 154 | Error _ -> 155 failf "Round-trip write failed for %s" filename 156 with 157 | Gpx.Gpx_error _ -> 158 failf "Initial read failed for %s" filename 159 160(** Test validation works on all files *) 161let test_validation filename () = 162 let path = Filename.concat test_data_dir filename in 163 match Gpx_unix.read path with 164 | Ok gpx -> 165 let validation = Gpx.validate_gpx gpx in 166 check bool "Validation runs without error" true true; 167 (* All our test files should be valid *) 168 if filename <> "invalid.gpx" then 169 check bool "Test file is valid" true validation.is_valid 170 | Error _ -> 171 (* Invalid.gpx should fail to parse - this is expected *) 172 if filename = "invalid.gpx" then 173 check bool "Invalid file correctly fails to parse" true true 174 else 175 failf "Could not read %s for validation test" filename 176 177(** Test error handling with invalid file *) 178let test_error_handling () = 179 let path = Filename.concat test_data_dir "invalid.gpx" in 180 181 (* Test Unix error handling *) 182 (match Gpx_unix.read path with 183 | Ok _ -> 184 failf "Unix should have failed to parse invalid.gpx" 185 | Error _ -> 186 check bool "Unix correctly rejects invalid file" true true); 187 188 (* Test Eio error handling *) 189 (try 190 Eio_main.run @@ fun env -> 191 let fs = Eio.Stdenv.fs env in 192 let _ = Gpx_eio.read ~fs path in 193 failf "Eio should have failed to parse invalid.gpx" 194 with 195 | Gpx.Gpx_error _ -> 196 check bool "Eio correctly rejects invalid file" true true) 197 198(** Performance comparison test *) 199let test_performance_comparison filename () = 200 let path = Filename.concat test_data_dir filename in 201 202 (* Time Unix parsing *) 203 let start_unix = Sys.time () in 204 let _ = Gpx_unix.read path in 205 let unix_time = Sys.time () -. start_unix in 206 207 (* Time Eio parsing *) 208 let start_eio = Sys.time () in 209 let _ = Eio_main.run @@ fun env -> 210 let fs = Eio.Stdenv.fs env in 211 try Some (Gpx_eio.read ~fs path) 212 with Gpx.Gpx_error _ -> None 213 in 214 let eio_time = Sys.time () -. start_eio in 215 216 (* Both should complete reasonably quickly (under 1 second for test files) *) 217 check bool "Unix parsing completes quickly" true (unix_time < 1.0); 218 check bool "Eio parsing completes quickly" true (eio_time < 1.0); 219 220 Printf.printf "Performance for %s: Unix=%.3fms, Eio=%.3fms\n" 221 filename (unix_time *. 1000.) (eio_time *. 1000.) 222 223(** Generate test cases for each file *) 224let make_unix_tests () = 225 List.map (fun filename -> 226 test_case filename `Quick (test_unix_parsing filename) 227 ) test_files 228 229let make_eio_tests () = 230 List.map (fun filename -> 231 test_case filename `Quick (test_eio_parsing filename) 232 ) test_files 233 234let make_equivalence_tests () = 235 List.map (fun filename -> 236 test_case filename `Quick (test_unix_eio_equivalence filename) 237 ) test_files 238 239let make_unix_round_trip_tests () = 240 List.map (fun filename -> 241 test_case filename `Quick (test_unix_round_trip filename) 242 ) test_files 243 244let make_eio_round_trip_tests () = 245 List.map (fun filename -> 246 test_case filename `Quick (test_eio_round_trip filename) 247 ) test_files 248 249let make_validation_tests () = 250 List.map (fun filename -> 251 test_case filename `Quick (test_validation filename) 252 ) (test_files @ ["invalid.gpx"]) 253 254let make_performance_tests () = 255 List.map (fun filename -> 256 test_case filename `Quick (test_performance_comparison filename) 257 ) test_files 258 259(** Main test suite *) 260let () = 261 run "GPX Corpus Tests" [ 262 "Unix parsing", make_unix_tests (); 263 "Eio parsing", make_eio_tests (); 264 "Unix vs Eio equivalence", make_equivalence_tests (); 265 "Unix round-trip", make_unix_round_trip_tests (); 266 "Eio round-trip", make_eio_round_trip_tests (); 267 "Validation", make_validation_tests (); 268 "Error handling", [ 269 test_case "invalid file handling" `Quick test_error_handling; 270 ]; 271 "Performance", make_performance_tests (); 272 ]