Skip to content

Commit 007fa90

Browse files
committed
Update tests to reflect recent fixes
1 parent bb1f2f9 commit 007fa90

10 files changed

Lines changed: 749 additions & 21 deletions

tests/test_all_paths.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.test import TestCase
44

55
from tests.helpers import DAGFixtureMixin, TenNodeDAGFixtureMixin
6+
from tests.testapp.models import NetworkNode
67

78

89
class AllPathsFromDAGTestCase(DAGFixtureMixin, TestCase):
@@ -94,3 +95,26 @@ def test_all_paths_root_to_c2(self):
9495
# Only one path: root -> a3 -> b3 -> c2
9596
paths = self.root.all_paths_as_pk_lists(self.c2)
9697
self.assertEqual(len(paths), 1)
98+
99+
100+
class AllUpwardPathsMaxResultsTestCase(TenNodeDAGFixtureMixin, TestCase):
101+
"""Tests for upward all-paths with max_results (AllUpwardPathsQuery)."""
102+
103+
def test_upward_all_paths_max_results(self):
104+
"""Upward all-paths with max_results limits results."""
105+
paths = self.c1.all_paths_as_pk_lists(self.root, directional=False, max_results=1)
106+
self.assertEqual(len(paths), 1)
107+
self.assertEqual(paths[0][0], self.c1.pk)
108+
self.assertEqual(paths[0][-1], self.root.pk)
109+
110+
def test_upward_all_paths_returns_all_without_limit(self):
111+
"""Without max_results, all upward paths are returned."""
112+
paths = self.c1.all_paths_as_pk_lists(self.root, directional=False)
113+
# c1 -> b3 -> a3 -> root AND c1 -> b4 -> a3 -> root
114+
self.assertEqual(len(paths), 2)
115+
116+
def test_upward_all_paths_no_path(self):
117+
"""Upward all-paths returns empty when no path exists."""
118+
island = NetworkNode.objects.create(name="island")
119+
paths = island.all_paths_as_pk_lists(self.root, directional=False)
120+
self.assertEqual(len(paths), 0)

tests/test_connected_graph.py

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class ConnectedComponentsTestCase(DAGFixtureMixin, TestCase):
6767
"""Tests for NodeManager.connected_components()."""
6868

6969
def test_returns_two_components(self):
70-
"""DAGFixtureMixin has a main graph and an island should be 2 components."""
70+
"""DAGFixtureMixin has a main graph and an island - should be 2 components."""
7171
components = NetworkNode.objects.connected_components()
7272
self.assertEqual(len(components), 2)
7373

@@ -82,7 +82,7 @@ def test_each_component_is_queryset(self):
8282
"""Each component should be a standard Django QuerySet (not raw)."""
8383
components = NetworkNode.objects.connected_components()
8484
for component in components:
85-
# Can call .filter() this verifies it's a regular QuerySet
85+
# Can call .filter() - this verifies it's a regular QuerySet
8686
self.assertTrue(component.filter(pk__isnull=False).exists())
8787

8888
def test_empty_graph(self):
@@ -101,20 +101,92 @@ def test_single_island(self):
101101

102102

103103
class ConnectedGraphFilterTestCase(DAGFixtureMixin, TestCase):
104-
"""Tests that ConnectedGraphQuery no-op filter methods are exercised (lines 430-446)."""
104+
"""Tests that ConnectedGraphQuery filter methods work correctly."""
105105

