Merge lp:~cr3/checkbox/pre-task-525.5 into lp:checkbox/0.9

Proposed by Marc Tardif
Status: Merged
Merged at revision: 809
Proposed branch: lp:~cr3/checkbox/pre-task-525.5
Merge into: lp:checkbox/0.9
Diff against target: 6242 lines (+3652/-975)
72 files modified
apport/source_checkbox.py (+3/-0)
backend (+5/-0)
checkbox/application.py (+9/-6)
checkbox/contrib/persist.py (+20/-13)
checkbox/dispatcher.py (+201/-0)
checkbox/job.py (+12/-3)
checkbox/lib/bit.py (+8/-4)
checkbox/lib/conversion.py (+131/-42)
checkbox/lib/dmi.py (+169/-18)
checkbox/lib/enum.py (+72/-0)
checkbox/lib/fifo.py (+29/-4)
checkbox/lib/log.py (+12/-3)
checkbox/lib/process.py (+7/-5)
checkbox/lib/safe.py (+5/-0)
checkbox/lib/selector.py (+207/-0)
checkbox/lib/template.py (+4/-1)
checkbox/lib/template_i18n.py (+2/-1)
checkbox/lib/transport.py (+85/-2)
checkbox/lib/usb.py (+2/-0)
checkbox/message.py (+21/-4)
checkbox/parsers/cpuinfo.py (+180/-0)
checkbox/parsers/cputable (+40/-0)
checkbox/parsers/cputable.py (+42/-0)
checkbox/parsers/deferred.py (+27/-0)
checkbox/parsers/description.py (+74/-0)
checkbox/parsers/device.py (+24/-0)
checkbox/parsers/dmidecode.py (+123/-0)
checkbox/parsers/meminfo.py (+46/-0)
checkbox/parsers/submission.py (+540/-0)
checkbox/parsers/tests/cputable.py (+74/-0)
checkbox/parsers/tests/description.py (+146/-0)
checkbox/parsers/tests/dmi.py (+80/-0)
checkbox/parsers/tests/dmidecode.py (+60/-0)
checkbox/parsers/tests/udevadm.py (+68/-0)
checkbox/parsers/udevadm.py (+470/-0)
checkbox/parsers/utils.py (+36/-0)
checkbox/properties.py (+6/-1)
checkbox/reactor.py (+16/-11)
checkbox/report.py (+39/-12)
checkbox/resource.py (+32/-13)
checkbox/tests/report.py (+1/-1)
checkbox/variables.py (+15/-1)
checkbox_cli/cli_interface.py (+16/-9)
debian/control (+1/-1)
examples/checkbox.ini (+1/-1)
jobs/resource.txt.in (+5/-0)
plugins/apport_prompt.py (+20/-5)
plugins/backend_info.py (+66/-15)
plugins/jobs_info.py (+40/-8)
plugins/jobs_prompt.py (+42/-4)
plugins/lock_prompt.py (+14/-0)
plugins/manual_test.py (+3/-1)
plugins/message_info.py (+30/-1)
plugins/persist_info.py (+7/-4)
plugins/remote_suite.py (+1/-0)
plugins/report_prompt.py (+7/-0)
plugins/resource_info.py (+16/-2)
plugins/scripts_info.py (+3/-1)
plugins/shell_test.py (+3/-2)
plugins/suites_prompt.py (+35/-20)
plugins/system_info.py (+12/-4)
plugins/user_interface.py (+6/-0)
po/checkbox.pot (+67/-45)
scripts/cdimage_resource (+2/-1)
scripts/cpuinfo_resource (+18/-158)
scripts/dmi_resource (+55/-0)
scripts/gconf_resource (+1/-1)
scripts/hal_resource (+0/-5)
scripts/meminfo_resource (+15/-27)
scripts/package_resource (+6/-0)
scripts/udev_resource (+15/-514)
setup.py (+2/-1)
To merge this branch: bzr merge lp:~cr3/checkbox/pre-task-525.5
Reviewer Review Type Date Requested Status
Brendan Donegan (community) Approve
Marc Tardif (community) Needs Resubmitting
Review via email: mp+93881@code.launchpad.net

Description of the change

This has been tested to build successfully and submit results to Launchpad. No errors encountered in the log when running.

To post a comment you must log in.
lp:~cr3/checkbox/pre-task-525.5 updated
810. By Marc Tardif

Added python-dateutil to checkbox recommends.

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

Added python-dateutil to checkbox recommends, thanks!

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

