Merge lp:~cr3/checkbox/0.13_resolver into lp:checkbox/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
Reviewer Review Type Date Requested Status
Javier Collado (community) Approve
Review via email: mp+112610@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

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.

review: Approve

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", []))

Subscribers

People subscribed via source and target branches