106-
def test_connected_graph_with_all_filter_params(self):
107-
"""Pass all filter kwargs -- ConnectedGraphQuery stubs them all as no-ops."""
108-
edge_set = EdgeSet.objects.create(name="cg_set")
106+
def setUp(self):
107+
super().setUp()
108+
self.edge_set = EdgeSet.objects.create(name="cg_set")
109+
# Assign all edges to edge_set
110+
NetworkEdge.objects.all().update(edge_set=self.edge_set)
111+
112+
def test_connected_graph_disallow_nodes(self):
113+
"""Disallowing a node should exclude it and nodes only reachable through it."""
114+
disallowed = NetworkNode.objects.filter(pk=self.a1.pk)
115+
result = self.root.connected_graph(disallowed_nodes_queryset=disallowed)
116+
result_names = set(result.values_list("name", flat=True))
117+
self.assertNotIn("a1", result_names)
118+
# root, a2, a3 should still be present
119+
self.assertIn("root", result_names)
120+
self.assertIn("a2", result_names)
121+
self.assertIn("a3", result_names)
122+
123+
def test_connected_graph_disallow_edges(self):
124+
"""Disallowing an edge should prevent traversal along it."""
125+
edge_root_a1 = NetworkEdge.objects.get(parent=self.root, child=self.a1)
126+
disallowed = NetworkEdge.objects.filter(pk=edge_root_a1.pk)
127+
result = self.root.connected_graph(disallowed_edges_queryset=disallowed)
128+
result_names = set(result.values_list("name", flat=True))
129+
# a1 and b1 may still be reachable via a2->b1 path, but a1 is only reachable via root->a1
130+
self.assertIn("root", result_names)
131+
132+
def test_connected_graph_allow_nodes(self):
133+
"""Only traverse through allowed nodes."""
134+
allowed = NetworkNode.objects.filter(name__in=["root", "a1", "b1"])
135+
result = self.root.connected_graph(allowed_nodes_queryset=allowed)
136+
result_names = set(result.values_list("name", flat=True))
137+
self.assertIn("root", result_names)
138+
self.assertIn("a1", result_names)
139+
self.assertIn("b1", result_names)
140+
self.assertNotIn("a2", result_names)
141+
self.assertNotIn("a3", result_names)
142+
143+
def test_connected_graph_allow_edges(self):
144+
"""Only traverse along allowed edges."""
145+
edge_root_a1 = NetworkEdge.objects.get(parent=self.root, child=self.a1)
146+
edge_a1_b1 = NetworkEdge.objects.get(parent=self.a1, child=self.b1)
147+
allowed = NetworkEdge.objects.filter(pk__in=[edge_root_a1.pk, edge_a1_b1.pk])
148+
result = self.root.connected_graph(allowed_edges_queryset=allowed)
149+
result_names = set(result.values_list("name", flat=True))
150+
self.assertIn("root", result_names)
151+
self.assertIn("a1", result_names)
152+
self.assertIn("b1", result_names)
153+
self.assertNotIn("a2", result_names)
154+
155+
def test_connected_graph_limiting_edges_set_fk(self):
156+
"""Limiting edges by FK should restrict traversal to those edges."""
157+
other_set = EdgeSet.objects.create(name="other_set")
158+
# Only assign root->a1 and a1->b1 to other_set
159+
NetworkEdge.objects.filter(parent=self.root, child=self.a1).update(edge_set=other_set)
160+
NetworkEdge.objects.filter(parent=self.a1, child=self.b1).update(edge_set=other_set)
161+
result = self.root.connected_graph(limiting_edges_set_fk=other_set)
162+
result_names = set(result.values_list("name", flat=True))
163+
self.assertIn("root", result_names)
164+
self.assertIn("a1", result_names)
165+
self.assertIn("b1", result_names)
166+
self.assertNotIn("a2", result_names)
167+
168+
def test_connected_graph_limiting_nodes_set_fk_noop(self):
169+
"""limiting_nodes_set_fk is still a no-op but should not error."""
109170
node_set = NodeSet.objects.create(name="cg_ns")
110-
result = self.root.connected_graph(
111-
limiting_nodes_set_fk=node_set,
112-
limiting_edges_set_fk=edge_set,
113-
disallowed_nodes_queryset=NetworkNode.objects.filter(pk=self.island.pk),
114-
disallowed_edges_queryset=NetworkEdge.objects.all(),
115-
allowed_nodes_queryset=NetworkNode.objects.all(),
116-
allowed_edges_queryset=NetworkEdge.objects.all(),
117-
)
118-
# All filters are no-ops, so result should include the full connected component
119-
self.assertIn(self.root, result)
120-
self.assertIn(self.a1, result)
171+
result = self.root.connected_graph(limiting_nodes_set_fk=node_set)
172+
# No-op, so all connected nodes should be returned
173+
result_names = set(result.values_list("name", flat=True))
174+
self.assertIn("root", result_names)
175+
self.assertIn("a1", result_names)
176+
177+
def test_connected_graph_max_depth(self):
178+
"""max_depth should limit how far connected_graph traverses."""
179+
# With max_depth=1, from root we should reach immediate neighbors only
180+
result = self.root.connected_graph(max_depth=2)
181+
result_names = set(result.values_list("name", flat=True))
182+
# max_depth=2 means path array can be at most length 2 (root + 1 hop)
183+
self.assertIn("root", result_names)
184+
self.assertIn("a1", result_names)
185+
self.assertIn("a2", result_names)
186+
self.assertIn("a3", result_names)
187+
188+
def test_connected_graph_no_duplicate_rows(self):
189+
"""Ensure the rewritten CTE does not produce duplicate rows."""
190+
result = self.root.connected_graph()
191+
pks = list(result.values_list("pk", flat=True))
192+
self.assertEqual(len(pks), len(set(pks)))

