Merge lp:~javier.collado/checkbox/bug990075 into lp:checkbox

Proposed by Javier Collado
Status: Rejected
Rejected by: Javier Collado
Proposed branch: lp:~javier.collado/checkbox/bug990075
Merge into: lp:checkbox
Diff against target: 249 lines (+140/-20) (has conflicts)
3 files modified
checkbox/job.py (+0/-15)
debian/changelog (+8/-0)
plugins/jobs_info.py (+132/-5)
Text conflict in debian/changelog
Text conflict in plugins/jobs_info.py
To merge this branch: bzr merge lp:~javier.collado/checkbox/bug990075
Reviewer Review Type Date Requested Status
Javier Collado (community) Disapprove
Marc Tardif (community) Needs Fixing
Brendan Donegan (community) Needs Fixing
Review via email: mp+104369@code.launchpad.net

Description of the change

This change fixes the job ordering problem found bug990075 as follows:

- Removes the code in JobStore that takes of ordering and duplicate removal
The JobStore was expected to order jobs based on their dependencies when added one by one. This wasn't very efficient because the store had to perform the reordering every time a job was added and it was confusing to the programmer because it wasn't clear the algorithm being used.

- Adds topological sorting to the jobs_info.py plugin
The jobs_info.py plugin contains the whitelist ordering code, so it makes sense to take care of reordering jobs right there also to meet dependencies. Since whitelist ordering might be applied as well, the strategy used is check if the whitelist already provides a valid topological sort. If the check isn't successful, then the sorting is performed once for the whole graph using depth first search before adding any job to the JobStore.

One more thing to consider is that when test suites are added to the job store, "report-message" events are triggered with messages that contain updated information for test cases (like which one is their test suite). Before this fix, a new message was added to the list of messages and the JobStore took care of removing duplicates. Now, since the jobs are already ordered, they are updated instead to preserve their ordering in the jobs_info.py plugin prior to adding jobs to the JobStore.

To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

One pending thing to check is what happens in the presence of circular dependencies.
Since depth-first search stops when a job has already been inspected, I don't expect
any side effect like and infinite loop.

Looking at the code, it seems that they're already handled in the suites_prompt.py plugin
using checkbox.lib.resolver.Resolver, which actually seems to address topological order,
but I didn't use it because it wasn't used either in the original code (I guess there was
a good reason for that).

Hence, what should happen is that an exception is raised in checkbox.lib.resolver.Resolver
as it would have happened in the past.

Revision history for this message
Brendan Donegan (brendan-donegan) wrote :

I found that running the client-cert.whitelist results in the mediacard tests (before suspend) being run after the suspend test, even though they are listed before it in the whitelist.

review: Needs Fixing
Revision history for this message
Javier Collado (javier.collado) wrote :

The problem is that there are some dependencies that are not explicitly set in the jobs.

For example, there are many jobs that are expected to be executed before suspend/suspend_advanced, but they haven't been added as a dependency because that will make its list of dependencies really long.

One way to workaround this issue would be to reorder the whitelist directly to make sure that the ordering is the expected one, but this will also require to be reviewed every time a job is added, so it will be easy to break the whitelist inadvertently.

Ideally, there should be some midpoint in which a whitelist that is ordered almost nicely can be reordered without breaking any of these hidden dependencies against the suspend/suspend_advanced job.

1377. By Javier Collado

Implemented new algorithm to order jobs

The new algorithm no longer uses a standard topological ordering algorithm
because of the lack of information available in job descriptions. For example,
most of the jobs are expected to be executed before suspend/suspend_advanced,
but they are not listed as a dependency for this job. Instead, just the jobs
that are expected to be executed after suspend depend on
suspend/suspend_advanced.

To address this problem, it's assumed that the whitelist is already almost
fine, but there are just a few test cases that are out of order (if this is not
the case, the final ordering will probably not be valid). After that, a list of
dependencies and reverse dependencies is created and an attempt to order all
the messages in the whitelist one by one is made. If the order in the whitelist
is fine, then the messsage is added to the list of sorted jobs, if that's not
the case, the message is cached and added as soon as the missing dependencies
have been added as well.

