Skip to content

Commit 0b59c90

Browse files
committed
test(sedona-geo-traits-ext): add WKB extension trait tests
test(sedona-geo-traits-ext): add non-trivial LineString tests (rev_lines, is_closed); triangles left pending due to overflow panic fix(LineStringTraitExt::triangles): use saturating_sub for <3 coords and add comprehensive tests test(sedona-geo-traits-ext): split LineString combined test into rev_lines, triangles, and is_closed refactor(tests): switch to let-else pattern in WKB tests for flatter control flow test: expand LineString tests (empty, 2pt, 3pt, 4pt) for rev_lines/is_closed/triangles Fix
1 parent a8a4645 commit 0b59c90

File tree

3 files changed

+306
-1
lines changed

3 files changed

+306
-1
lines changed

rust/sedona-geo-traits-ext/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ num-traits = { version = "0.2", default-features = false, features = ["libm"] }
3434
wkb = "0.9.1"
3535
byteorder = "1"
3636

37+
[dev-dependencies]
38+
wkt = "0.14.0"
39+
3740
[patch.crates-io]
3841
wkb = { git = "https://github.com/georust/wkb.git", rev = "130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a" }

rust/sedona-geo-traits-ext/src/line_string.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ where
8787
&'_ self,
8888
) -> impl ExactSizeIterator<Item = Triangle<<Self as GeometryTrait>::T>> + '_ {
8989
let num_coords = self.num_coords();
90-
(0..num_coords - 2).map(|i| unsafe {
90+
let end = num_coords.saturating_sub(2);
91+
(0..end).map(|i| unsafe {
9192
let coord1 = self.coord_unchecked_ext(i);
9293
let coord2 = self.coord_unchecked_ext(i + 1);
9394
let coord3 = self.coord_unchecked_ext(i + 2);
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Tests for the WKB extension traits implemented in `wkb_ext`.
19+
20+
use geo_traits::{
21+
CoordTrait, GeometryTrait, LineStringTrait, MultiLineStringTrait, MultiPointTrait,
22+
MultiPolygonTrait, PointTrait, PolygonTrait,
23+
};
24+
use sedona_geo_traits_ext::*;
25+
use std::str::FromStr;
26+
use wkb::reader::Wkb;
27+
use wkt::Wkt;
28+
29+
/// Helper to create WKB from WKT string using the wkb writer
30+
fn wkb_from_wkt(wkt_str: &str) -> Vec<u8> {
31+
let geometry = Wkt::<f64>::from_str(wkt_str).unwrap();
32+
let mut buf = Vec::new();
33+
let options = wkb::writer::WriteOptions {
34+
endianness: wkb::Endianness::LittleEndian,
35+
};
36+
wkb::writer::write_geometry(&mut buf, &geometry, &options).unwrap();
37+
buf
38+
}
39+
40+
#[test]
41+
fn test_point_ext() {
42+
let buf = wkb_from_wkt("POINT(1.5 -2.0)");
43+
let wkb = Wkb::try_new(&buf).unwrap();
44+
let geo_traits::GeometryType::Point(p) = wkb.as_type() else {
45+
panic!("expected point")
46+
};
47+
let c = geo_traits::PointTrait::coord(&p).unwrap();
48+
assert_eq!(c.x(), 1.5);
49+
assert_eq!(c.y(), -2.0);
50+
assert_eq!(p.coord_ext().unwrap().geo_coord().x, 1.5);
51+
}
52+
53+
#[test]
54+
fn test_linestring_iterators() {
55+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 1, 2 1.5)");
56+
let wkb = Wkb::try_new(&buf).unwrap();
57+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
58+
panic!("expected linestring")
59+
};
60+
61+
let coords = &[(0.0, 0.0), (1.0, 1.0), (2.0, 1.5)];
62+
let v: Vec<_> = ls.coord_iter().collect();
63+
assert_eq!(v.len(), coords.len());
64+
for (got, (ex_x, ex_y)) in v.iter().zip(coords.iter()) {
65+
assert!((got.x - ex_x).abs() < 1e-9);
66+
assert!((got.y - ex_y).abs() < 1e-9);
67+
}
68+
let segs: Vec<_> = ls.lines().collect();
69+
assert_eq!(segs.len(), coords.len() - 1);
70+
assert_eq!(segs[0].start.x, 0.0);
71+
assert_eq!(segs[0].end.x, 1.0);
72+
}
73+
74+
#[test]
75+
fn test_polygon_ext() {
76+
let buf = wkb_from_wkt("POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))");
77+
let wkb = Wkb::try_new(&buf).unwrap();
78+
let geo_traits::GeometryType::Polygon(p) = wkb.as_type() else {
79+
panic!("expected polygon")
80+
};
81+
assert_eq!(PolygonTrait::num_interiors(&p), 0);
82+
let exterior = PolygonTrait::exterior(&p).unwrap();
83+
assert_eq!(exterior.num_coords(), 5);
84+
}
85+
86+
#[test]
87+
fn test_multi_geometries() {
88+
// MultiPoint
89+
let buf = wkb_from_wkt("MULTIPOINT(1 1, 2 2)");
90+
let wkb = Wkb::try_new(&buf).unwrap();
91+
let geo_traits::GeometryType::MultiPoint(mp) = wkb.as_type() else {
92+
panic!("expected multipoint")
93+
};
94+
assert_eq!(MultiPointTrait::num_points(&mp), 2);
95+
let p0 = geo_traits::MultiPointTrait::point(&mp, 0).unwrap();
96+
assert_eq!(p0.coord().unwrap().x(), 1.0);
97+
98+
// MultiLineString
99+
let buf = wkb_from_wkt("MULTILINESTRING((0 0, 1 0))");
100+
let wkb = Wkb::try_new(&buf).unwrap();
101+
let geo_traits::GeometryType::MultiLineString(mls) = wkb.as_type() else {
102+
panic!("expected multilinestring")
103+
};
104+
assert_eq!(MultiLineStringTrait::num_line_strings(&mls), 1);
105+
let ls0 = geo_traits::MultiLineStringTrait::line_string(&mls, 0).unwrap();
106+
assert_eq!(LineStringTrait::num_coords(&ls0), 2);
107+
108+
// MultiPolygon
109+
let buf = wkb_from_wkt("MULTIPOLYGON(((0 0, 1 0, 0 0)))");
110+
let wkb = Wkb::try_new(&buf).unwrap();
111+
let geo_traits::GeometryType::MultiPolygon(mp) = wkb.as_type() else {
112+
panic!("expected multipolygon")
113+
};
114+
assert_eq!(MultiPolygonTrait::num_polygons(&mp), 1);
115+
let poly0 = geo_traits::MultiPolygonTrait::polygon(&mp, 0).unwrap();
116+
assert_eq!(PolygonTrait::exterior(&poly0).unwrap().num_coords(), 3);
117+
}
118+
119+
#[test]
120+
fn test_geometry_collection_ext() {
121+
let buf = wkb_from_wkt("GEOMETRYCOLLECTION(POINT(0 0), POINT(1 1))");
122+
let wkb = Wkb::try_new(&buf).unwrap();
123+
124+
// GeometryTraitExt is implemented for Wkb in wkb_ext. Use those helpers.
125+
assert!(wkb.is_collection());
126+
assert_eq!(wkb.num_geometries_ext(), 2);
127+
128+
let child0 = wkb.geometry_ext(0).unwrap();
129+
let geo_traits::GeometryType::Point(_) = child0.as_type() else {
130+
panic!("child0 expected point");
131+
};
132+
133+
// Iterate via geometries_ext
134+
let types: Vec<_> = wkb
135+
.geometries_ext()
136+
.map(|g| match g.as_type() {
137+
geo_traits::GeometryType::Point(_) => "P",
138+
_ => "?",
139+
})
140+
.collect();
141+
assert_eq!(types, vec!["P", "P"]);
142+
}
143+
144+
/// Helper to create big-endian WKB linestring (for testing endianness handling)
145+
fn wkb_linestring_be(coords: &[(f64, f64)]) -> Vec<u8> {
146+
let mut b = Vec::new();
147+
b.push(0u8); // Big endian
148+
b.extend_from_slice(&2u32.to_be_bytes());
149+
b.extend_from_slice(&(coords.len() as u32).to_be_bytes());
150+
for (x, y) in coords {
151+
b.extend_from_slice(&x.to_be_bytes());
152+
b.extend_from_slice(&y.to_be_bytes());
153+
}
154+
b
155+
}
156+
157+
#[test]
158+
fn test_linestring_iter_exact_size() {
159+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 2 1, 3 1)");
160+
let wkb = Wkb::try_new(&buf).unwrap();
161+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
162+
panic!("expected linestring")
163+
};
164+
let mut iter = ls.lines();
165+
assert_eq!(iter.len(), 3); // ExactSizeIterator::len (4 coords - 1)
166+
assert!(iter.next().is_some());
167+
}
168+
169+
#[test]
170+
fn test_linestring_rev_lines() {
171+
// Empty linestring
172+
let buf = wkb_from_wkt("LINESTRING EMPTY");
173+
let wkb = Wkb::try_new(&buf).unwrap();
174+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
175+
panic!("expected linestring")
176+
};
177+
assert_eq!(ls.rev_lines().count(), 0);
178+
179+
// Two-point linestring: 1 segment
180+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 1)");
181+
let wkb = Wkb::try_new(&buf).unwrap();
182+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
183+
panic!("expected linestring")
184+
};
185+
let forward: Vec<_> = ls.lines().collect();
186+
let reverse: Vec<_> = ls.rev_lines().collect();
187+
assert_eq!(forward.len(), 1);
188+
assert_eq!(reverse.len(), 1);
189+
assert_eq!(forward[0].start.x, reverse[0].start.x);
190+
assert_eq!(forward[0].end.x, reverse[0].end.x);
191+
192+
// Multi-point linestring: rev_lines should produce segments in reverse order
193+
let buf = wkb_from_wkt("LINESTRING(0 0, 2 0, 2 2, 0 2)");
194+
let wkb = Wkb::try_new(&buf).unwrap();
195+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
196+
panic!("expected linestring")
197+
};
198+
let forward: Vec<_> = ls.lines().collect();
199+
let reverse: Vec<_> = ls.rev_lines().collect();
200+
assert_eq!(forward.len(), 3);
201+
assert_eq!(reverse.len(), 3);
202+
for i in 0..forward.len() {
203+
let f_rev = &forward[forward.len() - 1 - i];
204+
let r = &reverse[i];
205+
assert_eq!(f_rev.start.x, r.start.x);
206+
assert_eq!(f_rev.end.x, r.end.x);
207+
}
208+
}
209+
210+
#[test]
211+
fn test_linestring_is_closed() {
212+
// Empty line string is considered closed
213+
let buf = wkb_from_wkt("LINESTRING EMPTY");
214+
let wkb = Wkb::try_new(&buf).unwrap();
215+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
216+
panic!("expected linestring")
217+
};
218+
assert!(ls.is_closed());
219+
220+
// Non-closed line string
221+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 2 0)");
222+
let wkb = Wkb::try_new(&buf).unwrap();
223+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
224+
panic!("expected linestring")
225+
};
226+
assert!(!ls.is_closed());
227+
228+
// Closed linestring (square ring) with repeated first/last
229+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 1 1, 0 1, 0 0)");
230+
let wkb = Wkb::try_new(&buf).unwrap();
231+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
232+
panic!("expected linestring")
233+
};
234+
assert!(ls.is_closed());
235+
}
236+
237+
#[test]
238+
fn test_linestring_triangles() {
239+
// Empty - no triangles
240+
let buf = wkb_from_wkt("LINESTRING EMPTY");
241+
let wkb = Wkb::try_new(&buf).unwrap();
242+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
243+
panic!("expected linestring")
244+
};
245+
assert_eq!(ls.triangles().count(), 0);
246+
247+
// Two points - no triangles (need at least 3)
248+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 1)");
249+
let wkb = Wkb::try_new(&buf).unwrap();
250+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
251+
panic!("expected linestring")
252+
};
253+
assert_eq!(ls.triangles().count(), 0);
254+
255+
// Three points - one triangle
256+
let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 1 1)");
257+
let wkb = Wkb::try_new(&buf).unwrap();
258+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
259+
panic!("expected linestring")
260+
};
261+
assert_eq!(ls.triangles().count(), 1);
262+
263+
// Four points - two triangles
264+
let buf = wkb_from_wkt("LINESTRING(0 0, 2 0, 2 2, 0 2)");
265+
let wkb = Wkb::try_new(&buf).unwrap();
266+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
267+
panic!("expected linestring")
268+
};
269+
assert_eq!(ls.triangles().count(), 2);
270+
271+
// Single point - degenerate case
272+
let buf = wkb_from_wkt("LINESTRING(5 5)");
273+
let wkb = Wkb::try_new(&buf).unwrap();
274+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
275+
panic!("expected linestring")
276+
};
277+
assert_eq!(ls.triangles().count(), 0);
278+
assert_eq!(ls.lines().len(), 0);
279+
}
280+
281+
#[test]
282+
fn test_linestring_big_endian_and_coord_iter() {
283+
// Big endian variant to exercise EndianCoordIter and EndianLineIter
284+
let coords = &[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)];
285+
let buf = wkb_linestring_be(coords);
286+
let wkb = Wkb::try_new(&buf).unwrap();
287+
let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else {
288+
panic!("expected linestring")
289+
};
290+
291+
let collected: Vec<_> = ls.coord_iter().collect();
292+
assert_eq!(collected.len(), coords.len());
293+
for (c, (ex_x, ex_y)) in collected.iter().zip(coords.iter()) {
294+
assert_eq!((c.x, c.y), (*ex_x, *ex_y));
295+
}
296+
297+
let segs: Vec<_> = ls.lines().collect();
298+
assert_eq!(segs.len(), 2);
299+
assert_eq!(segs[0].start.x, 1.0);
300+
assert_eq!(segs[1].end.x, 3.0);
301+
}

0 commit comments

Comments
 (0)