tests/test_depth_annotation.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.test import TestCase
44

55
from tests.helpers import DAGFixtureMixin, TenNodeDAGFixtureMixin
6+
from tests.testapp.models import EdgeSet, NetworkEdge, NetworkNode
67

78

89
class DepthAnnotationFromDAGTestCase(DAGFixtureMixin, TestCase):
@@ -88,3 +89,78 @@ def test_descendants_returns_nodes_not_just_pks(self):
8889
for node, depth in result:
8990
self.assertTrue(hasattr(node, "name"))
9091
self.assertIsInstance(depth, int)
92+
93+
94+
class DepthAnnotationFilterTestCase(DAGFixtureMixin, TestCase):
95+
"""Tests for filters on AncestorDepthQuery and DescendantDepthQuery."""
96+
97+
def setUp(self):
98+
super().setUp()
99+
self.edge_set = EdgeSet.objects.create(name="depth_set")
100+
NetworkEdge.objects.all().update(edge_set=self.edge_set)
101+
102+
def test_descendants_with_depth_disallow_nodes(self):
103+
"""Disallow a1 from root's descendants_with_depth."""
104+
disallowed = NetworkNode.objects.filter(pk=self.a1.pk)
105+
result = self.root.descendants_with_depth(disallowed_nodes_queryset=disallowed)
106+
depth_dict = {node.name: depth for node, depth in result}
107+
self.assertNotIn("a1", depth_dict)
108+
# a2 and a3 should still be present
109+
self.assertIn("a2", depth_dict)
110+
self.assertIn("a3", depth_dict)
111+
112+
def test_ancestors_with_depth_disallow_nodes(self):
113+
"""Disallow a1 from b1's ancestors_with_depth."""
114+
disallowed = NetworkNode.objects.filter(pk=self.a1.pk)
115+
result = self.b1.ancestors_with_depth(disallowed_nodes_queryset=disallowed)
116+
depth_dict = {node.name: depth for node, depth in result}
117+
self.assertNotIn("a1", depth_dict)
118+
# a2 should still be reachable
119+
self.assertIn("a2", depth_dict)
120+
121+
def test_descendants_with_depth_allow_nodes(self):
122+
"""Only allow specific nodes in root's descendants_with_depth."""
123+
allowed = NetworkNode.objects.filter(pk__in=[self.a1.pk, self.b1.pk])
124+
result = self.root.descendants_with_depth(allowed_nodes_queryset=allowed)
125+
depth_dict = {node.name: depth for node, depth in result}
126+
self.assertIn("a1", depth_dict)
127+
self.assertNotIn("a2", depth_dict)
128+
129+
def test_ancestors_with_depth_allow_nodes(self):
130+
"""Only allow specific nodes in b1's ancestors_with_depth."""
131+
allowed = NetworkNode.objects.filter(pk__in=[self.a1.pk, self.root.pk])
132+
result = self.b1.ancestors_with_depth(allowed_nodes_queryset=allowed)
133+
depth_dict = {node.name: depth for node, depth in result}
134+
self.assertNotIn("a2", depth_dict)
135+
136+
def test_descendants_with_depth_limiting_edges_set_fk(self):
137+
"""Limit descendants_with_depth to edges in a specific edge set."""
138+
other_set = EdgeSet.objects.create(name="other_set")
139+
# Only assign root->a1 and a1->b1 to other_set
140+
NetworkEdge.objects.filter(parent=self.root, child=self.a1).update(edge_set=other_set)
141+
NetworkEdge.objects.filter(parent=self.a1, child=self.b1).update(edge_set=other_set)
142+
result = self.root.descendants_with_depth(limiting_edges_set_fk=other_set)
143+
depth_dict = {node.name: depth for node, depth in result}
144+
self.assertIn("a1", depth_dict)
145+
self.assertIn("b1", depth_dict)
146+
self.assertNotIn("a2", depth_dict)
147+
148+
def test_ancestors_with_depth_limiting_edges_set_fk(self):
149+
"""Limit ancestors_with_depth to edges in a specific edge set."""
150+
other_set = EdgeSet.objects.create(name="other_set")
151+
NetworkEdge.objects.filter(parent=self.root, child=self.a1).update(edge_set=other_set)
152+
NetworkEdge.objects.filter(parent=self.a1, child=self.b1).update(edge_set=other_set)
153+
result = self.b1.ancestors_with_depth(limiting_edges_set_fk=other_set)
154+
depth_dict = {node.name: depth for node, depth in result}
155+
self.assertIn("a1", depth_dict)
156+
self.assertIn("root", depth_dict)
157+
self.assertNotIn("a2", depth_dict)
158+
159+
def test_descendants_with_depth_disallow_edges(self):
160+
"""Disallow specific edges in descendants_with_depth."""
161+
edge_root_a1 = NetworkEdge.objects.get(parent=self.root, child=self.a1)
162+
disallowed = NetworkEdge.objects.filter(pk=edge_root_a1.pk)
163+
result = self.root.descendants_with_depth(disallowed_edges_queryset=disallowed)
164+
depth_dict = {node.name: depth for node, depth in result}
165+
self.assertNotIn("a1", depth_dict)
166+
self.assertIn("a2", depth_dict)

