@@ -147,7 +147,6 @@ def _compare_bit_exact(cpp_result, py_result, label=""):
147147_OPS_NAMES = {
148148 "centroid_point" , "centroid_linestring" , "centroid_polygon" ,
149149 "project_linestring_point" , "interpolate_linestring" ,
150- "interpolate_linestring_normalized" ,
151150 "intersection_area_polygon_polygon" ,
152151 "polygon_exterior" ,
153152 "nearest_points" , "nearest_points_ls_pt" ,
@@ -353,16 +352,12 @@ def _call_ops(api_name, cpp, *args, **kwargs):
353352 c_ls = _build_cpp_geom (cpp , * geoms [0 ])
354353 py_ls = _build_py_geom (* geoms [0 ])
355354 dist = float (extras [0 ]) if extras else 5.0
356- c_result = cpp .interpolate_linestring (c_ls , dist )
357- py_pt = py_ls .interpolate (dist )
358- return np .array (c_result ), np .array ((py_pt .x , py_pt .y ))
359-
360- if api_name == "interpolate_linestring_normalized" :
361- c_ls = _build_cpp_geom (cpp , * geoms [0 ])
362- py_ls = _build_py_geom (* geoms [0 ])
363- t = float (extras [0 ]) if extras else 0.5
364- c_result = cpp .interpolate_linestring_normalized (c_ls , t )
365- py_pt = py_ls .interpolate (t , normalized = True )
355+ normalized = bool (kwargs .get ("normalized" , False ))
356+ if normalized :
357+ c_result = cpp .interpolate_linestring (c_ls , dist , normalized = True )
358+ else :
359+ c_result = cpp .interpolate_linestring (c_ls , dist )
360+ py_pt = py_ls .interpolate (dist , normalized = normalized )
366361 return np .array (c_result ), np .array ((py_pt .x , py_pt .y ))
367362
368363 if api_name == "intersection_area_polygon_polygon" :
@@ -534,6 +529,19 @@ def _call_class_method(api_name, cpp, *args, **kwargs):
534529_I_A = [(0 , 0 ), (5 , 0 ), (5 , 5 ), (0 , 5 )]
535530_I_B = [(3 , 0 ), (8 , 0 ), (8 , 5 ), (3 , 5 )]
536531
532+ # ---- NaN/Inf test data presets ------------------------------------------------
533+
534+ _NAN_1 = [(float ('nan' ), 0 ), (1 , 1 )]
535+ _INF_1 = [(float ('inf' ), 0 ), (1 , 1 )]
536+ _NINF_1 = [(float ('-inf' ), 0 ), (1 , 1 )]
537+ _NAN_PT = (float ('nan' ), 0 )
538+ _INF_PT = (float ('inf' ), 0 )
539+
540+ _MNAN = [(float ('nan' ), 0 ), (10 , 10 ), (20 , 5 )] # multipoint nan
541+ _MLNAN = [[(float ('nan' ), 0 ), (5 , 0 )], [(10 , 0 ), (15 , 0 )]] # multiline nan
542+ _MPNAN = [[(0 , 0 ), (10 , 0 ), (10 , 10 ), (0 , 10 )], # multipoly nan
543+ [(float ('nan' ), 0 ), (20 , 0 ), (20 , 10 ), (10 , 10 )]]
544+
537545
538546# ---- Group 1: factories (7 APIs) --------------------------------------------
539547
@@ -877,7 +885,7 @@ def _catalog_ops():
877885 yield _TestCase ("project_linestring_point" , (ls , pt ), {}, "f64" , tag ,
878886 "scalar_eq" , True , None , "ops" )
879887
880- # -- interpolate_linestring --
888+ # -- interpolate_linestring (absolute distance) --
881889 for ls , dist , tag in [
882890 (("ls" , _L_H ), 5.0 , "h_mid" ),
883891 (("ls" , _L_V ), 5.0 , "v_mid" ),
@@ -888,15 +896,15 @@ def _catalog_ops():
888896 yield _TestCase ("interpolate_linestring" , (ls , dist ), {}, "f64" , tag ,
889897 "bit_exact" , True , None , "ops" )
890898
891- # -- interpolate_linestring_normalized --
892- for ls , t , py_t_expected_tag , tag in [
893- (("ls" , _L_H ), 0.5 , "h_mid" , "h_mid" ), # t=0.5 → midpoint of 10-length horizontal
894- (("ls" , _L_H ), 0.0 , "h_start" , "h_start" ), # t=0.0 → start
895- (("ls" , _L_H ), 1.0 , "h_end" , "h_end" ), # t=1.0 → end
896- (("ls" , _L_V ), 0.5 , "v_mid" , "v_mid" ), # vertical 10-length, t=0.5
897- (("ls" , _L_3 ), 1.0 , "v3pt" , "v3pt_end" ), # 3-point L, t=1.0
899+ # -- interpolate_linestring (normalized) -- same API, kwargs={"normalized": True}
900+ for ls , t , tag in [
901+ (("ls" , _L_H ), 0.5 , "norm_h_mid" ),
902+ (("ls" , _L_H ), 0.0 , "norm_h_start" ),
903+ (("ls" , _L_H ), 1.0 , "norm_h_end" ),
904+ (("ls" , _L_V ), 0.5 , "norm_v_mid" ),
905+ (("ls" , _L_3 ), 1.0 , "norm_v3pt_end" ),
898906 ]:
899- yield _TestCase ("interpolate_linestring_normalized " , (ls , t ), {},
907+ yield _TestCase ("interpolate_linestring " , (ls , t ), {"normalized" : True },
900908 "f64" , tag , "bit_exact" , True , None , "ops" )
901909
902910 # -- Centroid --
@@ -955,6 +963,37 @@ def api_catalog():
955963 yield tc ._replace (group = "predicates" )
956964 for tc in _catalog_ops ():
957965 yield tc ._replace (group = "ops" )
966+ for tc in _catalog_errors ():
967+ yield tc ._replace (group = "errors" )
968+
969+
970+ def _catalog_errors ():
971+ """NaN/Inf rejection: every geometry factory must reject NaN/Inf coords."""
972+ # point
973+ for coords , tag in [(_NAN_PT , "nan" ), (_INF_PT , "inf" )]:
974+ yield _TestCase ("point" , (("pt" , coords ),), {}, "f64" , tag ,
975+ "expect_error" , True , None , "errors" )
976+ # linestring
977+ for coords , tag in [(_NAN_1 , "nan" ), (_INF_1 , "inf" ), (_NINF_1 , "ninf" )]:
978+ yield _TestCase ("linestring" , (coords ,), {}, "f64" , tag ,
979+ "expect_error" , True , None , "errors" )
980+ # polygon
981+ for coords , tag in [(_NAN_1 , "nan" ), (_INF_1 , "inf" )]:
982+ yield _TestCase ("polygon" , (coords ,), {}, "f64" , tag ,
983+ "expect_error" , True , None , "errors" )
984+ # linearring
985+ for coords , tag in [(_NAN_1 , "nan" ), (_INF_1 , "inf" )]:
986+ yield _TestCase ("linearring" , (coords ,), {}, "f64" , tag ,
987+ "expect_error" , True , None , "errors" )
988+ # multipoint
989+ yield _TestCase ("multipoint" , (_MNAN ,), {}, "f64" , "nan" ,
990+ "expect_error" , True , None , "errors" )
991+ # multilinestring
992+ yield _TestCase ("multilinestring" , (_MLNAN ,), {}, "f64" , "nan" ,
993+ "expect_error" , True , None , "errors" )
994+ # multipolygon
995+ yield _TestCase ("multipolygon" , (_MPNAN ,), {}, "f64" , "nan" ,
996+ "expect_error" , True , None , "errors" )
958997
959998
960999# =============================================================================
@@ -1028,6 +1067,28 @@ def test_api(tc, cpp):
10281067 tc .setup_fn (args , kwargs )
10291068 return
10301069
1070+ # Error tests: expect exception from C++
1071+ if strategy == "expect_error" :
1072+ try :
1073+ call_cpp_py (api_name , cpp , * args , ** kwargs )
1074+ # Should have raised — didn't
1075+ raise AssertionError (
1076+ f"[{ api_name } ] expected exception for NaN/Inf input, but no error raised" )
1077+ except Exception as e :
1078+ # Re-raise assertion errors, accept everything else
1079+ if isinstance (e , AssertionError ):
1080+ _REPORT_ROWS .append (dict (
1081+ category = tc .group , api = api_name , mode = _geos_info (cpp ),
1082+ dtype = tc .dtype_label , feature = tc .category ,
1083+ result = "FAIL" , ulp = - 1 ))
1084+ raise
1085+ # Expected: any exception (invalid_argument, runtime_error, etc.)
1086+ _REPORT_ROWS .append (dict (
1087+ category = tc .group , api = api_name , mode = _geos_info (cpp ),
1088+ dtype = tc .dtype_label , feature = tc .category ,
1089+ result = "PASS" , ulp = 0 ))
1090+ return
1091+
10311092 cpp_r , py_r = call_cpp_py (api_name , cpp , * args , ** kwargs )
10321093 if check_py and py_r is None :
10331094 pytest .skip (f"no shapely equivalent for { api_name } " )
0 commit comments