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