tests/test_mutations.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,55 @@ def test_disable_circular_check(self):
220220
self.assertTrue(NetworkEdge.objects.filter(parent=self.n1, child=self.n2).exists())
221221

222222
def test_allow_duplicate_edges_false(self):
223-
"""Should raise ValidationError when duplicate edge exists"""
223+
"""Should raise ValidationError when exact duplicate edge exists"""
224224
self.n1.add_child(self.n2)
225-
with self.assertRaises(ValidationError):
225+
with self.assertRaises(ValidationError) as ctx:
226226
self.n1.add_child(self.n2, allow_duplicate_edges=False)
227+
self.assertEqual(ctx.exception.message, "An edge already exists between these nodes.")
227228

228229
def test_duplicate_edge_allowed_by_default(self):
229230
"""By default, duplicate edges are allowed"""
230231
self.n1.add_child(self.n2)
231232
self.n1.add_child(self.n2)
232233
self.assertEqual(NetworkEdge.objects.filter(parent=self.n1, child=self.n2).count(), 2)
234+
235+
236+
class RedundantEdgeCheckerTestCase(TestCase):
237+
"""Tests for the redundant_edge_checker (transitive reachability check)."""
238+
239+
def setUp(self):
240+
self.n1 = NetworkNode.objects.create(name="n1")
241+
self.n2 = NetworkNode.objects.create(name="n2")
242+
self.n3 = NetworkNode.objects.create(name="n3")
243+
self.n1.add_child(self.n2)
244+
self.n2.add_child(self.n3)
245+
246+
def test_redundant_edge_blocked_when_disabled(self):
247+
"""Adding n1->n3 when n3 is already reachable via n1->n2->n3 should raise."""
248+
with self.assertRaises(ValidationError) as ctx:
249+
self.n1.add_child(self.n3, allow_redundant_edges=False)
250+
self.assertEqual(ctx.exception.message, "The child is already reachable from the parent.")
251+
252+
def test_redundant_edge_allowed_by_default(self):
253+
"""By default, redundant edges are allowed."""
254+
self.n1.add_child(self.n3)
255+
self.assertTrue(NetworkEdge.objects.filter(parent=self.n1, child=self.n3).exists())
256+
257+
def test_duplicate_checker_does_not_block_redundant(self):
258+
"""duplicate_edge_checker should not block n1->n3 (not an exact duplicate)."""
259+
self.n1.add_child(self.n3, allow_duplicate_edges=False)
260+
self.assertTrue(NetworkEdge.objects.filter(parent=self.n1, child=self.n3).exists())
261+
262+
def test_both_checkers_together(self):
263+
"""Both checks can be enabled at the same time."""
264+
# First add n1->n3 (redundant but allowed since we only block duplicates here)
265+
self.n1.add_child(self.n3, allow_duplicate_edges=False)
266+
# Now adding the exact same edge should fail on duplicate check
267+
with self.assertRaises(ValidationError) as ctx:
268+
self.n1.add_child(self.n3, allow_duplicate_edges=False, allow_redundant_edges=False)
269+
self.assertEqual(ctx.exception.message, "An edge already exists between these nodes.")
270+
271+
def test_add_parent_passes_through_allow_redundant_edges(self):
272+
"""add_parent should also support allow_redundant_edges."""
273+
with self.assertRaises(ValidationError):
274+
self.n3.add_parent(self.n1, allow_redundant_edges=False)

0 commit comments

Comments
 (0)