Revision history for this message
Javier Collado (javier.collado) wrote :

The reordering method has been changed as explained in the commite message for rev. 1377.

The following test suites have been tested: client-cert whitelist, smoke and the one attached to the bug and they look good.

review: Needs Resubmitting
Revision history for this message
Brendan Donegan (brendan-donegan) wrote :

This sounds more like what I assumed would happen. I'll give it a test in a little while and try to merge it today

1378. By Javier Collado

Added message to let the user know what's the final ordering being used

1379. By Javier Collado

Updated whitelist error message to display detailed information properly

This change needs the fix for bug1012052 to work fine.

Revision history for this message
Marc Tardif (cr3) wrote :

Since the time this branch was first proposed for merging, the trunk has been converted to Python 3. I've just noticed that this branch uses the cmp keyword argument to the sorted function, which is no longer supported in Python 3. And, there might be other constructs that are no longer supported. So, I'm setting this branch as "Needs Fixing" just so that it's at least tested against Python 3 before being resubmitted.

review: Needs Fixing
1380. By Javier Collado

Removed custom code to provide readable output and replaced with difflib

Instead of keeping track of broken dependencies, difflib is used against the
original whitelist and the new whitelist after the job reordering to provide a
diff style output that is much more readable to the user.

1381. By Javier Collado

Added text to make clear what is related to the old/new whitelist

Revision history for this message
Javier Collado (javier.collado) wrote :

Since the branch is now old after all the python3 changes and was also has been used to create a merge request against 0.13, I'm rejecting this merge request and creating a new one with an updated version of the changes.

review: Disapprove

Unmerged revisions

1382. By Javier Collado

Fixed dependency ordering check

1381. By Javier Collado

Added text to make clear what is related to the old/new whitelist

1380. By Javier Collado

Removed custom code to provide readable output and replaced with difflib

Instead of keeping track of broken dependencies, difflib is used against the
original whitelist and the new whitelist after the job reordering to provide a
diff style output that is much more readable to the user.

1379. By Javier Collado

Updated whitelist error message to display detailed information properly

This change needs the fix for bug1012052 to work fine.

1378. By Javier Collado

Added message to let the user know what's the final ordering being used

1377. By Javier Collado

Implemented new algorithm to order jobs

The new algorithm no longer uses a standard topological ordering algorithm
because of the lack of information available in job descriptions. For example,
most of the jobs are expected to be executed before suspend/suspend_advanced,
but they are not listed as a dependency for this job. Instead, just the jobs
that are expected to be executed after suspend depend on
suspend/suspend_advanced.

To address this problem, it's assumed that the whitelist is already almost
fine, but there are just a few test cases that are out of order (if this is not
the case, the final ordering will probably not be valid). After that, a list of
dependencies and reverse dependencies is created and an attempt to order all
the messages in the whitelist one by one is made. If the order in the whitelist
is fine, then the messsage is added to the list of sorted jobs, if that's not
the case, the message is cached and added as soon as the missing dependencies
have been added as well.

1376. By Javier Collado

