Merge lp:~jeffmarcom/opencompute/checkbox-ocp_update-plainbox into lp:opencompute/checkbox

Proposed by Jeff Marcom
Status: Merged
Approved by: Jeff Marcom
Approved revision: 2148
Merged at revision: 2147
Proposed branch: lp:~jeffmarcom/opencompute/checkbox-ocp_update-plainbox
Merge into: lp:opencompute/checkbox
Diff against target: 27983 lines (+24980/-1105)
97 files modified
debian/changelog (+9/-0)
plainbox/MANIFEST.in (+3/-2)
plainbox/contrib/com.canonical.certification.PlainBox1.service (+3/-0)
plainbox/contrib/dbus-mini-client.py (+430/-0)
plainbox/docs/author/checkbox-job-format.rst (+168/-0)
plainbox/docs/author/index.rst (+3/-0)
plainbox/docs/dev/reference.rst (+42/-5)
plainbox/docs/dev/resources.rst (+2/-0)
plainbox/plainbox/abc.py (+115/-6)
plainbox/plainbox/data/report/checkbox.js (+16/-0)
plainbox/plainbox/data/report/styles.css (+258/-0)
plainbox/plainbox/impl/applogic.py (+104/-6)
plainbox/plainbox/impl/box.py (+10/-241)
plainbox/plainbox/impl/commands/__init__.py (+322/-1)
plainbox/plainbox/impl/commands/analyze.py (+7/-7)
plainbox/plainbox/impl/commands/checkbox.py (+20/-17)
plainbox/plainbox/impl/commands/dev.py (+5/-5)
plainbox/plainbox/impl/commands/run.py (+113/-39)
plainbox/plainbox/impl/commands/script.py (+13/-7)
plainbox/plainbox/impl/commands/service.py (+132/-0)
plainbox/plainbox/impl/commands/special.py (+5/-5)
plainbox/plainbox/impl/commands/sru.py (+33/-13)
plainbox/plainbox/impl/commands/test_dev.py (+4/-4)
plainbox/plainbox/impl/commands/test_run.py (+21/-2)
plainbox/plainbox/impl/commands/test_script.py (+23/-17)
plainbox/plainbox/impl/commands/test_sru.py (+14/-1)
plainbox/plainbox/impl/config.py (+13/-0)
plainbox/plainbox/impl/dbus/__init__.py (+47/-0)
plainbox/plainbox/impl/dbus/decorators.py (+351/-0)
plainbox/plainbox/impl/dbus/service.py (+662/-0)
plainbox/plainbox/impl/exporter/__init__.py (+15/-10)
plainbox/plainbox/impl/exporter/html.py (+145/-0)
plainbox/plainbox/impl/exporter/test_html.py (+142/-0)
plainbox/plainbox/impl/exporter/test_init.py (+11/-16)
plainbox/plainbox/impl/exporter/xlsx.py (+570/-0)
plainbox/plainbox/impl/exporter/xml.py (+15/-13)
plainbox/plainbox/impl/highlevel.py (+117/-0)
plainbox/plainbox/impl/integration_tests.py (+117/-44)
plainbox/plainbox/impl/job.py (+53/-52)
plainbox/plainbox/impl/logging.py (+277/-159)
plainbox/plainbox/impl/providers/__init__.py (+58/-0)
plainbox/plainbox/impl/providers/checkbox.py (+117/-0)
plainbox/plainbox/impl/providers/special.py (+69/-0)
plainbox/plainbox/impl/providers/stubbox/__init__.py (+43/-0)
plainbox/plainbox/impl/providers/stubbox/data/whitelists/stub.whitelist (+12/-0)
plainbox/plainbox/impl/providers/stubbox/data/whitelists/stub1.whitelist (+7/-0)
plainbox/plainbox/impl/providers/stubbox/data/whitelists/stub2.whitelist (+7/-0)
plainbox/plainbox/impl/providers/stubbox/jobs/local.txt.in (+5/-0)
plainbox/plainbox/impl/providers/stubbox/jobs/multilevel.txt.in (+20/-0)
plainbox/plainbox/impl/providers/stubbox/jobs/stub.txt.in (+80/-0)
plainbox/plainbox/impl/providers/stubbox/scripts/stub_package_list (+3/-0)
plainbox/plainbox/impl/providers/test_checkbox.py (+40/-0)
plainbox/plainbox/impl/providers/test_special.py (+104/-0)
plainbox/plainbox/impl/providers/v1.py (+229/-0)
plainbox/plainbox/impl/result.py (+198/-93)
plainbox/plainbox/impl/rfc822.py (+15/-4)
plainbox/plainbox/impl/runner.py (+125/-101)
plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py (+37/-24)
plainbox/plainbox/impl/service.py (+1058/-0)
plainbox/plainbox/impl/session/__init__.py (+95/-0)
plainbox/plainbox/impl/session/jobs.py (+290/-0)
plainbox/plainbox/impl/session/legacy.py (+271/-0)
plainbox/plainbox/impl/session/manager.py (+241/-0)
plainbox/plainbox/impl/session/resume.py (+477/-0)
plainbox/plainbox/impl/session/state.py (+651/-0)
plainbox/plainbox/impl/session/storage.py (+608/-0)
plainbox/plainbox/impl/session/suspend.py (+321/-0)
plainbox/plainbox/impl/session/test_jobs.py (+218/-0)
plainbox/plainbox/impl/session/test_legacy.py (+65/-0)
plainbox/plainbox/impl/session/test_resume.py (+1290/-0)
plainbox/plainbox/impl/session/test_state.py (+556/-0)
plainbox/plainbox/impl/session/test_storage.py (+161/-0)
plainbox/plainbox/impl/session/test_suspend.py (+502/-0)
plainbox/plainbox/impl/signal.py (+128/-0)
plainbox/plainbox/impl/test_applogic.py (+49/-0)
plainbox/plainbox/impl/test_box.py (+29/-8)
plainbox/plainbox/impl/test_config.py (+12/-1)
plainbox/plainbox/impl/test_job.py (+28/-49)
plainbox/plainbox/impl/test_result.py (+99/-83)
plainbox/plainbox/impl/test_rfc822.py (+53/-1)
plainbox/plainbox/impl/test_runner.py (+25/-24)
plainbox/plainbox/impl/test_signal.py (+46/-0)
plainbox/plainbox/impl/test_testing_utils.py (+54/-0)
plainbox/plainbox/impl/testing_utils.py (+53/-19)
plainbox/plainbox/test-data/html-exporter/example-data.html (+10931/-0)
plainbox/plainbox/test-data/html-exporter/html-inliner.html (+19/-0)
plainbox/plainbox/test-data/integration-tests/smoke/true.json (+11/-0)
plainbox/plainbox/testing_utils/resource.py (+104/-0)
plainbox/plainbox/testing_utils/test_testcases.py (+11/-0)
plainbox/plainbox/testing_utils/testcases.py (+6/-2)
plainbox/plainbox/vendor/extcmd/__init__.py (+101/-24)
plainbox/plainbox/vendor/funcsigs/LICENSE (+13/-0)
plainbox/plainbox/vendor/funcsigs/__init__.py (+810/-0)
plainbox/plainbox/vendor/funcsigs/version.py (+1/-0)
plainbox/requirements/deb-dbus.txt (+1/-0)
plainbox/requirements/pip-dbus.txt (+1/-0)
plainbox/setup.py (+12/-0)
To merge this branch: bzr merge lp:~jeffmarcom/opencompute/checkbox-ocp_update-plainbox
Reviewer Review Type Date Requested Status
Jeff Lane  Approve
Review via email: mp+185549@code.launchpad.net

Commit message

Update the plainbox code to current trunk in lp:checkbox

Description of the change

This updates the included version of plainbox within checkbox for the opencompute project.

To post a comment you must log in.
Revision history for this message
Jeff Lane  (bladernr) wrote :

big, but looks ok as best as I can tell. Seems to build and such.

review: Approve
Revision history for this message
Jeff Marcom (jeffmarcom) wrote :

