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
=== modified file 'checkbox/job.py'
--- checkbox/job.py 2012-05-31 20:21:08 +0000
+++ checkbox/job.py 2012-06-13 16:02:19 +0000
@@ -107,26 +107,11 @@
107 def add(self, job):107 def add(self, job):
108 # TODO: Order alphabetically within suite or non-suite108 # TODO: Order alphabetically within suite or non-suite
109109
110 # Remove the same job if it already exists without a suite
111 if "suite" in job:
112 for filename in self._find_matching_messages(name=job["name"], suite=None):
113 os.unlink(filename)
114
115 # Return if the same job is already in the store110 # Return if the same job is already in the store
116 if list(self._find_matching_messages(name=job["name"])):111 if list(self._find_matching_messages(name=job["name"])):
117 return112 return
118113
119 message_id = super(JobStore, self).add(job)114 message_id = super(JobStore, self).add(job)
120
121 # TODO: Apply dependencies
122 if "depends" in job:
123 for depends in job["depends"]:
124 for filename in self._find_matching_messages():
125 message = self._read_message(filename, cache=True)
126 if job["name"] in message.get("depends", []):
127 new_filename = self._get_next_message_filename()
128 os.rename(filename, new_filename)
129
130 return message_id115 return message_id
131116
132 # TODO: Optimize by only searching backwards until a given condition117 # TODO: Optimize by only searching backwards until a given condition
133118
=== modified file 'debian/changelog'
--- debian/changelog 2012-06-13 15:25:12 +0000
+++ debian/changelog 2012-06-13 16:02:19 +0000
@@ -115,6 +115,7 @@
115115
116checkbox (0.13.8) precise; urgency=low116checkbox (0.13.8) precise; urgency=low
117117
118 [ Daniel Manrique ]
118 [Brendan Donegan]119 [Brendan Donegan]
119 * Run fwts_test as root so that the log can be written to on servers and120 * Run fwts_test as root so that the log can be written to on servers and
120 also because it's supposed to be run as root (LP: #989701)121 also because it's supposed to be run as root (LP: #989701)
@@ -159,11 +160,18 @@
159 * [FEATURE] jobs/info.txt.in: added new attachments, lspci -vvnnQ and160 * [FEATURE] jobs/info.txt.in: added new attachments, lspci -vvnnQ and
160 lsusb -vv and ensure outputs of lscpi, lsusb and dmidecode return UTF8.161 lsusb -vv and ensure outputs of lscpi, lsusb and dmidecode return UTF8.
161162
163<<<<<<< TREE
162 [Tim Chen]164 [Tim Chen]
163 * Use nmcli con delete instead of deleting the connection file, also avoid165 * Use nmcli con delete instead of deleting the connection file, also avoid
164 bringing eth0 down when running the wireless_monitoring tests.166 bringing eth0 down when running the wireless_monitoring tests.
165167
166 -- Jeff Lane <jeff@ubuntu.com> Mon, 14 May 2012 10:20:59 -0400168 -- Jeff Lane <jeff@ubuntu.com> Mon, 14 May 2012 10:20:59 -0400
169=======
170 [ Javier Collado ]
171 * Make sure that jobs are topologically ordered (LP: #990075)
172
173 -- Javier Collado <javier.collado@canonical.com> Wed, 02 May 2012 12:40:29 +0200
174>>>>>>> MERGE-SOURCE
167175
168checkbox (0.13.7) precise; urgency=low176checkbox (0.13.7) precise; urgency=low
169177
170178
=== modified file 'plugins/jobs_info.py'
--- plugins/jobs_info.py 2012-06-12 15:03:42 +0000
+++ plugins/jobs_info.py 2012-06-13 16:02:19 +0000
@@ -16,9 +16,12 @@
16# You should have received a copy of the GNU General Public License16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18#18#
19import os, sys, re19import os
20import sys
21import re
20import gettext22import gettext
21import logging23import logging
24import difflib
2225
23from collections import defaultdict26from collections import defaultdict
2427
@@ -108,18 +111,23 @@
108 return [re.compile(r"^%s$" % s) for s in strings111 return [re.compile(r"^%s$" % s) for s in strings
109 if s and not s.startswith("#")]112 if s and not s.startswith("#")]
110113
111
112 def gather(self):114 def gather(self):
113 # Register temporary handler for report-message events115 # Register temporary handler for report-message events
114 messages = []116 messages = []
115117
116 def report_message(message):118 def report_message(new_message):
119 name = new_message["name"]
117 if self.whitelist_patterns:120 if self.whitelist_patterns:
118 name = message["name"]
119 if not [name for p in self.whitelist_patterns if p.match(name)]:121 if not [name for p in self.whitelist_patterns if p.match(name)]:
120 return122 return
121123
122 messages.append(message)124 for message in messages:
125 if message['name'] == name:
126 # Update message to be added to the store
127 message.update(new_message)
128 else:
129 # Add message if it wasn't previously added
130 messages.append(new_message)
123131
124 # Set domain and message event handler132 # Set domain and message event handler
125 old_domain = gettext.textdomain()133 old_domain = gettext.textdomain()
@@ -130,6 +138,7 @@
130 self._manager.reactor.fire("message-directory", directory)138 self._manager.reactor.fire("message-directory", directory)
131139
132 # Apply whitelist ordering140 # Apply whitelist ordering
141<<<<<<< TREE
133 def key_function(obj):142 def key_function(obj):
134 name = obj["name"]143 name = obj["name"]
135 for pattern in self.whitelist_patterns:144 for pattern in self.whitelist_patterns:
@@ -139,8 +148,52 @@
139148
140 return index149 return index
141150
151=======
152>>>>>>> MERGE-SOURCE
142 if self.whitelist_patterns:153 if self.whitelist_patterns:
154<<<<<<< TREE
143 messages = sorted(messages, key=key_function)155 messages = sorted(messages, key=key_function)
156=======
157 def cmp_function(a, b):
158 a_name = a["name"]
159 b_name = b["name"]
160 for pattern in self.whitelist_patterns:
161 if pattern.match(a_name):
162 a_index = self.whitelist_patterns.index(pattern)
163 elif pattern.match(b_name):
164 b_index = self.whitelist_patterns.index(pattern)
165
166 return cmp(a_index, b_index)
167
168 messages = sorted(messages, cmp=cmp_function)
169
170 # Check if messages are already topologically ordered
171 is_topological_sort = self.is_topological_sort(messages)
172
173 if not is_topological_sort:
174 # Apply topological sorting
175 old_message_names = ['{0}\n'.format(message['name'])
176 for message in messages]
177 messages = self.topological_sort(messages)
178 new_message_names = ['{0}\n'.format(message['name'])
179 for message in messages]
180
181 if self.whitelist_patterns:
182 diff = ''.join(difflib
183 .unified_diff(old_message_names,
184 new_message_names,
185 'old whitelist',
186 'new whitelist'))
187 detailed_text = 'Whitelist diff output:\n{0}'.format(diff)
188 self._manager.reactor.fire(
189 'prompt-error',
190 self.interface,
191 'Whitelist not topologically ordered',
192 'Jobs have been reordered to fix broken dependencies',
193 detailed_text)
194
195 # Add jobs to the store
196>>>>>>> MERGE-SOURCE
144 for message in messages:197 for message in messages:
145 self._manager.reactor.fire("report-job", message)198 self._manager.reactor.fire("report-job", message)
146199
@@ -148,6 +201,80 @@
148 self._manager.reactor.cancel_call(event_id)201 self._manager.reactor.cancel_call(event_id)
149 gettext.textdomain(old_domain)202 gettext.textdomain(old_domain)
150203
204<<<<<<< TREE
205=======
206 def is_topological_sort(self, messages):
207 """
208 Return True if messages are topologically ordered
209 """
210 message_ordering = {message['name']: index
211 for index, message in enumerate(messages)}
212
213 def check_dependencies_order(message):
214 dependencies = message.get('depends', '').split()
215 message_order = message_ordering[message['name']]
216 return all(message_ordering[dependency] > message_order
217 for dependency in dependencies)
218
219 return all(check_dependencies_order(message)
220 for message in messages)
221
222 def topological_sort(self, messages):
223 """
224 Sort messages topologically
225 """
226 whitelist = {message['name']: message
227 for message in messages}
228
229 # Map messages to their dependencies
230 depends = defaultdict(list)
231 for message in messages:
232 m_name = message['name']
233 m_depends = message.get('depends', '').split()
234 for dependency in m_depends:
235 # Ignore dependencies against
236 # messages that haven't been whitelisted
237 if dependency in whitelist:
238 depends[m_name].append(dependency)
239
240 # Map messages to their reverse dependencies
241 rdepends = defaultdict(list)
242 for m_name, m_depends in depends.items():
243 for dependency in m_depends:
244 rdepends[dependency].append(m_name)
245
246 sorted_messages = []
247 skipped_messages = set()
248
249 def message_append(m_name):
250 """
251 Append messages without breaking dependencies
252 trying to follow whitelist order closely
253 """
254 # Skip message if depends on other messages
255 if len(depends[m_name]) > 0:
256 skipped_messages.add(m_name)
257 return
258
259 message = whitelist[m_name]
260 sorted_messages.append(message)
261
262 for r_name in rdepends[m_name]:
263 depends[r_name].remove(m_name)
264
265 if r_name in skipped_messages:
266 skipped_messages.remove(r_name)
267 message_append(r_name)
268
269 # Traverse messages and add them to sorted_messages
270 # in the correct order that doesn't break any dependency
271 for message in messages:
272 message_append(message['name'])
273
274 assert len(messages) == len(sorted_messages)
275 return sorted_messages
276
277>>>>>>> MERGE-SOURCE
151 def post_gather(self, interface):278 def post_gather(self, interface):
152 """279 """
153 Verify that all patterns were used280 Verify that all patterns were used

Subscribers

People subscribed via source and target branches