Make sure that jobs are topologically ordered (LP: #990075)

1375. By Javier Collado

Removed unused code to delete duplicates from job store

jobs_info.py plugin now takes care of the duplicates by updating the message
list before any message is added to the store. Hence, the code to remove files
from store for duplicated message is no longer needed.

1374. By Javier Collado

Fixed report-message callback to update existing messages

In the past messages for the same job were added multiple times and it looks
like the store took care of removing duplicates. Now, given that jobs are
already ordered, they can be updated in place before adding them to the store.

1373. By Javier Collado

Added topological sort implementation

Unfortunately, this breaks the selection dialog and the test cases
are no long displayed under their test suites.

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-05-31 20:21:08 +0000
3+++ checkbox/job.py 2012-06-13 16:02:19 +0000
4@@ -107,26 +107,11 @@
5 def add(self, job):
6 # TODO: Order alphabetically within suite or non-suite
7
8- # Remove the same job if it already exists without a suite
9- if "suite" in job:
10- for filename in self._find_matching_messages(name=job["name"], suite=None):
11- os.unlink(filename)
12-
13 # Return if the same job is already in the store
14 if list(self._find_matching_messages(name=job["name"])):
15 return
16
17 message_id = super(JobStore, self).add(job)
18-
19- # TODO: Apply dependencies
20- if "depends" in job:
21- for depends in job["depends"]:
22- for filename in self._find_matching_messages():
23- message = self._read_message(filename, cache=True)
24- if job["name"] in message.get("depends", []):
25- new_filename = self._get_next_message_filename()
26- os.rename(filename, new_filename)
27-
28 return message_id
29
30 # TODO: Optimize by only searching backwards until a given condition
31
32=== modified file 'debian/changelog'
33--- debian/changelog 2012-06-13 15:25:12 +0000
34+++ debian/changelog 2012-06-13 16:02:19 +0000
35@@ -115,6 +115,7 @@
36
37 checkbox (0.13.8) precise; urgency=low
38
39+ [ Daniel Manrique ]
40 [Brendan Donegan]
41 * Run fwts_test as root so that the log can be written to on servers and
42 also because it's supposed to be run as root (LP: #989701)
43@@ -159,11 +160,18 @@
44 * [FEATURE] jobs/info.txt.in: added new attachments, lspci -vvnnQ and
45 lsusb -vv and ensure outputs of lscpi, lsusb and dmidecode return UTF8.
46
47+<<<<<<< TREE
48 [Tim Chen]
49 * Use nmcli con delete instead of deleting the connection file, also avoid
50 bringing eth0 down when running the wireless_monitoring tests.
51
52 -- Jeff Lane <jeff@ubuntu.com> Mon, 14 May 2012 10:20:59 -0400
53+=======
54+ [ Javier Collado ]
55+ * Make sure that jobs are topologically ordered (LP: #990075)
56+
57+ -- Javier Collado <javier.collado@canonical.com> Wed, 02 May 2012 12:40:29 +0200
58+>>>>>>> MERGE-SOURCE
59
60 checkbox (0.13.7) precise; urgency=low
61
62
63=== modified file 'plugins/jobs_info.py'
64--- plugins/jobs_info.py 2012-06-12 15:03:42 +0000
65+++ plugins/jobs_info.py 2012-06-13 16:02:19 +0000
66@@ -16,9 +16,12 @@
67 # You should have received a copy of the GNU General Public License
68 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
69 #
70-import os, sys, re
71+import os
72+import sys
73+import re
74 import gettext
75 import logging
76+import difflib
77
78 from collections import defaultdict
79
80@@ -108,18 +111,23 @@
81 return [re.compile(r"^%s$" % s) for s in strings
82 if s and not s.startswith("#")]
83
84-
85 def gather(self):
86 # Register temporary handler for report-message events
87 messages = []
88
89- def report_message(message):
90+ def report_message(new_message):
91+ name = new_message["name"]
92 if self.whitelist_patterns:
93- name = message["name"]
94 if not [name for p in self.whitelist_patterns if p.match(name)]:
95 return
96
97- messages.append(message)
98+ for message in messages:
99+ if message['name'] == name:
100+ # Update message to be added to the store
101+ message.update(new_message)
102+ else:
103+ # Add message if it wasn't previously added
104+ messages.append(new_message)
105
106 # Set domain and message event handler
107 old_domain = gettext.textdomain()
108@@ -130,6 +138,7 @@
109 self._manager.reactor.fire("message-directory", directory)
110
111 # Apply whitelist ordering
112+<<<<<<< TREE
113 def key_function(obj):
114 name = obj["name"]
115 for pattern in self.whitelist_patterns:
116@@ -139,8 +148,52 @@
117
118 return index
119
120+=======
121+>>>>>>> MERGE-SOURCE
122 if self.whitelist_patterns:
123+<<<<<<< TREE
124 messages = sorted(messages, key=key_function)
125+=======
126+ def cmp_function(a, b):
127+ a_name = a["name"]
128+ b_name = b["name"]
129+ for pattern in self.whitelist_patterns:
130+ if pattern.match(a_name):
131+ a_index = self.whitelist_patterns.index(pattern)
132+ elif pattern.match(b_name):
133+ b_index = self.whitelist_patterns.index(pattern)
134+
135+ return cmp(a_index, b_index)
136+
137+ messages = sorted(messages, cmp=cmp_function)
138+
139+ # Check if messages are already topologically ordered
140+ is_topological_sort = self.is_topological_sort(messages)
141+
142+ if not is_topological_sort:
143+ # Apply topological sorting
144+ old_message_names = ['{0}\n'.format(message['name'])
145+ for message in messages]
146+ messages = self.topological_sort(messages)
147+ new_message_names = ['{0}\n'.format(message['name'])
148+ for message in messages]
149+
150+ if self.whitelist_patterns:
151+ diff = ''.join(difflib
152+ .unified_diff(old_message_names,
153+ new_message_names,
154+ 'old whitelist',
155+ 'new whitelist'))
156+ detailed_text = 'Whitelist diff output:\n{0}'.format(diff)
157+ self._manager.reactor.fire(
158+ 'prompt-error',
159+ self.interface,
160+ 'Whitelist not topologically ordered',
161+ 'Jobs have been reordered to fix broken dependencies',
162+ detailed_text)
163+
164+ # Add jobs to the store
165+>>>>>>> MERGE-SOURCE
166 for message in messages:
167 self._manager.reactor.fire("report-job", message)
168
169@@ -148,6 +201,80 @@
170 self._manager.reactor.cancel_call(event_id)
171 gettext.textdomain(old_domain)
172
173+<<<<<<< TREE
174+=======
175+ def is_topological_sort(self, messages):
176+ """
177+ Return True if messages are topologically ordered
178+ """
179+ message_ordering = {message['name']: index
180+ for index, message in enumerate(messages)}
181+
182+ def check_dependencies_order(message):
183+ dependencies = message.get('depends', '').split()
184+ message_order = message_ordering[message['name']]
185+ return all(message_ordering[dependency] > message_order
186+ for dependency in dependencies)
187+
188+ return all(check_dependencies_order(message)
189+ for message in messages)
190+
191+ def topological_sort(self, messages):
192+ """
193+ Sort messages topologically
194+ """
195+ whitelist = {message['name']: message
196+ for message in messages}
197+
198+ # Map messages to their dependencies
199+ depends = defaultdict(list)
200+ for message in messages:
201+ m_name = message['name']
202+ m_depends = message.get('depends', '').split()
203+ for dependency in m_depends:
204+ # Ignore dependencies against
205+ # messages that haven't been whitelisted
206+ if dependency in whitelist:
207+ depends[m_name].append(dependency)
208+
209+ # Map messages to their reverse dependencies
210+ rdepends = defaultdict(list)
211+ for m_name, m_depends in depends.items():
212+ for dependency in m_depends:
213+ rdepends[dependency].append(m_name)
214+
215+ sorted_messages = []
216+ skipped_messages = set()
217+
218+ def message_append(m_name):
219+ """
220+ Append messages without breaking dependencies
221+ trying to follow whitelist order closely
222+ """
223+ # Skip message if depends on other messages
224+ if len(depends[m_name]) > 0:
225+ skipped_messages.add(m_name)
226+ return
227+
228+ message = whitelist[m_name]
229+ sorted_messages.append(message)
230+
231+ for r_name in rdepends[m_name]:
232+ depends[r_name].remove(m_name)
233+
234+ if r_name in skipped_messages:
235+ skipped_messages.remove(r_name)
236+ message_append(r_name)
237+
238+ # Traverse messages and add them to sorted_messages
239+ # in the correct order that doesn't break any dependency
240+ for message in messages:
241+ message_append(message['name'])
242+
243+ assert len(messages) == len(sorted_messages)
244+ return sorted_messages
245+
246+>>>>>>> MERGE-SOURCE
247 def post_gather(self, interface):
248 """
249 Verify that all patterns were used

Subscribers

People subscribed via source and target branches