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 ]