Considering that this has been checked by both of us now and I've had a thorough read of the diff, it's time to take the plunge and merge it.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'apport/source_checkbox.py'
2--- apport/source_checkbox.py 2010-02-25 19:09:59 +0000
3+++ apport/source_checkbox.py 2012-02-21 14:59:19 +0000
4@@ -15,3 +15,6 @@
5
6 SUBMISSION = os.path.join(CACHE_HOME, 'checkbox', 'submission.xml')
7 attach_file_if_exists(report, SUBMISSION)
8+
9+ CHECKBOX_LOG = os.path.join(CACHE_HOME, 'checkbox', 'checkbox.log')
10+ attach_file_if_exists(report, CHECKBOX_LOG)
11
12=== modified file 'backend'
13--- backend 2010-04-20 16:23:16 +0000
14+++ backend 2012-02-21 14:59:19 +0000
15@@ -28,6 +28,11 @@
16 while True:
17 try:
18 message = reader.read_object()
19+ if message == "stop":
20+ break
21+ if message == "ping":
22+ writer.write_object("pong")
23+ continue
24 if isinstance(message, dict) and "command" in message:
25 job = Job(message["command"], message.get("environ"),
26 message.get("timeout"))
27
28=== modified file 'checkbox/application.py'
29--- checkbox/application.py 2010-02-25 17:37:40 +0000
30+++ checkbox/application.py 2012-02-21 14:59:19 +0000
31@@ -16,6 +16,7 @@
32 # You should have received a copy of the GNU General Public License
33 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
34 #
35+import os
36 import sys
37 import logging
38 import posixpath
39@@ -58,7 +59,8 @@
40
41 application_factory = Application
42
43- default_log_level = "critical"
44+ default_log = os.path.join(get_variable("CHECKBOX_DATA", "."), "checkbox.log")
45+ default_log_level = "debug"
46
47 def parse_options(self, args):
48 usage = _("Usage: checkbox [OPTIONS]")
49@@ -68,6 +70,7 @@
50 help=_("Print version information and exit."))
51 parser.add_option("-l", "--log",
52 metavar="FILE",
53+ default=self.default_log,
54 help=_("The file to write the log to."))
55 parser.add_option("--log-level",
56 default=self.default_log_level,
57@@ -78,13 +81,13 @@
58 default=[],
59 help=_("Configuration override parameters."))
60 parser.add_option("-b", "--blacklist",
61- help=_("Shorthand for --config=checkbox/plugins/jobs_info/blacklist."))
62+ help=_("Shorthand for --config=.*/jobs_info/blacklist."))
63 parser.add_option("-B", "--blacklist-file",
64- help=_("Shorthand for --config=checkbox/plugins/jobs_info/blacklist_file."))
65+ help=_("Shorthand for --config=.*/jobs_info/blacklist_file."))
66 parser.add_option("-w", "--whitelist",
67- help=_("Shorthand for --config=checkbox/plugins/jobs_info/whitelist."))
68+ help=_("Shorthand for --config=.*/jobs_info/whitelist."))
69 parser.add_option("-W", "--whitelist-file",
70- help=_("Shorthand for --config=checkbox/plugins/jobs_info/whitelist_file."))
71+ help=_("Shorthand for --config=.*/jobs_info/whitelist_file."))
72 return parser.parse_args(args)
73
74 def create_application(self, args=sys.argv):
75@@ -99,7 +102,7 @@
76
77 # Replace shorthands
78 for shorthand in "blacklist", "blacklist_file", "whitelist", "whitelist_file":
79- key = "checkbox/plugins/jobs_info/%s" % shorthand
80+ key = ".*/jobs_info/%s" % shorthand
81 value = getattr(options, shorthand)
82 if value:
83 options.config.append("=".join([key, value]))
84
85=== modified file 'checkbox/contrib/persist.py'
86--- checkbox/contrib/persist.py 2009-02-15 15:40:07 +0000
87+++ checkbox/contrib/persist.py 2012-02-21 14:59:19 +0000
88@@ -23,8 +23,9 @@
89 import re
90 import posixpath
91
92+from checkbox.lib.safe import safe_close
93
94-__all__ = ["Persist", "PickleBackend", "BPickleBackend",
95+__all__ = ["Persist", "MemoryBackend", "PickleBackend", "BPickleBackend",
96 "path_string_to_tuple", "path_tuple_to_string", "RootedPersist",
97 "PersistError", "PersistReadOnlyError"]
98
99@@ -128,7 +129,7 @@
100 """
101 if filepath is None:
102 if self.filename is None:
103- raise PersistError("Need a filename!")
104+ return
105 filepath = self.filename
106 filepath = posixpath.expanduser(filepath)
107 if posixpath.isfile(filepath):
108@@ -410,7 +411,7 @@
109 class Backend(object):
110
111 def new(self):
112- raise NotImplementedError
113+ return {}
114
115 def load(self, filepath):
116 raise NotImplementedError
117@@ -494,28 +495,37 @@
118 return NotImplemented
119
120
121+class MemoryBackend(Backend):
122+
123+ def __init__(self):
124+ self._store = {}
125+
126+ def load(self, filepath):
127+ return self._store.get(filepath)
128+
129+ def save(self, filepath, map):
130+ self._store[filepath] = map
131+
132+
133 class PickleBackend(Backend):
134
135 def __init__(self):
136 import cPickle
137 self._pickle = cPickle
138
139- def new(self):
140- return {}
141-
142 def load(self, filepath):
143 file = open(filepath)
144 try:
145 return self._pickle.load(file)
146 finally:
147- file.close()
148+ safe_close(file)
149
150 def save(self, filepath, map):
151 file = open(filepath, "w")
152 try:
153 self._pickle.dump(map, file, 2)
154 finally:
155- file.close()
156+ safe_close(file)
157
158
159 class BPickleBackend(Backend):
160@@ -524,19 +534,16 @@
161 from checkbox.contrib import bpickle
162 self._bpickle = bpickle
163
164- def new(self):
165- return {}
166-
167 def load(self, filepath):
168 file = open(filepath)
169 try:
170 return self._bpickle.loads(file.read())
171 finally:
172- file.close()
173+ safe_close(file)
174
175 def save(self, filepath, map):
176 file = open(filepath, "w")
177 try:
178 file.write(self._bpickle.dumps(map))
179 finally:
180- file.close()
181+ safe_close(file)
182
183=== added file 'checkbox/dispatcher.py'
184--- checkbox/dispatcher.py 1970-01-01 00:00:00 +0000
185+++ checkbox/dispatcher.py 2012-02-21 14:59:19 +0000
186@@ -0,0 +1,201 @@
187+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
188+# GNU Affero General Public License version 3 (see the file LICENSE).
189+
190+__metaclass__ = type
191+
192+__all__ = [
193+ "Dispatcher",
194+ "DispatcherList",
195+ "DispatcherQueue",
196+ ]
197+
198+import logging
199+
200+from itertools import product
201+
202+
203+class Event:
204+ """Event payload containing the positional and keywoard arguments
205+ passed to the handler in the event listener."""
206+
207+ def __init__(self, type, *args, **kwargs):
208+ self.type = type
209+ self.args = args
210+ self.kwargs = kwargs
211+
212+
213+class Listener:
214+ """Event listener notified when events are published by the dispatcher."""
215+
216+ def __init__(self, event_type, handler, count):
217+ self.event_type = event_type
218+ self.handler = handler
219+ self.count = count
220+
221+ def notify(self, event):
222+ """Notify the handler with the payload of the event.
223+
224+ :param event: The event containint the payload for the handler.
225+ """
226+ if self.count is None or self.count:
227+ self.handler(*event.args, **event.kwargs)
228+ if self.count:
229+ self.count -= 1
230+
231+
232+class ListenerList(Listener):
233+ """Event listener notified for lists of events."""
234+
235+ def __init__(self, *args, **kwargs):
236+ super(ListenerList, self).__init__(*args, **kwargs)
237+ self.event_types = set(self.event_type)
238+ self.kwargs = {}
239+
240+ def notify(self, event):
241+ """Only notify the handler when all the events for this listener
242+ have been published by the dispatcher. When duplicate events
243+ occur, the latest event is preserved and the previous one are
244+ overwritten until all events have been published.
245+ """
246+ if self.count is None or self.count:
247+ self.kwargs[event.type] = event.args[0]
248+ if self.event_types.issubset(self.kwargs):
249+ self.handler(**self.kwargs)
250+ if self.count:
251+ self.count -= 1
252+
253+
254+class ListenerQueue(ListenerList):
255+
256+ def notify(self, event):
257+ """Only notify the handler when all the events for this listener
258+ have been published by the dispatcher. Duplicate events are enqueued
259+ and dequeued only when all events have been published.
260+ """
261+ arg = event.args[0]
262+ queue = self.kwargs.setdefault(event.type, [])
263+
264+ # Strip duplicates from the queue.
265+ if arg not in queue:
266+ queue.append(arg)
267+
268+ # Once the queue has handler has been called, the queue
269+ # then behaves like a list using the latest events.
270+ if self.event_types.issubset(self.kwargs):
271+ self.notify = notify = super(ListenerQueue, self).notify
272+ keys = self.kwargs.keys()
273+ for values in product(*self.kwargs.values()):
274+ self.kwargs = dict(zip(keys, values))
275+ notify(event)
276+
277+
278+class Dispatcher:
279+ """Register handlers and publish events for them identified by strings."""
280+
281+ listener_factory = Listener
282+
283+ def __init__(self, listener_factory=None):
284+ self._event_listeners = {}
285+
286+ if listener_factory is not None:
287+ self.listener_factory = listener_factory
288+
289+ def registerHandler(self, event_type, handler, count=None):
290+ """Register an event handler and return its listener.
291+
292+ :param event_type: The name of the event type to handle.
293+ :param handler: The function handling the given event type.
294+ :param count: Optionally, the number times to call the handler.
295+ """
296+ listener = self.listener_factory(event_type, handler, count)
297+
298+ listeners = self._event_listeners.setdefault(event_type, [])
299+ listeners.append(listener)
300+
301+ return listener
302+
303+ def unregisterHandler(self, handler):
304+ """Unregister a handler.
305+
306+ :param handler: The handler to unregister.
307+ """
308+ for event_type, listeners in self._event_listeners.items():
309+ listeners = [
310+ listener for listener in listeners
311+ if listener.handler == handler]
312+ if listeners:
313+ self._event_listeners[event_type] = listeners
314+ else:
315+ del self._event_listeners[event_type]
316+
317+ def unregisterListener(self, listener, event_type=None):
318+ """Unregister a listener.
319+
320+ :param listener: The listener of the handler to unregister.
321+ :param event_type: Optionally, the event_type to unregister.
322+ """
323+ if event_type is None:
324+ event_type = listener.event_type
325+
326+ self._event_listeners[event_type].remove(listener)
327+ if not self._event_listeners[event_type]:
328+ del self._event_listeners[event_type]
329+
330+ def publishEvent(self, event_type, *args, **kwargs):
331+ """Publish an event of a given type and notify all listeners.
332+
333+ :param event_type: The name of the event type to publish.
334+ :param args: Positional arguments to pass to the registered handlers.
335+ :param kwargs: Keyword arguments to pass to the registered handlers.
336+ """
337+ if event_type in self._event_listeners:
338+ event = Event(event_type, *args, **kwargs)
339+ for listener in list(self._event_listeners[event_type]):
340+ try:
341+ listener.notify(event)
342+ if listener.count is not None and not listener.count:
343+ self.unregisterListener(listener)
344+ except:
345+ logging.exception(
346+ "Error running event handler for %r with args %r %r",
347+ event_type, args, kwargs)
348+
349+
350+class DispatcherList(Dispatcher):
351+ """
352+ Register handlers and publish events for them identified by lists
353+ of strings.
354+ """
355+
356+ listener_factory = ListenerList
357+
358+ def registerHandler(self, event_types, handler, count=None):
359+ """See Dispatcher."""
360+ if not isinstance(event_types, (list, tuple,)):
361+ event_types = (event_types,)
362+
363+ listener = self.listener_factory(event_types, handler, count)
364+ for event_type in event_types:
365+ listeners = self._event_listeners.setdefault(event_type, [])
366+ listeners.append(listener)
367+
368+ return listener
369+
370+ def unregisterListener(self, listener):
371+ """See Dispatcher."""
372+ for event_type in listener.event_types:
373+ super(DispatcherList, self).unregisterListener(
374+ listener, event_type)
375+
376+ def publishEvent(self, event_type, arg):
377+ """See Dispatcher."""
378+ super(DispatcherList, self).publishEvent(event_type, arg)
379+
380+
381+class DispatcherQueue(DispatcherList):
382+ """
383+ Register handlers and publish events for them identified by lists
384+ of strings in queue order.
385+ """
386+
387+ listener_factory = ListenerQueue
388
389=== modified file 'checkbox/job.py'
390--- checkbox/job.py 2010-04-20 16:23:16 +0000
391+++ checkbox/job.py 2012-02-21 14:59:19 +0000
392@@ -37,6 +37,7 @@
393
394 ALL_STATUS = [FAIL, PASS, UNINITIATED, UNRESOLVED, UNSUPPORTED, UNTESTED]
395
396+DEFAULT_JOB_TIMEOUT = 30 # used in case a job specifies invalid timeout
397
398 class Job(object):
399
400@@ -47,6 +48,14 @@
401 self.command = command
402 self.environ = environ
403 self.timeout = timeout
404+ if self.timeout is not None:
405+ try:
406+ self.timeout = float(self.timeout)
407+ except:
408+ self.timeout = DEFAULT_JOB_TIMEOUT
409+ finally:
410+ if self.timeout < 0:
411+ self.timeout = DEFAULT_JOB_TIMEOUT
412
413 def execute(self):
414 # Sanitize environment
415@@ -112,8 +121,8 @@
416 # TODO: Apply dependencies
417 if "depends" in job:
418 for depends in job["depends"]:
419- for filename in self._find_matching_messages(suite=job.get("suite")):
420- message = self._read_message(filename)
421+ for filename in self._find_matching_messages():
422+ message = self._read_message(filename, cache=True)
423 if job["name"] in message.get("depends", []):
424 new_filename = self._get_next_message_filename()
425 os.rename(filename, new_filename)
426@@ -123,7 +132,7 @@
427 # TODO: Optimize by only searching backwards until a given condition
428 def _find_matching_messages(self, **kwargs):
429 for filename in self._walk_messages():
430- message = self._read_message(filename)
431+ message = self._read_message(filename,cache=True)
432 for key, value in kwargs.iteritems():
433 if message.get(key) != value:
434 break
435
436=== modified file 'checkbox/lib/bit.py'
437--- checkbox/lib/bit.py 2009-08-29 18:43:36 +0000
438+++ checkbox/lib/bit.py 2012-02-21 14:59:19 +0000
439@@ -16,6 +16,9 @@
440 # You should have received a copy of the GNU General Public License
441 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
442 #
443+from struct import calcsize
444+
445+
446 def get_bitmask(key):
447 bitmask = []
448 for value in reversed(key.split()):
449@@ -33,10 +36,11 @@
450
451 return bitcount
452
453-def test_bit(bit, bitmask):
454- bits_per_long = 8 * 8
455- offset = bit % bits_per_long
456- long = int(bit / bits_per_long)
457+def test_bit(bit, bitmask, bits=None):
458+ if bits is None:
459+ bits = calcsize("l") * 8
460+ offset = bit % bits
461+ long = int(bit / bits)
462 if long >= len(bitmask):
463 return 0
464 return (bitmask[long] >> offset) & 1
465
466=== modified file 'checkbox/lib/conversion.py'
467--- checkbox/lib/conversion.py 2009-02-16 15:48:24 +0000
468+++ checkbox/lib/conversion.py 2012-02-21 14:59:19 +0000
469@@ -18,48 +18,110 @@
470 #
471 import re
472
473-
474-def string_to_type(value):
475- conversion_table = (
476- ("(yes|true)", lambda v: True),
477- ("(no|false)", lambda v: False),
478- ("\d+", lambda v: int(v.group(0))),
479- ("\d+\.\d+", lambda v: float(v.group(0))),
480- ("(\d+) ?([kmgt]?b?)", lambda v: int(v.group(1))),
481- ("(\d+\.\d+) ?([kmgt]?b?)", lambda v: float(v.group(1))),
482- ("(\d+) ?([kmgt]?hz?)", lambda v: int(v.group(1))),
483- ("(\d+\.\d+) ?([kmgt]?hz?)", lambda v: float(v.group(1))))
484-
485- multiplier_table = (
486- ("b", 1),
487- ("kb?", 1024),
488- ("mb?", 1024 * 1024),
489- ("gb?", 1024 * 1024 * 1024),
490- ("tb?", 1024 * 1024 * 1024 * 1024),
491- ("hz", 1),
492- ("khz?", 1024),
493- ("mhz?", 1024 * 1024),
494- ("ghz?", 1024 * 1024 * 1024),
495- ("thz?", 1024 * 1024 * 1024 * 1024))
496-
497- if isinstance(value, basestring):
498- for regex, conversion in conversion_table:
499- match = re.match("^%s$" % regex, value, re.IGNORECASE)
500- if match:
501- value = conversion(match)
502- if len(match.groups()) < 2:
503- return value
504-
505- unit = match.group(2)
506- for regex, multiplier in multiplier_table:
507- match = re.match("^%s$" % regex, unit, re.IGNORECASE)
508- if match:
509- value *= multiplier
510- return value
511- else:
512- raise Exception, "Unknown multiplier: %s" % unit
513-
514- return value
515+from dateutil import tz
516+from datetime import (
517+ datetime,
518+ timedelta,
519+ )
520+
521+
522+DATETIME_RE = re.compile(r"""
523+ ^(?P<year>\d\d\d\d)-?(?P<month>\d\d)-?(?P<day>\d\d)
524+ T(?P<hour>\d\d):?(?P<minute>\d\d):?(?P<second>\d\d)
525+ (?:\.(?P<second_fraction>\d{0,6}))?
526+ (?P<tz>
527+ (?:(?P<tz_sign>[-+])(?P<tz_hour>\d\d):(?P<tz_minute>\d\d))
528+ | Z)?$
529+ """, re.VERBOSE)
530+
531+TYPE_FORMATS = (
532+ (r"(yes|true)", lambda v: True),
533+ (r"(no|false)", lambda v: False),
534+ (r"-?\d+", lambda v: int(v.group(0))),
535+ (r"-?\d+\.\d+", lambda v: float(v.group(0))),
536+ (r"(-?\d+) ?([kmgt]?b?)", lambda v: int(v.group(1))),
537+ (r"(-?\d+\.\d+) ?([kmgt]?b?)", lambda v: float(v.group(1))),
538+ (r"(-?\d+) ?([kmgt]?hz)", lambda v: int(v.group(1))),
539+ (r"(-?\d+\.\d+) ?([kmgt]?hz)", lambda v: float(v.group(1))))
540+TYPE_FORMATS = tuple(
541+ (re.compile(r"^%s$" % pattern, re.IGNORECASE), format)
542+ for pattern, format in TYPE_FORMATS)
543+
544+TYPE_MULTIPLIERS = (
545+ (r"b", 1),
546+ (r"kb?", 1024),
547+ (r"mb?", 1024 * 1024),
548+ (r"gb?", 1024 * 1024 * 1024),
549+ (r"tb?", 1024 * 1024 * 1024 * 1024),
550+ (r"hz", 1),
551+ (r"khz?", 1024),
552+ (r"mhz?", 1024 * 1024),
553+ (r"ghz?", 1024 * 1024 * 1024),
554+ (r"thz?", 1024 * 1024 * 1024 * 1024))
555+TYPE_MULTIPLIERS = tuple(
556+ (re.compile(r"^%s$" % pattern, re.IGNORECASE), multiplier)
557+ for pattern, multiplier in TYPE_MULTIPLIERS)
558+
559+
560+def datetime_to_string(dt):
561+ """Return a consistent string representation for a given datetime.
562+
563+ :param dt: The datetime object.
564+ """
565+ return dt.isoformat()
566+
567+def string_to_datetime(string):
568+ """Return a datetime object from a consistent string representation.
569+
570+ :param string: The string representation.
571+ """
572+ # we cannot use time.strptime: this function accepts neither fractions
573+ # of a second nor a time zone given e.g. as '+02:30'.
574+ match = DATETIME_RE.match(string)
575+
576+ # The Relax NG schema allows a leading minus sign and year numbers
577+ # with more than four digits, which are not "covered" by _time_regex.
578+ if not match:
579+ raise ValueError("Datetime with unreasonable value: %s" % string)
580+
581+ time_parts = match.groupdict()
582+
583+ year = int(time_parts['year'])
584+ month = int(time_parts['month'])
585+ day = int(time_parts['day'])
586+ hour = int(time_parts['hour'])
587+ minute = int(time_parts['minute'])
588+ second = int(time_parts['second'])
589+ second_fraction = time_parts['second_fraction']
590+ if second_fraction is not None:
591+ milliseconds = second_fraction + '0' * (6 - len(second_fraction))
592+ milliseconds = int(milliseconds)
593+ else:
594+ milliseconds = 0
595+
596+ # The Relax NG validator accepts leap seconds, but the datetime
597+ # constructor rejects them. The time values submitted by the HWDB
598+ # client are not necessarily very precise, hence we can round down
599+ # to 59.999999 seconds without losing any real precision.
600+ if second > 59:
601+ second = 59
602+ milliseconds = 999999
603+
604+ dt = datetime(
605+ year, month, day, hour, minute, second, milliseconds,
606+ tzinfo=tz.tzutc())
607+
608+ tz_sign = time_parts['tz_sign']
609+ tz_hour = time_parts['tz_hour']
610+ tz_minute = time_parts['tz_minute']
611+ if tz_sign in ('-', '+'):
612+ delta = timedelta(hours=int(tz_hour), minutes=int(tz_minute))
613+ if tz_sign == '-':
614+ dt = dt + delta
615+ else:
616+ dt = dt - delta
617+
618+ return dt
619
620 def sizeof_bytes(bytes):
621 for x in ["bytes", "KB", "MB", "GB", "TB"]:
622@@ -78,3 +140,30 @@
623 hertz /= 1000.0
624
625 return string
626+
627+def string_to_type(string):
628+ """Return a typed representation for the given string.
629+
630+ The result might be a bool, int or float. The string might also be
631+ supplemented by a multiplier like KB which would return an int or
632+ float multiplied by 1024 for example.
633+
634+ :param string: The string representation.
635+ """
636+ if isinstance(string, basestring):
637+ for regex, formatter in TYPE_FORMATS:
638+ match = regex.match(string)
639+ if match:
640+ string = formatter(match)
641+ if len(match.groups()) > 1:
642+ unit = match.group(2)
643+ for regex, multiplier in TYPE_MULTIPLIERS:
644+ match = regex.match(unit)
645+ if match:
646+ string *= multiplier
647+ break
648+ else:
649+ raise ValueError("Unknown multiplier: %s" % unit)
650+ break
651+
652+ return string
653
654=== modified file 'checkbox/lib/dmi.py'
655--- checkbox/lib/dmi.py 2009-09-06 14:19:16 +0000
656+++ checkbox/lib/dmi.py 2012-02-21 14:59:19 +0000
657@@ -16,13 +16,15 @@
658 # You should have received a copy of the GNU General Public License
659 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
660 #
661+import os
662+
663
664 # See also 3.3.4.1 of the "System Management BIOS Reference Specification,
665 # Version 2.6.1" (Preliminary Standard) document, available from
666 # http://www.dmtf.org/standards/smbios.
667 class Dmi:
668 chassis = (
669- ("Undefined", "unknown"), # 0x00
670+ ("Undefined", "unknown"), # 0x00
671 ("Other", "unknown"),
672 ("Unknown", "unknown"),
673 ("Desktop", "desktop"),
674@@ -53,22 +55,171 @@
675 ("Blade", "server"),
676 ("Blade Enclosure", "unknown"))
677
678- chassis_names = [c[0] for c in chassis]
679- chassis_types = [c[1] for c in chassis]
680+ chassis_names = tuple(c[0] for c in chassis)
681+ chassis_types = tuple(c[1] for c in chassis)
682 chassis_name_to_type = dict(chassis)
683
684-
685-class DmiNotAvailable(object):
686- def __init__(self, function):
687- self._function = function
688-
689- def __get__(self, instance, cls=None):
690- self._instance = instance
691- return self
692-
693- def __call__(self, *args, **kwargs):
694- name = self._function(self._instance, *args, **kwargs)
695- if name == "Not Available":
696- name = None
697-
698- return name
699+ type_names = (
700+ "BIOS", # 0x00
701+ "System",
702+ "Base Board",
703+ "Chassis",
704+ "Processor",
705+ "Memory Controller",
706+ "Memory Module",
707+ "Cache",
708+ "Port Connector",
709+ "System Slots",
710+ "On Board Devices",
711+ "OEM Strings",
712+ "System Configuration Options",
713+ "BIOS Language",
714+ "Group Associations",
715+ "System Event Log",
716+ "Physical Memory Array",
717+ "Memory Device",
718+ "32-bit Memory Error",
719+ "Memory Array Mapped Address",
720+ "Memory Device Mapped Address",
721+ "Built-in Pointing Device",
722+ "Portable Battery",
723+ "System Reset",
724+ "Hardware Security",
725+ "System Power Controls",
726+ "Voltage Probe",
727+ "Cooling Device",
728+ "Temperature Probe",
729+ "Electrical Current Probe",
730+ "Out-of-band Remote Access",
731+ "Boot Integrity Services",
732+ "System Boot",
733+ "64-bit Memory Error",
734+ "Management Device",
735+ "Management Device Component",
736+ "Management Device Threshold Data",
737+ "Memory Channel",
738+ "IPMI Device",
739+ "Power Supply",
740+ )
741+
742+
743+class DmiDevice:
744+
745+ bus = "dmi"
746+ driver = None
747+ product_id = None
748+ vendor_id = None
749+
750+ _product_blacklist = (
751+ "<BAD INDEX>",
752+ "N/A",
753+ "Not Available",
754+ "INVALID",
755+ "OEM",
756+ "Product Name",
757+ "System Product Name",
758+ "To be filled by O.E.M.",
759+ "To Be Filled By O.E.M.",
760+ "To Be Filled By O.E.M. by More String",
761+ "Unknown",
762+ "Uknown",
763+ "Unknow",
764+ "xxxxxxxxxxxxxx",
765+ )
766+ _vendor_blacklist = (
767+ "<BAD INDEX>",
768+ "Not Available",
769+ "OEM",
770+ "OEM Manufacturer",
771+ "System manufacturer",
772+ "System Manufacturer",
773+ "System Name",
774+ "To be filled by O.E.M.",
775+ "To Be Filled By O.E.M.",
776+ "To Be Filled By O.E.M. by More String",
777+ "Unknow", # XXX This is correct mispelling
778+ "Unknown",
779+ )
780+ _serial_blacklist = (
781+ "0",
782+ "00000000",
783+ "00 00 00 00 00 00 00 00",
784+ "0123456789",
785+ "Base Board Serial Number",
786+ "Chassis Serial Number",
787+ "N/A",
788+ "None",
789+ "Not Applicable",
790+ "Not Available",
791+ "Not Specified",
792+ "OEM",
793+ "System Serial Number",
794+ )
795+ _version_blacklist = (
796+ "-1",
797+ "<BAD INDEX>",
798+ "N/A",
799+ "None",
800+ "Not Applicable",
801+ "Not Available",
802+ "Not Specified",
803+ "OEM",
804+ "System Version",
805+ "Unknown",
806+ "x.x",
807+ )
808+
809+ def __init__(self, attributes, category):
810+ self._attributes = attributes
811+ self.category = category
812+
813+ @property
814+ def path(self):
815+ path = "/devices/virtual/dmi/id"
816+ return os.path.join(path, self.category.lower())
817+
818+ @property
819+ def product(self):
820+ if self.category == "CHASSIS":
821+ type_string = self._attributes.get("chassis_type", "0")
822+ try:
823+ type_index = int(type_string)
824+ return Dmi.chassis_names[type_index]
825+ except ValueError:
826+ return type_string
827+
828+ for name in "name", "version":
829+ attribute = "%s_%s" % (self.category.lower(), name)
830+ product = self._attributes.get(attribute)
831+ if product and product not in self._product_blacklist:
832+ return product
833+
834+ return None
835+
836+ @property
837+ def vendor(self):
838+ for name in "manufacturer", "vendor":
839+ attribute = "%s_%s" % (self.category.lower(), name)
840+ vendor = self._attributes.get(attribute)
841+ if vendor and vendor not in self._vendor_blacklist:
842+ return vendor
843+
844+ return None
845+
846+ @property
847+ def serial(self):
848+ attribute = "%s_serial" % self.category.lower()
849+ serial = self._attributes.get(attribute)
850+ if serial and serial not in self._serial_blacklist:
851+ return serial
852+
853+ return None
854+
855+ @property
856+ def version(self):
857+ attribute = "%s_version" % self.category.lower()
858+ version = self._attributes.get(attribute)
859+ if version and version not in self._version_blacklist:
860+ return version
861+
862+ return None
863
864=== added file 'checkbox/lib/enum.py'
865--- checkbox/lib/enum.py 1970-01-01 00:00:00 +0000
866+++ checkbox/lib/enum.py 2012-02-21 14:59:19 +0000
867@@ -0,0 +1,72 @@
868+#
869+# This file is part of Checkbox.
870+#
871+# Copyright 2011 Canonical Ltd.
872+#
873+# Checkbox is free software: you can redistribute it and/or modify
874+# it under the terms of the GNU General Public License as published by
875+# the Free Software Foundation, either version 3 of the License, or
876+# (at your option) any later version.
877+#
878+# Checkbox is distributed in the hope that it will be useful,
879+# but WITHOUT ANY WARRANTY; without even the implied warranty of
880+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
881+# GNU General Public License for more details.
882+#
883+# You should have received a copy of the GNU General Public License
884+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
885+#
886+__all__ = [
887+ "EnumException", "Enum", "enum_name_to_value", "enum_value_to_name"]
888+
889+
890+class EnumException(Exception):
891+
892+ pass
893+
894+
895+class Enum(object):
896+
897+ def __init__(self, *names):
898+ value = 0
899+ unique_names = set()
900+ for name in names:
901+ if isinstance(name, (list, tuple)):
902+ name, value = name
903+
904+ if not isinstance(name, str):
905+ raise EnumException(
906+ "enum name is not a string: %s" % name)
907+
908+ if isinstance(value, str):
909+ if value not in unique_names:
910+ raise EnumException(
911+ "enum value does not define: %s" % value)
912+ value = getattr(self, value)
913+
914+ if not isinstance(value, (int, long)):
915+ raise EnumException(
916+ "enum value is not an integer: %s" % value)
917+
918+ if name in unique_names:
919+ raise EnumException(
920+ "enum name is not unique: %s" % name)
921+
922+ unique_names.add(name)
923+ setattr(self, name, value)
924+ value += 1
925+
926+
927+def enum_name_to_value(enum, name):
928+ if not hasattr(enum, name):
929+ raise EnumException("enum name unknown: %s" % name)
930+
931+ return getattr(enum, name)
932+
933+
934+def enum_value_to_name(enum, value):
935+ for k, v in enum.__dict__.items():
936+ if v == value:
937+ return k
938+
939+ raise EnumException("enum value unknown: %s" % value)
940
941=== modified file 'checkbox/lib/fifo.py'
942--- checkbox/lib/fifo.py 2010-03-28 03:00:14 +0000
943+++ checkbox/lib/fifo.py 2012-02-21 14:59:19 +0000
944@@ -20,15 +20,16 @@
945 import struct
946
947 from checkbox.contrib.bpickle import dumps, loads
948-
949+from checkbox.lib.selector import Selector, SelectorIO
950
951 class FifoBase(object):
952
953 mode = None
954
955- def __init__(self, path):
956+ def __init__(self, path, timeout=None):
957 self.path = path
958 self.file = open(path, self.mode)
959+ self._timeout = timeout
960
961 def __del__(self):
962 self.close()
963@@ -36,12 +37,29 @@
964 def close(self):
965 self.file.close()
966
967+ def wait_for(self, operation):
968+ if self._timeout is not None:
969+ selector = Selector()
970+ selector.set_timeout(self._timeout)
971+ selector.add_fd(self.file.fileno(), operation)
972+
973+ selector.execute()
974+
975+ if not selector.has_ready():
976+ return False
977+ return True
978
979 class FifoReader(FifoBase):
980
981- mode = "r"
982+ #on Linux, opening a FIFO in read-write mode is non-blocking and
983+ #succeeds even if other end is not open as per FIFO(7)
984+ mode = "w+"
985
986 def read_string(self):
987+ # Check if a connection arrived within the timeout
988+ if not self.wait_for(SelectorIO.READ):
989+ return None
990+
991 size = struct.calcsize("i")
992 length_string = self.file.read(size)
993 if not length_string:
994@@ -60,9 +78,16 @@
995
996 class FifoWriter(FifoBase):
997
998- mode = "w"
999+ #on Linux, opening a FIFO in read-write mode is non-blocking and
1000+ #succeeds even if other end is not open as per FIFO(7)
1001+ mode = "w+"
1002
1003 def write_string(self, string):
1004+
1005+ # Wait until I can write
1006+ if not self.wait_for(SelectorIO.WRITE):
1007+ return None
1008+
1009 length = len(string)
1010 length_string = struct.pack(">i", length)
1011 self.file.write(length_string)
1012
1013=== modified file 'checkbox/lib/log.py'
1014--- checkbox/lib/log.py 2009-12-06 18:07:40 +0000
1015+++ checkbox/lib/log.py 2012-02-21 14:59:19 +0000
1016@@ -25,21 +25,30 @@
1017 def format_class(cls):
1018 return "%s.%s" % (cls.__module__, cls.__name__)
1019
1020-def format_object(object):
1021+def format_object(object, *args, **kwargs):
1022 """
1023 Returns a fully-qualified name for the specified object, such as
1024 'checkbox.log.format_object()'.
1025 """
1026+ args_string = ""
1027+ if args:
1028+ args_string += ", ".join(str(a) for a in args)
1029+ if kwargs:
1030+ args_string += ", "
1031+ if kwargs:
1032+ args_string += ", ".join(["%s=%s" % (k, v) for k, v in kwargs.iteritems()])
1033+
1034+ module = object.__module__ if object.__module__ else inspect.getfile(object)
1035 if inspect.ismethod(object):
1036 # FIXME If the method is implemented on a base class of
1037 # object's class, the module name and function name will be
1038 # from the base class and the method's class name will be from
1039 # object's class.
1040 name = repr(object).split(" ")[2]
1041- return "%s.%s()" % (object.__module__, name)
1042+ return "%s %s(%s)" % (module, name, args_string)
1043 elif inspect.isfunction(object):
1044 name = repr(object).split(" ")[1]
1045- return "%s.%s()" % (object.__module__, name)
1046+ return "%s %s(%s)" % (module, name, args_string)
1047 return format_class(object.__class__)
1048
1049 def format_delta(seconds):
1050
1051=== modified file 'checkbox/lib/process.py'
1052--- checkbox/lib/process.py 2009-12-07 16:55:14 +0000
1053+++ checkbox/lib/process.py 2012-02-21 14:59:19 +0000
1054@@ -16,16 +16,21 @@
1055 # You should have received a copy of the GNU General Public License
1056 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1057 #
1058+from __future__ import absolute_import
1059+
1060 import os
1061 import time
1062 import fcntl
1063 import select
1064+import signal
1065
1066
1067 STDIN_FILENO = 0
1068 STDOUT_FILENO = 1
1069 STDERR_FILENO = 2
1070
1071+DEFAULT_OPEN_MAX = 1024
1072+
1073
1074 class Process:
1075 """Class representing a child process which is non blocking. This makes
1076@@ -56,14 +61,11 @@
1077 self._outeof = self._erreof = False
1078
1079 def _child(self, cmd, env):
1080- # Note sh below doesn't setup a seperate group (job control)
1081- # for non interactive shells (hmm maybe -m option does?)
1082- os.setpgrp() #seperate group so we can kill it
1083 # Force FD_CLOEXEC on all inherited descriptors
1084 try:
1085 open_max = os.sysconf('SC_OPEN_MAX')
1086 except (AttributeError, ValueError):
1087- open_max = 1024
1088+ open_max = DEFAULT_OPEN_MAX
1089 for fileno in range(STDERR_FILENO+1, open_max):
1090 try:
1091 flags = fcntl.fcntl(fileno, fcntl.F_GETFD)
1092@@ -120,7 +122,7 @@
1093 return has_finished
1094
1095 def kill(self):
1096- os.kill(-self.pid, 15) # SIGTERM whole group
1097+ os.kill(self.pid, signal.SIGTERM)
1098
1099 def cleanup(self):
1100 """Wait for and return the exit status of the child process."""
1101
1102=== modified file 'checkbox/lib/safe.py'
1103--- checkbox/lib/safe.py 2009-03-09 14:43:55 +0000
1104+++ checkbox/lib/safe.py 2012-02-21 14:59:19 +0000
1105@@ -94,3 +94,8 @@
1106 md5sum = digest.hexdigest()
1107
1108 return md5sum
1109+
1110+def safe_close(file):
1111+ file.flush()
1112+ os.fsync(file.fileno())
1113+ file.close()
1114
1115=== added file 'checkbox/lib/selector.py'
1116--- checkbox/lib/selector.py 1970-01-01 00:00:00 +0000
1117+++ checkbox/lib/selector.py 2012-02-21 14:59:19 +0000
1118@@ -0,0 +1,207 @@
1119+#
1120+# This file is part of Checkbox.
1121+#
1122+# Copyright 2011 Canonical Ltd.
1123+#
1124+# Checkbox is free software: you can redistribute it and/or modify
1125+# it under the terms of the GNU General Public License as published by
1126+# the Free Software Foundation, either version 3 of the License, or
1127+# (at your option) any later version.
1128+#
1129+# Checkbox is distributed in the hope that it will be useful,
1130+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1131+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1132+# GNU General Public License for more details.
1133+#
1134+# You should have received a copy of the GNU General Public License
1135+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1136+#
1137+import os
1138+import errno
1139+import select
1140+
1141+from checkbox.lib.enum import Enum
1142+
1143+
1144+__all__ = ["SelectorIO", "SelectorState", "Selector"]
1145+
1146+
1147+SelectorIO = Enum(
1148+ "READ",
1149+ "WRITE",
1150+ "EXCEPT")
1151+
1152+SelectorState = Enum(
1153+ "VIRGIN",
1154+ "READY",
1155+ "TIMED_OUT",
1156+ "SIGNALED",
1157+ "FAILED")
1158+
1159+
1160+class Selector(object):
1161+ __slots__ = ("_read_fds", "_write_fds", "_except_fds",
1162+ "_save_read_fds", "_save_write_fds", "_save_except_fds",
1163+ "_fd_set_size", "_timeout", "_state", "_errno")
1164+
1165+ _fd_select_size = -1
1166+ _cached_read_fds = None
1167+ _cached_save_read_fds = None
1168+ _cached_write_fds = None
1169+ _cached_save_write_fds = None
1170+ _cached_except_fds = None
1171+ _cached_save_except_fds = None
1172+
1173+ def __init__(self):
1174+ if Selector._cached_read_fds:
1175+ self._read_fds = Selector._cached_read_fds
1176+ self._write_fds = Selector._cached_write_fds
1177+ self._except_fds = Selector._cached_except_fds
1178+
1179+ self._save_read_fds = Selector._cached_save_read_fds
1180+ self._save_write_fds = Selector._cached_save_write_fds
1181+ self._save_except_fds = Selector._cached_save_except_fds
1182+
1183+ Selector._cached_read_fds = None
1184+ Selector._cached_write_fds = None
1185+ Selector._cached_except_fds = None
1186+
1187+ Selector._cached_save_read_fds = None
1188+ Selector._cached_save_write_fds = None
1189+ Selector._cached_save_except_fds = None
1190+
1191+ else:
1192+ self._read_fds = []
1193+ self._write_fds = []
1194+ self._except_fds = []
1195+
1196+ self._save_read_fds = []
1197+ self._save_write_fds = []
1198+ self._save_except_fds = []
1199+
1200+ self.reset()
1201+
1202+ def __del__(self):
1203+ if Selector._cached_read_fds is None:
1204+ Selector._cached_read_fds = self._read_fds
1205+ Selector._cached_write_fds = self._write_fds
1206+ Selector._cached_except_fds = self._except_fds
1207+
1208+ Selector._cached_save_read_fds = self._save_read_fds
1209+ Selector._cached_save_write_fds = self._save_write_fds
1210+ Selector._cached_save_except_fds = self._save_except_fds
1211+
1212+ def reset(self):
1213+ self._errno = 0
1214+ self._state = SelectorState.VIRGIN
1215+ self._timeout = None
1216+ self._save_read_fds = []
1217+ self._save_write_fds = []
1218+ self._save_except_fds = []
1219+
1220+ @staticmethod
1221+ def get_fd_select_size():
1222+ if Selector._fd_select_size == -1:
1223+ try:
1224+ Selector._fd_select_size = os.sysconf("SC_OPEN_MAX")
1225+ except (AttributeError, ValueError):
1226+ Selector._fd_select_size = 1024
1227+
1228+ return Selector._fd_select_size
1229+
1230+ def get_errno(self):
1231+ return self._errno
1232+
1233+ def get_errstr(self):
1234+ return os.strerror(self._errno)
1235+
1236+ def set_timeout(self, timeout):
1237+ self._timeout = timeout
1238+
1239+ def unset_timeout(self):
1240+ self._timeout = None
1241+
1242+ def has_timed_out(self):
1243+ return self._state == SelectorState.TIMED_OUT
1244+
1245+ def has_signaled(self):
1246+ return self._state == SelectorState.SIGNALED
1247+
1248+ def has_failed(self):
1249+ return self._state == SelectorState.FAILED
1250+
1251+ def has_ready(self):
1252+ return self._state == SelectorState.READY
1253+
1254+ def add_fd(self, fd, interest):
1255+ if fd < 0 or fd >= Selector.get_fd_select_size():
1256+ raise Exception("File descriptor %d outside of range 0-%d"
1257+ % (fd, Selector._fd_select_size))
1258+
1259+ if interest == SelectorIO.READ:
1260+ self._save_read_fds.append(fd)
1261+
1262+ elif interest == SelectorIO.WRITE:
1263+ self._save_write_fds.append(fd)
1264+
1265+ elif interest == SelectorIO.EXCEPT:
1266+ self._save_except_fds.append(fd)
1267+
1268+ def remove_fd(self, fd, interest):
1269+ if fd < 0 or fd >= Selector.get_fd_select_size():
1270+ raise Exception("File descriptor %d outside of range 0-%d"
1271+ % (fd, Selector._fd_select_size))
1272+
1273+ if interest == SelectorIO.READ:
1274+ self._save_read_fds.remove(fd)
1275+
1276+ elif interest == SelectorIO.WRITE:
1277+ self._save_write_fds.remove(fd)
1278+
1279+ elif interest == SelectorIO.EXCEPT:
1280+ self._save_except_fds.remove(fd)
1281+
1282+ def execute(self):
1283+ try:
1284+ self._read_fds, self._write_fds, self._except_fds = select.select(
1285+ self._save_read_fds, self._save_write_fds,
1286+ self._save_except_fds, self._timeout)
1287+ except select.error, e:
1288+ self._errno = e.errno
1289+ if e.errno == errno.EINTR:
1290+ self._state = SelectorState.SIGNALED
1291+
1292+ else:
1293+ self._state = SelectorState.FAILED
1294+
1295+ return
1296+
1297+ # Just in case
1298+ self._errno = 0
1299+ if not self._read_fds \
1300+ and not self._write_fds \
1301+ and not self._except_fds:
1302+ self._state = SelectorState.TIMED_OUT
1303+
1304+ else:
1305+ self._state = SelectorState.READY
1306+
1307+ def is_fd_ready(self, fd, interest):
1308+ if self._state != SelectorState.READY \
1309+ and self._state != SelectorState.TIMED_OUT:
1310+ raise Exception(
1311+ "Selector requested descriptor not in ready state")
1312+
1313+ if fd < 0 or fd >= Selector.get_fd_select_size():
1314+ return False
1315+
1316+ if interest == SelectorIO.READ:
1317+ return fd in self._read_fds
1318+
1319+ elif interest == SelectorIO.WRITE:
1320+ return fd in self._write_fds
1321+
1322+ elif interest == SelectorIO.EXCEPT:
1323+ return fd in self._except_fds
1324+
1325+ return False
1326
1327=== modified file 'checkbox/lib/template.py'
1328--- checkbox/lib/template.py 2009-09-11 16:41:43 +0000
1329+++ checkbox/lib/template.py 2012-02-21 14:59:19 +0000
1330@@ -92,7 +92,10 @@
1331 field = value = extended = ""
1332 for line in string.split("\n"):
1333 line.strip()
1334- match = re.search(r"^([-_.A-Za-z0-9]*):\s?(.*)", line)
1335+ if line.startswith("#"):
1336+ continue
1337+
1338+ match = re.search(r"^([-_.A-Za-z0-9@]*):\s?(.*)", line)
1339 if match:
1340 _save(field, value, extended)
1341 field = match.groups()[0].lower()
1342
1343=== modified file 'checkbox/lib/template_i18n.py'
1344--- checkbox/lib/template_i18n.py 2010-02-16 21:09:28 +0000
1345+++ checkbox/lib/template_i18n.py 2012-02-21 14:59:19 +0000
1346@@ -77,7 +77,8 @@
1347 languages = []
1348 if environ.has_key("LANGUAGE") and environ["LANGUAGE"]:
1349 for language in environ["LANGUAGE"].split(":"):
1350- languages.extend(self._get_language_list(language))
1351+ if language:
1352+ languages.extend(self._get_language_list(language))
1353
1354 language = locale.setlocale(locale.LC_MESSAGES)
1355 languages.extend(self._get_language_list(language))
1356
1357=== modified file 'checkbox/lib/transport.py'
1358--- checkbox/lib/transport.py 2009-03-09 14:30:47 +0000
1359+++ checkbox/lib/transport.py 2012-02-21 14:59:19 +0000
1360@@ -19,6 +19,7 @@
1361 import logging
1362
1363 import os
1364+import re
1365 import stat
1366 import sys
1367 import posixpath
1368@@ -42,6 +43,29 @@
1369 ssl_sock = socket.ssl(sock, key_file, cert_file)
1370 return httplib.FakeSocket(sock, ssl_sock)
1371
1372+try:
1373+ # Python 2.6 introduced create_connection convenience function
1374+ create_connection = socket.create_connection
1375+except AttributeError:
1376+ def create_connection(address, timeout=None):
1377+ msg = "getaddrinfo returns an empty list"
1378+ host, port = address
1379+ for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
1380+ af, socktype, proto, canonname, sa = res
1381+ sock = None
1382+ try:
1383+ sock = socket.socket(af, socktype, proto)
1384+ if timeout is not None:
1385+ sock.settimeout(timeout)
1386+ sock.connect(sa)
1387+ return sock
1388+
1389+ except socket.error, msg:
1390+ if sock is not None:
1391+ sock.close()
1392+
1393+ raise socket.error, msg
1394+
1395
1396 class ProxyHTTPConnection(httplib.HTTPConnection):
1397
1398@@ -63,6 +87,9 @@
1399 port = self._ports[scheme]
1400 except KeyError:
1401 raise ValueError, "unknown protocol for: %s" % url
1402+ else:
1403+ port = int(port)
1404+
1405 self._real_host = host
1406 self._real_port = port
1407 httplib.HTTPConnection.request(self, method, url, body, headers)
1408@@ -101,6 +128,59 @@
1409 self.sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
1410
1411
1412+class VerifiedHTTPSConnection(httplib.HTTPSConnection):
1413+
1414+ # Compatibility layer with Python 2.5
1415+ timeout = None
1416+ _tunnel_host = None
1417+
1418+ def match_name(self, name):
1419+ parts = []
1420+ for fragment in name.split(r"."):
1421+ if fragment == "*":
1422+ parts.append(".+")
1423+ else:
1424+ fragment = re.escape(fragment)
1425+ parts.append(fragment.replace(r"\*", ".*"))
1426+ return re.match(r"\A" + r"\.".join(parts) + r"\Z", self.host, re.IGNORECASE)
1427+
1428+ def verify_cert(self, cert):
1429+ # verify that the hostname matches that of the certificate
1430+ if cert:
1431+ san = cert.get("subjectAltName", ())
1432+ for key, value in san:
1433+ if key == "DNS" and self.match_name(value):
1434+ return True
1435+
1436+ if not san:
1437+ for subject in cert.get("subject", ()):
1438+ for key, value in subject:
1439+ if key == "commonName" and self.match_name(value):
1440+ return True
1441+
1442+ return False
1443+
1444+ def connect(self):
1445+ # overrides the version in httplib so that we do
1446+ # certificate verification
1447+ sock = create_connection((self.host, self.port), self.timeout)
1448+ if self._tunnel_host:
1449+ self.sock = sock
1450+ self._tunnel()
1451+
1452+ # wrap the socket using verification with the root
1453+ # certs in trusted_root_certs
1454+ self.sock = _ssl_wrap_socket(sock,
1455+ self.key_file,
1456+ self.cert_file,
1457+ cert_reqs=ssl.CERT_REQUIRED,
1458+ ca_certs="/etc/ssl/certs/ca-certificates.crt")
1459+
1460+ if not self.verify_cert(self.sock.getpeercert()):
1461+ raise ValueError(
1462+ "Failed to verify cert for hostname: %s" % self.host)
1463+
1464+
1465 class HTTPTransport(object):
1466 """Transport makes a request to exchange message data over HTTP."""
1467
1468@@ -115,6 +195,9 @@
1469 scheme, rest = urllib.splittype(string)
1470 host, rest = urllib.splithost(rest)
1471 host, port = urllib.splitport(host)
1472+ if port is not None:
1473+ port = int(port)
1474+
1475 return (host, port)
1476
1477 def _get_connection(self, timeout=0):
1478@@ -135,7 +218,7 @@
1479 connection = ProxyHTTPSConnection(host, port)
1480 else:
1481 host, port = self._unpack_host_and_port(self.url)
1482- connection = httplib.HTTPSConnection(host, port)
1483+ connection = VerifiedHTTPSConnection(host, port)
1484 else:
1485 raise Exception, "Unknown URL scheme: %s" % scheme
1486
1487@@ -247,7 +330,7 @@
1488 else:
1489 if response.status == httplib.FOUND:
1490 # TODO prevent infinite redirect loop
1491- self.url = self._get_location_header(response)
1492+ self.url = response.getheader('location')
1493 response = self.exchange(body, headers, timeout)
1494
1495 return response
1496
1497=== modified file 'checkbox/lib/usb.py'
1498--- checkbox/lib/usb.py 2009-09-14 18:41:46 +0000
1499+++ checkbox/lib/usb.py 2012-02-21 14:59:19 +0000
1500@@ -55,3 +55,5 @@
1501 BASE_CLASS_WIRELESS = 224
1502 CLASS_WIRELESS_RADIO_FREQUENCY = 1
1503 CLASS_WIRELESS_USB_ADAPTER = 2
1504+
1505+ PROTOCOL_BLUETOOTH = 1
1506
1507=== modified file 'checkbox/message.py'
1508--- checkbox/message.py 2010-02-16 15:18:39 +0000
1509+++ checkbox/message.py 2012-02-21 14:59:19 +0000
1510@@ -20,9 +20,10 @@
1511 import logging
1512 import itertools
1513 import posixpath
1514+import sys
1515
1516 from checkbox.contrib import bpickle
1517-
1518+from checkbox.lib.safe import safe_close
1519
1520 HELD = "h"
1521 BROKEN = "b"
1522@@ -40,6 +41,9 @@
1523 class MessageStore(object):
1524 """A message store which stores its messages in a file system hierarchy."""
1525
1526+ #This caches everything but a message's data, making it manageable to keep in memory.
1527+ _message_cache = {}
1528+
1529 def __init__(self, persist, directory, directory_size=1000):
1530 self._directory = directory
1531 self._directory_size = directory_size
1532@@ -215,7 +219,7 @@
1533 try:
1534 return file.read()
1535 finally:
1536- file.close()
1537+ safe_close(file)
1538
1539 def _get_flags(self, path):
1540 basename = posixpath.basename(path)
1541@@ -240,7 +244,11 @@
1542 def _dump_message(self, message):
1543 return bpickle.dumps(message)
1544
1545- def _read_message(self, filename):
1546+ def _read_message(self, filename, cache=False):
1547+ #cache basically indicates whether the caller cares about having "data"
1548+ if cache and filename in self._message_cache:
1549+ return Message(self._message_cache[filename],filename)
1550+
1551 data = self._get_content(filename)
1552 message = self._load_message(data)
1553 return Message(message, filename)
1554@@ -253,9 +261,18 @@
1555
1556 file = open(filename + ".tmp", "w")
1557 file.write(message_data)
1558- file.close()
1559+ safe_close(file)
1560+
1561 os.rename(filename + ".tmp", filename)
1562
1563+ #Strip the big data element and shove it in the cache
1564+
1565+ temp_message=dict(message)
1566+ if "data" in temp_message:
1567+ temp_message["data"] = None
1568+
1569+ self._message_cache[filename] = temp_message
1570+
1571 # For now we use the inode as the message id, as it will work
1572 # correctly even faced with holding/unholding. It will break
1573 # if the store is copied over for some reason, but this shouldn't
1574
1575=== added directory 'checkbox/parsers'
1576=== added file 'checkbox/parsers/__init__.py'
1577=== added file 'checkbox/parsers/cpuinfo.py'
1578--- checkbox/parsers/cpuinfo.py 1970-01-01 00:00:00 +0000
1579+++ checkbox/parsers/cpuinfo.py 2012-02-21 14:59:19 +0000
1580@@ -0,0 +1,180 @@
1581+#
1582+# This file is part of Checkbox.
1583+#
1584+# Copyright 2011 Canonical Ltd.
1585+#
1586+# Checkbox is free software: you can redistribute it and/or modify
1587+# it under the terms of the GNU General Public License as published by
1588+# the Free Software Foundation, either version 3 of the License, or
1589+# (at your option) any later version.
1590+#
1591+# Checkbox is distributed in the hope that it will be useful,
1592+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1593+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1594+# GNU General Public License for more details.
1595+#
1596+# You should have received a copy of the GNU General Public License
1597+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1598+#
1599+import re
1600+
1601+from os import uname
1602+
1603+from checkbox.lib.conversion import string_to_type
1604+
1605+
1606+class CpuinfoParser:
1607+ """Parser for the /proc/cpuinfo file."""
1608+
1609+ def __init__(self, stream, machine=None):
1610+ self.stream = stream
1611+ self.machine = machine or uname()[4].lower()
1612+
1613+ def getAttributes(self):
1614+ count = 0
1615+ attributes = {}
1616+ cpuinfo = self.stream.read()
1617+ for block in re.split(r"\n{2,}", cpuinfo):
1618+ block = block.strip()
1619+ if not block:
1620+ continue
1621+
1622+ count += 1
1623+ if count > 1:
1624+ continue
1625+
1626+ for line in block.split("\n"):
1627+ if not line:
1628+ continue
1629+
1630+ key, value = line.split(":")
1631+ key, value = key.strip(), value.strip()
1632+
1633+ # Handle bogomips on sparc
1634+ if key.endswith("Bogo"):
1635+ key = "bogomips"
1636+
1637+ attributes[key.lower()] = value
1638+
1639+ if attributes:
1640+ attributes["count"] = count
1641+
1642+ return attributes
1643+
1644+ def run(self, result):
1645+ attributes = self.getAttributes()
1646+ if not attributes:
1647+ return
1648+
1649+ # Default values
1650+ machine = self.machine
1651+ processor = {
1652+ "platform": machine,
1653+ "count": 1,
1654+ "type": machine,
1655+ "model": machine,
1656+ "model_number": "",
1657+ "model_version": "",
1658+ "model_revision": "",
1659+ "cache": 0,
1660+ "bogomips": 0,
1661+ "speed": -1,
1662+ "other": ""}
1663+
1664+ # Conversion table
1665+ platform_to_conversion = {
1666+ ("i386", "i486", "i586", "i686", "x86_64",): {
1667+ "type": "vendor_id",
1668+ "model": "model name",
1669+ "model_number": "cpu family",
1670+ "model_version": "model",
1671+ "model_revision": "stepping",
1672+ "cache": "cache size",
1673+ "other": "flags",
1674+ "speed": "cpu mhz"},
1675+ ("alpha", "alphaev6",): {
1676+ "count": "cpus detected",
1677+ "type": "cpu",
1678+ "model": "cpu model",
1679+ "model_number": "cpu variation",
1680+ "model_version": ("system type", "system variation",),
1681+ "model_revision": "cpu revision",
1682+ "other": "platform string",
1683+ "speed": "cycle frequency [Hz]"},
1684+ ("armv7l",): {
1685+ "type": "hardware",
1686+ "model": "processor",
1687+ "model_number": "cpu variant",
1688+ "model_version": "cpu architecture",
1689+ "model_revision": "cpu revision",
1690+ "other": "features"},
1691+ ("ia64",): {
1692+ "type": "vendor",
1693+ "model": "family",
1694+ "model_version": "archrev",
1695+ "model_revision": "revision",
1696+ "other": "features",
1697+ "speed": "cpu mhz"},
1698+ ("ppc64", "ppc",): {
1699+ "type": "platform",
1700+ "model": "cpu",
1701+ "model_version": "revision",
1702+ "speed": "clock"},
1703+ ("sparc64", "sparc",): {
1704+ "count": "ncpus probed",
1705+ "type": "type",
1706+ "model": "cpu",
1707+ "model_version": "type",
1708+ "speed": "bogomips"}}
1709+
1710+ processor["count"] = attributes.get("count", 1)
1711+ bogompips_string = attributes.get("bogomips", "0.0")
1712+ processor["bogomips"] = int(round(float(bogompips_string)))
1713+ for platform, conversion in platform_to_conversion.iteritems():
1714+ if machine in platform:
1715+ for pkey, ckey in conversion.iteritems():
1716+ if isinstance(ckey, (list, tuple)):
1717+ processor[pkey] = "/".join([attributes[k]
1718+ for k in ckey])
1719+ elif ckey in attributes:
1720+ processor[pkey] = attributes[ckey]
1721+
1722+ # Adjust platform
1723+ if machine[0] == "i" and machine[-2:] == "86":
1724+ processor["platform"] = u"i386"
1725+ elif machine[:5] == "alpha":
1726+ processor["platform"] = u"alpha"
1727+
1728+ # Adjust cache
1729+ if processor["cache"]:
1730+ processor["cache"] = string_to_type(processor["cache"])
1731+
1732+ # Adjust speed
1733+ try:
1734+ if machine[:5] == "alpha":
1735+ speed = processor["speed"].split()[0]
1736+ processor["speed"] = int(round(float(speed))) / 1000000
1737+ elif machine[:5] == "sparc":
1738+ speed = processor["speed"]
1739+ processor["speed"] = int(round(float(speed))) / 2
1740+ else:
1741+ if machine[:3] == "ppc":
1742+ # String is appended with "mhz"
1743+ speed = processor["speed"][:-3]
1744+ else:
1745+ speed = processor["speed"]
1746+ processor["speed"] = int(round(float(speed)) - 1)
1747+ except ValueError:
1748+ processor["speed"] = -1
1749+
1750+ # Adjust count
1751+ try:
1752+ processor["count"] = int(processor["count"])
1753+ except ValueError:
1754+ processor["count"] = 1
1755+ else:
1756+ # There is at least one processor
1757+ if processor["count"] == 0:
1758+ processor["count"] = 1
1759+
1760+ result.setProcessor(processor)
1761
1762=== added file 'checkbox/parsers/cputable'
1763--- checkbox/parsers/cputable 1970-01-01 00:00:00 +0000
1764+++ checkbox/parsers/cputable 2012-02-21 14:59:19 +0000
1765@@ -0,0 +1,40 @@
1766+# This file contains the table of known CPU names.
1767+#
1768+# Architecture names are formed as a combination of the system name
1769+# (from ostable) and CPU name (from this table) after mapping from
1770+# the Debian triplet (from triplettable). A list of architecture
1771+# names in the Debian ‘sid’ distribution can be found in the archtable
1772+# file.
1773+#
1774+# Column 1 is the Debian name for the CPU, used to form the cpu part in
1775+# the Debian triplet.
1776+# Column 2 is the GNU name for the CPU, used to output build and host
1777+# targets in ‘dpkg-architecture’.
1778+# Column 3 is an extended regular expression used to match against the
1779+# CPU part of the output of the GNU config.guess script.
1780+# Column 4 is the size (in bits) of the integers/pointers
1781+# Column 5 is the endianness (byte ordering in numbers)
1782+#
1783+# <Debian name> <GNU name> <config.guess regex> <Bits> <Endianness>
1784+i386 i686 (i[3456]86|pentium) 32 little
1785+ia64 ia64 ia64 64 little
1786+alpha alpha alpha.* 64 little
1787+amd64 x86_64 x86_64 64 little
1788+armeb armeb arm.*b 32 big
1789+arm arm arm.* 32 little
1790+avr32 avr32 avr32 32 big
1791+hppa hppa hppa.* 32 big
1792+m32r m32r m32r 32 big
1793+m68k m68k m68k 32 big
1794+mips mips mips(eb)? 32 big
1795+mipsel mipsel mipsel 32 little
1796+powerpc powerpc (powerpc|ppc) 32 big
1797+ppc64 powerpc64 (powerpc|ppc)64 64 big
1798+s390 s390 s390 32 big
1799+s390x s390x s390x 64 big
1800+sh3 sh3 sh3 32 little
1801+sh3eb sh3eb sh3eb 32 big
1802+sh4 sh4 sh4 32 little
1803+sh4eb sh4eb sh4eb 32 big
1804+sparc sparc sparc 32 big
1805+sparc64 sparc64 sparc64 64 big
1806
1807=== added file 'checkbox/parsers/cputable.py'
1808--- checkbox/parsers/cputable.py 1970-01-01 00:00:00 +0000
1809+++ checkbox/parsers/cputable.py 2012-02-21 14:59:19 +0000
1810@@ -0,0 +1,42 @@
1811+#
1812+# This file is part of Checkbox.
1813+#
1814+# Copyright 2011 Canonical Ltd.
1815+#
1816+# Checkbox is free software: you can redistribute it and/or modify
1817+# it under the terms of the GNU General Public License as published by
1818+# the Free Software Foundation, either version 3 of the License, or
1819+# (at your option) any later version.
1820+#
1821+# Checkbox is distributed in the hope that it will be useful,
1822+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1823+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1824+# GNU General Public License for more details.
1825+#
1826+# You should have received a copy of the GNU General Public License
1827+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1828+#
1829+import re
1830+
1831+
1832+CPUTABLE_RE = re.compile(
1833+ r"^(?!\#)(?P<debian_name>\S+)"
1834+ r"\s+(?P<gnu_name>\S+)"
1835+ r"\s+(?P<regex>\S+)"
1836+ r"\s+(?P<bits>\d+)"
1837+ r"\s+(?P<endianness>big|little)")
1838+
1839+
1840+class CputableParser:
1841+ """Parser for the /usr/share/dpkg/cputable file."""
1842+
1843+ def __init__(self, stream):
1844+ self.stream = stream
1845+
1846+ def run(self, result):
1847+ for line in self.stream.readlines():
1848+ match = CPUTABLE_RE.match(line)
1849+ if match:
1850+ cpu = match.groupdict()
1851+ cpu["bits"] = int(cpu["bits"])
1852+ result.addCpu(cpu)
1853
1854=== added file 'checkbox/parsers/deferred.py'
1855--- checkbox/parsers/deferred.py 1970-01-01 00:00:00 +0000
1856+++ checkbox/parsers/deferred.py 2012-02-21 14:59:19 +0000
1857@@ -0,0 +1,27 @@
1858+#
1859+# This file is part of Checkbox.
1860+#
1861+# Copyright 2011 Canonical Ltd.
1862+#
1863+# Checkbox is free software: you can redistribute it and/or modify
1864+# it under the terms of the GNU General Public License as published by
1865+# the Free Software Foundation, either version 3 of the License, or
1866+# (at your option) any later version.
1867+#
1868+# Checkbox is distributed in the hope that it will be useful,
1869+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1870+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1871+# GNU General Public License for more details.
1872+#
1873+# You should have received a copy of the GNU General Public License
1874+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1875+#
1876+class DeferredParser:
1877+ """Parser for deferred dispatching of events."""
1878+
1879+ def __init__(self, dispatcher, event_type="result"):
1880+ self.dispatcher = dispatcher
1881+ self.event_type = event_type
1882+
1883+ def run(self, result):
1884+ self.dispatcher.publishEvent(self.event_type, result)
1885
1886=== added file 'checkbox/parsers/description.py'
1887--- checkbox/parsers/description.py 1970-01-01 00:00:00 +0000
1888+++ checkbox/parsers/description.py 2012-02-21 14:59:19 +0000
1889@@ -0,0 +1,74 @@
1890+#
1891+# This file is part of Checkbox.
1892+#
1893+# Copyright 2011 Canonical Ltd.
1894+#
1895+# Checkbox is free software: you can redistribute it and/or modify
1896+# it under the terms of the GNU General Public License as published by
1897+# the Free Software Foundation, either version 3 of the License, or
1898+# (at your option) any later version.
1899+#
1900+# Checkbox is distributed in the hope that it will be useful,
1901+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1902+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1903+# GNU General Public License for more details.
1904+#
1905+# You should have received a copy of the GNU General Public License
1906+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1907+#
1908+from checkbox.lib.enum import Enum
1909+
1910+
1911+DescriptionState = Enum(
1912+ "INIT",
1913+ "PURPOSE",
1914+ "STEPS",
1915+ "INFO",
1916+ "VERIFICATION",
1917+ "OTHER")
1918+
1919+TransitionStates = {
1920+ DescriptionState.INIT: DescriptionState.PURPOSE,
1921+ DescriptionState.PURPOSE: DescriptionState.STEPS,
1922+ DescriptionState.STEPS: DescriptionState.VERIFICATION,
1923+ DescriptionState.INFO: DescriptionState.VERIFICATION,
1924+ DescriptionState.VERIFICATION: DescriptionState.OTHER,
1925+ DescriptionState.OTHER: DescriptionState.OTHER,
1926+ }
1927+
1928+
1929+class DescriptionParser:
1930+ """Parser for the description field in jobs."""
1931+
1932+ def __init__(self, stream):
1933+ self.stream = stream
1934+
1935+ def run(self, result):
1936+ parts = {}
1937+ state = DescriptionState.INIT
1938+
1939+ for line in self.stream.readlines():
1940+ # Check for upper case characters without leading spaces.
1941+ if not line[0].isspace() \
1942+ and line.isupper():
1943+ state = TransitionStates[state]
1944+
1945+ # Append to description parts between INIT and OTHER states.
1946+ elif state > DescriptionState.INIT \
1947+ and state < DescriptionState.OTHER:
1948+ # Handle optional INFO state and translations of $output.
1949+ if state == DescriptionState.VERIFICATION \
1950+ and "$" in line:
1951+ state = DescriptionState.INFO
1952+ line = "$output\n"
1953+
1954+ parts.setdefault(state, "")
1955+ parts[state] += line.lstrip()
1956+
1957+ # Only set the description if the last state is still VERIFICATION.
1958+ if state == DescriptionState.VERIFICATION:
1959+ result.setDescription(
1960+ parts[DescriptionState.PURPOSE],
1961+ parts[DescriptionState.STEPS],
1962+ parts[DescriptionState.VERIFICATION],
1963+ parts.get(DescriptionState.INFO))
1964
1965=== added file 'checkbox/parsers/device.py'
1966--- checkbox/parsers/device.py 1970-01-01 00:00:00 +0000
1967+++ checkbox/parsers/device.py 2012-02-21 14:59:19 +0000
1968@@ -0,0 +1,24 @@
1969+#
1970+# This file is part of Checkbox.
1971+#
1972+# Copyright 2011 Canonical Ltd.
1973+#
1974+# Checkbox is free software: you can redistribute it and/or modify
1975+# it under the terms of the GNU General Public License as published by
1976+# the Free Software Foundation, either version 3 of the License, or
1977+# (at your option) any later version.
1978+#
1979+# Checkbox is distributed in the hope that it will be useful,
1980+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1981+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1982+# GNU General Public License for more details.
1983+#
1984+# You should have received a copy of the GNU General Public License
1985+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
1986+#
1987+
1988+
1989+class DeviceResult(object):
1990+
1991+ def addDevice(self, device):
1992+ pass
1993
1994=== added file 'checkbox/parsers/dmidecode.py'
1995--- checkbox/parsers/dmidecode.py 1970-01-01 00:00:00 +0000
1996+++ checkbox/parsers/dmidecode.py 2012-02-21 14:59:19 +0000
1997@@ -0,0 +1,123 @@
1998+#
1999+# This file is part of Checkbox.
2000+#
2001+# Copyright 2011 Canonical Ltd.
2002+#
2003+# Checkbox is free software: you can redistribute it and/or modify
2004+# it under the terms of the GNU General Public License as published by
2005+# the Free Software Foundation, either version 3 of the License, or
2006+# (at your option) any later version.
2007+#
2008+# Checkbox is distributed in the hope that it will be useful,
2009+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2010+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2011+# GNU General Public License for more details.
2012+#
2013+# You should have received a copy of the GNU General Public License
2014+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2015+#
2016+import re
2017+
2018+from string import (
2019+ hexdigits,
2020+ uppercase,
2021+ )
2022+
2023+from checkbox.lib.dmi import (
2024+ Dmi,
2025+ DmiDevice,
2026+ )
2027+
2028+
2029+HANDLE_RE = re.compile(
2030+ r"^Handle (?P<handle>0x[%s]{4}), "
2031+ r"DMI type (?P<type>\d+), "
2032+ r"(?P<size>\d+) bytes$"
2033+ % hexdigits)
2034+KEY_VALUE_RE = re.compile(
2035+ r"^\t(?P<key>[%s].+):( (?P<value>.+))?$"
2036+ % uppercase)
2037+
2038+
2039+class DmidecodeParser:
2040+ """Parser for the dmidecode command."""
2041+
2042+ _key_map = {
2043+ "ID": "serial",
2044+ "Manufacturer": "vendor",
2045+ "Product Name": "name",
2046+ "Serial Number": "serial",
2047+ "Type": "type",
2048+ "Vendor": "vendor",
2049+ "Version": "version",
2050+ }
2051+
2052+ def __init__(self, stream):
2053+ self.stream = stream
2054+
2055+ def _parseKey(self, key):
2056+ return self._key_map.get(key)
2057+
2058+ def _parseValue(self, value):
2059+ if value is not None:
2060+ value = value.strip()
2061+ if not value:
2062+ value = None
2063+
2064+ return value
2065+
2066+ def run(self, result):
2067+ output = self.stream.read()
2068+ for record in re.split(r"\n{2,}", output):
2069+ record = record.strip()
2070+ # Skip empty records
2071+ if not record:
2072+ continue
2073+
2074+ # Skip header record
2075+ lines = record.split("\n")
2076+ line = lines.pop(0)
2077+ if line.startswith("#"):
2078+ continue
2079+
2080+ # Skip records with an unsupported handle
2081+ match = HANDLE_RE.match(line)
2082+ if not match:
2083+ continue
2084+
2085+ # Skip records that are empty or inactive
2086+ if not lines or lines.pop(0) == "Inactive":
2087+ continue
2088+
2089+ # Skip disabled entries and end-of-table marker
2090+ type_index = int(match.group("type"))
2091+ if type_index >= len(Dmi.type_names):
2092+ continue
2093+
2094+ category = Dmi.type_names[type_index]
2095+ category = category.upper().split(" ")[-1]
2096+ if category not in (
2097+ "BOARD", "BIOS", "CHASSIS", "PROCESSOR", "SYSTEM"):
2098+ continue
2099+
2100+ # Parse attributes
2101+ attributes = {}
2102+ for line in lines:
2103+ # Skip lines with an unsupported key/value pair
2104+ match = KEY_VALUE_RE.match(line)
2105+ if not match:
2106+ continue
2107+
2108+ # Skip lines with an unsupported key
2109+ key = self._parseKey(match.group("key"))
2110+ if not key:
2111+ continue
2112+
2113+ key = "%s_%s" % (category.lower(), key)
2114+ value = self._parseValue(match.group("value"))
2115+ attributes[key] = value
2116+
2117+ device = DmiDevice(attributes, category)
2118+ result.addDmiDevice(device)
2119+
2120+ return result
2121
2122=== added file 'checkbox/parsers/meminfo.py'
2123--- checkbox/parsers/meminfo.py 1970-01-01 00:00:00 +0000
2124+++ checkbox/parsers/meminfo.py 2012-02-21 14:59:19 +0000
2125@@ -0,0 +1,46 @@
2126+#
2127+# This file is part of Checkbox.
2128+#
2129+# Copyright 2011 Canonical Ltd.
2130+#
2131+# Checkbox is free software: you can redistribute it and/or modify
2132+# it under the terms of the GNU General Public License as published by
2133+# the Free Software Foundation, either version 3 of the License, or
2134+# (at your option) any later version.
2135+#
2136+# Checkbox is distributed in the hope that it will be useful,
2137+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2138+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2139+# GNU General Public License for more details.
2140+#
2141+# You should have received a copy of the GNU General Public License
2142+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2143+#
2144+import re
2145+
2146+
2147+class MeminfoParser:
2148+ """Parser for the /proc/meminfo file."""
2149+
2150+ def __init__(self, stream):
2151+ self.stream = stream
2152+
2153+ def run(self, result):
2154+ key_value_pattern = re.compile(r"(?P<key>.*):\s+(?P<value>.*)")
2155+ meminfo_map = {
2156+ "MemTotal": "total",
2157+ "SwapTotal": "swap"}
2158+
2159+ meminfo = {}
2160+ for line in self.stream.readlines():
2161+ line = line.strip()
2162+ match = key_value_pattern.match(line)
2163+ if match:
2164+ key = match.group("key")
2165+ if key in meminfo_map:
2166+ key = meminfo_map[key]
2167+ value = match.group("value")
2168+ (integer, factor) = value.split()
2169+ meminfo[key] = int(integer) * 1024
2170+
2171+ result.setMemory(meminfo)
2172
2173=== added file 'checkbox/parsers/submission.py'
2174--- checkbox/parsers/submission.py 1970-01-01 00:00:00 +0000
2175+++ checkbox/parsers/submission.py 2012-02-21 14:59:19 +0000
2176@@ -0,0 +1,540 @@
2177+#
2178+# This file is part of Checkbox.
2179+#
2180+# Copyright 2011 Canonical Ltd.
2181+#
2182+# Checkbox is free software: you can redistribute it and/or modify
2183+# it under the terms of the GNU General Public License as published by
2184+# the Free Software Foundation, either version 3 of the License, or
2185+# (at your option) any later version.
2186+#
2187+# Checkbox is distributed in the hope that it will be useful,
2188+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2189+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2190+# GNU General Public License for more details.
2191+#
2192+# You should have received a copy of the GNU General Public License
2193+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2194+#
2195+try:
2196+ import xml.etree.cElementTree as etree
2197+except ImportError:
2198+ import cElementTree as etree
2199+
2200+from StringIO import StringIO
2201+from logging import getLogger
2202+from pkg_resources import resource_string
2203+
2204+from checkbox.lib.conversion import string_to_datetime
2205+
2206+from checkbox import parsers
2207+from checkbox.dispatcher import DispatcherQueue
2208+from checkbox.parsers.cpuinfo import CpuinfoParser
2209+from checkbox.parsers.cputable import CputableParser
2210+from checkbox.parsers.deferred import DeferredParser
2211+from checkbox.parsers.dmidecode import DmidecodeParser
2212+from checkbox.parsers.meminfo import MeminfoParser
2213+from checkbox.parsers.udevadm import UdevadmParser
2214+from checkbox.job import (FAIL, PASS, UNINITIATED, UNRESOLVED,
2215+ UNSUPPORTED, UNTESTED)
2216+
2217+
2218+class SubmissionResult:
2219+
2220+ def __init__(self, test_run_factory, **kwargs):
2221+ self.test_run_factory = test_run_factory
2222+ self.test_run_kwargs = kwargs
2223+ self.dispatcher = DispatcherQueue()
2224+
2225+ # Register handlers to incrementally add information
2226+ register = self.dispatcher.registerHandler
2227+ register(("cpu", "architecture",), self.addCpuArchitecture)
2228+ register(("identifier",), self.addIdentifier)
2229+ register(("test_run", "attachment",), self.addAttachment)
2230+ register(("test_run", "device",), self.addDeviceState)
2231+ register(("test_run", "distribution",), self.setDistribution)
2232+ register(("test_run", "package_version",), self.addPackageVersion)
2233+ register(("test_run", "test_result",), self.addTestResult)
2234+
2235+ # Register handlers to set information once
2236+ register(("architecture",), self.setArchitecture, count=1)
2237+ register(
2238+ ("cpuinfo", "machine", "cpuinfo_result",),
2239+ self.setCpuinfo, count=1)
2240+ register(
2241+ ("meminfo", "meminfo_result",),
2242+ self.setMeminfo, count=1)
2243+ register(
2244+ ("project", "series",),
2245+ self.setTestRun, count=1)
2246+ register(
2247+ ("test_run", "architecture",),
2248+ self.setArchitectureState, count=1)
2249+ register(
2250+ ("test_run", "memory",),
2251+ self.setMemoryState, count=1)
2252+ register(
2253+ ("test_run", "processor",),
2254+ self.setProcessorState, count=1)
2255+ register(
2256+ ("udevadm", "bits", "udevadm_result",),
2257+ self.setUdevadm, count=1)
2258+
2259+ # Publish events passed as keyword arguments
2260+ if "project" in kwargs:
2261+ self.dispatcher.publishEvent("project", kwargs.pop("project"))
2262+ self.dispatcher.publishEvent("series", kwargs.pop("series", None))
2263+
2264+ def addAttachment(self, test_run, attachment):
2265+ test_run.addAttachment(**attachment)
2266+
2267+ def addContext(self, text, command=None):
2268+ if text.strip() == "Command not found.":
2269+ return
2270+
2271+ self.dispatcher.publishEvent(
2272+ "attachment", {"name": command, "content": text })
2273+
2274+ parsers = {
2275+ "cat /proc/cpuinfo": self.parseCpuinfo,
2276+ "cat /proc/meminfo": self.parseMeminfo,
2277+ "dmidecode": DmidecodeParser,
2278+ "udevadm info --export-db": self.parseUdevadm,
2279+ }
2280+ parser = parsers.get(command)
2281+ if parser:
2282+ if not isinstance(text, unicode):
2283+ text = text.decode("utf-8")
2284+ stream = StringIO(text)
2285+ p = parser(stream)
2286+ p.run(self)
2287+
2288+ def addCpu(self, cpu):
2289+ self.dispatcher.publishEvent("cpu", cpu)
2290+
2291+ def addCpuArchitecture(self, cpu, architecture):
2292+ if cpu["debian_name"] == architecture:
2293+ self.dispatcher.publishEvent("machine", cpu["gnu_name"])
2294+ self.dispatcher.publishEvent("bits", cpu["bits"])
2295+
2296+ def addDevice(self, device):
2297+ self.dispatcher.publishEvent("device", device)
2298+
2299+ def addDeviceState(self, test_run, device):
2300+ test_run.addDeviceState(
2301+ bus_name=device.bus, category_name=device.category,
2302+ product_name=device.product, vendor_name=device.vendor,
2303+ product_id=device.product_id, vendor_id=device.vendor_id,
2304+ subproduct_id=device.subproduct_id,
2305+ subvendor_id=device.subvendor_id,
2306+ driver_name=device.driver, path=device.path)
2307+
2308+ def addDmiDevice(self, device):
2309+ if device.serial:
2310+ self.dispatcher.publishEvent("identifier", device.serial)
2311+
2312+ if device.category in ("BOARD", "SYSTEM") \
2313+ and device.vendor != device.product \
2314+ and device.product is not None:
2315+ self.dispatcher.publishEvent("model", device.product)
2316+ self.dispatcher.publishEvent("make", device.vendor)
2317+ self.dispatcher.publishEvent("version", device.version)
2318+
2319+ def addIdentifier(self, identifier):
2320+ try:
2321+ self.identifiers.append(identifier)
2322+ except AttributeError:
2323+ self.identifiers = [identifier]
2324+ self.dispatcher.publishEvent("identifiers", self.identifiers)
2325+
2326+ def addPackage(self, package):
2327+ package_version = {
2328+ "name": package["name"],
2329+ "version": package["properties"]["version"],
2330+ }
2331+ self.dispatcher.publishEvent("package_version", package_version)
2332+
2333+ def addPackageVersion(self, test_run, package_version):
2334+ test_run.addPackageVersion(**package_version)
2335+
2336+ def addQuestion(self, question):
2337+ answer_to_status = {
2338+ "fail": FAIL,
2339+ "no": FAIL,
2340+ "pass": PASS,
2341+ "skip": UNTESTED,
2342+ "uninitiated": UNINITIATED,
2343+ "unresolved": UNRESOLVED,
2344+ "unsupported": UNSUPPORTED,
2345+ "untested": UNTESTED,
2346+ "yes": PASS,
2347+ }
2348+
2349+ test_result = dict(
2350+ name=question["name"],
2351+ output=question["comment"],
2352+ status=answer_to_status[question["answer"]["value"]],
2353+ )
2354+ test_result.update(self.test_run_kwargs)
2355+ self.dispatcher.publishEvent("test_result", test_result)
2356+
2357+ def addTestResult(self, test_run, test_result):
2358+ test_run.addTestResult(**test_result)
2359+
2360+ def addSummary(self, name, value):
2361+ if name == "architecture":
2362+ self.dispatcher.publishEvent("architecture", value)
2363+ elif name == "distribution":
2364+ self.dispatcher.publishEvent("project", value)
2365+ elif name == "distroseries":
2366+ self.dispatcher.publishEvent("series", value)
2367+
2368+ def parseCpuinfo(self, cpuinfo):
2369+ self.dispatcher.publishEvent("cpuinfo", cpuinfo)
2370+ return DeferredParser(self.dispatcher, "cpuinfo_result")
2371+
2372+ def parseMeminfo(self, meminfo):
2373+ self.dispatcher.publishEvent("meminfo", meminfo)
2374+ return DeferredParser(self.dispatcher, "meminfo_result")
2375+
2376+ def parseUdevadm(self, udevadm):
2377+ self.dispatcher.publishEvent("udevadm", udevadm)
2378+ return DeferredParser(self.dispatcher, "udevadm_result")
2379+
2380+ def setArchitecture(self, architecture):
2381+ string = resource_string(parsers.__name__, "cputable")
2382+ stream = StringIO(string.decode("utf-8"))
2383+ parser = CputableParser(stream)
2384+ parser.run(self)
2385+
2386+ def setArchitectureState(self, test_run, architecture):
2387+ test_run.setArchitectureState(architecture)
2388+
2389+ def setCpuinfo(self, cpuinfo, machine, cpuinfo_result):
2390+ parser = CpuinfoParser(cpuinfo, machine)
2391+ parser.run(cpuinfo_result)
2392+
2393+ def setMeminfo(self, meminfo, meminfo_result):
2394+ parser = MeminfoParser(meminfo)
2395+ parser.run(meminfo_result)
2396+
2397+ def setDistribution(self, test_run, distribution):
2398+ test_run.setDistribution(**distribution)
2399+
2400+ def setLSBRelease(self, lsb_release):
2401+ self.dispatcher.publishEvent("distribution", lsb_release)
2402+
2403+ def setMemory(self, memory):
2404+ self.dispatcher.publishEvent("memory", memory)
2405+
2406+ def setMemoryState(self, test_run, memory):
2407+ test_run.setMemoryState(**memory)
2408+
2409+ def setProcessor(self, processor):
2410+ self.dispatcher.publishEvent("processor", processor)
2411+
2412+ def setProcessorState(self, test_run, processor):
2413+ test_run.setProcessorState(
2414+ platform_name=processor["platform"],
2415+ make=processor["type"], model=processor["model"],
2416+ model_number=processor["model_number"],
2417+ model_version=processor["model_version"],
2418+ model_revision=processor["model_revision"],
2419+ cache=processor["cache"], other=processor["other"],
2420+ bogomips=processor["bogomips"], speed=processor["speed"],
2421+ count=processor["count"])
2422+
2423+ def setTestRun(self, project, series):
2424+ test_run = self.test_run_factory(
2425+ **self.test_run_kwargs)
2426+ self.dispatcher.publishEvent("test_run", test_run)
2427+
2428+ def setUdevadm(self, udevadm, bits, udevadm_result):
2429+ parser = UdevadmParser(udevadm, bits)
2430+ parser.run(udevadm_result)
2431+
2432+
2433+class SubmissionParser:
2434+
2435+ def __init__(self, file):
2436+ self.file = file
2437+ self.logger = getLogger()
2438+
2439+ def _getClient(self, node):
2440+ """Return a dictionary with the name and version of the client."""
2441+ return {
2442+ "name": node.get("name"),
2443+ "version": node.get("version"),
2444+ }
2445+
2446+ def _getProperty(self, node):
2447+ """Return the (name, value) of a property."""
2448+ return (node.get("name"), self._getValueAsType(node))
2449+
2450+ def _getProperties(self, node):
2451+ """Return a dictionary of properties."""
2452+ properties = {}
2453+ for child in node.getchildren():
2454+ assert child.tag == "property", \
2455+ "Unexpected tag <%s>, expected <property>" % child.tag
2456+ name, value = self._getProperty(child)
2457+ properties[name] = value
2458+
2459+ return properties
2460+
2461+ def _getValueAsType(self, node):
2462+ """Return value of a node as the type attribute."""
2463+ type_ = node.get("type")
2464+ if type_ in ("bool",):
2465+ value = node.text.strip()
2466+ assert value in ("True", "False",), \
2467+ "Unexpected boolean value '%s' in <%s>" % (value, node.tag)
2468+ return value == "True"
2469+ elif type_ in ("str",):
2470+ return unicode(node.text.strip())
2471+ elif type_ in ("int", "long",):
2472+ return int(node.text.strip())
2473+ elif type_ in ("float",):
2474+ return float(node.text.strip())
2475+ elif type_ in ("list",):
2476+ return list(self._getValueAsType(child)
2477+ for child in node.getchildren())
2478+ elif type_ in ("dict",):
2479+ return dict((child.get("name"), self._getValueAsType(child))
2480+ for child in node.getchildren())
2481+ else:
2482+ raise AssertionError(
2483+ "Unexpected type '%s' in <%s>" % (type_, node.tag))
2484+
2485+ def _getValueAsBoolean(self, node):
2486+ """Return the value of the attribute "value" as a boolean."""
2487+ value = node.attrib["value"]
2488+ assert value in ("True", "False",), \
2489+ "Unexpected boolean value '%s' in tag <%s>" % (value, node.tag)
2490+ return value == "True"
2491+
2492+ def _getValueAsDatetime(self, node):
2493+ """Return the value of the attribute "value" as a datetime."""
2494+ string = node.attrib["value"]
2495+ return string_to_datetime(string)
2496+
2497+ def _getValueAsString(self, node):
2498+ """Return the value of the attribute "value"."""
2499+ return unicode(node.attrib["value"])
2500+
2501+ def parseContext(self, result, node):
2502+ """Parse the <context> part of a submission."""
2503+ duplicates = set()
2504+ for child in node.getchildren():
2505+ assert child.tag == "info", \
2506+ "Unexpected tag <%s>, expected <info>" % child.tag
2507+ command = child.get("command")
2508+ if command not in duplicates:
2509+ duplicates.add(command)
2510+ text = child.text
2511+ if text is None:
2512+ text = ""
2513+ result.addContext(text, command)
2514+ else:
2515+ self.logger.debug(
2516+ "Duplicate command found in tag <info>: %s" % command)
2517+
2518+ def parseHardware(self, result, node):
2519+ """Parse the <hardware> section of a submission."""
2520+ parsers = {
2521+ "dmi": DmidecodeParser,
2522+ "processors": self.parseProcessors,
2523+ "udev": result.parseUdevadm,
2524+ }
2525+
2526+ for child in node.getchildren():
2527+ parser = parsers.get(child.tag)
2528+ if parser:
2529+ if child.getchildren():
2530+ parser(result, child)
2531+ else:
2532+ text = child.text
2533+ if not isinstance(text, unicode):
2534+ text = text.decode("utf-8")
2535+ stream = StringIO(text)
2536+ p = parser(stream)
2537+ p.run(result)
2538+ else:
2539+ self.logger.debug(
2540+ "Unsupported tag <%s> in <hardware>" % child.tag)
2541+
2542+ def parseLSBRelease(self, result, node):
2543+ """Parse the <lsbrelease> part of a submission."""
2544+ properties = self._getProperties(node)
2545+ result.setLSBRelease(properties)
2546+
2547+ def parsePackages(self, result, node):
2548+ """Parse the <packages> part of a submission."""
2549+ for child in node.getchildren():
2550+ assert child.tag == "package", \
2551+ "Unexpected tag <%s>, expected <package>" % child.tag
2552+
2553+ package = {
2554+ "name": child.get("name"),
2555+ "properties": self._getProperties(child),
2556+ }
2557+ result.addPackage(package)
2558+
2559+ def parseProcessors(self, result, node):
2560+ """Parse the <processors> part of a submission."""
2561+ processors = []
2562+ for child in node.getchildren():
2563+ assert child.tag == "processor", \
2564+ "Unexpected tag <%s>, expected <processor>" % child.tag
2565+
2566+ # Convert lists to space separated strings.
2567+ properties = self._getProperties(child)
2568+ for key, value in properties.iteritems():
2569+ if key in ("bogomips", "cache", "count", "speed",):
2570+ properties[key] = int(value)
2571+ elif isinstance(value, list):
2572+ properties[key] = u" ".join(value)
2573+ processors.append(properties)
2574+
2575+ # Check if /proc/cpuinfo was parsed already.
2576+ if any("platform" in processor for processor in processors):
2577+ result.setProcessor(processors[0])
2578+ else:
2579+ lines = []
2580+ for processor in processors:
2581+ # Convert some keys with underscores to spaces instead.
2582+ for key, value in processor.iteritems():
2583+ if "_" in key and key != "vendor_id":
2584+ key = key.replace("_", " ")
2585+
2586+ lines.append(u"%s: %s" % (key, value))
2587+
2588+ lines.append(u"")
2589+
2590+ stream = StringIO(u"\n".join(lines))
2591+ parser = result.parseCpuinfo(stream)
2592+ parser.run(result)
2593+
2594+ def parseQuestions(self, result, node):
2595+ """Parse the <questions> part of a submission."""
2596+ for child in node.getchildren():
2597+ assert child.tag == "question", \
2598+ "Unexpected tag <%s>, expected <question>" % child.tag
2599+ question = {
2600+ "name": child.get("name"),
2601+ "targets": [],
2602+ }
2603+ plugin = child.get("plugin", None)
2604+ if plugin is not None:
2605+ question["plugin"] = plugin
2606+
2607+ answer_choices = []
2608+ for sub_node in child.getchildren():
2609+ sub_tag = sub_node.tag
2610+ if sub_tag == "answer":
2611+ question["answer"] = answer = {}
2612+ answer["type"] = sub_node.get("type")
2613+ if answer["type"] == "multiple_choice":
2614+ question["answer_choices"] = answer_choices
2615+ unit = sub_node.get("unit", None)
2616+ if unit is not None:
2617+ answer["unit"] = unit
2618+ answer["value"] = sub_node.text.strip()
2619+
2620+ elif sub_tag == "answer_choices":
2621+ for value_node in sub_node.getchildren():
2622+ answer_choices.append(
2623+ self._getValueAsType(value_node))
2624+
2625+ elif sub_tag == "target":
2626+ # The Relax NG schema ensures that the attribute
2627+ # id exists and that it is an integer
2628+ target = {"id": int(sub_node.get("id"))}
2629+ target["drivers"] = drivers = []
2630+ for driver_node in sub_node.getchildren():
2631+ drivers.append(driver_node.text.strip())
2632+ question["targets"].append(target)
2633+
2634+ elif sub_tag in ("comment", "command",):
2635+ text = sub_node.text
2636+ if text is None:
2637+ text = ""
2638+ question[sub_tag] = text.strip()
2639+
2640+ else:
2641+ raise AssertionError(
2642+ "Unexpected tag <%s> in <question>" % sub_tag)
2643+
2644+ result.addQuestion(question)
2645+
2646+ def parseSoftware(self, result, node):
2647+ """Parse the <software> section of a submission."""
2648+ parsers = {
2649+ "lsbrelease": self.parseLSBRelease,
2650+ "packages": self.parsePackages,
2651+ }
2652+
2653+ for child in node.getchildren():
2654+ parser = parsers.get(child.tag)
2655+ if parser:
2656+ parser(result, child)
2657+ else:
2658+ self.logger.debug(
2659+ "Unsupported tag <%s> in <software>" % child.tag)
2660+
2661+ def parseSummary(self, result, node):
2662+ """Parse the <summary> section of a submission."""
2663+ parsers = {
2664+ "architecture": self._getValueAsString,
2665+ "client": self._getClient,
2666+ "contactable": self._getValueAsBoolean,
2667+ "date_created": self._getValueAsDatetime,
2668+ "distribution": self._getValueAsString,
2669+ "distroseries": self._getValueAsString,
2670+ "kernel-release": self._getValueAsString,
2671+ "live_cd": self._getValueAsBoolean,
2672+ "private": self._getValueAsBoolean,
2673+ "system_id": self._getValueAsString,
2674+ }
2675+
2676+ for child in node.getchildren():
2677+ parser = parsers.get(child.tag)
2678+ if parser:
2679+ value = parser(child)
2680+ result.addSummary(child.tag, value)
2681+ else:
2682+ self.logger.debug(
2683+ "Unsupported tag <%s> in <summary>" % child.tag)
2684+
2685+ def parseRoot(self, result, node):
2686+ """Parse the <system> root of a submission."""
2687+ parsers = {
2688+ "context": self.parseContext,
2689+ "hardware": self.parseHardware,
2690+ "questions": self.parseQuestions,
2691+ "software": self.parseSoftware,
2692+ "summary": self.parseSummary,
2693+ }
2694+
2695+ # Iterate over the root children, "summary" first
2696+ for child in node.getchildren():
2697+ parser = parsers.get(child.tag)
2698+ if parser:
2699+ parser(result, child)
2700+ else:
2701+ self.logger.debug(
2702+ "Unsupported tag <%s> in <system>" % child.tag)
2703+
2704+ def run(self, test_run_factory, **kwargs):
2705+ parser = etree.XMLParser()
2706+
2707+ tree = etree.parse(self.file, parser=parser)
2708+ root = tree.getroot()
2709+ if root.tag != "system":
2710+ raise AssertionError(
2711+ "Unexpected tag <%s> at root, expected <system>" % root.tag)
2712+
2713+ result = SubmissionResult(test_run_factory, **kwargs)
2714+ self.parseRoot(result, root)
2715+
2716+ return result
2717
2718=== added directory 'checkbox/parsers/tests'
2719=== added file 'checkbox/parsers/tests/__init__.py'
2720=== added file 'checkbox/parsers/tests/cputable.py'
2721--- checkbox/parsers/tests/cputable.py 1970-01-01 00:00:00 +0000
2722+++ checkbox/parsers/tests/cputable.py 2012-02-21 14:59:19 +0000
2723@@ -0,0 +1,74 @@
2724+#
2725+# This file is part of Checkbox.
2726+#
2727+# Copyright 2012 Canonical Ltd.
2728+#
2729+# Checkbox is free software: you can redistribute it and/or modify
2730+# it under the terms of the GNU General Public License as published by
2731+# the Free Software Foundation, either version 3 of the License, or
2732+# (at your option) any later version.
2733+#
2734+# Checkbox is distributed in the hope that it will be useful,
2735+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2736+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2737+# GNU General Public License for more details.
2738+#
2739+# You should have received a copy of the GNU General Public License
2740+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2741+#
2742+from StringIO import StringIO
2743+
2744+from unittest import TestCase
2745+
2746+from checkbox.parsers.cputable import CputableParser
2747+
2748+
2749+class CputableResult:
2750+
2751+ def __init__(self):
2752+ self.cpus = []
2753+
2754+ def addCpu(self, cpu):
2755+ self.cpus.append(cpu)
2756+
2757+ def getByDebianName(self, name):
2758+ for cpu in self.cpus:
2759+ if cpu["debian_name"] == name:
2760+ return cpu
2761+
2762+ return None
2763+
2764+ def getByGnuName(self, name):
2765+ for cpu in self.cpus:
2766+ if cpu["gnu_name"] == name:
2767+ return cpu
2768+
2769+ return None
2770+
2771+
2772+class TestCputableParser(TestCase):
2773+
2774+ def getParser(self, string):
2775+ stream = StringIO(string)
2776+ return CputableParser(stream)
2777+
2778+ def getResult(self, string):
2779+ parser = self.getParser(string)
2780+ result = CputableResult()
2781+ parser.run(result)
2782+ return result
2783+
2784+ def test_empty(self):
2785+ result = self.getResult("")
2786+ self.assertEquals(result.cpus, [])
2787+
2788+ def test_i386(self):
2789+ result = self.getResult("""
2790+# <Debian name> <GNU name> <config.guess regex> <Bits> <Endianness>
2791+i386 i686 (i[3456]86|pentium) 32 little
2792+""")
2793+ debian_cpu = result.getByDebianName("i386")
2794+ self.assertNotEquals(debian_cpu, None)
2795+ gnu_cpu = result.getByGnuName("i686")
2796+ self.assertNotEquals(gnu_cpu, None)
2797+ self.assertEquals(debian_cpu, gnu_cpu)
2798
2799=== added file 'checkbox/parsers/tests/description.py'
2800--- checkbox/parsers/tests/description.py 1970-01-01 00:00:00 +0000
2801+++ checkbox/parsers/tests/description.py 2012-02-21 14:59:19 +0000
2802@@ -0,0 +1,146 @@
2803+# -*- coding: utf-8 -*-
2804+#
2805+# This file is part of Checkbox.
2806+#
2807+# Copyright 2012 Canonical Ltd.
2808+#
2809+# Checkbox is free software: you can redistribute it and/or modify
2810+# it under the terms of the GNU General Public License as published by
2811+# the Free Software Foundation, either version 3 of the License, or
2812+# (at your option) any later version.
2813+#
2814+# Checkbox is distributed in the hope that it will be useful,
2815+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2816+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2817+# GNU General Public License for more details.
2818+#
2819+# You should have received a copy of the GNU General Public License
2820+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2821+#
2822+from StringIO import StringIO
2823+
2824+from unittest import TestCase
2825+
2826+from checkbox.parsers.description import DescriptionParser
2827+
2828+
2829+class DescriptionResult:
2830+
2831+ purpose = None
2832+ steps = None
2833+ verification = None
2834+ info = None
2835+
2836+ def setDescription(self, purpose, steps, verification, info):
2837+ self.purpose = purpose
2838+ self.steps = steps
2839+ self.verification = verification
2840+ self.info = info
2841+
2842+
2843+class TestDescriptionParser(TestCase):
2844+
2845+ def getParser(self, string):
2846+ stream = StringIO(string)
2847+ return DescriptionParser(stream)
2848+
2849+ def getResult(self, string):
2850+ parser = self.getParser(string)
2851+ result = DescriptionResult()
2852+ parser.run(result)
2853+ return result
2854+
2855+ def assertResult(
2856+ self, result, purpose=None, steps=None, verification=None, info=None):
2857+ self.assertEquals(result.purpose, purpose)
2858+ self.assertEquals(result.steps, steps)
2859+ self.assertEquals(result.verification, verification)
2860+ self.assertEquals(result.info, info)
2861+
2862+ def test_empty(self):
2863+ result = self.getResult("")
2864+ self.assertEquals(result.purpose, None)
2865+
2866+ def test_purpose(self):
2867+ result = self.getResult("""
2868+PURPOSE:
2869+ foo
2870+""")
2871+ self.assertResult(result)
2872+
2873+ def test_purpose_steps(self):
2874+ result = self.getResult("""
2875+PURPOSE:
2876+ foo
2877+STEPS:
2878+ bar
2879+""")
2880+ self.assertResult(result)
2881+
2882+ def test_purpose_steps_verification(self):
2883+ result = self.getResult("""
2884+PURPOSE:
2885+ foo
2886+STEPS:
2887+ bar
2888+VERIFICATION:
2889+ baz
2890+""")
2891+ self.assertResult(result, "foo\n", "bar\n", "baz\n")
2892+
2893+ def test_purpose_steps_info_verification(self):
2894+ result = self.getResult("""
2895+PURPOSE:
2896+ foo
2897+STEPS:
2898+ bar
2899+INFO:
2900+ $output
2901+VERIFICATION:
2902+ baz
2903+""")
2904+ self.assertResult(result, "foo\n", "bar\n", "baz\n", "$output\n")
2905+
2906+ def test_purpose_steps_verification_other(self):
2907+ result = self.getResult("""
2908+PURPOSE:
2909+ foo
2910+STEPS:
2911+ bar
2912+VERIFICATION:
2913+ baz
2914+OTHER:
2915+ blah
2916+""")
2917+ self.assertResult(result)
2918+
2919+ def test_es(self):
2920+ result = self.getResult(u"""
2921+PROPÓSITO:
2922+ Esta prueba verifica los diferentes modos de vídeo detectados
2923+PASOS:
2924+ 1. Se han detectado las siguientes pantallas y modos de vídeo
2925+INFORMACIÓN:
2926+ $ salida
2927+VERIFICACIÓN:
2928+ ¿Son las pantallas y los modos de vídeo correctos?
2929+""")
2930+ self.assertNotEquals(result.purpose, None)
2931+ self.assertNotEquals(result.steps, None)
2932+ self.assertNotEquals(result.verification, None)
2933+ self.assertEquals(result.info, "$output\n")
2934+
2935+ def test_ru(self):
2936+ result = self.getResult(u"""
2937+ЦЕЛЬ:
2938+ Эта проверка позволит убедиться в работоспособности штекера наушников
2939+ДЕЙСТВИЯ:
2940+ 1. Подсоедините наушники к вашему звуковому устройству
2941+ 2. Щёлкните кнопку Проверить для воспроизведения звукового сигнала через звуковое устройство
2942+ПРОВЕРКА:
2943+ Был ли слышен звук в наушниках и был ли он воспроизведён в ваших наушниках без искажений, щелчков или других искажённых звуков?"
2944+""")
2945+ self.assertNotEquals(result.purpose, None)
2946+ self.assertNotEquals(result.steps, None)
2947+ self.assertNotEquals(result.verification, None)
2948+ self.assertEquals(result.info, None)
2949
2950=== added file 'checkbox/parsers/tests/dmi.py'
2951--- checkbox/parsers/tests/dmi.py 1970-01-01 00:00:00 +0000
2952+++ checkbox/parsers/tests/dmi.py 2012-02-21 14:59:19 +0000
2953@@ -0,0 +1,80 @@
2954+#
2955+# This file is part of Checkbox.
2956+#
2957+# Copyright 2012 Canonical Ltd.
2958+#
2959+# Checkbox is free software: you can redistribute it and/or modify
2960+# it under the terms of the GNU General Public License as published by
2961+# the Free Software Foundation, either version 3 of the License, or
2962+# (at your option) any later version.
2963+#
2964+# Checkbox is distributed in the hope that it will be useful,
2965+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2966+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2967+# GNU General Public License for more details.
2968+#
2969+# You should have received a copy of the GNU General Public License
2970+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2971+#
2972+class DmiResult:
2973+
2974+ def __init__(self):
2975+ self.devices = []
2976+
2977+ def addDmiDevice(self, device):
2978+ self.devices.append(device)
2979+
2980+ def getDevice(self, category):
2981+ for device in self.devices:
2982+ if device.category == category:
2983+ return device
2984+
2985+ return None
2986+
2987+
2988+class TestDmiMixin:
2989+
2990+ def getParser(self):
2991+ raise NotImplementedError()
2992+
2993+ def getResult(self):
2994+ parser = self.getParser()
2995+ result = DmiResult()
2996+ parser.run(result)
2997+ return result
2998+
2999+ def test_devices(self):
3000+ result = self.getResult()
3001+ self.assertEquals(len(result.devices), 4)
3002+
3003+ def test_bios(self):
3004+ result = self.getResult()
3005+ device = result.getDevice("BIOS")
3006+ self.assertTrue(device)
3007+ self.assertEquals(device.product, "BIOS PRODUCT")
3008+ self.assertEquals(device.vendor, "BIOS VENDOR")
3009+ self.assertEquals(device.serial, None)
3010+
3011+ def test_board(self):
3012+ result = self.getResult()
3013+ device = result.getDevice("BOARD")
3014+ self.assertTrue(device)
3015+ self.assertEquals(device.product, None)
3016+ self.assertEquals(device.vendor, None)
3017+ self.assertEquals(device.serial, None)
3018+
3019+ def test_chassis(self):
3020+ result = self.getResult()
3021+ device = result.getDevice("CHASSIS")
3022+ self.assertTrue(device)
3023+ self.assertEquals(device.product, "Notebook")
3024+ self.assertEquals(device.vendor, "CHASSIS VENDOR")
3025+ self.assertEquals(device.serial, None)
3026+
3027+ def test_system(self):
3028+ result = self.getResult()
3029+ device = result.getDevice("SYSTEM")
3030+ self.assertTrue(device)
3031+ self.assertEquals(device.product, "SYSTEM PRODUCT")
3032+ self.assertEquals(device.vendor, "SYSTEM VENDOR")
3033+ self.assertEquals(device.serial, "SYSTEM SERIAL")
3034
3035=== added file 'checkbox/parsers/tests/dmidecode.py'
3036--- checkbox/parsers/tests/dmidecode.py 1970-01-01 00:00:00 +0000
3037+++ checkbox/parsers/tests/dmidecode.py 2012-02-21 14:59:19 +0000
3038@@ -0,0 +1,60 @@
3039+#
3040+# This file is part of Checkbox.
3041+#
3042+# Copyright 2012 Canonical Ltd.
3043+#
3044+# Checkbox is free software: you can redistribute it and/or modify
3045+# it under the terms of the GNU General Public License as published by
3046+# the Free Software Foundation, either version 3 of the License, or
3047+# (at your option) any later version.
3048+#
3049+# Checkbox is distributed in the hope that it will be useful,
3050+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3051+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3052+# GNU General Public License for more details.
3053+#
3054+# You should have received a copy of the GNU General Public License
3055+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3056+#
3057+from cStringIO import StringIO
3058+
3059+from unittest import TestCase
3060+
3061+from checkbox.parsers.dmidecode import DmidecodeParser
3062+from checkbox.parsers.tests.dmi import TestDmiMixin
3063+
3064+
3065+FAKE_DMIDECODE = """\
3066+# dmidecode 2.9
3067+SMBIOS 2.4 present.
3068+
3069+Handle 0x0000, DMI type 0, 24 bytes
3070+BIOS Information
3071+\tVendor: BIOS VENDOR
3072+\tVersion: BIOS PRODUCT
3073+
3074+Handle 0x0001, DMI type 1, 27 bytes
3075+System Information
3076+\tManufacturer: SYSTEM VENDOR
3077+\tProduct Name: SYSTEM PRODUCT
3078+\tSerial Number: SYSTEM SERIAL
3079+
3080+Handle 0x0002, DMI type 2, 8 bytes
3081+Base Board Information
3082+\tManufacturer: Not Available
3083+\tProduct Name: Not Available
3084+\tSerial Number: Not Available
3085+
3086+Handle 0x0003, DMI type 3, 13 bytes
3087+Chassis Information
3088+\tManufacturer: CHASSIS VENDOR
3089+\tType: Notebook
3090+\tSerial Number: Not Available
3091+"""
3092+
3093+
3094+class TestDmidecodeParser(TestCase, TestDmiMixin):
3095+
3096+ def getParser(self):
3097+ stream = StringIO(FAKE_DMIDECODE)
3098+ return DmidecodeParser(stream)
3099
3100=== added file 'checkbox/parsers/tests/udevadm.py'
3101--- checkbox/parsers/tests/udevadm.py 1970-01-01 00:00:00 +0000
3102+++ checkbox/parsers/tests/udevadm.py 2012-02-21 14:59:19 +0000
3103@@ -0,0 +1,68 @@
3104+#
3105+# This file is part of Checkbox.
3106+#
3107+# Copyright 2012 Canonical Ltd.
3108+#
3109+# Checkbox is free software: you can redistribute it and/or modify
3110+# it under the terms of the GNU General Public License as published by
3111+# the Free Software Foundation, either version 3 of the License, or
3112+# (at your option) any later version.
3113+#
3114+# Checkbox is distributed in the hope that it will be useful,
3115+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3116+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3117+# GNU General Public License for more details.
3118+#
3119+# You should have received a copy of the GNU General Public License
3120+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3121+#
3122+from cStringIO import StringIO
3123+
3124+from unittest import TestCase
3125+
3126+from checkbox.parsers.udevadm import UdevadmParser
3127+
3128+
3129+class DeviceResult:
3130+
3131+ def __init__(self):
3132+ self.devices = []
3133+
3134+ def addDevice(self, device):
3135+ self.devices.append(device)
3136+
3137+ def getDevice(self, category):
3138+ for device in self.devices:
3139+ if device.category == category:
3140+ return device
3141+
3142+ return None
3143+
3144+
3145+class TestUdevadmParser(TestCase):
3146+
3147+ def getParser(self, string):
3148+ stream = StringIO(string)
3149+ return UdevadmParser(stream)
3150+
3151+ def getResult(self, string):
3152+ parser = self.getParser(string)
3153+ result = DeviceResult()
3154+ parser.run(result)
3155+ return result
3156+
3157+ def test_usb_capture(self):
3158+ result = self.getResult("""
3159+P: /devices/pci0000:00/0000:00:1a.7/usb1/1-6/1-6:1.0
3160+E: UDEV_LOG=3
3161+E: DEVPATH=/devices/pci0000:00/0000:00:1a.7/usb1/1-6/1-6:1.0
3162+E: DEVTYPE=usb_interface
3163+E: DRIVER=uvcvideo
3164+E: PRODUCT=17ef/480c/3134
3165+E: TYPE=239/2/1
3166+E: INTERFACE=14/1/0
3167+E: MODALIAS=usb:v17EFp480Cd3134dcEFdsc02dp01ic0Eisc01ip00
3168+E: SUBSYSTEM=usb
3169+""")
3170+ device = result.getDevice("CAPTURE")
3171+ self.assertTrue(device)
3172
3173=== added file 'checkbox/parsers/udevadm.py'
3174--- checkbox/parsers/udevadm.py 1970-01-01 00:00:00 +0000
3175+++ checkbox/parsers/udevadm.py 2012-02-21 14:59:19 +0000
3176@@ -0,0 +1,470 @@
3177+#
3178+# This file is part of Checkbox.
3179+#
3180+# Copyright 2011 Canonical Ltd.
3181+#
3182+# Checkbox is free software: you can redistribute it and/or modify
3183+# it under the terms of the GNU General Public License as published by
3184+# the Free Software Foundation, either version 3 of the License, or
3185+# (at your option) any later version.
3186+#
3187+# Checkbox is distributed in the hope that it will be useful,
3188+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3189+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3190+# GNU General Public License for more details.
3191+#
3192+# You should have received a copy of the GNU General Public License
3193+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3194+#
3195+import re
3196+import string
3197+
3198+from checkbox.lib.bit import (
3199+ get_bitmask,
3200+ test_bit,
3201+ )
3202+from checkbox.lib.input import Input
3203+from checkbox.lib.pci import Pci
3204+from checkbox.lib.usb import Usb
3205+
3206+
3207+PCI_RE = re.compile(
3208+ r"^pci:"
3209+ r"v(?P<vendor_id>[%(hex)s]{8})"
3210+ r"d(?P<product_id>[%(hex)s]{8})"
3211+ r"sv(?P<subvendor_id>[%(hex)s]{8})"
3212+ r"sd(?P<subproduct_id>[%(hex)s]{8})"
3213+ r"bc(?P<class>[%(hex)s]{2})"
3214+ r"sc(?P<subclass>[%(hex)s]{2})"
3215+ r"i(?P<interface>[%(hex)s]{2})"
3216+ % {"hex": string.hexdigits})
3217+PNP_RE = re.compile(
3218+ r"^acpi:"
3219+ r"(?P<vendor_name>[%(upper)s]{3})"
3220+ r"(?P<product_id>[%(hex)s]{4}):"
3221+ % {"upper": string.uppercase, "hex": string.hexdigits})
3222+USB_RE = re.compile(
3223+ r"^usb:"
3224+ r"v(?P<vendor_id>[%(hex)s]{4})"
3225+ r"p(?P<product_id>[%(hex)s]{4})"
3226+ r"d(?P<revision>[%(hex)s]{4})"
3227+ r"dc(?P<class>[%(hex)s]{2})"
3228+ r"dsc(?P<subclass>[%(hex)s]{2})"
3229+ r"dp(?P<protocol>[%(hex)s]{2})"
3230+ r"ic(?P<interface_class>[%(hex)s]{2})"
3231+ r"isc(?P<interface_subclass>[%(hex)s]{2})"
3232+ r"ip(?P<interface_protocol>[%(hex)s]{2})"
3233+ % {"hex": string.hexdigits})
3234+SCSI_RE = re.compile(
3235+ r"^scsi:"
3236+ r"t-0x(?P<type>[%(hex)s]{2})"
3237+ % {"hex": string.hexdigits})
3238+
3239+
3240+class UdevadmDevice:
3241+ __slots__ = ("_environment", "_bits", "_stack",)
3242+
3243+ def __init__(self, environment, bits=None, stack=[]):
3244+ self._environment = environment
3245+ self._bits = bits
3246+ self._stack = stack
3247+
3248+ @property
3249+ def bus(self):
3250+ # Change the bus from 'acpi' to 'pnp' for some devices
3251+ if PNP_RE.match(self._environment.get("MODALIAS", "")) \
3252+ and self.path.endswith(":00"):
3253+ return "pnp"
3254+
3255+ # Change the bus from 'block' to parent
3256+ if self._environment.get("DEVTYPE") == "disk" and self._stack:
3257+ return self._stack[-1]._environment.get("SUBSYSTEM")
3258+
3259+ bus = self._environment.get("SUBSYSTEM")
3260+ if bus == "pnp":
3261+ return None
3262+
3263+ return bus
3264+
3265+ @property
3266+ def category(self):
3267+ if "IFINDEX" in self._environment:
3268+ return "NETWORK"
3269+
3270+ if "PCI_CLASS" in self._environment:
3271+ pci_class_string = self._environment["PCI_CLASS"]
3272+ pci_class = int(pci_class_string, 16)
3273+
3274+ # Strip prog_if if defined
3275+ if pci_class > 0xFFFF:
3276+ pci_class >>= 8
3277+
3278+ subclass_id = pci_class & 0xFF
3279+ class_id = (pci_class >> 8) & 0xFF
3280+
3281+ if class_id == Pci.BASE_CLASS_NETWORK:
3282+ if subclass_id == Pci.CLASS_NETWORK_WIRELESS:
3283+ return "WIRELESS"
3284+ else:
3285+ return "NETWORK"
3286+
3287+ if class_id == Pci.BASE_CLASS_DISPLAY:
3288+ if subclass_id == Pci.CLASS_DISPLAY_VGA:
3289+ return "VIDEO"
3290+ else:
3291+ return "OTHER"
3292+
3293+ if class_id == Pci.BASE_CLASS_SERIAL \
3294+ and subclass_id == Pci.CLASS_SERIAL_USB:
3295+ return "USB"
3296+
3297+ if class_id == Pci.BASE_CLASS_STORAGE:
3298+ if subclass_id == Pci.CLASS_STORAGE_SCSI:
3299+ return "SCSI"
3300+
3301+ if subclass_id == Pci.CLASS_STORAGE_IDE:
3302+ return "IDE"
3303+
3304+ if subclass_id == Pci.CLASS_STORAGE_FLOPPY:
3305+ return "FLOPPY"
3306+
3307+ if subclass_id == Pci.CLASS_STORAGE_RAID:
3308+ return "RAID"
3309+
3310+ if class_id == Pci.BASE_CLASS_COMMUNICATION \
3311+ and subclass_id == Pci.CLASS_COMMUNICATION_MODEM:
3312+ return "MODEM"
3313+
3314+ if class_id == Pci.BASE_CLASS_INPUT \
3315+ and subclass_id == Pci.CLASS_INPUT_SCANNER:
3316+ return "SCANNER"
3317+
3318+ if class_id == Pci.BASE_CLASS_MULTIMEDIA:
3319+ if subclass_id == Pci.CLASS_MULTIMEDIA_VIDEO:
3320+ return "CAPTURE"
3321+
3322+ if subclass_id == Pci.CLASS_MULTIMEDIA_AUDIO \
3323+ or subclass_id == Pci.CLASS_MULTIMEDIA_AUDIO_DEVICE:
3324+ return "AUDIO"
3325+
3326+ if class_id == Pci.BASE_CLASS_SERIAL \
3327+ and subclass_id == Pci.CLASS_SERIAL_FIREWIRE:
3328+ return "FIREWIRE"
3329+
3330+ if class_id == Pci.BASE_CLASS_BRIDGE \
3331+ and (subclass_id == Pci.CLASS_BRIDGE_PCMCIA \
3332+ or subclass_id == Pci.CLASS_BRIDGE_CARDBUS):
3333+ return "SOCKET"
3334+
3335+ if "TYPE" in self._environment and "INTERFACE" in self._environment:
3336+ interface_class, interface_subclass, interface_protocol = (
3337+ int(i) for i in self._environment["INTERFACE"].split("/"))
3338+
3339+ if interface_class == Usb.BASE_CLASS_AUDIO:
3340+ return "AUDIO"
3341+
3342+ if interface_class == Usb.BASE_CLASS_PRINTER:
3343+ return "PRINTER"
3344+
3345+ if interface_class == Usb.BASE_CLASS_STORAGE:
3346+ if interface_subclass == Usb.CLASS_STORAGE_FLOPPY:
3347+ return "FLOPPY"
3348+
3349+ if interface_subclass == Usb.CLASS_STORAGE_SCSI:
3350+ return "SCSI"
3351+
3352+ if interface_class == Usb.BASE_CLASS_VIDEO:
3353+ return "CAPTURE"
3354+
3355+ if interface_class == Usb.BASE_CLASS_WIRELESS:
3356+ if interface_protocol == Usb.PROTOCOL_BLUETOOTH:
3357+ return "BLUETOOTH"
3358+ else:
3359+ return "WIRELESS"
3360+
3361+ if "KEY" in self._environment:
3362+ key = self._environment["KEY"].strip("=")
3363+ bitmask = get_bitmask(key)
3364+
3365+ for i in range(Input.KEY_Q, Input.KEY_P + 1):
3366+ if not test_bit(i, bitmask, self._bits):
3367+ break
3368+ else:
3369+ return "KEYBOARD"
3370+
3371+ if test_bit(Input.KEY_CAMERA, bitmask, self._bits):
3372+ return "CAPTURE"
3373+
3374+ if test_bit(Input.BTN_TOUCH, bitmask, self._bits):
3375+ return "TOUCH"
3376+
3377+ if test_bit(Input.BTN_MOUSE, bitmask, self._bits):
3378+ return "MOUSE"
3379+
3380+ if "ID_TYPE" in self._environment:
3381+ id_type = self._environment["ID_TYPE"]
3382+
3383+ if id_type == "cd":
3384+ return "CDROM"
3385+
3386+ if id_type == "disk":
3387+ return "DISK"
3388+
3389+ if id_type == "video":
3390+ return "VIDEO"
3391+
3392+ if "DEVTYPE" in self._environment:
3393+ devtype = self._environment["DEVTYPE"]
3394+ if devtype == "disk":
3395+ if "ID_CDROM" in self._environment:
3396+ return "CDROM"
3397+
3398+ if "ID_DRIVE_FLOPPY" in self._environment:
3399+ return "FLOPPY"
3400+
3401+ if devtype == "scsi_device":
3402+ match = SCSI_RE.match(self._environment.get("MODALIAS", ""))
3403+ type = int(match.group("type"), 16) if match else -1
3404+
3405+ # Check FLASH drives, see /lib/udev/rules.d/80-udisks.rules
3406+ if type in (0, 7, 14) \
3407+ and not any(d.driver == "rts_pstor" for d in self._stack):
3408+ return "DISK"
3409+
3410+ if type == 1:
3411+ return "TAPE"
3412+
3413+ if type == 2:
3414+ return "PRINTER"
3415+
3416+ if type in (4, 5):
3417+ return "CDROM"
3418+
3419+ if type == 6:
3420+ return "SCANNER"
3421+
3422+ if type == 12:
3423+ return "RAID"
3424+
3425+ if "DRIVER" in self._environment:
3426+ if self._environment["DRIVER"] == "floppy":
3427+ return "FLOPPY"
3428+
3429+ if self.product:
3430+ return "OTHER"
3431+
3432+ return None
3433+
3434+ @property
3435+ def driver(self):
3436+ if "DRIVER" in self._environment:
3437+ return self._environment["DRIVER"]
3438+
3439+ # Check parent device for driver
3440+ if self._stack:
3441+ parent = self._stack[-1]
3442+ if "DRIVER" in parent._environment:
3443+ return parent._environment["DRIVER"]
3444+
3445+ return None
3446+
3447+ @property
3448+ def path(self):
3449+ devpath = self._environment.get("DEVPATH")
3450+ if self._environment.get("DEVTYPE") == "disk" and self._stack:
3451+ devpath = re.sub(r"/[^/]+/[^/]+$", "", devpath)
3452+
3453+ return devpath
3454+
3455+ @property
3456+ def product_id(self):
3457+ # pci
3458+ match = PCI_RE.match(self._environment.get("MODALIAS", ""))
3459+ if match:
3460+ return int(match.group("product_id"), 16)
3461+
3462+ # usb
3463+ match = USB_RE.match(self._environment.get("MODALIAS", ""))
3464+ if match:
3465+ return int(match.group("product_id"), 16)
3466+
3467+ # pnp
3468+ match = PNP_RE.match(self._environment.get("MODALIAS", ""))
3469+ if match:
3470+ product_id = int(match.group("product_id"), 16)
3471+ # Ignore interrupt controllers
3472+ if product_id > 0x0100:
3473+ return product_id
3474+
3475+ return None
3476+
3477+ @property
3478+ def vendor_id(self):
3479+ # pci
3480+ match = PCI_RE.match(self._environment.get("MODALIAS", ""))
3481+ if match:
3482+ return int(match.group("vendor_id"), 16)
3483+
3484+ # usb
3485+ match = USB_RE.match(self._environment.get("MODALIAS", ""))
3486+ if match:
3487+ return int(match.group("vendor_id"), 16)
3488+
3489+ return None
3490+
3491+ @property
3492+ def subproduct_id(self):
3493+ if "PCI_SUBSYS_ID" in self._environment:
3494+ pci_subsys_id = self._environment["PCI_SUBSYS_ID"]
3495+ subvendor_id, subproduct_id = pci_subsys_id.split(":")
3496+ return int(subproduct_id, 16)
3497+
3498+ return None
3499+
3500+ @property
3501+ def subvendor_id(self):
3502+ if "PCI_SUBSYS_ID" in self._environment:
3503+ pci_subsys_id = self._environment["PCI_SUBSYS_ID"]
3504+ subvendor_id, subproduct_id = pci_subsys_id.split(":")
3505+ return int(subvendor_id, 16)
3506+
3507+ return None
3508+
3509+ @property
3510+ def product(self):
3511+ for element in ("NAME",
3512+ "RFKILL_NAME",
3513+ "POWER_SUPPLY_MODEL_NAME"):
3514+ if element in self._environment:
3515+ return self._environment[element].strip('"')
3516+
3517+ # disk
3518+ if self._environment.get("DEVTYPE") == "scsi_device":
3519+ for device in reversed(self._stack):
3520+ if device._environment.get("ID_BUS") == "usb":
3521+ return decode_id(device._environment["ID_MODEL_ENC"])
3522+
3523+ if self._environment.get("DEVTYPE") == "disk" \
3524+ and self._environment.get("ID_BUS") == "ata":
3525+ return decode_id(self._environment["ID_MODEL_ENC"])
3526+
3527+ # floppy
3528+ if self.driver == "floppy":
3529+ return u"Platform Device"
3530+
3531+ return None
3532+
3533+ @property
3534+ def vendor(self):
3535+ if "RFKILL_NAME" in self._environment:
3536+ return None
3537+
3538+ if "POWER_SUPPLY_MANUFACTURER" in self._environment:
3539+ return self._environment["POWER_SUPPLY_MANUFACTURER"]
3540+
3541+ # pnp
3542+ match = PNP_RE.match(self._environment.get("MODALIAS", ""))
3543+ if match:
3544+ return match.group("vendor_name")
3545+
3546+ # disk
3547+ if self._environment.get("DEVTYPE") == "scsi_device":
3548+ for device in reversed(self._stack):
3549+ if device._environment.get("ID_BUS") == "usb":
3550+ return decode_id(device._environment["ID_VENDOR_ENC"])
3551+
3552+ return None
3553+
3554+
3555+class UdevadmParser:
3556+ """Parser for the udevadm command."""
3557+
3558+ device_factory = UdevadmDevice
3559+
3560+ def __init__(self, stream, bits=None):
3561+ self.stream = stream
3562+ self.bits = bits
3563+
3564+ def _ignoreDevice(self, device):
3565+ # Ignore devices without bus information
3566+ if not device.bus:
3567+ return True
3568+
3569+ # Ignore devices without product information
3570+ if not device.product and device.product_id is None:
3571+ return True
3572+
3573+ # Ignore invalid subsystem information
3574+ if ((device.subproduct_id is None
3575+ and device.subvendor_id is not None)
3576+ or (device.subproduct_id is not None
3577+ and device.subvendor_id is None)):
3578+ return True
3579+
3580+ # Ignore ACPI devices
3581+ if device.bus == "acpi":
3582+ return True
3583+
3584+ return False
3585+
3586+ def getAttributes(self, path):
3587+ return {}
3588+
3589+ def run(self, result):
3590+ # Some attribute lines have a space character after the
3591+ # ':', others don't have it (see udevadm-info.c).
3592+ line_pattern = re.compile(r"(?P<key>[A-Z]):\s*(?P<value>.*)")
3593+ multi_pattern = re.compile(r"(?P<key>[^=]+)=(?P<value>.*)")
3594+
3595+ stack = []
3596+ output = self.stream.read()
3597+ for record in re.split("\n{2,}", output):
3598+ record = record.strip()
3599+ if not record:
3600+ continue
3601+
3602+ # Determine path and environment
3603+ path = None
3604+ element = None
3605+ environment = {}
3606+ for line in record.split("\n"):
3607+ line_match = line_pattern.match(line)
3608+ if not line_match:
3609+ if environment:
3610+ # Append to last environment element
3611+ environment[element] += line
3612+ continue
3613+
3614+ key = line_match.group("key")
3615+ value = line_match.group("value")
3616+
3617+ if key == "P":
3618+ path = value
3619+ elif key == "E":
3620+ key_match = multi_pattern.match(value)
3621+ if not key_match:
3622+ raise Exception(
3623+ "Device property not supported: %s" % value)
3624+ element = key_match.group("key")
3625+ environment[element] = key_match.group("value")
3626+
3627+ # Update stack
3628+ while stack:
3629+ if stack[-1].path + "/" in path:
3630+ break
3631+ stack.pop()
3632+
3633+ # Set default DEVPATH
3634+ environment.setdefault("DEVPATH", path)
3635+
3636+ device = self.device_factory(environment, self.bits, list(stack))
3637+ if not self._ignoreDevice(device):
3638+ result.addDevice(device)
3639+
3640+ stack.append(device)
3641+
3642+
3643+def decode_id(id):
3644+ encoded_id = id.encode("utf-8")
3645+ decoded_id = encoded_id.decode("string-escape").decode("utf-8")
3646+ return decoded_id.strip()
3647
3648=== added file 'checkbox/parsers/utils.py'
3649--- checkbox/parsers/utils.py 1970-01-01 00:00:00 +0000
3650+++ checkbox/parsers/utils.py 2012-02-21 14:59:19 +0000
3651@@ -0,0 +1,36 @@
3652+#
3653+# This file is part of Checkbox.
3654+#
3655+# Copyright 2011 Canonical Ltd.
3656+#
3657+# Checkbox is free software: you can redistribute it and/or modify
3658+# it under the terms of the GNU General Public License as published by
3659+# the Free Software Foundation, either version 3 of the License, or
3660+# (at your option) any later version.
3661+#
3662+# Checkbox is distributed in the hope that it will be useful,
3663+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3664+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3665+# GNU General Public License for more details.
3666+#
3667+# You should have received a copy of the GNU General Public License
3668+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3669+#
3670+
3671+
3672+def implement_from_dict(class_name, methods=[], superclass=object):
3673+ """Return a class that implements empty methods.
3674+
3675+ :param superclass: The superclass of the class to be generated.
3676+ :param methods: A list of method names to implement as empty.
3677+ """
3678+ def empty(*args, **kwargs):
3679+ pass
3680+
3681+ class_dict = {}
3682+ for method in methods:
3683+ class_dict[method] = empty
3684+
3685+ if not isinstance(superclass, tuple):
3686+ superclass = (superclass,)
3687+ return type(class_name, superclass, class_dict)
3688
3689=== modified file 'checkbox/properties.py'
3690--- checkbox/properties.py 2010-02-16 21:09:28 +0000
3691+++ checkbox/properties.py 2012-02-21 14:59:19 +0000
3692@@ -20,7 +20,7 @@
3693 from checkbox.variables import (ConstantVariable, BoolVariable, StringVariable,
3694 PathVariable, UnicodeVariable, IntVariable, FloatVariable, TimeVariable,
3695 ListVariable, TupleVariable, AnyVariable, DictVariable, MapVariable,
3696- VariableFactory, Variable, get_variable)
3697+ FileVariable, VariableFactory, Variable, get_variable)
3698
3699
3700 class Property(object):
3701@@ -196,6 +196,11 @@
3702 super(Map, self).__init__(**kwargs)
3703
3704
3705+class File(PropertyType):
3706+
3707+ variable_class = FileVariable
3708+
3709+
3710 class Message(Map):
3711
3712 def __init__(self, type, schema={}, **kwargs):
3713
3714=== modified file 'checkbox/reactor.py'
3715--- checkbox/reactor.py 2010-02-16 15:18:39 +0000
3716+++ checkbox/reactor.py 2012-02-21 14:59:19 +0000
3717@@ -44,6 +44,7 @@
3718 def __init__(self):
3719 self._event_handlers = {}
3720 self._local = threading.local()
3721+ self._depth = 0
3722
3723 def call_on(self, event_type, handler, priority=0):
3724 pair = (handler, priority)
3725@@ -55,18 +56,21 @@
3726 return EventID(event_type, pair)
3727
3728 def fire(self, event_type, *args, **kwargs):
3729- logging.debug("Started firing %s.", event_type)
3730+ indent = " " * self._depth
3731+ self._depth += 1
3732+ logging.debug("%sStarted firing %s.", indent, event_type)
3733
3734 handlers = self._event_handlers.get(event_type, ())
3735 if not handlers:
3736- logging.debug("No handlers found for event type: %s", event_type)
3737+ logging.debug("%sNo handlers found for event type: %s", indent, event_type)
3738
3739 results = []
3740 handlers = sorted(handlers, key=lambda pair: pair[1])
3741 for handler, priority in handlers:
3742 try:
3743- logging.debug("Calling %s for %s with priority %d.",
3744- format_object(handler), event_type, priority)
3745+ logging.debug("%sCalling %s for %s with priority %d.",
3746+ indent, format_object(handler, *args, **kwargs),
3747+ event_type, priority)
3748 results.append(handler(*args, **kwargs))
3749 except StopException:
3750 break
3751@@ -74,17 +78,18 @@
3752 raise
3753 except KeyboardInterrupt:
3754 logging.exception("Keyboard interrupt while running event "
3755- "handler %s for event type %r with "
3756- "args %r %r.", format_object(handler),
3757- event_type, args, kwargs)
3758+ "handler %s for event type %r",
3759+ format_object(handler, *args, **kwargs),
3760+ event_type)
3761 self.stop_all()
3762 except:
3763 logging.exception("Error running event handler %s for "
3764- "event type %r with args %r %r.",
3765- format_object(handler), event_type,
3766- args, kwargs)
3767+ "event type %r",
3768+ format_object(handler, *args, **kwargs),
3769+ event_type)
3770
3771- logging.debug("Finished firing %s.", event_type)
3772+ logging.debug("%sFinished firing %s.", indent, event_type)
3773+ self._depth -= 1
3774 return results
3775
3776 def has_call(self, event_type):
3777
3778=== modified file 'checkbox/report.py'
3779--- checkbox/report.py 2009-07-22 18:02:57 +0000
3780+++ checkbox/report.py 2012-02-21 14:59:19 +0000
3781@@ -16,6 +16,9 @@
3782 # You should have received a copy of the GNU General Public License
3783 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3784 #
3785+import libxml2
3786+import posixpath
3787+
3788 from xml.dom.minidom import Document, Element, parseString
3789
3790
3791@@ -27,13 +30,13 @@
3792 handlers to understand the formats for dumping and loading actions.
3793 """
3794
3795- def __init__(self, name, version=None, stylesheet=None):
3796+ def __init__(self, name, version=None, stylesheet=None, schema=None):
3797 self.name = name
3798 self.version = version
3799 self.stylesheet = stylesheet
3800+ self.schema = schema
3801 self.dumps_table = {}
3802 self.loads_table = {}
3803- self.document = None
3804
3805 def handle_dumps(self, type, handler):
3806 """
3807@@ -83,17 +86,17 @@
3808 Dump the given object which may be a container of any objects
3809 supported by the reports added to the manager.
3810 """
3811- self.document = Document()
3812+ document = Document()
3813
3814 if self.stylesheet:
3815 type = "text/xsl"
3816- href = "file://%s" % self.stylesheet
3817- style = self.document.createProcessingInstruction("xml-stylesheet",
3818+ href = "file://%s" % posixpath.abspath(self.stylesheet)
3819+ style = document.createProcessingInstruction("xml-stylesheet",
3820 "type=\"%s\" href=\"%s\"" % (type, href))
3821- self.document.appendChild(style)
3822+ document.appendChild(style)
3823
3824- node = self.document.createElement(self.name)
3825- self.document.appendChild(node)
3826+ node = document.createElement(self.name)
3827+ document.appendChild(node)
3828
3829 if self.version:
3830 node.setAttribute("version", self.version)
3831@@ -103,8 +106,6 @@
3832 except KeyError, e:
3833 raise ValueError, "Unsupported type: %s" % e
3834
3835- document = self.document
3836- self.document = None
3837 return document
3838
3839 def loads(self, str):
3840@@ -123,6 +124,32 @@
3841
3842 return ret
3843
3844+ def validate(self, str):
3845+ """
3846+ Validate the given string
3847+ """
3848+ if not self.schema:
3849+ return False
3850+
3851+ file = open(self.schema)
3852+ try:
3853+ schema = file.read()
3854+ finally:
3855+ file.close()
3856+
3857+ rngParser = libxml2.relaxNGNewMemParserCtxt(schema, len(schema))
3858+ rngSchema = rngParser.relaxNGParse()
3859+ ctxt = rngSchema.relaxNGNewValidCtxt()
3860+ doc = libxml2.parseDoc(str)
3861+ is_valid = doc.relaxNGValidateDoc(ctxt)
3862+
3863+ # Clean up
3864+ doc.freeDoc()
3865+ del rngParser, rngSchema, ctxt
3866+ libxml2.relaxNGCleanupTypes()
3867+ libxml2.cleanupParser()
3868+ return is_valid == 0
3869+
3870
3871 class Report(object):
3872 """A convenience for writing reports.
3873@@ -133,12 +160,12 @@
3874 """
3875
3876 def _create_element(self, name, parent):
3877- element = self._manager.document.createElement(name)
3878+ element = parent.ownerDocument.createElement(name)
3879 parent.appendChild(element)
3880 return element
3881
3882 def _create_text_node(self, text, parent):
3883- text_node = self._manager.document.createTextNode(text)
3884+ text_node = parent.ownerDocument.createTextNode(text)
3885 parent.appendChild(text_node)
3886 return text_node
3887
3888
3889=== modified file 'checkbox/resource.py'
3890--- checkbox/resource.py 2010-04-11 20:05:28 +0000
3891+++ checkbox/resource.py 2012-02-21 14:59:19 +0000
3892@@ -23,10 +23,10 @@
3893
3894
3895 class ResourceObject(object):
3896- __slots__ = ("_list", "_name", "_convert",)
3897+ __slots__ = ("_iterator", "_name", "_convert",)
3898
3899- def __init__(self, list, name, convert=lambda x: x):
3900- self._list = list
3901+ def __init__(self, iterator, name, convert=lambda x: x):
3902+ self._iterator = iterator
3903 self._name = name
3904 self._convert = convert
3905
3906@@ -51,25 +51,41 @@
3907 def __ne__(self, other):
3908 return self._try(other, lambda a, b: a != b)
3909
3910+ def __contains__(self, other):
3911+ return self._try(other, lambda a, b: b in a)
3912+
3913 def _try(self, other, function, until=True, default=False):
3914 found = False
3915- for item in self._list:
3916+ for item in self._iterator:
3917 if self._name in item:
3918 value = self._convert(item[self._name])
3919 if function(value, other) == until:
3920- # Append item to list of results
3921- self._list._map._results.append(item)
3922+ # Append item to results
3923+ self._iterator._map._results.append(item)
3924 found = True
3925
3926 return until if found else default
3927
3928
3929-class ResourceList(list):
3930- __slots__ = ("_map",)
3931+class ResourceIterator(object):
3932+ __slots__ = ("_map", "_values",)
3933
3934- def __init__(self, map, *args, **kwargs):
3935- super(ResourceList, self).__init__(*args, **kwargs)
3936+ def __init__(self, map, values):
3937 self._map = map
3938+ self._values = values
3939+
3940+ def __contains__(self, elt):
3941+ found = False
3942+ for value in self._values:
3943+ if elt in value:
3944+ self._map._results.append(value)
3945+ found = True
3946+
3947+ return found
3948+
3949+ def __iter__(self):
3950+ for value in self._values:
3951+ yield value
3952
3953 def __getattr__(self, name):
3954 return ResourceObject(self, name)
3955@@ -82,7 +98,7 @@
3956 self._function = function
3957
3958 def __call__(self, object):
3959- return ResourceObject(object._list, object._name, self._function)
3960+ return ResourceObject(object._iterator, object._name, self._function)
3961
3962
3963 class ResourceGlobals(dict):
3964@@ -102,7 +118,10 @@
3965 def __getitem__(self, key):
3966 value = super(ResourceMap, self).__getitem__(key)
3967 if isinstance(value, (list, tuple)):
3968- return ResourceList(self, value)
3969+ return ResourceIterator(self, value)
3970+
3971+ elif isinstance(value, dict):
3972+ return ResourceIterator(self, [value])
3973
3974 else:
3975 return value
3976@@ -118,4 +137,4 @@
3977 except Exception:
3978 pass
3979
3980- return []
3981+ return None
3982
3983=== modified file 'checkbox/tests/report.py'
3984--- checkbox/tests/report.py 2009-02-16 15:48:24 +0000
3985+++ checkbox/tests/report.py 2012-02-21 14:59:19 +0000
3986@@ -33,7 +33,7 @@
3987 self._manager.handle_loads("default", self.loads_default)
3988
3989 def dumps_test(self, obj, parent):
3990- text_node = self._manager.document.createTextNode(obj)
3991+ text_node = parent.ownerDocument.createTextNode(obj)
3992 parent.appendChild(text_node)
3993
3994 def loads_test(self, node):
3995
3996=== modified file 'checkbox/variables.py'
3997--- checkbox/variables.py 2010-02-25 17:37:40 +0000
3998+++ checkbox/variables.py 2012-02-21 14:59:19 +0000
3999@@ -19,6 +19,8 @@
4000 import re
4001 import posixpath
4002
4003+from StringIO import StringIO
4004+
4005 from checkbox.lib.text import split
4006
4007
4008@@ -137,7 +139,7 @@
4009
4010 def coerce(self, value):
4011 if isinstance(value, str):
4012- value = unicode(value)
4013+ value = unicode(value, encoding="utf-8")
4014 elif not isinstance(value, unicode):
4015 raise ValueError("%r is not a unicode" % (value,))
4016
4017@@ -270,6 +272,18 @@
4018 return value
4019
4020
4021+class FileVariable(Variable):
4022+ __slots__ = ()
4023+
4024+ def coerce(self, value):
4025+ if isinstance(value, basestring):
4026+ value = StringIO(value)
4027+ elif not hasattr(value, "read"):
4028+ raise ValueError("%r is not a file" % (value,))
4029+
4030+ return value
4031+
4032+
4033 def get_variable(obj, attribute):
4034 return get_variables(obj)[attribute]
4035
4036
4037=== modified file 'checkbox_cli/cli_interface.py'
4038--- checkbox_cli/cli_interface.py 2010-03-07 19:57:57 +0000
4039+++ checkbox_cli/cli_interface.py 2012-02-21 14:59:19 +0000
4040@@ -145,7 +145,17 @@
4041
4042 def add_option(self, option, key=None):
4043 if key is None:
4044- key = string.lowercase[len(self.keys)]
4045+ keys = option.lower() + \
4046+ string.ascii_letters + \
4047+ string.digits + \
4048+ string.punctuation
4049+
4050+ keys = keys.replace(' ','')
4051+ keys = keys.replace('+', '')
4052+
4053+ for key in keys:
4054+ if key not in self.keys:
4055+ break
4056 self.keys.append(key)
4057 self.options.append(option)
4058
4059@@ -205,7 +215,7 @@
4060 elif isinstance(options[key], (list, tuple,)):
4061 results[key] = []
4062 else:
4063- raise Exception, "Unknown result type: %s" % type(results)
4064+ results[key] = None
4065
4066 for k in options[key]:
4067 self._toggle_results(k, options[key], results[key])
4068@@ -216,9 +226,6 @@
4069 elif key in options:
4070 results.append(key)
4071
4072- else:
4073- raise Exception, "Unknown result type: %s" % type(results)
4074-
4075 def show_progress_start(self, text):
4076 self.progress = CLIProgressDialog(text)
4077 self.progress.show()
4078@@ -240,7 +247,7 @@
4079 for option in options:
4080 dialog.add_option(option)
4081
4082- dialog.add_option("Space when finished", " ")
4083+ dialog.add_option(_("Space when finished"), " ")
4084
4085 results = default
4086 while True:
4087@@ -270,8 +277,8 @@
4088 for option in keys:
4089 dialog.add_option(option)
4090
4091- dialog.add_option("Plus to expand options", "+")
4092- dialog.add_option("Space when finished", " ")
4093+ dialog.add_option(_("Combine with character above to expand node"), "+")
4094+ dialog.add_option(_("Space when finished"), " ")
4095
4096 do_expand = False
4097 results = default
4098@@ -339,7 +346,7 @@
4099 answer = OPTION_TO_ANSWER[option]
4100 if answer == NO_ANSWER:
4101 text = _("Further information:")
4102- dialog = CLITextDialog(test.name, text)
4103+ dialog = CLITextDialog(text)
4104 test["data"] = dialog.run(_("Please type here and press"
4105 " Ctrl-D when finished:\n"))
4106 else:
4107
4108=== modified file 'debian/control'
4109--- debian/control 2010-04-17 16:14:22 +0000
4110+++ debian/control 2012-02-21 14:59:19 +0000
4111@@ -14,7 +14,7 @@
4112 Replaces: hwtest (<< 0.1-0ubuntu12)
4113 Provides: hwtest
4114 Depends: ${misc:Depends}, ${python:Depends}, debconf, udev
4115-Recommends: dpkg, lsb-release, python-apport, python-apt, python-gst0.10
4116+Recommends: dpkg, lsb-release, python-apport, python-apt, python-dateutil, python-gst0.10
4117 Suggests: checkbox-cli | checkbox-gtk
4118 Conflicts: hwtest (<< 0.1-0ubuntu12)
4119 XB-Python-Version: ${python:Versions}
4120
4121=== modified file 'examples/checkbox.ini'
4122--- examples/checkbox.ini 2010-02-25 17:37:40 +0000
4123+++ examples/checkbox.ini 2012-02-21 14:59:19 +0000
4124@@ -22,5 +22,5 @@
4125 [checkbox/plugins/jobs_info]
4126
4127 # Blacklist of jobs that are potentially dangerous
4128-blacklist = autotest ltp phoronix qa-regression-testing
4129+blacklist = autotest/full_suite ltp phoronix qa-regression-testing
4130 clone06.1 ioctl03.1 lchown02.* swapon03.1
4131
4132=== modified file 'jobs/resource.txt.in'
4133--- jobs/resource.txt.in 2012-01-27 13:28:28 +0000
4134+++ jobs/resource.txt.in 2012-02-21 14:59:19 +0000
4135@@ -35,6 +35,11 @@
4136 plugin: resource
4137 command: udev_resource
4138
4139+name: dmi
4140+plugin: resource
4141+user: root
4142+command: dmi_resource
4143+
4144 name: uname
4145 plugin: resource
4146 command: uname_resource
4147
4148=== modified file 'plugins/apport_prompt.py'
4149--- plugins/apport_prompt.py 2010-03-08 15:29:47 +0000
4150+++ plugins/apport_prompt.py 2012-02-21 14:59:19 +0000
4151@@ -39,10 +39,10 @@
4152
4153
4154 CATEGORY_TO_PACKAGE = {
4155- "VIDEO": "xorg",
4156 "SOUND": "alsa-base"}
4157
4158 CATEGORY_TO_SYMPTOM = {
4159+ "VIDEO": "display",
4160 "DISK": "storage"}
4161
4162
4163@@ -55,6 +55,8 @@
4164 self.symptom = symptom
4165 self.pid = None
4166 self.save = False
4167+ self.tag = '' #Additional tags to add to reports filed
4168+ #through this tool
4169
4170
4171 class ApportUserInterface(UserInterface):
4172@@ -135,6 +137,11 @@
4173
4174 return [options.index(r) for r in results]
4175
4176+ def ui_question_yesno(self, text):
4177+ self.interface.show_progress_stop()
4178+ result = self.interface.show_radio(text, ["Yes", "No"])
4179+ return result == "Yes"
4180+
4181 def open_url(self, url):
4182 self.interface.show_url(url)
4183
4184@@ -192,13 +199,13 @@
4185
4186 # Give lowest priority to required packages
4187 for resource in test.get("resources", []):
4188- if resource["suite"] == "package":
4189+ if "version" in resource:
4190 package = resource["name"]
4191 break
4192
4193 # Give highest priority to required devices
4194 for resource in test.get("resources", []):
4195- if resource["suite"] == "device":
4196+ if "bus" in resource:
4197 category = resource["category"]
4198 if category in CATEGORY_TO_PACKAGE:
4199 package = CATEGORY_TO_PACKAGE[category]
4200@@ -216,14 +223,22 @@
4201 if not package and not symptom:
4202 return
4203
4204- response = interface.show_info(_("Do you want to report a bug?"),
4205+ if test.get("suite"):
4206+ failed_test_message = _("Test %(name)s from suite %(suite)s failed.") % {
4207+ 'name' : test["name"],
4208+ 'suite:' : test["suite"]}
4209+ else:
4210+ failed_test_message = _("Test %s failed.") % test["name"]
4211+ failed_test_message += "\n" + _("Do you want to report a bug?")
4212+
4213+ response = interface.show_info(failed_test_message,
4214 ["yes", "no"], "no")
4215 if response == "no":
4216 return
4217
4218 # Determine corresponding device
4219 for resource in test.get("resources", []):
4220- if resource["suite"] == "device":
4221+ if "bus" in resource:
4222 device = resource["category"].lower()
4223 break
4224
4225
4226=== modified file 'plugins/backend_info.py'
4227--- plugins/backend_info.py 2010-04-20 16:23:16 +0000
4228+++ plugins/backend_info.py 2012-02-21 14:59:19 +0000
4229@@ -18,6 +18,7 @@
4230 #
4231 import os
4232 import shutil
4233+import logging
4234
4235 from subprocess import call, PIPE
4236 from tempfile import mkdtemp
4237@@ -25,11 +26,15 @@
4238 from checkbox.lib.fifo import FifoReader, FifoWriter, create_fifo
4239
4240 from checkbox.plugin import Plugin
4241-from checkbox.properties import Path
4242-
4243+from checkbox.properties import Path, Float
4244+from checkbox.job import FAIL
4245
4246 class BackendInfo(Plugin):
4247
4248+ # how long to wait for I/O from/to the backend before the call returns.
4249+ # How we behave if I/O times out is dependent on the situation.
4250+ timeout = Float(default=60.0)
4251+
4252 command = Path(default="%(checkbox_share)s/backend")
4253
4254 def register(self, manager):
4255@@ -57,46 +62,92 @@
4256 stdout=PIPE, stderr=PIPE) == 0 and \
4257 call(["pgrep", "-x", "-u", str(uid), "ksmserver"],
4258 stdout=PIPE, stderr=PIPE) == 0:
4259- prefix = ["kdesudo", "--"]
4260+ prefix = ["kdesudo", "--desktop", "/usr/share/applications/checkbox-gtk.desktop", "--"]
4261 elif os.getenv("DISPLAY") and \
4262 call(["which", "gksu"],
4263 stdout=PIPE, stderr=PIPE) == 0 and \
4264 call(["pgrep", "-x", "-u", str(uid), "gnome-panel|gconfd-2"],
4265 stdout=PIPE, stderr=PIPE) == 0:
4266- prefix = ["gksu", "--"]
4267+ prefix = ["gksu", "--description", "/usr/share/applications/checkbox-gtk.desktop", "--"]
4268 else:
4269 prefix = ["sudo"]
4270
4271 return prefix + self.get_command(*args)
4272
4273+ def spawn_backend(self, input_fifo, output_fifo):
4274+ self.pid = os.fork()
4275+ if self.pid == 0:
4276+ root_command = self.get_root_command(input_fifo, output_fifo)
4277+ os.execvp(root_command[0], root_command)
4278+ # Should never get here
4279+
4280+ def ping_backend(self):
4281+ if not self.parent_reader or not self.parent_writer:
4282+ return False
4283+ self.parent_writer.write_object("ping")
4284+ result = self.parent_reader.read_object()
4285+ return result == "pong"
4286+
4287+
4288 def gather(self):
4289 self.directory = mkdtemp(prefix="checkbox")
4290 child_input = create_fifo(os.path.join(self.directory, "input"), 0600)
4291 child_output = create_fifo(os.path.join(self.directory, "output"), 0600)
4292
4293- self.pid = os.fork()
4294- if self.pid > 0:
4295- self.parent_writer = FifoWriter(child_input)
4296- self.parent_reader = FifoReader(child_output)
4297+ self.backend_is_alive = False
4298+ for attempt in range(1,4):
4299+ self.spawn_backend(child_input, child_output)
4300+ #Only returns if I'm still the parent, so I can do parent stuff here
4301+ self.parent_writer = FifoWriter(child_input, timeout=self.timeout)
4302+ self.parent_reader = FifoReader(child_output, timeout=self.timeout)
4303+ if self.ping_backend():
4304+ logging.debug("Backend responded, continuing execution.")
4305+ self.backend_is_alive = True
4306+ break
4307+ else:
4308+ logging.debug("Backend didn't respond, trying to create again.")
4309
4310- else:
4311- root_command = self.get_root_command(child_input, child_output)
4312- os.execvp(root_command[0], root_command)
4313- # Should never get here
4314+ if not self.backend_is_alive:
4315+ logging.warning("Privileged backend not responding. " +
4316+ "jobs specifying user will not be run")
4317
4318 def message_exec(self, message):
4319 if "user" in message:
4320- self.parent_writer.write_object(message)
4321- result = self.parent_reader.read_object()
4322+ if "environ" in message:
4323+ #Prepare variables to be "exported" from my environment
4324+ #to the backend's.
4325+ backend_environ=["%s=%s" % (key, os.environ[key])
4326+ for key in message["environ"]
4327+ if key in os.environ]
4328+ message=dict(message) #so as to not wreck the
4329+ #original message
4330+ message["environ"]=backend_environ
4331+
4332+ if (self.backend_is_alive and not self.ping_backend()):
4333+ self.backend_is_alive = False
4334+
4335+ if self.backend_is_alive:
4336+ self.parent_writer.write_object(message)
4337+ while True:
4338+ result = self.parent_reader.read_object()
4339+ if result:
4340+ break
4341+ else:
4342+ logging.info("Waiting for result...")
4343+ else:
4344+ result = (FAIL, "Unable to test. Privileges are " +
4345+ "required for this job.", 0,)
4346 if result:
4347 self._manager.reactor.fire("message-result", *result)
4348
4349 def stop(self):
4350+ self.parent_writer.write_object("stop")
4351 self.parent_writer.close()
4352 self.parent_reader.close()
4353 shutil.rmtree(self.directory)
4354
4355- os.waitpid(self.pid, 0)
4356+ if self.backend_is_alive:
4357+ os.waitpid(self.pid, 0)
4358
4359
4360 factory = BackendInfo
4361
4362=== modified file 'plugins/jobs_info.py'
4363--- plugins/jobs_info.py 2010-02-23 01:50:41 +0000
4364+++ plugins/jobs_info.py 2012-02-21 14:59:19 +0000
4365@@ -16,7 +16,7 @@
4366 # You should have received a copy of the GNU General Public License
4367 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4368 #
4369-import re
4370+import os, sys, re
4371 import gettext
4372 import logging
4373
4374@@ -32,6 +32,10 @@
4375 "status": String(required=False),
4376 "suite": String(required=False),
4377 "description": Unicode(required=False),
4378+ "purpose": Unicode(required=False),
4379+ "steps": Unicode(required=False),
4380+ "info": Unicode(required=False),
4381+ "verification": Unicode(required=False),
4382 "command": String(required=False),
4383 "depends": List(String(), required=False),
4384 "duration": Float(required=False),
4385@@ -50,16 +54,16 @@
4386
4387 # Space separated list of directories where job files are stored.
4388 directories = List(Path(),
4389- default_factory=lambda:"%(checkbox_share)s/jobs")
4390+ default_factory=lambda: "%(checkbox_share)s/jobs")
4391
4392 # List of jobs to blacklist
4393- blacklist = List(String(), default_factory=lambda:"")
4394+ blacklist = List(String(), default_factory=lambda: "")
4395
4396 # Path to blacklist file
4397 blacklist_file = Path(required=False)
4398
4399 # List of jobs to whitelist
4400- whitelist = List(String(), default_factory=lambda:"")
4401+ whitelist = List(String(), default_factory=lambda: "")
4402
4403 # Path to whitelist file
4404 whitelist_file = Path(required=False)
4405@@ -78,17 +82,28 @@
4406 try:
4407 file = open(filename)
4408 except IOError, e:
4409- logging.info("Failed to open file '%s': %s",
4410- filename, e.strerror)
4411+ error_message=(gettext.gettext("Failed to open file '%s': %s") %
4412+ (filename, e.strerror))
4413+ logging.critical(error_message)
4414+ sys.stderr.write("%s\n" % error_message)
4415+ sys.exit(os.EX_NOINPUT)
4416 else:
4417 strings.extend([l.strip() for l in file.readlines()])
4418
4419- return [re.compile(r"^%s$" % s) for s in strings if s]
4420+ return [re.compile(r"^%s$" % s) for s in strings
4421+ if s and not s.startswith("#")]
4422
4423 def gather(self):
4424 # Register temporary handler for report-message events
4425+ messages = []
4426+
4427 def report_message(message):
4428- self._manager.reactor.fire("report-job", message)
4429+ if self.whitelist_patterns:
4430+ name = message["name"]
4431+ if not [name for p in self.whitelist_patterns if p.match(name)]:
4432+ return
4433+
4434+ messages.append(message)
4435
4436 # Set domain and message event handler
4437 old_domain = gettext.textdomain()
4438@@ -98,6 +113,23 @@
4439 for directory in self.directories:
4440 self._manager.reactor.fire("message-directory", directory)
4441
4442+ # Apply whitelist ordering
4443+ def cmp_function(a, b):
4444+ a_name = a["name"]
4445+ b_name = b["name"]
4446+ for pattern in self.whitelist_patterns:
4447+ if pattern.match(a_name):
4448+ a_index = self.whitelist_patterns.index(pattern)
4449+ elif pattern.match(b_name):
4450+ b_index = self.whitelist_patterns.index(pattern)
4451+
4452+ return cmp(a_index, b_index)
4453+
4454+ if self.whitelist_patterns:
4455+ messages = sorted(messages, cmp=cmp_function)
4456+ for message in messages:
4457+ self._manager.reactor.fire("report-job", message)
4458+
4459 # Unset domain and event handler
4460 self._manager.reactor.cancel_call(event_id)
4461 gettext.textdomain(old_domain)
4462
4463=== modified file 'plugins/jobs_prompt.py'
4464--- plugins/jobs_prompt.py 2010-02-22 19:57:55 +0000
4465+++ plugins/jobs_prompt.py 2012-02-21 14:59:19 +0000
4466@@ -16,7 +16,9 @@
4467 # You should have received a copy of the GNU General Public License
4468 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4469 #
4470-from checkbox.job import JobStore, UNINITIATED, UNTESTED
4471+from checkbox.contrib.persist import Persist, MemoryBackend
4472+
4473+from checkbox.job import JobStore, PASS, UNINITIATED, UNTESTED
4474 from checkbox.properties import Int, Path
4475 from checkbox.plugin import Plugin
4476 from checkbox.user_interface import NEXT, PREV
4477@@ -30,13 +32,31 @@
4478 # Maximum number of messages per directory
4479 store_directory_size = Int(default=1000)
4480
4481+ @property
4482+ def persist(self):
4483+ if self._persist is None:
4484+ self._persist = Persist(backend=MemoryBackend())
4485+
4486+ return self._persist.root_at("jobs_prompt")
4487+
4488+ @property
4489+ def store(self):
4490+ if self._store is None:
4491+ self._store = JobStore(self.persist, self.store_directory,
4492+ self.store_directory_size)
4493+
4494+ return self._store
4495+
4496 def register(self, manager):
4497 super(JobsPrompt, self).register(manager)
4498
4499 self._ignore = []
4500+ self._persist = None
4501+ self._store = None
4502
4503 for (rt, rh) in [
4504 ("begin-persist", self.begin_persist),
4505+ ("begin-recover", self.begin_recover),
4506 ("ignore-jobs", self.ignore_jobs),
4507 ("prompt-job", self.prompt_job),
4508 ("prompt-jobs", self.prompt_jobs),
4509@@ -46,9 +66,11 @@
4510 self._manager.reactor.call_on(rt, rh)
4511
4512 def begin_persist(self, persist):
4513- persist = persist.root_at("jobs_prompt")
4514- self.store = JobStore(persist, self.store_directory,
4515- self.store_directory_size)
4516+ self._persist = persist
4517+
4518+ def begin_recover(self, recover):
4519+ if not recover:
4520+ self.store.delete_all_messages()
4521
4522 def ignore_jobs(self, jobs):
4523 self._ignore = jobs
4524@@ -65,6 +87,19 @@
4525 if job[attribute] in self._ignore:
4526 job["status"] = UNTESTED
4527 else:
4528+ if "depends" in job:
4529+ offset = self.store.get_pending_offset()
4530+ self.store.set_pending_offset(0)
4531+ messages = self.store.get_pending_messages()
4532+ self.store.set_pending_offset(offset)
4533+
4534+ # Skip if any message in the depends doesn't pass
4535+ depends = job["depends"]
4536+ for message in messages:
4537+ if message["name"] in depends \
4538+ and message["status"] != PASS:
4539+ return
4540+
4541 self._manager.reactor.fire("prompt-%s" % job["plugin"], interface, job)
4542
4543 def prompt_jobs(self, interface):
4544@@ -96,6 +131,9 @@
4545 tests = [m for m in messages if m.get("type") in ("test", "metric")]
4546 self._manager.reactor.fire("report-tests", tests)
4547
4548+ suites = [m for m in messages if m.get("type") == "suite"]
4549+ self._manager.reactor.fire("report-suites", suites)
4550+
4551 attachments = [m for m in messages if m.get("type") == "attachment" and "data" in m]
4552 self._manager.reactor.fire("report-attachments", attachments)
4553
4554
4555=== modified file 'plugins/lock_prompt.py'
4556--- plugins/lock_prompt.py 2010-02-22 17:01:03 +0000
4557+++ plugins/lock_prompt.py 2012-02-21 14:59:19 +0000
4558@@ -17,7 +17,9 @@
4559 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4560 #
4561 import os
4562+import fcntl
4563 import posixpath
4564+import signal
4565
4566 from time import time
4567
4568@@ -51,6 +53,7 @@
4569 directory = posixpath.dirname(self.filename)
4570 safe_make_directory(directory)
4571
4572+ # Try to lock the process
4573 self._lock = GlobalLock(self.filename)
4574 try:
4575 self._lock.acquire()
4576@@ -60,5 +63,16 @@
4577 _("There is another checkbox running. Please close it first."))
4578 self._manager.reactor.stop_all()
4579
4580+ # Stop the process if the lock is deleted
4581+ def handler(signum, frame):
4582+ if not posixpath.exists(self.filename):
4583+ self._manager.reactor.stop_all()
4584+
4585+ signal.signal(signal.SIGIO, handler)
4586+ fd = os.open(directory, os.O_RDONLY)
4587+
4588+ fcntl.fcntl(fd, fcntl.F_SETSIG, 0)
4589+ fcntl.fcntl(fd, fcntl.F_NOTIFY, fcntl.DN_DELETE|fcntl.DN_MULTISHOT)
4590+
4591
4592 factory = LockPrompt
4593
4594=== modified file 'plugins/manual_test.py'
4595--- plugins/manual_test.py 2010-03-07 19:57:57 +0000
4596+++ plugins/manual_test.py 2012-02-21 14:59:19 +0000
4597@@ -34,7 +34,9 @@
4598 # Avoid modifying the content of test in place
4599 temp = dict(test)
4600 self._manager.reactor.fire("prompt-shell", interface, temp)
4601- return (temp["status"], temp["data"], temp["duration"])
4602+ return (temp["status"],
4603+ temp.get("data", ""),
4604+ temp.get("duration", 0))
4605
4606 interface.show_test(test, runner)
4607 self._manager.reactor.fire("prompt-test", interface, test)
4608
4609=== modified file 'plugins/message_info.py'
4610--- plugins/message_info.py 2010-02-25 17:37:40 +0000
4611+++ plugins/message_info.py 2012-02-21 14:59:19 +0000
4612@@ -16,17 +16,34 @@
4613 # You should have received a copy of the GNU General Public License
4614 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4615 #
4616+import os
4617 import re
4618 import posixpath
4619+import signal
4620+
4621 from StringIO import StringIO
4622
4623 from checkbox.lib.path import path_expand_recursive
4624 from checkbox.lib.template_i18n import TemplateI18n
4625
4626+from checkbox.parsers.description import DescriptionParser
4627+
4628 from checkbox.job import Job, PASS
4629 from checkbox.plugin import Plugin
4630
4631
4632+class DescriptionResult:
4633+
4634+ def __init__(self, message):
4635+ self.message = message
4636+
4637+ def setDescription(self, purpose, steps, verification, info):
4638+ self.message["purpose"] = purpose
4639+ self.message["steps"] = steps
4640+ self.message["verification"] = verification
4641+ self.message["info"] = info
4642+
4643+
4644 class MessageInfo(Plugin):
4645
4646 def register(self, manager):
4647@@ -49,7 +66,6 @@
4648 whitelist_patterns = [re.compile(r"^%s$" % r) for r in whitelist if r]
4649 blacklist_patterns = [re.compile(r"^%s$" % r) for r in blacklist if r]
4650
4651- filenames = []
4652 for filename in path_expand_recursive(directory):
4653 name = posixpath.basename(filename)
4654 if name.startswith(".") or name.endswith("~"):
4655@@ -66,9 +82,18 @@
4656
4657 def message_exec(self, message):
4658 if "user" not in message:
4659+
4660+ def stop():
4661+ os.kill(0, signal.SIGTERM)
4662+
4663 job = Job(message["command"], message.get("environ"),
4664 message.get("timeout"))
4665+
4666+ # Kill the job if the stop event is fired during execution
4667+ event_id = self._manager.reactor.call_on("stop", stop)
4668 result = job.execute()
4669+ self._manager.reactor.cancel_call(event_id)
4670+
4671 self._manager.reactor.fire("message-result", *result)
4672
4673 def message_file(self, file, filename="<stream>"):
4674@@ -80,6 +105,10 @@
4675 if long_key.endswith(long_ext):
4676 short_key = long_key.replace(long_ext, "")
4677 message[short_key] = message.pop(long_key)
4678+ if "description" in message:
4679+ parser = DescriptionParser(StringIO(message["description"]))
4680+ result = DescriptionResult(message)
4681+ parser.run(result)
4682
4683 if messages:
4684 self._manager.reactor.fire("report-messages", messages)
4685
4686=== modified file 'plugins/persist_info.py'
4687--- plugins/persist_info.py 2010-04-09 12:56:55 +0000
4688+++ plugins/persist_info.py 2012-02-21 14:59:19 +0000
4689@@ -34,15 +34,18 @@
4690
4691 for (rt, rh) in [
4692 ("begin", self.begin),
4693- ("prompt-job", self.save)]:
4694+ ("prompt-begin", self.begin),
4695+ ("prompt-job", self.save),
4696+ ("report-job", self.save)]:
4697 self._manager.reactor.call_on(rt, rh, -100)
4698
4699 # Save persist data last
4700 self._manager.reactor.call_on("stop", self.save, 1000)
4701
4702- def begin(self):
4703- self.persist = Persist(self.filename)
4704- self._manager.reactor.fire("begin-persist", self.persist)
4705+ def begin(self, interface=None):
4706+ if self.persist is None:
4707+ self.persist = Persist(self.filename)
4708+ self._manager.reactor.fire("begin-persist", self.persist)
4709
4710 def save(self, *args):
4711 # Flush data to disk
4712
4713=== modified file 'plugins/remote_suite.py'
4714--- plugins/remote_suite.py 2010-02-22 15:16:05 +0000
4715+++ plugins/remote_suite.py 2012-02-21 14:59:19 +0000
4716@@ -35,6 +35,7 @@
4717 # Register temporary handler for report-message events
4718 def report_message(message):
4719 message["suite"] = suite["name"]
4720+ self._manager.reactor.fire("report-job", message)
4721
4722 event_id = self._manager.reactor.call_on("report-message", report_message)
4723 self._manager.reactor.fire("message-exec", suite)
4724
4725=== modified file 'plugins/report_prompt.py'
4726--- plugins/report_prompt.py 2010-02-22 15:16:05 +0000
4727+++ plugins/report_prompt.py 2012-02-21 14:59:19 +0000
4728@@ -30,9 +30,16 @@
4729 self._manager.reactor.call_on("prompt-report", self.prompt_report)
4730
4731 def prompt_report(self, interface):
4732+ def report_error(e):
4733+ self._manager.reactor.fire("prompt-error", interface, e)
4734+
4735+ event_id = self._manager.reactor.call_on("report-error", report_error)
4736+
4737 if interface.direction != PREV:
4738 interface.show_progress(_("Building report..."),
4739 self._manager.reactor.fire, "report")
4740
4741+ self._manager.reactor.cancel_call(event_id)
4742+
4743
4744 factory = ReportPrompt
4745
4746=== modified file 'plugins/resource_info.py'
4747--- plugins/resource_info.py 2010-03-04 03:41:05 +0000
4748+++ plugins/resource_info.py 2012-02-21 14:59:19 +0000
4749@@ -34,17 +34,31 @@
4750 def prompt_job(self, interface, job):
4751 mask = []
4752 values = []
4753+ failed_requirements = []
4754+
4755 for require in job.get("requires", []):
4756 new_values = self.resources.eval(require)
4757 mask.append(bool(new_values))
4758- values.extend(new_values)
4759+
4760+ if not bool(new_values):
4761+ failed_requirements.append(require)
4762+
4763+ if new_values is not None:
4764+ values.extend(new_values)
4765
4766 if all(mask):
4767 job["resources"] = values
4768
4769 else:
4770 job["status"] = UNSUPPORTED
4771- job["data"] = "Job requirements not met."
4772+
4773+ data = "Job requirement%s not met:" % (
4774+ 's' if len(failed_requirements) > 1 else '')
4775+
4776+ for failed_require in failed_requirements:
4777+ data = data + " '" + failed_require + "'"
4778+
4779+ job["data"] = data
4780 self._manager.reactor.stop()
4781
4782 def report_resource(self, resource):
4783
4784=== modified file 'plugins/scripts_info.py'
4785--- plugins/scripts_info.py 2010-02-16 15:18:39 +0000
4786+++ plugins/scripts_info.py 2012-02-21 14:59:19 +0000
4787@@ -16,7 +16,7 @@
4788 # You should have received a copy of the GNU General Public License
4789 # along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4790 #
4791-from checkbox.lib.environ import prepend_path
4792+from checkbox.lib.environ import append_path, prepend_path
4793
4794 from checkbox.plugin import Plugin
4795 from checkbox.properties import Path
4796@@ -34,6 +34,8 @@
4797
4798 def gather(self):
4799 prepend_path(self.scripts_path)
4800+ append_path("/sbin")
4801+ append_path("/usr/sbin")
4802
4803
4804 factory = ScriptsInfo
4805
4806=== modified file 'plugins/shell_test.py'
4807--- plugins/shell_test.py 2010-03-05 19:31:13 +0000
4808+++ plugins/shell_test.py 2012-02-21 14:59:19 +0000
4809@@ -35,7 +35,8 @@
4810 def prompt_shell(self, interface, test):
4811 command = test.get("command")
4812 status = test.get("status", UNINITIATED)
4813- if command and status == UNINITIATED:
4814+ plugin = test.get("plugin", "shell")
4815+ if command and (status == UNINITIATED or plugin != "shell"):
4816 # Register temporary handler for message-result events
4817 def message_result(status, data, duration):
4818 test["status"] = status
4819@@ -48,7 +49,7 @@
4820 event_id = self._manager.reactor.call_on("message-result", message_result, -100)
4821
4822 interface.show_progress(
4823- _("Running %s..." % test["name"]), self._manager.reactor.fire,
4824+ _("Running %s...") % test["name"], self._manager.reactor.fire,
4825 "message-exec", test)
4826
4827 self._manager.reactor.cancel_call(event_id)
4828
4829=== modified file 'plugins/suites_prompt.py'
4830--- plugins/suites_prompt.py 2010-02-22 17:01:03 +0000
4831+++ plugins/suites_prompt.py 2012-02-21 14:59:19 +0000
4832@@ -18,6 +18,10 @@
4833 #
4834 import copy
4835
4836+from checkbox.contrib.persist import Persist, MemoryBackend
4837+
4838+from checkbox.job import UNINITIATED
4839+
4840 from checkbox.lib.resolver import Resolver
4841
4842 from checkbox.plugin import Plugin
4843@@ -28,12 +32,21 @@
4844
4845 class SuitesPrompt(Plugin):
4846
4847+ @property
4848+ def persist(self):
4849+ if self._persist is None:
4850+ self._persist = Persist(backend=MemoryBackend())
4851+
4852+ return self._persist.root_at("suites_prompt")
4853+
4854 def register(self, manager):
4855 super(SuitesPrompt, self).register(manager)
4856
4857 self._depends = {}
4858 self._jobs = {}
4859- self._recover = False
4860+ self._statuses = {}
4861+ self._persist = None
4862+ self._recover = False
4863
4864 for (rt, rh) in [
4865 ("begin-persist", self.begin_persist),
4866@@ -48,10 +61,13 @@
4867 self._manager.reactor.call_on(rt, rh, 100)
4868
4869 def begin_persist(self, persist):
4870- self.persist = persist.root_at("suites_prompt")
4871-
4872- def begin_recover(self):
4873- self._recover = True
4874+ self._persist = persist
4875+
4876+ def begin_recover(self, recover):
4877+ self._recover = recover
4878+
4879+ if not self._recover:
4880+ self.persist.remove("default")
4881
4882 def report_suite(self, suite):
4883 suite.setdefault("type", "suite")
4884@@ -66,6 +82,8 @@
4885 self._jobs[job["name"]] = job[attribute]
4886 if "suite" in job:
4887 self._depends[job["name"]] = [job["suite"]]
4888+ if job.get("type") == "test":
4889+ self._statuses[job["name"]] = job["status"]
4890
4891 def prompt_gather(self, interface):
4892 # Resolve dependencies
4893@@ -80,31 +98,28 @@
4894 suboptions = options
4895 dependencies = resolver.get_dependencies(job)
4896 for dependency in dependencies:
4897- suboptions = suboptions.setdefault(self._jobs[dependency], {})
4898+ value = self._statuses.get(dependency, {})
4899+ suboptions = suboptions.setdefault(self._jobs[dependency], value)
4900
4901 # Build defaults
4902 defaults = self.persist.get("default")
4903 if defaults is None:
4904 defaults = copy.deepcopy(options)
4905
4906- # Only prompt if not recovering
4907- if interface.direction == PREV or not self._recover:
4908- self._recover = False
4909-
4910- # Get results
4911- defaults = interface.show_tree(_("Select the suites to test"),
4912- options, defaults)
4913- self.persist.set("default", defaults)
4914+ # Get results
4915+ defaults = interface.show_tree(_("Select the suites to test"),
4916+ options, defaults)
4917+ self.persist.set("default", defaults)
4918
4919 # Get tests to ignore
4920 def get_ignore_jobs(options, results):
4921 jobs = []
4922- for k, v in options.iteritems():
4923- if not v and k not in results:
4924- jobs.append(k)
4925-
4926- else:
4927- jobs.extend(get_ignore_jobs(options[k], results.get(k, {})))
4928+ if isinstance(options,dict):
4929+ for k, v in options.iteritems():
4930+ if v == UNINITIATED and k not in results:
4931+ jobs.append(k)
4932+ else:
4933+ jobs.extend(get_ignore_jobs(options[k], results.get(k, {})))
4934
4935 return jobs
4936
4937
4938=== modified file 'plugins/system_info.py'
4939--- plugins/system_info.py 2010-03-08 15:29:47 +0000
4940+++ plugins/system_info.py 2012-02-21 14:59:19 +0000
4941@@ -33,11 +33,12 @@
4942 def register(self, manager):
4943 super(SystemInfo, self).register(manager)
4944
4945+ self.persist = None
4946 self.resource = None
4947
4948 for (rt, rh) in [
4949 ("begin-persist", self.begin_persist),
4950- ("report-device", self.report_device)]:
4951+ ("report-dmi", self.report_dmi)]:
4952 self._manager.reactor.call_on(rt, rh)
4953
4954 # System report should be generated early.
4955@@ -46,14 +47,20 @@
4956 def begin_persist(self, persist):
4957 self.persist = persist.root_at("system_info")
4958
4959- def report_device(self, resources):
4960+ def report_dmi(self, resources):
4961 for resource in resources:
4962 if resource.get("category") == "CHASSIS":
4963 self.resource = resource
4964
4965 # TODO: report this upon gathering
4966 def report(self):
4967- system_id = self.system_id or self.persist.get("system_id")
4968+ if self.system_id:
4969+ system_id = self.system_id
4970+ elif self.persist and self.persist.has("system_id"):
4971+ system_id = self.persist.get("system_id")
4972+ else:
4973+ system_id = None
4974+
4975 if not system_id:
4976 resource = self.resource
4977 if resource is None or "product" not in resource:
4978@@ -71,7 +78,8 @@
4979 fingerprint.update(str(field))
4980
4981 system_id = fingerprint.hexdigest()
4982- self.persist.set("system_id", system_id)
4983+ if self.persist:
4984+ self.persist.set("system_id", system_id)
4985
4986 message = system_id
4987 logging.info("System ID: %s", message)
4988
4989=== modified file 'plugins/user_interface.py'
4990--- plugins/user_interface.py 2010-02-16 15:18:39 +0000
4991+++ plugins/user_interface.py 2012-02-21 14:59:19 +0000
4992@@ -21,6 +21,8 @@
4993 from checkbox.user_interface import PREV
4994
4995 import gettext
4996+import posixpath
4997+
4998 from gettext import gettext as _
4999
5000
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches