@@ -73,8 +73,8 @@ def _fake_validator(self, files, external_exts=None, valid=True, error="folder i
7373
7474 # ---------------- tests ----------------
7575
76- def test_missing_u_id_logged_and_no_keyerror (self ):
77- """Edges missing `_u_id` should log a friendly error instead of raising KeyError."""
76+ def test_missing_u_id_reports_error_without_keyerror (self ):
77+ """Edges missing `_u_id` should report a friendly error instead of raising KeyError."""
7878 fake_files = ["/tmp/nodes.geojson" , "/tmp/edges.geojson" ]
7979 nodes = self ._gdf_nodes ([1 , 2 ])
8080 # edges WITHOUT _u_id; include _id to bypass duplicated('_id') KeyError
@@ -175,7 +175,7 @@ def _rf(path):
175175 shown_ids = [x .strip () for x in displayed .split ("," )]
176176 self .assertLessEqual (len (shown_ids ), 20 )
177177
178- def test_load_osw_file_logs_json_decode_error (self ):
178+ def test_load_osw_file_reports_json_decode_error (self ):
179179 """Invalid JSON should surface a detailed message with location context."""
180180 validator = OSWValidation (zipfile_path = "dummy.zip" )
181181 with tempfile .NamedTemporaryFile ("w" , suffix = ".geojson" , delete = False ) as tmp :
@@ -200,7 +200,7 @@ def test_load_osw_file_logs_json_decode_error(self):
200200 self .assertIsNone (issue ["feature_index" ])
201201 self .assertEqual (issue ["error_message" ], message )
202202
203- def test_load_osw_file_logs_os_error (self ):
203+ def test_load_osw_file_reports_os_error (self ):
204204 """Missing files should log a readable OS error message."""
205205 validator = OSWValidation (zipfile_path = "dummy.zip" )
206206 missing_path = os .path .join (tempfile .gettempdir (), "nonexistent_osw_file.geojson" )
@@ -252,7 +252,7 @@ def test_validate_reports_json_decode_error(self):
252252 finally :
253253 os .unlink (bad_path )
254254
255- def test_validate_logs_read_file_exception (self ):
255+ def test_validate_reports_read_file_exception (self ):
256256 """GeoDataFrame read failures are logged and do not crash."""
257257 fake_files = ["/tmp/edges.geojson" ]
258258
@@ -274,8 +274,8 @@ def test_validate_logs_read_file_exception(self):
274274 self .assertTrue (any ("Failed to read 'edges.geojson' as GeoJSON: boom" in e for e in (res .errors or [])),
275275 f"Errors were: { res .errors } " )
276276
277- def test_missing_w_id_logs_error (self ):
278- """Zones missing _w_id should log a clear message."""
277+ def test_missing_w_id_reports_error (self ):
278+ """Zones missing _w_id should report a clear message."""
279279 fake_files = ["/tmp/nodes.geojson" , "/tmp/zones.geojson" ]
280280 nodes = self ._gdf_nodes ([1 , 2 ])
281281 # zones without _w_id column
@@ -310,8 +310,8 @@ def _rf(path):
310310 self .assertTrue (any ("Missing required column '_w_id' in zones." in e for e in (res .errors or [])),
311311 f"Errors were: { res .errors } " )
312312
313- def test_extension_read_failure_is_logged (self ):
314- """Failure reading an extension file should be logged and skipped."""
313+ def test_extension_read_failure_reports_error (self ):
314+ """Failure reading an extension file should be reported and skipped."""
315315 fake_files = ["/tmp/nodes.geojson" ]
316316 nodes = self ._gdf_nodes ([1 ])
317317 ext_path = "/tmp/custom.geojson"
@@ -346,8 +346,8 @@ def _rf(path):
346346 self .assertTrue (any ("Failed to read extension 'custom.geojson' as GeoJSON: boom" in e for e in (res .errors or [])),
347347 f"Errors were: { res .errors } " )
348348
349- def test_extension_invalid_ids_logging_failure (self ):
350- """If invalid extension features exist but id extraction fails, we log gracefully."""
349+ def test_extension_invalid_ids_reports_extraction_failure (self ):
350+ """If invalid extension features exist but id extraction fails, we report gracefully."""
351351 ext_path = "/tmp/custom.geojson"
352352 fake_files = ["/tmp/nodes.geojson" ]
353353 nodes = self ._gdf_nodes ([1 ])
@@ -392,6 +392,94 @@ def _rf(path):
392392 for e in (res .errors or [])),
393393 f"Errors were: { res .errors } " )
394394
395+ def test_extension_invalid_geometries_reported (self ):
396+ """Invalid geometries inside an extension file should surface a clear error."""
397+ ext_path = "/tmp/custom.geojson"
398+ fake_files = ["/tmp/nodes.geojson" ]
399+ nodes = self ._gdf_nodes ([1 ])
400+
401+ invalid_geojson = MagicMock ()
402+ invalid_geojson .__len__ .return_value = 2
403+ invalid_geojson .get .return_value = ["a" , "b" ]
404+
405+ extension_file = MagicMock ()
406+ extension_file .is_valid = [False , False ]
407+ extension_file .__getitem__ .return_value = invalid_geojson
408+ extension_file .drop .return_value = pd .DataFrame ()
409+
410+ with patch (_PATCH_ZIP ) as PZip , \
411+ patch (_PATCH_EV ) as PVal , \
412+ patch (_PATCH_VALIDATE , return_value = True ), \
413+ patch (_PATCH_READ_FILE ) as PRead , \
414+ patch (_PATCH_DATASET_FILES , _CANON_DATASET_FILES ):
415+
416+ z = MagicMock ()
417+ z .extract_zip .return_value = "/tmp/extracted"
418+ z .remove_extracted_files .return_value = None
419+ PZip .return_value = z
420+
421+ val = self ._fake_validator (fake_files , external_exts = [ext_path ])
422+ PVal .return_value = val
423+
424+ def _rf (path ):
425+ b = os .path .basename (path )
426+ if "nodes" in b :
427+ return nodes
428+ if os .path .basename (path ) == os .path .basename (ext_path ):
429+ return extension_file
430+ return gpd .GeoDataFrame ()
431+
432+ PRead .side_effect = _rf
433+
434+ res = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
435+
436+ self .assertFalse (res .is_valid )
437+ self .assertTrue (any ("Invalid geometries found in extension file `custom.geojson`" in e
438+ for e in (res .errors or [])),
439+ f"Errors were: { res .errors } " )
440+
441+ def test_extension_serialization_failure_reported (self ):
442+ """Non-serializable extension properties should be reported."""
443+ ext_path = "/tmp/custom.geojson"
444+ fake_files = ["/tmp/nodes.geojson" ]
445+ nodes = self ._gdf_nodes ([1 ])
446+
447+ extension_file = MagicMock ()
448+ extension_file .is_valid = [True ]
449+ extension_file .__getitem__ .return_value = gpd .GeoDataFrame () # no invalid geometries
450+ extension_file .drop .side_effect = Exception ("serialize boom" )
451+
452+ with patch (_PATCH_ZIP ) as PZip , \
453+ patch (_PATCH_EV ) as PVal , \
454+ patch (_PATCH_VALIDATE , return_value = True ), \
455+ patch (_PATCH_READ_FILE ) as PRead , \
456+ patch (_PATCH_DATASET_FILES , _CANON_DATASET_FILES ):
457+
458+ z = MagicMock ()
459+ z .extract_zip .return_value = "/tmp/extracted"
460+ z .remove_extracted_files .return_value = None
461+ PZip .return_value = z
462+
463+ val = self ._fake_validator (fake_files , external_exts = [ext_path ])
464+ PVal .return_value = val
465+
466+ def _rf (path ):
467+ b = os .path .basename (path )
468+ if "nodes" in b :
469+ return nodes
470+ if os .path .basename (path ) == os .path .basename (ext_path ):
471+ return extension_file
472+ return gpd .GeoDataFrame ()
473+
474+ PRead .side_effect = _rf
475+
476+ res = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
477+
478+ self .assertFalse (res .is_valid )
479+ self .assertTrue (any ("Extension file `custom.geojson` has non-serializable properties: serialize boom" in e
480+ for e in (res .errors or [])),
481+ f"Errors were: { res .errors } " )
482+
395483 def test_duplicate_ids_detection (self ):
396484 """Duplicates inside a single file are reported."""
397485 fake_files = ["/tmp/nodes.geojson" ]
@@ -554,7 +642,7 @@ def test_get_colset_returns_set_when_column_present(self):
554642 s = v ._get_colset (gdf , "_id" , "nodes" )
555643 self .assertEqual (s , {1 , 2 , 3 })
556644
557- def test_get_colset_logs_and_returns_empty_when_missing (self ):
645+ def test_get_colset_reports_missing_column (self ):
558646 v = OSWValidation (zipfile_path = "dummy.zip" )
559647 gdf = self ._gdf ({"foo" : [1 , 2 ]}, geom = "Point" )
560648 s = v ._get_colset (gdf , "_id" , "nodes" )
@@ -575,7 +663,7 @@ def test_get_colset_with_none_gdf(self):
575663 s = v ._get_colset (None , "_id" , "nodes" )
576664 self .assertEqual (s , set ())
577665
578- def test_get_colset_logs_when_stringify_fails (self ):
666+ def test_get_colset_reports_stringify_failure (self ):
579667 class BadObj :
580668 def __hash__ (self ):
581669 raise TypeError ("no hash" )
@@ -590,6 +678,16 @@ def __str__(self):
590678 self .assertTrue (any ("Could not create set for column 'meta' in nodes." in e for e in (v .errors or [])),
591679 f"Errors were: { v .errors } " )
592680
681+ def test_load_osw_schema_reports_missing_file (self ):
682+ v = OSWValidation (zipfile_path = "dummy.zip" )
683+ missing_schema = os .path .join (tempfile .gettempdir (), "missing_schema.json" )
684+ if os .path .exists (missing_schema ):
685+ os .unlink (missing_schema )
686+ with self .assertRaises (Exception ):
687+ v .load_osw_schema (missing_schema )
688+ self .assertTrue (any ("Invalid or missing schema file" in e for e in (v .errors or [])),
689+ f"Errors were: { v .errors } " )
690+
593691 def test_schema_02_rejects_tree_and_custom (self ):
594692 base = {
595693 "$schema" : "https://sidewalks.washington.edu/opensidewalks/0.2/schema.json" ,
@@ -758,6 +856,32 @@ def test_pick_schema_force_single_schema_override(self):
758856 self .assertEqual (v .pick_schema_for_file ("/tmp/my.edges.geojson" , {"features" : []}), force )
759857 self .assertEqual (v .pick_schema_for_file ("/any/path.json" , {"features" : [{"geometry" : {"type" : "Point" }}]}), force )
760858
859+ def test_unexpected_exception_surfaces_unable_to_validate (self ):
860+ """Any unexpected exception should be surfaced via 'Unable to validate'."""
861+ fake_files = ["/tmp/nodes.geojson" ]
862+ with patch (_PATCH_ZIP ) as PZip , \
863+ patch (_PATCH_EV ) as PVal , \
864+ patch (_PATCH_VALIDATE , side_effect = RuntimeError ("boom" )), \
865+ patch (_PATCH_DATASET_FILES , _CANON_DATASET_FILES ):
866+
867+ z = MagicMock ()
868+ z .extract_zip .return_value = "/tmp/extracted"
869+ z .remove_extracted_files .return_value = None
870+ PZip .return_value = z
871+
872+ val = MagicMock ()
873+ val .files = fake_files
874+ val .externalExtensions = []
875+ val .is_valid .return_value = True
876+ val .error = None
877+ PVal .return_value = val
878+
879+ res = OSWValidation (zipfile_path = "dummy.zip" ).validate ()
880+
881+ self .assertFalse (res .is_valid )
882+ self .assertTrue (any ("Unable to validate: boom" in e for e in (res .errors or [])),
883+ f"Errors were: { res .errors } " )
884+
761885
762886class TestOSWValidationInvalidGeometryLogging (unittest .TestCase ):
763887 """Covers the 'invalid geometries' logging branch, including _id-present and _id-missing fallback."""
0 commit comments