The attempt to merge lp:~jeffmarcom/opencompute/checkbox-ocp_update-plainbox into lp:opencompute/checkbox failed. Below is the output from the failed tests.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2013-08-21 16:23:56 +0000
3+++ debian/changelog 2013-09-13 17:12:45 +0000
4@@ -1,3 +1,12 @@
5+
6+checkbox (1.16.7~OCP) UNRELEASED; urgency=low
7+
8+ [ Jeff Marcom ]
9+ * Updated plainbox based on version 0.4.dev in lp:checkbox (16.12)
10+
11+ -- Jeff Marcom <jeff.marcom@canonical.com> Fri, 13 Sept 2013 10:13:04 -0400
12+
13+
14 checkbox (1.16.6~OCP) UNRELEASED; urgency=low
15
16 [ Jeff Marcom ]
17
18=== modified file 'plainbox/MANIFEST.in'
19--- plainbox/MANIFEST.in 2013-05-09 18:39:35 +0000
20+++ plainbox/MANIFEST.in 2013-09-13 17:12:45 +0000
21@@ -1,9 +1,10 @@
22 include README.md
23 include COPYING
24 include mk-interesting-graphs.sh
25-recursive-include plainbox/test-data/ *.json *.xml *.txt
26+recursive-include plainbox/test-data *.json *.xml *.txt
27 recursive-include docs *.rst
28 include docs/conf.py
29-include plainbox/data/report/hardware-1_0.rng
30+recursive-include plainbox/data/report *.rng *.css *.xsl *.js
31+recursive-include plainbox/data/report/images *.png
32 include contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy
33 include contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy
34
35=== added file 'plainbox/contrib/com.canonical.certification.PlainBox1.service'
36--- plainbox/contrib/com.canonical.certification.PlainBox1.service 1970-01-01 00:00:00 +0000
37+++ plainbox/contrib/com.canonical.certification.PlainBox1.service 2013-09-13 17:12:45 +0000
38@@ -0,0 +1,3 @@
39+[D-BUS Service]
40+Name=com.canonical.certification.PlainBox1
41+Exec=/usr/bin/plainbox service
42
43=== added file 'plainbox/contrib/dbus-mini-client.py'
44--- plainbox/contrib/dbus-mini-client.py 1970-01-01 00:00:00 +0000
45+++ plainbox/contrib/dbus-mini-client.py 2013-09-13 17:12:45 +0000
46@@ -0,0 +1,430 @@
47+#!/usr/bin/env python3
48+########
49+#This simple script provides a small reference and example of how to
50+#invoke plainbox methods through d-bus to accomplish useful tasks.
51+#
52+#Use of the d-feet tool is suggested for interactive exploration of
53+#the plainbox objects, interfaces and d-bus API. However, d-feet can be
54+#cumbersome to use for more advanced testing and experimentation.
55+#
56+#This script can be adapted to fit other testing needs as well.
57+#
58+#To run it, first launch plainbox in service mode using the stub provider:
59+# $ plainbox -c stub service
60+#
61+#then run the script itself. It does the following things:
62+#
63+# 1- Obtain a whitelist and a job provider
64+# 2- Use the whitelist to "qualify" jobs offered by the provider,
65+# In essence filtering them to obtain a desired list of jobs to run.
66+# 3- Run each job in the run_list, this implicitly updates the session's
67+# results and state map.
68+# 4- Print the job names and outcomes and some other data
69+# 5- Export the session's data (job results) to xml in /tmp.
70+#####
71+
72+import dbus
73+from gi.repository import GObject
74+from dbus.mainloop.glib import DBusGMainLoop
75+from plainbox.abc import IJobResult
76+
77+bus = dbus.SessionBus(mainloop=DBusGMainLoop())
78+
79+# TODO: Create a class to remove all global var.
80+current_job_path = None
81+service = None
82+session_object_path = None
83+session_object = None
84+run_list = None
85+desired_job_list = None
86+whitelist = None
87+exports_count = 0
88+
89+def main():
90+ global service
91+ global session_object_path
92+ global session_object
93+ global run_list
94+ global desired_job_list
95+ global whitelist
96+
97+ whitelist = bus.get_object(
98+ 'com.canonical.certification.PlainBox1',
99+ '/plainbox/whitelist/stub'
100+ )
101+
102+ provider = bus.get_object(
103+ 'com.canonical.certification.PlainBox1',
104+ '/plainbox/provider/stubbox'
105+ )
106+
107+ #whitelist = bus.get_object(
108+ # 'com.canonical.certification.PlainBox1',
109+ # '/plainbox/whitelist/default'
110+ #)
111+
112+ #provider = bus.get_object(
113+ # 'com.canonical.certification.PlainBox1',
114+ # '/plainbox/provider/checkbox'
115+ #)
116+
117+ #A provider manages objects other than jobs.
118+ provider_objects = provider.GetManagedObjects(
119+ dbus_interface='org.freedesktop.DBus.ObjectManager')
120+
121+ #Create a session and "seed" it with my job list:
122+ job_list = [k for k, v in provider_objects.items() if not 'whitelist' in k]
123+ service = bus.get_object(
124+ 'com.canonical.certification.PlainBox1',
125+ '/plainbox/service1'
126+ )
127+ session_object_path = service.CreateSession(
128+ job_list,
129+ dbus_interface='com.canonical.certification.PlainBox.Service1'
130+ )
131+ session_object = bus.get_object(
132+ 'com.canonical.certification.PlainBox1',
133+ session_object_path
134+ )
135+
136+ if session_object.PreviousSessionFile():
137+ if ask_for_resume():
138+ session_object.Resume()
139+ else:
140+ session_object.Clean()
141+
142+ #to get only the *jobs* that are designated by the whitelist.
143+ desired_job_list = [
144+ object for object in provider_objects if whitelist.Designates(
145+ object,
146+ dbus_interface='com.canonical.certification.PlainBox.WhiteList1')]
147+
148+ desired_local_job_list = sorted([
149+ object for object in desired_job_list if
150+ bus.get_object('com.canonical.certification.PlainBox1', object).Get(
151+ 'com.canonical.certification.CheckBox.JobDefinition1',
152+ 'plugin') == 'local'
153+ ])
154+
155+ #Now I update the desired job list.
156+ session_object.UpdateDesiredJobList(
157+ desired_local_job_list,
158+ dbus_interface='com.canonical.certification.PlainBox.Session1'
159+ )
160+
161+ #Now, the run_list contains the list of jobs I actually need to run \o/
162+ run_list = session_object.Get(
163+ 'com.canonical.certification.PlainBox.Session1',
164+ 'run_list'
165+ )
166+
167+ # Add some signal receivers
168+ bus.add_signal_receiver(
169+ catchall_local_job_result_available_signals_handler,
170+ dbus_interface="com.canonical.certification.PlainBox.Service1",
171+ signal_name="JobResultAvailable")
172+
173+ # Start running jobs
174+ print("[ Running All Local Jobs ]".center(80, '='))
175+ run_local_jobs()
176+
177+ #PersistentSave can be called at any point to checkpoint session state.
178+ #In here, we're just calling it at the end, as an example.
179+ print("[ Saving the session ]".center(80, '='))
180+ session_object.PersistentSave()
181+
182+
183+def ask_for_outcome(prompt=None, allowed=None):
184+ if prompt is None:
185+ prompt = "what is the outcome? "
186+ if allowed is None:
187+ allowed = (IJobResult.OUTCOME_PASS, "p",
188+ IJobResult.OUTCOME_FAIL, "f",
189+ IJobResult.OUTCOME_SKIP, "s")
190+ answer = None
191+ while answer not in allowed:
192+ print("Allowed answers are: {}".format(", ".join(allowed)))
193+ answer = input(prompt)
194+ # Useful shortcuts for testing
195+ if answer == "f":
196+ answer = IJobResult.OUTCOME_FAIL
197+ if answer == "p":
198+ answer = IJobResult.OUTCOME_PASS
199+ if answer == "s":
200+ answer = IJobResult.OUTCOME_SKIP
201+ return answer
202+
203+
204+def ask_for_test(prompt=None, allowed=None):
205+ if prompt is None:
206+ prompt = "Run the test command? "
207+ if allowed is None:
208+ allowed = ("y",
209+ "n",
210+ )
211+ answer = None
212+ while answer not in allowed:
213+ print("Allowed answers are: {}".format(", ".join(allowed)))
214+ answer = input(prompt)
215+ return answer
216+
217+def ask_for_resume():
218+ prompt = "Do you want to resume the previous session [Y/n]? "
219+ allowed = ('', 'y', 'Y', 'n', 'N')
220+ answer = None
221+ while answer not in allowed:
222+ answer = input(prompt)
223+ return False if answer in ('n', 'N') else True
224+
225+
226+# Asynchronous calls need reply handlers
227+def handle_export_reply(s):
228+ print("Export to buffer: I got {} bytes of export data".format(len(s)))
229+ maybe_quit_after_export()
230+
231+def handle_export_to_file_reply(s):
232+ print("Export to file: completed to {}".format(s))
233+ maybe_quit_after_export()
234+
235+def maybe_quit_after_export():
236+ # Two asynchronous callbacks calling this may result in a race
237+ # condition. Don't do this at home, use a semaphore or lock.
238+ global exports_count
239+ exports_count += 1
240+ if exports_count >= 2:
241+ loop.quit()
242+
243+def handle_error(e):
244+ print(str(e))
245+ loop.quit()
246+
247+
248+def catchall_ask_for_outcome_signals_handler(current_runner_path):
249+ global current_job_path
250+ job_def_object = bus.get_object(
251+ 'com.canonical.certification.PlainBox1', current_job_path)
252+ job_cmd = job_def_object.Get(
253+ 'com.canonical.certification.CheckBox.JobDefinition1',
254+ 'command')
255+ job_runner_object = bus.get_object(
256+ 'com.canonical.certification.PlainBox1', current_runner_path)
257+ if job_cmd:
258+ run_test = ask_for_test()
259+ if run_test == 'y':
260+ job_runner_object.RunCommand()
261+ return
262+ outcome_from_command = job_runner_object.Get(
263+ 'com.canonical.certification.PlainBox.RunningJob1',
264+ 'outcome_from_command')
265+ print("Return code from the command indicates: {} ".format(
266+ outcome_from_command))
267+ outcome = ask_for_outcome()
268+ comments = 'Test plainbox comments'
269+ job_runner_object.SetOutcome(
270+ outcome,
271+ comments,
272+ dbus_interface='com.canonical.certification.PlainBox.RunningJob1')
273+
274+
275+def catchall_io_log_generated_signals_handler(offset, name, data):
276+ try:
277+ print("(<{}:{:05}>) {}".format(
278+ name, int(offset), data.decode('UTF-8').rstrip()))
279+ except UnicodeDecodeError:
280+ pass
281+
282+
283+def catchall_local_job_result_available_signals_handler(job, result):
284+ # XXX: check if the job path actually matches the current_job_path
285+ # Update the session job state map and run new jobs
286+ global session_object
287+ session_object.UpdateJobResult(
288+ job, result,
289+ reply_handler=run_local_jobs,
290+ error_handler=handle_error,
291+ dbus_interface='com.canonical.certification.PlainBox.Session1')
292+
293+
294+def catchall_job_result_available_signals_handler(job, result):
295+ # XXX: check if the job path actually matches the current_job_path
296+ # Update the session job state map and run new jobs
297+ global session_object
298+ session_object.UpdateJobResult(
299+ job, result,
300+ reply_handler=run_jobs,
301+ error_handler=handle_error,
302+ dbus_interface='com.canonical.certification.PlainBox.Session1')
303+
304+
305+def run_jobs():
306+ global run_list
307+ #Now the actual run, job by job.
308+ if run_list:
309+ job_path = run_list.pop(0)
310+ global current_job_path
311+ global session_object_path
312+ current_job_path = job_path
313+ job_def_object = bus.get_object(
314+ 'com.canonical.certification.PlainBox', current_job_path)
315+ job_name = job_def_object.Get(
316+ 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
317+ job_desc = job_def_object.Get(
318+ 'com.canonical.certification.PlainBox.JobDefinition1',
319+ 'description')
320+ print("[ {} ]".format(job_name).center(80, '-'))
321+ if job_desc:
322+ print(job_desc)
323+ print("^" * len(job_desc.splitlines()[-1]))
324+ print()
325+ service.RunJob(session_object_path, job_path)
326+ else:
327+ show_results()
328+
329+
330+def run_local_jobs():
331+ global run_list
332+ global desired_job_list
333+ global whitelist
334+ if run_list:
335+ job_path = run_list.pop(0)
336+ global current_job_path
337+ global session_object_path
338+ current_job_path = job_path
339+ job_def_object = bus.get_object(
340+ 'com.canonical.certification.PlainBox1', current_job_path)
341+ job_name = job_def_object.Get(
342+ 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
343+ job_desc = job_def_object.Get(
344+ 'com.canonical.certification.PlainBox.JobDefinition1',
345+ 'description')
346+ print("[ {} ]".format(job_name).center(80, '-'))
347+ if job_desc:
348+ print(job_desc)
349+ service.RunJob(session_object_path, job_path)
350+ else:
351+ #Now I update the desired job list to get jobs created from local jobs.
352+ session_object.UpdateDesiredJobList(
353+ desired_job_list,
354+ dbus_interface='com.canonical.certification.PlainBox.Session1'
355+ )
356+ bus.add_signal_receiver(
357+ catchall_ask_for_outcome_signals_handler,
358+ dbus_interface="com.canonical.certification.PlainBox.Service1",
359+ signal_name="AskForOutcome")
360+
361+ bus.add_signal_receiver(
362+ catchall_io_log_generated_signals_handler,
363+ dbus_interface="com.canonical.certification.PlainBox.Service1",
364+ signal_name="IOLogGenerated",
365+ byte_arrays=True) # To easily convert the byte arrays to strings
366+
367+ # Replace the job result handler we created for local jobs for by the
368+ # one dedicated to regular job types
369+ bus.remove_signal_receiver(
370+ catchall_local_job_result_available_signals_handler,
371+ dbus_interface="com.canonical.certification.PlainBox.Service1",
372+ signal_name="JobResultAvailable")
373+
374+ bus.add_signal_receiver(
375+ catchall_job_result_available_signals_handler,
376+ dbus_interface="com.canonical.certification.PlainBox.Service1",
377+ signal_name="JobResultAvailable")
378+
379+ job_list = session_object.Get(
380+ 'com.canonical.certification.PlainBox.Session1',
381+ 'job_list'
382+ )
383+
384+ #to get only the *jobs* that are designated by the whitelist.
385+ desired_job_list = [
386+ object for object in job_list if whitelist.Designates(
387+ object,
388+ dbus_interface=
389+ 'com.canonical.certification.PlainBox.WhiteList1')]
390+
391+ #Now I update the desired job list.
392+ # XXX: Remove previous local jobs from this list to avoid evaluating
393+ # them twice
394+ session_object.UpdateDesiredJobList(
395+ desired_job_list,
396+ dbus_interface='com.canonical.certification.PlainBox.Session1'
397+ )
398+
399+ #Now, the run_list contains the list of jobs I actually need to run \o/
400+ run_list = session_object.Get(
401+ 'com.canonical.certification.PlainBox.Session1',
402+ 'run_list'
403+ )
404+
405+ print("[ Running All Jobs ]".center(80, '='))
406+ run_jobs()
407+
408+
409+def show_results():
410+ global session_object_path
411+ session_object = bus.get_object(
412+ 'com.canonical.certification.PlainBox1',
413+ session_object_path
414+ )
415+ job_state_map = session_object.Get(
416+ 'com.canonical.certification.PlainBox.Session1', 'job_state_map')
417+ print("[ Results ]".center(80, '='))
418+ for k, job_state_path in job_state_map.items():
419+ job_state_object = bus.get_object(
420+ 'com.canonical.certification.PlainBox1',
421+ job_state_path
422+ )
423+ # Get the job definition object and some properties
424+ job_def_path = job_state_object.Get(
425+ 'com.canonical.certification.PlainBox.JobState1', 'job')
426+ job_def_object = bus.get_object(
427+ 'com.canonical.certification.PlainBox1', job_def_path)
428+ job_name = job_def_object.Get(
429+ 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
430+ # Ask the via value (e.g. to comptute job categories)
431+ # if a job is a child of a local job
432+ job_via = job_def_object.Get(
433+ 'com.canonical.certification.CheckBox.JobDefinition1', 'via')
434+
435+ # Get the current job result object and the outcome property
436+ job_result_path = job_state_object.Get(
437+ 'com.canonical.certification.PlainBox.JobState1', 'result')
438+ job_result_object = bus.get_object(
439+ 'com.canonical.certification.PlainBox1', job_result_path)
440+ outcome = job_result_object.Get(
441+ 'com.canonical.certification.PlainBox.Result1', 'outcome')
442+ comments = job_result_object.Get(
443+ 'com.canonical.certification.PlainBox.Result1', 'comments')
444+ io_log = job_result_object.Get(
445+ 'com.canonical.certification.PlainBox.Result1',
446+ 'io_log', byte_arrays=True)
447+
448+ print("{:55s} {:15s} {}".format(job_name, outcome, comments))
449+ export_session()
450+
451+def export_session():
452+ service.ExportSessionToFile(
453+ session_object_path,
454+ "xml",
455+ [''],
456+ "/tmp/report.xml",
457+ reply_handler=handle_export_to_file_reply,
458+ error_handler=handle_error
459+ )
460+ # The exports will apparently run in parallel. The callbacks
461+ # are responsible for ensuring exiting after this.
462+ service.ExportSession(
463+ session_object_path,
464+ "xml",
465+ [''],
466+ reply_handler=handle_export_reply,
467+ error_handler=handle_error
468+ )
469+
470+# Start the first call after a short delay
471+GObject.timeout_add(5, main)
472+loop = GObject.MainLoop()
473+loop.run()
474+
475+# Stop the Plainbox dbus service
476+service.Exit()
477
478=== added file 'plainbox/docs/author/checkbox-job-format.rst'
479--- plainbox/docs/author/checkbox-job-format.rst 1970-01-01 00:00:00 +0000
480+++ plainbox/docs/author/checkbox-job-format.rst 2013-09-13 17:12:45 +0000
481@@ -0,0 +1,168 @@
482+===================================
483+Checkbox job file format and fields
484+===================================
485+
486+This file contains NO examples, this is on purpose since the jobs
487+directory contains several hundred examples showcasing all the features
488+described here.
489+
490+File format and location
491+------------------------
492+Jobs are expressed as sections in text files that conform somewhat to
493+the rfc822 specification format. Each section defines a single job. The
494+section is delimited with an empty newline. Within each section, each
495+field starts with the field name, a colon, a space and then the field
496+contents. Multiple-line fields can be input by having a newline right
497+after the colon, and then entering text lines after that, each line
498+should start with at least one space.
499+
500+Fields that can be used on a job
501+--------------------------------
502+:name:
503+ (mandatory) - A name for the job. Should be unique, an error will
504+ be generated if there are duplicates. Should contain characters in
505+ [a-z0-9/-].
506+
507+:plugin:
508+
509+ (mandatory) - For historical reasons it's called "plugin" but it's
510+ better thought of as describing the "type" of job. The allowed types
511+ are:
512+
513+ :manual: jobs that require the user to perform an action and then
514+ decide on the test's outcome.
515+ :shell: jobs that run without user intervention and
516+ automatically set the test's outcome.
517+ :user-interact: jobs that require the user to perform an
518+ interaction, after which the outcome is automatically set.
519+ :user-verify: jobs that automatically perform an action or test
520+ and then request the user to decide on the test's outcome.
521+ :attachment: jobs whose command output will be attached to the
522+ test report or submission.
523+ :local: a job whose command output needs to be in :term:`CheckBox` job
524+ format. Jobs output by a local job will be added to the set of
525+ available jobs to be run.
526+ :resource: A job whose command output results in a set of rfc822
527+ records, containing key/value pairs, and that can be used in other
528+ jobs' ``requires`` expressions.
529+
530+:requires:
531+ (optional). If specified, the job will only run if the conditions
532+ expressed in this field are met.
533+
534+ Conditions are of the form ``<resource>.<key> <comparison-operator>
535+ 'value' (and|or) ...`` . Comparison operators can be ==, != and ``in``.
536+ Values to compare to can be scalars or (in the case of the ``in``
537+ operator) arrays or tuples. The ``not in`` operator is explicitly
538+ unsupported.
539+
540+ Requirements can be logically chained with ``or`` and
541+ ``and`` operators. They can also be placed in multiple lines,
542+ respecting the rfc822 multi-line syntax, in which case all
543+ requirements must be met for the job to run ( ``and`` ed).
544+
545+ The :term:`PlainBox` resource program evaluator is extensively documented,
546+ to see a detailed description including rationale and implementation of
547+ :term:`CheckBox` "legacy" compatibility, see :ref:`Resources in Plainbox
548+ <resources>`.
549+
550+:depends:
551+ (optional). If specified, the job will only run if all the listed
552+ jobs have run and passed. Multiple job names, separated by spaces,
553+ can be specified.
554+
555+:command:
556+ (optional). A command can be provided, to be executed under specific
557+ circumstances. For ``manual``, ``user-interact`` and ``user-verify``
558+ jobs, the command will be executed when the user presses a "test"
559+ button present in the user interface. For ``shell`` jobs, the
560+ command will be executed unconditionally as soon as the job is
561+ started. In both cases the exit code from the command (0 for
562+ success, !0 for failure) will be used to set the test's outcome. For
563+ ``manual``, ``user-interact`` and ``user-verify`` jobs, the user can
564+ override the command's outcome. The command will be run using the
565+ default system shell. If a specific shell is needed it should be
566+ instantiated in the command. A multi-line command or shell script
567+ can be used with the usual multi-line syntax.
568+
569+ Note that a ``shell`` job without a command will do nothing.
570+
571+:description:
572+ (mandatory). Provides a textual description for the job. This is
573+ mostly to aid people reading job descriptions in figuring out what a
574+ job does.
575+
576+ The description field, however, is used specially in ``manual``,
577+ ``user-interact`` and ``user-verify`` jobs. For these jobs, the
578+ description will be shown in the user interface, and in these cases
579+ it's expected to contain instructions for the user to follow, as
580+ well as criteria for him to decide whether the job passes or fails.
581+ For these types of jobs, the description needs to contain a few
582+ sub-fields, in order:
583+
584+ :PURPOSE: This indicates the purpose or intent of the test.
585+ :STEPS: A numbered list of steps for the user to follow.
586+ :INFO:
587+ (optional). Additional information about the test. This is
588+ commonly used to present command output for the user to validate.
589+ For this purpose, the ``$output`` substitution variable can be used
590+ (actually, it can be used anywhere in the description). If present,
591+ it will be replaced by the standard output generated from running
592+ the job's command (commonly when the user presses the "Test"
593+ button).
594+ :VERIFICATION:
595+ A question for the user to answer, deciding whether the test
596+ passes or fails. The question should be phrased in such a way
597+ that an answer of **Yes** means the test passed, and an answer of
598+ **No** means it failed.
599+:user:
600+ (optional). If specified, the job will be run as the user specified
601+ here. This is most commonly used to run jobs as the superuser
602+ (root).
603+
604+:environ:
605+ (optional). If specified, the listed environment variables
606+ (separated by spaces) will be taken from the invoking environment
607+ (i.e. the one :term:`CheckBox` is run under) and set to that value on the
608+ job execution environment (i.e. the one the job will run under).
609+ Note that only the *variable names* should be listed, not the
610+ *values*, which will be taken from the existing environment. This
611+ only makes sense for jobs that also have the ``user`` attribute.
612+ This key provides a mechanism to account for security policies in
613+ ``sudo`` and ``pkexec``, which provide a sanitized execution
614+ environment, with the downside that useful configuration specified
615+ in environment variables may be lost in the process.
616+
617+:estimated_duration:
618+ (optional) This field contains metadata about how long the job is
619+ expected to run for, as a positive float value indicating
620+ the estimated job duration in seconds.
621+
622+===========================
623+Extension of the job format
624+===========================
625+
626+The :term:`CheckBox` job format can be considered "extensible", in that
627+additional keys can be added to existing jobs to contain additional
628+data that may be needed.
629+
630+In order for these extra fields to be exposed through the API (i.e. as
631+properties of JobDefinition instances), they need to be declared as
632+properties in (:mod:`plainbox.impl.job`). This is a good place to document,
633+via a docstring, what the field is for and how to interpret it.
634+
635+Implementation note: if additional fields are added, *:term:`CheckBox`* needs
636+to be also told about them, the reason is that :term:`CheckBox` *does* perform
637+validation of the job descriptions, ensuring they contain only known fields and
638+that fields contain expected data types. The jobs_info plugin contains the job
639+schema declaration and can be consulted to verify the known fields, whether
640+they are optional or mandatory, and the type of data they're expected to
641+contain.
642+
643+Also, :term:`CheckBox` validates that fields contain data of a specific type,
644+so care must be taken not to simply change contents of fields if
645+:term:`CheckBox` compatibility of jobs is desired.
646+
647+:term:`PlainBox` does this validation on a per-accessor basis, so data in each
648+field must make sense as defined by that field's accessor. There is no need,
649+however, to declare field type beforehand.
650
651=== modified file 'plainbox/docs/author/index.rst'
652--- plainbox/docs/author/index.rst 2013-03-25 16:40:57 +0000
653+++ plainbox/docs/author/index.rst 2013-09-13 17:12:45 +0000
654@@ -11,6 +11,9 @@
655 is a guiding point for subsequent editions that will expand and provide
656 real value.
657
658+.. toctree::
659+ checkbox-job-format.rst
660+
661 Personas and stories
662 --------------------
663
664
665=== modified file 'plainbox/docs/dev/reference.rst'
666--- plainbox/docs/dev/reference.rst 2013-05-14 09:04:30 +0000
667+++ plainbox/docs/dev/reference.rst 2013-09-13 17:12:45 +0000
668@@ -94,6 +94,11 @@
669 :undoc-members:
670 :show-inheritance:
671
672+.. automodule:: plainbox.impl.exporter.html
673+ :members:
674+ :undoc-members:
675+ :show-inheritance:
676+
677 .. automodule:: plainbox.impl.secure
678 :members:
679 :undoc-members:
680@@ -144,11 +149,6 @@
681 :undoc-members:
682 :show-inheritance:
683
684-.. automodule:: plainbox.impl.mock_job
685- :members:
686- :undoc-members:
687- :show-inheritance:
688-
689 .. automodule:: plainbox.impl.resource
690 :members:
691 :undoc-members:
692@@ -173,6 +173,43 @@
693 :undoc-members:
694 :show-inheritance:
695
696+.. automodule:: plainbox.impl.session.state
697+ :members:
698+ :undoc-members:
699+ :show-inheritance:
700+
701+.. automodule:: plainbox.impl.session.jobs
702+ :members:
703+ :undoc-members:
704+ :show-inheritance:
705+
706+.. automodule:: plainbox.impl.session.storage
707+ :members:
708+ :undoc-members:
709+ :show-inheritance:
710+
711+.. automodule:: plainbox.impl.session.suspend
712+ :members:
713+ :undoc-members:
714+ :private-members:
715+ :show-inheritance:
716+
717+.. automodule:: plainbox.impl.session.resume
718+ :members:
719+ :undoc-members:
720+ :private-members:
721+ :show-inheritance:
722+
723+.. automodule:: plainbox.impl.session.legacy
724+ :members:
725+ :undoc-members:
726+ :show-inheritance:
727+
728+.. automodule:: plainbox.impl.session.manager
729+ :members:
730+ :undoc-members:
731+ :show-inheritance:
732+
733 .. automodule:: plainbox.impl.testing_utils
734 :members:
735 :undoc-members:
736
737=== modified file 'plainbox/docs/dev/resources.rst'
738--- plainbox/docs/dev/resources.rst 2013-05-11 13:56:36 +0000
739+++ plainbox/docs/dev/resources.rst 2013-09-13 17:12:45 +0000
740@@ -1,3 +1,5 @@
741+.. _resources:
742+
743 Resources
744 =========
745
746
747=== modified file 'plainbox/plainbox/abc.py'
748--- plainbox/plainbox/abc.py 2013-02-25 11:02:58 +0000
749+++ plainbox/plainbox/abc.py 2013-09-13 17:12:45 +0000
750@@ -114,13 +114,46 @@
751 # XXX: We could also store stuff like job duration and other meta-data but
752 # I wanted to avoid polluting this proposal with mundane details
753
754- @abstractproperty
755- def job(self):
756- """
757- Definition of the job
758+ # The outcome of a job is a one-word classification how how it ran. There
759+ # are several values that were not used in the original implementation but
760+ # their existence helps to organize and implement plainbox. They are
761+ # discussed below to make their intended meaning more detailed than is
762+ # possible from the variable name alone.
763+ #
764+ # The None outcome - a job that basically did not run at all.
765+ OUTCOME_NONE = None
766+ # The pass and fail outcomes are the two most essential, and externally
767+ # visible, job outcomes. They can be provided by either automated or manual
768+ # "classifier" - a script or a person that clicks a "pass" or "fail"
769+ # button.
770+ OUTCOME_PASS = 'pass'
771+ OUTCOME_FAIL = 'fail'
772+ # The skip outcome is used when the operator selected a job but then
773+ # skipped it. This is typically used for a manual job that is tedious or
774+ # was selected by accident.
775+ OUTCOME_SKIP = 'skip'
776+ # The not supported outcome is used when a job was about to run but a
777+ # dependency or resource requirement prevent it from running. XXX: perhaps
778+ # this should be called "not available", not supported has the "unsupported
779+ # code" feeling associated with it.
780+ OUTCOME_NOT_SUPPORTED = 'not-supported'
781+ # A temporary state that should be removed later on, used to indicate that
782+ # job runner is not implemented but the job "ran" so to speak.
783+ OUTCOME_NOT_IMPLEMENTED = 'not-implemented'
784+ # A temporary state before the user decides on the outcome of a manual
785+ # job or any other job that requires manual verification
786+ OUTCOME_UNDECIDED = 'undecided'
787
788- The object implements IJobDefinition
789- """
790+ # List of all valid values of OUTCOME_xxx
791+ ALL_OUTCOME_LIST = [
792+ OUTCOME_NONE,
793+ OUTCOME_PASS,
794+ OUTCOME_FAIL,
795+ OUTCOME_SKIP,
796+ OUTCOME_NOT_SUPPORTED,
797+ OUTCOME_NOT_IMPLEMENTED,
798+ OUTCOME_UNDECIDED,
799+ ]
800
801 @abstractproperty
802 def outcome(self):
803@@ -204,3 +237,79 @@
804 May raise NotImplementedError if the user interface cannot provide this
805 answer.
806 """
807+
808+
809+class IProviderBackend1(metaclass=ABCMeta):
810+ """
811+ Provider for the current type of tests.
812+
813+ This class provides the APIs required by the internal implementation
814+ that are not considered normal public APIs. The only consumer of the
815+ those methods and properties are internal to plainbox.
816+ """
817+
818+ @abstractproperty
819+ def CHECKBOX_SHARE(self):
820+ """
821+ Return the required value of CHECKBOX_SHARE environment variable.
822+
823+ .. note::
824+ This variable is only required by one script.
825+ It would be nice to remove this later on.
826+ """
827+
828+ @abstractproperty
829+ def extra_PYTHONPATH(self):
830+ """
831+ Return additional entry for PYTHONPATH, if needed.
832+
833+ This entry is required for CheckBox scripts to import the correct
834+ CheckBox python libraries.
835+
836+ .. note::
837+ The result may be None
838+ """
839+
840+ @abstractproperty
841+ def extra_PATH(self):
842+ """
843+ Return additional entry for PATH
844+
845+ This entry is required to lookup CheckBox scripts.
846+ """
847+
848+
849+class IProvider1(metaclass=ABCMeta):
850+ """
851+ Provider for the current type of tests
852+
853+ Also known as the 'checkbox-like' provider.
854+ """
855+
856+ @abstractproperty
857+ def name(self):
858+ """
859+ name of this provider
860+
861+ This name should be dbus-friendly. It should not be localizable.
862+ """
863+
864+ @abstractproperty
865+ def description(self):
866+ """
867+ description of this providr
868+
869+ This name should be dbus-friendly. It should not be localizable.
870+ """
871+
872+ @abstractmethod
873+ def get_builtin_jobs(self):
874+ """
875+ Load all the built-in jobs and return them
876+ """
877+
878+ @abstractmethod
879+ def get_builtin_whitelists(self):
880+ """
881+ Load all the built-in whitelists and return them
882+ """
883
884=== added file 'plainbox/plainbox/data/report/checkbox.js'
885--- plainbox/plainbox/data/report/checkbox.js 1970-01-01 00:00:00 +0000
886+++ plainbox/plainbox/data/report/checkbox.js 2013-09-13 17:12:45 +0000
887@@ -0,0 +1,16 @@
888+function showHide(what) {
889+ var heading = document.getElementById(what);
890+ var contents = document.getElementById(what + "-contents");
891+ var headingcontents = heading.innerHTML;
892+ var newcontents;
893+
894+ if (contents.style.display != "block") {
895+ newcontents = headingcontents.replace("closed", "open");
896+ contents.style.display = "block";
897+ } else {
898+ newcontents = headingcontents.replace("open", "closed");
899+ contents.style.display = "none";
900+ }
901+
902+ heading.innerHTML = newcontents;
903+}
904
905=== added directory 'plainbox/plainbox/data/report/images'
906=== added file 'plainbox/plainbox/data/report/images/body_bg.png'
907Binary files plainbox/plainbox/data/report/images/body_bg.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/body_bg.png 2013-09-13 17:12:45 +0000 differ
908=== added file 'plainbox/plainbox/data/report/images/bullet.png'
909Binary files plainbox/plainbox/data/report/images/bullet.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/bullet.png 2013-09-13 17:12:45 +0000 differ
910=== added file 'plainbox/plainbox/data/report/images/closed.png'
911Binary files plainbox/plainbox/data/report/images/closed.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/closed.png 2013-09-13 17:12:45 +0000 differ
912=== added file 'plainbox/plainbox/data/report/images/fail.png'
913Binary files plainbox/plainbox/data/report/images/fail.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/fail.png 2013-09-13 17:12:45 +0000 differ
914=== added file 'plainbox/plainbox/data/report/images/header_bg.png'
915Binary files plainbox/plainbox/data/report/images/header_bg.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/header_bg.png 2013-09-13 17:12:45 +0000 differ
916=== added file 'plainbox/plainbox/data/report/images/open.png'
917Binary files plainbox/plainbox/data/report/images/open.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/open.png 2013-09-13 17:12:45 +0000 differ
918=== added file 'plainbox/plainbox/data/report/images/pass.png'
919Binary files plainbox/plainbox/data/report/images/pass.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/pass.png 2013-09-13 17:12:45 +0000 differ
920=== added file 'plainbox/plainbox/data/report/images/skip.png'
921Binary files plainbox/plainbox/data/report/images/skip.png 1970-01-01 00:00:00 +0000 and plainbox/plainbox/data/report/images/skip.png 2013-09-13 17:12:45 +0000 differ
922=== added file 'plainbox/plainbox/data/report/styles.css'
923--- plainbox/plainbox/data/report/styles.css 1970-01-01 00:00:00 +0000
924+++ plainbox/plainbox/data/report/styles.css 2013-09-13 17:12:45 +0000
925@@ -0,0 +1,258 @@
926+body {
927+ font-family: "Ubuntu Beta", "Bitstream Vera Sans", DejaVu Sans, Tahoma, sans-serif;
928+ color: #333;
929+ background: white url(report/images/body_bg.png);
930+ font-size: 12px;
931+ line-height: 14px;
932+ margin: 0px;
933+ padding: 0px;
934+}
935+#container {
936+ background: #f7f6f5;
937+ margin: 0px auto 20px;
938+ padding: 0px;
939+ width: 976px;
940+}
941+#container-inner {
942+ background-color: #dfdcd9;
943+}
944+#header, #container-inner {
945+ -moz-border-radius: 0px 0px 5px 5px;
946+ -webkit-border-bottom-left-radius: 5px;
947+ -webkit-border-bottom-right-radius: 5px;
948+ -moz-box-shadow: #bbb 0px 0px 5px;
949+ -webkit-box-shadow: #bbb 0px 0px 5px;
950+}
951+#header {
952+ background: #dd4814 url(report/images/header_bg.png) top left repeat-x;
953+ height: 64px;
954+ margin: 0px;
955+ padding: 0px;
956+ position: relative;
957+}
958+
959+#menu-search {
960+ height: 40px;
961+ margin: 0 16px;
962+}
963+
964+#title {
965+ padding: 28px 24px;
966+}
967+
968+#content {
969+ /*padding: 32px 80px 32px 80px;*/
970+ padding: 32px 240px 32px 160px;
971+ margin: 0 16px 16px;
972+ width: 544px;
973+ background-color: #fff;
974+ -moz-border-radius: 4px;
975+ -webkit-border-radius: 4px;
976+}
977+#end-content {
978+ clear: both;
979+}
980+
981+#content-panel {
982+ width: 446px;
983+ margin: 0px 0px 0px 0px;
984+ padding: 8px 8px 32px 8px;
985+ background-color: #fff;
986+ -moz-border-radius: 4px;
987+ -webkit-border-radius: 4px;
988+}
989+
990+#copyright {
991+ background-position: 803px 40px;
992+ background-repeat: no-repeat;
993+ text-align: center;
994+ margin: 0 16px;
995+ padding: 40px 0 0 0;
996+ height: 32px;
997+}
998+#copyright p {
999+ color: #aea79f;
1000+ font-size: 10px;
1001+ line-height: 14px;
1002+ margin: 2px 0;
1003+}
1004+
1005+#footer {
1006+ padding-top: 16px;
1007+}
1008+#footer * {
1009+ font-size: 10px;
1010+ line-height: 14px;
1011+}
1012+#footer p {
1013+ margin: 0;
1014+ padding-bottom: 3px;
1015+ border-bottom: 1px dotted #aea79f;
1016+}
1017+#footer p.footer-title {
1018+ font-weight: bold;
1019+}
1020+#footer .footer-div {
1021+ width: 144px;
1022+ float: left;
1023+ margin-left: 16px;
1024+}
1025+#footer .last-div {
1026+ margin-right: 16px;
1027+}
1028+#footer ul {
1029+ list-style: none;
1030+ margin: 0;
1031+ padding: 0;
1032+}
1033+#footer li {
1034+ margin: 0;
1035+ padding: 3px 0;
1036+ border-bottom: 1px dotted #aea79f;
1037+}
1038+
1039+h1, h2, h3, h4, h5 {
1040+ padding: 0;
1041+ margin: 0;
1042+ font-weight: normal;
1043+}
1044+h1 {
1045+ font-size: 36px;
1046+ line-height: 40px;
1047+ color: #dd4814;
1048+}
1049+h2 {
1050+ font-size: 24px;
1051+ line-height: 28px;
1052+ margin-bottom: 8px;
1053+}
1054+h3 {
1055+ font-size: 16px;
1056+ line-height: 20px;
1057+ margin-bottom: 8px;
1058+}
1059+h3.link-other {
1060+ color: #333;
1061+}
1062+h3.link-services {
1063+ color: #fff;
1064+}
1065+h4 {
1066+ font-size: 12px;
1067+ line-height: 14px;
1068+}
1069+h4.partners {
1070+ color: #333;
1071+ font-size: 16px;
1072+ line-height: 20px;
1073+}
1074+h5 {
1075+ color: #333;
1076+ font-size: 10px;
1077+ line-height: 14px;
1078+}
1079+h1 span.grey, h2 span.grey, h1 span, h2 span{
1080+ color: #aea79f;
1081+}
1082+p {
1083+ font-size: 12px;
1084+ line-height: 14px;
1085+ margin-bottom: 8px;
1086+}
1087+strong {
1088+ font-weight: bold;
1089+}
1090+
1091+a {
1092+ color: #333;
1093+ text-decoration: none;
1094+}
1095+a:hover {
1096+ color: #dd4814;
1097+ text-decoration: underline;
1098+}
1099+div.footer-div:hover a, div#content:hover a {
1100+ color: #dd4814;
1101+ text-decoration: none;
1102+}
1103+div.footer-div:hover a:hover, div#content:hover a:hover {
1104+ color: #dd4814;
1105+ text-decoration: underline;
1106+}
1107+
1108+ul {
1109+ margin-bottom: 16px;
1110+ list-style-image: url(report/images/bullet.png);
1111+}
1112+ul li {
1113+ margin-bottom: 8px;
1114+ line-height: 14px;
1115+}
1116+ul li:last-child {
1117+ margin-bottom: 0px;
1118+}
1119+
1120+p.call-to-action {
1121+ color: #333;
1122+}
1123+p.case-study {
1124+ color: #333;
1125+}
1126+p.highlight {
1127+ font-size: 16px;
1128+ line-height: 20px;
1129+}
1130+p.introduction {
1131+ color: #333;
1132+ font-size: 16px;
1133+ line-height: 20px;
1134+}
1135+p.services {
1136+ color: #fff;
1137+}
1138+p.small-text {
1139+ color: #333;
1140+ font-size: 10px;
1141+}
1142+
1143+/* Clearing floats without extra markup
1144+Based on How To Clear Floats Without Structural Markup by PiE
1145+[http://www.positioniseverything.net/easyclearing.html] */
1146+.clearfix:after {
1147+ content: ".";
1148+ display: block;
1149+ height: 0;
1150+ clear: both;
1151+ visibility: hidden;
1152+}
1153+.clearfix {
1154+ -moz-border-radius: 5px 5px 5px 5px;
1155+ -webkit-border-bottom-top-radius: 5px;
1156+ -webkit-border-bottom-left-radius: 5px;
1157+ -webkit-border-bottom-bottom-radius: 5px;
1158+ -webkit-border-bottom-right-radius: 5px;
1159+ -moz-box-shadow: #bbb 0px 0px 5px;
1160+ -webkit-box-shadow: #bbb 0px 0px 5px;
1161+ display: inline-block;
1162+} /* for IE/Mac */
1163+td
1164+{
1165+ margin: 0;
1166+ padding-bottom: 3px;
1167+ border-bottom: 1px dotted #aea79f;
1168+ font-size: 10px;
1169+ line-height: 14px;
1170+}
1171+.resultimg
1172+{
1173+ height: 12px;
1174+}
1175+.disclosureimg
1176+{
1177+ height: .75em;
1178+ vertical-align: middle;
1179+}
1180+.data
1181+{
1182+ display: none;
1183+}
1184
1185=== modified file 'plainbox/plainbox/impl/applogic.py'
1186--- plainbox/plainbox/impl/applogic.py 2013-06-22 06:06:21 +0000
1187+++ plainbox/plainbox/impl/applogic.py 2013-09-13 17:12:45 +0000
1188@@ -30,8 +30,9 @@
1189 import os
1190 import re
1191
1192+from plainbox.abc import IJobResult
1193 from plainbox.impl import config
1194-from plainbox.impl.result import JobResult
1195+from plainbox.impl.result import MemoryJobResult
1196
1197
1198 class IJobQualifier(metaclass=ABCMeta):
1199@@ -66,6 +67,13 @@
1200 self._pattern = re.compile(pattern)
1201 self._pattern_text = pattern
1202
1203+ @property
1204+ def pattern_text(self):
1205+ """
1206+ text of the regular expression embedded in this qualifier
1207+ """
1208+ return self._pattern_text
1209+
1210 def designates(self, job):
1211 return self._pattern.match(job.name)
1212
1213@@ -113,6 +121,86 @@
1214 return False
1215
1216
1217+# NOTE: using CompositeQualifier seems strange but it's a tested proven
1218+# component so all we have to ensure is that we read the whitelist files
1219+# correctly.
1220+class WhiteList(CompositeQualifier):
1221+ """
1222+ A qualifier that understands checkbox whitelist files.
1223+
1224+ A whitelist file is a plain text, line oriented file. Each line represents
1225+ a regular expression pattern that can be matched against the name of a job.
1226+
1227+ The file can contain simple shell-style comments that begin with the pound
1228+ or hash key (#). Those are ignored. Comments can span both a fraction of a
1229+ line as well as the whole line.
1230+
1231+ For historical reasons each pattern has an implicit '^' and '$' prepended
1232+ and appended (respectively) to the actual pattern specified in the file.
1233+ """
1234+
1235+ def __init__(self, pattern_list, name=None):
1236+ """
1237+ Initialize a whitelist object with the specified list of patterns.
1238+
1239+ The patterns must be already mangled with '^' and '$'.
1240+ """
1241+ inclusive = [RegExpJobQualifier(pattern) for pattern in pattern_list]
1242+ exclusive = ()
1243+ super(WhiteList, self).__init__(inclusive, exclusive)
1244+ self._name = name
1245+
1246+ def __repr__(self):
1247+ return "<{} name:{!r}>".format(self.__class__.__name__, self.name)
1248+
1249+ @property
1250+ def name(self):
1251+ """
1252+ name of this WhiteList (might be None)
1253+ """
1254+ return self._name
1255+
1256+ @classmethod
1257+ def from_file(cls, pathname):
1258+ """
1259+ Load and initialize the WhiteList object from the specified file.
1260+
1261+ :param pathname: file to load
1262+ :returns: a fresh WhiteList object
1263+ """
1264+ pattern_list = cls._load_patterns(pathname)
1265+ name = os.path.splitext(os.path.basename(pathname))[0]
1266+ return cls(pattern_list, name=name)
1267+
1268+ @classmethod
1269+ def _load_patterns(self, pathname):
1270+ """
1271+ Load whitelist patterns from the specified file
1272+ """
1273+ pattern_list = []
1274+ # Load the file
1275+ with open(pathname, "rt", encoding="UTF-8") as stream:
1276+ for line in stream:
1277+ # Strip shell-style comments if there are any
1278+ try:
1279+ index = line.index("#")
1280+ except ValueError:
1281+ pass
1282+ else:
1283+ line = line[:index]
1284+ # Strip whitespace
1285+ line = line.strip()
1286+ # Skip empty lines (especially after stripping comments)
1287+ if line == "":
1288+ continue
1289+ # Surround the pattern with ^ and $
1290+ # so that it wont just match a part of the job name.
1291+ regexp_pattern = r"^{pattern}$".format(pattern=line)
1292+ # Accumulate patterns into the list
1293+ pattern_list.append(regexp_pattern)
1294+ return pattern_list
1295+
1296+
1297 def get_matching_job_list(job_list, qualifier):
1298 """
1299 Get a list of jobs that are designated by the specified qualifier.
1300@@ -137,16 +225,15 @@
1301 # OUTCOME_NOT_SUPPORTED _except_ if any of the inhibitors point to
1302 # a job with an OUTCOME_SKIP outcome, if that is the case mirror
1303 # that outcome. This makes 'skip' stronger than 'not-supported'
1304- outcome = JobResult.OUTCOME_NOT_SUPPORTED
1305+ outcome = IJobResult.OUTCOME_NOT_SUPPORTED
1306 for inhibitor in job_state.readiness_inhibitor_list:
1307 if inhibitor.cause != inhibitor.FAILED_DEP:
1308 continue
1309 related_job_state = session.job_state_map[
1310 inhibitor.related_job.name]
1311- if related_job_state.result.outcome == JobResult.OUTCOME_SKIP:
1312- outcome = JobResult.OUTCOME_SKIP
1313- job_result = JobResult({
1314- 'job': job,
1315+ if related_job_state.result.outcome == IJobResult.OUTCOME_SKIP:
1316+ outcome = IJobResult.OUTCOME_SKIP
1317+ job_result = MemoryJobResult({
1318 'outcome': outcome,
1319 'comments': job_state.get_readiness_description()
1320 })
1321@@ -177,9 +264,20 @@
1322 section="sru",
1323 help_text="Location of the fallback file")
1324
1325+ whitelist = config.Variable(
1326+ section="sru",
1327+ help_text="Optional whitelist with which to run SRU testing")
1328+
1329 environment = config.Section(
1330 help_text="Environment variables for scripts and jobs")
1331
1332+ default_provider = config.Variable(
1333+ section="common",
1334+ help_text="Name of the default provider to use",
1335+ validator_list=[
1336+ config.ChoiceValidator(['auto', 'src', 'deb', 'stub', 'ihv'])],
1337+ default="auto")
1338+
1339 class Meta:
1340
1341 # TODO: properly depend on xdg and use real code that also handles
1342
1343=== modified file 'plainbox/plainbox/impl/box.py'
1344--- plainbox/plainbox/impl/box.py 2013-06-22 06:06:21 +0000
1345+++ plainbox/plainbox/impl/box.py 2013-09-13 17:12:45 +0000
1346@@ -26,55 +26,28 @@
1347 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
1348 """
1349
1350-import argparse
1351-import errno
1352 import logging
1353-import pdb
1354-import sys
1355
1356 from plainbox import __version__ as version
1357 from plainbox.impl.applogic import PlainBoxConfig
1358-from plainbox.impl.checkbox import CheckBox
1359+from plainbox.impl.commands import PlainBoxToolBase
1360 from plainbox.impl.commands.check_config import CheckConfigCommand
1361 from plainbox.impl.commands.dev import DevCommand
1362 from plainbox.impl.commands.run import RunCommand
1363 from plainbox.impl.commands.selftest import SelfTestCommand
1364+from plainbox.impl.commands.service import ServiceCommand
1365 from plainbox.impl.commands.sru import SRUCommand
1366-from plainbox.impl.logging import setup_logging, adjust_logging
1367+from plainbox.impl.logging import setup_logging
1368
1369
1370 logger = logging.getLogger("plainbox.box")
1371
1372
1373-class PlainBox:
1374+class PlainBoxTool(PlainBoxToolBase):
1375 """
1376 Command line interface to PlainBox
1377 """
1378
1379- def __init__(self):
1380- """
1381- Initialize all the variables, real stuff happens in main()
1382- """
1383- self._early_parser = None # set in _early_init()
1384- self._config = None # set in _late_init()
1385- self._checkbox = None # set in _late_init()
1386- self._parser = None # set in _late_init()
1387-
1388- def main(self, argv=None):
1389- """
1390- Run as if invoked from command line directly
1391- """
1392- self.early_init()
1393- early_ns = self._early_parser.parse_args(argv)
1394- self.late_init(early_ns)
1395- logger.debug("parsed early namespace: %s", early_ns)
1396- # parse the full command line arguments, this is also where we
1397- # do argcomplete-dictated exit if bash shell completion is requested
1398- ns = self._parser.parse_args(argv)
1399- logger.debug("parsed full namespace: %s", ns)
1400- self.final_init(ns)
1401- return self.dispatch_and_catch_exceptions(ns)
1402-
1403 @classmethod
1404 def get_config_cls(cls):
1405 """
1406@@ -107,221 +80,17 @@
1407 top-level subcommands.
1408 """
1409 # TODO: switch to plainbox plugins
1410- RunCommand(self._checkbox).register_parser(subparsers)
1411+ RunCommand(self._provider).register_parser(subparsers)
1412 SelfTestCommand().register_parser(subparsers)
1413- SRUCommand(self._checkbox, self._config).register_parser(subparsers)
1414+ SRUCommand(self._provider, self._config).register_parser(subparsers)
1415 CheckConfigCommand(self._config).register_parser(subparsers)
1416- DevCommand(self._checkbox, self._config).register_parser(subparsers)
1417-
1418- def early_init(self):
1419- """
1420- Do very early initialization. This is where we initalize stuff even
1421- without seeing a shred of command line data or anything else.
1422- """
1423- self._early_parser = self.construct_early_parser()
1424-
1425- def late_init(self, early_ns):
1426- """
1427- Initialize with early command line arguments being already parsed
1428- """
1429- adjust_logging(
1430- level=early_ns.log_level, trace_list=early_ns.trace,
1431- debug_console=early_ns.debug_console)
1432- # Load plainbox configuration
1433- self._config = self.get_config_cls().get()
1434- # Load and initialize checkbox provider
1435- # TODO: rename to provider, switch to plugins
1436- self._checkbox = CheckBox(
1437- mode=None if early_ns.checkbox == 'auto' else early_ns.checkbox)
1438- # Construct the full command line argument parser
1439- self._parser = self.construct_parser()
1440-
1441- def final_init(self, ns):
1442- """
1443- Do some final initialization just before the command gets
1444- dispatched. This is empty here but maybe useful for subclasses.
1445- """
1446-
1447- def construct_early_parser(self):
1448- """
1449- Create a parser that captures some of the early data we need to
1450- be able to have a real parser and initialize the rest.
1451- """
1452- parser = argparse.ArgumentParser(add_help=False)
1453- # Fake --help and --version
1454- parser.add_argument("-h", "--help", action="store_const", const=None)
1455- parser.add_argument("--version", action="store_const", const=None)
1456- self.add_early_parser_arguments(parser)
1457- # A catch-all net for everything else
1458- parser.add_argument("rest", nargs="...")
1459- return parser
1460-
1461- def construct_parser(self):
1462- parser = argparse.ArgumentParser(prog=self.get_exec_name())
1463- parser.add_argument(
1464- "--version", action="version", version=self.get_exec_version())
1465- # Add all the things really parsed by the early parser so that it
1466- # shows up in --help and bash tab completion.
1467- self.add_early_parser_arguments(parser)
1468- subparsers = parser.add_subparsers()
1469- self.add_subcommands(subparsers)
1470- # Enable argcomplete if it is available.
1471- try:
1472- import argcomplete
1473- except ImportError:
1474- pass
1475- else:
1476- argcomplete.autocomplete(parser)
1477- return parser
1478-
1479- def add_early_parser_arguments(self, parser):
1480- # Since we need a CheckBox instance to create the main argument parser
1481- # and we need to be able to specify where Checkbox is, we parse that
1482- # option alone before parsing everything else
1483- # TODO: rename this to -p | --provider
1484- parser.add_argument(
1485- '-c', '--checkbox',
1486- action='store',
1487- # TODO: have some public API for this, pretty please
1488- choices=list(CheckBox._DIRECTORY_MAP.keys()) + ['auto'],
1489- default='auto',
1490- help="where to find the installation of CheckBox.")
1491- group = parser.add_argument_group(
1492- title="logging and debugging")
1493- # Add the --log-level argument
1494- group.add_argument(
1495- "-l", "--log-level",
1496- action="store",
1497- choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),
1498- default=None,
1499- help=argparse.SUPPRESS)
1500- # Add the --verbose argument
1501- group.add_argument(
1502- "-v", "--verbose",
1503- dest="log_level",
1504- action="store_const",
1505- const="INFO",
1506- help="be more verbose (same as --log-level=INFO)")
1507- # Add the --debug flag
1508- group.add_argument(
1509- "-D", "--debug",
1510- dest="log_level",
1511- action="store_const",
1512- const="DEBUG",
1513- help="enable DEBUG messages on the root logger")
1514- # Add the --debug flag
1515- group.add_argument(
1516- "-C", "--debug-console",
1517- action="store_true",
1518- help="display DEBUG messages in the console")
1519- # Add the --trace flag
1520- group.add_argument(
1521- "-T", "--trace",
1522- metavar="LOGGER",
1523- action="append",
1524- default=[],
1525- help=("enable DEBUG messages on the specified logger "
1526- "(can be used multiple times)"))
1527- # Add the --pdb flag
1528- group.add_argument(
1529- "-P", "--pdb",
1530- action="store_true",
1531- default=False,
1532- help="jump into pdb (python debugger) when a command crashes")
1533- # Add the --debug-interrupt flag
1534- group.add_argument(
1535- "-I", "--debug-interrupt",
1536- action="store_true",
1537- default=False,
1538- help="crash on SIGINT/KeyboardInterrupt, useful with --pdb")
1539-
1540- def dispatch_command(self, ns):
1541- # Argh the horrror!
1542- #
1543- # Since CPython revision cab204a79e09 (landed for python3.3)
1544- # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py
1545- # the argparse module behaves differently than it did in python3.2
1546- #
1547- # In practical terms subparsers are now optional in 3.3 so all of the
1548- # commands are no longer required parameters.
1549- #
1550- # To compensate, on python3.3 and beyond, when the user just runs
1551- # plainbox without specifying the command, we manually, explicitly do
1552- # what python3.2 did: call parser.error(_('too few arguments'))
1553- if (sys.version_info[:2] >= (3, 3)
1554- and getattr(ns, "command", None) is None):
1555- self._parser.error(argparse._("too few arguments"))
1556- else:
1557- return ns.command.invoked(ns)
1558-
1559- def dispatch_and_catch_exceptions(self, ns):
1560- try:
1561- return self.dispatch_command(ns)
1562- except SystemExit:
1563- # Don't let SystemExit be caught in the logic below, we really
1564- # just want to exit when that gets thrown.
1565- logger.debug("caught SystemExit, exiting")
1566- # We may want to raise SystemExit as it can carry a status code
1567- # along and we cannot just consume that.
1568- raise
1569- except BaseException as exc:
1570- logger.debug("caught %r, deciding on what to do next", exc)
1571- # For all other exceptions (and I mean all), do a few checks
1572- # and perform actions depending on the command line arguments
1573- # By default we want to re-raise the exception
1574- action = 'raise'
1575- # We want to ignore IOErrors that are really EPIPE
1576- if isinstance(exc, IOError):
1577- if exc.errno == errno.EPIPE:
1578- action = 'ignore'
1579- # We want to ignore KeyboardInterrupt unless --debug-interrupt
1580- # was passed on command line
1581- elif isinstance(exc, KeyboardInterrupt):
1582- if ns.debug_interrupt:
1583- action = 'debug'
1584- else:
1585- action = 'ignore'
1586- else:
1587- # For all other execptions, debug if requested
1588- if ns.pdb:
1589- action = 'debug'
1590- logger.debug("action for exception %r is %s", exc, action)
1591- if action == 'ignore':
1592- return 0
1593- elif action == 'raise':
1594- logging.getLogger("plainbox.crashes").fatal(
1595- "Executable %r invoked with %r has crashed",
1596- self.get_exec_name(), ns, exc_info=1)
1597- raise
1598- elif action == 'debug':
1599- logger.error("caught runaway exception: %r", exc)
1600- logger.error("starting debugger...")
1601- pdb.post_mortem()
1602- return 1
1603+ DevCommand(self._provider, self._config).register_parser(subparsers)
1604+ ServiceCommand(self._provider, self._config).register_parser(
1605+ subparsers)
1606
1607
1608 def main(argv=None):
1609- # Another try/catch block for catching KeyboardInterrupt
1610- # This one is really only meant for the early init abort
1611- # (when someone runs main but bails out before we really
1612- # get to the point when we do something useful and setup
1613- # all the exception handlers).
1614- try:
1615- raise SystemExit(PlainBox().main(argv))
1616- except KeyboardInterrupt:
1617- pass
1618-
1619-
1620-def get_builtin_jobs():
1621- raise NotImplementedError("get_builtin_jobs() not implemented")
1622-
1623-
1624-def save(something, somewhere):
1625- raise NotImplementedError("save() not implemented")
1626-
1627-
1628-def run(*args, **kwargs):
1629- raise NotImplementedError("run() not implemented")
1630+ raise SystemExit(PlainBoxTool().main(argv))
1631
1632
1633 # Setup logging before anything else starts working.
1634
1635=== modified file 'plainbox/plainbox/impl/commands/__init__.py'
1636--- plainbox/plainbox/impl/commands/__init__.py 2013-02-25 11:02:58 +0000
1637+++ plainbox/plainbox/impl/commands/__init__.py 2013-09-13 17:12:45 +0000
1638@@ -27,11 +27,26 @@
1639 """
1640
1641 from abc import abstractmethod, ABCMeta
1642+import argparse
1643+import errno
1644+import logging
1645+import pdb
1646+import sys
1647+
1648+from plainbox.impl.logging import adjust_logging
1649+from plainbox.impl.providers.v1 import all_providers
1650+
1651+
1652+logger = logging.getLogger("plainbox.commands")
1653
1654
1655 class PlainBoxCommand(metaclass=ABCMeta):
1656 """
1657- Simple interface class for plainbox commands
1658+ Simple interface class for plainbox commands.
1659+
1660+ Command objects like this are consumed by PlainBoxTool subclasses to
1661+ implement hierarchical command system. The API supports arbitrary
1662+ many sub commands in arbitrary nesting arrangement.
1663 """
1664
1665 @abstractmethod
1666@@ -49,3 +64,309 @@
1667 command. The subparsers argument is the return value of
1668 ArgumentParser.add_subparsers()
1669 """
1670+
1671+
1672+class PlainBoxToolBase(metaclass=ABCMeta):
1673+ """
1674+ Base class for implementing commands like 'plainbox'.
1675+
1676+ The tools support a variety of sub-commands, logging and debugging
1677+ support. If argcomplete module is available and used properly in
1678+ the shell then advanced tab-completion is also available.
1679+
1680+ There are three methods to implement for a basic tool. Those are:
1681+
1682+ 1. :meth:`get_config_cls()` -- to know which config to use
1683+ 2. :meth:`get_exec_name()` -- to know how the command will be called
1684+ 3. :meth:`add_subcommands()` -- to add some actual commands to execute
1685+
1686+ This class has some complex control flow to support important
1687+ and interesting use cases. There are some concerns to people
1688+ that subclass this in order to implement their own command line tools.
1689+
1690+ The first concern is that input is parsed with two parsers, the early
1691+ parser and the full parser. The early parser quickly checks for a fraction
1692+ of supported arguments and uses that data to initialize environment
1693+ before construction of a full parser is possible. The full parser
1694+ sees the reminder of the input and does not re-parse things that where
1695+ already handled.
1696+
1697+ The second concern is that this command natively supports the concept
1698+ of a config object and a provider object. This may not be desired by
1699+ all users but it is the current state as of this writing. This means
1700+ that by the time eary init is done we have a known provider and config
1701+ objects that can be used to instantiate command objects
1702+ in :meth:`add_subcommands()`. This API might change when full
1703+ multi-provider is available but details are not known yet.
1704+ """
1705+
1706+ def __init__(self):
1707+ """
1708+ Initialize all the variables, real stuff happens in main()
1709+ """
1710+ self._early_parser = None # set in _early_init()
1711+ self._config = None # set in _late_init()
1712+ self._provider = None # set in _late_init()
1713+ self._parser = None # set in _late_init()
1714+
1715+ def main(self, argv=None):
1716+ """
1717+ Run as if invoked from command line directly
1718+ """
1719+ # Another try/catch block for catching KeyboardInterrupt
1720+ # This one is really only meant for the early init abort
1721+ # (when someone runs main but bails out before we really
1722+ # get to the point when we do something useful and setup
1723+ # all the exception handlers).
1724+ try:
1725+ self.early_init()
1726+ early_ns = self._early_parser.parse_args(argv)
1727+ self.late_init(early_ns)
1728+ logger.debug("parsed early namespace: %s", early_ns)
1729+ # parse the full command line arguments, this is also where we
1730+ # do argcomplete-dictated exit if bash shell completion
1731+ # is requested
1732+ ns = self._parser.parse_args(argv)
1733+ logger.debug("parsed full namespace: %s", ns)
1734+ self.final_init(ns)
1735+ except KeyboardInterrupt:
1736+ pass
1737+ else:
1738+ return self.dispatch_and_catch_exceptions(ns)
1739+
1740+ @classmethod
1741+ @abstractmethod
1742+ def get_config_cls(cls):
1743+ """
1744+ Get the Config class that is used by this implementation.
1745+
1746+ This can be overriden by subclasses to use a different config class
1747+ that is suitable for the particular application.
1748+ """
1749+
1750+ @classmethod
1751+ @abstractmethod
1752+ def get_exec_name(cls):
1753+ """
1754+ Get the name of this executable
1755+ """
1756+
1757+ @classmethod
1758+ @abstractmethod
1759+ def get_exec_version(cls):
1760+ """
1761+ Get the version reported by this executable
1762+ """
1763+
1764+ @abstractmethod
1765+ def add_subcommands(self, subparsers):
1766+ """
1767+ Add top-level subcommands to the argument parser.
1768+
1769+ This can be overriden by subclasses to use a different set of
1770+ top-level subcommands.
1771+ """
1772+
1773+ def early_init(self):
1774+ """
1775+ Do very early initialization. This is where we initalize stuff even
1776+ without seeing a shred of command line data or anything else.
1777+ """
1778+ self._early_parser = self.construct_early_parser()
1779+
1780+ def late_init(self, early_ns):
1781+ """
1782+ Initialize with early command line arguments being already parsed
1783+ """
1784+ adjust_logging(
1785+ level=early_ns.log_level, trace_list=early_ns.trace,
1786+ debug_console=early_ns.debug_console)
1787+ # Load plainbox configuration
1788+ self._config = self.get_config_cls().get()
1789+ # Load and initialize checkbox provider
1790+ # TODO: rename to provider, switch to plugins
1791+ all_providers.load()
1792+ # If the default value of 'None' was set for the checkbox (provider)
1793+ # argument then load the actual provider name from the configuration
1794+ # object (default for that is 'auto').
1795+ if early_ns.checkbox is None:
1796+ early_ns.checkbox = self._config.default_provider
1797+ assert early_ns.checkbox in ('auto', 'src', 'deb', 'stub', 'ihv')
1798+ if early_ns.checkbox == 'auto':
1799+ provider_name = 'checkbox-auto'
1800+ elif early_ns.checkbox == 'src':
1801+ provider_name = 'checkbox-src'
1802+ elif early_ns.checkbox == 'deb':
1803+ provider_name = 'checkbox-deb'
1804+ elif early_ns.checkbox == 'stub':
1805+ provider_name = 'stubbox'
1806+ elif early_ns.checkbox == 'ihv':
1807+ provider_name = 'ihv'
1808+ self._provider = all_providers.get_by_name(
1809+ provider_name).plugin_object()
1810+ # Construct the full command line argument parser
1811+ self._parser = self.construct_parser()
1812+
1813+ def final_init(self, ns):
1814+ """
1815+ Do some final initialization just before the command gets
1816+ dispatched. This is empty here but maybe useful for subclasses.
1817+ """
1818+
1819+ def construct_early_parser(self):
1820+ """
1821+ Create a parser that captures some of the early data we need to
1822+ be able to have a real parser and initialize the rest.
1823+ """
1824+ parser = argparse.ArgumentParser(add_help=False)
1825+ # Fake --help and --version
1826+ parser.add_argument("-h", "--help", action="store_const", const=None)
1827+ parser.add_argument("--version", action="store_const", const=None)
1828+ self.add_early_parser_arguments(parser)
1829+ # A catch-all net for everything else
1830+ parser.add_argument("rest", nargs="...")
1831+ return parser
1832+
1833+ def construct_parser(self):
1834+ parser = argparse.ArgumentParser(prog=self.get_exec_name())
1835+ parser.add_argument(
1836+ "--version", action="version", version=self.get_exec_version())
1837+ # Add all the things really parsed by the early parser so that it
1838+ # shows up in --help and bash tab completion.
1839+ self.add_early_parser_arguments(parser)
1840+ subparsers = parser.add_subparsers()
1841+ self.add_subcommands(subparsers)
1842+ # Enable argcomplete if it is available.
1843+ try:
1844+ import argcomplete
1845+ except ImportError:
1846+ pass
1847+ else:
1848+ argcomplete.autocomplete(parser)
1849+ return parser
1850+
1851+ def add_early_parser_arguments(self, parser):
1852+ # Since we need a CheckBox instance to create the main argument parser
1853+ # and we need to be able to specify where Checkbox is, we parse that
1854+ # option alone before parsing everything else
1855+ # TODO: rename this to -p | --provider
1856+ parser.add_argument(
1857+ '-c', '--checkbox',
1858+ action='store',
1859+ # TODO: have some public API for this, pretty please
1860+ choices=['src', 'deb', 'auto', 'stub', 'ihv'],
1861+ # None is a special value that means 'use whatever configured'
1862+ default=None,
1863+ help="where to find the installation of CheckBox.")
1864+ group = parser.add_argument_group(
1865+ title="logging and debugging")
1866+ # Add the --log-level argument
1867+ group.add_argument(
1868+ "-l", "--log-level",
1869+ action="store",
1870+ choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),
1871+ default=None,
1872+ help=argparse.SUPPRESS)
1873+ # Add the --verbose argument
1874+ group.add_argument(
1875+ "-v", "--verbose",
1876+ dest="log_level",
1877+ action="store_const",
1878+ const="INFO",
1879+ help="be more verbose (same as --log-level=INFO)")
1880+ # Add the --debug flag
1881+ group.add_argument(
1882+ "-D", "--debug",
1883+ dest="log_level",
1884+ action="store_const",
1885+ const="DEBUG",
1886+ help="enable DEBUG messages on the root logger")
1887+ # Add the --debug flag
1888+ group.add_argument(
1889+ "-C", "--debug-console",
1890+ action="store_true",
1891+ help="display DEBUG messages in the console")
1892+ # Add the --trace flag
1893+ group.add_argument(
1894+ "-T", "--trace",
1895+ metavar="LOGGER",
1896+ action="append",
1897+ default=[],
1898+ help=("enable DEBUG messages on the specified logger "
1899+ "(can be used multiple times)"))
1900+ # Add the --pdb flag
1901+ group.add_argument(
1902+ "-P", "--pdb",
1903+ action="store_true",
1904+ default=False,
1905+ help="jump into pdb (python debugger) when a command crashes")
1906+ # Add the --debug-interrupt flag
1907+ group.add_argument(
1908+ "-I", "--debug-interrupt",
1909+ action="store_true",
1910+ default=False,
1911+ help="crash on SIGINT/KeyboardInterrupt, useful with --pdb")
1912+
1913+ def dispatch_command(self, ns):
1914+ # Argh the horrror!
1915+ #
1916+ # Since CPython revision cab204a79e09 (landed for python3.3)
1917+ # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py
1918+ # the argparse module behaves differently than it did in python3.2
1919+ #
1920+ # In practical terms subparsers are now optional in 3.3 so all of the
1921+ # commands are no longer required parameters.
1922+ #
1923+ # To compensate, on python3.3 and beyond, when the user just runs
1924+ # plainbox without specifying the command, we manually, explicitly do
1925+ # what python3.2 did: call parser.error(_('too few arguments'))
1926+ if (sys.version_info[:2] >= (3, 3)
1927+ and getattr(ns, "command", None) is None):
1928+ self._parser.error(argparse._("too few arguments"))
1929+ else:
1930+ return ns.command.invoked(ns)
1931+
1932+ def dispatch_and_catch_exceptions(self, ns):
1933+ try:
1934+ return self.dispatch_command(ns)
1935+ except SystemExit:
1936+ # Don't let SystemExit be caught in the logic below, we really
1937+ # just want to exit when that gets thrown.
1938+ logger.debug("caught SystemExit, exiting")
1939+ # We may want to raise SystemExit as it can carry a status code
1940+ # along and we cannot just consume that.
1941+ raise
1942+ except BaseException as exc:
1943+ logger.debug("caught %r, deciding on what to do next", exc)
1944+ # For all other exceptions (and I mean all), do a few checks
1945+ # and perform actions depending on the command line arguments
1946+ # By default we want to re-raise the exception
1947+ action = 'raise'
1948+ # We want to ignore IOErrors that are really EPIPE
1949+ if isinstance(exc, IOError):
1950+ if exc.errno == errno.EPIPE:
1951+ action = 'ignore'
1952+ # We want to ignore KeyboardInterrupt unless --debug-interrupt
1953+ # was passed on command line
1954+ elif isinstance(exc, KeyboardInterrupt):
1955+ if ns.debug_interrupt:
1956+ action = 'debug'
1957+ else:
1958+ action = 'ignore'
1959+ else:
1960+ # For all other execptions, debug if requested
1961+ if ns.pdb:
1962+ action = 'debug'
1963+ logger.debug("action for exception %r is %s", exc, action)
1964+ if action == 'ignore':
1965+ return 0
1966+ elif action == 'raise':
1967+ logging.getLogger("plainbox.crashes").fatal(
1968+ "Executable %r invoked with %r has crashed",
1969+ self.get_exec_name(), ns, exc_info=1)
1970+ raise
1971+ elif action == 'debug':
1972+ logger.error("caught runaway exception: %r", exc)
1973+ logger.error("starting debugger...")
1974+ pdb.post_mortem()
1975+ return 1
1976
1977=== modified file 'plainbox/plainbox/impl/commands/analyze.py'
1978--- plainbox/plainbox/impl/commands/analyze.py 2013-06-22 06:06:21 +0000
1979+++ plainbox/plainbox/impl/commands/analyze.py 2013-09-13 17:12:45 +0000
1980@@ -31,7 +31,7 @@
1981 from plainbox.impl.commands import PlainBoxCommand
1982 from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
1983 from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
1984-from plainbox.impl.session import SessionState
1985+from plainbox.impl.session import SessionStateLegacyAPI as SessionState
1986 from plainbox.impl.runner import JobRunner
1987
1988
1989@@ -40,8 +40,8 @@
1990
1991 class AnalyzeInvocation(CheckBoxInvocationMixIn):
1992
1993- def __init__(self, checkbox, ns):
1994- super(AnalyzeInvocation, self).__init__(checkbox)
1995+ def __init__(self, provider, ns):
1996+ super(AnalyzeInvocation, self).__init__(provider)
1997 self.ns = ns
1998 self.job_list = self.get_job_list(ns)
1999 self.desired_job_list = self._get_matching_job_list(ns, self.job_list)
2000@@ -64,7 +64,7 @@
2001 with self.session.open():
2002 runner = JobRunner(
2003 self.session.session_dir, self.session.jobs_io_log_dir,
2004- command_io_delegate=self, outcome_callback=None)
2005+ command_io_delegate=self, interaction_callback=None)
2006 again = True
2007 while again:
2008 for job in self.session.run_list:
2009@@ -127,11 +127,11 @@
2010 Implementation of ``$ plainbox dev analyze``
2011 """
2012
2013- def __init__(self, checkbox):
2014- self.checkbox = checkbox
2015+ def __init__(self, provider):
2016+ self.provider = provider
2017
2018 def invoked(self, ns):
2019- return AnalyzeInvocation(self.checkbox, ns).run()
2020+ return AnalyzeInvocation(self.provider, ns).run()
2021
2022 def register_parser(self, subparsers):
2023 parser = subparsers.add_parser(
2024
2025=== modified file 'plainbox/plainbox/impl/commands/checkbox.py'
2026--- plainbox/plainbox/impl/commands/checkbox.py 2013-05-10 16:49:14 +0000
2027+++ plainbox/plainbox/impl/commands/checkbox.py 2013-09-13 17:12:45 +0000
2028@@ -32,14 +32,14 @@
2029
2030 class CheckBoxInvocationMixIn:
2031
2032- def __init__(self, checkbox):
2033- self.checkbox = checkbox
2034+ def __init__(self, provider):
2035+ self.provider = provider
2036
2037 def get_job_list(self, ns):
2038 """
2039 Load and return a list of JobDefinition instances
2040 """
2041- return self.checkbox.get_builtin_jobs()
2042+ return self.provider.get_builtin_jobs()
2043
2044 def _get_matching_job_list(self, ns, job_list):
2045 # Find jobs that matched patterns
2046@@ -47,27 +47,29 @@
2047 # Pre-seed the include pattern list with data read from
2048 # the whitelist file.
2049 if ns.whitelist:
2050- ns.include_pattern_list.extend([
2051- pattern.strip()
2052- for pattern in ns.whitelist.readlines()])
2053+ for whitelist in ns.whitelist:
2054+ ns.include_pattern_list.extend([
2055+ pattern.strip()
2056+ for pattern in whitelist.readlines()])
2057 # Decide which of the known jobs to include
2058- for job in job_list:
2059- # Reject all jobs that match any of the exclude
2060- # patterns, matching strictly from the start to
2061- # the end of the line.
2062+ if ns.exclude_pattern_list:
2063 for pattern in ns.exclude_pattern_list:
2064+ # Reject all jobs that match any of the exclude
2065+ # patterns, matching strictly from the start to
2066+ # the end of the line.
2067 regexp_pattern = r"^{pattern}$".format(pattern=pattern)
2068- if re.match(regexp_pattern, job.name):
2069- break
2070- else:
2071- # Then accept (include) all job that matches
2072+ for job in job_list:
2073+ if re.match(regexp_pattern, job.name):
2074+ job_list.remove(job)
2075+ if ns.include_pattern_list:
2076+ for pattern in ns.include_pattern_list:
2077+ # Accept (include) all job that matches
2078 # any of include patterns, matching strictly
2079 # from the start to the end of the line.
2080- for pattern in ns.include_pattern_list:
2081- regexp_pattern = r"^{pattern}$".format(pattern=pattern)
2082+ regexp_pattern = r"^{pattern}$".format(pattern=pattern)
2083+ for job in job_list:
2084 if re.match(regexp_pattern, job.name):
2085 matching_job_list.append(job)
2086- break
2087 return matching_job_list
2088
2089
2090@@ -95,6 +97,7 @@
2091 # TODO: Find a way to handle the encoding of the file
2092 group.add_argument(
2093 '-w', '--whitelist',
2094+ action="append",
2095 metavar="WHITELIST",
2096 type=FileType("rt"),
2097 help="Load whitelist containing run patterns")
2098
2099=== modified file 'plainbox/plainbox/impl/commands/dev.py'
2100--- plainbox/plainbox/impl/commands/dev.py 2013-06-22 06:06:21 +0000
2101+++ plainbox/plainbox/impl/commands/dev.py 2013-09-13 17:12:45 +0000
2102@@ -45,8 +45,8 @@
2103 Command hub for various development commands.
2104 """
2105
2106- def __init__(self, checkbox, config):
2107- self.checkbox = checkbox
2108+ def __init__(self, provider, config):
2109+ self.provider = provider
2110 self.config = config
2111
2112 def invoked(self, ns):
2113@@ -56,9 +56,9 @@
2114 parser = subparsers.add_parser(
2115 "dev", help="development commands")
2116 subdev = parser.add_subparsers()
2117- ScriptCommand(self.checkbox, self.config).register_parser(subdev)
2118- SpecialCommand(self.checkbox).register_parser(subdev)
2119- AnalyzeCommand(self.checkbox).register_parser(subdev)
2120+ ScriptCommand(self.provider, self.config).register_parser(subdev)
2121+ SpecialCommand(self.provider).register_parser(subdev)
2122+ AnalyzeCommand(self.provider).register_parser(subdev)
2123 ParseCommand().register_parser(subdev)
2124 CrashCommand().register_parser(subdev)
2125 LogTestCommand().register_parser(subdev)
2126
2127=== modified file 'plainbox/plainbox/impl/commands/run.py'
2128--- plainbox/plainbox/impl/commands/run.py 2013-06-22 06:06:21 +0000
2129+++ plainbox/plainbox/impl/commands/run.py 2013-09-13 17:12:45 +0000
2130@@ -35,17 +35,19 @@
2131
2132 from requests.exceptions import ConnectionError, InvalidSchema, HTTPError
2133
2134+from plainbox.abc import IJobResult
2135+from plainbox.impl.providers.checkbox import CheckBoxDebProvider
2136 from plainbox.impl.commands import PlainBoxCommand
2137 from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
2138 from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
2139 from plainbox.impl.depmgr import DependencyDuplicateError
2140 from plainbox.impl.exporter import ByteStringStreamTranslator
2141 from plainbox.impl.exporter import get_all_exporters
2142-from plainbox.impl.result import JobResult
2143+from plainbox.impl.result import DiskJobResult, MemoryJobResult
2144 from plainbox.impl.runner import JobRunner
2145 from plainbox.impl.runner import authenticate_warmup
2146 from plainbox.impl.runner import slugify
2147-from plainbox.impl.session import SessionState
2148+from plainbox.impl.session import SessionStateLegacyAPI as SessionState
2149 from plainbox.impl.transport import get_all_transports
2150
2151
2152@@ -54,8 +56,8 @@
2153
2154 class RunInvocation(CheckBoxInvocationMixIn):
2155
2156- def __init__(self, checkbox, ns):
2157- super(RunInvocation, self).__init__(checkbox)
2158+ def __init__(self, provider, ns):
2159+ super(RunInvocation, self).__init__(provider)
2160 self.ns = ns
2161
2162 def run(self):
2163@@ -110,25 +112,46 @@
2164 except ValueError as exc:
2165 raise SystemExit(str(exc))
2166
2167- def ask_for_resume(self, prompt=None, allowed=None):
2168- # FIXME: Add support/callbacks for a GUI
2169- if prompt is None:
2170- prompt = "Do you want to resume the previous session [Y/n]? "
2171- if allowed is None:
2172- allowed = ('', 'y', 'Y', 'n', 'N')
2173+ def ask_for_resume(self):
2174+ return self.ask_user(
2175+ "Do you want to resume the previous session?", ('y', 'n')
2176+ ).lower() == "y"
2177+
2178+ def ask_for_resume_action(self):
2179+ return self.ask_user(
2180+ "What do you want to do with that job?", ('skip', 'fail', 'run'))
2181+
2182+ def ask_user(self, prompt, allowed):
2183 answer = None
2184 while answer not in allowed:
2185- answer = input(prompt)
2186- return False if answer in ('n', 'N') else True
2187+ answer = input("{} [{}] ".format(prompt, ", ".join(allowed)))
2188+ return answer
2189+
2190+ def _maybe_skip_last_job_after_resume(self, session):
2191+ last_job = session.metadata.running_job_name
2192+ if last_job is None:
2193+ return
2194+ print("We have previously tried to execute {}".format(last_job))
2195+ action = self.ask_for_resume_action()
2196+ if action == 'skip':
2197+ result = MemoryJobResult({
2198+ 'outcome': 'skip',
2199+ 'comment': "Skipped after resuming execution"
2200+ })
2201+ elif action == 'fail':
2202+ result = MemoryJobResult({
2203+ 'outcome': 'fail',
2204+ 'comment': "Failed after resuming execution"
2205+ })
2206+ elif action == 'run':
2207+ result = None
2208+ if result:
2209+ session.update_job_result(
2210+ session.job_state_map[last_job].job, result)
2211+ session.metadata.running_job_name = None
2212+ session.persistent_save()
2213
2214 def _run_jobs(self, ns, job_list, exporter, transport=None):
2215- # Ask the password before anything else in order to run jobs requiring
2216- # privileges
2217- if self.checkbox._mode == 'deb':
2218- print("[ Authentication ]".center(80, '='))
2219- return_code = authenticate_warmup()
2220- if return_code:
2221- raise SystemExit(return_code)
2222 # Compute the run list, this can give us notification about problems in
2223 # the selected jobs. Currently we just display each problem
2224 matching_job_list = self._get_matching_job_list(ns, job_list)
2225@@ -150,18 +173,28 @@
2226 if session.previous_session_file():
2227 if self.ask_for_resume():
2228 session.resume()
2229+ self._maybe_skip_last_job_after_resume(session)
2230 else:
2231 session.clean()
2232+ session.metadata.title = " ".join(sys.argv)
2233+ session.persistent_save()
2234 self._update_desired_job_list(session, matching_job_list)
2235+ # Ask the password before anything else in order to run jobs
2236+ # requiring privileges
2237+ if self._auth_warmup_needed(session):
2238+ print("[ Authentication ]".center(80, '='))
2239+ return_code = authenticate_warmup()
2240+ if return_code:
2241+ raise SystemExit(return_code)
2242 if (sys.stdin.isatty() and sys.stdout.isatty() and not
2243 ns.not_interactive):
2244- outcome_callback = self.ask_for_outcome
2245+ interaction_callback = self._interaction_callback
2246 else:
2247- outcome_callback = None
2248+ interaction_callback = None
2249 runner = JobRunner(
2250 session.session_dir,
2251 session.jobs_io_log_dir,
2252- outcome_callback=outcome_callback,
2253+ interaction_callback=interaction_callback,
2254 dry_run=ns.dry_run
2255 )
2256 self._run_jobs_with_session(ns, session, runner)
2257@@ -189,6 +222,18 @@
2258 # FIXME: sensible return value
2259 return 0
2260
2261+ def _auth_warmup_needed(self, session):
2262+ # Don't use authentication warm-up in modes other than 'deb' as it
2263+ # makes no sense to do so.
2264+ if not isinstance(self.provider, CheckBoxDebProvider):
2265+ return False
2266+ # Don't use authentication warm-up if none of the jobs on the run list
2267+ # requires it.
2268+ if all(job.user is None for job in session.run_list):
2269+ return False
2270+ # Otherwise, do pre-authentication
2271+ return True
2272+
2273 def _save_results(self, output_file, input_stream):
2274 if output_file is sys.stdout:
2275 print("[ Results ]".center(80, '='))
2276@@ -203,18 +248,32 @@
2277 if output_file is not sys.stdout:
2278 output_file.close()
2279
2280- def ask_for_outcome(self, prompt=None, allowed=None):
2281+ def _interaction_callback(self, runner, job, config, prompt=None,
2282+ allowed_outcome=None):
2283+ result = {}
2284 if prompt is None:
2285- prompt = "what is the outcome? "
2286- if allowed is None:
2287- allowed = (JobResult.OUTCOME_PASS,
2288- JobResult.OUTCOME_FAIL,
2289- JobResult.OUTCOME_SKIP)
2290- answer = None
2291- while answer not in allowed:
2292- print("Allowed answers are: {}".format(", ".join(allowed)))
2293- answer = input(prompt)
2294- return answer
2295+ prompt = "Select an outcome or an action: "
2296+ if allowed_outcome is None:
2297+ allowed_outcome = [IJobResult.OUTCOME_PASS,
2298+ IJobResult.OUTCOME_FAIL,
2299+ IJobResult.OUTCOME_SKIP]
2300+ allowed_actions = ['comments']
2301+ if job.command:
2302+ allowed_actions.append('test')
2303+ result['outcome'] = IJobResult.OUTCOME_UNDECIDED
2304+ while result['outcome'] not in allowed_outcome:
2305+ print("Allowed answers are: {}".format(", ".join(allowed_outcome +
2306+ allowed_actions)))
2307+ choice = input(prompt)
2308+ if choice in allowed_outcome:
2309+ result['outcome'] = choice
2310+ break
2311+ elif choice == 'test':
2312+ (result['return_code'],
2313+ result['io_log_filename']) = runner._run_command(job, config)
2314+ elif choice == 'comments':
2315+ result['comments'] = input('Please enter your comments:\n')
2316+ return DiskJobResult(result)
2317
2318 def _update_desired_job_list(self, session, desired_job_list):
2319 problem_list = session.update_desired_job_list(desired_job_list)
2320@@ -224,6 +283,18 @@
2321 for problem in problem_list:
2322 print(" * {}".format(problem))
2323 print("Problematic jobs will not be considered")
2324+ (estimated_duration_auto,
2325+ estimated_duration_manual) = session.get_estimated_duration()
2326+ if estimated_duration_auto:
2327+ print("Estimated duration is {:.2f} for automated jobs.".format(
2328+ estimated_duration_auto))
2329+ else:
2330+ print("Estimated duration cannot be determined for automated jobs.")
2331+ if estimated_duration_manual:
2332+ print("Estimated duration is {:.2f} for manual jobs.".format(
2333+ estimated_duration_manual))
2334+ else:
2335+ print("Estimated duration cannot be determined for manual jobs.")
2336
2337 def _run_jobs_with_session(self, ns, session, runner):
2338 # TODO: run all resource jobs concurrently with multiprocessing
2339@@ -272,13 +343,16 @@
2340 if job_state.can_start():
2341 print("Running... (output in {}.*)".format(
2342 join(session.jobs_io_log_dir, slugify(job.name))))
2343+ session.metadata.running_job_name = job.name
2344+ session.persistent_save()
2345 job_result = runner.run_job(job)
2346+ session.metadata.running_job_name = None
2347+ session.persistent_save()
2348 print("Outcome: {}".format(job_result.outcome))
2349 print("Comments: {}".format(job_result.comments))
2350 else:
2351- job_result = JobResult({
2352- 'job': job,
2353- 'outcome': JobResult.OUTCOME_NOT_SUPPORTED,
2354+ job_result = MemoryJobResult({
2355+ 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED,
2356 'comments': job_state.get_readiness_description()
2357 })
2358 if job_result is not None:
2359@@ -287,11 +361,11 @@
2360
2361 class RunCommand(PlainBoxCommand, CheckBoxCommandMixIn):
2362
2363- def __init__(self, checkbox):
2364- self.checkbox = checkbox
2365+ def __init__(self, provider):
2366+ self.provider = provider
2367
2368 def invoked(self, ns):
2369- return RunInvocation(self.checkbox, ns).run()
2370+ return RunInvocation(self.provider, ns).run()
2371
2372 def register_parser(self, subparsers):
2373 parser = subparsers.add_parser("run", help="run a test job")
2374
2375=== modified file 'plainbox/plainbox/impl/commands/script.py'
2376--- plainbox/plainbox/impl/commands/script.py 2013-06-22 06:06:21 +0000
2377+++ plainbox/plainbox/impl/commands/script.py 2013-09-13 17:12:45 +0000
2378@@ -48,8 +48,8 @@
2379 the command is to be invoked.
2380 """
2381
2382- def __init__(self, checkbox, config, job_name):
2383- self.checkbox = checkbox
2384+ def __init__(self, provider, config, job_name):
2385+ self.provider = provider
2386 self.config = config
2387 self.job_name = job_name
2388
2389@@ -67,8 +67,10 @@
2390 bait_dir = os.path.join(scratch, 'files-created-in-current-dir')
2391 os.mkdir(bait_dir)
2392 with TestCwd(bait_dir):
2393- return_code, fjson = runner._run_command(job, self.config)
2394+ return_code, record_path = runner._run_command(
2395+ job, self.config)
2396 self._display_side_effects(scratch)
2397+ self._display_script_outcome(job, return_code)
2398 return return_code
2399
2400 def _display_file(self, pathname, origin):
2401@@ -85,9 +87,13 @@
2402 self._display_file(
2403 os.path.join(dirpath, filename), scratch)
2404
2405+ def _display_script_outcome(self, job, return_code):
2406+ print(job.name, "returned", return_code)
2407+ print("command:", job.command)
2408+
2409 def _get_job(self):
2410 job_list = get_matching_job_list(
2411- self.checkbox.get_builtin_jobs(),
2412+ self.provider.get_builtin_jobs(),
2413 NameJobQualifier(self.job_name))
2414 if len(job_list) == 0:
2415 return None
2416@@ -101,12 +107,12 @@
2417 unconditionally.
2418 """
2419
2420- def __init__(self, checkbox, config):
2421- self.checkbox = checkbox
2422+ def __init__(self, provider, config):
2423+ self.provider = provider
2424 self.config = config
2425
2426 def invoked(self, ns):
2427- return ScriptInvocation(self.checkbox, self.config, ns.job_name).run()
2428+ return ScriptInvocation(self.provider, self.config, ns.job_name).run()
2429
2430 def register_parser(self, subparsers):
2431 parser = subparsers.add_parser(
2432
2433=== added file 'plainbox/plainbox/impl/commands/service.py'
2434--- plainbox/plainbox/impl/commands/service.py 1970-01-01 00:00:00 +0000
2435+++ plainbox/plainbox/impl/commands/service.py 2013-09-13 17:12:45 +0000
2436@@ -0,0 +1,132 @@
2437+# This file is part of Checkbox.
2438+#
2439+# Copyright 2013 Canonical Ltd.
2440+# Written by:
2441+# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
2442+#
2443+# Checkbox is free software: you can redistribute it and/or modify
2444+# it under the terms of the GNU General Public License as published by
2445+# the Free Software Foundation, either version 3 of the License, or
2446+# (at your option) any later version.
2447+#
2448+# Checkbox is distributed in the hope that it will be useful,
2449+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2450+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2451+# GNU General Public License for more details.
2452+#
2453+# You should have received a copy of the GNU General Public License
2454+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
2455+
2456+"""
2457+:mod:`plainbox.impl.commands.service` -- service sub-command
2458+============================================================
2459+
2460+"""
2461+
2462+import logging
2463+import os
2464+
2465+from dbus import StarterBus, SessionBus
2466+from dbus.mainloop.glib import DBusGMainLoop, threads_init
2467+from dbus.service import BusName
2468+from gi.repository import GObject
2469+
2470+from plainbox.impl.commands import PlainBoxCommand
2471+from plainbox.impl.highlevel import Service
2472+from plainbox.impl.service import ServiceWrapper
2473+
2474+
2475+logger = logging.getLogger("plainbox.commands.service")
2476+
2477+
2478+def connect_to_session_bus():
2479+ """
2480+ Connect to the session bus properly.
2481+
2482+ Returns a tuple (session_bus, loop) where loop is a GObject.MainLoop
2483+ instance. The loop is there so that you can listen to signals.
2484+ """
2485+ # We'll need an event loop to observe signals. We will need the instance
2486+ # later below so let's keep it. Note that we're not passing it directly
2487+ # below as DBus needs specific API. The DBusGMainLoop class that we
2488+ # instantiate and pass is going to work with this instance transparently.
2489+ #
2490+ # NOTE: DBus tutorial suggests that we should create the loop _before_
2491+ # connecting to the bus.
2492+ logger.debug("Setting up glib-based event loop")
2493+ # Make sure gobject threads don't crash
2494+ GObject.threads_init()
2495+ threads_init()
2496+ loop = GObject.MainLoop()
2497+ # Let's get the system bus object.
2498+ logger.debug("Connecting to DBus session bus")
2499+ if os.getenv("DBUS_STARTER_ADDRESS"):
2500+ session_bus = StarterBus(mainloop=DBusGMainLoop())
2501+ else:
2502+ session_bus = SessionBus(mainloop=DBusGMainLoop())
2503+ return session_bus, loop
2504+
2505+
2506+class ServiceInvocation:
2507+
2508+ def __init__(self, provider, config, ns):
2509+ self.provider = provider
2510+ self.config = config
2511+ self.ns = ns
2512+
2513+ def run(self):
2514+ bus, loop = connect_to_session_bus()
2515+ logger.info("Setting up DBus objects...")
2516+ provider_list = [self.provider]
2517+ session_list = [] # TODO: load sessions
2518+ logger.debug("Constructing Service object")
2519+ service_obj = Service(provider_list, session_list)
2520+ logger.debug("Constructing ServiceWrapper")
2521+ service_wrp = ServiceWrapper(service_obj, on_exit=lambda: loop.quit())
2522+ logger.info("Publishing all objects on DBus")
2523+ service_wrp.publish_related_objects(bus)
2524+ logger.info("Publishing all managed objects (events should fire there)")
2525+ service_wrp.publish_managed_objects()
2526+ logger.debug("Attempting to claim bus name: %s", self.ns.bus_name)
2527+ bus_name = BusName(self.ns.bus_name, bus)
2528+ logger.info(
2529+ "PlainBox DBus service ready, claimed name: %s",
2530+ bus_name.get_name())
2531+ try:
2532+ loop.run()
2533+ except KeyboardInterrupt:
2534+ logger.warning((
2535+ "Main loop interrupted!"
2536+ " It is recommended to call the Exit() method on the"
2537+ " exported service object instead"))
2538+ finally:
2539+ logger.debug("Releasing %s", bus_name)
2540+ # XXX: ugly but that's how one can reliably release a bus name
2541+ del bus_name
2542+ # Remove objects from the bus
2543+ service_wrp.remove_from_connection()
2544+ logger.debug("Closing %s", bus)
2545+ bus.close()
2546+ logger.debug("Main loop terminated, exiting...")
2547+
2548+
2549+class ServiceCommand(PlainBoxCommand):
2550+ """
2551+ DBus service for PlainBox
2552+ """
2553+
2554+ # XXX: Maybe drop provider / config and handle them differently
2555+ def __init__(self, provider, config):
2556+ self.provider = provider
2557+ self.config = config
2558+
2559+ def invoked(self, ns):
2560+ return ServiceInvocation(self.provider, self.config, ns).run()
2561+
2562+ def register_parser(self, subparsers):
2563+ parser = subparsers.add_parser("service", help="spawn dbus service")
2564+ parser.add_argument(
2565+ '--bus-name', action="store",
2566+ default="com.canonical.certification.PlainBox1",
2567+ help="Use the specified DBus bus name")
2568+ parser.set_defaults(command=self)
2569
2570=== modified file 'plainbox/plainbox/impl/commands/special.py'
2571--- plainbox/plainbox/impl/commands/special.py 2013-05-13 08:49:00 +0000
2572+++ plainbox/plainbox/impl/commands/special.py 2013-09-13 17:12:45 +0000
2573@@ -38,8 +38,8 @@
2574
2575 class SpecialInvocation(CheckBoxInvocationMixIn):
2576
2577- def __init__(self, checkbox, ns):
2578- super(SpecialInvocation, self).__init__(checkbox)
2579+ def __init__(self, provider, ns):
2580+ super(SpecialInvocation, self).__init__(provider)
2581 self.ns = ns
2582
2583 def run(self):
2584@@ -124,11 +124,11 @@
2585 Implementation of ``$ plainbox special``
2586 """
2587
2588- def __init__(self, checkbox):
2589- self.checkbox = checkbox
2590+ def __init__(self, provider):
2591+ self.provider = provider
2592
2593 def invoked(self, ns):
2594- return SpecialInvocation(self.checkbox, ns).run()
2595+ return SpecialInvocation(self.provider, ns).run()
2596
2597 def register_parser(self, subparsers):
2598 parser = subparsers.add_parser(
2599
2600=== modified file 'plainbox/plainbox/impl/commands/sru.py'
2601--- plainbox/plainbox/impl/commands/sru.py 2013-05-10 16:49:15 +0000
2602+++ plainbox/plainbox/impl/commands/sru.py 2013-09-13 17:12:45 +0000
2603@@ -28,21 +28,24 @@
2604 """
2605 import logging
2606 import os
2607+import sys
2608 import tempfile
2609
2610 from requests.exceptions import ConnectionError, InvalidSchema, HTTPError
2611
2612+from plainbox.impl.applogic import WhiteList
2613 from plainbox.impl.applogic import get_matching_job_list
2614 from plainbox.impl.applogic import run_job_if_possible
2615-from plainbox.impl.checkbox import WhiteList
2616 from plainbox.impl.commands import PlainBoxCommand
2617 from plainbox.impl.commands.check_config import CheckConfigInvocation
2618+from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
2619+from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
2620 from plainbox.impl.config import ValidationError, Unset
2621 from plainbox.impl.depmgr import DependencyDuplicateError
2622 from plainbox.impl.exporter import ByteStringStreamTranslator
2623 from plainbox.impl.exporter.xml import XMLSessionStateExporter
2624 from plainbox.impl.runner import JobRunner
2625-from plainbox.impl.session import SessionState
2626+from plainbox.impl.session import SessionStateLegacyAPI as SessionState
2627 from plainbox.impl.transport.certification import CertificationTransport
2628 from plainbox.impl.transport.certification import InvalidSecureIDError
2629
2630@@ -50,20 +53,26 @@
2631 logger = logging.getLogger("plainbox.commands.sru")
2632
2633
2634-class _SRUInvocation:
2635+class _SRUInvocation(CheckBoxInvocationMixIn):
2636 """
2637 Helper class instantiated to perform a particular invocation of the sru
2638 command. Unlike the SRU command itself, this class is instantiated each
2639 time.
2640 """
2641
2642- def __init__(self, checkbox, config, ns):
2643- self.checkbox = checkbox
2644+ def __init__(self, provider, config, ns):
2645+ self.provider = provider
2646 self.config = config
2647 self.ns = ns
2648- self.whitelist = WhiteList.from_file(os.path.join(
2649- self.checkbox.whitelists_dir, "sru.whitelist"))
2650- self.job_list = self.checkbox.get_builtin_jobs()
2651+ if self.ns.whitelist:
2652+ self.whitelist = WhiteList.from_file(self.ns.whitelist[0].name)
2653+ elif self.config.whitelist is not Unset:
2654+ self.whitelist = WhiteList.from_file(self.config.whitelist)
2655+ else:
2656+ self.whitelist = WhiteList.from_file(os.path.join(
2657+ self.provider.whitelists_dir, "sru.whitelist"))
2658+
2659+ self.job_list = self.provider.get_builtin_jobs()
2660 # XXX: maybe allow specifying system_id from command line?
2661 self.exporter = XMLSessionStateExporter(system_id=None)
2662 self.session = None
2663@@ -91,7 +100,7 @@
2664 self.session.session_dir,
2665 self.session.jobs_io_log_dir,
2666 command_io_delegate=self,
2667- outcome_callback=None, # SRU runs are never interactive
2668+ interaction_callback=None, # SRU runs are never interactive
2669 dry_run=self.ns.dry_run
2670 )
2671 self._run_all_jobs()
2672@@ -178,9 +187,11 @@
2673
2674 def _run_single_job(self, job):
2675 print("- {}:".format(job.name), end=' ')
2676+ sys.stdout.flush()
2677 job_state, job_result = run_job_if_possible(
2678 self.session, self.runner, self.config, job)
2679 print("{0}".format(job_result.outcome))
2680+ sys.stdout.flush()
2681 if job_result.comments is not None:
2682 print("comments: {0}".format(job_result.comments))
2683 if job_state.readiness_inhibitor_list:
2684@@ -190,7 +201,7 @@
2685 self.session.update_job_result(job, job_result)
2686
2687
2688-class SRUCommand(PlainBoxCommand):
2689+class SRUCommand(PlainBoxCommand, CheckBoxCommandMixIn):
2690 """
2691 Command for running Stable Release Update (SRU) tests.
2692
2693@@ -204,8 +215,8 @@
2694 plainbox core on realistic workloads.
2695 """
2696
2697- def __init__(self, checkbox, config):
2698- self.checkbox = checkbox
2699+ def __init__(self, provider, config):
2700+ self.provider = provider
2701 self.config = config
2702
2703 def invoked(self, ns):
2704@@ -226,7 +237,7 @@
2705 retval = CheckConfigInvocation(self.config).run()
2706 if retval != 0:
2707 return retval
2708- return _SRUInvocation(self.checkbox, self.config, ns).run()
2709+ return _SRUInvocation(self.provider, self.config, ns).run()
2710
2711 def register_parser(self, subparsers):
2712 parser = subparsers.add_parser(
2713@@ -262,6 +273,12 @@
2714 action='store',
2715 help=("POST the test report XML to this URL"
2716 " (%(default)s)"))
2717+ group.add_argument(
2718+ '--staging',
2719+ dest='c3_url',
2720+ action='store_const',
2721+ const='https://certification.staging.canonical.com/submissions/submit/',
2722+ help='Override --destination to use the staging certification website')
2723 group = parser.add_argument_group(title="execution options")
2724 group.add_argument(
2725 '-n', '--dry-run',
2726@@ -269,3 +286,6 @@
2727 default=False,
2728 help=("Skip all usual jobs."
2729 " Only local, resource and attachment jobs are started"))
2730+ # Call enhance_parser from CheckBoxCommandMixIn
2731+ self.enhance_parser(parser)
2732+
2733
2734=== modified file 'plainbox/plainbox/impl/commands/test_dev.py'
2735--- plainbox/plainbox/impl/commands/test_dev.py 2013-06-22 06:06:21 +0000
2736+++ plainbox/plainbox/impl/commands/test_dev.py 2013-09-13 17:12:45 +0000
2737@@ -39,17 +39,17 @@
2738 def setUp(self):
2739 self.parser = argparse.ArgumentParser(prog='test')
2740 self.subparsers = self.parser.add_subparsers()
2741- self.checkbox = mock.Mock()
2742+ self.provider = mock.Mock()
2743 self.config = mock.Mock()
2744 self.ns = mock.Mock()
2745
2746 def test_init(self):
2747- dev_cmd = DevCommand(self.checkbox, self.config)
2748- self.assertIs(dev_cmd.checkbox, self.checkbox)
2749+ dev_cmd = DevCommand(self.provider, self.config)
2750+ self.assertIs(dev_cmd.provider, self.provider)
2751 self.assertIs(dev_cmd.config, self.config)
2752
2753 def test_register_parser(self):
2754- DevCommand(self.checkbox, self.config).register_parser(
2755+ DevCommand(self.provider, self.config).register_parser(
2756 self.subparsers)
2757 with TestIO() as io:
2758 self.parser.print_help()
2759
2760=== modified file 'plainbox/plainbox/impl/commands/test_run.py'
2761--- plainbox/plainbox/impl/commands/test_run.py 2013-06-22 06:06:21 +0000
2762+++ plainbox/plainbox/impl/commands/test_run.py 2013-09-13 17:12:45 +0000
2763@@ -29,11 +29,16 @@
2764 import shutil
2765 import tempfile
2766
2767+from collections import OrderedDict
2768 from inspect import cleandoc
2769 from mock import patch
2770 from unittest import TestCase
2771
2772 from plainbox.impl.box import main
2773+from plainbox.impl.exporter.json import JSONSessionStateExporter
2774+from plainbox.impl.exporter.rfc822 import RFC822SessionStateExporter
2775+from plainbox.impl.exporter.text import TextSessionStateExporter
2776+from plainbox.impl.exporter.xml import XMLSessionStateExporter
2777 from plainbox.testing_utils.io import TestIO
2778
2779
2780@@ -46,6 +51,12 @@
2781 self._sandbox = tempfile.mkdtemp()
2782 self._env = os.environ
2783 os.environ['XDG_CACHE_HOME'] = self._sandbox
2784+ self._exporters = OrderedDict([
2785+ ('json', JSONSessionStateExporter),
2786+ ('rfc822', RFC822SessionStateExporter),
2787+ ('text', TextSessionStateExporter),
2788+ ('xml', XMLSessionStateExporter),
2789+ ])
2790
2791 def test_help(self):
2792 with TestIO(combined=True) as io:
2793@@ -107,12 +118,16 @@
2794 self.assertEqual(call.exception.args, (0,))
2795 expected1 = """
2796 ===============================[ Analyzing Jobs ]===============================
2797+ Estimated duration cannot be determined for automated jobs.
2798+ Estimated duration cannot be determined for manual jobs.
2799 ==============================[ Running All Jobs ]==============================
2800 ==================================[ Results ]===================================
2801 """
2802 expected2 = """
2803 ===============================[ Authentication ]===============================
2804 ===============================[ Analyzing Jobs ]===============================
2805+ Estimated duration cannot be determined for automated jobs.
2806+ Estimated duration cannot be determined for manual jobs.
2807 ==============================[ Running All Jobs ]==============================
2808 ==================================[ Results ]===================================
2809 """
2810@@ -123,7 +138,9 @@
2811 def test_output_format_list(self):
2812 with TestIO(combined=True) as io:
2813 with self.assertRaises(SystemExit) as call:
2814- main(['run', '--output-format=?'])
2815+ with patch('plainbox.impl.commands.run.get_all_exporters') as mock_get_all_exporters:
2816+ mock_get_all_exporters.return_value = self._exporters
2817+ main(['run', '--output-format=?'])
2818 self.assertEqual(call.exception.args, (0,))
2819 expected = """
2820 Available output formats: json, rfc822, text, xml
2821@@ -133,7 +150,9 @@
2822 def test_output_option_list(self):
2823 with TestIO(combined=True) as io:
2824 with self.assertRaises(SystemExit) as call:
2825- main(['run', '--output-option=?'])
2826+ with patch('plainbox.impl.commands.run.get_all_exporters') as mock_get_all_exporters:
2827+ mock_get_all_exporters.return_value = self._exporters
2828+ main(['run', '--output-option=?'])
2829 self.assertEqual(call.exception.args, (0,))
2830 expected = """
2831 Each format may support a different set of options
2832
2833=== modified file 'plainbox/plainbox/impl/commands/test_script.py'
2834--- plainbox/plainbox/impl/commands/test_script.py 2013-06-22 06:06:21 +0000
2835+++ plainbox/plainbox/impl/commands/test_script.py 2013-09-13 17:12:45 +0000
2836@@ -32,7 +32,7 @@
2837
2838 from plainbox.impl.applogic import PlainBoxConfig
2839 from plainbox.impl.commands.script import ScriptInvocation, ScriptCommand
2840-from plainbox.impl.provider import DummyProvider1
2841+from plainbox.impl.providers.v1 import DummyProvider1
2842 from plainbox.impl.testing_utils import make_job
2843 from plainbox.testing_utils.io import TestIO
2844
2845@@ -42,17 +42,17 @@
2846 def setUp(self):
2847 self.parser = argparse.ArgumentParser(prog='test')
2848 self.subparsers = self.parser.add_subparsers()
2849- self.checkbox = mock.Mock()
2850+ self.provider = mock.Mock()
2851 self.config = mock.Mock()
2852 self.ns = mock.Mock()
2853
2854 def test_init(self):
2855- script_cmd = ScriptCommand(self.checkbox, self.config)
2856- self.assertIs(script_cmd.checkbox, self.checkbox)
2857+ script_cmd = ScriptCommand(self.provider, self.config)
2858+ self.assertIs(script_cmd.provider, self.provider)
2859 self.assertIs(script_cmd.config, self.config)
2860
2861 def test_register_parser(self):
2862- ScriptCommand(self.checkbox, self.config).register_parser(
2863+ ScriptCommand(self.provider, self.config).register_parser(
2864 self.subparsers)
2865 with TestIO() as io:
2866 self.parser.print_help()
2867@@ -75,26 +75,26 @@
2868
2869 @mock.patch("plainbox.impl.commands.script.ScriptInvocation")
2870 def test_invoked(self, patched_ScriptInvocation):
2871- retval = ScriptCommand(self.checkbox, self.config).invoked(self.ns)
2872+ retval = ScriptCommand(self.provider, self.config).invoked(self.ns)
2873 patched_ScriptInvocation.assert_called_once_with(
2874- self.checkbox, self.config, self.ns.job_name)
2875+ self.provider, self.config, self.ns.job_name)
2876 self.assertEqual(
2877 retval, patched_ScriptInvocation(
2878- self.checkbox, self.config,
2879+ self.provider, self.config,
2880 self.ns.job_name).run.return_value)
2881
2882
2883 class ScriptInvocationTests(TestCase):
2884
2885 def setUp(self):
2886- self.checkbox = mock.Mock()
2887+ self.provider = mock.Mock()
2888 self.config = PlainBoxConfig()
2889 self.job_name = mock.Mock()
2890
2891 def test_init(self):
2892 script_inv = ScriptInvocation(
2893- self.checkbox, self.config, self.job_name)
2894- self.assertIs(script_inv.checkbox, self.checkbox)
2895+ self.provider, self.config, self.job_name)
2896+ self.assertIs(script_inv.provider, self.provider)
2897 self.assertIs(script_inv.config, self.config)
2898 self.assertIs(script_inv.job_name, self.job_name)
2899
2900@@ -124,22 +124,27 @@
2901 self.assertEqual(retval, 125)
2902
2903 def test_job_with_command(self):
2904+ dummy_name = 'foo'
2905+ dummy_command = 'echo ok'
2906 provider = DummyProvider1([
2907- make_job('foo', command='echo ok')])
2908- script_inv = ScriptInvocation(provider, self.config, 'foo')
2909+ make_job(dummy_name, command=dummy_command)])
2910+ script_inv = ScriptInvocation(provider, self.config, dummy_name)
2911 with TestIO() as io:
2912 retval = script_inv.run()
2913 self.assertEqual(
2914 io.stdout, cleandoc(
2915 """
2916 (job foo, <stdout:00001>) ok
2917- """) + '\n')
2918+ """) + '\n' + "{} returned 0\n".format(dummy_name) +
2919+ "command: {}\n".format(dummy_command))
2920 self.assertEqual(retval, 0)
2921
2922 def test_job_with_command_making_files(self):
2923+ dummy_name = 'foo'
2924+ dummy_command = 'echo ok > file'
2925 provider = DummyProvider1([
2926- make_job('foo', command='echo ok > file')])
2927- script_inv = ScriptInvocation(provider, self.config, 'foo')
2928+ make_job(dummy_name, command=dummy_command)])
2929+ script_inv = ScriptInvocation(provider, self.config, dummy_name)
2930 with TestIO() as io:
2931 retval = script_inv.run()
2932 self.maxDiff = None
2933@@ -148,5 +153,6 @@
2934 """
2935 Leftover file detected: 'files-created-in-current-dir/file':
2936 files-created-in-current-dir/file:1: ok
2937- """) + '\n')
2938+ """) + '\n' + "{} returned 0\n".format(dummy_name) +
2939+ "command: {}\n".format(dummy_command))
2940 self.assertEqual(retval, 0)
2941
2942=== modified file 'plainbox/plainbox/impl/commands/test_sru.py'
2943--- plainbox/plainbox/impl/commands/test_sru.py 2013-04-24 17:50:58 +0000
2944+++ plainbox/plainbox/impl/commands/test_sru.py 2013-09-13 17:12:45 +0000
2945@@ -41,7 +41,8 @@
2946 self.maxDiff = None
2947 expected = """
2948 usage: plainbox sru [-h] [--check-config] --secure-id SECURE-ID
2949- [--fallback FILE] [--destination URL] [-n]
2950+ [--fallback FILE] [--destination URL] [--staging] [-n]
2951+ [-i PATTERN] [-x PATTERN] [-w WHITELIST]
2952
2953 optional arguments:
2954 -h, --help show this help message and exit
2955@@ -55,9 +56,21 @@
2956 (unset)
2957 --destination URL POST the test report XML to this URL (https://certific
2958 ation.canonical.com/submissions/submit/)
2959+ --staging Override --destination to use the staging
2960+ certification website
2961
2962 execution options:
2963 -n, --dry-run Skip all usual jobs. Only local, resource and
2964 attachment jobs are started
2965+
2966+ job definition options:
2967+ -i PATTERN, --include-pattern PATTERN
2968+ Run jobs matching the given regular expression.
2969+ Matches from the start to the end of the line.
2970+ -x PATTERN, --exclude-pattern PATTERN
2971+ Do not run jobs matching the given regular expression.
2972+ Matches from the start to the end of the line.
2973+ -w WHITELIST, --whitelist WHITELIST
2974+ Load whitelist containing run patterns
2975 """
2976 self.assertEqual(io.combined, cleandoc(expected) + "\n")
2977
2978=== modified file 'plainbox/plainbox/impl/config.py'
2979--- plainbox/plainbox/impl/config.py 2013-06-22 06:06:21 +0000
2980+++ plainbox/plainbox/impl/config.py 2013-09-13 17:12:45 +0000
2981@@ -543,3 +543,16 @@
2982 def __call__(self, variable, new_value):
2983 if not self.pattern.match(new_value):
2984 return "does not match pattern: {!r}".format(self.pattern_text)
2985+
2986+
2987+class ChoiceValidator(IValidator):
2988+ """
2989+ A validator ensuring that values are in a given set
2990+ """
2991+
2992+ def __init__(self, choice_list):
2993+ self.choice_list = choice_list
2994+
2995+ def __call__(self, variable, new_value):
2996+ if new_value not in self.choice_list:
2997+ return "must be one of {}".format(", ".join(self.choice_list))
2998
2999=== added directory 'plainbox/plainbox/impl/dbus'
3000=== added file 'plainbox/plainbox/impl/dbus/__init__.py'
3001--- plainbox/plainbox/impl/dbus/__init__.py 1970-01-01 00:00:00 +0000
3002+++ plainbox/plainbox/impl/dbus/__init__.py 2013-09-13 17:12:45 +0000
3003@@ -0,0 +1,47 @@
3004+# This file is part of Checkbox.
3005+#
3006+# Copyright 2013 Canonical Ltd.
3007+# Written by:
3008+# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
3009+#
3010+# Checkbox is free software: you can redistribute it and/or modify
3011+# it under the terms of the GNU General Public License as published by
3012+# the Free Software Foundation, either version 3 of the License, or
3013+# (at your option) any later version.
3014+#
3015+# Checkbox is distributed in the hope that it will be useful,
3016+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3017+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3018+# GNU General Public License for more details.
3019+#
3020+# You should have received a copy of the GNU General Public License
3021+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3022+
3023+"""
3024+:mod:`plainbox.impl.dbus` -- DBus support code for PlainBox
3025+===========================================================
3026+"""
3027+
3028+__all__ = [
3029+ 'service',
3030+ 'exceptions',
3031+ 'Signature',
3032+ 'Struct',
3033+ 'types',
3034+ 'INTROSPECTABLE_IFACE',
3035+ 'PEER_IFACE',
3036+ 'PROPERTIES_IFACE',
3037+ 'OBJECT_MANAGER_IFACE',
3038+]
3039+
3040+from dbus import INTROSPECTABLE_IFACE
3041+from dbus import PEER_IFACE
3042+from dbus import PROPERTIES_IFACE
3043+from dbus import Signature
3044+from dbus import Struct
3045+from dbus import exceptions
3046+from dbus import types
3047+
3048+OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
3049+
3050+from plainbox.impl.dbus import service
3051
3052=== added file 'plainbox/plainbox/impl/dbus/decorators.py'
3053--- plainbox/plainbox/impl/dbus/decorators.py 1970-01-01 00:00:00 +0000
3054+++ plainbox/plainbox/impl/dbus/decorators.py 2013-09-13 17:12:45 +0000
3055@@ -0,0 +1,351 @@
3056+"""Service-side D-Bus decorators."""
3057+
3058+# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/>
3059+# Copyright (C) 2003 David Zeuthen
3060+# Copyright (C) 2004 Rob Taylor
3061+# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/>
3062+#
3063+# Permission is hereby granted, free of charge, to any person
3064+# obtaining a copy of this software and associated documentation
3065+# files (the "Software"), to deal in the Software without
3066+# restriction, including without limitation the rights to use, copy,
3067+# modify, merge, publish, distribute, sublicense, and/or sell copies
3068+# of the Software, and to permit persons to whom the Software is
3069+# furnished to do so, subject to the following conditions:
3070+#
3071+# The above copyright notice and this permission notice shall be
3072+# included in all copies or substantial portions of the Software.
3073+#
3074+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
3075+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
3076+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
3077+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
3078+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
3079+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3080+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
3081+# DEALINGS IN THE SOFTWARE.
3082+
3083+__all__ = ('method', 'signal')
3084+__docformat__ = 'restructuredtext'
3085+
3086+import inspect
3087+
3088+from dbus import validate_interface_name, Signature, validate_member_name
3089+from dbus.lowlevel import SignalMessage
3090+from dbus.exceptions import DBusException
3091+from dbus._compat import is_py2
3092+
3093+
3094+def method(dbus_interface, in_signature=None, out_signature=None,
3095+ async_callbacks=None,
3096+ sender_keyword=None, path_keyword=None, destination_keyword=None,
3097+ message_keyword=None, connection_keyword=None,
3098+ byte_arrays=False,
3099+ rel_path_keyword=None, **kwargs):
3100+ """Factory for decorators used to mark methods of a `dbus.service.Object`
3101+ to be exported on the D-Bus.
3102+
3103+ The decorated method will be exported over D-Bus as the method of the
3104+ same name on the given D-Bus interface.
3105+
3106+ :Parameters:
3107+ `dbus_interface` : str
3108+ Name of a D-Bus interface
3109+ `in_signature` : str or None
3110+ If not None, the signature of the method parameters in the usual
3111+ D-Bus notation
3112+ `out_signature` : str or None
3113+ If not None, the signature of the return value in the usual
3114+ D-Bus notation
3115+ `async_callbacks` : tuple containing (str,str), or None
3116+ If None (default) the decorated method is expected to return
3117+ values matching the `out_signature` as usual, or raise
3118+ an exception on error. If not None, the following applies:
3119+
3120+ `async_callbacks` contains the names of two keyword arguments to
3121+ the decorated function, which will be used to provide a success
3122+ callback and an error callback (in that order).
3123+
3124+ When the decorated method is called via the D-Bus, its normal
3125+ return value will be ignored; instead, a pair of callbacks are
3126+ passed as keyword arguments, and the decorated method is
3127+ expected to arrange for one of them to be called.
3128+
3129+ On success the success callback must be called, passing the
3130+ results of this method as positional parameters in the format
3131+ given by the `out_signature`.
3132+
3133+ On error the decorated method may either raise an exception
3134+ before it returns, or arrange for the error callback to be
3135+ called with an Exception instance as parameter.
3136+
3137+ `sender_keyword` : str or None
3138+ If not None, contains the name of a keyword argument to the
3139+ decorated function, conventionally ``'sender'``. When the
3140+ method is called, the sender's unique name will be passed as
3141+ this keyword argument.
3142+
3143+ `path_keyword` : str or None
3144+ If not None (the default), the decorated method will receive
3145+ the destination object path as a keyword argument with this
3146+ name. Normally you already know the object path, but in the
3147+ case of "fallback paths" you'll usually want to use the object
3148+ path in the method's implementation.
3149+
3150+ For fallback objects, `rel_path_keyword` (new in 0.82.2) is
3151+ likely to be more useful.
3152+
3153+ :Since: 0.80.0?
3154+
3155+ `rel_path_keyword` : str or None
3156+ If not None (the default), the decorated method will receive
3157+ the destination object path, relative to the path at which the
3158+ object was exported, as a keyword argument with this
3159+ name. For non-fallback objects the relative path will always be
3160+ '/'.
3161+
3162+ :Since: 0.82.2
3163+
3164+ `destination_keyword` : str or None
3165+ If not None (the default), the decorated method will receive
3166+ the destination bus name as a keyword argument with this name.
3167+ Included for completeness - you shouldn't need this.
3168+
3169+ :Since: 0.80.0?
3170+
3171+ `message_keyword` : str or None
3172+ If not None (the default), the decorated method will receive
3173+ the `dbus.lowlevel.MethodCallMessage` as a keyword argument
3174+ with this name.
3175+
3176+ :Since: 0.80.0?
3177+
3178+ `connection_keyword` : str or None
3179+ If not None (the default), the decorated method will receive
3180+ the `dbus.connection.Connection` as a keyword argument
3181+ with this name. This is generally only useful for objects
3182+ that are available on more than one connection.
3183+
3184+ :Since: 0.82.0
3185+
3186+ `utf8_strings` : bool
3187+ If False (default), D-Bus strings are passed to the decorated
3188+ method as objects of class dbus.String, a unicode subclass.
3189+
3190+ If True, D-Bus strings are passed to the decorated method
3191+ as objects of class dbus.UTF8String, a str subclass guaranteed
3192+ to be encoded in UTF-8.
3193+
3194+ This option does not affect object-paths and signatures, which
3195+ are always 8-bit strings (str subclass) encoded in ASCII.
3196+
3197+ :Since: 0.80.0
3198+
3199+ `byte_arrays` : bool
3200+ If False (default), a byte array will be passed to the decorated
3201+ method as an `Array` (a list subclass) of `Byte` objects.
3202+
3203+ If True, a byte array will be passed to the decorated method as
3204+ a `ByteArray`, a str subclass. This is usually what you want,
3205+ but is switched off by default to keep dbus-python's API
3206+ consistent.
3207+
3208+ :Since: 0.80.0
3209+ """
3210+ validate_interface_name(dbus_interface)
3211+
3212+ def decorator(func):
3213+ # If the function is decorated and uses @functools.wrapper then use the
3214+ # __wrapped__ attribute to look at the original function signature.
3215+ #
3216+ # This allows us to see past the generic *args, **kwargs seen on most decorators.
3217+ if hasattr(func, '__wrapped__'):
3218+ args = inspect.getfullargspec(func.__wrapped__)[0]
3219+ else:
3220+ args = inspect.getfullargspec(func)[0]
3221+ args.pop(0)
3222+ if async_callbacks:
3223+ if type(async_callbacks) != tuple:
3224+ raise TypeError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)')
3225+ if len(async_callbacks) != 2:
3226+ raise ValueError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)')
3227+ args.remove(async_callbacks[0])
3228+ args.remove(async_callbacks[1])
3229+
3230+ if sender_keyword:
3231+ args.remove(sender_keyword)
3232+ if rel_path_keyword:
3233+ args.remove(rel_path_keyword)
3234+ if path_keyword:
3235+ args.remove(path_keyword)
3236+ if destination_keyword:
3237+ args.remove(destination_keyword)
3238+ if message_keyword:
3239+ args.remove(message_keyword)
3240+ if connection_keyword:
3241+ args.remove(connection_keyword)
3242+
3243+ if in_signature:
3244+ in_sig = tuple(Signature(in_signature))
3245+
3246+ if len(in_sig) > len(args):
3247+ raise ValueError('input signature is longer than the number of arguments taken')
3248+ elif len(in_sig) < len(args):
3249+ raise ValueError('input signature is shorter than the number of arguments taken')
3250+
3251+ func._dbus_is_method = True
3252+ func._dbus_async_callbacks = async_callbacks
3253+ func._dbus_interface = dbus_interface
3254+ func._dbus_in_signature = in_signature
3255+ func._dbus_out_signature = out_signature
3256+ func._dbus_sender_keyword = sender_keyword
3257+ func._dbus_path_keyword = path_keyword
3258+ func._dbus_rel_path_keyword = rel_path_keyword
3259+ func._dbus_destination_keyword = destination_keyword
3260+ func._dbus_message_keyword = message_keyword
3261+ func._dbus_connection_keyword = connection_keyword
3262+ func._dbus_args = args
3263+ func._dbus_get_args_options = dict(byte_arrays=byte_arrays)
3264+ if is_py2:
3265+ func._dbus_get_args_options['utf8_strings'] = kwargs.get(
3266+ 'utf8_strings', False)
3267+ elif 'utf8_strings' in kwargs:
3268+ raise TypeError("unexpected keyword argument 'utf8_strings'")
3269+ return func
3270+
3271+ return decorator
3272+
3273+
3274+def signal(dbus_interface, signature=None, path_keyword=None,
3275+ rel_path_keyword=None):
3276+ """Factory for decorators used to mark methods of a `dbus.service.Object`
3277+ to emit signals on the D-Bus.
3278+
3279+ Whenever the decorated method is called in Python, after the method
3280+ body is executed, a signal with the same name as the decorated method,
3281+ with the given D-Bus interface, will be emitted from this object.
3282+
3283+ :Parameters:
3284+ `dbus_interface` : str
3285+ The D-Bus interface whose signal is emitted
3286+ `signature` : str
3287+ The signature of the signal in the usual D-Bus notation
3288+
3289+ `path_keyword` : str or None
3290+ A keyword argument to the decorated method. If not None,
3291+ that argument will not be emitted as an argument of
3292+ the signal, and when the signal is emitted, it will appear
3293+ to come from the object path given by the keyword argument.
3294+
3295+ Note that when calling the decorated method, you must always
3296+ pass in the object path as a keyword argument, not as a
3297+ positional argument.
3298+
3299+ This keyword argument cannot be used on objects where
3300+ the class attribute ``SUPPORTS_MULTIPLE_OBJECT_PATHS`` is true.
3301+
3302+ :Deprecated: since 0.82.0. Use `rel_path_keyword` instead.
3303+
3304+ `rel_path_keyword` : str or None
3305+ A keyword argument to the decorated method. If not None,
3306+ that argument will not be emitted as an argument of
3307+ the signal.
3308+
3309+ When the signal is emitted, if the named keyword argument is given,
3310+ the signal will appear to come from the object path obtained by
3311+ appending the keyword argument to the object's object path.
3312+ This is useful to implement "fallback objects" (objects which
3313+ own an entire subtree of the object-path tree).
3314+
3315+ If the object is available at more than one object-path on the
3316+ same or different connections, the signal will be emitted at
3317+ an appropriate object-path on each connection - for instance,
3318+ if the object is exported at /abc on connection 1 and at
3319+ /def and /x/y/z on connection 2, and the keyword argument is
3320+ /foo, then signals will be emitted from /abc/foo and /def/foo
3321+ on connection 1, and /x/y/z/foo on connection 2.
3322+
3323+ :Since: 0.82.0
3324+ """
3325+ validate_interface_name(dbus_interface)
3326+
3327+ if path_keyword is not None:
3328+ from warnings import warn
3329+ warn(DeprecationWarning('dbus.service.signal::path_keyword has been '
3330+ 'deprecated since dbus-python 0.82.0, and '
3331+ 'will not work on objects that support '
3332+ 'multiple object paths'),
3333+ DeprecationWarning, stacklevel=2)
3334+ if rel_path_keyword is not None:
3335+ raise TypeError('dbus.service.signal::path_keyword and '
3336+ 'rel_path_keyword cannot both be used')
3337+
3338+ def decorator(func):
3339+ member_name = func.__name__
3340+ validate_member_name(member_name)
3341+
3342+ def emit_signal(self, *args, **keywords):
3343+ abs_path = None
3344+ if path_keyword is not None:
3345+ if self.SUPPORTS_MULTIPLE_OBJECT_PATHS:
3346+ raise TypeError('path_keyword cannot be used on the '
3347+ 'signals of an object that supports '
3348+ 'multiple object paths')
3349+ abs_path = keywords.pop(path_keyword, None)
3350+ if (abs_path != self.__dbus_object_path__ and
3351+ not self.__dbus_object_path__.startswith(abs_path + '/')):
3352+ raise ValueError('Path %r is not below %r', abs_path,
3353+ self.__dbus_object_path__)
3354+
3355+ rel_path = None
3356+ if rel_path_keyword is not None:
3357+ rel_path = keywords.pop(rel_path_keyword, None)
3358+
3359+ func(self, *args, **keywords)
3360+
3361+ for location in self.locations:
3362+ if abs_path is None:
3363+ # non-deprecated case
3364+ if rel_path is None or rel_path in ('/', ''):
3365+ object_path = location[1]
3366+ else:
3367+ # will be validated by SignalMessage ctor in a moment
3368+ object_path = location[1] + rel_path
3369+ else:
3370+ object_path = abs_path
3371+
3372+ message = SignalMessage(object_path,
3373+ dbus_interface,
3374+ member_name)
3375+ message.append(signature=signature, *args)
3376+
3377+ location[0].send_message(message)
3378+ # end emit_signal
3379+
3380+ args = inspect.getargspec(func)[0]
3381+ args.pop(0)
3382+
3383+ for keyword in rel_path_keyword, path_keyword:
3384+ if keyword is not None:
3385+ try:
3386+ args.remove(keyword)
3387+ except ValueError:
3388+ raise ValueError('function has no argument "%s"' % keyword)
3389+
3390+ if signature:
3391+ sig = tuple(Signature(signature))
3392+
3393+ if len(sig) > len(args):
3394+ raise ValueError('signal signature is longer than the number of arguments provided')
3395+ elif len(sig) < len(args):
3396+ raise ValueError('signal signature is shorter than the number of arguments provided')
3397+
3398+ emit_signal.__name__ = func.__name__
3399+ emit_signal.__doc__ = func.__doc__
3400+ emit_signal._dbus_is_signal = True
3401+ emit_signal._dbus_interface = dbus_interface
3402+ emit_signal._dbus_signature = signature
3403+ emit_signal._dbus_args = args
3404+ return emit_signal
3405+
3406+ return decorator
3407
3408=== added file 'plainbox/plainbox/impl/dbus/service.py'
3409--- plainbox/plainbox/impl/dbus/service.py 1970-01-01 00:00:00 +0000
3410+++ plainbox/plainbox/impl/dbus/service.py 2013-09-13 17:12:45 +0000
3411@@ -0,0 +1,662 @@
3412+# This file is part of Checkbox.
3413+#
3414+# Copyright 2013 Canonical Ltd.
3415+# Written by:
3416+# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
3417+#
3418+# Checkbox is free software: you can redistribute it and/or modify
3419+# it under the terms of the GNU General Public License as published by
3420+# the Free Software Foundation, either version 3 of the License, or
3421+# (at your option) any later version.
3422+#
3423+# Checkbox is distributed in the hope that it will be useful,
3424+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3425+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3426+# GNU General Public License for more details.
3427+#
3428+# You should have received a copy of the GNU General Public License
3429+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
3430+
3431+"""
3432+:mod:`plainbox.impl.dbus.service` -- DBus Service support code for PlainBox
3433+===========================================================================
3434+"""
3435+
3436+import logging
3437+import threading
3438+import weakref
3439+
3440+import _dbus_bindings
3441+import dbus
3442+import dbus.service
3443+import dbus.exceptions
3444+
3445+from plainbox.impl.signal import Signal
3446+from plainbox.impl.dbus import INTROSPECTABLE_IFACE
3447+from plainbox.impl.dbus import OBJECT_MANAGER_IFACE
3448+from plainbox.impl.dbus import PROPERTIES_IFACE
3449+# Note: use our own version of the decorators because
3450+# vanilla versions choke on annotations
3451+from plainbox.impl.dbus.decorators import method, signal
3452+
3453+
3454+# This is the good old standard python property decorator
3455+_property = property
3456+
3457+__all__ = [
3458+ 'Interface',
3459+ 'Object',
3460+ 'method',
3461+ 'property',
3462+ 'signal',
3463+]
3464+
3465+logger = logging.getLogger("plainbox.dbus")
3466+
3467+
3468+class InterfaceType(dbus.service.InterfaceType):
3469+ """
3470+ Subclass of :class:`dbus.service.InterfaceType` that also handles
3471+ properties.
3472+ """
3473+
3474+ def _reflect_on_property(cls, func):
3475+ reflection_data = (
3476+ ' <property name="{}" type="{}" access="{}"/>\n').format(
3477+ func._dbus_property, func._signature,
3478+ func.dbus_access_flag)
3479+ return reflection_data
3480+
3481+
3482+#Subclass of :class:`dbus.service.Interface` that also handles properties
3483+Interface = InterfaceType('Interface', (dbus.service.Interface,), {})
3484+
3485+
3486+class property:
3487+ """
3488+ property that handles DBus stuff
3489+ """
3490+
3491+ def __init__(self, signature, dbus_interface, dbus_property=None,
3492+ setter=False):
3493+ """
3494+ Initialize new dbus_property with the given interface name.
3495+
3496+ If dbus_property is not specified it is set to the name of the
3497+ decorated method. In special circumstances you may wish to specify
3498+ alternate dbus property name explicitly.
3499+
3500+ If setter is set to True then the implicit decorated function is a
3501+ setter, not the default getter. This allows to define write-only
3502+ properties.
3503+ """
3504+ self.__name__ = None
3505+ self.__doc__ = None
3506+ self._signature = signature
3507+ self._dbus_interface = dbus_interface
3508+ self._dbus_property = dbus_property
3509+ self._getf = None
3510+ self._setf = None
3511+ self._implicit_setter = setter
3512+
3513+ def __repr__(self):
3514+ return "<dbus.service.property {!r}>".format(self.__name__)
3515+
3516+ @_property
3517+ def dbus_access_flag(self):
3518+ """
3519+ access flag of this DBus property
3520+
3521+ :returns: either "readwrite", "read" or "write"
3522+ :raises TypeError: if the property is ill-defined
3523+ """
3524+ if self._getf and self._setf:
3525+ return "readwrite"
3526+ elif self._getf:
3527+ return "read"
3528+ elif self._setf:
3529+ return "write"
3530+ else:
3531+ raise TypeError(
3532+ "property provides neither readable nor writable")
3533+
3534+ @_property
3535+ def dbus_interface(self):
3536+ """
3537+ name of the DBus interface of this DBus property
3538+ """
3539+ return self._dbus_interface
3540+
3541+ @_property
3542+ def dbus_property(self):
3543+ """
3544+ name of this DBus property
3545+ """
3546+ return self._dbus_property
3547+
3548+ @_property
3549+ def signature(self):
3550+ """
3551+ signature of this DBus property
3552+ """
3553+ return self._signature
3554+
3555+ @_property
3556+ def setter(self):
3557+ """
3558+ decorator for setter functions
3559+
3560+ This property can be used to decorate additional method that
3561+ will be used as a property setter. Otherwise properties cannot
3562+ be assigned.
3563+ """
3564+ def decorator(func):
3565+ self._setf = func
3566+ return self
3567+ return decorator
3568+
3569+ @_property
3570+ def getter(self):
3571+ """
3572+ decorator for getter functions
3573+
3574+ This property can be used to decorate additional method that
3575+ will be used as a property getter. It is only provider for parity
3576+ as by default, the @dbus.service.property() decorator designates
3577+ a getter function. This behavior can be controlled by passing
3578+ setter=True to the property initializer.
3579+ """
3580+ def decorator(func):
3581+ self._getf = func
3582+ return self
3583+ return decorator
3584+
3585+ def __call__(self, func):
3586+ """
3587+ Decorate a getter function and return the property object
3588+
3589+ This method sets __name__, __doc__ and _dbus_property.
3590+ """
3591+ self.__name__ = func.__name__
3592+ if self.__doc__ is None:
3593+ self.__doc__ = func.__doc__
3594+ if self._dbus_property is None:
3595+ self._dbus_property = func.__name__
3596+ if self._implicit_setter:
3597+ return self.setter(func)
3598+ else:
3599+ return self.getter(func)
3600+
3601+ def __get__(self, instance, owner):
3602+ if instance is None:
3603+ return self
3604+ else:
3605+ if self._getf is None:
3606+ raise dbus.exceptions.DBusException(
3607+ "property is not readable")
3608+ return self._getf(instance)
3609+
3610+ def __set__(self, instance, value):
3611+ if self._setf is None:
3612+ raise dbus.exceptions.DBusException(
3613+ "property is not writable")
3614+ self._setf(instance, value)
3615+
3616+ # This little helper is here is to help :meth:`Object.Introspect()`
3617+ # figure out how to handle properties.
3618+ _dbus_is_property = True
3619+
3620+
3621+class Object(Interface, dbus.service.Object):
3622+ """
3623+ dbus.service.Object subclass that providers additional features.
3624+
3625+ This class providers the following additional features:
3626+
3627+ * Implementation of the PROPERTIES_IFACE. This includes the methods
3628+ Get(), Set(), GetAll() and the signal PropertiesChanged()
3629+
3630+ * Implementation of the OBJECT_MANAGER_IFACE. This includes the method
3631+ GetManagedObjects() and signals InterfacesAdded() and
3632+ InterfacesRemoved().
3633+
3634+ * Tracking of object-path-to-object association using the new
3635+ :meth:`find_object_by_path()` method
3636+
3637+ * Selective activation of any of the above interfaces using
3638+ :meth:`should_expose_interface()` method.
3639+
3640+ * Improved version of the INTROSPECTABLE_IFACE that understands properties
3641+ """
3642+
3643+ def __init__(self, conn=None, object_path=None, bus_name=None):
3644+ dbus.service.Object.__init__(self, conn, object_path, bus_name)
3645+ self._managed_object_list = []
3646+
3647+ # [ Public DBus methods of the INTROSPECTABLE_IFACE interface ]
3648+
3649+ @method(
3650+ dbus_interface=INTROSPECTABLE_IFACE,
3651+ in_signature='', out_signature='s',
3652+ path_keyword='object_path', connection_keyword='connection')
3653+ def Introspect(self, object_path, connection):
3654+ """
3655+ Return a string of XML encoding this object's supported interfaces,
3656+ methods and signals.
3657+ """
3658+ logger.debug("Introspect(object_path=%r)", object_path)
3659+ reflection_data = (
3660+ _dbus_bindings.DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE)
3661+ reflection_data += '<node name="%s">\n' % object_path
3662+ interfaces = self._dct_entry
3663+ for (name, funcs) in interfaces.items():
3664+ # Allow classes to ignore certain interfaces This is useful because
3665+ # this class implements all kinds of methods internally (for
3666+ # simplicity) but does not really advertise them all directly
3667+ # unless asked to.
3668+ if not self.should_expose_interface(name):
3669+ continue
3670+ reflection_data += ' <interface name="%s">\n' % (name)
3671+ for func in funcs.values():
3672+ if getattr(func, '_dbus_is_method', False):
3673+ reflection_data += self.__class__._reflect_on_method(func)
3674+ elif getattr(func, '_dbus_is_signal', False):
3675+ reflection_data += self.__class__._reflect_on_signal(func)
3676+ elif getattr(func, '_dbus_is_property', False):
3677+ reflection_data += (
3678+ self.__class__._reflect_on_property(func))
3679+ reflection_data += ' </interface>\n'
3680+ for name in connection.list_exported_child_objects(object_path):
3681+ reflection_data += ' <node name="%s"/>\n' % name
3682+ reflection_data += '</node>\n'
3683+ logger.debug("Introspect() returns: %s", reflection_data)
3684+ return reflection_data
3685+
3686+ # [ Public DBus methods of the PROPERTIES_IFACE interface ]
3687+
3688+ @dbus.service.method(
3689+ dbus_interface=dbus.PROPERTIES_IFACE,
3690+ in_signature="ss", out_signature="v")
3691+ def Get(self, interface_name, property_name):
3692+ """
3693+ Get the value of a property @property_name on interface
3694+ @interface_name.
3695+ """
3696+ logger.debug(
3697+ "%r.Get(%r, %r) -> ...",
3698+ self, interface_name, property_name)
3699+ try:
3700+ props = self._dct_entry[interface_name]
3701+ except KeyError:
3702+ raise dbus.exceptions.DBusException(
3703+ dbus.PROPERTIES_IFACE,
3704+ "No such interface {}".format(interface_name))
3705+ try:
3706+ prop = props[property_name]
3707+ except KeyError:
3708+ raise dbus.exceptions.DBusException(
3709+ dbus.PROPERTIES_IFACE,
3710+ "No such property {}:{}".format(
3711+ interface_name, property_name))
3712+ try:
3713+ value = prop.__get__(self, self.__class__)
3714+ except dbus.exceptions.DBusException:
3715+ raise
3716+ except Exception as exc:
3717+ logger.exception(
3718+ "runaway exception from Get(%r, %r)",
3719+ interface_name, property_name)
3720+ raise dbus.exceptions.DBusException(
3721+ dbus.PROPERTIES_IFACE,
3722+ "Unable to get property interface/property {}:{}: {!r}".format(
3723+ interface_name, property_name, exc))
3724+ else:
3725+ logger.debug(
3726+ "%r.Get(%r, %r) -> %r",
3727+ self, interface_name, property_name, value)
3728+ return value
3729+
3730+ @dbus.service.method(
3731+ dbus_interface=dbus.PROPERTIES_IFACE,
3732+ in_signature="ssv", out_signature="")
3733+ def Set(self, interface_name, property_name, value):
3734+ logger.debug(
3735+ "%r.Set(%r, %r, %r) -> ...",
3736+ self, interface_name, property_name, value)
3737+ try:
3738+ props = self._dct_entry[interface_name]
3739+ except KeyError:
3740+ raise dbus.exceptions.DBusException(
3741+ dbus.PROPERTIES_IFACE,
3742+ "No such interface {}".format(interface_name))
3743+ try:
3744+ # Map the real property name
3745+ prop = {
3746+ prop.dbus_property: prop
3747+ for prop in props.values()
3748+ if isinstance(prop, property)
3749+ }[property_name]
3750+ if not isinstance(prop, property):
3751+ raise KeyError(property_name)
3752+ except KeyError:
3753+ raise dbus.exceptions.DBusException(
3754+ dbus.PROPERTIES_IFACE,
3755+ "No such property {}:{}".format(
3756+ interface_name, property_name))
3757+ try:
3758+ prop.__set__(self, value)
3759+ except dbus.exceptions.DBusException:
3760+ raise
3761+ except Exception as exc:
3762+ logger.exception(
3763+ "runaway exception from %r.Set(%r, %r, %r)",
3764+ self, interface_name, property_name, value)
3765+ raise dbus.exceptions.DBusException(
3766+ dbus.PROPERTIES_IFACE,
3767+ "Unable to set property {}:{}: {!r}".format(
3768+ interface_name, property_name, exc))
3769+ logger.debug(
3770+ "%r.Set(%r, %r, %r) -> None",
3771+ self, interface_name, property_name, value)
3772+
3773+ @dbus.service.method(
3774+ dbus_interface=dbus.PROPERTIES_IFACE,
3775+ in_signature="s", out_signature="a{sv}")
3776+ def GetAll(self, interface_name):
3777+ logger.debug("%r.GetAll(%r)", self, interface_name)
3778+ try:
3779+ props = self._dct_entry[interface_name]
3780+ except KeyError:
3781+ raise dbus.exceptions.DBusException(
3782+ dbus.PROPERTIES_IFACE,
3783+ "No such interface {}".format(interface_name))
3784+ result = {}
3785+ for prop in props.values():
3786+ if not isinstance(prop, property):
3787+ continue
3788+ prop_name = prop.dbus_property
3789+ try:
3790+ prop_value = prop.__get__(self, self.__class__)
3791+ except:
3792+ logger.exception(
3793+ "Unable to read property %r from %r", prop, self)
3794+ else:
3795+ result[prop_name] = prop_value
3796+ return result
3797+
3798+ @dbus.service.signal(
3799+ dbus_interface=dbus.PROPERTIES_IFACE,
3800+ signature='sa{sv}as')
3801+ def PropertiesChanged(
3802+ self, interface_name, changed_properties, invalidated_properties):
3803+ logger.debug(
3804+ "PropertiesChanged(%r, %r, %r)",
3805+ interface_name, changed_properties, invalidated_properties)
3806+
3807+ # [ Public DBus methods of the OBJECT_MANAGER_IFACE interface ]
3808+
3809+ @dbus.service.method(
3810+ dbus_interface=OBJECT_MANAGER_IFACE,
3811+ in_signature="", out_signature="a{oa{sa{sv}}}")
3812+ def GetManagedObjects(self):
3813+ logger.debug("%r.GetManagedObjects() -> ...", self)
3814+ result = {}
3815+ for obj in self._managed_object_list:
3816+ logger.debug("Looking for stuff exported by %r", obj)
3817+ result[obj] = {}
3818+ for iface_name in obj._dct_entry.keys():
3819+ props = obj.GetAll(iface_name)
3820+ if len(props):
3821+ result[obj][iface_name] = props
3822+ logger.debug("%r.GetManagedObjects() -> %r", self, result)
3823+ return result
3824+
3825+ @dbus.service.signal(
3826+ dbus_interface=OBJECT_MANAGER_IFACE,
3827+ signature='oa{sa{sv}}')
3828+ def InterfacesAdded(self, object_path, interfaces_and_properties):
3829+ logger.debug("%r.InterfacesAdded(%r, %r)",
3830+ self, object_path, interfaces_and_properties)
3831+
3832+ @dbus.service.signal(
3833+ dbus_interface=OBJECT_MANAGER_IFACE, signature='oas')
3834+ def InterfacesRemoved(self, object_path, interfaces):
3835+ logger.debug("%r.InterfacesRemoved(%r, %r)",
3836+ self, object_path, interfaces)
3837+
3838+ # [ Overridden methods of dbus.service.Object ]
3839+
3840+ def add_to_connection(self, connection, path):
3841+ """
3842+ Version of dbus.service.Object.add_to_connection() that keeps track of
3843+ all object paths.
3844+ """
3845+ with self._object_path_map_lock:
3846+ # Super-call add_to_connection(). This can fail which is
3847+ # okay as we haven't really modified anything yet.
3848+ super(Object, self).add_to_connection(connection, path)
3849+ # Touch self.connection, this will fail if the call above failed
3850+ # and self._connection (mind the leading underscore) is still None.
3851+ # It will also fail if the object is being exposed on multiple
3852+ # connections (so self._connection is _MANY). We are interested in
3853+ # the second check as _MANY connections are not supported here.
3854+ self.connection
3855+ # If everything is okay, just add the specified path to the
3856+ # _object_path_to_object_map.
3857+ self._object_path_to_object_map[path] = self
3858+
3859+ def remove_from_connection(self, connection=None, path=None):
3860+ with self._object_path_map_lock:
3861+ # Touch self.connection, this triggers a number of interesting
3862+ # checks, in particular checks for self._connection (mind the
3863+ # leading underscore) being _MANY or being None. Both of those
3864+ # throw an AttributeError that we can simply propagate at this
3865+ # point.
3866+ self.connection
3867+ # Create a copy of locations. This is required because locations
3868+ # are modified by remove_from_connection() which can also fail. If
3869+ # we were to use self.locations here directly we would have to undo
3870+ # any changes if remove_from_connection() raises an exception.
3871+ # Instead it is easier to first super-call remove_from_connection()
3872+ # and then do what we need to at this layer, after
3873+ # remove_from_connection() finishes successfully.
3874+ locations_copy = list(self.locations)
3875+ # Super-call remove_from_connection()
3876+ super(Object, self).remove_from_connection(connection, path)
3877+ # If either path or connection are none then treat them like
3878+ # match-any wild-cards. The same logic is implemented in the
3879+ # superclass version of this method.
3880+ if path is None or connection is None:
3881+ # Location is a tuple of at least two elements, connection and
3882+ # path. There may be other elements added later so let's not
3883+ # assume this is a simple pair.
3884+ for location in locations_copy:
3885+ location_conn = location[0]
3886+ location_path = location[1]
3887+ # If (connection matches or is None)
3888+ # and (path matches or is None)
3889+ # then remove that association
3890+ if ((location_conn == connection or connection is None)
3891+ and (path == location_path or path is None)):
3892+ del self._object_path_to_object_map[location_path]
3893+ else:
3894+ # If connection and path were specified, just remove the
3895+ # association from the specified path.
3896+ del self._object_path_to_object_map[path]
3897+
3898+ # [ Custom Extension Methods ]
3899+
3900+ def should_expose_interface(self, iface_name):
3901+ """
3902+ Check if the specified interface should be exposed.
3903+
3904+ This method controls which of the interfaces are visible as implemented
3905+ by this Object. By default objects don't implement any interface expect
3906+ for PEER_IFACE. There are two more interfaces that are implemented
3907+ internally but need to be explicitly exposed: the PROPERTIES_IFACE and
3908+ OBJECT_MANAGER_IFACE.
3909+
3910+ Typically subclasses should NOT override this method, instead
3911+ subclasses should define class-scope HIDDEN_INTERFACES as a
3912+ frozenset() of classes to hide and remove one of the entries found in
3913+ _STD_INTERFACES from it to effectively enable that interface.
3914+ """
3915+ return iface_name not in self.HIDDEN_INTERFACES
3916+
3917+ @classmethod
3918+ def find_object_by_path(cls, object_path):
3919+ """
3920+ Find and return the object that is exposed as object_path on any
3921+ connection. Using multiple connections is not supported at this time.
3922+
3923+ .. note::
3924+ This obviously only works for objects exposed from the same
3925+ application. The main use case is to have a way to lookup object
3926+ paths that may be passed as arguments and also originate in the
3927+ same application.
3928+ """
3929+ # XXX: ideally this would be per-connection method.
3930+ with cls._object_path_map_lock:
3931+ return cls._object_path_to_object_map[object_path]
3932+
3933+ @_property
3934+ def managed_objects(self):
3935+ """
3936+ list of of managed objects.
3937+
3938+ This collection is a part of the OBJECT_MANAGER_IFACE. While it can be
3939+ manipulated directly (technically) it should only be manipulated using
3940+ :meth:`add_managed_object()`, :meth:`add_manage_object_list()`,
3941+ :meth:`remove_managed_object()` and
3942+ :meth:`remove_managed_object_list()` as they send appropriate DBus
3943+ signals.
3944+ """
3945+ return self._managed_object_list
3946+
3947+ def add_managed_object(self, obj):
3948+ self.add_managed_object_list([obj])
3949+
3950+ def remove_managed_object(self, obj):
3951+ self.remove_managed_object_list([obj])
3952+
3953+ def add_managed_object_list(self, obj_list):
3954+ logger.debug("Adding managed objects: %s", obj_list)
3955+ for obj in obj_list:
3956+ if not isinstance(obj, Object):
3957+ raise TypeError("obj must be of type {!r}".format(Object))
3958+ old = self._managed_object_list
3959+ new = list(old)
3960+ new.extend(obj_list)
3961+ self._managed_object_list = new
3962+ self._on_managed_objects_changed(old, new)
3963+
3964+ def remove_managed_object_list(self, obj_list):
3965+ logger.debug("Removing managed objects: %s", obj_list)
3966+ for obj in obj_list:
3967+ if not isinstance(obj, Object):
3968+ raise TypeError("obj must be of type {!r}".format(Object))
3969+ old = self._managed_object_list
3970+ new = list(old)
3971+ for obj in obj_list:
3972+ new.remove(obj)
3973+ self._managed_object_list = new
3974+ self._on_managed_objects_changed(old, new)
3975+
3976+ # [ Custom Private Implementation Data ]
3977+
3978+ _STD_INTERFACES = frozenset([
3979+ INTROSPECTABLE_IFACE,
3980+ OBJECT_MANAGER_IFACE,
3981+ # TODO: peer interface is not implemented in this class
3982+ # PEER_IFACE,
3983+ PROPERTIES_IFACE
3984+ ])
3985+
3986+ HIDDEN_INTERFACES = frozenset([
3987+ OBJECT_MANAGER_IFACE,
3988+ PROPERTIES_IFACE
3989+ ])
3990+
3991+ # Lock protecting access to _object_path_to_object_map.
3992+ # XXX: ideally this would be a per-connection attribute
3993+ _object_path_map_lock = threading.Lock()
3994+
3995+ # Map of object_path -> dbus.service.Object instances
3996+ # XXX: ideally this would be a per-connection attribute
3997+ _object_path_to_object_map = weakref.WeakValueDictionary()
3998+
3999+ # [ Custom Private Implementation Methods ]
4000+
4001+ @_property
4002+ def _dct_key(self):
4003+ """
4004+ the key indexing this Object in Object.__class__._dbus_class_table
4005+ """
4006+ return self.__class__.__module__ + '.' + self.__class__.__name__
4007+
4008+ @_property
4009+ def _dct_entry(self):
4010+ """
4011+ same as self.__class__._dbus_class_table[self._dct_key]
4012+ """
4013+ return self.__class__._dbus_class_table[self._dct_key]
4014+
4015+ @Signal.define
4016+ def _on_managed_objects_changed(self, old_objs, new_objs):
4017+ logger.debug("%r._on_managed_objects_changed(%r, %r)",
4018+ self, old_objs, new_objs)
4019+ for obj in frozenset(new_objs) - frozenset(old_objs):
4020+ ifaces_and_props = {}
4021+ for iface_name in obj._dct_entry.keys():
4022+ try:
4023+ props = obj.GetAll(iface_name)
4024+ except dbus.exceptions.DBusException as exc:
4025+ logger.warning("Caught %r", exc)
4026+ else:
4027+ if len(props):
4028+ ifaces_and_props[iface_name] = props
4029+ self.InterfacesAdded(obj.__dbus_object_path__, ifaces_and_props)
4030+ for obj in frozenset(old_objs) - frozenset(new_objs):
4031+ ifaces = list(obj._dct_entry.keys())
4032+ self.InterfacesRemoved(obj.__dbus_object_path__, ifaces)
4033+
4034+
4035+class ObjectWrapper(Object):
4036+ """
4037+ Wrapper for a single python object which makes it easier to expose over
4038+ DBus as a service. The object should be injected into something that
4039+ extends dbus.service.Object class.
4040+
4041+ The class maintains an association between each wrapper and native object
4042+ and offers methods for converting between the two.
4043+ """
4044+
4045+ # Lock protecting access to _native_id_to_wrapper_map
4046+ _native_id_map_lock = threading.Lock()
4047+
4048+ # Man of id(wrapper.native) -> wrapper
4049+ _native_id_to_wrapper_map = weakref.WeakValueDictionary()
4050+
4051+ def __init__(self, native, conn=None, object_path=None, bus_name=None):
4052+ """
4053+ Create a new wrapper for the specified native object
4054+ """
4055+ super(ObjectWrapper, self).__init__(conn, object_path, bus_name)
4056+ with self._native_id_map_lock:
4057+ self._native_id_to_wrapper_map[id(native)] = self
4058+ self._native = native
4059+
4060+ @_property
4061+ def native(self):
4062+ """
4063+ native python object being wrapped by this wrapper
4064+ """
4065+ return self._native
4066+
4067+ @classmethod
4068+ def find_wrapper_by_native(cls, native):
4069+ """
4070+ Find the wrapper associated with the specified native object
4071+ """
4072+ with cls._native_id_map_lock:
4073+ return cls._native_id_to_wrapper_map[id(native)]
4074
4075=== modified file 'plainbox/plainbox/impl/exporter/__init__.py'
4076--- plainbox/plainbox/impl/exporter/__init__.py 2013-06-22 06:06:21 +0000
4077+++ plainbox/plainbox/impl/exporter/__init__.py 2013-09-13 17:12:45 +0000
4078@@ -148,6 +148,9 @@
4079 continue
4080 data['result_map'][job_name] = OrderedDict()
4081 data['result_map'][job_name]['outcome'] = job_state.result.outcome
4082+ if job_state.result.execution_duration:
4083+ data['result_map'][job_name]['execution_duration'] = \
4084+ job_state.result.execution_duration
4085 if self.OPTION_WITH_COMMENTS in self._option_list:
4086 data['result_map'][job_name]['comments'] = \
4087 job_state.result.comments
4088@@ -155,12 +158,12 @@
4089 # Add Parent hash if requested
4090 if self.OPTION_WITH_JOB_VIA in self._option_list:
4091 data['result_map'][job_name]['via'] = \
4092- job_state.result.job.via
4093+ job_state.job.via
4094
4095 # Add Job hash if requested
4096 if self.OPTION_WITH_JOB_HASH in self._option_list:
4097 data['result_map'][job_name]['hash'] = \
4098- job_state.result.job.get_checksum()
4099+ job_state.job.get_checksum()
4100
4101 # Add Job definitions if requested
4102 if self.OPTION_WITH_JOB_DEFS in self._option_list:
4103@@ -170,17 +173,17 @@
4104 'command',
4105 'description',
4106 ):
4107- if not getattr(job_state.result.job, prop):
4108+ if not getattr(job_state.job, prop):
4109 continue
4110 data['result_map'][job_name][prop] = getattr(
4111- job_state.result.job, prop)
4112+ job_state.job, prop)
4113
4114 # Add Attachments if requested
4115- if job_state.result.job.plugin == 'attachment':
4116+ if job_state.job.plugin == 'attachment':
4117 if self.OPTION_WITH_ATTACHMENTS in self._option_list:
4118 raw_bytes = b''.join(
4119 (record[2] for record in
4120- job_state.result.io_log if record[1] == 'stdout'))
4121+ job_state.result.get_io_log() if record[1] == 'stdout'))
4122 data['attachment_map'][job_name] = \
4123 base64.standard_b64encode(raw_bytes).decode('ASCII')
4124 continue # Don't add attachments IO logs to the result_map
4125@@ -191,12 +194,12 @@
4126 # saved, discarding stream name and the relative timestamp.
4127 if self.OPTION_SQUASH_IO_LOG in self._option_list:
4128 io_log_data = self._squash_io_log(
4129- job_state.result.io_log)
4130+ job_state.result.get_io_log())
4131 elif self.OPTION_FLATTEN_IO_LOG in self._option_list:
4132 io_log_data = self._flatten_io_log(
4133- job_state.result.io_log)
4134+ job_state.result.get_io_log())
4135 else:
4136- io_log_data = self._io_log(job_state.result.io_log)
4137+ io_log_data = self._io_log(job_state.result.get_io_log())
4138 data['result_map'][job_name]['io_log'] = io_log_data
4139 return data
4140
4141@@ -288,8 +291,10 @@
4142 for entry_point in sorted(iterator, key=lambda ep: ep.name):
4143 try:
4144 exporter_cls = entry_point.load()
4145+ except pkg_resources.DistributionNotFound as exc:
4146+ logger.info("Unable to load %s: %s", entry_point, exc)
4147 except ImportError as exc:
4148- logger.exception("Unable to import {}: {}", entry_point, exc)
4149+ logger.exception("Unable to import %s: %s", entry_point, exc)
4150 else:
4151 exporter_map[entry_point.name] = exporter_cls
4152 return exporter_map
4153
4154=== added file 'plainbox/plainbox/impl/exporter/html.py'
4155--- plainbox/plainbox/impl/exporter/html.py 1970-01-01 00:00:00 +0000
4156+++ plainbox/plainbox/impl/exporter/html.py 2013-09-13 17:12:45 +0000
4157@@ -0,0 +1,145 @@
4158+# This file is part of Checkbox.
4159+#
4160+# Copyright 2013 Canonical Ltd.
4161+# Written by:
4162+# Sylvain Pineau <sylvain.pineau@canonical.com>
4163+# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
4164+# Daniel Manrique <daniel.manrique@canonical.com>
4165+#
4166+# Checkbox is free software: you can redistribute it and/or modify
4167+# it under the terms of the GNU General Public License as published by
4168+# the Free Software Foundation, either version 3 of the License, or
4169+# (at your option) any later version.
4170+#
4171+# Checkbox is distributed in the hope that it will be useful,
4172+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4173+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4174+# GNU General Public License for more details.
4175+#
4176+# You should have received a copy of the GNU General Public License
4177+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4178+
4179+"""
4180+:mod:`plainbox.impl.exporter.html`
4181+==================================
4182+
4183+HTML exporter for human consumption
4184+
4185+.. warning::
4186+ THIS MODULE DOES NOT HAVE A STABLE PUBLIC API
4187+"""
4188+
4189+from string import Template
4190+import base64
4191+import logging
4192+import mimetypes
4193+
4194+from lxml import etree as ET
4195+from pkg_resources import resource_filename
4196+
4197+from plainbox.impl.exporter.xml import XMLSessionStateExporter
4198+
4199+
4200+logger = logging.getLogger("plainbox.exporter.html")
4201+
4202+
4203+class HTMLResourceInliner(object):
4204+ """ A helper class to inline resources referenced in an lxml tree.
4205+ """
4206+ def _resource_content(self, url):
4207+ try:
4208+ with open(url, 'rb') as f:
4209+ file_contents = f.read()
4210+ except (IOError, OSError):
4211+ logger.warning("Unable to load resource %s, not inlining",
4212+ url)
4213+ return ""
4214+ type, encoding = mimetypes.guess_type(url)
4215+ if not encoding:
4216+ encoding = "utf-8"
4217+ if type in("text/css", "application/javascript"):
4218+ return file_contents.decode(encoding)
4219+ elif type in("image/png", "image/jpg"):
4220+ b64_data = base64.b64encode(file_contents)
4221+ b64_data = b64_data.decode("ascii")
4222+ return_string = "data:{};base64,{}".format(type, b64_data)
4223+ return return_string
4224+ else:
4225+ logger.warning("Resource of type %s unknown", type)
4226+ #Strip it out, better not to have it.
4227+ return ""
4228+
4229+ def inline_resources(self, document_tree):
4230+ """
4231+ Replace references to external resources by an in-place (inlined)
4232+ representation of each resource.
4233+
4234+ Currently images, stylesheets and scripts are inlined.
4235+
4236+ Only local (i.e. file) resources/locations are supported. If a
4237+ non-local resource is requested for inlining, it will be removed
4238+ (replaced by a blank string), with the goal that the resulting
4239+ lxml tree will not reference any unreachable resources.
4240+
4241+ :param document_tree:
4242+ lxml tree to process.
4243+
4244+ :returns:
4245+ lxml tree with some elements replaced by their inlined
4246+ representation.
4247+ """
4248+ # Try inlining using result_tree here.
4249+ for node in document_tree.xpath('//script'):
4250+ # These have src attribute, need to remove the
4251+ # attribute and add the content of the src file
4252+ # as the node's text
4253+ src = node.attrib.pop('src')
4254+ node.text = self._resource_content(src)
4255+
4256+ for node in document_tree.xpath('//link[@rel="stylesheet"]'):
4257+ # These have a href attribute and need to be completely replaced
4258+ # by a new <style> node with contents of the href file
4259+ # as its text.
4260+ src = node.attrib.pop('href')
4261+ type = node.attrib.pop('type')
4262+ style_elem = ET.Element("style")
4263+ style_elem.attrib['type'] = type
4264+ style_elem.text = self._resource_content(src)
4265+ node.getparent().append(style_elem)
4266+ # Now zorch the existing node
4267+ node.getparent().remove(node)
4268+
4269+ for node in document_tree.xpath('//img'):
4270+ # src attribute points to a file and needs to
4271+ # contain the base64 encoded version of that file.
4272+ src = node.attrib.pop('src')
4273+ node.attrib['src'] = self._resource_content(src)
4274+ return document_tree
4275+
4276+
4277+class HTMLSessionStateExporter(XMLSessionStateExporter):
4278+ """
4279+ Session state exporter creating HTML documents.
4280+
4281+ It basically applies an xslt to the XMLSessionStateExporter output,
4282+ and then inlines some resources to produce a monolithic report in a
4283+ single file.
4284+ """
4285+
4286+ def dump(self, data, stream):
4287+ """
4288+ Public method to dump the HTML report to a stream
4289+ """
4290+ root = self.get_root_element(data)
4291+ self.xslt_filename = resource_filename(
4292+ "plainbox", "data/report/checkbox.xsl")
4293+ template_substitutions = {
4294+ 'PLAINBOX_ASSETS': resource_filename("plainbox", "data/")}
4295+ with open(self.xslt_filename, encoding="UTF-8") as xslt_file:
4296+ xslt_template = Template(xslt_file.read())
4297+ xslt_data = xslt_template.substitute(template_substitutions)
4298+ xslt_root = ET.XML(xslt_data)
4299+ transformer = ET.XSLT(xslt_root)
4300+ r_tree = transformer(root)
4301+ inlined_result_tree = HTMLResourceInliner().inline_resources(r_tree)
4302+ stream.write(ET.tostring(inlined_result_tree, pretty_print=True))
4303
4304=== added file 'plainbox/plainbox/impl/exporter/test_html.py'
4305--- plainbox/plainbox/impl/exporter/test_html.py 1970-01-01 00:00:00 +0000
4306+++ plainbox/plainbox/impl/exporter/test_html.py 2013-09-13 17:12:45 +0000
4307@@ -0,0 +1,142 @@
4308+# This file is part of Checkbox.
4309+#
4310+# Copyright 2013 Canonical Ltd.
4311+# Written by:
4312+# Sylvain Pineau <sylvain.pineau@canonical.com>
4313+# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
4314+# Daniel Manrique <daniel.manrique@canonical.com>
4315+#
4316+# Checkbox is free software: you can redistribute it and/or modify
4317+# it under the terms of the GNU General Public License as published by
4318+# the Free Software Foundation, either version 3 of the License, or
4319+# (at your option) any later version.
4320+#
4321+# Checkbox is distributed in the hope that it will be useful,
4322+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4323+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4324+# GNU General Public License for more details.
4325+#
4326+# You should have received a copy of the GNU General Public License
4327+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4328+
4329+"""
4330+plainbox.impl.exporter.test_html
4331+================================
4332+
4333+Test definitions for plainbox.impl.exporter.html module
4334+"""
4335+from io import StringIO
4336+from string import Template
4337+from unittest import TestCase
4338+import io
4339+
4340+from lxml import etree as ET
4341+from pkg_resources import resource_filename
4342+from pkg_resources import resource_string
4343+
4344+from plainbox.testing_utils import resource_json
4345+from plainbox.impl.exporter.html import HTMLResourceInliner
4346+from plainbox.impl.exporter.html import HTMLSessionStateExporter
4347+
4348+
4349+class HTMLInlinerTests(TestCase):
4350+ def setUp(self):
4351+ template_substitutions = {
4352+ 'PLAINBOX_ASSETS': resource_filename("plainbox", "data/")}
4353+ test_file_location = "test-data/html-exporter/html-inliner.html"
4354+ test_file = resource_filename("plainbox",
4355+ test_file_location)
4356+ with open(test_file) as html_file:
4357+ html_template = Template(html_file.read())
4358+ html_content = html_template.substitute(template_substitutions)
4359+ self.tree = ET.parse(StringIO(html_content), ET.HTMLParser())
4360+ # Now self.tree contains a tree with adequately-substituted
4361+ # paths and resources
4362+ inliner = HTMLResourceInliner()
4363+ self.inlined_tree = inliner.inline_resources(self.tree)
4364+
4365+ def test_script_inlining(self):
4366+ """Test that a <script> resource gets inlined."""
4367+ for node in self.inlined_tree.xpath('//script'):
4368+ self.assertTrue(node.text)
4369+
4370+ def test_img_inlining(self):
4371+ """
4372+ Test that a <img> gets inlined.
4373+ It should be replaced by a base64 representation of the
4374+ referenced image's data as per RFC2397.
4375+ """
4376+ for node in self.inlined_tree.xpath('//img'):
4377+ # Skip image that purposefully points to a remote
4378+ # resource
4379+ if node.attrib.get('class') != "remote_resource":
4380+ self.assertTrue("base64" in node.attrib['src'])
4381+
4382+ def test_css_inlining(self):
4383+ """Test that a <style> resource gets inlined."""
4384+ for node in self.inlined_tree.xpath('//style'):
4385+ # Skip a fake remote_resource node that's purposefully
4386+ # not inlined
4387+ if not 'nonexistent_resource' in node.attrib['type']:
4388+ self.assertTrue("body" in node.text)
4389+
4390+ def test_remote_resource_inlining(self):
4391+ """
4392+ Test that a resource with a non-local (i.e. not file://
4393+ url) does NOT get inlined (rather it's replaced by an
4394+ empty string). We use <style> in this test.
4395+ """
4396+ for node in self.inlined_tree.xpath('//style'):
4397+ # The not-inlined remote_resource
4398+ if 'nonexistent_resource' in node.attrib['type']:
4399+ self.assertTrue(node.text == "")
4400+
4401+ def test_unfindable_file_inlining(self):
4402+ """
4403+ Test that a resource whose file does not exist does NOT
4404+ get inlined, and is instead replaced by empty string.
4405+ We use <img> in this test.
4406+ """
4407+ for node in self.inlined_tree.xpath('//img'):
4408+ if node.attrib.get('class') == "remote_resource":
4409+ self.assertEqual("", node.attrib['src'])
4410+
4411+
4412+class HTMLExporterTests(TestCase):
4413+
4414+ def setUp(self):
4415+ data = resource_json(
4416+ "plainbox", "test-data/xml-exporter/example-data.json",
4417+ exact=True)
4418+ exporter = HTMLSessionStateExporter(
4419+ system_id="",
4420+ timestamp="2012-12-21T12:00:00",
4421+ client_version="1.0")
4422+ stream = io.BytesIO()
4423+ exporter.dump(data, stream)
4424+ self.actual_result = stream.getvalue() # This is bytes
4425+ self.assertIsInstance(self.actual_result, bytes)
4426+
4427+ def test_html_output(self):
4428+ """
4429+ Test that output from the exporter is HTML (or at least,
4430+ appears to be).
4431+ """
4432+ # A pretty simplistic test since we just validate the output
4433+ # appears to be HTML. Looking at the exporter's code, it's mostly
4434+ # boilerplate use of lxml and etree, so let's not fall into testing
4435+ # an external library.
4436+ self.assertIn(b"<html>",
4437+ self.actual_result)
4438+ self.assertIn(b"<title>System Testing Report</title>",
4439+ self.actual_result)
4440+
4441+ def test_perfect_match(self):
4442+ """
4443+ Test that output from the exporter exactly matches known
4444+ good HTML output, inlining and everything included.
4445+ """
4446+ expected_result = resource_string(
4447+ "plainbox", "test-data/html-exporter/example-data.html"
4448+ ) # unintuitively, resource_string returns bytes
4449+ self.assertEqual(self.actual_result, expected_result)
4450
4451=== modified file 'plainbox/plainbox/impl/exporter/test_init.py'
4452--- plainbox/plainbox/impl/exporter/test_init.py 2013-06-22 06:06:21 +0000
4453+++ plainbox/plainbox/impl/exporter/test_init.py 2013-09-13 17:12:45 +0000
4454@@ -29,13 +29,14 @@
4455 from tempfile import TemporaryDirectory
4456 from unittest import TestCase
4457
4458+from plainbox.abc import IJobResult
4459 from plainbox.impl.exporter import ByteStringStreamTranslator
4460 from plainbox.impl.exporter import SessionStateExporterBase
4461 from plainbox.impl.exporter import classproperty
4462 from plainbox.impl.job import JobDefinition
4463-from plainbox.impl.result import JobResult, IOLogRecord
4464+from plainbox.impl.result import MemoryJobResult, IOLogRecord
4465 from plainbox.impl.session import SessionState
4466-from plainbox.impl.testing_utils import make_io_log, make_job, make_job_result
4467+from plainbox.impl.testing_utils import make_job, make_job_result
4468
4469
4470 class ClassPropertyTests(TestCase):
4471@@ -76,8 +77,8 @@
4472 job_b = make_job('job_b')
4473 session = SessionState([job_a, job_b])
4474 session.update_desired_job_list([job_a, job_b])
4475- result_a = make_job_result(job_a, 'pass')
4476- result_b = make_job_result(job_b, 'fail')
4477+ result_a = make_job_result(outcome=IJobResult.OUTCOME_PASS)
4478+ result_b = make_job_result(outcome=IJobResult.OUTCOME_FAIL)
4479 session.update_job_result(job_a, result_a)
4480 session.update_job_result(job_b, result_b)
4481 return session
4482@@ -115,22 +116,16 @@
4483 })
4484 session = SessionState([job_a, job_b])
4485 session.update_desired_job_list([job_a, job_b])
4486- result_a = JobResult({
4487- 'job': job_a,
4488- 'outcome': 'pass',
4489+ result_a = MemoryJobResult({
4490+ 'outcome': IJobResult.OUTCOME_PASS,
4491 'return_code': 0,
4492- 'io_log': make_io_log(
4493- (IOLogRecord(0, 'stdout', b'testing\n'),),
4494- session_dir)
4495+ 'io_log': [(0, 'stdout', b'testing\n')],
4496 })
4497- result_b = JobResult({
4498- 'job': job_b,
4499- 'outcome': 'pass',
4500+ result_b = MemoryJobResult({
4501+ 'outcome': IJobResult.OUTCOME_PASS,
4502 'return_code': 0,
4503 'comments': 'foo',
4504- 'io_log': make_io_log(
4505- (IOLogRecord(0, 'stdout', b'ready: yes\n'),),
4506- session_dir)
4507+ 'io_log': [(0, 'stdout', b'ready: yes\n')],
4508 })
4509 session.update_job_result(job_a, result_a)
4510 session.update_job_result(job_b, result_b)
4511
4512=== added file 'plainbox/plainbox/impl/exporter/xlsx.py'
4513--- plainbox/plainbox/impl/exporter/xlsx.py 1970-01-01 00:00:00 +0000
4514+++ plainbox/plainbox/impl/exporter/xlsx.py 2013-09-13 17:12:45 +0000
4515@@ -0,0 +1,570 @@
4516+# This file is part of Checkbox.
4517+#
4518+# Copyright 2013 Canonical Ltd.
4519+# Written by:
4520+# Sylvain Pineau <sylvain.pineau@canonical.com>
4521+#
4522+# Checkbox is free software: you can redistribute it and/or modify
4523+# it under the terms of the GNU General Public License as published by
4524+# the Free Software Foundation, either version 3 of the License, or
4525+# (at your option) any later version.
4526+#
4527+# Checkbox is distributed in the hope that it will be useful,
4528+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4529+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4530+# GNU General Public License for more details.
4531+#
4532+# You should have received a copy of the GNU General Public License
4533+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
4534+
4535+"""
4536+:mod:`plainbox.impl.exporter.xlsx`
4537+=================================
4538+
4539+XLSX exporter
4540+
4541+.. warning::
4542+ THIS MODULE DOES NOT HAVE A STABLE PUBLIC API
4543+"""
4544+
4545+from base64 import standard_b64decode
4546+from collections import defaultdict, OrderedDict
4547+import re
4548+
4549+from plainbox.impl.exporter import SessionStateExporterBase
4550+from plainbox.abc import IJobResult
4551+from xlsxwriter.workbook import Workbook
4552+from xlsxwriter.utility import xl_rowcol_to_cell
4553+
4554+
4555+class XLSXSessionStateExporter(SessionStateExporterBase):
4556+ """
4557+ Session state exporter creating XLSX documents
4558+
4559+ The hardware devices are extracted from the content of the following
4560+ attachment:
4561+ * lspci_attachment
4562+
4563+ The following resource jobs are needed to populate the system info section
4564+ of this report:
4565+ * dmi
4566+ * device
4567+ * cpuinfo
4568+ * meminfo
4569+ * package
4570+ """
4571+
4572+ OPTION_WITH_SYSTEM_INFO = 'with-sys-info'
4573+ OPTION_WITH_SUMMARY = 'with-summary'
4574+ OPTION_WITH_DESCRIPTION = 'with-job-description'
4575+ OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments'
4576+
4577+ SUPPORTED_OPTION_LIST = (
4578+ OPTION_WITH_SYSTEM_INFO,
4579+ OPTION_WITH_SUMMARY,
4580+ OPTION_WITH_DESCRIPTION,
4581+ OPTION_WITH_TEXT_ATTACHMENTS,
4582+ )
4583+
4584+ def __init__(self, option_list=None):
4585+ """
4586+ Initialize a new XLSXSessionStateExporter.
4587+ """
4588+ # Super-call with empty option list
4589+ super(XLSXSessionStateExporter, self).__init__(())
4590+ # All the "options" are simply a required configuration element and are
4591+ # not optional in any way. There is no way to opt-out.
4592+ if option_list is None:
4593+ option_list = ()
4594+ for option in option_list:
4595+ if option not in self.supported_option_list:
4596+ raise ValueError("Unsupported option: {}".format(option))
4597+ self._option_list = (
4598+ SessionStateExporterBase.OPTION_WITH_IO_LOG,
4599+ SessionStateExporterBase.OPTION_FLATTEN_IO_LOG,
4600+ SessionStateExporterBase.OPTION_WITH_JOB_DEFS,
4601+ SessionStateExporterBase.OPTION_WITH_JOB_VIA,
4602+ SessionStateExporterBase.OPTION_WITH_JOB_HASH,
4603+ SessionStateExporterBase.OPTION_WITH_RESOURCE_MAP,
4604+ SessionStateExporterBase.OPTION_WITH_ATTACHMENTS)
4605+ self._option_list += tuple(option_list)
4606+ self.total_pass = 0
4607+ self.total_fail = 0
4608+ self.total_skip = 0
4609+ self.total = 0
4610+
4611+ def _set_formats(self):
4612+ # Main Title format (Orange)
4613+ self.format01 = self.workbook.add_format({
4614+ 'align': 'left', 'size': 24, 'font_color': '#DC4C00',
4615+ })
4616+ # Default font
4617+ self.format02 = self.workbook.add_format({
4618+ 'align': 'left', 'valign': 'vcenter', 'size': 10,
4619+ })
4620+ # Titles
4621+ self.format03 = self.workbook.add_format({
4622+ 'align': 'left', 'size': 12, 'bold': 1,
4623+ })
4624+ # Titles + borders
4625+ self.format04 = self.workbook.add_format({
4626+ 'align': 'left', 'size': 12, 'bold': 1, 'border': 7
4627+ })
4628+ # System info with borders
4629+ self.format05 = self.workbook.add_format({
4630+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4631+ 'border': 7,
4632+ })
4633+ # System info with borders, grayed out background
4634+ self.format06 = self.workbook.add_format({
4635+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4636+ 'border': 7, 'bg_color': '#E6E6E6',
4637+ })
4638+ # Headlines (center)
4639+ self.format07 = self.workbook.add_format({
4640+ 'align': 'center', 'size': 10, 'bold': 1,
4641+ })
4642+ # Table rows without borders
4643+ self.format08 = self.workbook.add_format({
4644+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4645+ })
4646+ # Table rows without borders, grayed out background
4647+ self.format09 = self.workbook.add_format({
4648+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4649+ 'bg_color': '#E6E6E6',
4650+ })
4651+ # Green background / Size 8
4652+ self.format10 = self.workbook.add_format({
4653+ 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4654+ 'bg_color': 'lime', 'border': 7, 'border_color': 'white',
4655+ })
4656+ # Red background / Size 8
4657+ self.format11 = self.workbook.add_format({
4658+ 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4659+ 'bg_color': 'red', 'border': 7, 'border_color': 'white',
4660+ })
4661+ # Gray background / Size 8
4662+ self.format12 = self.workbook.add_format({
4663+ 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4664+ 'bg_color': 'gray', 'border': 7, 'border_color': 'white',
4665+ })
4666+ # Attachments
4667+ self.format13 = self.workbook.add_format({
4668+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
4669+ 'font': 'Courier New',
4670+ })
4671+ # Invisible man
4672+ self.format14 = self.workbook.add_format({'font_color': 'white'})
4673+ # Headlines (left-aligned)
4674+ self.format15 = self.workbook.add_format({
4675+ 'align': 'left', 'size': 10, 'bold': 1,
4676+ })
4677+ # Table rows without borders, indent level 1
4678+ self.format16 = self.workbook.add_format({
4679+ 'align': 'left', 'valign': 'vcenter', 'size': 8, 'indent': 1,
4680+ })
4681+ # Table rows without borders, grayed out background, indent level 1
4682+ self.format17 = self.workbook.add_format({
4683+ 'align': 'left', 'valign': 'vcenter', 'size': 8,
4684+ 'bg_color': '#E6E6E6', 'indent': 1,
4685+ })
4686+
4687+ def _hw_collection(self, data):
4688+ hw_info = defaultdict(lambda: 'NA')
4689+ if 'dmi' in data['resource_map']:
4690+ result = ['{} {} ({})'.format(i['vendor'], i['product'],
4691+ i['version']) for i in data["resource_map"]['dmi']
4692+ if i['category'] == 'SYSTEM']
4693+ if result:
4694+ hw_info['platform'] = result.pop()
4695+ result = ['{}'.format(i['version'])
4696+ for i in data["resource_map"]['dmi']
4697+ if i['category'] == 'BIOS']
4698+ if result:
4699+ hw_info['bios'] = result.pop()
4700+ if 'cpuinfo' in data['resource_map']:
4701+ result = ['{} x {}'.format(i['model'], i['count'])
4702+ for i in data["resource_map"]['cpuinfo']]
4703+ if result:
4704+ hw_info['processors'] = result.pop()
4705+ if 'lspci_attachment' in data['attachment_map']:
4706+ lspci = data['attachment_map']['lspci_attachment']
4707+ content = standard_b64decode(lspci.encode()).decode("UTF-8")
4708+ match = re.search('ISA bridge.*?:\s(?P<chipset>.*?)\sLPC', content)
4709+ if match:
4710+ hw_info['chipset'] = match.group('chipset')
4711+ match = re.search(
4712+ 'Audio device.*?:\s(?P<audio>.*?)\s\[\w+:\w+]', content)
4713+ if match:
4714+ hw_info['audio'] = match.group('audio')
4715+ match = re.search(
4716+ 'Ethernet controller.*?:\s(?P<nic>.*?)\s\[\w+:\w+]', content)
4717+ if match:
4718+ hw_info['nic'] = match.group('nic')
4719+ match = re.search(
4720+ 'Network controller.*?:\s(?P<wireless>.*?)\s\[\w+:\w+]',
4721+ content)
4722+ if match:
4723+ hw_info['wireless'] = match.group('wireless')
4724+ for i, match in enumerate(re.finditer(
4725+ 'VGA compatible controller.*?:\s(?P<video>.*?)\s\[\w+:\w+]',
4726+ content), start=1
4727+ ):
4728+ hw_info['video{}'.format(i)] = match.group('video')
4729+ vram = 0
4730+ for match in re.finditer(
4731+ 'Memory.+ prefetchable\) \[size=(?P<vram>\d+)M\]',
4732+ content):
4733+ vram += int(match.group('vram'))
4734+ if vram:
4735+ hw_info['vram'] = '{} MiB'.format(vram)
4736+ if 'meminfo' in data['resource_map']:
4737+ result = ['{} GiB'.format(format(int(i['total']) / 1073741824,
4738+ '.1f')) for i in data["resource_map"]['meminfo']]
4739+ if result:
4740+ hw_info['memory'] = result.pop()
4741+ if 'device' in data['resource_map']:
4742+ result = ['{}'.format(i['product'])
4743+ for i in data["resource_map"]['device']
4744+ if ('category' in i and
4745+ i['category'] == 'BLUETOOTH' and 'driver' in i)]
4746+ if result:
4747+ hw_info['bluetooth'] = result.pop()
4748+ return hw_info
4749+
4750+ def write_systeminfo(self, data):
4751+ self.worksheet1.set_column(0, 0, 4)
4752+ self.worksheet1.set_column(1, 1, 34)
4753+ self.worksheet1.set_column(2, 3, 58)
4754+ hw_info = self._hw_collection(data)
4755+ self.worksheet1.write(5, 1, 'Platform Name', self.format03)
4756+ self.worksheet1.write(5, 2, hw_info['platform'], self.format03)
4757+ self.worksheet1.write(7, 1, 'BIOS', self.format04)
4758+ self.worksheet1.write(7, 2, hw_info['bios'], self.format06)
4759+ self.worksheet1.write(8, 1, 'Processors', self.format04)
4760+ self.worksheet1.write(8, 2, hw_info['processors'], self.format05)
4761+ self.worksheet1.write(9, 1, 'Chipset', self.format04)
4762+ self.worksheet1.write(9, 2, hw_info['chipset'], self.format06)
4763+ self.worksheet1.write(10, 1, 'Memory', self.format04)
4764+ self.worksheet1.write(10, 2, hw_info['memory'], self.format05)
4765+ self.worksheet1.write(11, 1, 'Video (on board)', self.format04)
4766+ self.worksheet1.write(11, 2, hw_info['video1'], self.format06)
4767+ self.worksheet1.write(12, 1, 'Video (add-on)', self.format04)
4768+ self.worksheet1.write(12, 2, hw_info['video2'], self.format05)
4769+ self.worksheet1.write(13, 1, 'Video memory', self.format04)
4770+ self.worksheet1.write(13, 2, hw_info['vram'], self.format06)
4771+ self.worksheet1.write(14, 1, 'Audio', self.format04)
4772+ self.worksheet1.write(14, 2, hw_info['audio'], self.format05)
4773+ self.worksheet1.write(15, 1, 'NIC', self.format04)
4774+ self.worksheet1.write(15, 2, hw_info['nic'], self.format06)
4775+ self.worksheet1.write(16, 1, 'Wireless', self.format04)
4776+ self.worksheet1.write(16, 2, hw_info['wireless'], self.format05)
4777+ self.worksheet1.write(17, 1, 'Bluetooth', self.format04)
4778+ self.worksheet1.write(17, 2, hw_info['bluetooth'], self.format06)
4779+ if "package" in data["resource_map"]:
4780+ self.worksheet1.write(19, 1, 'Packages Installed', self.format03)
4781+ self.worksheet1.write_row(
4782+ 21, 1, ['Name', 'Version'], self.format07
4783+ )
4784+ for i in range(20, 22):
4785+ self.worksheet1.set_row(
4786+ i, None, None, {'level': 1, 'hidden': True}
4787+ )
4788+ for i, pkg in enumerate(data["resource_map"]["package"]):
4789+ self.worksheet1.write_row(
4790+ 22 + i, 1,
4791+ [pkg['name'], pkg['version']],
4792+ self.format08 if i % 2 else self.format09
4793+ )
4794+ self.worksheet1.set_row(
4795+ 22 + i, None, None, {'level': 1, 'hidden': True}
4796+ )
4797+ self.worksheet1.set_row(
4798+ 22+len(data["resource_map"]["package"]),
4799+ None, None, {'collapsed': True}
4800+ )
4801+
4802+ def write_summary(self, data):
4803+ self.worksheet2.set_column(0, 0, 5)
4804+ self.worksheet2.set_column(1, 1, 2)
4805+ self.worksheet2.set_column(3, 3, 27)
4806+ self.worksheet2.write(3, 1, 'Failures summary', self.format03)
4807+ self.worksheet2.write(4, 1, '✔', self.format10)
4808+ self.worksheet2.write(
4809+ 4, 2,
4810+ '{} Tests passed - Success Rate: {:.2f}% ({}/{})'.format(
4811+ self.total_pass, self.total_pass / self.total * 100,
4812+ self.total_pass, self.total), self.format02)
4813+ self.worksheet2.write(5, 1, '✘', self.format11)
4814+ self.worksheet2.write(
4815+ 5, 2,
4816+ '{} Tests failed - Failure Rate: {:.2f}% ({}/{})'.format(
4817+ self.total_fail, self.total_fail / self.total * 100,
4818+ self.total_fail, self.total), self.format02)
4819+ self.worksheet2.write(6, 1, '-', self.format12)
4820+ self.worksheet2.write(
4821+ 6, 2,
4822+ '{} Tests skipped - Skip Rate: {:.2f}% ({}/{})'.format(
4823+ self.total_skip, self.total_skip / self.total * 100,
4824+ self.total_skip, self.total), self.format02)
4825+ self.worksheet2.write_column(
4826+ 'L3', ['Fail', 'Skip', 'Pass'], self.format14)
4827+ self.worksheet2.write_column(
4828+ 'M3', [self.total_fail, self.total_skip, self.total_pass],
4829+ self.format14)
4830+ # Configure the series.
4831+ chart = self.workbook.add_chart({'type': 'pie'})
4832+ chart.set_legend({'position': 'none'})
4833+ chart.add_series({
4834+ 'points': [
4835+ {'fill': {'color': 'red'}},
4836+ {'fill': {'color': 'gray'}},
4837+ {'fill': {'color': 'lime'}},
4838+ ],
4839+ 'categories': '=Summary!$L$3:$L$5',
4840+ 'values': '=Summary!$M$3:$M$5'}
4841+ )
4842+ # Insert the chart into the worksheet.
4843+ self.worksheet2.insert_chart('F4', chart, {
4844+ 'x_offset': 0, 'y_offset': 10, 'x_scale': 0.25, 'y_scale': 0.25
4845+ })
4846+
4847+ def _set_category_status(self, result_map, via, child):
4848+ for parent in [j for j in result_map if result_map[j]['hash'] == via]:
4849+ if 'category_status' not in result_map[parent]:
4850+ result_map[parent]['category_status'] = None
4851+ child_status = result_map[child]['outcome']
4852+ if 'category_status' in result_map[child]:
4853+ child_status = result_map[child]['category_status']
4854+ if child_status == IJobResult.OUTCOME_FAIL:
4855+ result_map[parent]['category_status'] = IJobResult.OUTCOME_FAIL
4856+ elif (
4857+ child_status == IJobResult.OUTCOME_PASS and
4858+ result_map[parent]['category_status'] != IJobResult.OUTCOME_FAIL
4859+ ):
4860+ result_map[parent]['category_status'] = IJobResult.OUTCOME_PASS
4861+ elif (
4862+ result_map[parent]['category_status'] not in
4863+ (IJobResult.OUTCOME_PASS, IJobResult.OUTCOME_FAIL)
4864+ ):
4865+ result_map[parent]['category_status'] = IJobResult.OUTCOME_SKIP
4866+
4867+ def _tree(self, result_map, via=None, level=0, max_level=0):
4868+ res = {}
4869+ for job_name in [j for j in result_map if result_map[j]['via'] == via]:
4870+ if re.search(
4871+ 'resource|attachment',
4872+ result_map[job_name]['plugin']):
4873+ continue
4874+ level += 1
4875+ # Find the maximum depth of the test tree
4876+ if level > max_level:
4877+ max_level = level
4878+ res[job_name], max_level = self._tree(
4879+ result_map, result_map[job_name]['hash'], level, max_level)
4880+ # Generate parent categories status
4881+ if via is not None:
4882+ self._set_category_status(result_map, via, job_name)
4883+ level -= 1
4884+ return res, max_level
4885+
4886+ def _write_job(self, tree, result_map, max_level, level=0):
4887+ for job, children in OrderedDict(
4888+ sorted(
4889+ tree.items(),
4890+ key=lambda t: 'z' + t[0] if t[1] else 'a' + t[0])).items():
4891+ self._lineno += 1
4892+ if children:
4893+ self.worksheet3.write(
4894+ self._lineno, level + 1,
4895+ result_map[job]['description'], self.format15)
4896+ if (
4897+ result_map[job]['category_status'] ==
4898+ IJobResult.OUTCOME_PASS
4899+ ):
4900+ self.worksheet3.write(
4901+ self._lineno, max_level + 2, 'PASS', self.format10)
4902+ elif (
4903+ result_map[job]['category_status'] ==
4904+ IJobResult.OUTCOME_FAIL
4905+ ):
4906+ self.worksheet3.write(
4907+ self._lineno, max_level + 2, 'FAIL', self.format11)
4908+ elif (
4909+ result_map[job]['category_status'] ==
4910+ IJobResult.OUTCOME_SKIP
4911+ ):
4912+ self.worksheet3.write(
4913+ self._lineno, max_level + 2, 'skip', self.format12)
4914+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4915+ self.worksheet4.write(
4916+ self._lineno, level + 1,
4917+ result_map[job]['description'], self.format15)
4918+ if level:
4919+ self.worksheet3.set_row(
4920+ self._lineno, 13, None, {'level': level})
4921+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4922+ self.worksheet4.set_row(
4923+ self._lineno, 13, None, {'level': level})
4924+ else:
4925+ self.worksheet3.set_row(self._lineno, 13)
4926+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4927+ self.worksheet4.set_row(self._lineno, 13)
4928+ self._write_job(children, result_map, max_level, level + 1)
4929+ else:
4930+ self.worksheet3.write(
4931+ self._lineno, max_level + 1, job,
4932+ self.format08 if self._lineno % 2 else self.format09)
4933+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4934+ link_cell = xl_rowcol_to_cell(self._lineno, max_level + 1)
4935+ self.worksheet3.write_url(
4936+ self._lineno, max_level + 1,
4937+ 'internal:Test Descriptions!' + link_cell,
4938+ self.format08 if self._lineno % 2 else self.format09,
4939+ job)
4940+ self.worksheet4.write(
4941+ self._lineno, max_level + 1, job,
4942+ self.format08 if self._lineno % 2 else self.format09)
4943+ self.total += 1
4944+ if result_map[job]['outcome'] == IJobResult.OUTCOME_PASS:
4945+ self.worksheet3.write(
4946+ self._lineno, max_level, '✔', self.format10)
4947+ self.worksheet3.write(
4948+ self._lineno, max_level + 2, 'PASS', self.format10)
4949+ self.total_pass += 1
4950+ elif result_map[job]['outcome'] == IJobResult.OUTCOME_FAIL:
4951+ self.worksheet3.write(
4952+ self._lineno, max_level, '✘', self.format11)
4953+ self.worksheet3.write(
4954+ self._lineno, max_level + 2, 'FAIL', self.format11)
4955+ self.total_fail += 1
4956+ elif result_map[job]['outcome'] == IJobResult.OUTCOME_SKIP:
4957+ self.worksheet3.write(
4958+ self._lineno, max_level, '-', self.format12)
4959+ self.worksheet3.write(
4960+ self._lineno, max_level + 2, 'skip', self.format12)
4961+ self.total_skip += 1
4962+ elif result_map[job]['outcome'] == \
4963+ IJobResult.OUTCOME_NOT_SUPPORTED:
4964+ self.worksheet3.write(
4965+ self._lineno, max_level, '-', self.format12)
4966+ self.worksheet3.write(
4967+ self._lineno, max_level + 2,
4968+ 'not supported', self.format12)
4969+ self.total_skip += 1
4970+ else:
4971+ self.worksheet3.write(
4972+ self._lineno, max_level, '-', self.format12)
4973+ self.worksheet3.write(
4974+ self._lineno, max_level + 2, None, self.format12)
4975+ self.total_skip += 1
4976+ io_log = ' '
4977+ if result_map[job]['io_log']:
4978+ io_log = standard_b64decode(
4979+ result_map[job]['io_log'].encode()).decode(
4980+ 'UTF-8').rstrip()
4981+ io_lines = len(io_log.splitlines()) - 1
4982+ desc_lines = len(result_map[job]['description'].splitlines())
4983+ desc_lines -= 1
4984+ self.worksheet3.write(
4985+ self._lineno, max_level + 3, io_log,
4986+ self.format16 if self._lineno % 2 else self.format17)
4987+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4988+ self.worksheet4.write(
4989+ self._lineno, max_level + 2,
4990+ result_map[job]['description'],
4991+ self.format16 if self._lineno % 2 else self.format17)
4992+ if level:
4993+ self.worksheet3.set_row(
4994+ self._lineno, 12 + 9.71 * io_lines,
4995+ None, {'level': level})
4996+ if self.OPTION_WITH_DESCRIPTION in self._option_list:
4997+ self.worksheet4.set_row(
4998+ self._lineno, 12 + 9.71 * desc_lines,
4999+ None, {'level': level})
5000+ else:
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches