Merge lp:~cr3/checkbox/0.13_resolver into lp:checkbox/0.13
- 0.13_resolver
- Merge into 0.13
Proposed by
Marc Tardif
Status: | Merged |
---|---|
Approved by: | Javier Collado |
Approved revision: | 1388 |
Merged at revision: | 1387 |
Proposed branch: | lp:~cr3/checkbox/0.13_resolver |
Merge into: | lp:checkbox/0.13 |
Diff against target: |
412 lines (+175/-139) 5 files modified
checkbox/job.py (+15/-1) checkbox/lib/resolver.py (+115/-96) checkbox/lib/tests/resolver.py (+39/-41) debian/changelog (+5/-0) plugins/jobs_info.py (+1/-1) |
To merge this branch: | bzr merge lp:~cr3/checkbox/0.13_resolver |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Javier Collado (community) | Approve | ||
Review via email: mp+112610@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'checkbox/job.py' |
2 | --- checkbox/job.py 2012-06-20 20:34:40 +0000 |
3 | +++ checkbox/job.py 2012-06-28 16:54:19 +0000 |
4 | @@ -104,4 +104,18 @@ |
5 | class JobStore(MessageStore): |
6 | """A job store which stores its jobs in a file system hierarchy.""" |
7 | |
8 | - pass |
9 | + def add(self, job): |
10 | + # Return if the same job is already in the store |
11 | + if list(self._find_matching_messages(name=job["name"])): |
12 | + return |
13 | + |
14 | + return super(JobStore, self).add(job) |
15 | + |
16 | + def _find_matching_messages(self, **kwargs): |
17 | + for filename in self._walk_messages(): |
18 | + message = self._read_message(filename,cache=True) |
19 | + for key, value in kwargs.items(): |
20 | + if message.get(key) != value: |
21 | + break |
22 | + else: |
23 | + yield filename |
24 | |
25 | === modified file 'checkbox/lib/resolver.py' |
26 | --- checkbox/lib/resolver.py 2012-06-19 20:45:11 +0000 |
27 | +++ checkbox/lib/resolver.py 2012-06-28 16:54:19 +0000 |
28 | @@ -16,111 +16,130 @@ |
29 | # You should have received a copy of the GNU General Public License |
30 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
31 | # |
32 | +from collections import defaultdict, OrderedDict |
33 | + |
34 | + |
35 | class Resolver: |
36 | """ |
37 | Main class. Instantiate with the root directory of your items. |
38 | """ |
39 | |
40 | - def __init__(self, compare=None, key=None): |
41 | - if compare is None: |
42 | - compare = lambda a, b: cmp(a, b) |
43 | - if key is None: |
44 | - key = lambda k: k |
45 | - |
46 | - self.compare = compare |
47 | - self.key = key |
48 | - |
49 | - # detect repeated resolution attempts - these indicate some circular dependency |
50 | - self.reentrant_resolution = set() |
51 | - |
52 | - # collect all items |
53 | - self.items = {} |
54 | - |
55 | - # for each item, keep a set of dependents and dependencies |
56 | - self.dependents = {} |
57 | - self.dependencies = {} |
58 | + def __init__(self, key_func=None): |
59 | + if key_func is None: |
60 | + key_func = lambda k: k |
61 | + self.key_func = key_func |
62 | + |
63 | + self.items_added = OrderedDict() |
64 | + self.depends = {} # Dependencies |
65 | + self.rdepends = defaultdict(list) # Reverse dependencies |
66 | + |
67 | + # Data used in _resolve method |
68 | + self.items = None |
69 | + self.items_blocked = None |
70 | + self.resolved = False |
71 | |
72 | def add(self, item, *dependencies): |
73 | - key = self.key(item) |
74 | - if key in self.items: |
75 | - raise Exception, "%s: key already exists" % key |
76 | - self.items[key] = item |
77 | - |
78 | - self.dependencies[key] = set(dependencies) |
79 | - |
80 | - def remove(self, item): |
81 | - key = self.key(item) |
82 | - del self.items[key] |
83 | - del self.dependencies[key] |
84 | - |
85 | - def resolve(self, item, found=None): |
86 | - """ |
87 | - the center piece. |
88 | - recursively resolve dependencies of scripts |
89 | - return them as a flat list, sorted according to ancestral relationships |
90 | - """ |
91 | - key = self.key(item) |
92 | - resolved = self.dependents.get(key, None) |
93 | - if resolved is not None: |
94 | - return resolved |
95 | - |
96 | - if key not in self.items: |
97 | - msg = "no dependencies found for %s" % key |
98 | - if found: |
99 | - msg += " while resolving %s" % found |
100 | - raise Exception, msg |
101 | - |
102 | - dependencies = self.dependencies.get(key, set()) |
103 | - resolved = set() |
104 | - |
105 | + """ |
106 | + Add item as pending |
107 | + """ |
108 | + key = self.key_func(item) |
109 | + if key in self.items_added: |
110 | + raise Exception("%s: key already exists" % key) |
111 | + |
112 | + # Dependencies bookkeeping |
113 | + self.items_added[key] = item |
114 | + self.depends[key] = list(dependencies) |
115 | for dependency in dependencies: |
116 | - resolution_step = (key, dependency) |
117 | - if resolution_step in self.reentrant_resolution: |
118 | - if found: |
119 | - scapegoat = found |
120 | - else: |
121 | - scapegoat = dependency |
122 | - raise Exception, "circular dependency involving %s and %s" % (key, scapegoat) |
123 | - # add resolution |
124 | - self.reentrant_resolution.add(resolution_step) |
125 | - # and its dependencies, if any |
126 | - if dependency in self.items: |
127 | - resolved.update(self.resolve(self.items[dependency], found=key)) |
128 | - |
129 | - # now it's time for sorting hierarchically... Since circular dependencies are excluded, |
130 | - # ancestors will always have fewer dependencies than descendants, so sorting by the |
131 | - # number of dependencies will give the desired order. |
132 | - resolved = sorted(resolved, key=lambda x: len(self.dependents[x])) |
133 | - resolved.append(key) |
134 | - self.dependents[key] = resolved |
135 | - |
136 | - return resolved |
137 | + self.rdepends[dependency].append(key) |
138 | + |
139 | + # Circular dependencies check |
140 | + def circular_dependencies(node): |
141 | + seen = circular_dependencies.seen |
142 | + for dependency in self.depends.get(node, []): |
143 | + if key == dependency: |
144 | + raise Exception("circular dependency involving " |
145 | + "%s and %s" % (key, node)) |
146 | + if dependency in seen: |
147 | + continue |
148 | + seen.add(dependency) |
149 | + circular_dependencies(dependency) |
150 | + circular_dependencies.seen = set((key,)) |
151 | + circular_dependencies(key) |
152 | + |
153 | + # Resolve on next call to get_dependencies/get_dependents |
154 | + self.resolved = False |
155 | + |
156 | + def _resolve(self): |
157 | + """ |
158 | + Work through the pending items and reorder them properly |
159 | + """ |
160 | + if self.resolved: |
161 | + return |
162 | + |
163 | + # Initialize internal ordering data |
164 | + self.items = OrderedDict() |
165 | + self.items_blocked = {} |
166 | + |
167 | + def add_unblocked(key): |
168 | + """Add items that have been unblocked""" |
169 | + for dependent in self.rdepends[key]: |
170 | + if not dependent in self.items_blocked: |
171 | + continue |
172 | + |
173 | + unblocked = all(dependency in self.items |
174 | + for dependency in self.depends[dependent] |
175 | + if dependency in self.items_added) |
176 | + if unblocked: |
177 | + item = self.items_blocked[dependent] |
178 | + self.items[dependent] = item |
179 | + del self.items_blocked[dependent] |
180 | + |
181 | + add_unblocked(dependent) |
182 | + |
183 | + for key, item in self.items_added.items(): |
184 | + # Avoid adding an item until all dependencies have been met |
185 | + blocked = any(dependency not in self.items |
186 | + for dependency in self.depends[key] |
187 | + if dependency in self.items_added) |
188 | + |
189 | + if blocked: |
190 | + self.items_blocked[key] = item |
191 | + else: |
192 | + self.items[key] = item |
193 | + add_unblocked(key) |
194 | + |
195 | + if self.items_blocked: |
196 | + raise Exception('There are {} items blocked: {}' |
197 | + .format(len(self.items_blocked), |
198 | + ', '.join(self.items_blocked.keys()))) |
199 | + |
200 | + # Don't resolve again on next call to get_dependencies/get_dependents |
201 | + # unless a new item is added |
202 | + self.resolved = True |
203 | |
204 | def get_dependencies(self, item): |
205 | - return [self.items[r] for r in self.resolve(item)] |
206 | + """ |
207 | + Return a list of the dependencies for a given item |
208 | + """ |
209 | + self._resolve() |
210 | + key = self.key_func(item) |
211 | + if key not in self.depends: |
212 | + msg = "no dependencies found for %s" % key |
213 | + raise Exception(msg) |
214 | + |
215 | + dependencies = self.depends[key] + [key] |
216 | + |
217 | + return dependencies |
218 | |
219 | def get_dependents(self, item=None): |
220 | - dependents = [] |
221 | - if item: |
222 | - # Immediate dependents |
223 | - key = self.key(item) |
224 | - all_dependents = filter( |
225 | - lambda x: key in self.resolve(x)[:-1], |
226 | - self.items.itervalues()) |
227 | - dependents = filter( |
228 | - lambda x: self.key(self.get_dependencies(x)[-2]) == key, |
229 | - all_dependents) |
230 | - else: |
231 | - # First level of dependents |
232 | - dependents = filter(lambda x: len(self.resolve(x)) == 1, self.items.itervalues()) |
233 | - |
234 | - index = 0 |
235 | - dependents = sorted(dependents, key=self.key) |
236 | - while index < len(dependents): |
237 | - sub_dependents = self.get_dependents(dependents[index]) |
238 | - if sub_dependents: |
239 | - dependents[index+1:index+1] = sub_dependents |
240 | - index += len(sub_dependents) |
241 | - index += 1 |
242 | - |
243 | - return dependents |
244 | + """ |
245 | + Return a list of the items that depend on the given one |
246 | + or the whole list of items topologically ordered |
247 | + """ |
248 | + self._resolve() |
249 | + items = list(self.items.values()) |
250 | + if item is not None: |
251 | + index = items.index(item) |
252 | + items = items[index + 1:] |
253 | + |
254 | + return items |
255 | |
256 | === modified file 'checkbox/lib/tests/resolver.py' |
257 | --- checkbox/lib/tests/resolver.py 2009-07-22 03:48:49 +0000 |
258 | +++ checkbox/lib/tests/resolver.py 2012-06-28 16:54:19 +0000 |
259 | @@ -22,71 +22,62 @@ |
260 | |
261 | |
262 | class ResolverTest(unittest.TestCase): |
263 | + def setUp(self): |
264 | + self.resolver = Resolver() |
265 | + |
266 | def test_dependencies_none(self): |
267 | - resolver = Resolver() |
268 | try: |
269 | - resolver.get_dependencies('a') |
270 | - except Exception, error: |
271 | + self.resolver.get_dependencies('a') |
272 | + except Exception as error: |
273 | self.assertTrue(error.args[0].startswith('no dependencies')) |
274 | else: |
275 | self.fail('non existing element accepted by resolver') |
276 | |
277 | def test_dependencies_one_level(self): |
278 | - resolver = Resolver() |
279 | - resolver.add('a') |
280 | + self.resolver.add('a') |
281 | |
282 | - results = resolver.get_dependencies('a') |
283 | - self.assertTrue(len(results) == 1) |
284 | - self.assertTrue(results[0] == 'a') |
285 | + results = self.resolver.get_dependencies('a') |
286 | + self.assertListEqual(results, ['a']) |
287 | |
288 | def test_dependencies_two_level(self): |
289 | - resolver = Resolver() |
290 | - resolver.add('a') |
291 | - resolver.add('b', 'a') |
292 | + self.resolver.add('a') |
293 | + self.resolver.add('b', 'a') |
294 | |
295 | - results = resolver.get_dependencies('b') |
296 | - self.assertTrue(len(results) == 2) |
297 | - self.assertTrue(results[0] == 'a') |
298 | - self.assertTrue(results[1] == 'b') |
299 | + results = self.resolver.get_dependencies('b') |
300 | + self.assertListEqual(results, ['a', 'b']) |
301 | |
302 | def test_dependencies_multiple(self): |
303 | - resolver = Resolver() |
304 | - resolver.add('a') |
305 | - resolver.add('b') |
306 | - resolver.add('c', 'a', 'b') |
307 | + self.resolver.add('a') |
308 | + # A appears as a dependency multiple times |
309 | + # in b and c, but isn't a circular dependency |
310 | + self.resolver.add('b', 'a') |
311 | + self.resolver.add('c', 'a', 'b') |
312 | |
313 | - results = resolver.get_dependencies('c') |
314 | - self.assertTrue(len(results) == 3) |
315 | - self.assertTrue(results[0] == 'a') |
316 | - self.assertTrue(results[1] == 'b') |
317 | - self.assertTrue(results[2] == 'c') |
318 | + results = self.resolver.get_dependencies('c') |
319 | + self.assertListEqual(results, ['a', 'b', 'c']) |
320 | |
321 | def test_dependencies_circular(self): |
322 | - resolver = Resolver() |
323 | - resolver.add('a', 'b') |
324 | - resolver.add('b', 'a') |
325 | try: |
326 | - resolver.get_dependencies('a') |
327 | - except Exception, error: |
328 | + self.resolver.add('a', 'b') |
329 | + self.resolver.add('b', 'a') |
330 | + self.resolver.get_dependencies('a') |
331 | + except Exception as error: |
332 | self.assertTrue(error.args[0].startswith('circular dependency')) |
333 | else: |
334 | self.fail('circular dependency not detected') |
335 | |
336 | def test_dependents_none(self): |
337 | - resolver = Resolver() |
338 | - resolver.add('a') |
339 | + self.resolver.add('a') |
340 | |
341 | - results = resolver.get_dependents('a') |
342 | + results = self.resolver.get_dependents('a') |
343 | self.assertTrue(len(results) == 0) |
344 | |
345 | def test_dependents_one(self): |
346 | - resolver = Resolver() |
347 | - resolver.add('a') |
348 | - resolver.add('b', 'a') |
349 | + self.resolver.add('a') |
350 | + self.resolver.add('b', 'a') |
351 | |
352 | - results = resolver.get_dependents('a') |
353 | - self.assertTrue(len(results) == 1) |
354 | - self.assertTrue(results[0] == 'b') |
355 | + results = self.resolver.get_dependents('a') |
356 | + self.assertListEqual(results, ['b']) |
357 | |
358 | def test_dependents_two(self): |
359 | resolver = Resolver() |
360 | @@ -95,6 +86,13 @@ |
361 | resolver.add('c', 'b') |
362 | |
363 | results = resolver.get_dependents('a') |
364 | - self.assertTrue(len(results) == 2) |
365 | - self.assertTrue(results[0] == 'b') |
366 | - self.assertTrue(results[1] == 'c') |
367 | + self.assertListEqual(results, ['b', 'c']) |
368 | + |
369 | + def test_multiple_resolve_steps(self): |
370 | + self.resolver.add('a', 'b') |
371 | + results = self.resolver.get_dependents() |
372 | + self.assertListEqual(results, ['a']) |
373 | + |
374 | + self.resolver.add('b') |
375 | + results = self.resolver.get_dependents() |
376 | + self.assertListEqual(results, ['b', 'a']) |
377 | |
378 | === modified file 'debian/changelog' |
379 | --- debian/changelog 2012-06-25 19:26:57 +0000 |
380 | +++ debian/changelog 2012-06-28 16:54:19 +0000 |
381 | @@ -15,6 +15,8 @@ |
382 | |
383 | [Javier Collado] |
384 | * Make sure that jobs are topologically ordered (LP: #990075) |
385 | + * Keep job ordering as close to whitelist as possible (LP: #1017951) |
386 | + * Fixed detection of circular references in resolver. |
387 | |
388 | [Jeff Lane] |
389 | * Bumped revision to 0.13.8 |
390 | @@ -47,6 +49,9 @@ |
391 | * [FEATURE] checkbox_gtk/gtk_interface.py: Capture ESC keypresses so that |
392 | Checkbox doesn't close/die when user presses ESC. |
393 | |
394 | + [Marc Tardif] |
395 | + * Fixed duplicate jobs appearing in the store when rerunning jobs. |
396 | + |
397 | [Sylvain Pineau] |
398 | * [FEATURE] jobs/info.txt.in: added new attachments, lspci -vvnnQ and |
399 | lsusb -vv and ensure outputs of lscpi, lsusb and dmidecode return UTF8. |
400 | |
401 | === modified file 'plugins/jobs_info.py' |
402 | --- plugins/jobs_info.py 2012-06-21 17:08:04 +0000 |
403 | +++ plugins/jobs_info.py 2012-06-28 16:54:19 +0000 |
404 | @@ -183,7 +183,7 @@ |
405 | |
406 | if not self.check_ordered_messages(messages): |
407 | old_message_names = [message["name"] + "\n" for message in messages] |
408 | - resolver = Resolver(key=lambda m: m["name"]) |
409 | + resolver = Resolver(key_func=lambda m: m["name"]) |
410 | for message in messages: |
411 | resolver.add( |
412 | message, *message.get("depends", [])) |
Changes still past unit tests and work correctly with the same whitelists that were used to verify them in trunk. Besides this, no error or traceback is printed to the log file on a regular execution.