1+ import json
12import os
3+ import tempfile
24import unittest
35from unittest .mock import patch , MagicMock
46import geopandas as gpd
@@ -172,6 +174,83 @@ def _rf(path):
172174 shown_ids = [x .strip () for x in displayed .split ("," )]
173175 self .assertLessEqual (len (shown_ids ), 20 )
174176
177+ def test_load_osw_file_logs_json_decode_error (self ):
178+ """Invalid JSON should surface a detailed message with location context."""
179+ validator = OSWValidation (zipfile_path = "dummy.zip" )
180+ with tempfile .NamedTemporaryFile ("w" , suffix = ".geojson" , delete = False ) as tmp :
181+ tmp .write ('{"features": [1, }' )
182+ bad_path = tmp .name
183+ try :
184+ with self .assertRaises (json .JSONDecodeError ):
185+ validator .load_osw_file (bad_path )
186+ finally :
187+ os .unlink (bad_path )
188+
189+ self .assertTrue (any ("Failed to parse" in e for e in (validator .errors or [])),
190+ f"Errors were: { validator .errors } " )
191+ message = validator .errors [- 1 ]
192+ basename = os .path .basename (bad_path )
193+ self .assertIn (basename , message )
194+ self .assertIn ("line" , message )
195+ self .assertIn ("column" , message )
196+
197+ issue = validator .issues [- 1 ]
198+ self .assertEqual (issue ["filename" ], basename )
199+ self .assertIsNone (issue ["feature_index" ])
200+ self .assertEqual (issue ["error_message" ], message )
201+
202+ def test_load_osw_file_logs_os_error (self ):
203+ """Missing files should log a readable OS error message."""
204+ validator = OSWValidation (zipfile_path = "dummy.zip" )
205+ missing_path = os .path .join (tempfile .gettempdir (), "nonexistent_osw_file.geojson" )
206+ if os .path .exists (missing_path ):
207+ os .unlink (missing_path )
208+
209+ with self .assertRaises (OSError ):
210+ validator .load_osw_file (missing_path )
211+
212+ self .assertTrue (any ("Unable to read file" in e for e in (validator .errors or [])),
213+ f"Errors were: { validator .errors } " )
214+ message = validator .errors [- 1 ]
215+ basename = os .path .basename (missing_path )
216+ self .assertIn (basename , message )
217+ self .assertIn ("Unable to read file" , message )
218+
219+ issue = validator .issues [- 1 ]
220+ self .assertEqual (issue ["filename" ], basename )
221+ self .assertIsNone (issue ["feature_index" ])
222+ self .assertEqual (issue ["error_message" ], message )
223+
224+ def test_validate_reports_json_decode_error (self ):
225+ """Full validation flow should surface parse errors before schema checks."""
226+ with tempfile .NamedTemporaryFile ("w" , suffix = ".geojson" , delete = False ) as tmp :
227+ tmp .write ('{"type": "FeatureCollection", "features": [1, }' )
228+ bad_path = tmp .name
229+
230+ try :
231+ with patch (_PATCH_ZIP ) as PZip , patch (_PATCH_EV ) as PVal :
232+ z = MagicMock ()
233+ z .extract_zip .return_value = "/tmp/extracted"
234+ z .remove_extracted_files .return_value = None
235+ PZip .return_value = z
236+
237+ PVal .return_value = self ._fake_validator ([bad_path ])
238+
239+ result = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
240+
241+ self .assertFalse (result .is_valid )
242+ message = next ((e for e in (result .errors or []) if "Failed to parse" in e ), None )
243+ self .assertIsNotNone (message , f"Expected parse error message. Errors: { result .errors } " )
244+ basename = os .path .basename (bad_path )
245+ self .assertIn (basename , message )
246+ self .assertIn ("line" , message )
247+
248+ issue = next ((i for i in (result .issues or []) if i ["filename" ] == basename ), None )
249+ self .assertIsNotNone (issue , f"Issues were: { result .issues } " )
250+ self .assertEqual (issue ["error_message" ], message )
251+ finally :
252+ os .unlink (bad_path )
253+
175254 def test_duplicate_ids_detection (self ):
176255 """Duplicates inside a single file are reported."""
177256 fake_files = ["/tmp/nodes.geojson" ]
@@ -190,7 +269,35 @@ def test_duplicate_ids_detection(self):
190269
191270 res = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
192271 self .assertFalse (res .is_valid )
193- self .assertTrue (any ("Duplicate _id's found in nodes" in e for e in (res .errors or [])))
272+ msg = next ((e for e in (res .errors or []) if "Duplicate _id's found in nodes" in e ), None )
273+ self .assertEqual (msg , "Duplicate _id's found in nodes: 2" )
274+
275+ def test_duplicate_ids_detection_is_limited_to_20 (self ):
276+ """Duplicate messages cap the number of displayed IDs."""
277+ fake_files = ["/tmp/nodes.geojson" ]
278+ duplicate_ids = [f"id{ i } " for i in range (25 ) for _ in (0 , 1 )] # 25 unique duplicates
279+ nodes = self ._gdf_nodes (duplicate_ids )
280+
281+ with patch (_PATCH_ZIP ) as PZip , \
282+ patch (_PATCH_EV ) as PVal , \
283+ patch (_PATCH_VALIDATE , return_value = True ), \
284+ patch (_PATCH_READ_FILE , return_value = nodes ), \
285+ patch (_PATCH_DATASET_FILES , _CANON_DATASET_FILES ):
286+ z = MagicMock ()
287+ z .extract_zip .return_value = "/tmp/extracted"
288+ PZip .return_value = z
289+ PVal .return_value = self ._fake_validator (fake_files )
290+
291+ res = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
292+ self .assertFalse (res .is_valid )
293+ msg = next ((e for e in (res .errors or []) if "Duplicate _id's found in nodes" in e ), None )
294+ self .assertIsNotNone (msg , "Expected duplicate-id error not found" )
295+ self .assertIn ("showing first 20 of 25 duplicates" , msg )
296+ displayed = msg .split ("duplicates:" )[- 1 ].strip ()
297+ shown_ids = [part .strip () for part in displayed .split ("," ) if part .strip ()]
298+ self .assertLessEqual (len (shown_ids ), 20 )
299+ expected_ids = [f"id{ i } " for i in range (20 )]
300+ self .assertEqual (shown_ids , expected_ids )
194301
195302 def test_pick_schema_by_geometry_and_by_filename (self ):
196303 """Point/LineString/Polygon ⇒ proper schema; filename fallback when features empty."""
0 commit comments