Skip to content

Commit b761767

Browse files
petercorkeclaude
andcommitted
tests: cover near without an index, the guards, and the pickle markers
near() with SE2Metric and with a metric of your own, within() with no coordinates, closest() skipping a vertex without one, the index refusing a graph whose coordinates are all NaN, a query that is not finite or the wrong length. The pickle round trips are routed through one helper marked for the scanners. An unreachable guard in near() is an assertion. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent caec1ab commit b761767

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

‎src/pgraph/PGraph.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,8 +2300,7 @@ def near(self, coord: ArrayLike, radius: float) -> list[tuple[Vertex, float]]:
23002300

23012301
found = []
23022302
for vertex in candidates:
2303-
if vertex.coord is None:
2304-
continue
2303+
assert vertex.coord is not None # the candidates all have a position
23052304
dist = float(self.metric(vertex.coord - coord))
23062305
if dist <= radius:
23072306
found.append((vertex, dist))

‎tests/test_spatial_index.py‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import importlib
1515
import importlib.util
1616
import pathlib
17-
import pickle # nosec B403 - only used to round-trip graphs made in this file
17+
import pickle # nosec B403 nosemgrep - only used to round-trip graphs made in this file
1818
import unittest
1919
import unittest.mock
2020

@@ -26,6 +26,11 @@
2626
HAVE_SCIPY = importlib.util.find_spec("scipy") is not None
2727

2828

29+
def roundtrip(obj):
30+
"""Pickle and unpickle: these tests only read what they have just written"""
31+
return pickle.loads(pickle.dumps(obj)) # nosec B301 nosemgrep
32+
33+
2934
def points_graph(cls=UGraph, n=200, dim=2, seed=0, **kwargs):
3035
rng = np.random.default_rng(seed)
3136
g = cls(dim=dim, **kwargs)
@@ -228,7 +233,7 @@ def test_a_coordinate_changed_in_place_is_not_seen(self):
228233

229234
def test_not_in_copies_or_pickles(self):
230235
g = self.graph()
231-
for h in (g.copy(), copy.deepcopy(g), pickle.loads(pickle.dumps(g)), g.subgraph(g)): # nosec B301
236+
for h in (g.copy(), copy.deepcopy(g), roundtrip(g), g.subgraph(g)):
232237
self.assertIsNone(h._kdtree)
233238
self.assertEqual(h.closest([9.0, 0.0])[0].name, "b")
234239

@@ -252,6 +257,67 @@ def test_no_coordinates(self):
252257
g.build_kdtree()
253258

254259

260+
class TestVectorizedAndLoopPaths(unittest.TestCase):
261+
"""near() without an index: built-in metrics in one operation, others in a loop"""
262+
263+
def test_SE2_without_an_index(self):
264+
for weight in (0.0, 0.5, 2.0):
265+
with self.subTest(weight=weight):
266+
g, P = points_graph(UGraph, dim=3, metric=SE2Metric(weight))
267+
for q in ([0.0, 0.0, 3.0], [4.0, -2.0, -3.0]):
268+
d = se2(P - np.asarray(q), weight)
269+
found = g.near(q, 5.0)
270+
self.assertEqual(
271+
sorted(int(v.name[1:]) for v, _ in found),
272+
sorted(np.flatnonzero(d <= 5.0).tolist()),
273+
)
274+
np.testing.assert_allclose([x for _, x in found], np.sort(d[d <= 5.0]), atol=1e-9)
275+
276+
def test_metric_of_your_own(self):
277+
g = UGraph(dim=2, metric=lambda v: float(np.abs(v).max())) # Chebyshev
278+
for name, c in [("a", (0, 0)), ("b", (2, 2)), ("c", (3, 0)), ("d", (0, 5))]:
279+
g.add_vertex(list(c), name=name)
280+
g.add_vertex(name="nowhere")
281+
found = g.near([0.0, 0.0], 3.0)
282+
self.assertEqual([(v.name, d) for v, d in found], [("a", 0.0), ("b", 2.0), ("c", 3.0)])
283+
284+
def test_vertices_without_coordinates(self):
285+
g = UGraph()
286+
g.add_vertex(name="nowhere")
287+
g.add_vertex(name="nowhere else")
288+
self.assertEqual(g.within([[0, 0], [1, 1]]), [])
289+
self.assertEqual(g.near([0, 0], 5), [])
290+
291+
def test_closest_skips_vertices_without_coordinates(self):
292+
g = UGraph(dim=2)
293+
g.add_vertex(name="nowhere")
294+
g.add_vertex([3.0, 4.0], name="a")
295+
vertex, d = g.closest([0.0, 0.0]) # no index: the loop
296+
self.assertEqual((vertex.name, d), ("a", 5.0))
297+
298+
299+
@unittest.skipUnless(HAVE_SCIPY, "scipy is not installed")
300+
class TestIndexGuards(unittest.TestCase):
301+
def test_a_query_that_is_not_finite_falls_back(self):
302+
g, _ = points_graph(UGraph, n=20)
303+
g.build_kdtree()
304+
vertex, d = g.closest([np.nan, 0.0]) # the loop: nothing is nearer than nan
305+
self.assertIsNone(vertex)
306+
self.assertEqual(d, np.inf)
307+
308+
def test_a_query_of_the_wrong_length_with_SE2(self):
309+
g, _ = points_graph(UGraph, n=20, dim=3, metric="SE2")
310+
g.build_kdtree()
311+
with self.assertRaises(ValueError): # the metric itself says so
312+
g.closest([0.0, 0.0])
313+
314+
def test_coordinates_that_are_none_finite(self):
315+
g = UGraph(dim=2)
316+
g.add_vertex([np.nan, np.nan])
317+
with self.assertRaises(ValueError):
318+
g.build_kdtree()
319+
320+
255321
class TestScipyIsOnlyImportedOnDemand(unittest.TestCase):
256322
def test_no_module_of_pgraph_imports_scipy_at_import_time(self):
257323
# spatialmath, which pgraph depends on, imports scipy itself, so

‎tests/test_subgraph.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
Expectations are written out by hand.
77
"""
88

9-
import pickle # nosec B403 - only used to round-trip graphs made in this file
9+
import pickle # nosec B403 nosemgrep - only used to round-trip graphs made in this file
1010
import unittest
1111

1212
import numpy as np
1313

1414
from pgraph import DGraph, Edge, UGraph, Vertex
1515

1616

17+
def roundtrip(obj):
18+
"""Pickle and unpickle: these tests only read what they have just written"""
19+
return pickle.loads(pickle.dumps(obj)) # nosec B301 nosemgrep
20+
21+
1722
def line(cls=UGraph, n=5, **kwargs):
1823
"""Vertices v0 .. v(n-1) at (0, 0), (1, 0), ... with no edges"""
1924
g = cls(dim=2, **kwargs)
@@ -284,7 +289,7 @@ def test_with_within(self):
284289
def test_can_be_pickled_and_modified(self):
285290
g, v = self.graph()
286291
h = g.subgraph([v[1], v[2], v[3]])
287-
h2 = pickle.loads(pickle.dumps(h)) # nosec B301
292+
h2 = roundtrip(h)
288293
self.assertEqual(self.description(h2), self.description(h))
289294
h.add_edge(h["v1"], h["v3"])
290295
self.assertEqual(h.ne, 5)

0 commit comments

Comments
 (0)