blob: 00432537f22d006d5a0855933e5341270aa2cd91 [file] [log] [blame]
Pablo Galindo2f172d82020-06-01 00:41:14 +01001from itertools import chain
2import graphlib
3import os
4import unittest
5
6from test.support.script_helper import assert_python_ok
7
8class TestTopologicalSort(unittest.TestCase):
9 def _test_graph(self, graph, expected):
10 def static_order_with_groups(ts):
11 ts.prepare()
12 while ts.is_active():
13 nodes = ts.get_ready()
14 for node in nodes:
15 ts.done(node)
16 yield nodes
17
18 ts = graphlib.TopologicalSorter(graph)
19 self.assertEqual(list(static_order_with_groups(ts)), list(expected))
20
21 ts = graphlib.TopologicalSorter(graph)
22 self.assertEqual(list(ts.static_order()), list(chain(*expected)))
23
24 def _assert_cycle(self, graph, cycle):
25 ts = graphlib.TopologicalSorter()
26 for node, dependson in graph.items():
27 ts.add(node, *dependson)
28 try:
29 ts.prepare()
30 except graphlib.CycleError as e:
31 msg, seq = e.args
32 self.assertIn(" ".join(map(str, cycle)), " ".join(map(str, seq * 2)))
33 else:
34 raise
35
36 def test_simple_cases(self):
37 self._test_graph(
38 {2: {11}, 9: {11, 8}, 10: {11, 3}, 11: {7, 5}, 8: {7, 3}},
39 [(3, 5, 7), (11, 8), (2, 10, 9)],
40 )
41
42 self._test_graph({1: {}}, [(1,)])
43
44 self._test_graph(
45 {x: {x + 1} for x in range(10)}, [(x,) for x in range(10, -1, -1)]
46 )
47
48 self._test_graph(
49 {2: {3}, 3: {4}, 4: {5}, 5: {1}, 11: {12}, 12: {13}, 13: {14}, 14: {15}},
50 [(1, 15), (5, 14), (4, 13), (3, 12), (2, 11)],
51 )
52
53 self._test_graph(
54 {
55 0: [1, 2],
56 1: [3],
57 2: [5, 6],
58 3: [4],
59 4: [9],
60 5: [3],
61 6: [7],
62 7: [8],
63 8: [4],
64 9: [],
65 },
66 [(9,), (4,), (3, 8), (1, 5, 7), (6,), (2,), (0,)],
67 )
68
69 self._test_graph({0: [1, 2], 1: [], 2: [3], 3: []}, [(1, 3), (2,), (0,)])
70
71 self._test_graph(
72 {0: [1, 2], 1: [], 2: [3], 3: [], 4: [5], 5: [6], 6: []},
73 [(1, 3, 6), (2, 5), (0, 4)],
74 )
75
76 def test_no_dependencies(self):
77 self._test_graph({1: {2}, 3: {4}, 5: {6}}, [(2, 4, 6), (1, 3, 5)])
78
79 self._test_graph({1: set(), 3: set(), 5: set()}, [(1, 3, 5)])
80
81 def test_the_node_multiple_times(self):
82 # Test same node multiple times in dependencies
83 self._test_graph({1: {2}, 3: {4}, 0: [2, 4, 4, 4, 4, 4]}, [(2, 4), (1, 3, 0)])
84
85 # Test adding the same dependency multiple times
86 ts = graphlib.TopologicalSorter()
87 ts.add(1, 2)
88 ts.add(1, 2)
89 ts.add(1, 2)
90 self.assertEqual([*ts.static_order()], [2, 1])
91
92 def test_graph_with_iterables(self):
93 dependson = (2 * x + 1 for x in range(5))
94 ts = graphlib.TopologicalSorter({0: dependson})
95 self.assertEqual(list(ts.static_order()), [1, 3, 5, 7, 9, 0])
96
97 def test_add_dependencies_for_same_node_incrementally(self):
98 # Test same node multiple times
99 ts = graphlib.TopologicalSorter()
100 ts.add(1, 2)
101 ts.add(1, 3)
102 ts.add(1, 4)
103 ts.add(1, 5)
104
105 ts2 = graphlib.TopologicalSorter({1: {2, 3, 4, 5}})
106 self.assertEqual([*ts.static_order()], [*ts2.static_order()])
107
108 def test_empty(self):
109 self._test_graph({}, [])
110
111 def test_cycle(self):
112 # Self cycle
113 self._assert_cycle({1: {1}}, [1, 1])
114 # Simple cycle
115 self._assert_cycle({1: {2}, 2: {1}}, [1, 2, 1])
116 # Indirect cycle
117 self._assert_cycle({1: {2}, 2: {3}, 3: {1}}, [1, 3, 2, 1])
118 # not all elements involved in a cycle
119 self._assert_cycle({1: {2}, 2: {3}, 3: {1}, 5: {4}, 4: {6}}, [1, 3, 2, 1])
120 # Multiple cycles
121 self._assert_cycle({1: {2}, 2: {1}, 3: {4}, 4: {5}, 6: {7}, 7: {6}}, [1, 2, 1])
122 # Cycle in the middle of the graph
123 self._assert_cycle({1: {2}, 2: {3}, 3: {2, 4}, 4: {5}}, [3, 2])
124
125 def test_calls_before_prepare(self):
126 ts = graphlib.TopologicalSorter()
127
128 with self.assertRaisesRegex(ValueError, r"prepare\(\) must be called first"):
129 ts.get_ready()
130 with self.assertRaisesRegex(ValueError, r"prepare\(\) must be called first"):
131 ts.done(3)
132 with self.assertRaisesRegex(ValueError, r"prepare\(\) must be called first"):
133 ts.is_active()
134
135 def test_prepare_multiple_times(self):
136 ts = graphlib.TopologicalSorter()
137 ts.prepare()
138 with self.assertRaisesRegex(ValueError, r"cannot prepare\(\) more than once"):
139 ts.prepare()
140
141 def test_invalid_nodes_in_done(self):
142 ts = graphlib.TopologicalSorter()
143 ts.add(1, 2, 3, 4)
144 ts.add(2, 3, 4)
145 ts.prepare()
146 ts.get_ready()
147
148 with self.assertRaisesRegex(ValueError, "node 2 was not passed out"):
149 ts.done(2)
150 with self.assertRaisesRegex(ValueError, r"node 24 was not added using add\(\)"):
151 ts.done(24)
152
153 def test_done(self):
154 ts = graphlib.TopologicalSorter()
155 ts.add(1, 2, 3, 4)
156 ts.add(2, 3)
157 ts.prepare()
158
159 self.assertEqual(ts.get_ready(), (3, 4))
160 # If we don't mark anything as done, get_ready() returns nothing
161 self.assertEqual(ts.get_ready(), ())
162 ts.done(3)
163 # Now 2 becomes available as 3 is done
164 self.assertEqual(ts.get_ready(), (2,))
165 self.assertEqual(ts.get_ready(), ())
166 ts.done(4)
167 ts.done(2)
168 # Only 1 is missing
169 self.assertEqual(ts.get_ready(), (1,))
170 self.assertEqual(ts.get_ready(), ())
171 ts.done(1)
172 self.assertEqual(ts.get_ready(), ())
173 self.assertFalse(ts.is_active())
174
175 def test_is_active(self):
176 ts = graphlib.TopologicalSorter()
177 ts.add(1, 2)
178 ts.prepare()
179
180 self.assertTrue(ts.is_active())
181 self.assertEqual(ts.get_ready(), (2,))
182 self.assertTrue(ts.is_active())
183 ts.done(2)
184 self.assertTrue(ts.is_active())
185 self.assertEqual(ts.get_ready(), (1,))
186 self.assertTrue(ts.is_active())
187 ts.done(1)
188 self.assertFalse(ts.is_active())
189
190 def test_not_hashable_nodes(self):
191 ts = graphlib.TopologicalSorter()
192 self.assertRaises(TypeError, ts.add, dict(), 1)
193 self.assertRaises(TypeError, ts.add, 1, dict())
194 self.assertRaises(TypeError, ts.add, dict(), dict())
195
196 def test_order_of_insertion_does_not_matter_between_groups(self):
197 def get_groups(ts):
198 ts.prepare()
199 while ts.is_active():
200 nodes = ts.get_ready()
201 ts.done(*nodes)
202 yield set(nodes)
203
204 ts = graphlib.TopologicalSorter()
205 ts.add(3, 2, 1)
206 ts.add(1, 0)
207 ts.add(4, 5)
208 ts.add(6, 7)
209 ts.add(4, 7)
210
211 ts2 = graphlib.TopologicalSorter()
212 ts2.add(1, 0)
213 ts2.add(3, 2, 1)
214 ts2.add(4, 7)
215 ts2.add(6, 7)
216 ts2.add(4, 5)
217
218 self.assertEqual(list(get_groups(ts)), list(get_groups(ts2)))
219
220 def test_static_order_does_not_change_with_the_hash_seed(self):
221 def check_order_with_hash_seed(seed):
222 code = """if 1:
223 import graphlib
224 ts = graphlib.TopologicalSorter()
225 ts.add('blech', 'bluch', 'hola')
226 ts.add('abcd', 'blech', 'bluch', 'a', 'b')
227 ts.add('a', 'a string', 'something', 'b')
228 ts.add('bluch', 'hola', 'abcde', 'a', 'b')
229 print(list(ts.static_order()))
230 """
231 env = os.environ.copy()
232 # signal to assert_python not to do a copy
233 # of os.environ on its own
234 env["__cleanenv"] = True
235 env["PYTHONHASHSEED"] = str(seed)
236 out = assert_python_ok("-c", code, **env)
237 return out
238
239 run1 = check_order_with_hash_seed(1234)
240 run2 = check_order_with_hash_seed(31415)
241
242 self.assertNotEqual(run1, "")
243 self.assertNotEqual(run2, "")
244 self.assertEqual(run1, run2)