Merge lp:~lifeless/testresources/bug-522338 into lp:~testresources-developers/testresources/trunk
- bug-522338
- Merge into trunk
Proposed by
Robert Collins
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | not available | ||||
Proposed branch: | lp:~lifeless/testresources/bug-522338 | ||||
Merge into: | lp:~testresources-developers/testresources/trunk | ||||
Diff against target: |
598 lines (+447/-34) 5 files modified
NEWS (+8/-0) lib/testresources/__init__.py (+248/-28) lib/testresources/tests/__init__.py (+2/-0) lib/testresources/tests/test_optimising_test_suite.py (+48/-6) lib/testresources/tests/test_resource_graph.py (+141/-0) |
||||
To merge this branch: | bzr merge lp:~lifeless/testresources/bug-522338 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
testresources developers | Pending | ||
Review via email: mp+19684@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2010-02-12 02:01:38 +0000 | |||
3 | +++ NEWS 2010-02-19 05:54:16 +0000 | |||
4 | @@ -13,6 +13,14 @@ | |||
5 | 13 | * Rename TestResource to TestResourceManager leaving TestResource as an alias. | 13 | * Rename TestResource to TestResourceManager leaving TestResource as an alias. |
6 | 14 | Fixes bug #520769. | 14 | Fixes bug #520769. |
7 | 15 | 15 | ||
8 | 16 | * Test sorting no longer performs N! work on tests that can never benefit from | ||
9 | 17 | order optimisations (when resources are not shared an arbitrary order is | ||
10 | 18 | sufficient). Partial fix for bug #522338. | ||
11 | 19 | |||
12 | 20 | * Test sorting now uses a heuristic on each partition to get a sort that is | ||
13 | 21 | no worse than twice the optimal number of setup/teardown operations and is | ||
14 | 22 | fast to calculate. Fixes bug #522338 | ||
15 | 23 | |||
16 | 16 | 0.2.3 | 24 | 0.2.3 |
17 | 17 | ----- | 25 | ----- |
18 | 18 | 26 | ||
19 | 19 | 27 | ||
20 | === modified file 'lib/testresources/__init__.py' | |||
21 | --- lib/testresources/__init__.py 2010-02-12 02:01:38 +0000 | |||
22 | +++ lib/testresources/__init__.py 2010-02-19 05:54:16 +0000 | |||
23 | @@ -19,7 +19,9 @@ | |||
24 | 19 | 19 | ||
25 | 20 | """TestResources: declarative management of external resources for tests.""" | 20 | """TestResources: declarative management of external resources for tests.""" |
26 | 21 | 21 | ||
27 | 22 | import heapq | ||
28 | 22 | import inspect | 23 | import inspect |
29 | 24 | import operator | ||
30 | 23 | import unittest | 25 | import unittest |
31 | 24 | 26 | ||
32 | 25 | 27 | ||
33 | @@ -28,6 +30,105 @@ | |||
34 | 28 | return testresources.tests.test_suite() | 30 | return testresources.tests.test_suite() |
35 | 29 | 31 | ||
36 | 30 | 32 | ||
37 | 33 | def _digraph_to_graph(digraph, prime_node_mapping): | ||
38 | 34 | """Convert digraph to a graph. | ||
39 | 35 | |||
40 | 36 | :param digraph: A directed graph in the form | ||
41 | 37 | {from:{to:value}}. | ||
42 | 38 | :param prime_node_mapping: A mapping from every | ||
43 | 39 | node in digraph to a new unique and not in digraph node. | ||
44 | 40 | :return: A symmetric graph in the form {from:to:value}} created by | ||
45 | 41 | creating edges in the result between every N to M-prime with the | ||
46 | 42 | original N-M value and from every N to N-prime with a cost of 0. | ||
47 | 43 | No other edges are created. | ||
48 | 44 | """ | ||
49 | 45 | result = {} | ||
50 | 46 | for from_node, from_prime_node in prime_node_mapping.iteritems(): | ||
51 | 47 | result[from_node] = {from_prime_node:0} | ||
52 | 48 | result[from_prime_node] = {from_node:0} | ||
53 | 49 | for from_node, to_nodes in digraph.iteritems(): | ||
54 | 50 | from_prime = prime_node_mapping[from_node] | ||
55 | 51 | for to_node, value in to_nodes.iteritems(): | ||
56 | 52 | to_prime = prime_node_mapping[to_node] | ||
57 | 53 | result[from_prime][to_node] = value | ||
58 | 54 | result[to_node][from_prime] = value | ||
59 | 55 | return result | ||
60 | 56 | |||
61 | 57 | |||
62 | 58 | def _kruskals_graph_MST(graph): | ||
63 | 59 | """Find the minimal spanning tree in graph using Kruskals algorithm. | ||
64 | 60 | |||
65 | 61 | See http://en.wikipedia.org/wiki/Kruskal%27s_algorithm. | ||
66 | 62 | :param graph: A graph in {from:{to:value}} form. Every node present in | ||
67 | 63 | graph must be in the outer dict (because graph is not a directed graph. | ||
68 | 64 | :return: A graph with all nodes and those vertices that are part of the MST | ||
69 | 65 | for graph. If graph is not connected, then the result will also be a | ||
70 | 66 | forest. | ||
71 | 67 | """ | ||
72 | 68 | # forest contains all the nodes -> graph that node is in. | ||
73 | 69 | forest = {} | ||
74 | 70 | # graphs is the count of graphs we have yet to combine. | ||
75 | 71 | for node in graph: | ||
76 | 72 | forest[node] = {node:{}} | ||
77 | 73 | graphs = len(forest) | ||
78 | 74 | # collect edges: every edge is present twice (due to the graph | ||
79 | 75 | # representation), so normalise. | ||
80 | 76 | edges = set() | ||
81 | 77 | for from_node, to_nodes in graph.iteritems(): | ||
82 | 78 | for to_node, value in to_nodes.iteritems(): | ||
83 | 79 | edge = (value,) + tuple(sorted([from_node, to_node])) | ||
84 | 80 | edges.add(edge) | ||
85 | 81 | edges = list(edges) | ||
86 | 82 | heapq.heapify(edges) | ||
87 | 83 | while edges and graphs > 1: | ||
88 | 84 | # more edges to go and we haven't gotten a spanning tree yet. | ||
89 | 85 | edge = heapq.heappop(edges) | ||
90 | 86 | g1 = forest[edge[1]] | ||
91 | 87 | g2 = forest[edge[2]] | ||
92 | 88 | if g1 is g2: | ||
93 | 89 | continue # already joined | ||
94 | 90 | # combine g1 and g2 into g1 | ||
95 | 91 | graphs -= 1 | ||
96 | 92 | for from_node, to_nodes in g2.iteritems(): | ||
97 | 93 | #remember its symmetric, don't need to do 'to'. | ||
98 | 94 | forest[from_node] = g1 | ||
99 | 95 | g1.setdefault(from_node, {}).update(to_nodes) | ||
100 | 96 | # add edge | ||
101 | 97 | g1[edge[1]][edge[2]] = edge[0] | ||
102 | 98 | g1[edge[2]][edge[1]] = edge[0] | ||
103 | 99 | # union the remaining graphs | ||
104 | 100 | _, result = forest.popitem() | ||
105 | 101 | for _, g2 in forest.iteritems(): | ||
106 | 102 | if g2 is result: # common case | ||
107 | 103 | continue | ||
108 | 104 | for from_node, to_nodes in g2.iteritems(): | ||
109 | 105 | result.setdefault(from_node, {}).update(to_nodes) | ||
110 | 106 | return result | ||
111 | 107 | |||
112 | 108 | |||
113 | 109 | def _resource_graph(resource_sets): | ||
114 | 110 | """Convert an iterable of resource_sets into a graph. | ||
115 | 111 | |||
116 | 112 | Each resource_set in the iterable is treated as a node, and each resource | ||
117 | 113 | in that resource_set is used as an edge to other nodes. | ||
118 | 114 | """ | ||
119 | 115 | nodes = {} | ||
120 | 116 | edges = {} | ||
121 | 117 | for resource_set in resource_sets: | ||
122 | 118 | # put node in nodes | ||
123 | 119 | node = frozenset(resource_set) | ||
124 | 120 | nodes[node] = set() | ||
125 | 121 | # put its contents in as edges | ||
126 | 122 | for resource in resource_set: | ||
127 | 123 | edges.setdefault(resource, []).append(node) | ||
128 | 124 | # populate the adjacent members of nodes | ||
129 | 125 | for node, connected in nodes.iteritems(): | ||
130 | 126 | for resource in node: | ||
131 | 127 | connected.update(edges[resource]) | ||
132 | 128 | connected.discard(node) | ||
133 | 129 | return nodes | ||
134 | 130 | |||
135 | 131 | |||
136 | 31 | def split_by_resources(tests): | 132 | def split_by_resources(tests): |
137 | 32 | """Split a list of tests by the resources that the tests use. | 133 | """Split a list of tests by the resources that the tests use. |
138 | 33 | 134 | ||
139 | @@ -47,6 +148,30 @@ | |||
140 | 47 | return resource_set_tests | 148 | return resource_set_tests |
141 | 48 | 149 | ||
142 | 49 | 150 | ||
143 | 151 | def _strongly_connected_components(graph, no_resources): | ||
144 | 152 | """Find the strongly connected components in graph. | ||
145 | 153 | |||
146 | 154 | This is essentially a nonrecursive flatterning of Tarjan's method. It | ||
147 | 155 | may be worth profiling against an actual Tarjan's implementation at some | ||
148 | 156 | point, but sets are often faster than python calls. | ||
149 | 157 | |||
150 | 158 | graph gets consumed, but that could be changed easily enough. | ||
151 | 159 | """ | ||
152 | 160 | partitions = [] | ||
153 | 161 | while graph: | ||
154 | 162 | node, pending = graph.popitem() | ||
155 | 163 | current_partition = set([node]) | ||
156 | 164 | while pending: | ||
157 | 165 | # add all the nodes connected to a connected node to pending. | ||
158 | 166 | node = pending.pop() | ||
159 | 167 | current_partition.add(node) | ||
160 | 168 | pending.update(graph.pop(node, [])) | ||
161 | 169 | # don't try to process things we've allready processed. | ||
162 | 170 | pending.difference_update(current_partition) | ||
163 | 171 | partitions.append(current_partition) | ||
164 | 172 | return partitions | ||
165 | 173 | |||
166 | 174 | |||
167 | 50 | class OptimisingTestSuite(unittest.TestSuite): | 175 | class OptimisingTestSuite(unittest.TestSuite): |
168 | 51 | """A resource creation optimising TestSuite.""" | 176 | """A resource creation optimising TestSuite.""" |
169 | 52 | 177 | ||
170 | @@ -126,54 +251,149 @@ | |||
171 | 126 | def sortTests(self): | 251 | def sortTests(self): |
172 | 127 | """Attempt to topographically sort the contained tests. | 252 | """Attempt to topographically sort the contained tests. |
173 | 128 | 253 | ||
174 | 254 | This function biases to reusing a resource: it assumes that resetting | ||
175 | 255 | a resource is usually cheaper than a teardown + setup; and that most | ||
176 | 256 | resources are not dirtied by most tests. | ||
177 | 257 | |||
178 | 129 | Feel free to override to improve the sort behaviour. | 258 | Feel free to override to improve the sort behaviour. |
179 | 130 | """ | 259 | """ |
180 | 131 | # We group the tests by the resource combinations they use, | 260 | # We group the tests by the resource combinations they use, |
181 | 132 | # since there will usually be fewer resource combinations than | 261 | # since there will usually be fewer resource combinations than |
183 | 133 | # actual tests and there can never be more. | 262 | # actual tests and there can never be more: This gives us 'nodes' or |
184 | 263 | # 'resource_sets' that represent many tests using the same set of | ||
185 | 264 | # resources. | ||
186 | 134 | resource_set_tests = split_by_resources(self._tests) | 265 | resource_set_tests = split_by_resources(self._tests) |
189 | 135 | 266 | # Partition into separate sets of resources, there is no ordering | |
190 | 136 | graph = self._getGraph(resource_set_tests.keys()) | 267 | # preference between sets that do not share members. Rationale: |
191 | 268 | # If resource_set A and B have no common resources, AB and BA are | ||
192 | 269 | # equally good - the global setup/teardown sums are identical. Secondly | ||
193 | 270 | # if A shares one or more resources with C, then pairing AC|CA is better | ||
194 | 271 | # than having B between A and C, because the shared resources can be | ||
195 | 272 | # reset or reused. Having partitioned we can use connected graph logic | ||
196 | 273 | # on each partition. | ||
197 | 274 | resource_set_graph = _resource_graph(resource_set_tests) | ||
198 | 137 | no_resources = frozenset() | 275 | no_resources = frozenset() |
220 | 138 | # Recursive visit-all-nodes all-permutations. | 276 | # A list of resource_set_tests, all fully internally connected. |
221 | 139 | def cost(from_set, resource_sets): | 277 | partitions = _strongly_connected_components(resource_set_graph, |
222 | 140 | """Get the cost of resource traversal for resource sets. | 278 | no_resources) |
223 | 141 | 279 | result = [] | |
224 | 142 | :return: cost, order | 280 | for partition in partitions: |
225 | 143 | """ | 281 | # we process these at the end for no particularly good reason (it makes |
226 | 144 | if not resource_sets: | 282 | # testing slightly easier). |
227 | 145 | # tear down last resources | 283 | if partition == [no_resources]: |
228 | 146 | return graph[from_set][no_resources], [] | 284 | continue |
229 | 147 | costs = [] | 285 | order = self._makeOrder(partition) |
230 | 148 | for to_set in resource_sets: | 286 | # Spit this partition out into result |
231 | 149 | child_cost, child_order = cost( | 287 | for resource_set in order: |
232 | 150 | to_set, resource_sets - set([to_set])) | 288 | result.extend(resource_set_tests[resource_set]) |
233 | 151 | costs.append((graph[from_set][to_set] + child_cost, | 289 | result.extend(resource_set_tests[no_resources]) |
234 | 152 | [to_set] + child_order)) | 290 | self._tests = result |
214 | 153 | return min(costs) | ||
215 | 154 | _, order = cost(no_resources, | ||
216 | 155 | set(resource_set_tests) - set([no_resources])) | ||
217 | 156 | order.append(no_resources) | ||
218 | 157 | self._tests = sum( | ||
219 | 158 | (resource_set_tests[resource_set] for resource_set in order), []) | ||
235 | 159 | 291 | ||
236 | 160 | def _getGraph(self, resource_sets): | 292 | def _getGraph(self, resource_sets): |
237 | 161 | """Build a graph of the resource-using nodes. | 293 | """Build a graph of the resource-using nodes. |
238 | 162 | 294 | ||
239 | 295 | This special cases set(['root']) to be a node with no resources and | ||
240 | 296 | edges to everything. | ||
241 | 297 | |||
242 | 163 | :return: A complete directed graph of the switching costs | 298 | :return: A complete directed graph of the switching costs |
244 | 164 | between each resource combination. | 299 | between each resource combination. Note that links from N to N are |
245 | 300 | not included. | ||
246 | 165 | """ | 301 | """ |
247 | 302 | no_resources = frozenset() | ||
248 | 166 | graph = {} | 303 | graph = {} |
249 | 304 | root = set(['root']) | ||
250 | 305 | # bottom = set(['bottom']) | ||
251 | 167 | for from_set in resource_sets: | 306 | for from_set in resource_sets: |
252 | 168 | graph[from_set] = {} | 307 | graph[from_set] = {} |
253 | 308 | if from_set == root: | ||
254 | 309 | from_resources = no_resources | ||
255 | 310 | #elif from_set == bottom: | ||
256 | 311 | # continue # no links from bottom | ||
257 | 312 | else: | ||
258 | 313 | from_resources = from_set | ||
259 | 169 | for to_set in resource_sets: | 314 | for to_set in resource_sets: |
260 | 170 | if from_set is to_set: | 315 | if from_set is to_set: |
262 | 171 | graph[from_set][to_set] = 0 | 316 | continue # no self-edges |
263 | 317 | #if to_set == bottom: | ||
264 | 318 | # if from_set == root: | ||
265 | 319 | # continue # no short cuts! | ||
266 | 320 | # to_resources = no_resources | ||
267 | 321 | #el | ||
268 | 322 | if to_set == root: | ||
269 | 323 | continue # no links to root | ||
270 | 172 | else: | 324 | else: |
273 | 173 | graph[from_set][to_set] = self.cost_of_switching( | 325 | to_resources = to_set |
274 | 174 | from_set, to_set) | 326 | graph[from_set][to_set] = self.cost_of_switching( |
275 | 327 | from_resources, to_resources) | ||
276 | 175 | return graph | 328 | return graph |
277 | 176 | 329 | ||
278 | 330 | def _makeOrder(self, partition): | ||
279 | 331 | """Return a order for the resource sets in partition.""" | ||
280 | 332 | # This problem is NP-C - find the lowest cost hamiltonian path. It | ||
281 | 333 | # also meets the triangle inequality, so we can use an approximation. | ||
282 | 334 | # TODO: implement Christofides. | ||
283 | 335 | # See http://en.wikipedia.org/wiki/Travelling_salesman_problem#Metric_TSP | ||
284 | 336 | # We need a root | ||
285 | 337 | root = frozenset(['root']) | ||
286 | 338 | partition.add(root) | ||
287 | 339 | # and an end | ||
288 | 340 | # partition.add(frozenset(['bottom'])) | ||
289 | 341 | # get rid of 'noresources' | ||
290 | 342 | partition.discard(frozenset()) | ||
291 | 343 | digraph = self._getGraph(partition) | ||
292 | 344 | # build a prime map | ||
293 | 345 | primes = {} | ||
294 | 346 | prime = frozenset(['prime']) | ||
295 | 347 | for node in digraph: | ||
296 | 348 | primes[node] = node.union(prime) | ||
297 | 349 | graph = _digraph_to_graph(digraph, primes) | ||
298 | 350 | mst = _kruskals_graph_MST(graph) | ||
299 | 351 | # Because the representation is a digraph, we can build an Eulerian | ||
300 | 352 | # cycle directly from the representation by just following the links: | ||
301 | 353 | # a node with only 1 'edge' has two directed edges; and we can only | ||
302 | 354 | # enter and leave it once, so the edge lookups will match precisely. | ||
303 | 355 | # As the mst is a spanning tree, the graph will become disconnected | ||
304 | 356 | # (we choose non-disconnecting edges first) | ||
305 | 357 | # - for a stub node (1 outgoing link): when exiting it unless it is | ||
306 | 358 | # the first node started at | ||
307 | 359 | # - for a non-stub node if choosing an outgoing link where some other | ||
308 | 360 | # endpoints incoming link has not been traversed. [exit by a | ||
309 | 361 | # different node than entering, until all exits taken]. | ||
310 | 362 | # We don't need the mst after, so it gets modified in place. | ||
311 | 363 | node = root | ||
312 | 364 | cycle = [node] | ||
313 | 365 | steps = 2 * (len(mst) - 1) | ||
314 | 366 | for step in xrange(steps): | ||
315 | 367 | found = False | ||
316 | 368 | outgoing = None # For clearer debugging. | ||
317 | 369 | for outgoing in mst[node]: | ||
318 | 370 | if node in mst[outgoing]: | ||
319 | 371 | # we have a return path: take it | ||
320 | 372 | # print node, '->', outgoing, ' can return' | ||
321 | 373 | del mst[node][outgoing] | ||
322 | 374 | node = outgoing | ||
323 | 375 | cycle.append(node) | ||
324 | 376 | found = True | ||
325 | 377 | break | ||
326 | 378 | if not found: | ||
327 | 379 | # none of the outgoing links have an incoming, so follow an | ||
328 | 380 | # arbitrary one (the last examined outgoing) | ||
329 | 381 | # print node, '->', outgoing | ||
330 | 382 | del mst[node][outgoing] | ||
331 | 383 | node = outgoing | ||
332 | 384 | cycle.append(node) | ||
333 | 385 | # Convert to a path: | ||
334 | 386 | visited = set() | ||
335 | 387 | order = [] | ||
336 | 388 | for node in cycle: | ||
337 | 389 | if node in visited: | ||
338 | 390 | continue | ||
339 | 391 | if node in primes: | ||
340 | 392 | order.append(node) | ||
341 | 393 | visited.add(node) | ||
342 | 394 | assert order[0] == root | ||
343 | 395 | return order[1:] | ||
344 | 396 | |||
345 | 177 | 397 | ||
346 | 178 | class TestLoader(unittest.TestLoader): | 398 | class TestLoader(unittest.TestLoader): |
347 | 179 | """Custom TestLoader to set the right TestSuite class.""" | 399 | """Custom TestLoader to set the right TestSuite class.""" |
348 | 180 | 400 | ||
349 | === modified file 'lib/testresources/tests/__init__.py' | |||
350 | --- lib/testresources/tests/__init__.py 2009-07-13 08:22:13 +0000 | |||
351 | +++ lib/testresources/tests/__init__.py 2010-02-19 05:54:16 +0000 | |||
352 | @@ -28,10 +28,12 @@ | |||
353 | 28 | import testresources.tests.test_resourced_test_case | 28 | import testresources.tests.test_resourced_test_case |
354 | 29 | import testresources.tests.test_test_loader | 29 | import testresources.tests.test_test_loader |
355 | 30 | import testresources.tests.test_test_resource | 30 | import testresources.tests.test_test_resource |
356 | 31 | import testresources.tests.test_resource_graph | ||
357 | 31 | result = TestUtil.TestSuite() | 32 | result = TestUtil.TestSuite() |
358 | 32 | result.addTest(testresources.tests.test_test_loader.test_suite()) | 33 | result.addTest(testresources.tests.test_test_loader.test_suite()) |
359 | 33 | result.addTest(testresources.tests.test_test_resource.test_suite()) | 34 | result.addTest(testresources.tests.test_test_resource.test_suite()) |
360 | 34 | result.addTest(testresources.tests.test_resourced_test_case.test_suite()) | 35 | result.addTest(testresources.tests.test_resourced_test_case.test_suite()) |
361 | 36 | result.addTest(testresources.tests.test_resource_graph.test_suite()) | ||
362 | 35 | result.addTest( | 37 | result.addTest( |
363 | 36 | testresources.tests.test_optimising_test_suite.test_suite()) | 38 | testresources.tests.test_optimising_test_suite.test_suite()) |
364 | 37 | return result | 39 | return result |
365 | 38 | 40 | ||
366 | === modified file 'lib/testresources/tests/test_optimising_test_suite.py' | |||
367 | --- lib/testresources/tests/test_optimising_test_suite.py 2010-01-05 05:51:14 +0000 | |||
368 | +++ lib/testresources/tests/test_optimising_test_suite.py 2010-02-19 05:54:16 +0000 | |||
369 | @@ -403,7 +403,7 @@ | |||
370 | 403 | resource = self.makeResource() | 403 | resource = self.makeResource() |
371 | 404 | suite = testresources.OptimisingTestSuite() | 404 | suite = testresources.OptimisingTestSuite() |
372 | 405 | graph = suite._getGraph([frozenset()]) | 405 | graph = suite._getGraph([frozenset()]) |
374 | 406 | self.assertEqual({frozenset(): {frozenset(): 0}}, graph) | 406 | self.assertEqual({frozenset(): {}}, graph) |
375 | 407 | 407 | ||
376 | 408 | def testTwoCasesInGraph(self): | 408 | def testTwoCasesInGraph(self): |
377 | 409 | res1 = self.makeResource() | 409 | res1 = self.makeResource() |
378 | @@ -415,9 +415,9 @@ | |||
379 | 415 | 415 | ||
380 | 416 | suite = testresources.OptimisingTestSuite() | 416 | suite = testresources.OptimisingTestSuite() |
381 | 417 | graph = suite._getGraph([no_resources, set1, set2]) | 417 | graph = suite._getGraph([no_resources, set1, set2]) |
385 | 418 | self.assertEqual({no_resources: {no_resources: 0, set1: 2, set2: 1}, | 418 | self.assertEqual({no_resources: {set1: 2, set2: 1}, |
386 | 419 | set1: {no_resources: 2, set1: 0, set2: 1}, | 419 | set1: {no_resources: 2, set2: 1}, |
387 | 420 | set2: {no_resources: 1, set1: 1, set2: 0}}, graph) | 420 | set2: {no_resources: 1, set1: 1 }}, graph) |
388 | 421 | 421 | ||
389 | 422 | 422 | ||
390 | 423 | class TestGraphStuff(testtools.TestCase): | 423 | class TestGraphStuff(testtools.TestCase): |
391 | @@ -489,10 +489,15 @@ | |||
392 | 489 | return permutations | 489 | return permutations |
393 | 490 | 490 | ||
394 | 491 | def testBasicSortTests(self): | 491 | def testBasicSortTests(self): |
397 | 492 | # Test every permutation of inputs, with equal cost resources and | 492 | # Test every permutation of inputs, with legacy tests. |
398 | 493 | # legacy tests. | 493 | # Cannot use equal costs because of the use of |
399 | 494 | # a 2*optimal heuristic for sorting: with equal | ||
400 | 495 | # costs the wrong sort order is < twice the optimal | ||
401 | 496 | # weight, and thus can be selected. | ||
402 | 494 | resource_one = testresources.TestResource() | 497 | resource_one = testresources.TestResource() |
403 | 495 | resource_two = testresources.TestResource() | 498 | resource_two = testresources.TestResource() |
404 | 499 | resource_two.setUpCost = 5 | ||
405 | 500 | resource_two.tearDownCost = 5 | ||
406 | 496 | resource_three = testresources.TestResource() | 501 | resource_three = testresources.TestResource() |
407 | 497 | 502 | ||
408 | 498 | self.case1.resources = [ | 503 | self.case1.resources = [ |
409 | @@ -584,6 +589,43 @@ | |||
410 | 584 | permutation.index(self.case3) < permutation.index(self.case4), | 589 | permutation.index(self.case3) < permutation.index(self.case4), |
411 | 585 | sorted.index(self.case3) < sorted.index(self.case4)) | 590 | sorted.index(self.case3) < sorted.index(self.case4)) |
412 | 586 | 591 | ||
413 | 592 | def testSortingTwelveIndependentIsFast(self): | ||
414 | 593 | # Given twelve independent resource sets, my patience is not exhausted. | ||
415 | 594 | managers = [] | ||
416 | 595 | for pos in range(12): | ||
417 | 596 | managers.append(testresources.TestResourceManager()) | ||
418 | 597 | # Add more sample tests | ||
419 | 598 | cases = [self.case1, self.case2, self.case3, self.case4] | ||
420 | 599 | for pos in range(5,13): | ||
421 | 600 | cases.append( | ||
422 | 601 | testtools.clone_test_with_new_id(cases[0], 'case%d' % pos)) | ||
423 | 602 | # We care that this is fast in this test, so we don't need to have | ||
424 | 603 | # overlapping resource usage | ||
425 | 604 | for case, manager in zip(cases, managers): | ||
426 | 605 | case.resources = [('_resource', manager)] | ||
427 | 606 | # Any sort is ok, as long as its the right length :) | ||
428 | 607 | result = self.sortTests(cases) | ||
429 | 608 | self.assertEqual(12, len(result)) | ||
430 | 609 | |||
431 | 610 | def testSortingTwelveOverlappingIsFast(self): | ||
432 | 611 | # Given twelve connected resource sets, my patience is not exhausted. | ||
433 | 612 | managers = [] | ||
434 | 613 | for pos in range(12): | ||
435 | 614 | managers.append(testresources.TestResourceManager()) | ||
436 | 615 | # Add more sample tests | ||
437 | 616 | cases = [self.case1, self.case2, self.case3, self.case4] | ||
438 | 617 | for pos in range(5,13): | ||
439 | 618 | cases.append( | ||
440 | 619 | testtools.clone_test_with_new_id(cases[0], 'case%d' % pos)) | ||
441 | 620 | tempdir = testresources.TestResourceManager() | ||
442 | 621 | # give all tests a tempdir, enough to provoke a single partition in | ||
443 | 622 | # the current code. | ||
444 | 623 | for case, manager in zip(cases, managers): | ||
445 | 624 | case.resources = [('_resource', manager), ('tempdir', tempdir)] | ||
446 | 625 | # Any sort is ok, as long as its the right length :) | ||
447 | 626 | result = self.sortTests(cases) | ||
448 | 627 | self.assertEqual(12, len(result)) | ||
449 | 628 | |||
450 | 587 | def testSortConsidersDependencies(self): | 629 | def testSortConsidersDependencies(self): |
451 | 588 | """Tests with different dependencies are sorted together.""" | 630 | """Tests with different dependencies are sorted together.""" |
452 | 589 | # We test this by having two resources (one and two) that share a very | 631 | # We test this by having two resources (one and two) that share a very |
453 | 590 | 632 | ||
454 | === added file 'lib/testresources/tests/test_resource_graph.py' | |||
455 | --- lib/testresources/tests/test_resource_graph.py 1970-01-01 00:00:00 +0000 | |||
456 | +++ lib/testresources/tests/test_resource_graph.py 2010-02-19 05:54:16 +0000 | |||
457 | @@ -0,0 +1,141 @@ | |||
458 | 1 | # | ||
459 | 2 | # testresources: extensions to python unittest to allow declaritive use | ||
460 | 3 | # of resources by test cases. | ||
461 | 4 | # Copyright (C) 2010 Robert Collins <robertc@robertcollins.net> | ||
462 | 5 | # | ||
463 | 6 | # This program is free software; you can redistribute it and/or modify | ||
464 | 7 | # it under the terms of the GNU General Public License as published by | ||
465 | 8 | # the Free Software Foundation; either version 2 of the License, or | ||
466 | 9 | # (at your option) any later version. | ||
467 | 10 | # | ||
468 | 11 | # This program is distributed in the hope that it will be useful, | ||
469 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
470 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
471 | 14 | # GNU General Public License for more details. | ||
472 | 15 | # | ||
473 | 16 | # You should have received a copy of the GNU General Public License | ||
474 | 17 | # along with this program; if not, write to the Free Software | ||
475 | 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | ||
476 | 19 | # | ||
477 | 20 | |||
478 | 21 | """Test _resource_graph(resource_sets).""" | ||
479 | 22 | |||
480 | 23 | import testtools | ||
481 | 24 | import testresources | ||
482 | 25 | from testresources import split_by_resources, _resource_graph | ||
483 | 26 | from testresources.tests import ResultWithResourceExtensions | ||
484 | 27 | import unittest | ||
485 | 28 | |||
486 | 29 | |||
487 | 30 | def test_suite(): | ||
488 | 31 | from testresources.tests import TestUtil | ||
489 | 32 | loader = TestUtil.TestLoader() | ||
490 | 33 | result = loader.loadTestsFromName(__name__) | ||
491 | 34 | return result | ||
492 | 35 | |||
493 | 36 | |||
494 | 37 | class TestResourceGraph(testtools.TestCase): | ||
495 | 38 | |||
496 | 39 | def test_empty(self): | ||
497 | 40 | no_resources = frozenset() | ||
498 | 41 | resource_sets = [no_resources] | ||
499 | 42 | self.assertEqual({no_resources:set([])}, _resource_graph(resource_sets)) | ||
500 | 43 | |||
501 | 44 | def test_discrete(self): | ||
502 | 45 | resset1 = frozenset([testresources.TestResourceManager()]) | ||
503 | 46 | resset2 = frozenset([testresources.TestResourceManager()]) | ||
504 | 47 | resource_sets = [resset1, resset2] | ||
505 | 48 | result = _resource_graph(resource_sets) | ||
506 | 49 | self.assertEqual({resset1:set([]), resset2:set([])}, result) | ||
507 | 50 | |||
508 | 51 | def test_overlapping(self): | ||
509 | 52 | res1 = testresources.TestResourceManager() | ||
510 | 53 | res2 = testresources.TestResourceManager() | ||
511 | 54 | resset1 = frozenset([res1]) | ||
512 | 55 | resset2 = frozenset([res2]) | ||
513 | 56 | resset3 = frozenset([res1, res2]) | ||
514 | 57 | resource_sets = [resset1, resset2, resset3] | ||
515 | 58 | result = _resource_graph(resource_sets) | ||
516 | 59 | self.assertEqual( | ||
517 | 60 | {resset1:set([resset3]), | ||
518 | 61 | resset2:set([resset3]), | ||
519 | 62 | resset3:set([resset1, resset2])}, | ||
520 | 63 | result) | ||
521 | 64 | |||
522 | 65 | |||
523 | 66 | class TestDigraphToGraph(testtools.TestCase): | ||
524 | 67 | |||
525 | 68 | def test_wikipedia_example(self): | ||
526 | 69 | """Converting a digraph mirrors it in the XZ axis (matrix view). | ||
527 | 70 | |||
528 | 71 | See http://en.wikipedia.org/wiki/Travelling_salesman_problem \ | ||
529 | 72 | #Solving_by_conversion_to_Symmetric_TSP | ||
530 | 73 | """ | ||
531 | 74 | # A B C | ||
532 | 75 | # A 1 2 | ||
533 | 76 | # B 6 3 | ||
534 | 77 | # C 5 4 | ||
535 | 78 | A = "A" | ||
536 | 79 | Ap = "A'" | ||
537 | 80 | B = "B" | ||
538 | 81 | Bp = "B'" | ||
539 | 82 | C = "C" | ||
540 | 83 | Cp = "C'" | ||
541 | 84 | digraph = {A:{ B:1, C:2}, | ||
542 | 85 | B:{A:6, C:3}, | ||
543 | 86 | C:{A:5, B:4 }} | ||
544 | 87 | # and the output | ||
545 | 88 | # A B C A' B' C' | ||
546 | 89 | # A 0 6 5 | ||
547 | 90 | # B 1 0 4 | ||
548 | 91 | # C 2 3 0 | ||
549 | 92 | # A' 0 1 2 | ||
550 | 93 | # B' 6 0 3 | ||
551 | 94 | # C' 5 4 0 | ||
552 | 95 | expected = { | ||
553 | 96 | A :{ Ap:0, Bp:6, Cp:5}, | ||
554 | 97 | B :{ Ap:1, Bp:0, Cp:4}, | ||
555 | 98 | C :{ Ap:2, Bp:3, Cp:0}, | ||
556 | 99 | Ap:{A:0, B:1, C:2 }, | ||
557 | 100 | Bp:{A:6, B:0, C:3 }, | ||
558 | 101 | Cp:{A:5, B:4, C:0 }} | ||
559 | 102 | self.assertEqual(expected, | ||
560 | 103 | testresources._digraph_to_graph(digraph, {A:Ap, B:Bp, C:Cp})) | ||
561 | 104 | |||
562 | 105 | |||
563 | 106 | class TestKruskalsMST(testtools.TestCase): | ||
564 | 107 | |||
565 | 108 | def test_wikipedia_example(self): | ||
566 | 109 | """Performing KruskalsMST on a graph returns a spanning tree. | ||
567 | 110 | |||
568 | 111 | See http://en.wikipedia.org/wiki/Kruskal%27s_algorithm. | ||
569 | 112 | """ | ||
570 | 113 | A = "A" | ||
571 | 114 | B = "B" | ||
572 | 115 | C = "C" | ||
573 | 116 | D = "D" | ||
574 | 117 | E = "E" | ||
575 | 118 | F = "F" | ||
576 | 119 | G = "G" | ||
577 | 120 | graph = { | ||
578 | 121 | A:{ B:7, D:5}, | ||
579 | 122 | B:{A:7, C:8, D:9, E:7}, | ||
580 | 123 | C:{ B:8, E:5}, | ||
581 | 124 | D:{A:5, B:9, E:15, F:6}, | ||
582 | 125 | E:{ B:7, C:5, D:15, F:8, G:9}, | ||
583 | 126 | F:{ D:6, E:8, G:11}, | ||
584 | 127 | G:{ E:9, F:11}} | ||
585 | 128 | expected = { | ||
586 | 129 | A:{ B:7, D:5}, | ||
587 | 130 | B:{A:7, E:7}, | ||
588 | 131 | C:{ E:5}, | ||
589 | 132 | D:{A:5, F:6}, | ||
590 | 133 | E:{ B:7, C:5, G:9}, | ||
591 | 134 | F:{ D:6}, | ||
592 | 135 | G:{ E:9}} | ||
593 | 136 | result = testresources._kruskals_graph_MST(graph) | ||
594 | 137 | e_weight = sum(sum(row.itervalues()) for row in expected.itervalues()) | ||
595 | 138 | r_weight = sum(sum(row.itervalues()) for row in result.itervalues()) | ||
596 | 139 | self.assertEqual(e_weight, r_weight) | ||
597 | 140 | self.assertEqual(expected, | ||
598 | 141 | testresources._kruskals_graph_MST(graph)) |
Faster stuff good. Good stuff good. Stuff is good. Faster is good, so stuff is faster.