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
=== modified file 'debian/changelog'
--- debian/changelog 2013-08-21 16:23:56 +0000
+++ debian/changelog 2013-09-13 17:12:45 +0000
@@ -1,3 +1,12 @@
1
2checkbox (1.16.7~OCP) UNRELEASED; urgency=low
3
4 [ Jeff Marcom ]
5 * Updated plainbox based on version 0.4.dev in lp:checkbox (16.12)
6
7 -- Jeff Marcom <jeff.marcom@canonical.com> Fri, 13 Sept 2013 10:13:04 -0400
8
9
1checkbox (1.16.6~OCP) UNRELEASED; urgency=low10checkbox (1.16.6~OCP) UNRELEASED; urgency=low
211
3 [ Jeff Marcom ]12 [ Jeff Marcom ]
413
=== modified file 'plainbox/MANIFEST.in'
--- plainbox/MANIFEST.in 2013-05-09 18:39:35 +0000
+++ plainbox/MANIFEST.in 2013-09-13 17:12:45 +0000
@@ -1,9 +1,10 @@
1include README.md1include README.md
2include COPYING2include COPYING
3include mk-interesting-graphs.sh3include mk-interesting-graphs.sh
4recursive-include plainbox/test-data/ *.json *.xml *.txt4recursive-include plainbox/test-data *.json *.xml *.txt
5recursive-include docs *.rst5recursive-include docs *.rst
6include docs/conf.py6include docs/conf.py
7include plainbox/data/report/hardware-1_0.rng7recursive-include plainbox/data/report *.rng *.css *.xsl *.js
8recursive-include plainbox/data/report/images *.png
8include contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy9include contrib/policykit_yes/org.freedesktop.policykit.pkexec.policy
9include contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy10include contrib/policykit_auth_admin_keep/org.freedesktop.policykit.pkexec.policy
1011
=== added file 'plainbox/contrib/com.canonical.certification.PlainBox1.service'
--- plainbox/contrib/com.canonical.certification.PlainBox1.service 1970-01-01 00:00:00 +0000
+++ plainbox/contrib/com.canonical.certification.PlainBox1.service 2013-09-13 17:12:45 +0000
@@ -0,0 +1,3 @@
1[D-BUS Service]
2Name=com.canonical.certification.PlainBox1
3Exec=/usr/bin/plainbox service
04
=== added file 'plainbox/contrib/dbus-mini-client.py'
--- plainbox/contrib/dbus-mini-client.py 1970-01-01 00:00:00 +0000
+++ plainbox/contrib/dbus-mini-client.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,430 @@
1#!/usr/bin/env python3
2########
3#This simple script provides a small reference and example of how to
4#invoke plainbox methods through d-bus to accomplish useful tasks.
5#
6#Use of the d-feet tool is suggested for interactive exploration of
7#the plainbox objects, interfaces and d-bus API. However, d-feet can be
8#cumbersome to use for more advanced testing and experimentation.
9#
10#This script can be adapted to fit other testing needs as well.
11#
12#To run it, first launch plainbox in service mode using the stub provider:
13# $ plainbox -c stub service
14#
15#then run the script itself. It does the following things:
16#
17# 1- Obtain a whitelist and a job provider
18# 2- Use the whitelist to "qualify" jobs offered by the provider,
19# In essence filtering them to obtain a desired list of jobs to run.
20# 3- Run each job in the run_list, this implicitly updates the session's
21# results and state map.
22# 4- Print the job names and outcomes and some other data
23# 5- Export the session's data (job results) to xml in /tmp.
24#####
25
26import dbus
27from gi.repository import GObject
28from dbus.mainloop.glib import DBusGMainLoop
29from plainbox.abc import IJobResult
30
31bus = dbus.SessionBus(mainloop=DBusGMainLoop())
32
33# TODO: Create a class to remove all global var.
34current_job_path = None
35service = None
36session_object_path = None
37session_object = None
38run_list = None
39desired_job_list = None
40whitelist = None
41exports_count = 0
42
43def main():
44 global service
45 global session_object_path
46 global session_object
47 global run_list
48 global desired_job_list
49 global whitelist
50
51 whitelist = bus.get_object(
52 'com.canonical.certification.PlainBox1',
53 '/plainbox/whitelist/stub'
54 )
55
56 provider = bus.get_object(
57 'com.canonical.certification.PlainBox1',
58 '/plainbox/provider/stubbox'
59 )
60
61 #whitelist = bus.get_object(
62 # 'com.canonical.certification.PlainBox1',
63 # '/plainbox/whitelist/default'
64 #)
65
66 #provider = bus.get_object(
67 # 'com.canonical.certification.PlainBox1',
68 # '/plainbox/provider/checkbox'
69 #)
70
71 #A provider manages objects other than jobs.
72 provider_objects = provider.GetManagedObjects(
73 dbus_interface='org.freedesktop.DBus.ObjectManager')
74
75 #Create a session and "seed" it with my job list:
76 job_list = [k for k, v in provider_objects.items() if not 'whitelist' in k]
77 service = bus.get_object(
78 'com.canonical.certification.PlainBox1',
79 '/plainbox/service1'
80 )
81 session_object_path = service.CreateSession(
82 job_list,
83 dbus_interface='com.canonical.certification.PlainBox.Service1'
84 )
85 session_object = bus.get_object(
86 'com.canonical.certification.PlainBox1',
87 session_object_path
88 )
89
90 if session_object.PreviousSessionFile():
91 if ask_for_resume():
92 session_object.Resume()
93 else:
94 session_object.Clean()
95
96 #to get only the *jobs* that are designated by the whitelist.
97 desired_job_list = [
98 object for object in provider_objects if whitelist.Designates(
99 object,
100 dbus_interface='com.canonical.certification.PlainBox.WhiteList1')]
101
102 desired_local_job_list = sorted([
103 object for object in desired_job_list if
104 bus.get_object('com.canonical.certification.PlainBox1', object).Get(
105 'com.canonical.certification.CheckBox.JobDefinition1',
106 'plugin') == 'local'
107 ])
108
109 #Now I update the desired job list.
110 session_object.UpdateDesiredJobList(
111 desired_local_job_list,
112 dbus_interface='com.canonical.certification.PlainBox.Session1'
113 )
114
115 #Now, the run_list contains the list of jobs I actually need to run \o/
116 run_list = session_object.Get(
117 'com.canonical.certification.PlainBox.Session1',
118 'run_list'
119 )
120
121 # Add some signal receivers
122 bus.add_signal_receiver(
123 catchall_local_job_result_available_signals_handler,
124 dbus_interface="com.canonical.certification.PlainBox.Service1",
125 signal_name="JobResultAvailable")
126
127 # Start running jobs
128 print("[ Running All Local Jobs ]".center(80, '='))
129 run_local_jobs()
130
131 #PersistentSave can be called at any point to checkpoint session state.
132 #In here, we're just calling it at the end, as an example.
133 print("[ Saving the session ]".center(80, '='))
134 session_object.PersistentSave()
135
136
137def ask_for_outcome(prompt=None, allowed=None):
138 if prompt is None:
139 prompt = "what is the outcome? "
140 if allowed is None:
141 allowed = (IJobResult.OUTCOME_PASS, "p",
142 IJobResult.OUTCOME_FAIL, "f",
143 IJobResult.OUTCOME_SKIP, "s")
144 answer = None
145 while answer not in allowed:
146 print("Allowed answers are: {}".format(", ".join(allowed)))
147 answer = input(prompt)
148 # Useful shortcuts for testing
149 if answer == "f":
150 answer = IJobResult.OUTCOME_FAIL
151 if answer == "p":
152 answer = IJobResult.OUTCOME_PASS
153 if answer == "s":
154 answer = IJobResult.OUTCOME_SKIP
155 return answer
156
157
158def ask_for_test(prompt=None, allowed=None):
159 if prompt is None:
160 prompt = "Run the test command? "
161 if allowed is None:
162 allowed = ("y",
163 "n",
164 )
165 answer = None
166 while answer not in allowed:
167 print("Allowed answers are: {}".format(", ".join(allowed)))
168 answer = input(prompt)
169 return answer
170
171def ask_for_resume():
172 prompt = "Do you want to resume the previous session [Y/n]? "
173 allowed = ('', 'y', 'Y', 'n', 'N')
174 answer = None
175 while answer not in allowed:
176 answer = input(prompt)
177 return False if answer in ('n', 'N') else True
178
179
180# Asynchronous calls need reply handlers
181def handle_export_reply(s):
182 print("Export to buffer: I got {} bytes of export data".format(len(s)))
183 maybe_quit_after_export()
184
185def handle_export_to_file_reply(s):
186 print("Export to file: completed to {}".format(s))
187 maybe_quit_after_export()
188
189def maybe_quit_after_export():
190 # Two asynchronous callbacks calling this may result in a race
191 # condition. Don't do this at home, use a semaphore or lock.
192 global exports_count
193 exports_count += 1
194 if exports_count >= 2:
195 loop.quit()
196
197def handle_error(e):
198 print(str(e))
199 loop.quit()
200
201
202def catchall_ask_for_outcome_signals_handler(current_runner_path):
203 global current_job_path
204 job_def_object = bus.get_object(
205 'com.canonical.certification.PlainBox1', current_job_path)
206 job_cmd = job_def_object.Get(
207 'com.canonical.certification.CheckBox.JobDefinition1',
208 'command')
209 job_runner_object = bus.get_object(
210 'com.canonical.certification.PlainBox1', current_runner_path)
211 if job_cmd:
212 run_test = ask_for_test()
213 if run_test == 'y':
214 job_runner_object.RunCommand()
215 return
216 outcome_from_command = job_runner_object.Get(
217 'com.canonical.certification.PlainBox.RunningJob1',
218 'outcome_from_command')
219 print("Return code from the command indicates: {} ".format(
220 outcome_from_command))
221 outcome = ask_for_outcome()
222 comments = 'Test plainbox comments'
223 job_runner_object.SetOutcome(
224 outcome,
225 comments,
226 dbus_interface='com.canonical.certification.PlainBox.RunningJob1')
227
228
229def catchall_io_log_generated_signals_handler(offset, name, data):
230 try:
231 print("(<{}:{:05}>) {}".format(
232 name, int(offset), data.decode('UTF-8').rstrip()))
233 except UnicodeDecodeError:
234 pass
235
236
237def catchall_local_job_result_available_signals_handler(job, result):
238 # XXX: check if the job path actually matches the current_job_path
239 # Update the session job state map and run new jobs
240 global session_object
241 session_object.UpdateJobResult(
242 job, result,
243 reply_handler=run_local_jobs,
244 error_handler=handle_error,
245 dbus_interface='com.canonical.certification.PlainBox.Session1')
246
247
248def catchall_job_result_available_signals_handler(job, result):
249 # XXX: check if the job path actually matches the current_job_path
250 # Update the session job state map and run new jobs
251 global session_object
252 session_object.UpdateJobResult(
253 job, result,
254 reply_handler=run_jobs,
255 error_handler=handle_error,
256 dbus_interface='com.canonical.certification.PlainBox.Session1')
257
258
259def run_jobs():
260 global run_list
261 #Now the actual run, job by job.
262 if run_list:
263 job_path = run_list.pop(0)
264 global current_job_path
265 global session_object_path
266 current_job_path = job_path
267 job_def_object = bus.get_object(
268 'com.canonical.certification.PlainBox', current_job_path)
269 job_name = job_def_object.Get(
270 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
271 job_desc = job_def_object.Get(
272 'com.canonical.certification.PlainBox.JobDefinition1',
273 'description')
274 print("[ {} ]".format(job_name).center(80, '-'))
275 if job_desc:
276 print(job_desc)
277 print("^" * len(job_desc.splitlines()[-1]))
278 print()
279 service.RunJob(session_object_path, job_path)
280 else:
281 show_results()
282
283
284def run_local_jobs():
285 global run_list
286 global desired_job_list
287 global whitelist
288 if run_list:
289 job_path = run_list.pop(0)
290 global current_job_path
291 global session_object_path
292 current_job_path = job_path
293 job_def_object = bus.get_object(
294 'com.canonical.certification.PlainBox1', current_job_path)
295 job_name = job_def_object.Get(
296 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
297 job_desc = job_def_object.Get(
298 'com.canonical.certification.PlainBox.JobDefinition1',
299 'description')
300 print("[ {} ]".format(job_name).center(80, '-'))
301 if job_desc:
302 print(job_desc)
303 service.RunJob(session_object_path, job_path)
304 else:
305 #Now I update the desired job list to get jobs created from local jobs.
306 session_object.UpdateDesiredJobList(
307 desired_job_list,
308 dbus_interface='com.canonical.certification.PlainBox.Session1'
309 )
310 bus.add_signal_receiver(
311 catchall_ask_for_outcome_signals_handler,
312 dbus_interface="com.canonical.certification.PlainBox.Service1",
313 signal_name="AskForOutcome")
314
315 bus.add_signal_receiver(
316 catchall_io_log_generated_signals_handler,
317 dbus_interface="com.canonical.certification.PlainBox.Service1",
318 signal_name="IOLogGenerated",
319 byte_arrays=True) # To easily convert the byte arrays to strings
320
321 # Replace the job result handler we created for local jobs for by the
322 # one dedicated to regular job types
323 bus.remove_signal_receiver(
324 catchall_local_job_result_available_signals_handler,
325 dbus_interface="com.canonical.certification.PlainBox.Service1",
326 signal_name="JobResultAvailable")
327
328 bus.add_signal_receiver(
329 catchall_job_result_available_signals_handler,
330 dbus_interface="com.canonical.certification.PlainBox.Service1",
331 signal_name="JobResultAvailable")
332
333 job_list = session_object.Get(
334 'com.canonical.certification.PlainBox.Session1',
335 'job_list'
336 )
337
338 #to get only the *jobs* that are designated by the whitelist.
339 desired_job_list = [
340 object for object in job_list if whitelist.Designates(
341 object,
342 dbus_interface=
343 'com.canonical.certification.PlainBox.WhiteList1')]
344
345 #Now I update the desired job list.
346 # XXX: Remove previous local jobs from this list to avoid evaluating
347 # them twice
348 session_object.UpdateDesiredJobList(
349 desired_job_list,
350 dbus_interface='com.canonical.certification.PlainBox.Session1'
351 )
352
353 #Now, the run_list contains the list of jobs I actually need to run \o/
354 run_list = session_object.Get(
355 'com.canonical.certification.PlainBox.Session1',
356 'run_list'
357 )
358
359 print("[ Running All Jobs ]".center(80, '='))
360 run_jobs()
361
362
363def show_results():
364 global session_object_path
365 session_object = bus.get_object(
366 'com.canonical.certification.PlainBox1',
367 session_object_path
368 )
369 job_state_map = session_object.Get(
370 'com.canonical.certification.PlainBox.Session1', 'job_state_map')
371 print("[ Results ]".center(80, '='))
372 for k, job_state_path in job_state_map.items():
373 job_state_object = bus.get_object(
374 'com.canonical.certification.PlainBox1',
375 job_state_path
376 )
377 # Get the job definition object and some properties
378 job_def_path = job_state_object.Get(
379 'com.canonical.certification.PlainBox.JobState1', 'job')
380 job_def_object = bus.get_object(
381 'com.canonical.certification.PlainBox1', job_def_path)
382 job_name = job_def_object.Get(
383 'com.canonical.certification.PlainBox.JobDefinition1', 'name')
384 # Ask the via value (e.g. to comptute job categories)
385 # if a job is a child of a local job
386 job_via = job_def_object.Get(
387 'com.canonical.certification.CheckBox.JobDefinition1', 'via')
388
389 # Get the current job result object and the outcome property
390 job_result_path = job_state_object.Get(
391 'com.canonical.certification.PlainBox.JobState1', 'result')
392 job_result_object = bus.get_object(
393 'com.canonical.certification.PlainBox1', job_result_path)
394 outcome = job_result_object.Get(
395 'com.canonical.certification.PlainBox.Result1', 'outcome')
396 comments = job_result_object.Get(
397 'com.canonical.certification.PlainBox.Result1', 'comments')
398 io_log = job_result_object.Get(
399 'com.canonical.certification.PlainBox.Result1',
400 'io_log', byte_arrays=True)
401
402 print("{:55s} {:15s} {}".format(job_name, outcome, comments))
403 export_session()
404
405def export_session():
406 service.ExportSessionToFile(
407 session_object_path,
408 "xml",
409 [''],
410 "/tmp/report.xml",
411 reply_handler=handle_export_to_file_reply,
412 error_handler=handle_error
413 )
414 # The exports will apparently run in parallel. The callbacks
415 # are responsible for ensuring exiting after this.
416 service.ExportSession(
417 session_object_path,
418 "xml",
419 [''],
420 reply_handler=handle_export_reply,
421 error_handler=handle_error
422 )
423
424# Start the first call after a short delay
425GObject.timeout_add(5, main)
426loop = GObject.MainLoop()
427loop.run()
428
429# Stop the Plainbox dbus service
430service.Exit()
0431
=== added file 'plainbox/docs/author/checkbox-job-format.rst'
--- plainbox/docs/author/checkbox-job-format.rst 1970-01-01 00:00:00 +0000
+++ plainbox/docs/author/checkbox-job-format.rst 2013-09-13 17:12:45 +0000
@@ -0,0 +1,168 @@
1===================================
2Checkbox job file format and fields
3===================================
4
5This file contains NO examples, this is on purpose since the jobs
6directory contains several hundred examples showcasing all the features
7described here.
8
9File format and location
10------------------------
11Jobs are expressed as sections in text files that conform somewhat to
12the rfc822 specification format. Each section defines a single job. The
13section is delimited with an empty newline. Within each section, each
14field starts with the field name, a colon, a space and then the field
15contents. Multiple-line fields can be input by having a newline right
16after the colon, and then entering text lines after that, each line
17should start with at least one space.
18
19Fields that can be used on a job
20--------------------------------
21:name:
22 (mandatory) - A name for the job. Should be unique, an error will
23 be generated if there are duplicates. Should contain characters in
24 [a-z0-9/-].
25
26:plugin:
27
28 (mandatory) - For historical reasons it's called "plugin" but it's
29 better thought of as describing the "type" of job. The allowed types
30 are:
31
32 :manual: jobs that require the user to perform an action and then
33 decide on the test's outcome.
34 :shell: jobs that run without user intervention and
35 automatically set the test's outcome.
36 :user-interact: jobs that require the user to perform an
37 interaction, after which the outcome is automatically set.
38 :user-verify: jobs that automatically perform an action or test
39 and then request the user to decide on the test's outcome.
40 :attachment: jobs whose command output will be attached to the
41 test report or submission.
42 :local: a job whose command output needs to be in :term:`CheckBox` job
43 format. Jobs output by a local job will be added to the set of
44 available jobs to be run.
45 :resource: A job whose command output results in a set of rfc822
46 records, containing key/value pairs, and that can be used in other
47 jobs' ``requires`` expressions.
48
49:requires:
50 (optional). If specified, the job will only run if the conditions
51 expressed in this field are met.
52
53 Conditions are of the form ``<resource>.<key> <comparison-operator>
54 'value' (and|or) ...`` . Comparison operators can be ==, != and ``in``.
55 Values to compare to can be scalars or (in the case of the ``in``
56 operator) arrays or tuples. The ``not in`` operator is explicitly
57 unsupported.
58
59 Requirements can be logically chained with ``or`` and
60 ``and`` operators. They can also be placed in multiple lines,
61 respecting the rfc822 multi-line syntax, in which case all
62 requirements must be met for the job to run ( ``and`` ed).
63
64 The :term:`PlainBox` resource program evaluator is extensively documented,
65 to see a detailed description including rationale and implementation of
66 :term:`CheckBox` "legacy" compatibility, see :ref:`Resources in Plainbox
67 <resources>`.
68
69:depends:
70 (optional). If specified, the job will only run if all the listed
71 jobs have run and passed. Multiple job names, separated by spaces,
72 can be specified.
73
74:command:
75 (optional). A command can be provided, to be executed under specific
76 circumstances. For ``manual``, ``user-interact`` and ``user-verify``
77 jobs, the command will be executed when the user presses a "test"
78 button present in the user interface. For ``shell`` jobs, the
79 command will be executed unconditionally as soon as the job is
80 started. In both cases the exit code from the command (0 for
81 success, !0 for failure) will be used to set the test's outcome. For
82 ``manual``, ``user-interact`` and ``user-verify`` jobs, the user can
83 override the command's outcome. The command will be run using the
84 default system shell. If a specific shell is needed it should be
85 instantiated in the command. A multi-line command or shell script
86 can be used with the usual multi-line syntax.
87
88 Note that a ``shell`` job without a command will do nothing.
89
90:description:
91 (mandatory). Provides a textual description for the job. This is
92 mostly to aid people reading job descriptions in figuring out what a
93 job does.
94
95 The description field, however, is used specially in ``manual``,
96 ``user-interact`` and ``user-verify`` jobs. For these jobs, the
97 description will be shown in the user interface, and in these cases
98 it's expected to contain instructions for the user to follow, as
99 well as criteria for him to decide whether the job passes or fails.
100 For these types of jobs, the description needs to contain a few
101 sub-fields, in order:
102
103 :PURPOSE: This indicates the purpose or intent of the test.
104 :STEPS: A numbered list of steps for the user to follow.
105 :INFO:
106 (optional). Additional information about the test. This is
107 commonly used to present command output for the user to validate.
108 For this purpose, the ``$output`` substitution variable can be used
109 (actually, it can be used anywhere in the description). If present,
110 it will be replaced by the standard output generated from running
111 the job's command (commonly when the user presses the "Test"
112 button).
113 :VERIFICATION:
114 A question for the user to answer, deciding whether the test
115 passes or fails. The question should be phrased in such a way
116 that an answer of **Yes** means the test passed, and an answer of
117 **No** means it failed.
118:user:
119 (optional). If specified, the job will be run as the user specified
120 here. This is most commonly used to run jobs as the superuser
121 (root).
122
123:environ:
124 (optional). If specified, the listed environment variables
125 (separated by spaces) will be taken from the invoking environment
126 (i.e. the one :term:`CheckBox` is run under) and set to that value on the
127 job execution environment (i.e. the one the job will run under).
128 Note that only the *variable names* should be listed, not the
129 *values*, which will be taken from the existing environment. This
130 only makes sense for jobs that also have the ``user`` attribute.
131 This key provides a mechanism to account for security policies in
132 ``sudo`` and ``pkexec``, which provide a sanitized execution
133 environment, with the downside that useful configuration specified
134 in environment variables may be lost in the process.
135
136:estimated_duration:
137 (optional) This field contains metadata about how long the job is
138 expected to run for, as a positive float value indicating
139 the estimated job duration in seconds.
140
141===========================
142Extension of the job format
143===========================
144
145The :term:`CheckBox` job format can be considered "extensible", in that
146additional keys can be added to existing jobs to contain additional
147data that may be needed.
148
149In order for these extra fields to be exposed through the API (i.e. as
150properties of JobDefinition instances), they need to be declared as
151properties in (:mod:`plainbox.impl.job`). This is a good place to document,
152via a docstring, what the field is for and how to interpret it.
153
154Implementation note: if additional fields are added, *:term:`CheckBox`* needs
155to be also told about them, the reason is that :term:`CheckBox` *does* perform
156validation of the job descriptions, ensuring they contain only known fields and
157that fields contain expected data types. The jobs_info plugin contains the job
158schema declaration and can be consulted to verify the known fields, whether
159they are optional or mandatory, and the type of data they're expected to
160contain.
161
162Also, :term:`CheckBox` validates that fields contain data of a specific type,
163so care must be taken not to simply change contents of fields if
164:term:`CheckBox` compatibility of jobs is desired.
165
166:term:`PlainBox` does this validation on a per-accessor basis, so data in each
167field must make sense as defined by that field's accessor. There is no need,
168however, to declare field type beforehand.
0169
=== modified file 'plainbox/docs/author/index.rst'
--- plainbox/docs/author/index.rst 2013-03-25 16:40:57 +0000
+++ plainbox/docs/author/index.rst 2013-09-13 17:12:45 +0000
@@ -11,6 +11,9 @@
11 is a guiding point for subsequent editions that will expand and provide11 is a guiding point for subsequent editions that will expand and provide
12 real value.12 real value.
1313
14.. toctree::
15 checkbox-job-format.rst
16
14Personas and stories17Personas and stories
15--------------------18--------------------
1619
1720
=== modified file 'plainbox/docs/dev/reference.rst'
--- plainbox/docs/dev/reference.rst 2013-05-14 09:04:30 +0000
+++ plainbox/docs/dev/reference.rst 2013-09-13 17:12:45 +0000
@@ -94,6 +94,11 @@
94 :undoc-members:94 :undoc-members:
95 :show-inheritance:95 :show-inheritance:
9696
97.. automodule:: plainbox.impl.exporter.html
98 :members:
99 :undoc-members:
100 :show-inheritance:
101
97.. automodule:: plainbox.impl.secure102.. automodule:: plainbox.impl.secure
98 :members:103 :members:
99 :undoc-members:104 :undoc-members:
@@ -144,11 +149,6 @@
144 :undoc-members:149 :undoc-members:
145 :show-inheritance:150 :show-inheritance:
146151
147.. automodule:: plainbox.impl.mock_job
148 :members:
149 :undoc-members:
150 :show-inheritance:
151
152.. automodule:: plainbox.impl.resource152.. automodule:: plainbox.impl.resource
153 :members:153 :members:
154 :undoc-members:154 :undoc-members:
@@ -173,6 +173,43 @@
173 :undoc-members:173 :undoc-members:
174 :show-inheritance:174 :show-inheritance:
175175
176.. automodule:: plainbox.impl.session.state
177 :members:
178 :undoc-members:
179 :show-inheritance:
180
181.. automodule:: plainbox.impl.session.jobs
182 :members:
183 :undoc-members:
184 :show-inheritance:
185
186.. automodule:: plainbox.impl.session.storage
187 :members:
188 :undoc-members:
189 :show-inheritance:
190
191.. automodule:: plainbox.impl.session.suspend
192 :members:
193 :undoc-members:
194 :private-members:
195 :show-inheritance:
196
197.. automodule:: plainbox.impl.session.resume
198 :members:
199 :undoc-members:
200 :private-members:
201 :show-inheritance:
202
203.. automodule:: plainbox.impl.session.legacy
204 :members:
205 :undoc-members:
206 :show-inheritance:
207
208.. automodule:: plainbox.impl.session.manager
209 :members:
210 :undoc-members:
211 :show-inheritance:
212
176.. automodule:: plainbox.impl.testing_utils213.. automodule:: plainbox.impl.testing_utils
177 :members:214 :members:
178 :undoc-members:215 :undoc-members:
179216
=== modified file 'plainbox/docs/dev/resources.rst'
--- plainbox/docs/dev/resources.rst 2013-05-11 13:56:36 +0000
+++ plainbox/docs/dev/resources.rst 2013-09-13 17:12:45 +0000
@@ -1,3 +1,5 @@
1.. _resources:
2
1Resources3Resources
2=========4=========
35
46
=== modified file 'plainbox/plainbox/abc.py'
--- plainbox/plainbox/abc.py 2013-02-25 11:02:58 +0000
+++ plainbox/plainbox/abc.py 2013-09-13 17:12:45 +0000
@@ -114,13 +114,46 @@
114 # XXX: We could also store stuff like job duration and other meta-data but114 # XXX: We could also store stuff like job duration and other meta-data but
115 # I wanted to avoid polluting this proposal with mundane details115 # I wanted to avoid polluting this proposal with mundane details
116116
117 @abstractproperty117 # The outcome of a job is a one-word classification how how it ran. There
118 def job(self):118 # are several values that were not used in the original implementation but
119 """119 # their existence helps to organize and implement plainbox. They are
120 Definition of the job120 # discussed below to make their intended meaning more detailed than is
121 # possible from the variable name alone.
122 #
123 # The None outcome - a job that basically did not run at all.
124 OUTCOME_NONE = None
125 # The pass and fail outcomes are the two most essential, and externally
126 # visible, job outcomes. They can be provided by either automated or manual
127 # "classifier" - a script or a person that clicks a "pass" or "fail"
128 # button.
129 OUTCOME_PASS = 'pass'
130 OUTCOME_FAIL = 'fail'
131 # The skip outcome is used when the operator selected a job but then
132 # skipped it. This is typically used for a manual job that is tedious or
133 # was selected by accident.
134 OUTCOME_SKIP = 'skip'
135 # The not supported outcome is used when a job was about to run but a
136 # dependency or resource requirement prevent it from running. XXX: perhaps
137 # this should be called "not available", not supported has the "unsupported
138 # code" feeling associated with it.
139 OUTCOME_NOT_SUPPORTED = 'not-supported'
140 # A temporary state that should be removed later on, used to indicate that
141 # job runner is not implemented but the job "ran" so to speak.
142 OUTCOME_NOT_IMPLEMENTED = 'not-implemented'
143 # A temporary state before the user decides on the outcome of a manual
144 # job or any other job that requires manual verification
145 OUTCOME_UNDECIDED = 'undecided'
121146
122 The object implements IJobDefinition147 # List of all valid values of OUTCOME_xxx
123 """148 ALL_OUTCOME_LIST = [
149 OUTCOME_NONE,
150 OUTCOME_PASS,
151 OUTCOME_FAIL,
152 OUTCOME_SKIP,
153 OUTCOME_NOT_SUPPORTED,
154 OUTCOME_NOT_IMPLEMENTED,
155 OUTCOME_UNDECIDED,
156 ]
124157
125 @abstractproperty158 @abstractproperty
126 def outcome(self):159 def outcome(self):
@@ -204,3 +237,79 @@
204 May raise NotImplementedError if the user interface cannot provide this237 May raise NotImplementedError if the user interface cannot provide this
205 answer.238 answer.
206 """239 """
240
241
242class IProviderBackend1(metaclass=ABCMeta):
243 """
244 Provider for the current type of tests.
245
246 This class provides the APIs required by the internal implementation
247 that are not considered normal public APIs. The only consumer of the
248 those methods and properties are internal to plainbox.
249 """
250
251 @abstractproperty
252 def CHECKBOX_SHARE(self):
253 """
254 Return the required value of CHECKBOX_SHARE environment variable.
255
256 .. note::
257 This variable is only required by one script.
258 It would be nice to remove this later on.
259 """
260
261 @abstractproperty
262 def extra_PYTHONPATH(self):
263 """
264 Return additional entry for PYTHONPATH, if needed.
265
266 This entry is required for CheckBox scripts to import the correct
267 CheckBox python libraries.
268
269 .. note::
270 The result may be None
271 """
272
273 @abstractproperty
274 def extra_PATH(self):
275 """
276 Return additional entry for PATH
277
278 This entry is required to lookup CheckBox scripts.
279 """
280
281
282class IProvider1(metaclass=ABCMeta):
283 """
284 Provider for the current type of tests
285
286 Also known as the 'checkbox-like' provider.
287 """
288
289 @abstractproperty
290 def name(self):
291 """
292 name of this provider
293
294 This name should be dbus-friendly. It should not be localizable.
295 """
296
297 @abstractproperty
298 def description(self):
299 """
300 description of this providr
301
302 This name should be dbus-friendly. It should not be localizable.
303 """
304
305 @abstractmethod
306 def get_builtin_jobs(self):
307 """
308 Load all the built-in jobs and return them
309 """
310
311 @abstractmethod
312 def get_builtin_whitelists(self):
313 """
314 Load all the built-in whitelists and return them
315 """
207316
=== added file 'plainbox/plainbox/data/report/checkbox.js'
--- plainbox/plainbox/data/report/checkbox.js 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/data/report/checkbox.js 2013-09-13 17:12:45 +0000
@@ -0,0 +1,16 @@
1function showHide(what) {
2 var heading = document.getElementById(what);
3 var contents = document.getElementById(what + "-contents");
4 var headingcontents = heading.innerHTML;
5 var newcontents;
6
7 if (contents.style.display != "block") {
8 newcontents = headingcontents.replace("closed", "open");
9 contents.style.display = "block";
10 } else {
11 newcontents = headingcontents.replace("open", "closed");
12 contents.style.display = "none";
13 }
14
15 heading.innerHTML = newcontents;
16}
017
=== added directory 'plainbox/plainbox/data/report/images'
=== added file 'plainbox/plainbox/data/report/images/body_bg.png'
1Binary 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 differ18Binary 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
=== added file 'plainbox/plainbox/data/report/images/bullet.png'
2Binary 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 differ19Binary 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
=== added file 'plainbox/plainbox/data/report/images/closed.png'
3Binary 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 differ20Binary 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
=== added file 'plainbox/plainbox/data/report/images/fail.png'
4Binary 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 differ21Binary 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
=== added file 'plainbox/plainbox/data/report/images/header_bg.png'
5Binary 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 differ22Binary 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
=== added file 'plainbox/plainbox/data/report/images/open.png'
6Binary 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 differ23Binary 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
=== added file 'plainbox/plainbox/data/report/images/pass.png'
7Binary 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 differ24Binary 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
=== added file 'plainbox/plainbox/data/report/images/skip.png'
8Binary 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 differ25Binary 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
=== added file 'plainbox/plainbox/data/report/styles.css'
--- plainbox/plainbox/data/report/styles.css 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/data/report/styles.css 2013-09-13 17:12:45 +0000
@@ -0,0 +1,258 @@
1body {
2 font-family: "Ubuntu Beta", "Bitstream Vera Sans", DejaVu Sans, Tahoma, sans-serif;
3 color: #333;
4 background: white url(report/images/body_bg.png);
5 font-size: 12px;
6 line-height: 14px;
7 margin: 0px;
8 padding: 0px;
9}
10#container {
11 background: #f7f6f5;
12 margin: 0px auto 20px;
13 padding: 0px;
14 width: 976px;
15}
16#container-inner {
17 background-color: #dfdcd9;
18}
19#header, #container-inner {
20 -moz-border-radius: 0px 0px 5px 5px;
21 -webkit-border-bottom-left-radius: 5px;
22 -webkit-border-bottom-right-radius: 5px;
23 -moz-box-shadow: #bbb 0px 0px 5px;
24 -webkit-box-shadow: #bbb 0px 0px 5px;
25}
26#header {
27 background: #dd4814 url(report/images/header_bg.png) top left repeat-x;
28 height: 64px;
29 margin: 0px;
30 padding: 0px;
31 position: relative;
32}
33
34#menu-search {
35 height: 40px;
36 margin: 0 16px;
37}
38
39#title {
40 padding: 28px 24px;
41}
42
43#content {
44 /*padding: 32px 80px 32px 80px;*/
45 padding: 32px 240px 32px 160px;
46 margin: 0 16px 16px;
47 width: 544px;
48 background-color: #fff;
49 -moz-border-radius: 4px;
50 -webkit-border-radius: 4px;
51}
52#end-content {
53 clear: both;
54}
55
56#content-panel {
57 width: 446px;
58 margin: 0px 0px 0px 0px;
59 padding: 8px 8px 32px 8px;
60 background-color: #fff;
61 -moz-border-radius: 4px;
62 -webkit-border-radius: 4px;
63}
64
65#copyright {
66 background-position: 803px 40px;
67 background-repeat: no-repeat;
68 text-align: center;
69 margin: 0 16px;
70 padding: 40px 0 0 0;
71 height: 32px;
72}
73#copyright p {
74 color: #aea79f;
75 font-size: 10px;
76 line-height: 14px;
77 margin: 2px 0;
78}
79
80#footer {
81 padding-top: 16px;
82}
83#footer * {
84 font-size: 10px;
85 line-height: 14px;
86}
87#footer p {
88 margin: 0;
89 padding-bottom: 3px;
90 border-bottom: 1px dotted #aea79f;
91}
92#footer p.footer-title {
93 font-weight: bold;
94}
95#footer .footer-div {
96 width: 144px;
97 float: left;
98 margin-left: 16px;
99}
100#footer .last-div {
101 margin-right: 16px;
102}
103#footer ul {
104 list-style: none;
105 margin: 0;
106 padding: 0;
107}
108#footer li {
109 margin: 0;
110 padding: 3px 0;
111 border-bottom: 1px dotted #aea79f;
112}
113
114h1, h2, h3, h4, h5 {
115 padding: 0;
116 margin: 0;
117 font-weight: normal;
118}
119h1 {
120 font-size: 36px;
121 line-height: 40px;
122 color: #dd4814;
123}
124h2 {
125 font-size: 24px;
126 line-height: 28px;
127 margin-bottom: 8px;
128}
129h3 {
130 font-size: 16px;
131 line-height: 20px;
132 margin-bottom: 8px;
133}
134h3.link-other {
135 color: #333;
136}
137h3.link-services {
138 color: #fff;
139}
140h4 {
141 font-size: 12px;
142 line-height: 14px;
143}
144h4.partners {
145 color: #333;
146 font-size: 16px;
147 line-height: 20px;
148}
149h5 {
150 color: #333;
151 font-size: 10px;
152 line-height: 14px;
153}
154h1 span.grey, h2 span.grey, h1 span, h2 span{
155 color: #aea79f;
156}
157p {
158 font-size: 12px;
159 line-height: 14px;
160 margin-bottom: 8px;
161}
162strong {
163 font-weight: bold;
164}
165
166a {
167 color: #333;
168 text-decoration: none;
169}
170a:hover {
171 color: #dd4814;
172 text-decoration: underline;
173}
174div.footer-div:hover a, div#content:hover a {
175 color: #dd4814;
176 text-decoration: none;
177}
178div.footer-div:hover a:hover, div#content:hover a:hover {
179 color: #dd4814;
180 text-decoration: underline;
181}
182
183ul {
184 margin-bottom: 16px;
185 list-style-image: url(report/images/bullet.png);
186}
187ul li {
188 margin-bottom: 8px;
189 line-height: 14px;
190}
191ul li:last-child {
192 margin-bottom: 0px;
193}
194
195p.call-to-action {
196 color: #333;
197}
198p.case-study {
199 color: #333;
200}
201p.highlight {
202 font-size: 16px;
203 line-height: 20px;
204}
205p.introduction {
206 color: #333;
207 font-size: 16px;
208 line-height: 20px;
209}
210p.services {
211 color: #fff;
212}
213p.small-text {
214 color: #333;
215 font-size: 10px;
216}
217
218/* Clearing floats without extra markup
219Based on How To Clear Floats Without Structural Markup by PiE
220[http://www.positioniseverything.net/easyclearing.html] */
221.clearfix:after {
222 content: ".";
223 display: block;
224 height: 0;
225 clear: both;
226 visibility: hidden;
227}
228.clearfix {
229 -moz-border-radius: 5px 5px 5px 5px;
230 -webkit-border-bottom-top-radius: 5px;
231 -webkit-border-bottom-left-radius: 5px;
232 -webkit-border-bottom-bottom-radius: 5px;
233 -webkit-border-bottom-right-radius: 5px;
234 -moz-box-shadow: #bbb 0px 0px 5px;
235 -webkit-box-shadow: #bbb 0px 0px 5px;
236 display: inline-block;
237} /* for IE/Mac */
238td
239{
240 margin: 0;
241 padding-bottom: 3px;
242 border-bottom: 1px dotted #aea79f;
243 font-size: 10px;
244 line-height: 14px;
245}
246.resultimg
247{
248 height: 12px;
249}
250.disclosureimg
251{
252 height: .75em;
253 vertical-align: middle;
254}
255.data
256{
257 display: none;
258}
0259
=== modified file 'plainbox/plainbox/impl/applogic.py'
--- plainbox/plainbox/impl/applogic.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/applogic.py 2013-09-13 17:12:45 +0000
@@ -30,8 +30,9 @@
30import os30import os
31import re31import re
3232
33from plainbox.abc import IJobResult
33from plainbox.impl import config34from plainbox.impl import config
34from plainbox.impl.result import JobResult35from plainbox.impl.result import MemoryJobResult
3536
3637
37class IJobQualifier(metaclass=ABCMeta):38class IJobQualifier(metaclass=ABCMeta):
@@ -66,6 +67,13 @@
66 self._pattern = re.compile(pattern)67 self._pattern = re.compile(pattern)
67 self._pattern_text = pattern68 self._pattern_text = pattern
6869
70 @property
71 def pattern_text(self):
72 """
73 text of the regular expression embedded in this qualifier
74 """
75 return self._pattern_text
76
69 def designates(self, job):77 def designates(self, job):
70 return self._pattern.match(job.name)78 return self._pattern.match(job.name)
7179
@@ -113,6 +121,86 @@
113 return False121 return False
114122
115123
124# NOTE: using CompositeQualifier seems strange but it's a tested proven
125# component so all we have to ensure is that we read the whitelist files
126# correctly.
127class WhiteList(CompositeQualifier):
128 """
129 A qualifier that understands checkbox whitelist files.
130
131 A whitelist file is a plain text, line oriented file. Each line represents
132 a regular expression pattern that can be matched against the name of a job.
133
134 The file can contain simple shell-style comments that begin with the pound
135 or hash key (#). Those are ignored. Comments can span both a fraction of a
136 line as well as the whole line.
137
138 For historical reasons each pattern has an implicit '^' and '$' prepended
139 and appended (respectively) to the actual pattern specified in the file.
140 """
141
142 def __init__(self, pattern_list, name=None):
143 """
144 Initialize a whitelist object with the specified list of patterns.
145
146 The patterns must be already mangled with '^' and '$'.
147 """
148 inclusive = [RegExpJobQualifier(pattern) for pattern in pattern_list]
149 exclusive = ()
150 super(WhiteList, self).__init__(inclusive, exclusive)
151 self._name = name
152
153 def __repr__(self):
154 return "<{} name:{!r}>".format(self.__class__.__name__, self.name)
155
156 @property
157 def name(self):
158 """
159 name of this WhiteList (might be None)
160 """
161 return self._name
162
163 @classmethod
164 def from_file(cls, pathname):
165 """
166 Load and initialize the WhiteList object from the specified file.
167
168 :param pathname: file to load
169 :returns: a fresh WhiteList object
170 """
171 pattern_list = cls._load_patterns(pathname)
172 name = os.path.splitext(os.path.basename(pathname))[0]
173 return cls(pattern_list, name=name)
174
175 @classmethod
176 def _load_patterns(self, pathname):
177 """
178 Load whitelist patterns from the specified file
179 """
180 pattern_list = []
181 # Load the file
182 with open(pathname, "rt", encoding="UTF-8") as stream:
183 for line in stream:
184 # Strip shell-style comments if there are any
185 try:
186 index = line.index("#")
187 except ValueError:
188 pass
189 else:
190 line = line[:index]
191 # Strip whitespace
192 line = line.strip()
193 # Skip empty lines (especially after stripping comments)
194 if line == "":
195 continue
196 # Surround the pattern with ^ and $
197 # so that it wont just match a part of the job name.
198 regexp_pattern = r"^{pattern}$".format(pattern=line)
199 # Accumulate patterns into the list
200 pattern_list.append(regexp_pattern)
201 return pattern_list
202
203
116def get_matching_job_list(job_list, qualifier):204def get_matching_job_list(job_list, qualifier):
117 """205 """
118 Get a list of jobs that are designated by the specified qualifier.206 Get a list of jobs that are designated by the specified qualifier.
@@ -137,16 +225,15 @@
137 # OUTCOME_NOT_SUPPORTED _except_ if any of the inhibitors point to225 # OUTCOME_NOT_SUPPORTED _except_ if any of the inhibitors point to
138 # a job with an OUTCOME_SKIP outcome, if that is the case mirror226 # a job with an OUTCOME_SKIP outcome, if that is the case mirror
139 # that outcome. This makes 'skip' stronger than 'not-supported'227 # that outcome. This makes 'skip' stronger than 'not-supported'
140 outcome = JobResult.OUTCOME_NOT_SUPPORTED228 outcome = IJobResult.OUTCOME_NOT_SUPPORTED
141 for inhibitor in job_state.readiness_inhibitor_list:229 for inhibitor in job_state.readiness_inhibitor_list:
142 if inhibitor.cause != inhibitor.FAILED_DEP:230 if inhibitor.cause != inhibitor.FAILED_DEP:
143 continue231 continue
144 related_job_state = session.job_state_map[232 related_job_state = session.job_state_map[
145 inhibitor.related_job.name]233 inhibitor.related_job.name]
146 if related_job_state.result.outcome == JobResult.OUTCOME_SKIP:234 if related_job_state.result.outcome == IJobResult.OUTCOME_SKIP:
147 outcome = JobResult.OUTCOME_SKIP235 outcome = IJobResult.OUTCOME_SKIP
148 job_result = JobResult({236 job_result = MemoryJobResult({
149 'job': job,
150 'outcome': outcome,237 'outcome': outcome,
151 'comments': job_state.get_readiness_description()238 'comments': job_state.get_readiness_description()
152 })239 })
@@ -177,9 +264,20 @@
177 section="sru",264 section="sru",
178 help_text="Location of the fallback file")265 help_text="Location of the fallback file")
179266
267 whitelist = config.Variable(
268 section="sru",
269 help_text="Optional whitelist with which to run SRU testing")
270
180 environment = config.Section(271 environment = config.Section(
181 help_text="Environment variables for scripts and jobs")272 help_text="Environment variables for scripts and jobs")
182273
274 default_provider = config.Variable(
275 section="common",
276 help_text="Name of the default provider to use",
277 validator_list=[
278 config.ChoiceValidator(['auto', 'src', 'deb', 'stub', 'ihv'])],
279 default="auto")
280
183 class Meta:281 class Meta:
184282
185 # TODO: properly depend on xdg and use real code that also handles283 # TODO: properly depend on xdg and use real code that also handles
186284
=== modified file 'plainbox/plainbox/impl/box.py'
--- plainbox/plainbox/impl/box.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/box.py 2013-09-13 17:12:45 +0000
@@ -26,55 +26,28 @@
26 THIS MODULE DOES NOT HAVE STABLE PUBLIC API26 THIS MODULE DOES NOT HAVE STABLE PUBLIC API
27"""27"""
2828
29import argparse
30import errno
31import logging29import logging
32import pdb
33import sys
3430
35from plainbox import __version__ as version31from plainbox import __version__ as version
36from plainbox.impl.applogic import PlainBoxConfig32from plainbox.impl.applogic import PlainBoxConfig
37from plainbox.impl.checkbox import CheckBox33from plainbox.impl.commands import PlainBoxToolBase
38from plainbox.impl.commands.check_config import CheckConfigCommand34from plainbox.impl.commands.check_config import CheckConfigCommand
39from plainbox.impl.commands.dev import DevCommand35from plainbox.impl.commands.dev import DevCommand
40from plainbox.impl.commands.run import RunCommand36from plainbox.impl.commands.run import RunCommand
41from plainbox.impl.commands.selftest import SelfTestCommand37from plainbox.impl.commands.selftest import SelfTestCommand
38from plainbox.impl.commands.service import ServiceCommand
42from plainbox.impl.commands.sru import SRUCommand39from plainbox.impl.commands.sru import SRUCommand
43from plainbox.impl.logging import setup_logging, adjust_logging40from plainbox.impl.logging import setup_logging
4441
4542
46logger = logging.getLogger("plainbox.box")43logger = logging.getLogger("plainbox.box")
4744
4845
49class PlainBox:46class PlainBoxTool(PlainBoxToolBase):
50 """47 """
51 Command line interface to PlainBox48 Command line interface to PlainBox
52 """49 """
5350
54 def __init__(self):
55 """
56 Initialize all the variables, real stuff happens in main()
57 """
58 self._early_parser = None # set in _early_init()
59 self._config = None # set in _late_init()
60 self._checkbox = None # set in _late_init()
61 self._parser = None # set in _late_init()
62
63 def main(self, argv=None):
64 """
65 Run as if invoked from command line directly
66 """
67 self.early_init()
68 early_ns = self._early_parser.parse_args(argv)
69 self.late_init(early_ns)
70 logger.debug("parsed early namespace: %s", early_ns)
71 # parse the full command line arguments, this is also where we
72 # do argcomplete-dictated exit if bash shell completion is requested
73 ns = self._parser.parse_args(argv)
74 logger.debug("parsed full namespace: %s", ns)
75 self.final_init(ns)
76 return self.dispatch_and_catch_exceptions(ns)
77
78 @classmethod51 @classmethod
79 def get_config_cls(cls):52 def get_config_cls(cls):
80 """53 """
@@ -107,221 +80,17 @@
107 top-level subcommands.80 top-level subcommands.
108 """81 """
109 # TODO: switch to plainbox plugins82 # TODO: switch to plainbox plugins
110 RunCommand(self._checkbox).register_parser(subparsers)83 RunCommand(self._provider).register_parser(subparsers)
111 SelfTestCommand().register_parser(subparsers)84 SelfTestCommand().register_parser(subparsers)
112 SRUCommand(self._checkbox, self._config).register_parser(subparsers)85 SRUCommand(self._provider, self._config).register_parser(subparsers)
113 CheckConfigCommand(self._config).register_parser(subparsers)86 CheckConfigCommand(self._config).register_parser(subparsers)
114 DevCommand(self._checkbox, self._config).register_parser(subparsers)87 DevCommand(self._provider, self._config).register_parser(subparsers)
11588 ServiceCommand(self._provider, self._config).register_parser(
116 def early_init(self):89 subparsers)
117 """
118 Do very early initialization. This is where we initalize stuff even
119 without seeing a shred of command line data or anything else.
120 """
121 self._early_parser = self.construct_early_parser()
122
123 def late_init(self, early_ns):
124 """
125 Initialize with early command line arguments being already parsed
126 """
127 adjust_logging(
128 level=early_ns.log_level, trace_list=early_ns.trace,
129 debug_console=early_ns.debug_console)
130 # Load plainbox configuration
131 self._config = self.get_config_cls().get()
132 # Load and initialize checkbox provider
133 # TODO: rename to provider, switch to plugins
134 self._checkbox = CheckBox(
135 mode=None if early_ns.checkbox == 'auto' else early_ns.checkbox)
136 # Construct the full command line argument parser
137 self._parser = self.construct_parser()
138
139 def final_init(self, ns):
140 """
141 Do some final initialization just before the command gets
142 dispatched. This is empty here but maybe useful for subclasses.
143 """
144
145 def construct_early_parser(self):
146 """
147 Create a parser that captures some of the early data we need to
148 be able to have a real parser and initialize the rest.
149 """
150 parser = argparse.ArgumentParser(add_help=False)
151 # Fake --help and --version
152 parser.add_argument("-h", "--help", action="store_const", const=None)
153 parser.add_argument("--version", action="store_const", const=None)
154 self.add_early_parser_arguments(parser)
155 # A catch-all net for everything else
156 parser.add_argument("rest", nargs="...")
157 return parser
158
159 def construct_parser(self):
160 parser = argparse.ArgumentParser(prog=self.get_exec_name())
161 parser.add_argument(
162 "--version", action="version", version=self.get_exec_version())
163 # Add all the things really parsed by the early parser so that it
164 # shows up in --help and bash tab completion.
165 self.add_early_parser_arguments(parser)
166 subparsers = parser.add_subparsers()
167 self.add_subcommands(subparsers)
168 # Enable argcomplete if it is available.
169 try:
170 import argcomplete
171 except ImportError:
172 pass
173 else:
174 argcomplete.autocomplete(parser)
175 return parser
176
177 def add_early_parser_arguments(self, parser):
178 # Since we need a CheckBox instance to create the main argument parser
179 # and we need to be able to specify where Checkbox is, we parse that
180 # option alone before parsing everything else
181 # TODO: rename this to -p | --provider
182 parser.add_argument(
183 '-c', '--checkbox',
184 action='store',
185 # TODO: have some public API for this, pretty please
186 choices=list(CheckBox._DIRECTORY_MAP.keys()) + ['auto'],
187 default='auto',
188 help="where to find the installation of CheckBox.")
189 group = parser.add_argument_group(
190 title="logging and debugging")
191 # Add the --log-level argument
192 group.add_argument(
193 "-l", "--log-level",
194 action="store",
195 choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),
196 default=None,
197 help=argparse.SUPPRESS)
198 # Add the --verbose argument
199 group.add_argument(
200 "-v", "--verbose",
201 dest="log_level",
202 action="store_const",
203 const="INFO",
204 help="be more verbose (same as --log-level=INFO)")
205 # Add the --debug flag
206 group.add_argument(
207 "-D", "--debug",
208 dest="log_level",
209 action="store_const",
210 const="DEBUG",
211 help="enable DEBUG messages on the root logger")
212 # Add the --debug flag
213 group.add_argument(
214 "-C", "--debug-console",
215 action="store_true",
216 help="display DEBUG messages in the console")
217 # Add the --trace flag
218 group.add_argument(
219 "-T", "--trace",
220 metavar="LOGGER",
221 action="append",
222 default=[],
223 help=("enable DEBUG messages on the specified logger "
224 "(can be used multiple times)"))
225 # Add the --pdb flag
226 group.add_argument(
227 "-P", "--pdb",
228 action="store_true",
229 default=False,
230 help="jump into pdb (python debugger) when a command crashes")
231 # Add the --debug-interrupt flag
232 group.add_argument(
233 "-I", "--debug-interrupt",
234 action="store_true",
235 default=False,
236 help="crash on SIGINT/KeyboardInterrupt, useful with --pdb")
237
238 def dispatch_command(self, ns):
239 # Argh the horrror!
240 #
241 # Since CPython revision cab204a79e09 (landed for python3.3)
242 # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py
243 # the argparse module behaves differently than it did in python3.2
244 #
245 # In practical terms subparsers are now optional in 3.3 so all of the
246 # commands are no longer required parameters.
247 #
248 # To compensate, on python3.3 and beyond, when the user just runs
249 # plainbox without specifying the command, we manually, explicitly do
250 # what python3.2 did: call parser.error(_('too few arguments'))
251 if (sys.version_info[:2] >= (3, 3)
252 and getattr(ns, "command", None) is None):
253 self._parser.error(argparse._("too few arguments"))
254 else:
255 return ns.command.invoked(ns)
256
257 def dispatch_and_catch_exceptions(self, ns):
258 try:
259 return self.dispatch_command(ns)
260 except SystemExit:
261 # Don't let SystemExit be caught in the logic below, we really
262 # just want to exit when that gets thrown.
263 logger.debug("caught SystemExit, exiting")
264 # We may want to raise SystemExit as it can carry a status code
265 # along and we cannot just consume that.
266 raise
267 except BaseException as exc:
268 logger.debug("caught %r, deciding on what to do next", exc)
269 # For all other exceptions (and I mean all), do a few checks
270 # and perform actions depending on the command line arguments
271 # By default we want to re-raise the exception
272 action = 'raise'
273 # We want to ignore IOErrors that are really EPIPE
274 if isinstance(exc, IOError):
275 if exc.errno == errno.EPIPE:
276 action = 'ignore'
277 # We want to ignore KeyboardInterrupt unless --debug-interrupt
278 # was passed on command line
279 elif isinstance(exc, KeyboardInterrupt):
280 if ns.debug_interrupt:
281 action = 'debug'
282 else:
283 action = 'ignore'
284 else:
285 # For all other execptions, debug if requested
286 if ns.pdb:
287 action = 'debug'
288 logger.debug("action for exception %r is %s", exc, action)
289 if action == 'ignore':
290 return 0
291 elif action == 'raise':
292 logging.getLogger("plainbox.crashes").fatal(
293 "Executable %r invoked with %r has crashed",
294 self.get_exec_name(), ns, exc_info=1)
295 raise
296 elif action == 'debug':
297 logger.error("caught runaway exception: %r", exc)
298 logger.error("starting debugger...")
299 pdb.post_mortem()
300 return 1
30190
30291
303def main(argv=None):92def main(argv=None):
304 # Another try/catch block for catching KeyboardInterrupt93 raise SystemExit(PlainBoxTool().main(argv))
305 # This one is really only meant for the early init abort
306 # (when someone runs main but bails out before we really
307 # get to the point when we do something useful and setup
308 # all the exception handlers).
309 try:
310 raise SystemExit(PlainBox().main(argv))
311 except KeyboardInterrupt:
312 pass
313
314
315def get_builtin_jobs():
316 raise NotImplementedError("get_builtin_jobs() not implemented")
317
318
319def save(something, somewhere):
320 raise NotImplementedError("save() not implemented")
321
322
323def run(*args, **kwargs):
324 raise NotImplementedError("run() not implemented")
32594
32695
327# Setup logging before anything else starts working.96# Setup logging before anything else starts working.
32897
=== modified file 'plainbox/plainbox/impl/commands/__init__.py'
--- plainbox/plainbox/impl/commands/__init__.py 2013-02-25 11:02:58 +0000
+++ plainbox/plainbox/impl/commands/__init__.py 2013-09-13 17:12:45 +0000
@@ -27,11 +27,26 @@
27"""27"""
2828
29from abc import abstractmethod, ABCMeta29from abc import abstractmethod, ABCMeta
30import argparse
31import errno
32import logging
33import pdb
34import sys
35
36from plainbox.impl.logging import adjust_logging
37from plainbox.impl.providers.v1 import all_providers
38
39
40logger = logging.getLogger("plainbox.commands")
3041
3142
32class PlainBoxCommand(metaclass=ABCMeta):43class PlainBoxCommand(metaclass=ABCMeta):
33 """44 """
34 Simple interface class for plainbox commands45 Simple interface class for plainbox commands.
46
47 Command objects like this are consumed by PlainBoxTool subclasses to
48 implement hierarchical command system. The API supports arbitrary
49 many sub commands in arbitrary nesting arrangement.
35 """50 """
3651
37 @abstractmethod52 @abstractmethod
@@ -49,3 +64,309 @@
49 command. The subparsers argument is the return value of64 command. The subparsers argument is the return value of
50 ArgumentParser.add_subparsers()65 ArgumentParser.add_subparsers()
51 """66 """
67
68
69class PlainBoxToolBase(metaclass=ABCMeta):
70 """
71 Base class for implementing commands like 'plainbox'.
72
73 The tools support a variety of sub-commands, logging and debugging
74 support. If argcomplete module is available and used properly in
75 the shell then advanced tab-completion is also available.
76
77 There are three methods to implement for a basic tool. Those are:
78
79 1. :meth:`get_config_cls()` -- to know which config to use
80 2. :meth:`get_exec_name()` -- to know how the command will be called
81 3. :meth:`add_subcommands()` -- to add some actual commands to execute
82
83 This class has some complex control flow to support important
84 and interesting use cases. There are some concerns to people
85 that subclass this in order to implement their own command line tools.
86
87 The first concern is that input is parsed with two parsers, the early
88 parser and the full parser. The early parser quickly checks for a fraction
89 of supported arguments and uses that data to initialize environment
90 before construction of a full parser is possible. The full parser
91 sees the reminder of the input and does not re-parse things that where
92 already handled.
93
94 The second concern is that this command natively supports the concept
95 of a config object and a provider object. This may not be desired by
96 all users but it is the current state as of this writing. This means
97 that by the time eary init is done we have a known provider and config
98 objects that can be used to instantiate command objects
99 in :meth:`add_subcommands()`. This API might change when full
100 multi-provider is available but details are not known yet.
101 """
102
103 def __init__(self):
104 """
105 Initialize all the variables, real stuff happens in main()
106 """
107 self._early_parser = None # set in _early_init()
108 self._config = None # set in _late_init()
109 self._provider = None # set in _late_init()
110 self._parser = None # set in _late_init()
111
112 def main(self, argv=None):
113 """
114 Run as if invoked from command line directly
115 """
116 # Another try/catch block for catching KeyboardInterrupt
117 # This one is really only meant for the early init abort
118 # (when someone runs main but bails out before we really
119 # get to the point when we do something useful and setup
120 # all the exception handlers).
121 try:
122 self.early_init()
123 early_ns = self._early_parser.parse_args(argv)
124 self.late_init(early_ns)
125 logger.debug("parsed early namespace: %s", early_ns)
126 # parse the full command line arguments, this is also where we
127 # do argcomplete-dictated exit if bash shell completion
128 # is requested
129 ns = self._parser.parse_args(argv)
130 logger.debug("parsed full namespace: %s", ns)
131 self.final_init(ns)
132 except KeyboardInterrupt:
133 pass
134 else:
135 return self.dispatch_and_catch_exceptions(ns)
136
137 @classmethod
138 @abstractmethod
139 def get_config_cls(cls):
140 """
141 Get the Config class that is used by this implementation.
142
143 This can be overriden by subclasses to use a different config class
144 that is suitable for the particular application.
145 """
146
147 @classmethod
148 @abstractmethod
149 def get_exec_name(cls):
150 """
151 Get the name of this executable
152 """
153
154 @classmethod
155 @abstractmethod
156 def get_exec_version(cls):
157 """
158 Get the version reported by this executable
159 """
160
161 @abstractmethod
162 def add_subcommands(self, subparsers):
163 """
164 Add top-level subcommands to the argument parser.
165
166 This can be overriden by subclasses to use a different set of
167 top-level subcommands.
168 """
169
170 def early_init(self):
171 """
172 Do very early initialization. This is where we initalize stuff even
173 without seeing a shred of command line data or anything else.
174 """
175 self._early_parser = self.construct_early_parser()
176
177 def late_init(self, early_ns):
178 """
179 Initialize with early command line arguments being already parsed
180 """
181 adjust_logging(
182 level=early_ns.log_level, trace_list=early_ns.trace,
183 debug_console=early_ns.debug_console)
184 # Load plainbox configuration
185 self._config = self.get_config_cls().get()
186 # Load and initialize checkbox provider
187 # TODO: rename to provider, switch to plugins
188 all_providers.load()
189 # If the default value of 'None' was set for the checkbox (provider)
190 # argument then load the actual provider name from the configuration
191 # object (default for that is 'auto').
192 if early_ns.checkbox is None:
193 early_ns.checkbox = self._config.default_provider
194 assert early_ns.checkbox in ('auto', 'src', 'deb', 'stub', 'ihv')
195 if early_ns.checkbox == 'auto':
196 provider_name = 'checkbox-auto'
197 elif early_ns.checkbox == 'src':
198 provider_name = 'checkbox-src'
199 elif early_ns.checkbox == 'deb':
200 provider_name = 'checkbox-deb'
201 elif early_ns.checkbox == 'stub':
202 provider_name = 'stubbox'
203 elif early_ns.checkbox == 'ihv':
204 provider_name = 'ihv'
205 self._provider = all_providers.get_by_name(
206 provider_name).plugin_object()
207 # Construct the full command line argument parser
208 self._parser = self.construct_parser()
209
210 def final_init(self, ns):
211 """
212 Do some final initialization just before the command gets
213 dispatched. This is empty here but maybe useful for subclasses.
214 """
215
216 def construct_early_parser(self):
217 """
218 Create a parser that captures some of the early data we need to
219 be able to have a real parser and initialize the rest.
220 """
221 parser = argparse.ArgumentParser(add_help=False)
222 # Fake --help and --version
223 parser.add_argument("-h", "--help", action="store_const", const=None)
224 parser.add_argument("--version", action="store_const", const=None)
225 self.add_early_parser_arguments(parser)
226 # A catch-all net for everything else
227 parser.add_argument("rest", nargs="...")
228 return parser
229
230 def construct_parser(self):
231 parser = argparse.ArgumentParser(prog=self.get_exec_name())
232 parser.add_argument(
233 "--version", action="version", version=self.get_exec_version())
234 # Add all the things really parsed by the early parser so that it
235 # shows up in --help and bash tab completion.
236 self.add_early_parser_arguments(parser)
237 subparsers = parser.add_subparsers()
238 self.add_subcommands(subparsers)
239 # Enable argcomplete if it is available.
240 try:
241 import argcomplete
242 except ImportError:
243 pass
244 else:
245 argcomplete.autocomplete(parser)
246 return parser
247
248 def add_early_parser_arguments(self, parser):
249 # Since we need a CheckBox instance to create the main argument parser
250 # and we need to be able to specify where Checkbox is, we parse that
251 # option alone before parsing everything else
252 # TODO: rename this to -p | --provider
253 parser.add_argument(
254 '-c', '--checkbox',
255 action='store',
256 # TODO: have some public API for this, pretty please
257 choices=['src', 'deb', 'auto', 'stub', 'ihv'],
258 # None is a special value that means 'use whatever configured'
259 default=None,
260 help="where to find the installation of CheckBox.")
261 group = parser.add_argument_group(
262 title="logging and debugging")
263 # Add the --log-level argument
264 group.add_argument(
265 "-l", "--log-level",
266 action="store",
267 choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),
268 default=None,
269 help=argparse.SUPPRESS)
270 # Add the --verbose argument
271 group.add_argument(
272 "-v", "--verbose",
273 dest="log_level",
274 action="store_const",
275 const="INFO",
276 help="be more verbose (same as --log-level=INFO)")
277 # Add the --debug flag
278 group.add_argument(
279 "-D", "--debug",
280 dest="log_level",
281 action="store_const",
282 const="DEBUG",
283 help="enable DEBUG messages on the root logger")
284 # Add the --debug flag
285 group.add_argument(
286 "-C", "--debug-console",
287 action="store_true",
288 help="display DEBUG messages in the console")
289 # Add the --trace flag
290 group.add_argument(
291 "-T", "--trace",
292 metavar="LOGGER",
293 action="append",
294 default=[],
295 help=("enable DEBUG messages on the specified logger "
296 "(can be used multiple times)"))
297 # Add the --pdb flag
298 group.add_argument(
299 "-P", "--pdb",
300 action="store_true",
301 default=False,
302 help="jump into pdb (python debugger) when a command crashes")
303 # Add the --debug-interrupt flag
304 group.add_argument(
305 "-I", "--debug-interrupt",
306 action="store_true",
307 default=False,
308 help="crash on SIGINT/KeyboardInterrupt, useful with --pdb")
309
310 def dispatch_command(self, ns):
311 # Argh the horrror!
312 #
313 # Since CPython revision cab204a79e09 (landed for python3.3)
314 # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py
315 # the argparse module behaves differently than it did in python3.2
316 #
317 # In practical terms subparsers are now optional in 3.3 so all of the
318 # commands are no longer required parameters.
319 #
320 # To compensate, on python3.3 and beyond, when the user just runs
321 # plainbox without specifying the command, we manually, explicitly do
322 # what python3.2 did: call parser.error(_('too few arguments'))
323 if (sys.version_info[:2] >= (3, 3)
324 and getattr(ns, "command", None) is None):
325 self._parser.error(argparse._("too few arguments"))
326 else:
327 return ns.command.invoked(ns)
328
329 def dispatch_and_catch_exceptions(self, ns):
330 try:
331 return self.dispatch_command(ns)
332 except SystemExit:
333 # Don't let SystemExit be caught in the logic below, we really
334 # just want to exit when that gets thrown.
335 logger.debug("caught SystemExit, exiting")
336 # We may want to raise SystemExit as it can carry a status code
337 # along and we cannot just consume that.
338 raise
339 except BaseException as exc:
340 logger.debug("caught %r, deciding on what to do next", exc)
341 # For all other exceptions (and I mean all), do a few checks
342 # and perform actions depending on the command line arguments
343 # By default we want to re-raise the exception
344 action = 'raise'
345 # We want to ignore IOErrors that are really EPIPE
346 if isinstance(exc, IOError):
347 if exc.errno == errno.EPIPE:
348 action = 'ignore'
349 # We want to ignore KeyboardInterrupt unless --debug-interrupt
350 # was passed on command line
351 elif isinstance(exc, KeyboardInterrupt):
352 if ns.debug_interrupt:
353 action = 'debug'
354 else:
355 action = 'ignore'
356 else:
357 # For all other execptions, debug if requested
358 if ns.pdb:
359 action = 'debug'
360 logger.debug("action for exception %r is %s", exc, action)
361 if action == 'ignore':
362 return 0
363 elif action == 'raise':
364 logging.getLogger("plainbox.crashes").fatal(
365 "Executable %r invoked with %r has crashed",
366 self.get_exec_name(), ns, exc_info=1)
367 raise
368 elif action == 'debug':
369 logger.error("caught runaway exception: %r", exc)
370 logger.error("starting debugger...")
371 pdb.post_mortem()
372 return 1
52373
=== modified file 'plainbox/plainbox/impl/commands/analyze.py'
--- plainbox/plainbox/impl/commands/analyze.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/analyze.py 2013-09-13 17:12:45 +0000
@@ -31,7 +31,7 @@
31from plainbox.impl.commands import PlainBoxCommand31from plainbox.impl.commands import PlainBoxCommand
32from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn32from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
33from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn33from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
34from plainbox.impl.session import SessionState34from plainbox.impl.session import SessionStateLegacyAPI as SessionState
35from plainbox.impl.runner import JobRunner35from plainbox.impl.runner import JobRunner
3636
3737
@@ -40,8 +40,8 @@
4040
41class AnalyzeInvocation(CheckBoxInvocationMixIn):41class AnalyzeInvocation(CheckBoxInvocationMixIn):
4242
43 def __init__(self, checkbox, ns):43 def __init__(self, provider, ns):
44 super(AnalyzeInvocation, self).__init__(checkbox)44 super(AnalyzeInvocation, self).__init__(provider)
45 self.ns = ns45 self.ns = ns
46 self.job_list = self.get_job_list(ns)46 self.job_list = self.get_job_list(ns)
47 self.desired_job_list = self._get_matching_job_list(ns, self.job_list)47 self.desired_job_list = self._get_matching_job_list(ns, self.job_list)
@@ -64,7 +64,7 @@
64 with self.session.open():64 with self.session.open():
65 runner = JobRunner(65 runner = JobRunner(
66 self.session.session_dir, self.session.jobs_io_log_dir,66 self.session.session_dir, self.session.jobs_io_log_dir,
67 command_io_delegate=self, outcome_callback=None)67 command_io_delegate=self, interaction_callback=None)
68 again = True68 again = True
69 while again:69 while again:
70 for job in self.session.run_list:70 for job in self.session.run_list:
@@ -127,11 +127,11 @@
127 Implementation of ``$ plainbox dev analyze``127 Implementation of ``$ plainbox dev analyze``
128 """128 """
129129
130 def __init__(self, checkbox):130 def __init__(self, provider):
131 self.checkbox = checkbox131 self.provider = provider
132132
133 def invoked(self, ns):133 def invoked(self, ns):
134 return AnalyzeInvocation(self.checkbox, ns).run()134 return AnalyzeInvocation(self.provider, ns).run()
135135
136 def register_parser(self, subparsers):136 def register_parser(self, subparsers):
137 parser = subparsers.add_parser(137 parser = subparsers.add_parser(
138138
=== modified file 'plainbox/plainbox/impl/commands/checkbox.py'
--- plainbox/plainbox/impl/commands/checkbox.py 2013-05-10 16:49:14 +0000
+++ plainbox/plainbox/impl/commands/checkbox.py 2013-09-13 17:12:45 +0000
@@ -32,14 +32,14 @@
3232
33class CheckBoxInvocationMixIn:33class CheckBoxInvocationMixIn:
3434
35 def __init__(self, checkbox):35 def __init__(self, provider):
36 self.checkbox = checkbox36 self.provider = provider
3737
38 def get_job_list(self, ns):38 def get_job_list(self, ns):
39 """39 """
40 Load and return a list of JobDefinition instances40 Load and return a list of JobDefinition instances
41 """41 """
42 return self.checkbox.get_builtin_jobs()42 return self.provider.get_builtin_jobs()
4343
44 def _get_matching_job_list(self, ns, job_list):44 def _get_matching_job_list(self, ns, job_list):
45 # Find jobs that matched patterns45 # Find jobs that matched patterns
@@ -47,27 +47,29 @@
47 # Pre-seed the include pattern list with data read from47 # Pre-seed the include pattern list with data read from
48 # the whitelist file.48 # the whitelist file.
49 if ns.whitelist:49 if ns.whitelist:
50 ns.include_pattern_list.extend([50 for whitelist in ns.whitelist:
51 pattern.strip()51 ns.include_pattern_list.extend([
52 for pattern in ns.whitelist.readlines()])52 pattern.strip()
53 for pattern in whitelist.readlines()])
53 # Decide which of the known jobs to include54 # Decide which of the known jobs to include
54 for job in job_list:55 if ns.exclude_pattern_list:
55 # Reject all jobs that match any of the exclude
56 # patterns, matching strictly from the start to
57 # the end of the line.
58 for pattern in ns.exclude_pattern_list:56 for pattern in ns.exclude_pattern_list:
57 # Reject all jobs that match any of the exclude
58 # patterns, matching strictly from the start to
59 # the end of the line.
59 regexp_pattern = r"^{pattern}$".format(pattern=pattern)60 regexp_pattern = r"^{pattern}$".format(pattern=pattern)
60 if re.match(regexp_pattern, job.name):61 for job in job_list:
61 break62 if re.match(regexp_pattern, job.name):
62 else:63 job_list.remove(job)
63 # Then accept (include) all job that matches64 if ns.include_pattern_list:
65 for pattern in ns.include_pattern_list:
66 # Accept (include) all job that matches
64 # any of include patterns, matching strictly67 # any of include patterns, matching strictly
65 # from the start to the end of the line.68 # from the start to the end of the line.
66 for pattern in ns.include_pattern_list:69 regexp_pattern = r"^{pattern}$".format(pattern=pattern)
67 regexp_pattern = r"^{pattern}$".format(pattern=pattern)70 for job in job_list:
68 if re.match(regexp_pattern, job.name):71 if re.match(regexp_pattern, job.name):
69 matching_job_list.append(job)72 matching_job_list.append(job)
70 break
71 return matching_job_list73 return matching_job_list
7274
7375
@@ -95,6 +97,7 @@
95 # TODO: Find a way to handle the encoding of the file97 # TODO: Find a way to handle the encoding of the file
96 group.add_argument(98 group.add_argument(
97 '-w', '--whitelist',99 '-w', '--whitelist',
100 action="append",
98 metavar="WHITELIST",101 metavar="WHITELIST",
99 type=FileType("rt"),102 type=FileType("rt"),
100 help="Load whitelist containing run patterns")103 help="Load whitelist containing run patterns")
101104
=== modified file 'plainbox/plainbox/impl/commands/dev.py'
--- plainbox/plainbox/impl/commands/dev.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/dev.py 2013-09-13 17:12:45 +0000
@@ -45,8 +45,8 @@
45 Command hub for various development commands.45 Command hub for various development commands.
46 """46 """
4747
48 def __init__(self, checkbox, config):48 def __init__(self, provider, config):
49 self.checkbox = checkbox49 self.provider = provider
50 self.config = config50 self.config = config
5151
52 def invoked(self, ns):52 def invoked(self, ns):
@@ -56,9 +56,9 @@
56 parser = subparsers.add_parser(56 parser = subparsers.add_parser(
57 "dev", help="development commands")57 "dev", help="development commands")
58 subdev = parser.add_subparsers()58 subdev = parser.add_subparsers()
59 ScriptCommand(self.checkbox, self.config).register_parser(subdev)59 ScriptCommand(self.provider, self.config).register_parser(subdev)
60 SpecialCommand(self.checkbox).register_parser(subdev)60 SpecialCommand(self.provider).register_parser(subdev)
61 AnalyzeCommand(self.checkbox).register_parser(subdev)61 AnalyzeCommand(self.provider).register_parser(subdev)
62 ParseCommand().register_parser(subdev)62 ParseCommand().register_parser(subdev)
63 CrashCommand().register_parser(subdev)63 CrashCommand().register_parser(subdev)
64 LogTestCommand().register_parser(subdev)64 LogTestCommand().register_parser(subdev)
6565
=== modified file 'plainbox/plainbox/impl/commands/run.py'
--- plainbox/plainbox/impl/commands/run.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/run.py 2013-09-13 17:12:45 +0000
@@ -35,17 +35,19 @@
3535
36from requests.exceptions import ConnectionError, InvalidSchema, HTTPError36from requests.exceptions import ConnectionError, InvalidSchema, HTTPError
3737
38from plainbox.abc import IJobResult
39from plainbox.impl.providers.checkbox import CheckBoxDebProvider
38from plainbox.impl.commands import PlainBoxCommand40from plainbox.impl.commands import PlainBoxCommand
39from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn41from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
40from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn42from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
41from plainbox.impl.depmgr import DependencyDuplicateError43from plainbox.impl.depmgr import DependencyDuplicateError
42from plainbox.impl.exporter import ByteStringStreamTranslator44from plainbox.impl.exporter import ByteStringStreamTranslator
43from plainbox.impl.exporter import get_all_exporters45from plainbox.impl.exporter import get_all_exporters
44from plainbox.impl.result import JobResult46from plainbox.impl.result import DiskJobResult, MemoryJobResult
45from plainbox.impl.runner import JobRunner47from plainbox.impl.runner import JobRunner
46from plainbox.impl.runner import authenticate_warmup48from plainbox.impl.runner import authenticate_warmup
47from plainbox.impl.runner import slugify49from plainbox.impl.runner import slugify
48from plainbox.impl.session import SessionState50from plainbox.impl.session import SessionStateLegacyAPI as SessionState
49from plainbox.impl.transport import get_all_transports51from plainbox.impl.transport import get_all_transports
5052
5153
@@ -54,8 +56,8 @@
5456
55class RunInvocation(CheckBoxInvocationMixIn):57class RunInvocation(CheckBoxInvocationMixIn):
5658
57 def __init__(self, checkbox, ns):59 def __init__(self, provider, ns):
58 super(RunInvocation, self).__init__(checkbox)60 super(RunInvocation, self).__init__(provider)
59 self.ns = ns61 self.ns = ns
6062
61 def run(self):63 def run(self):
@@ -110,25 +112,46 @@
110 except ValueError as exc:112 except ValueError as exc:
111 raise SystemExit(str(exc))113 raise SystemExit(str(exc))
112114
113 def ask_for_resume(self, prompt=None, allowed=None):115 def ask_for_resume(self):
114 # FIXME: Add support/callbacks for a GUI116 return self.ask_user(
115 if prompt is None:117 "Do you want to resume the previous session?", ('y', 'n')
116 prompt = "Do you want to resume the previous session [Y/n]? "118 ).lower() == "y"
117 if allowed is None:119
118 allowed = ('', 'y', 'Y', 'n', 'N')120 def ask_for_resume_action(self):
121 return self.ask_user(
122 "What do you want to do with that job?", ('skip', 'fail', 'run'))
123
124 def ask_user(self, prompt, allowed):
119 answer = None125 answer = None
120 while answer not in allowed:126 while answer not in allowed:
121 answer = input(prompt)127 answer = input("{} [{}] ".format(prompt, ", ".join(allowed)))
122 return False if answer in ('n', 'N') else True128 return answer
129
130 def _maybe_skip_last_job_after_resume(self, session):
131 last_job = session.metadata.running_job_name
132 if last_job is None:
133 return
134 print("We have previously tried to execute {}".format(last_job))
135 action = self.ask_for_resume_action()
136 if action == 'skip':
137 result = MemoryJobResult({
138 'outcome': 'skip',
139 'comment': "Skipped after resuming execution"
140 })
141 elif action == 'fail':
142 result = MemoryJobResult({
143 'outcome': 'fail',
144 'comment': "Failed after resuming execution"
145 })
146 elif action == 'run':
147 result = None
148 if result:
149 session.update_job_result(
150 session.job_state_map[last_job].job, result)
151 session.metadata.running_job_name = None
152 session.persistent_save()
123153
124 def _run_jobs(self, ns, job_list, exporter, transport=None):154 def _run_jobs(self, ns, job_list, exporter, transport=None):
125 # Ask the password before anything else in order to run jobs requiring
126 # privileges
127 if self.checkbox._mode == 'deb':
128 print("[ Authentication ]".center(80, '='))
129 return_code = authenticate_warmup()
130 if return_code:
131 raise SystemExit(return_code)
132 # Compute the run list, this can give us notification about problems in155 # Compute the run list, this can give us notification about problems in
133 # the selected jobs. Currently we just display each problem156 # the selected jobs. Currently we just display each problem
134 matching_job_list = self._get_matching_job_list(ns, job_list)157 matching_job_list = self._get_matching_job_list(ns, job_list)
@@ -150,18 +173,28 @@
150 if session.previous_session_file():173 if session.previous_session_file():
151 if self.ask_for_resume():174 if self.ask_for_resume():
152 session.resume()175 session.resume()
176 self._maybe_skip_last_job_after_resume(session)
153 else:177 else:
154 session.clean()178 session.clean()
179 session.metadata.title = " ".join(sys.argv)
180 session.persistent_save()
155 self._update_desired_job_list(session, matching_job_list)181 self._update_desired_job_list(session, matching_job_list)
182 # Ask the password before anything else in order to run jobs
183 # requiring privileges
184 if self._auth_warmup_needed(session):
185 print("[ Authentication ]".center(80, '='))
186 return_code = authenticate_warmup()
187 if return_code:
188 raise SystemExit(return_code)
156 if (sys.stdin.isatty() and sys.stdout.isatty() and not189 if (sys.stdin.isatty() and sys.stdout.isatty() and not
157 ns.not_interactive):190 ns.not_interactive):
158 outcome_callback = self.ask_for_outcome191 interaction_callback = self._interaction_callback
159 else:192 else:
160 outcome_callback = None193 interaction_callback = None
161 runner = JobRunner(194 runner = JobRunner(
162 session.session_dir,195 session.session_dir,
163 session.jobs_io_log_dir,196 session.jobs_io_log_dir,
164 outcome_callback=outcome_callback,197 interaction_callback=interaction_callback,
165 dry_run=ns.dry_run198 dry_run=ns.dry_run
166 )199 )
167 self._run_jobs_with_session(ns, session, runner)200 self._run_jobs_with_session(ns, session, runner)
@@ -189,6 +222,18 @@
189 # FIXME: sensible return value222 # FIXME: sensible return value
190 return 0223 return 0
191224
225 def _auth_warmup_needed(self, session):
226 # Don't use authentication warm-up in modes other than 'deb' as it
227 # makes no sense to do so.
228 if not isinstance(self.provider, CheckBoxDebProvider):
229 return False
230 # Don't use authentication warm-up if none of the jobs on the run list
231 # requires it.
232 if all(job.user is None for job in session.run_list):
233 return False
234 # Otherwise, do pre-authentication
235 return True
236
192 def _save_results(self, output_file, input_stream):237 def _save_results(self, output_file, input_stream):
193 if output_file is sys.stdout:238 if output_file is sys.stdout:
194 print("[ Results ]".center(80, '='))239 print("[ Results ]".center(80, '='))
@@ -203,18 +248,32 @@
203 if output_file is not sys.stdout:248 if output_file is not sys.stdout:
204 output_file.close()249 output_file.close()
205250
206 def ask_for_outcome(self, prompt=None, allowed=None):251 def _interaction_callback(self, runner, job, config, prompt=None,
252 allowed_outcome=None):
253 result = {}
207 if prompt is None:254 if prompt is None:
208 prompt = "what is the outcome? "255 prompt = "Select an outcome or an action: "
209 if allowed is None:256 if allowed_outcome is None:
210 allowed = (JobResult.OUTCOME_PASS,257 allowed_outcome = [IJobResult.OUTCOME_PASS,
211 JobResult.OUTCOME_FAIL,258 IJobResult.OUTCOME_FAIL,
212 JobResult.OUTCOME_SKIP)259 IJobResult.OUTCOME_SKIP]
213 answer = None260 allowed_actions = ['comments']
214 while answer not in allowed:261 if job.command:
215 print("Allowed answers are: {}".format(", ".join(allowed)))262 allowed_actions.append('test')
216 answer = input(prompt)263 result['outcome'] = IJobResult.OUTCOME_UNDECIDED
217 return answer264 while result['outcome'] not in allowed_outcome:
265 print("Allowed answers are: {}".format(", ".join(allowed_outcome +
266 allowed_actions)))
267 choice = input(prompt)
268 if choice in allowed_outcome:
269 result['outcome'] = choice
270 break
271 elif choice == 'test':
272 (result['return_code'],
273 result['io_log_filename']) = runner._run_command(job, config)
274 elif choice == 'comments':
275 result['comments'] = input('Please enter your comments:\n')
276 return DiskJobResult(result)
218277
219 def _update_desired_job_list(self, session, desired_job_list):278 def _update_desired_job_list(self, session, desired_job_list):
220 problem_list = session.update_desired_job_list(desired_job_list)279 problem_list = session.update_desired_job_list(desired_job_list)
@@ -224,6 +283,18 @@
224 for problem in problem_list:283 for problem in problem_list:
225 print(" * {}".format(problem))284 print(" * {}".format(problem))
226 print("Problematic jobs will not be considered")285 print("Problematic jobs will not be considered")
286 (estimated_duration_auto,
287 estimated_duration_manual) = session.get_estimated_duration()
288 if estimated_duration_auto:
289 print("Estimated duration is {:.2f} for automated jobs.".format(
290 estimated_duration_auto))
291 else:
292 print("Estimated duration cannot be determined for automated jobs.")
293 if estimated_duration_manual:
294 print("Estimated duration is {:.2f} for manual jobs.".format(
295 estimated_duration_manual))
296 else:
297 print("Estimated duration cannot be determined for manual jobs.")
227298
228 def _run_jobs_with_session(self, ns, session, runner):299 def _run_jobs_with_session(self, ns, session, runner):
229 # TODO: run all resource jobs concurrently with multiprocessing300 # TODO: run all resource jobs concurrently with multiprocessing
@@ -272,13 +343,16 @@
272 if job_state.can_start():343 if job_state.can_start():
273 print("Running... (output in {}.*)".format(344 print("Running... (output in {}.*)".format(
274 join(session.jobs_io_log_dir, slugify(job.name))))345 join(session.jobs_io_log_dir, slugify(job.name))))
346 session.metadata.running_job_name = job.name
347 session.persistent_save()
275 job_result = runner.run_job(job)348 job_result = runner.run_job(job)
349 session.metadata.running_job_name = None
350 session.persistent_save()
276 print("Outcome: {}".format(job_result.outcome))351 print("Outcome: {}".format(job_result.outcome))
277 print("Comments: {}".format(job_result.comments))352 print("Comments: {}".format(job_result.comments))
278 else:353 else:
279 job_result = JobResult({354 job_result = MemoryJobResult({
280 'job': job,355 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED,
281 'outcome': JobResult.OUTCOME_NOT_SUPPORTED,
282 'comments': job_state.get_readiness_description()356 'comments': job_state.get_readiness_description()
283 })357 })
284 if job_result is not None:358 if job_result is not None:
@@ -287,11 +361,11 @@
287361
288class RunCommand(PlainBoxCommand, CheckBoxCommandMixIn):362class RunCommand(PlainBoxCommand, CheckBoxCommandMixIn):
289363
290 def __init__(self, checkbox):364 def __init__(self, provider):
291 self.checkbox = checkbox365 self.provider = provider
292366
293 def invoked(self, ns):367 def invoked(self, ns):
294 return RunInvocation(self.checkbox, ns).run()368 return RunInvocation(self.provider, ns).run()
295369
296 def register_parser(self, subparsers):370 def register_parser(self, subparsers):
297 parser = subparsers.add_parser("run", help="run a test job")371 parser = subparsers.add_parser("run", help="run a test job")
298372
=== modified file 'plainbox/plainbox/impl/commands/script.py'
--- plainbox/plainbox/impl/commands/script.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/script.py 2013-09-13 17:12:45 +0000
@@ -48,8 +48,8 @@
48 the command is to be invoked.48 the command is to be invoked.
49 """49 """
5050
51 def __init__(self, checkbox, config, job_name):51 def __init__(self, provider, config, job_name):
52 self.checkbox = checkbox52 self.provider = provider
53 self.config = config53 self.config = config
54 self.job_name = job_name54 self.job_name = job_name
5555
@@ -67,8 +67,10 @@
67 bait_dir = os.path.join(scratch, 'files-created-in-current-dir')67 bait_dir = os.path.join(scratch, 'files-created-in-current-dir')
68 os.mkdir(bait_dir)68 os.mkdir(bait_dir)
69 with TestCwd(bait_dir):69 with TestCwd(bait_dir):
70 return_code, fjson = runner._run_command(job, self.config)70 return_code, record_path = runner._run_command(
71 job, self.config)
71 self._display_side_effects(scratch)72 self._display_side_effects(scratch)
73 self._display_script_outcome(job, return_code)
72 return return_code74 return return_code
7375
74 def _display_file(self, pathname, origin):76 def _display_file(self, pathname, origin):
@@ -85,9 +87,13 @@
85 self._display_file(87 self._display_file(
86 os.path.join(dirpath, filename), scratch)88 os.path.join(dirpath, filename), scratch)
8789
90 def _display_script_outcome(self, job, return_code):
91 print(job.name, "returned", return_code)
92 print("command:", job.command)
93
88 def _get_job(self):94 def _get_job(self):
89 job_list = get_matching_job_list(95 job_list = get_matching_job_list(
90 self.checkbox.get_builtin_jobs(),96 self.provider.get_builtin_jobs(),
91 NameJobQualifier(self.job_name))97 NameJobQualifier(self.job_name))
92 if len(job_list) == 0:98 if len(job_list) == 0:
93 return None99 return None
@@ -101,12 +107,12 @@
101 unconditionally.107 unconditionally.
102 """108 """
103109
104 def __init__(self, checkbox, config):110 def __init__(self, provider, config):
105 self.checkbox = checkbox111 self.provider = provider
106 self.config = config112 self.config = config
107113
108 def invoked(self, ns):114 def invoked(self, ns):
109 return ScriptInvocation(self.checkbox, self.config, ns.job_name).run()115 return ScriptInvocation(self.provider, self.config, ns.job_name).run()
110116
111 def register_parser(self, subparsers):117 def register_parser(self, subparsers):
112 parser = subparsers.add_parser(118 parser = subparsers.add_parser(
113119
=== added file 'plainbox/plainbox/impl/commands/service.py'
--- plainbox/plainbox/impl/commands/service.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/commands/service.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,132 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`plainbox.impl.commands.service` -- service sub-command
22============================================================
23
24"""
25
26import logging
27import os
28
29from dbus import StarterBus, SessionBus
30from dbus.mainloop.glib import DBusGMainLoop, threads_init
31from dbus.service import BusName
32from gi.repository import GObject
33
34from plainbox.impl.commands import PlainBoxCommand
35from plainbox.impl.highlevel import Service
36from plainbox.impl.service import ServiceWrapper
37
38
39logger = logging.getLogger("plainbox.commands.service")
40
41
42def connect_to_session_bus():
43 """
44 Connect to the session bus properly.
45
46 Returns a tuple (session_bus, loop) where loop is a GObject.MainLoop
47 instance. The loop is there so that you can listen to signals.
48 """
49 # We'll need an event loop to observe signals. We will need the instance
50 # later below so let's keep it. Note that we're not passing it directly
51 # below as DBus needs specific API. The DBusGMainLoop class that we
52 # instantiate and pass is going to work with this instance transparently.
53 #
54 # NOTE: DBus tutorial suggests that we should create the loop _before_
55 # connecting to the bus.
56 logger.debug("Setting up glib-based event loop")
57 # Make sure gobject threads don't crash
58 GObject.threads_init()
59 threads_init()
60 loop = GObject.MainLoop()
61 # Let's get the system bus object.
62 logger.debug("Connecting to DBus session bus")
63 if os.getenv("DBUS_STARTER_ADDRESS"):
64 session_bus = StarterBus(mainloop=DBusGMainLoop())
65 else:
66 session_bus = SessionBus(mainloop=DBusGMainLoop())
67 return session_bus, loop
68
69
70class ServiceInvocation:
71
72 def __init__(self, provider, config, ns):
73 self.provider = provider
74 self.config = config
75 self.ns = ns
76
77 def run(self):
78 bus, loop = connect_to_session_bus()
79 logger.info("Setting up DBus objects...")
80 provider_list = [self.provider]
81 session_list = [] # TODO: load sessions
82 logger.debug("Constructing Service object")
83 service_obj = Service(provider_list, session_list)
84 logger.debug("Constructing ServiceWrapper")
85 service_wrp = ServiceWrapper(service_obj, on_exit=lambda: loop.quit())
86 logger.info("Publishing all objects on DBus")
87 service_wrp.publish_related_objects(bus)
88 logger.info("Publishing all managed objects (events should fire there)")
89 service_wrp.publish_managed_objects()
90 logger.debug("Attempting to claim bus name: %s", self.ns.bus_name)
91 bus_name = BusName(self.ns.bus_name, bus)
92 logger.info(
93 "PlainBox DBus service ready, claimed name: %s",
94 bus_name.get_name())
95 try:
96 loop.run()
97 except KeyboardInterrupt:
98 logger.warning((
99 "Main loop interrupted!"
100 " It is recommended to call the Exit() method on the"
101 " exported service object instead"))
102 finally:
103 logger.debug("Releasing %s", bus_name)
104 # XXX: ugly but that's how one can reliably release a bus name
105 del bus_name
106 # Remove objects from the bus
107 service_wrp.remove_from_connection()
108 logger.debug("Closing %s", bus)
109 bus.close()
110 logger.debug("Main loop terminated, exiting...")
111
112
113class ServiceCommand(PlainBoxCommand):
114 """
115 DBus service for PlainBox
116 """
117
118 # XXX: Maybe drop provider / config and handle them differently
119 def __init__(self, provider, config):
120 self.provider = provider
121 self.config = config
122
123 def invoked(self, ns):
124 return ServiceInvocation(self.provider, self.config, ns).run()
125
126 def register_parser(self, subparsers):
127 parser = subparsers.add_parser("service", help="spawn dbus service")
128 parser.add_argument(
129 '--bus-name', action="store",
130 default="com.canonical.certification.PlainBox1",
131 help="Use the specified DBus bus name")
132 parser.set_defaults(command=self)
0133
=== modified file 'plainbox/plainbox/impl/commands/special.py'
--- plainbox/plainbox/impl/commands/special.py 2013-05-13 08:49:00 +0000
+++ plainbox/plainbox/impl/commands/special.py 2013-09-13 17:12:45 +0000
@@ -38,8 +38,8 @@
3838
39class SpecialInvocation(CheckBoxInvocationMixIn):39class SpecialInvocation(CheckBoxInvocationMixIn):
4040
41 def __init__(self, checkbox, ns):41 def __init__(self, provider, ns):
42 super(SpecialInvocation, self).__init__(checkbox)42 super(SpecialInvocation, self).__init__(provider)
43 self.ns = ns43 self.ns = ns
4444
45 def run(self):45 def run(self):
@@ -124,11 +124,11 @@
124 Implementation of ``$ plainbox special``124 Implementation of ``$ plainbox special``
125 """125 """
126126
127 def __init__(self, checkbox):127 def __init__(self, provider):
128 self.checkbox = checkbox128 self.provider = provider
129129
130 def invoked(self, ns):130 def invoked(self, ns):
131 return SpecialInvocation(self.checkbox, ns).run()131 return SpecialInvocation(self.provider, ns).run()
132132
133 def register_parser(self, subparsers):133 def register_parser(self, subparsers):
134 parser = subparsers.add_parser(134 parser = subparsers.add_parser(
135135
=== modified file 'plainbox/plainbox/impl/commands/sru.py'
--- plainbox/plainbox/impl/commands/sru.py 2013-05-10 16:49:15 +0000
+++ plainbox/plainbox/impl/commands/sru.py 2013-09-13 17:12:45 +0000
@@ -28,21 +28,24 @@
28"""28"""
29import logging29import logging
30import os30import os
31import sys
31import tempfile32import tempfile
3233
33from requests.exceptions import ConnectionError, InvalidSchema, HTTPError34from requests.exceptions import ConnectionError, InvalidSchema, HTTPError
3435
36from plainbox.impl.applogic import WhiteList
35from plainbox.impl.applogic import get_matching_job_list37from plainbox.impl.applogic import get_matching_job_list
36from plainbox.impl.applogic import run_job_if_possible38from plainbox.impl.applogic import run_job_if_possible
37from plainbox.impl.checkbox import WhiteList
38from plainbox.impl.commands import PlainBoxCommand39from plainbox.impl.commands import PlainBoxCommand
39from plainbox.impl.commands.check_config import CheckConfigInvocation40from plainbox.impl.commands.check_config import CheckConfigInvocation
41from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn
42from plainbox.impl.commands.checkbox import CheckBoxInvocationMixIn
40from plainbox.impl.config import ValidationError, Unset43from plainbox.impl.config import ValidationError, Unset
41from plainbox.impl.depmgr import DependencyDuplicateError44from plainbox.impl.depmgr import DependencyDuplicateError
42from plainbox.impl.exporter import ByteStringStreamTranslator45from plainbox.impl.exporter import ByteStringStreamTranslator
43from plainbox.impl.exporter.xml import XMLSessionStateExporter46from plainbox.impl.exporter.xml import XMLSessionStateExporter
44from plainbox.impl.runner import JobRunner47from plainbox.impl.runner import JobRunner
45from plainbox.impl.session import SessionState48from plainbox.impl.session import SessionStateLegacyAPI as SessionState
46from plainbox.impl.transport.certification import CertificationTransport49from plainbox.impl.transport.certification import CertificationTransport
47from plainbox.impl.transport.certification import InvalidSecureIDError50from plainbox.impl.transport.certification import InvalidSecureIDError
4851
@@ -50,20 +53,26 @@
50logger = logging.getLogger("plainbox.commands.sru")53logger = logging.getLogger("plainbox.commands.sru")
5154
5255
53class _SRUInvocation:56class _SRUInvocation(CheckBoxInvocationMixIn):
54 """57 """
55 Helper class instantiated to perform a particular invocation of the sru58 Helper class instantiated to perform a particular invocation of the sru
56 command. Unlike the SRU command itself, this class is instantiated each59 command. Unlike the SRU command itself, this class is instantiated each
57 time.60 time.
58 """61 """
5962
60 def __init__(self, checkbox, config, ns):63 def __init__(self, provider, config, ns):
61 self.checkbox = checkbox64 self.provider = provider
62 self.config = config65 self.config = config
63 self.ns = ns66 self.ns = ns
64 self.whitelist = WhiteList.from_file(os.path.join(67 if self.ns.whitelist:
65 self.checkbox.whitelists_dir, "sru.whitelist"))68 self.whitelist = WhiteList.from_file(self.ns.whitelist[0].name)
66 self.job_list = self.checkbox.get_builtin_jobs()69 elif self.config.whitelist is not Unset:
70 self.whitelist = WhiteList.from_file(self.config.whitelist)
71 else:
72 self.whitelist = WhiteList.from_file(os.path.join(
73 self.provider.whitelists_dir, "sru.whitelist"))
74
75 self.job_list = self.provider.get_builtin_jobs()
67 # XXX: maybe allow specifying system_id from command line?76 # XXX: maybe allow specifying system_id from command line?
68 self.exporter = XMLSessionStateExporter(system_id=None)77 self.exporter = XMLSessionStateExporter(system_id=None)
69 self.session = None78 self.session = None
@@ -91,7 +100,7 @@
91 self.session.session_dir,100 self.session.session_dir,
92 self.session.jobs_io_log_dir,101 self.session.jobs_io_log_dir,
93 command_io_delegate=self,102 command_io_delegate=self,
94 outcome_callback=None, # SRU runs are never interactive103 interaction_callback=None, # SRU runs are never interactive
95 dry_run=self.ns.dry_run104 dry_run=self.ns.dry_run
96 )105 )
97 self._run_all_jobs()106 self._run_all_jobs()
@@ -178,9 +187,11 @@
178187
179 def _run_single_job(self, job):188 def _run_single_job(self, job):
180 print("- {}:".format(job.name), end=' ')189 print("- {}:".format(job.name), end=' ')
190 sys.stdout.flush()
181 job_state, job_result = run_job_if_possible(191 job_state, job_result = run_job_if_possible(
182 self.session, self.runner, self.config, job)192 self.session, self.runner, self.config, job)
183 print("{0}".format(job_result.outcome))193 print("{0}".format(job_result.outcome))
194 sys.stdout.flush()
184 if job_result.comments is not None:195 if job_result.comments is not None:
185 print("comments: {0}".format(job_result.comments))196 print("comments: {0}".format(job_result.comments))
186 if job_state.readiness_inhibitor_list:197 if job_state.readiness_inhibitor_list:
@@ -190,7 +201,7 @@
190 self.session.update_job_result(job, job_result)201 self.session.update_job_result(job, job_result)
191202
192203
193class SRUCommand(PlainBoxCommand):204class SRUCommand(PlainBoxCommand, CheckBoxCommandMixIn):
194 """205 """
195 Command for running Stable Release Update (SRU) tests.206 Command for running Stable Release Update (SRU) tests.
196207
@@ -204,8 +215,8 @@
204 plainbox core on realistic workloads.215 plainbox core on realistic workloads.
205 """216 """
206217
207 def __init__(self, checkbox, config):218 def __init__(self, provider, config):
208 self.checkbox = checkbox219 self.provider = provider
209 self.config = config220 self.config = config
210221
211 def invoked(self, ns):222 def invoked(self, ns):
@@ -226,7 +237,7 @@
226 retval = CheckConfigInvocation(self.config).run()237 retval = CheckConfigInvocation(self.config).run()
227 if retval != 0:238 if retval != 0:
228 return retval239 return retval
229 return _SRUInvocation(self.checkbox, self.config, ns).run()240 return _SRUInvocation(self.provider, self.config, ns).run()
230241
231 def register_parser(self, subparsers):242 def register_parser(self, subparsers):
232 parser = subparsers.add_parser(243 parser = subparsers.add_parser(
@@ -262,6 +273,12 @@
262 action='store',273 action='store',
263 help=("POST the test report XML to this URL"274 help=("POST the test report XML to this URL"
264 " (%(default)s)"))275 " (%(default)s)"))
276 group.add_argument(
277 '--staging',
278 dest='c3_url',
279 action='store_const',
280 const='https://certification.staging.canonical.com/submissions/submit/',
281 help='Override --destination to use the staging certification website')
265 group = parser.add_argument_group(title="execution options")282 group = parser.add_argument_group(title="execution options")
266 group.add_argument(283 group.add_argument(
267 '-n', '--dry-run',284 '-n', '--dry-run',
@@ -269,3 +286,6 @@
269 default=False,286 default=False,
270 help=("Skip all usual jobs."287 help=("Skip all usual jobs."
271 " Only local, resource and attachment jobs are started"))288 " Only local, resource and attachment jobs are started"))
289 # Call enhance_parser from CheckBoxCommandMixIn
290 self.enhance_parser(parser)
291
272292
=== modified file 'plainbox/plainbox/impl/commands/test_dev.py'
--- plainbox/plainbox/impl/commands/test_dev.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/test_dev.py 2013-09-13 17:12:45 +0000
@@ -39,17 +39,17 @@
39 def setUp(self):39 def setUp(self):
40 self.parser = argparse.ArgumentParser(prog='test')40 self.parser = argparse.ArgumentParser(prog='test')
41 self.subparsers = self.parser.add_subparsers()41 self.subparsers = self.parser.add_subparsers()
42 self.checkbox = mock.Mock()42 self.provider = mock.Mock()
43 self.config = mock.Mock()43 self.config = mock.Mock()
44 self.ns = mock.Mock()44 self.ns = mock.Mock()
4545
46 def test_init(self):46 def test_init(self):
47 dev_cmd = DevCommand(self.checkbox, self.config)47 dev_cmd = DevCommand(self.provider, self.config)
48 self.assertIs(dev_cmd.checkbox, self.checkbox)48 self.assertIs(dev_cmd.provider, self.provider)
49 self.assertIs(dev_cmd.config, self.config)49 self.assertIs(dev_cmd.config, self.config)
5050
51 def test_register_parser(self):51 def test_register_parser(self):
52 DevCommand(self.checkbox, self.config).register_parser(52 DevCommand(self.provider, self.config).register_parser(
53 self.subparsers)53 self.subparsers)
54 with TestIO() as io:54 with TestIO() as io:
55 self.parser.print_help()55 self.parser.print_help()
5656
=== modified file 'plainbox/plainbox/impl/commands/test_run.py'
--- plainbox/plainbox/impl/commands/test_run.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/test_run.py 2013-09-13 17:12:45 +0000
@@ -29,11 +29,16 @@
29import shutil29import shutil
30import tempfile30import tempfile
3131
32from collections import OrderedDict
32from inspect import cleandoc33from inspect import cleandoc
33from mock import patch34from mock import patch
34from unittest import TestCase35from unittest import TestCase
3536
36from plainbox.impl.box import main37from plainbox.impl.box import main
38from plainbox.impl.exporter.json import JSONSessionStateExporter
39from plainbox.impl.exporter.rfc822 import RFC822SessionStateExporter
40from plainbox.impl.exporter.text import TextSessionStateExporter
41from plainbox.impl.exporter.xml import XMLSessionStateExporter
37from plainbox.testing_utils.io import TestIO42from plainbox.testing_utils.io import TestIO
3843
3944
@@ -46,6 +51,12 @@
46 self._sandbox = tempfile.mkdtemp()51 self._sandbox = tempfile.mkdtemp()
47 self._env = os.environ52 self._env = os.environ
48 os.environ['XDG_CACHE_HOME'] = self._sandbox53 os.environ['XDG_CACHE_HOME'] = self._sandbox
54 self._exporters = OrderedDict([
55 ('json', JSONSessionStateExporter),
56 ('rfc822', RFC822SessionStateExporter),
57 ('text', TextSessionStateExporter),
58 ('xml', XMLSessionStateExporter),
59 ])
4960
50 def test_help(self):61 def test_help(self):
51 with TestIO(combined=True) as io:62 with TestIO(combined=True) as io:
@@ -107,12 +118,16 @@
107 self.assertEqual(call.exception.args, (0,))118 self.assertEqual(call.exception.args, (0,))
108 expected1 = """119 expected1 = """
109 ===============================[ Analyzing Jobs ]===============================120 ===============================[ Analyzing Jobs ]===============================
121 Estimated duration cannot be determined for automated jobs.
122 Estimated duration cannot be determined for manual jobs.
110 ==============================[ Running All Jobs ]==============================123 ==============================[ Running All Jobs ]==============================
111 ==================================[ Results ]===================================124 ==================================[ Results ]===================================
112 """125 """
113 expected2 = """126 expected2 = """
114 ===============================[ Authentication ]===============================127 ===============================[ Authentication ]===============================
115 ===============================[ Analyzing Jobs ]===============================128 ===============================[ Analyzing Jobs ]===============================
129 Estimated duration cannot be determined for automated jobs.
130 Estimated duration cannot be determined for manual jobs.
116 ==============================[ Running All Jobs ]==============================131 ==============================[ Running All Jobs ]==============================
117 ==================================[ Results ]===================================132 ==================================[ Results ]===================================
118 """133 """
@@ -123,7 +138,9 @@
123 def test_output_format_list(self):138 def test_output_format_list(self):
124 with TestIO(combined=True) as io:139 with TestIO(combined=True) as io:
125 with self.assertRaises(SystemExit) as call:140 with self.assertRaises(SystemExit) as call:
126 main(['run', '--output-format=?'])141 with patch('plainbox.impl.commands.run.get_all_exporters') as mock_get_all_exporters:
142 mock_get_all_exporters.return_value = self._exporters
143 main(['run', '--output-format=?'])
127 self.assertEqual(call.exception.args, (0,))144 self.assertEqual(call.exception.args, (0,))
128 expected = """145 expected = """
129 Available output formats: json, rfc822, text, xml146 Available output formats: json, rfc822, text, xml
@@ -133,7 +150,9 @@
133 def test_output_option_list(self):150 def test_output_option_list(self):
134 with TestIO(combined=True) as io:151 with TestIO(combined=True) as io:
135 with self.assertRaises(SystemExit) as call:152 with self.assertRaises(SystemExit) as call:
136 main(['run', '--output-option=?'])153 with patch('plainbox.impl.commands.run.get_all_exporters') as mock_get_all_exporters:
154 mock_get_all_exporters.return_value = self._exporters
155 main(['run', '--output-option=?'])
137 self.assertEqual(call.exception.args, (0,))156 self.assertEqual(call.exception.args, (0,))
138 expected = """157 expected = """
139 Each format may support a different set of options158 Each format may support a different set of options
140159
=== modified file 'plainbox/plainbox/impl/commands/test_script.py'
--- plainbox/plainbox/impl/commands/test_script.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/commands/test_script.py 2013-09-13 17:12:45 +0000
@@ -32,7 +32,7 @@
3232
33from plainbox.impl.applogic import PlainBoxConfig33from plainbox.impl.applogic import PlainBoxConfig
34from plainbox.impl.commands.script import ScriptInvocation, ScriptCommand34from plainbox.impl.commands.script import ScriptInvocation, ScriptCommand
35from plainbox.impl.provider import DummyProvider135from plainbox.impl.providers.v1 import DummyProvider1
36from plainbox.impl.testing_utils import make_job36from plainbox.impl.testing_utils import make_job
37from plainbox.testing_utils.io import TestIO37from plainbox.testing_utils.io import TestIO
3838
@@ -42,17 +42,17 @@
42 def setUp(self):42 def setUp(self):
43 self.parser = argparse.ArgumentParser(prog='test')43 self.parser = argparse.ArgumentParser(prog='test')
44 self.subparsers = self.parser.add_subparsers()44 self.subparsers = self.parser.add_subparsers()
45 self.checkbox = mock.Mock()45 self.provider = mock.Mock()
46 self.config = mock.Mock()46 self.config = mock.Mock()
47 self.ns = mock.Mock()47 self.ns = mock.Mock()
4848
49 def test_init(self):49 def test_init(self):
50 script_cmd = ScriptCommand(self.checkbox, self.config)50 script_cmd = ScriptCommand(self.provider, self.config)
51 self.assertIs(script_cmd.checkbox, self.checkbox)51 self.assertIs(script_cmd.provider, self.provider)
52 self.assertIs(script_cmd.config, self.config)52 self.assertIs(script_cmd.config, self.config)
5353
54 def test_register_parser(self):54 def test_register_parser(self):
55 ScriptCommand(self.checkbox, self.config).register_parser(55 ScriptCommand(self.provider, self.config).register_parser(
56 self.subparsers)56 self.subparsers)
57 with TestIO() as io:57 with TestIO() as io:
58 self.parser.print_help()58 self.parser.print_help()
@@ -75,26 +75,26 @@
7575
76 @mock.patch("plainbox.impl.commands.script.ScriptInvocation")76 @mock.patch("plainbox.impl.commands.script.ScriptInvocation")
77 def test_invoked(self, patched_ScriptInvocation):77 def test_invoked(self, patched_ScriptInvocation):
78 retval = ScriptCommand(self.checkbox, self.config).invoked(self.ns)78 retval = ScriptCommand(self.provider, self.config).invoked(self.ns)
79 patched_ScriptInvocation.assert_called_once_with(79 patched_ScriptInvocation.assert_called_once_with(
80 self.checkbox, self.config, self.ns.job_name)80 self.provider, self.config, self.ns.job_name)
81 self.assertEqual(81 self.assertEqual(
82 retval, patched_ScriptInvocation(82 retval, patched_ScriptInvocation(
83 self.checkbox, self.config,83 self.provider, self.config,
84 self.ns.job_name).run.return_value)84 self.ns.job_name).run.return_value)
8585
8686
87class ScriptInvocationTests(TestCase):87class ScriptInvocationTests(TestCase):
8888
89 def setUp(self):89 def setUp(self):
90 self.checkbox = mock.Mock()90 self.provider = mock.Mock()
91 self.config = PlainBoxConfig()91 self.config = PlainBoxConfig()
92 self.job_name = mock.Mock()92 self.job_name = mock.Mock()
9393
94 def test_init(self):94 def test_init(self):
95 script_inv = ScriptInvocation(95 script_inv = ScriptInvocation(
96 self.checkbox, self.config, self.job_name)96 self.provider, self.config, self.job_name)
97 self.assertIs(script_inv.checkbox, self.checkbox)97 self.assertIs(script_inv.provider, self.provider)
98 self.assertIs(script_inv.config, self.config)98 self.assertIs(script_inv.config, self.config)
99 self.assertIs(script_inv.job_name, self.job_name)99 self.assertIs(script_inv.job_name, self.job_name)
100100
@@ -124,22 +124,27 @@
124 self.assertEqual(retval, 125)124 self.assertEqual(retval, 125)
125125
126 def test_job_with_command(self):126 def test_job_with_command(self):
127 dummy_name = 'foo'
128 dummy_command = 'echo ok'
127 provider = DummyProvider1([129 provider = DummyProvider1([
128 make_job('foo', command='echo ok')])130 make_job(dummy_name, command=dummy_command)])
129 script_inv = ScriptInvocation(provider, self.config, 'foo')131 script_inv = ScriptInvocation(provider, self.config, dummy_name)
130 with TestIO() as io:132 with TestIO() as io:
131 retval = script_inv.run()133 retval = script_inv.run()
132 self.assertEqual(134 self.assertEqual(
133 io.stdout, cleandoc(135 io.stdout, cleandoc(
134 """136 """
135 (job foo, <stdout:00001>) ok137 (job foo, <stdout:00001>) ok
136 """) + '\n')138 """) + '\n' + "{} returned 0\n".format(dummy_name) +
139 "command: {}\n".format(dummy_command))
137 self.assertEqual(retval, 0)140 self.assertEqual(retval, 0)
138141
139 def test_job_with_command_making_files(self):142 def test_job_with_command_making_files(self):
143 dummy_name = 'foo'
144 dummy_command = 'echo ok > file'
140 provider = DummyProvider1([145 provider = DummyProvider1([
141 make_job('foo', command='echo ok > file')])146 make_job(dummy_name, command=dummy_command)])
142 script_inv = ScriptInvocation(provider, self.config, 'foo')147 script_inv = ScriptInvocation(provider, self.config, dummy_name)
143 with TestIO() as io:148 with TestIO() as io:
144 retval = script_inv.run()149 retval = script_inv.run()
145 self.maxDiff = None150 self.maxDiff = None
@@ -148,5 +153,6 @@
148 """153 """
149 Leftover file detected: 'files-created-in-current-dir/file':154 Leftover file detected: 'files-created-in-current-dir/file':
150 files-created-in-current-dir/file:1: ok155 files-created-in-current-dir/file:1: ok
151 """) + '\n')156 """) + '\n' + "{} returned 0\n".format(dummy_name) +
157 "command: {}\n".format(dummy_command))
152 self.assertEqual(retval, 0)158 self.assertEqual(retval, 0)
153159
=== modified file 'plainbox/plainbox/impl/commands/test_sru.py'
--- plainbox/plainbox/impl/commands/test_sru.py 2013-04-24 17:50:58 +0000
+++ plainbox/plainbox/impl/commands/test_sru.py 2013-09-13 17:12:45 +0000
@@ -41,7 +41,8 @@
41 self.maxDiff = None41 self.maxDiff = None
42 expected = """42 expected = """
43 usage: plainbox sru [-h] [--check-config] --secure-id SECURE-ID43 usage: plainbox sru [-h] [--check-config] --secure-id SECURE-ID
44 [--fallback FILE] [--destination URL] [-n]44 [--fallback FILE] [--destination URL] [--staging] [-n]
45 [-i PATTERN] [-x PATTERN] [-w WHITELIST]
4546
46 optional arguments:47 optional arguments:
47 -h, --help show this help message and exit48 -h, --help show this help message and exit
@@ -55,9 +56,21 @@
55 (unset)56 (unset)
56 --destination URL POST the test report XML to this URL (https://certific57 --destination URL POST the test report XML to this URL (https://certific
57 ation.canonical.com/submissions/submit/)58 ation.canonical.com/submissions/submit/)
59 --staging Override --destination to use the staging
60 certification website
5861
59 execution options:62 execution options:
60 -n, --dry-run Skip all usual jobs. Only local, resource and63 -n, --dry-run Skip all usual jobs. Only local, resource and
61 attachment jobs are started64 attachment jobs are started
65
66 job definition options:
67 -i PATTERN, --include-pattern PATTERN
68 Run jobs matching the given regular expression.
69 Matches from the start to the end of the line.
70 -x PATTERN, --exclude-pattern PATTERN
71 Do not run jobs matching the given regular expression.
72 Matches from the start to the end of the line.
73 -w WHITELIST, --whitelist WHITELIST
74 Load whitelist containing run patterns
62 """75 """
63 self.assertEqual(io.combined, cleandoc(expected) + "\n")76 self.assertEqual(io.combined, cleandoc(expected) + "\n")
6477
=== modified file 'plainbox/plainbox/impl/config.py'
--- plainbox/plainbox/impl/config.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/config.py 2013-09-13 17:12:45 +0000
@@ -543,3 +543,16 @@
543 def __call__(self, variable, new_value):543 def __call__(self, variable, new_value):
544 if not self.pattern.match(new_value):544 if not self.pattern.match(new_value):
545 return "does not match pattern: {!r}".format(self.pattern_text)545 return "does not match pattern: {!r}".format(self.pattern_text)
546
547
548class ChoiceValidator(IValidator):
549 """
550 A validator ensuring that values are in a given set
551 """
552
553 def __init__(self, choice_list):
554 self.choice_list = choice_list
555
556 def __call__(self, variable, new_value):
557 if new_value not in self.choice_list:
558 return "must be one of {}".format(", ".join(self.choice_list))
546559
=== added directory 'plainbox/plainbox/impl/dbus'
=== added file 'plainbox/plainbox/impl/dbus/__init__.py'
--- plainbox/plainbox/impl/dbus/__init__.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/dbus/__init__.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,47 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`plainbox.impl.dbus` -- DBus support code for PlainBox
22===========================================================
23"""
24
25__all__ = [
26 'service',
27 'exceptions',
28 'Signature',
29 'Struct',
30 'types',
31 'INTROSPECTABLE_IFACE',
32 'PEER_IFACE',
33 'PROPERTIES_IFACE',
34 'OBJECT_MANAGER_IFACE',
35]
36
37from dbus import INTROSPECTABLE_IFACE
38from dbus import PEER_IFACE
39from dbus import PROPERTIES_IFACE
40from dbus import Signature
41from dbus import Struct
42from dbus import exceptions
43from dbus import types
44
45OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
46
47from plainbox.impl.dbus import service
048
=== added file 'plainbox/plainbox/impl/dbus/decorators.py'
--- plainbox/plainbox/impl/dbus/decorators.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/dbus/decorators.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,351 @@
1"""Service-side D-Bus decorators."""
2
3# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/>
4# Copyright (C) 2003 David Zeuthen
5# Copyright (C) 2004 Rob Taylor
6# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/>
7#
8# Permission is hereby granted, free of charge, to any person
9# obtaining a copy of this software and associated documentation
10# files (the "Software"), to deal in the Software without
11# restriction, including without limitation the rights to use, copy,
12# modify, merge, publish, distribute, sublicense, and/or sell copies
13# of the Software, and to permit persons to whom the Software is
14# furnished to do so, subject to the following conditions:
15#
16# The above copyright notice and this permission notice shall be
17# included in all copies or substantial portions of the Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26# DEALINGS IN THE SOFTWARE.
27
28__all__ = ('method', 'signal')
29__docformat__ = 'restructuredtext'
30
31import inspect
32
33from dbus import validate_interface_name, Signature, validate_member_name
34from dbus.lowlevel import SignalMessage
35from dbus.exceptions import DBusException
36from dbus._compat import is_py2
37
38
39def method(dbus_interface, in_signature=None, out_signature=None,
40 async_callbacks=None,
41 sender_keyword=None, path_keyword=None, destination_keyword=None,
42 message_keyword=None, connection_keyword=None,
43 byte_arrays=False,
44 rel_path_keyword=None, **kwargs):
45 """Factory for decorators used to mark methods of a `dbus.service.Object`
46 to be exported on the D-Bus.
47
48 The decorated method will be exported over D-Bus as the method of the
49 same name on the given D-Bus interface.
50
51 :Parameters:
52 `dbus_interface` : str
53 Name of a D-Bus interface
54 `in_signature` : str or None
55 If not None, the signature of the method parameters in the usual
56 D-Bus notation
57 `out_signature` : str or None
58 If not None, the signature of the return value in the usual
59 D-Bus notation
60 `async_callbacks` : tuple containing (str,str), or None
61 If None (default) the decorated method is expected to return
62 values matching the `out_signature` as usual, or raise
63 an exception on error. If not None, the following applies:
64
65 `async_callbacks` contains the names of two keyword arguments to
66 the decorated function, which will be used to provide a success
67 callback and an error callback (in that order).
68
69 When the decorated method is called via the D-Bus, its normal
70 return value will be ignored; instead, a pair of callbacks are
71 passed as keyword arguments, and the decorated method is
72 expected to arrange for one of them to be called.
73
74 On success the success callback must be called, passing the
75 results of this method as positional parameters in the format
76 given by the `out_signature`.
77
78 On error the decorated method may either raise an exception
79 before it returns, or arrange for the error callback to be
80 called with an Exception instance as parameter.
81
82 `sender_keyword` : str or None
83 If not None, contains the name of a keyword argument to the
84 decorated function, conventionally ``'sender'``. When the
85 method is called, the sender's unique name will be passed as
86 this keyword argument.
87
88 `path_keyword` : str or None
89 If not None (the default), the decorated method will receive
90 the destination object path as a keyword argument with this
91 name. Normally you already know the object path, but in the
92 case of "fallback paths" you'll usually want to use the object
93 path in the method's implementation.
94
95 For fallback objects, `rel_path_keyword` (new in 0.82.2) is
96 likely to be more useful.
97
98 :Since: 0.80.0?
99
100 `rel_path_keyword` : str or None
101 If not None (the default), the decorated method will receive
102 the destination object path, relative to the path at which the
103 object was exported, as a keyword argument with this
104 name. For non-fallback objects the relative path will always be
105 '/'.
106
107 :Since: 0.82.2
108
109 `destination_keyword` : str or None
110 If not None (the default), the decorated method will receive
111 the destination bus name as a keyword argument with this name.
112 Included for completeness - you shouldn't need this.
113
114 :Since: 0.80.0?
115
116 `message_keyword` : str or None
117 If not None (the default), the decorated method will receive
118 the `dbus.lowlevel.MethodCallMessage` as a keyword argument
119 with this name.
120
121 :Since: 0.80.0?
122
123 `connection_keyword` : str or None
124 If not None (the default), the decorated method will receive
125 the `dbus.connection.Connection` as a keyword argument
126 with this name. This is generally only useful for objects
127 that are available on more than one connection.
128
129 :Since: 0.82.0
130
131 `utf8_strings` : bool
132 If False (default), D-Bus strings are passed to the decorated
133 method as objects of class dbus.String, a unicode subclass.
134
135 If True, D-Bus strings are passed to the decorated method
136 as objects of class dbus.UTF8String, a str subclass guaranteed
137 to be encoded in UTF-8.
138
139 This option does not affect object-paths and signatures, which
140 are always 8-bit strings (str subclass) encoded in ASCII.
141
142 :Since: 0.80.0
143
144 `byte_arrays` : bool
145 If False (default), a byte array will be passed to the decorated
146 method as an `Array` (a list subclass) of `Byte` objects.
147
148 If True, a byte array will be passed to the decorated method as
149 a `ByteArray`, a str subclass. This is usually what you want,
150 but is switched off by default to keep dbus-python's API
151 consistent.
152
153 :Since: 0.80.0
154 """
155 validate_interface_name(dbus_interface)
156
157 def decorator(func):
158 # If the function is decorated and uses @functools.wrapper then use the
159 # __wrapped__ attribute to look at the original function signature.
160 #
161 # This allows us to see past the generic *args, **kwargs seen on most decorators.
162 if hasattr(func, '__wrapped__'):
163 args = inspect.getfullargspec(func.__wrapped__)[0]
164 else:
165 args = inspect.getfullargspec(func)[0]
166 args.pop(0)
167 if async_callbacks:
168 if type(async_callbacks) != tuple:
169 raise TypeError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)')
170 if len(async_callbacks) != 2:
171 raise ValueError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)')
172 args.remove(async_callbacks[0])
173 args.remove(async_callbacks[1])
174
175 if sender_keyword:
176 args.remove(sender_keyword)
177 if rel_path_keyword:
178 args.remove(rel_path_keyword)
179 if path_keyword:
180 args.remove(path_keyword)
181 if destination_keyword:
182 args.remove(destination_keyword)
183 if message_keyword:
184 args.remove(message_keyword)
185 if connection_keyword:
186 args.remove(connection_keyword)
187
188 if in_signature:
189 in_sig = tuple(Signature(in_signature))
190
191 if len(in_sig) > len(args):
192 raise ValueError('input signature is longer than the number of arguments taken')
193 elif len(in_sig) < len(args):
194 raise ValueError('input signature is shorter than the number of arguments taken')
195
196 func._dbus_is_method = True
197 func._dbus_async_callbacks = async_callbacks
198 func._dbus_interface = dbus_interface
199 func._dbus_in_signature = in_signature
200 func._dbus_out_signature = out_signature
201 func._dbus_sender_keyword = sender_keyword
202 func._dbus_path_keyword = path_keyword
203 func._dbus_rel_path_keyword = rel_path_keyword
204 func._dbus_destination_keyword = destination_keyword
205 func._dbus_message_keyword = message_keyword
206 func._dbus_connection_keyword = connection_keyword
207 func._dbus_args = args
208 func._dbus_get_args_options = dict(byte_arrays=byte_arrays)
209 if is_py2:
210 func._dbus_get_args_options['utf8_strings'] = kwargs.get(
211 'utf8_strings', False)
212 elif 'utf8_strings' in kwargs:
213 raise TypeError("unexpected keyword argument 'utf8_strings'")
214 return func
215
216 return decorator
217
218
219def signal(dbus_interface, signature=None, path_keyword=None,
220 rel_path_keyword=None):
221 """Factory for decorators used to mark methods of a `dbus.service.Object`
222 to emit signals on the D-Bus.
223
224 Whenever the decorated method is called in Python, after the method
225 body is executed, a signal with the same name as the decorated method,
226 with the given D-Bus interface, will be emitted from this object.
227
228 :Parameters:
229 `dbus_interface` : str
230 The D-Bus interface whose signal is emitted
231 `signature` : str
232 The signature of the signal in the usual D-Bus notation
233
234 `path_keyword` : str or None
235 A keyword argument to the decorated method. If not None,
236 that argument will not be emitted as an argument of
237 the signal, and when the signal is emitted, it will appear
238 to come from the object path given by the keyword argument.
239
240 Note that when calling the decorated method, you must always
241 pass in the object path as a keyword argument, not as a
242 positional argument.
243
244 This keyword argument cannot be used on objects where
245 the class attribute ``SUPPORTS_MULTIPLE_OBJECT_PATHS`` is true.
246
247 :Deprecated: since 0.82.0. Use `rel_path_keyword` instead.
248
249 `rel_path_keyword` : str or None
250 A keyword argument to the decorated method. If not None,
251 that argument will not be emitted as an argument of
252 the signal.
253
254 When the signal is emitted, if the named keyword argument is given,
255 the signal will appear to come from the object path obtained by
256 appending the keyword argument to the object's object path.
257 This is useful to implement "fallback objects" (objects which
258 own an entire subtree of the object-path tree).
259
260 If the object is available at more than one object-path on the
261 same or different connections, the signal will be emitted at
262 an appropriate object-path on each connection - for instance,
263 if the object is exported at /abc on connection 1 and at
264 /def and /x/y/z on connection 2, and the keyword argument is
265 /foo, then signals will be emitted from /abc/foo and /def/foo
266 on connection 1, and /x/y/z/foo on connection 2.
267
268 :Since: 0.82.0
269 """
270 validate_interface_name(dbus_interface)
271
272 if path_keyword is not None:
273 from warnings import warn
274 warn(DeprecationWarning('dbus.service.signal::path_keyword has been '
275 'deprecated since dbus-python 0.82.0, and '
276 'will not work on objects that support '
277 'multiple object paths'),
278 DeprecationWarning, stacklevel=2)
279 if rel_path_keyword is not None:
280 raise TypeError('dbus.service.signal::path_keyword and '
281 'rel_path_keyword cannot both be used')
282
283 def decorator(func):
284 member_name = func.__name__
285 validate_member_name(member_name)
286
287 def emit_signal(self, *args, **keywords):
288 abs_path = None
289 if path_keyword is not None:
290 if self.SUPPORTS_MULTIPLE_OBJECT_PATHS:
291 raise TypeError('path_keyword cannot be used on the '
292 'signals of an object that supports '
293 'multiple object paths')
294 abs_path = keywords.pop(path_keyword, None)
295 if (abs_path != self.__dbus_object_path__ and
296 not self.__dbus_object_path__.startswith(abs_path + '/')):
297 raise ValueError('Path %r is not below %r', abs_path,
298 self.__dbus_object_path__)
299
300 rel_path = None
301 if rel_path_keyword is not None:
302 rel_path = keywords.pop(rel_path_keyword, None)
303
304 func(self, *args, **keywords)
305
306 for location in self.locations:
307 if abs_path is None:
308 # non-deprecated case
309 if rel_path is None or rel_path in ('/', ''):
310 object_path = location[1]
311 else:
312 # will be validated by SignalMessage ctor in a moment
313 object_path = location[1] + rel_path
314 else:
315 object_path = abs_path
316
317 message = SignalMessage(object_path,
318 dbus_interface,
319 member_name)
320 message.append(signature=signature, *args)
321
322 location[0].send_message(message)
323 # end emit_signal
324
325 args = inspect.getargspec(func)[0]
326 args.pop(0)
327
328 for keyword in rel_path_keyword, path_keyword:
329 if keyword is not None:
330 try:
331 args.remove(keyword)
332 except ValueError:
333 raise ValueError('function has no argument "%s"' % keyword)
334
335 if signature:
336 sig = tuple(Signature(signature))
337
338 if len(sig) > len(args):
339 raise ValueError('signal signature is longer than the number of arguments provided')
340 elif len(sig) < len(args):
341 raise ValueError('signal signature is shorter than the number of arguments provided')
342
343 emit_signal.__name__ = func.__name__
344 emit_signal.__doc__ = func.__doc__
345 emit_signal._dbus_is_signal = True
346 emit_signal._dbus_interface = dbus_interface
347 emit_signal._dbus_signature = signature
348 emit_signal._dbus_args = args
349 return emit_signal
350
351 return decorator
0352
=== added file 'plainbox/plainbox/impl/dbus/service.py'
--- plainbox/plainbox/impl/dbus/service.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/dbus/service.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,662 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`plainbox.impl.dbus.service` -- DBus Service support code for PlainBox
22===========================================================================
23"""
24
25import logging
26import threading
27import weakref
28
29import _dbus_bindings
30import dbus
31import dbus.service
32import dbus.exceptions
33
34from plainbox.impl.signal import Signal
35from plainbox.impl.dbus import INTROSPECTABLE_IFACE
36from plainbox.impl.dbus import OBJECT_MANAGER_IFACE
37from plainbox.impl.dbus import PROPERTIES_IFACE
38# Note: use our own version of the decorators because
39# vanilla versions choke on annotations
40from plainbox.impl.dbus.decorators import method, signal
41
42
43# This is the good old standard python property decorator
44_property = property
45
46__all__ = [
47 'Interface',
48 'Object',
49 'method',
50 'property',
51 'signal',
52]
53
54logger = logging.getLogger("plainbox.dbus")
55
56
57class InterfaceType(dbus.service.InterfaceType):
58 """
59 Subclass of :class:`dbus.service.InterfaceType` that also handles
60 properties.
61 """
62
63 def _reflect_on_property(cls, func):
64 reflection_data = (
65 ' <property name="{}" type="{}" access="{}"/>\n').format(
66 func._dbus_property, func._signature,
67 func.dbus_access_flag)
68 return reflection_data
69
70
71#Subclass of :class:`dbus.service.Interface` that also handles properties
72Interface = InterfaceType('Interface', (dbus.service.Interface,), {})
73
74
75class property:
76 """
77 property that handles DBus stuff
78 """
79
80 def __init__(self, signature, dbus_interface, dbus_property=None,
81 setter=False):
82 """
83 Initialize new dbus_property with the given interface name.
84
85 If dbus_property is not specified it is set to the name of the
86 decorated method. In special circumstances you may wish to specify
87 alternate dbus property name explicitly.
88
89 If setter is set to True then the implicit decorated function is a
90 setter, not the default getter. This allows to define write-only
91 properties.
92 """
93 self.__name__ = None
94 self.__doc__ = None
95 self._signature = signature
96 self._dbus_interface = dbus_interface
97 self._dbus_property = dbus_property
98 self._getf = None
99 self._setf = None
100 self._implicit_setter = setter
101
102 def __repr__(self):
103 return "<dbus.service.property {!r}>".format(self.__name__)
104
105 @_property
106 def dbus_access_flag(self):
107 """
108 access flag of this DBus property
109
110 :returns: either "readwrite", "read" or "write"
111 :raises TypeError: if the property is ill-defined
112 """
113 if self._getf and self._setf:
114 return "readwrite"
115 elif self._getf:
116 return "read"
117 elif self._setf:
118 return "write"
119 else:
120 raise TypeError(
121 "property provides neither readable nor writable")
122
123 @_property
124 def dbus_interface(self):
125 """
126 name of the DBus interface of this DBus property
127 """
128 return self._dbus_interface
129
130 @_property
131 def dbus_property(self):
132 """
133 name of this DBus property
134 """
135 return self._dbus_property
136
137 @_property
138 def signature(self):
139 """
140 signature of this DBus property
141 """
142 return self._signature
143
144 @_property
145 def setter(self):
146 """
147 decorator for setter functions
148
149 This property can be used to decorate additional method that
150 will be used as a property setter. Otherwise properties cannot
151 be assigned.
152 """
153 def decorator(func):
154 self._setf = func
155 return self
156 return decorator
157
158 @_property
159 def getter(self):
160 """
161 decorator for getter functions
162
163 This property can be used to decorate additional method that
164 will be used as a property getter. It is only provider for parity
165 as by default, the @dbus.service.property() decorator designates
166 a getter function. This behavior can be controlled by passing
167 setter=True to the property initializer.
168 """
169 def decorator(func):
170 self._getf = func
171 return self
172 return decorator
173
174 def __call__(self, func):
175 """
176 Decorate a getter function and return the property object
177
178 This method sets __name__, __doc__ and _dbus_property.
179 """
180 self.__name__ = func.__name__
181 if self.__doc__ is None:
182 self.__doc__ = func.__doc__
183 if self._dbus_property is None:
184 self._dbus_property = func.__name__
185 if self._implicit_setter:
186 return self.setter(func)
187 else:
188 return self.getter(func)
189
190 def __get__(self, instance, owner):
191 if instance is None:
192 return self
193 else:
194 if self._getf is None:
195 raise dbus.exceptions.DBusException(
196 "property is not readable")
197 return self._getf(instance)
198
199 def __set__(self, instance, value):
200 if self._setf is None:
201 raise dbus.exceptions.DBusException(
202 "property is not writable")
203 self._setf(instance, value)
204
205 # This little helper is here is to help :meth:`Object.Introspect()`
206 # figure out how to handle properties.
207 _dbus_is_property = True
208
209
210class Object(Interface, dbus.service.Object):
211 """
212 dbus.service.Object subclass that providers additional features.
213
214 This class providers the following additional features:
215
216 * Implementation of the PROPERTIES_IFACE. This includes the methods
217 Get(), Set(), GetAll() and the signal PropertiesChanged()
218
219 * Implementation of the OBJECT_MANAGER_IFACE. This includes the method
220 GetManagedObjects() and signals InterfacesAdded() and
221 InterfacesRemoved().
222
223 * Tracking of object-path-to-object association using the new
224 :meth:`find_object_by_path()` method
225
226 * Selective activation of any of the above interfaces using
227 :meth:`should_expose_interface()` method.
228
229 * Improved version of the INTROSPECTABLE_IFACE that understands properties
230 """
231
232 def __init__(self, conn=None, object_path=None, bus_name=None):
233 dbus.service.Object.__init__(self, conn, object_path, bus_name)
234 self._managed_object_list = []
235
236 # [ Public DBus methods of the INTROSPECTABLE_IFACE interface ]
237
238 @method(
239 dbus_interface=INTROSPECTABLE_IFACE,
240 in_signature='', out_signature='s',
241 path_keyword='object_path', connection_keyword='connection')
242 def Introspect(self, object_path, connection):
243 """
244 Return a string of XML encoding this object's supported interfaces,
245 methods and signals.
246 """
247 logger.debug("Introspect(object_path=%r)", object_path)
248 reflection_data = (
249 _dbus_bindings.DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE)
250 reflection_data += '<node name="%s">\n' % object_path
251 interfaces = self._dct_entry
252 for (name, funcs) in interfaces.items():
253 # Allow classes to ignore certain interfaces This is useful because
254 # this class implements all kinds of methods internally (for
255 # simplicity) but does not really advertise them all directly
256 # unless asked to.
257 if not self.should_expose_interface(name):
258 continue
259 reflection_data += ' <interface name="%s">\n' % (name)
260 for func in funcs.values():
261 if getattr(func, '_dbus_is_method', False):
262 reflection_data += self.__class__._reflect_on_method(func)
263 elif getattr(func, '_dbus_is_signal', False):
264 reflection_data += self.__class__._reflect_on_signal(func)
265 elif getattr(func, '_dbus_is_property', False):
266 reflection_data += (
267 self.__class__._reflect_on_property(func))
268 reflection_data += ' </interface>\n'
269 for name in connection.list_exported_child_objects(object_path):
270 reflection_data += ' <node name="%s"/>\n' % name
271 reflection_data += '</node>\n'
272 logger.debug("Introspect() returns: %s", reflection_data)
273 return reflection_data
274
275 # [ Public DBus methods of the PROPERTIES_IFACE interface ]
276
277 @dbus.service.method(
278 dbus_interface=dbus.PROPERTIES_IFACE,
279 in_signature="ss", out_signature="v")
280 def Get(self, interface_name, property_name):
281 """
282 Get the value of a property @property_name on interface
283 @interface_name.
284 """
285 logger.debug(
286 "%r.Get(%r, %r) -> ...",
287 self, interface_name, property_name)
288 try:
289 props = self._dct_entry[interface_name]
290 except KeyError:
291 raise dbus.exceptions.DBusException(
292 dbus.PROPERTIES_IFACE,
293 "No such interface {}".format(interface_name))
294 try:
295 prop = props[property_name]
296 except KeyError:
297 raise dbus.exceptions.DBusException(
298 dbus.PROPERTIES_IFACE,
299 "No such property {}:{}".format(
300 interface_name, property_name))
301 try:
302 value = prop.__get__(self, self.__class__)
303 except dbus.exceptions.DBusException:
304 raise
305 except Exception as exc:
306 logger.exception(
307 "runaway exception from Get(%r, %r)",
308 interface_name, property_name)
309 raise dbus.exceptions.DBusException(
310 dbus.PROPERTIES_IFACE,
311 "Unable to get property interface/property {}:{}: {!r}".format(
312 interface_name, property_name, exc))
313 else:
314 logger.debug(
315 "%r.Get(%r, %r) -> %r",
316 self, interface_name, property_name, value)
317 return value
318
319 @dbus.service.method(
320 dbus_interface=dbus.PROPERTIES_IFACE,
321 in_signature="ssv", out_signature="")
322 def Set(self, interface_name, property_name, value):
323 logger.debug(
324 "%r.Set(%r, %r, %r) -> ...",
325 self, interface_name, property_name, value)
326 try:
327 props = self._dct_entry[interface_name]
328 except KeyError:
329 raise dbus.exceptions.DBusException(
330 dbus.PROPERTIES_IFACE,
331 "No such interface {}".format(interface_name))
332 try:
333 # Map the real property name
334 prop = {
335 prop.dbus_property: prop
336 for prop in props.values()
337 if isinstance(prop, property)
338 }[property_name]
339 if not isinstance(prop, property):
340 raise KeyError(property_name)
341 except KeyError:
342 raise dbus.exceptions.DBusException(
343 dbus.PROPERTIES_IFACE,
344 "No such property {}:{}".format(
345 interface_name, property_name))
346 try:
347 prop.__set__(self, value)
348 except dbus.exceptions.DBusException:
349 raise
350 except Exception as exc:
351 logger.exception(
352 "runaway exception from %r.Set(%r, %r, %r)",
353 self, interface_name, property_name, value)
354 raise dbus.exceptions.DBusException(
355 dbus.PROPERTIES_IFACE,
356 "Unable to set property {}:{}: {!r}".format(
357 interface_name, property_name, exc))
358 logger.debug(
359 "%r.Set(%r, %r, %r) -> None",
360 self, interface_name, property_name, value)
361
362 @dbus.service.method(
363 dbus_interface=dbus.PROPERTIES_IFACE,
364 in_signature="s", out_signature="a{sv}")
365 def GetAll(self, interface_name):
366 logger.debug("%r.GetAll(%r)", self, interface_name)
367 try:
368 props = self._dct_entry[interface_name]
369 except KeyError:
370 raise dbus.exceptions.DBusException(
371 dbus.PROPERTIES_IFACE,
372 "No such interface {}".format(interface_name))
373 result = {}
374 for prop in props.values():
375 if not isinstance(prop, property):
376 continue
377 prop_name = prop.dbus_property
378 try:
379 prop_value = prop.__get__(self, self.__class__)
380 except:
381 logger.exception(
382 "Unable to read property %r from %r", prop, self)
383 else:
384 result[prop_name] = prop_value
385 return result
386
387 @dbus.service.signal(
388 dbus_interface=dbus.PROPERTIES_IFACE,
389 signature='sa{sv}as')
390 def PropertiesChanged(
391 self, interface_name, changed_properties, invalidated_properties):
392 logger.debug(
393 "PropertiesChanged(%r, %r, %r)",
394 interface_name, changed_properties, invalidated_properties)
395
396 # [ Public DBus methods of the OBJECT_MANAGER_IFACE interface ]
397
398 @dbus.service.method(
399 dbus_interface=OBJECT_MANAGER_IFACE,
400 in_signature="", out_signature="a{oa{sa{sv}}}")
401 def GetManagedObjects(self):
402 logger.debug("%r.GetManagedObjects() -> ...", self)
403 result = {}
404 for obj in self._managed_object_list:
405 logger.debug("Looking for stuff exported by %r", obj)
406 result[obj] = {}
407 for iface_name in obj._dct_entry.keys():
408 props = obj.GetAll(iface_name)
409 if len(props):
410 result[obj][iface_name] = props
411 logger.debug("%r.GetManagedObjects() -> %r", self, result)
412 return result
413
414 @dbus.service.signal(
415 dbus_interface=OBJECT_MANAGER_IFACE,
416 signature='oa{sa{sv}}')
417 def InterfacesAdded(self, object_path, interfaces_and_properties):
418 logger.debug("%r.InterfacesAdded(%r, %r)",
419 self, object_path, interfaces_and_properties)
420
421 @dbus.service.signal(
422 dbus_interface=OBJECT_MANAGER_IFACE, signature='oas')
423 def InterfacesRemoved(self, object_path, interfaces):
424 logger.debug("%r.InterfacesRemoved(%r, %r)",
425 self, object_path, interfaces)
426
427 # [ Overridden methods of dbus.service.Object ]
428
429 def add_to_connection(self, connection, path):
430 """
431 Version of dbus.service.Object.add_to_connection() that keeps track of
432 all object paths.
433 """
434 with self._object_path_map_lock:
435 # Super-call add_to_connection(). This can fail which is
436 # okay as we haven't really modified anything yet.
437 super(Object, self).add_to_connection(connection, path)
438 # Touch self.connection, this will fail if the call above failed
439 # and self._connection (mind the leading underscore) is still None.
440 # It will also fail if the object is being exposed on multiple
441 # connections (so self._connection is _MANY). We are interested in
442 # the second check as _MANY connections are not supported here.
443 self.connection
444 # If everything is okay, just add the specified path to the
445 # _object_path_to_object_map.
446 self._object_path_to_object_map[path] = self
447
448 def remove_from_connection(self, connection=None, path=None):
449 with self._object_path_map_lock:
450 # Touch self.connection, this triggers a number of interesting
451 # checks, in particular checks for self._connection (mind the
452 # leading underscore) being _MANY or being None. Both of those
453 # throw an AttributeError that we can simply propagate at this
454 # point.
455 self.connection
456 # Create a copy of locations. This is required because locations
457 # are modified by remove_from_connection() which can also fail. If
458 # we were to use self.locations here directly we would have to undo
459 # any changes if remove_from_connection() raises an exception.
460 # Instead it is easier to first super-call remove_from_connection()
461 # and then do what we need to at this layer, after
462 # remove_from_connection() finishes successfully.
463 locations_copy = list(self.locations)
464 # Super-call remove_from_connection()
465 super(Object, self).remove_from_connection(connection, path)
466 # If either path or connection are none then treat them like
467 # match-any wild-cards. The same logic is implemented in the
468 # superclass version of this method.
469 if path is None or connection is None:
470 # Location is a tuple of at least two elements, connection and
471 # path. There may be other elements added later so let's not
472 # assume this is a simple pair.
473 for location in locations_copy:
474 location_conn = location[0]
475 location_path = location[1]
476 # If (connection matches or is None)
477 # and (path matches or is None)
478 # then remove that association
479 if ((location_conn == connection or connection is None)
480 and (path == location_path or path is None)):
481 del self._object_path_to_object_map[location_path]
482 else:
483 # If connection and path were specified, just remove the
484 # association from the specified path.
485 del self._object_path_to_object_map[path]
486
487 # [ Custom Extension Methods ]
488
489 def should_expose_interface(self, iface_name):
490 """
491 Check if the specified interface should be exposed.
492
493 This method controls which of the interfaces are visible as implemented
494 by this Object. By default objects don't implement any interface expect
495 for PEER_IFACE. There are two more interfaces that are implemented
496 internally but need to be explicitly exposed: the PROPERTIES_IFACE and
497 OBJECT_MANAGER_IFACE.
498
499 Typically subclasses should NOT override this method, instead
500 subclasses should define class-scope HIDDEN_INTERFACES as a
501 frozenset() of classes to hide and remove one of the entries found in
502 _STD_INTERFACES from it to effectively enable that interface.
503 """
504 return iface_name not in self.HIDDEN_INTERFACES
505
506 @classmethod
507 def find_object_by_path(cls, object_path):
508 """
509 Find and return the object that is exposed as object_path on any
510 connection. Using multiple connections is not supported at this time.
511
512 .. note::
513 This obviously only works for objects exposed from the same
514 application. The main use case is to have a way to lookup object
515 paths that may be passed as arguments and also originate in the
516 same application.
517 """
518 # XXX: ideally this would be per-connection method.
519 with cls._object_path_map_lock:
520 return cls._object_path_to_object_map[object_path]
521
522 @_property
523 def managed_objects(self):
524 """
525 list of of managed objects.
526
527 This collection is a part of the OBJECT_MANAGER_IFACE. While it can be
528 manipulated directly (technically) it should only be manipulated using
529 :meth:`add_managed_object()`, :meth:`add_manage_object_list()`,
530 :meth:`remove_managed_object()` and
531 :meth:`remove_managed_object_list()` as they send appropriate DBus
532 signals.
533 """
534 return self._managed_object_list
535
536 def add_managed_object(self, obj):
537 self.add_managed_object_list([obj])
538
539 def remove_managed_object(self, obj):
540 self.remove_managed_object_list([obj])
541
542 def add_managed_object_list(self, obj_list):
543 logger.debug("Adding managed objects: %s", obj_list)
544 for obj in obj_list:
545 if not isinstance(obj, Object):
546 raise TypeError("obj must be of type {!r}".format(Object))
547 old = self._managed_object_list
548 new = list(old)
549 new.extend(obj_list)
550 self._managed_object_list = new
551 self._on_managed_objects_changed(old, new)
552
553 def remove_managed_object_list(self, obj_list):
554 logger.debug("Removing managed objects: %s", obj_list)
555 for obj in obj_list:
556 if not isinstance(obj, Object):
557 raise TypeError("obj must be of type {!r}".format(Object))
558 old = self._managed_object_list
559 new = list(old)
560 for obj in obj_list:
561 new.remove(obj)
562 self._managed_object_list = new
563 self._on_managed_objects_changed(old, new)
564
565 # [ Custom Private Implementation Data ]
566
567 _STD_INTERFACES = frozenset([
568 INTROSPECTABLE_IFACE,
569 OBJECT_MANAGER_IFACE,
570 # TODO: peer interface is not implemented in this class
571 # PEER_IFACE,
572 PROPERTIES_IFACE
573 ])
574
575 HIDDEN_INTERFACES = frozenset([
576 OBJECT_MANAGER_IFACE,
577 PROPERTIES_IFACE
578 ])
579
580 # Lock protecting access to _object_path_to_object_map.
581 # XXX: ideally this would be a per-connection attribute
582 _object_path_map_lock = threading.Lock()
583
584 # Map of object_path -> dbus.service.Object instances
585 # XXX: ideally this would be a per-connection attribute
586 _object_path_to_object_map = weakref.WeakValueDictionary()
587
588 # [ Custom Private Implementation Methods ]
589
590 @_property
591 def _dct_key(self):
592 """
593 the key indexing this Object in Object.__class__._dbus_class_table
594 """
595 return self.__class__.__module__ + '.' + self.__class__.__name__
596
597 @_property
598 def _dct_entry(self):
599 """
600 same as self.__class__._dbus_class_table[self._dct_key]
601 """
602 return self.__class__._dbus_class_table[self._dct_key]
603
604 @Signal.define
605 def _on_managed_objects_changed(self, old_objs, new_objs):
606 logger.debug("%r._on_managed_objects_changed(%r, %r)",
607 self, old_objs, new_objs)
608 for obj in frozenset(new_objs) - frozenset(old_objs):
609 ifaces_and_props = {}
610 for iface_name in obj._dct_entry.keys():
611 try:
612 props = obj.GetAll(iface_name)
613 except dbus.exceptions.DBusException as exc:
614 logger.warning("Caught %r", exc)
615 else:
616 if len(props):
617 ifaces_and_props[iface_name] = props
618 self.InterfacesAdded(obj.__dbus_object_path__, ifaces_and_props)
619 for obj in frozenset(old_objs) - frozenset(new_objs):
620 ifaces = list(obj._dct_entry.keys())
621 self.InterfacesRemoved(obj.__dbus_object_path__, ifaces)
622
623
624class ObjectWrapper(Object):
625 """
626 Wrapper for a single python object which makes it easier to expose over
627 DBus as a service. The object should be injected into something that
628 extends dbus.service.Object class.
629
630 The class maintains an association between each wrapper and native object
631 and offers methods for converting between the two.
632 """
633
634 # Lock protecting access to _native_id_to_wrapper_map
635 _native_id_map_lock = threading.Lock()
636
637 # Man of id(wrapper.native) -> wrapper
638 _native_id_to_wrapper_map = weakref.WeakValueDictionary()
639
640 def __init__(self, native, conn=None, object_path=None, bus_name=None):
641 """
642 Create a new wrapper for the specified native object
643 """
644 super(ObjectWrapper, self).__init__(conn, object_path, bus_name)
645 with self._native_id_map_lock:
646 self._native_id_to_wrapper_map[id(native)] = self
647 self._native = native
648
649 @_property
650 def native(self):
651 """
652 native python object being wrapped by this wrapper
653 """
654 return self._native
655
656 @classmethod
657 def find_wrapper_by_native(cls, native):
658 """
659 Find the wrapper associated with the specified native object
660 """
661 with cls._native_id_map_lock:
662 return cls._native_id_to_wrapper_map[id(native)]
0663
=== modified file 'plainbox/plainbox/impl/exporter/__init__.py'
--- plainbox/plainbox/impl/exporter/__init__.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/exporter/__init__.py 2013-09-13 17:12:45 +0000
@@ -148,6 +148,9 @@
148 continue148 continue
149 data['result_map'][job_name] = OrderedDict()149 data['result_map'][job_name] = OrderedDict()
150 data['result_map'][job_name]['outcome'] = job_state.result.outcome150 data['result_map'][job_name]['outcome'] = job_state.result.outcome
151 if job_state.result.execution_duration:
152 data['result_map'][job_name]['execution_duration'] = \
153 job_state.result.execution_duration
151 if self.OPTION_WITH_COMMENTS in self._option_list:154 if self.OPTION_WITH_COMMENTS in self._option_list:
152 data['result_map'][job_name]['comments'] = \155 data['result_map'][job_name]['comments'] = \
153 job_state.result.comments156 job_state.result.comments
@@ -155,12 +158,12 @@
155 # Add Parent hash if requested158 # Add Parent hash if requested
156 if self.OPTION_WITH_JOB_VIA in self._option_list:159 if self.OPTION_WITH_JOB_VIA in self._option_list:
157 data['result_map'][job_name]['via'] = \160 data['result_map'][job_name]['via'] = \
158 job_state.result.job.via161 job_state.job.via
159162
160 # Add Job hash if requested163 # Add Job hash if requested
161 if self.OPTION_WITH_JOB_HASH in self._option_list:164 if self.OPTION_WITH_JOB_HASH in self._option_list:
162 data['result_map'][job_name]['hash'] = \165 data['result_map'][job_name]['hash'] = \
163 job_state.result.job.get_checksum()166 job_state.job.get_checksum()
164167
165 # Add Job definitions if requested168 # Add Job definitions if requested
166 if self.OPTION_WITH_JOB_DEFS in self._option_list:169 if self.OPTION_WITH_JOB_DEFS in self._option_list:
@@ -170,17 +173,17 @@
170 'command',173 'command',
171 'description',174 'description',
172 ):175 ):
173 if not getattr(job_state.result.job, prop):176 if not getattr(job_state.job, prop):
174 continue177 continue
175 data['result_map'][job_name][prop] = getattr(178 data['result_map'][job_name][prop] = getattr(
176 job_state.result.job, prop)179 job_state.job, prop)
177180
178 # Add Attachments if requested181 # Add Attachments if requested
179 if job_state.result.job.plugin == 'attachment':182 if job_state.job.plugin == 'attachment':
180 if self.OPTION_WITH_ATTACHMENTS in self._option_list:183 if self.OPTION_WITH_ATTACHMENTS in self._option_list:
181 raw_bytes = b''.join(184 raw_bytes = b''.join(
182 (record[2] for record in185 (record[2] for record in
183 job_state.result.io_log if record[1] == 'stdout'))186 job_state.result.get_io_log() if record[1] == 'stdout'))
184 data['attachment_map'][job_name] = \187 data['attachment_map'][job_name] = \
185 base64.standard_b64encode(raw_bytes).decode('ASCII')188 base64.standard_b64encode(raw_bytes).decode('ASCII')
186 continue # Don't add attachments IO logs to the result_map189 continue # Don't add attachments IO logs to the result_map
@@ -191,12 +194,12 @@
191 # saved, discarding stream name and the relative timestamp.194 # saved, discarding stream name and the relative timestamp.
192 if self.OPTION_SQUASH_IO_LOG in self._option_list:195 if self.OPTION_SQUASH_IO_LOG in self._option_list:
193 io_log_data = self._squash_io_log(196 io_log_data = self._squash_io_log(
194 job_state.result.io_log)197 job_state.result.get_io_log())
195 elif self.OPTION_FLATTEN_IO_LOG in self._option_list:198 elif self.OPTION_FLATTEN_IO_LOG in self._option_list:
196 io_log_data = self._flatten_io_log(199 io_log_data = self._flatten_io_log(
197 job_state.result.io_log)200 job_state.result.get_io_log())
198 else:201 else:
199 io_log_data = self._io_log(job_state.result.io_log)202 io_log_data = self._io_log(job_state.result.get_io_log())
200 data['result_map'][job_name]['io_log'] = io_log_data203 data['result_map'][job_name]['io_log'] = io_log_data
201 return data204 return data
202205
@@ -288,8 +291,10 @@
288 for entry_point in sorted(iterator, key=lambda ep: ep.name):291 for entry_point in sorted(iterator, key=lambda ep: ep.name):
289 try:292 try:
290 exporter_cls = entry_point.load()293 exporter_cls = entry_point.load()
294 except pkg_resources.DistributionNotFound as exc:
295 logger.info("Unable to load %s: %s", entry_point, exc)
291 except ImportError as exc:296 except ImportError as exc:
292 logger.exception("Unable to import {}: {}", entry_point, exc)297 logger.exception("Unable to import %s: %s", entry_point, exc)
293 else:298 else:
294 exporter_map[entry_point.name] = exporter_cls299 exporter_map[entry_point.name] = exporter_cls
295 return exporter_map300 return exporter_map
296301
=== added file 'plainbox/plainbox/impl/exporter/html.py'
--- plainbox/plainbox/impl/exporter/html.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/exporter/html.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,145 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7# Daniel Manrique <daniel.manrique@canonical.com>
8#
9# Checkbox is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Checkbox is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
21
22"""
23:mod:`plainbox.impl.exporter.html`
24==================================
25
26HTML exporter for human consumption
27
28.. warning::
29 THIS MODULE DOES NOT HAVE A STABLE PUBLIC API
30"""
31
32from string import Template
33import base64
34import logging
35import mimetypes
36
37from lxml import etree as ET
38from pkg_resources import resource_filename
39
40from plainbox.impl.exporter.xml import XMLSessionStateExporter
41
42
43logger = logging.getLogger("plainbox.exporter.html")
44
45
46class HTMLResourceInliner(object):
47 """ A helper class to inline resources referenced in an lxml tree.
48 """
49 def _resource_content(self, url):
50 try:
51 with open(url, 'rb') as f:
52 file_contents = f.read()
53 except (IOError, OSError):
54 logger.warning("Unable to load resource %s, not inlining",
55 url)
56 return ""
57 type, encoding = mimetypes.guess_type(url)
58 if not encoding:
59 encoding = "utf-8"
60 if type in("text/css", "application/javascript"):
61 return file_contents.decode(encoding)
62 elif type in("image/png", "image/jpg"):
63 b64_data = base64.b64encode(file_contents)
64 b64_data = b64_data.decode("ascii")
65 return_string = "data:{};base64,{}".format(type, b64_data)
66 return return_string
67 else:
68 logger.warning("Resource of type %s unknown", type)
69 #Strip it out, better not to have it.
70 return ""
71
72 def inline_resources(self, document_tree):
73 """
74 Replace references to external resources by an in-place (inlined)
75 representation of each resource.
76
77 Currently images, stylesheets and scripts are inlined.
78
79 Only local (i.e. file) resources/locations are supported. If a
80 non-local resource is requested for inlining, it will be removed
81 (replaced by a blank string), with the goal that the resulting
82 lxml tree will not reference any unreachable resources.
83
84 :param document_tree:
85 lxml tree to process.
86
87 :returns:
88 lxml tree with some elements replaced by their inlined
89 representation.
90 """
91 # Try inlining using result_tree here.
92 for node in document_tree.xpath('//script'):
93 # These have src attribute, need to remove the
94 # attribute and add the content of the src file
95 # as the node's text
96 src = node.attrib.pop('src')
97 node.text = self._resource_content(src)
98
99 for node in document_tree.xpath('//link[@rel="stylesheet"]'):
100 # These have a href attribute and need to be completely replaced
101 # by a new <style> node with contents of the href file
102 # as its text.
103 src = node.attrib.pop('href')
104 type = node.attrib.pop('type')
105 style_elem = ET.Element("style")
106 style_elem.attrib['type'] = type
107 style_elem.text = self._resource_content(src)
108 node.getparent().append(style_elem)
109 # Now zorch the existing node
110 node.getparent().remove(node)
111
112 for node in document_tree.xpath('//img'):
113 # src attribute points to a file and needs to
114 # contain the base64 encoded version of that file.
115 src = node.attrib.pop('src')
116 node.attrib['src'] = self._resource_content(src)
117 return document_tree
118
119
120class HTMLSessionStateExporter(XMLSessionStateExporter):
121 """
122 Session state exporter creating HTML documents.
123
124 It basically applies an xslt to the XMLSessionStateExporter output,
125 and then inlines some resources to produce a monolithic report in a
126 single file.
127 """
128
129 def dump(self, data, stream):
130 """
131 Public method to dump the HTML report to a stream
132 """
133 root = self.get_root_element(data)
134 self.xslt_filename = resource_filename(
135 "plainbox", "data/report/checkbox.xsl")
136 template_substitutions = {
137 'PLAINBOX_ASSETS': resource_filename("plainbox", "data/")}
138 with open(self.xslt_filename, encoding="UTF-8") as xslt_file:
139 xslt_template = Template(xslt_file.read())
140 xslt_data = xslt_template.substitute(template_substitutions)
141 xslt_root = ET.XML(xslt_data)
142 transformer = ET.XSLT(xslt_root)
143 r_tree = transformer(root)
144 inlined_result_tree = HTMLResourceInliner().inline_resources(r_tree)
145 stream.write(ET.tostring(inlined_result_tree, pretty_print=True))
0146
=== added file 'plainbox/plainbox/impl/exporter/test_html.py'
--- plainbox/plainbox/impl/exporter/test_html.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/exporter/test_html.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,142 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7# Daniel Manrique <daniel.manrique@canonical.com>
8#
9# Checkbox is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Checkbox is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
21
22"""
23plainbox.impl.exporter.test_html
24================================
25
26Test definitions for plainbox.impl.exporter.html module
27"""
28from io import StringIO
29from string import Template
30from unittest import TestCase
31import io
32
33from lxml import etree as ET
34from pkg_resources import resource_filename
35from pkg_resources import resource_string
36
37from plainbox.testing_utils import resource_json
38from plainbox.impl.exporter.html import HTMLResourceInliner
39from plainbox.impl.exporter.html import HTMLSessionStateExporter
40
41
42class HTMLInlinerTests(TestCase):
43 def setUp(self):
44 template_substitutions = {
45 'PLAINBOX_ASSETS': resource_filename("plainbox", "data/")}
46 test_file_location = "test-data/html-exporter/html-inliner.html"
47 test_file = resource_filename("plainbox",
48 test_file_location)
49 with open(test_file) as html_file:
50 html_template = Template(html_file.read())
51 html_content = html_template.substitute(template_substitutions)
52 self.tree = ET.parse(StringIO(html_content), ET.HTMLParser())
53 # Now self.tree contains a tree with adequately-substituted
54 # paths and resources
55 inliner = HTMLResourceInliner()
56 self.inlined_tree = inliner.inline_resources(self.tree)
57
58 def test_script_inlining(self):
59 """Test that a <script> resource gets inlined."""
60 for node in self.inlined_tree.xpath('//script'):
61 self.assertTrue(node.text)
62
63 def test_img_inlining(self):
64 """
65 Test that a <img> gets inlined.
66 It should be replaced by a base64 representation of the
67 referenced image's data as per RFC2397.
68 """
69 for node in self.inlined_tree.xpath('//img'):
70 # Skip image that purposefully points to a remote
71 # resource
72 if node.attrib.get('class') != "remote_resource":
73 self.assertTrue("base64" in node.attrib['src'])
74
75 def test_css_inlining(self):
76 """Test that a <style> resource gets inlined."""
77 for node in self.inlined_tree.xpath('//style'):
78 # Skip a fake remote_resource node that's purposefully
79 # not inlined
80 if not 'nonexistent_resource' in node.attrib['type']:
81 self.assertTrue("body" in node.text)
82
83 def test_remote_resource_inlining(self):
84 """
85 Test that a resource with a non-local (i.e. not file://
86 url) does NOT get inlined (rather it's replaced by an
87 empty string). We use <style> in this test.
88 """
89 for node in self.inlined_tree.xpath('//style'):
90 # The not-inlined remote_resource
91 if 'nonexistent_resource' in node.attrib['type']:
92 self.assertTrue(node.text == "")
93
94 def test_unfindable_file_inlining(self):
95 """
96 Test that a resource whose file does not exist does NOT
97 get inlined, and is instead replaced by empty string.
98 We use <img> in this test.
99 """
100 for node in self.inlined_tree.xpath('//img'):
101 if node.attrib.get('class') == "remote_resource":
102 self.assertEqual("", node.attrib['src'])
103
104
105class HTMLExporterTests(TestCase):
106
107 def setUp(self):
108 data = resource_json(
109 "plainbox", "test-data/xml-exporter/example-data.json",
110 exact=True)
111 exporter = HTMLSessionStateExporter(
112 system_id="",
113 timestamp="2012-12-21T12:00:00",
114 client_version="1.0")
115 stream = io.BytesIO()
116 exporter.dump(data, stream)
117 self.actual_result = stream.getvalue() # This is bytes
118 self.assertIsInstance(self.actual_result, bytes)
119
120 def test_html_output(self):
121 """
122 Test that output from the exporter is HTML (or at least,
123 appears to be).
124 """
125 # A pretty simplistic test since we just validate the output
126 # appears to be HTML. Looking at the exporter's code, it's mostly
127 # boilerplate use of lxml and etree, so let's not fall into testing
128 # an external library.
129 self.assertIn(b"<html>",
130 self.actual_result)
131 self.assertIn(b"<title>System Testing Report</title>",
132 self.actual_result)
133
134 def test_perfect_match(self):
135 """
136 Test that output from the exporter exactly matches known
137 good HTML output, inlining and everything included.
138 """
139 expected_result = resource_string(
140 "plainbox", "test-data/html-exporter/example-data.html"
141 ) # unintuitively, resource_string returns bytes
142 self.assertEqual(self.actual_result, expected_result)
0143
=== modified file 'plainbox/plainbox/impl/exporter/test_init.py'
--- plainbox/plainbox/impl/exporter/test_init.py 2013-06-22 06:06:21 +0000
+++ plainbox/plainbox/impl/exporter/test_init.py 2013-09-13 17:12:45 +0000
@@ -29,13 +29,14 @@
29from tempfile import TemporaryDirectory29from tempfile import TemporaryDirectory
30from unittest import TestCase30from unittest import TestCase
3131
32from plainbox.abc import IJobResult
32from plainbox.impl.exporter import ByteStringStreamTranslator33from plainbox.impl.exporter import ByteStringStreamTranslator
33from plainbox.impl.exporter import SessionStateExporterBase34from plainbox.impl.exporter import SessionStateExporterBase
34from plainbox.impl.exporter import classproperty35from plainbox.impl.exporter import classproperty
35from plainbox.impl.job import JobDefinition36from plainbox.impl.job import JobDefinition
36from plainbox.impl.result import JobResult, IOLogRecord37from plainbox.impl.result import MemoryJobResult, IOLogRecord
37from plainbox.impl.session import SessionState38from plainbox.impl.session import SessionState
38from plainbox.impl.testing_utils import make_io_log, make_job, make_job_result39from plainbox.impl.testing_utils import make_job, make_job_result
3940
4041
41class ClassPropertyTests(TestCase):42class ClassPropertyTests(TestCase):
@@ -76,8 +77,8 @@
76 job_b = make_job('job_b')77 job_b = make_job('job_b')
77 session = SessionState([job_a, job_b])78 session = SessionState([job_a, job_b])
78 session.update_desired_job_list([job_a, job_b])79 session.update_desired_job_list([job_a, job_b])
79 result_a = make_job_result(job_a, 'pass')80 result_a = make_job_result(outcome=IJobResult.OUTCOME_PASS)
80 result_b = make_job_result(job_b, 'fail')81 result_b = make_job_result(outcome=IJobResult.OUTCOME_FAIL)
81 session.update_job_result(job_a, result_a)82 session.update_job_result(job_a, result_a)
82 session.update_job_result(job_b, result_b)83 session.update_job_result(job_b, result_b)
83 return session84 return session
@@ -115,22 +116,16 @@
115 })116 })
116 session = SessionState([job_a, job_b])117 session = SessionState([job_a, job_b])
117 session.update_desired_job_list([job_a, job_b])118 session.update_desired_job_list([job_a, job_b])
118 result_a = JobResult({119 result_a = MemoryJobResult({
119 'job': job_a,120 'outcome': IJobResult.OUTCOME_PASS,
120 'outcome': 'pass',
121 'return_code': 0,121 'return_code': 0,
122 'io_log': make_io_log(122 'io_log': [(0, 'stdout', b'testing\n')],
123 (IOLogRecord(0, 'stdout', b'testing\n'),),
124 session_dir)
125 })123 })
126 result_b = JobResult({124 result_b = MemoryJobResult({
127 'job': job_b,125 'outcome': IJobResult.OUTCOME_PASS,
128 'outcome': 'pass',
129 'return_code': 0,126 'return_code': 0,
130 'comments': 'foo',127 'comments': 'foo',
131 'io_log': make_io_log(128 'io_log': [(0, 'stdout', b'ready: yes\n')],
132 (IOLogRecord(0, 'stdout', b'ready: yes\n'),),
133 session_dir)
134 })129 })
135 session.update_job_result(job_a, result_a)130 session.update_job_result(job_a, result_a)
136 session.update_job_result(job_b, result_b)131 session.update_job_result(job_b, result_b)
137132
=== added file 'plainbox/plainbox/impl/exporter/xlsx.py'
--- plainbox/plainbox/impl/exporter/xlsx.py 1970-01-01 00:00:00 +0000
+++ plainbox/plainbox/impl/exporter/xlsx.py 2013-09-13 17:12:45 +0000
@@ -0,0 +1,570 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013 Canonical Ltd.
4# Written by:
5# Sylvain Pineau <sylvain.pineau@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`plainbox.impl.exporter.xlsx`
22=================================
23
24XLSX exporter
25
26.. warning::
27 THIS MODULE DOES NOT HAVE A STABLE PUBLIC API
28"""
29
30from base64 import standard_b64decode
31from collections import defaultdict, OrderedDict
32import re
33
34from plainbox.impl.exporter import SessionStateExporterBase
35from plainbox.abc import IJobResult
36from xlsxwriter.workbook import Workbook
37from xlsxwriter.utility import xl_rowcol_to_cell
38
39
40class XLSXSessionStateExporter(SessionStateExporterBase):
41 """
42 Session state exporter creating XLSX documents
43
44 The hardware devices are extracted from the content of the following
45 attachment:
46 * lspci_attachment
47
48 The following resource jobs are needed to populate the system info section
49 of this report:
50 * dmi
51 * device
52 * cpuinfo
53 * meminfo
54 * package
55 """
56
57 OPTION_WITH_SYSTEM_INFO = 'with-sys-info'
58 OPTION_WITH_SUMMARY = 'with-summary'
59 OPTION_WITH_DESCRIPTION = 'with-job-description'
60 OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments'
61
62 SUPPORTED_OPTION_LIST = (
63 OPTION_WITH_SYSTEM_INFO,
64 OPTION_WITH_SUMMARY,
65 OPTION_WITH_DESCRIPTION,
66 OPTION_WITH_TEXT_ATTACHMENTS,
67 )
68
69 def __init__(self, option_list=None):
70 """
71 Initialize a new XLSXSessionStateExporter.
72 """
73 # Super-call with empty option list
74 super(XLSXSessionStateExporter, self).__init__(())
75 # All the "options" are simply a required configuration element and are
76 # not optional in any way. There is no way to opt-out.
77 if option_list is None:
78 option_list = ()
79 for option in option_list:
80 if option not in self.supported_option_list:
81 raise ValueError("Unsupported option: {}".format(option))
82 self._option_list = (
83 SessionStateExporterBase.OPTION_WITH_IO_LOG,
84 SessionStateExporterBase.OPTION_FLATTEN_IO_LOG,
85 SessionStateExporterBase.OPTION_WITH_JOB_DEFS,
86 SessionStateExporterBase.OPTION_WITH_JOB_VIA,
87 SessionStateExporterBase.OPTION_WITH_JOB_HASH,
88 SessionStateExporterBase.OPTION_WITH_RESOURCE_MAP,
89 SessionStateExporterBase.OPTION_WITH_ATTACHMENTS)
90 self._option_list += tuple(option_list)
91 self.total_pass = 0
92 self.total_fail = 0
93 self.total_skip = 0
94 self.total = 0
95
96 def _set_formats(self):
97 # Main Title format (Orange)
98 self.format01 = self.workbook.add_format({
99 'align': 'left', 'size': 24, 'font_color': '#DC4C00',
100 })
101 # Default font
102 self.format02 = self.workbook.add_format({
103 'align': 'left', 'valign': 'vcenter', 'size': 10,
104 })
105 # Titles
106 self.format03 = self.workbook.add_format({
107 'align': 'left', 'size': 12, 'bold': 1,
108 })
109 # Titles + borders
110 self.format04 = self.workbook.add_format({
111 'align': 'left', 'size': 12, 'bold': 1, 'border': 7
112 })
113 # System info with borders
114 self.format05 = self.workbook.add_format({
115 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
116 'border': 7,
117 })
118 # System info with borders, grayed out background
119 self.format06 = self.workbook.add_format({
120 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
121 'border': 7, 'bg_color': '#E6E6E6',
122 })
123 # Headlines (center)
124 self.format07 = self.workbook.add_format({
125 'align': 'center', 'size': 10, 'bold': 1,
126 })
127 # Table rows without borders
128 self.format08 = self.workbook.add_format({
129 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
130 })
131 # Table rows without borders, grayed out background
132 self.format09 = self.workbook.add_format({
133 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
134 'bg_color': '#E6E6E6',
135 })
136 # Green background / Size 8
137 self.format10 = self.workbook.add_format({
138 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
139 'bg_color': 'lime', 'border': 7, 'border_color': 'white',
140 })
141 # Red background / Size 8
142 self.format11 = self.workbook.add_format({
143 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
144 'bg_color': 'red', 'border': 7, 'border_color': 'white',
145 })
146 # Gray background / Size 8
147 self.format12 = self.workbook.add_format({
148 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
149 'bg_color': 'gray', 'border': 7, 'border_color': 'white',
150 })
151 # Attachments
152 self.format13 = self.workbook.add_format({
153 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
154 'font': 'Courier New',
155 })
156 # Invisible man
157 self.format14 = self.workbook.add_format({'font_color': 'white'})
158 # Headlines (left-aligned)
159 self.format15 = self.workbook.add_format({
160 'align': 'left', 'size': 10, 'bold': 1,
161 })
162 # Table rows without borders, indent level 1
163 self.format16 = self.workbook.add_format({
164 'align': 'left', 'valign': 'vcenter', 'size': 8, 'indent': 1,
165 })
166 # Table rows without borders, grayed out background, indent level 1
167 self.format17 = self.workbook.add_format({
168 'align': 'left', 'valign': 'vcenter', 'size': 8,
169 'bg_color': '#E6E6E6', 'indent': 1,
170 })
171
172 def _hw_collection(self, data):
173 hw_info = defaultdict(lambda: 'NA')
174 if 'dmi' in data['resource_map']:
175 result = ['{} {} ({})'.format(i['vendor'], i['product'],
176 i['version']) for i in data["resource_map"]['dmi']
177 if i['category'] == 'SYSTEM']
178 if result:
179 hw_info['platform'] = result.pop()
180 result = ['{}'.format(i['version'])
181 for i in data["resource_map"]['dmi']
182 if i['category'] == 'BIOS']
183 if result:
184 hw_info['bios'] = result.pop()
185 if 'cpuinfo' in data['resource_map']:
186 result = ['{} x {}'.format(i['model'], i['count'])
187 for i in data["resource_map"]['cpuinfo']]
188 if result:
189 hw_info['processors'] = result.pop()
190 if 'lspci_attachment' in data['attachment_map']:
191 lspci = data['attachment_map']['lspci_attachment']
192 content = standard_b64decode(lspci.encode()).decode("UTF-8")
193 match = re.search('ISA bridge.*?:\s(?P<chipset>.*?)\sLPC', content)
194 if match:
195 hw_info['chipset'] = match.group('chipset')
196 match = re.search(
197 'Audio device.*?:\s(?P<audio>.*?)\s\[\w+:\w+]', content)
198 if match:
199 hw_info['audio'] = match.group('audio')
200 match = re.search(
201 'Ethernet controller.*?:\s(?P<nic>.*?)\s\[\w+:\w+]', content)
202 if match:
203 hw_info['nic'] = match.group('nic')
204 match = re.search(
205 'Network controller.*?:\s(?P<wireless>.*?)\s\[\w+:\w+]',
206 content)
207 if match:
208 hw_info['wireless'] = match.group('wireless')
209 for i, match in enumerate(re.finditer(
210 'VGA compatible controller.*?:\s(?P<video>.*?)\s\[\w+:\w+]',
211 content), start=1
212 ):
213 hw_info['video{}'.format(i)] = match.group('video')
214 vram = 0
215 for match in re.finditer(
216 'Memory.+ prefetchable\) \[size=(?P<vram>\d+)M\]',
217 content):
218 vram += int(match.group('vram'))
219 if vram:
220 hw_info['vram'] = '{} MiB'.format(vram)
221 if 'meminfo' in data['resource_map']:
222 result = ['{} GiB'.format(format(int(i['total']) / 1073741824,
223 '.1f')) for i in data["resource_map"]['meminfo']]
224 if result:
225 hw_info['memory'] = result.pop()
226 if 'device' in data['resource_map']:
227 result = ['{}'.format(i['product'])
228 for i in data["resource_map"]['device']
229 if ('category' in i and
230 i['category'] == 'BLUETOOTH' and 'driver' in i)]
231 if result:
232 hw_info['bluetooth'] = result.pop()
233 return hw_info
234
235 def write_systeminfo(self, data):
236 self.worksheet1.set_column(0, 0, 4)
237 self.worksheet1.set_column(1, 1, 34)
238 self.worksheet1.set_column(2, 3, 58)
239 hw_info = self._hw_collection(data)
240 self.worksheet1.write(5, 1, 'Platform Name', self.format03)
241 self.worksheet1.write(5, 2, hw_info['platform'], self.format03)
242 self.worksheet1.write(7, 1, 'BIOS', self.format04)
243 self.worksheet1.write(7, 2, hw_info['bios'], self.format06)
244 self.worksheet1.write(8, 1, 'Processors', self.format04)
245 self.worksheet1.write(8, 2, hw_info['processors'], self.format05)
246 self.worksheet1.write(9, 1, 'Chipset', self.format04)
247 self.worksheet1.write(9, 2, hw_info['chipset'], self.format06)
248 self.worksheet1.write(10, 1, 'Memory', self.format04)
249 self.worksheet1.write(10, 2, hw_info['memory'], self.format05)
250 self.worksheet1.write(11, 1, 'Video (on board)', self.format04)
251 self.worksheet1.write(11, 2, hw_info['video1'], self.format06)
252 self.worksheet1.write(12, 1, 'Video (add-on)', self.format04)
253 self.worksheet1.write(12, 2, hw_info['video2'], self.format05)
254 self.worksheet1.write(13, 1, 'Video memory', self.format04)
255 self.worksheet1.write(13, 2, hw_info['vram'], self.format06)
256 self.worksheet1.write(14, 1, 'Audio', self.format04)
257 self.worksheet1.write(14, 2, hw_info['audio'], self.format05)
258 self.worksheet1.write(15, 1, 'NIC', self.format04)
259 self.worksheet1.write(15, 2, hw_info['nic'], self.format06)
260 self.worksheet1.write(16, 1, 'Wireless', self.format04)
261 self.worksheet1.write(16, 2, hw_info['wireless'], self.format05)
262 self.worksheet1.write(17, 1, 'Bluetooth', self.format04)
263 self.worksheet1.write(17, 2, hw_info['bluetooth'], self.format06)
264 if "package" in data["resource_map"]:
265 self.worksheet1.write(19, 1, 'Packages Installed', self.format03)
266 self.worksheet1.write_row(
267 21, 1, ['Name', 'Version'], self.format07
268 )
269 for i in range(20, 22):
270 self.worksheet1.set_row(
271 i, None, None, {'level': 1, 'hidden': True}
272 )
273 for i, pkg in enumerate(data["resource_map"]["package"]):
274 self.worksheet1.write_row(
275 22 + i, 1,
276 [pkg['name'], pkg['version']],
277 self.format08 if i % 2 else self.format09
278 )
279 self.worksheet1.set_row(
280 22 + i, None, None, {'level': 1, 'hidden': True}
281 )
282 self.worksheet1.set_row(
283 22+len(data["resource_map"]["package"]),
284 None, None, {'collapsed': True}
285 )
286
287 def write_summary(self, data):
288 self.worksheet2.set_column(0, 0, 5)
289 self.worksheet2.set_column(1, 1, 2)
290 self.worksheet2.set_column(3, 3, 27)
291 self.worksheet2.write(3, 1, 'Failures summary', self.format03)
292 self.worksheet2.write(4, 1, '✔', self.format10)
293 self.worksheet2.write(
294 4, 2,
295 '{} Tests passed - Success Rate: {:.2f}% ({}/{})'.format(
296 self.total_pass, self.total_pass / self.total * 100,
297 self.total_pass, self.total), self.format02)
298 self.worksheet2.write(5, 1, '✘', self.format11)
299 self.worksheet2.write(
300 5, 2,
301 '{} Tests failed - Failure Rate: {:.2f}% ({}/{})'.format(
302 self.total_fail, self.total_fail / self.total * 100,
303 self.total_fail, self.total), self.format02)
304 self.worksheet2.write(6, 1, '-', self.format12)
305 self.worksheet2.write(
306 6, 2,
307 '{} Tests skipped - Skip Rate: {:.2f}% ({}/{})'.format(
308 self.total_skip, self.total_skip / self.total * 100,
309 self.total_skip, self.total), self.format02)
310 self.worksheet2.write_column(
311 'L3', ['Fail', 'Skip', 'Pass'], self.format14)
312 self.worksheet2.write_column(
313 'M3', [self.total_fail, self.total_skip, self.total_pass],
314 self.format14)
315 # Configure the series.
316 chart = self.workbook.add_chart({'type': 'pie'})
317 chart.set_legend({'position': 'none'})
318 chart.add_series({
319 'points': [
320 {'fill': {'color': 'red'}},
321 {'fill': {'color': 'gray'}},
322 {'fill': {'color': 'lime'}},
323 ],
324 'categories': '=Summary!$L$3:$L$5',
325 'values': '=Summary!$M$3:$M$5'}
326 )
327 # Insert the chart into the worksheet.
328 self.worksheet2.insert_chart('F4', chart, {
329 'x_offset': 0, 'y_offset': 10, 'x_scale': 0.25, 'y_scale': 0.25
330 })
331
332 def _set_category_status(self, result_map, via, child):
333 for parent in [j for j in result_map if result_map[j]['hash'] == via]:
334 if 'category_status' not in result_map[parent]:
335 result_map[parent]['category_status'] = None
336 child_status = result_map[child]['outcome']
337 if 'category_status' in result_map[child]:
338 child_status = result_map[child]['category_status']
339 if child_status == IJobResult.OUTCOME_FAIL:
340 result_map[parent]['category_status'] = IJobResult.OUTCOME_FAIL
341 elif (
342 child_status == IJobResult.OUTCOME_PASS and
343 result_map[parent]['category_status'] != IJobResult.OUTCOME_FAIL
344 ):
345 result_map[parent]['category_status'] = IJobResult.OUTCOME_PASS
346 elif (
347 result_map[parent]['category_status'] not in
348 (IJobResult.OUTCOME_PASS, IJobResult.OUTCOME_FAIL)
349 ):
350 result_map[parent]['category_status'] = IJobResult.OUTCOME_SKIP
351
352 def _tree(self, result_map, via=None, level=0, max_level=0):
353 res = {}
354 for job_name in [j for j in result_map if result_map[j]['via'] == via]:
355 if re.search(
356 'resource|attachment',
357 result_map[job_name]['plugin']):
358 continue
359 level += 1
360 # Find the maximum depth of the test tree
361 if level > max_level:
362 max_level = level
363 res[job_name], max_level = self._tree(
364 result_map, result_map[job_name]['hash'], level, max_level)
365 # Generate parent categories status
366 if via is not None:
367 self._set_category_status(result_map, via, job_name)
368 level -= 1
369 return res, max_level
370
371 def _write_job(self, tree, result_map, max_level, level=0):
372 for job, children in OrderedDict(
373 sorted(
374 tree.items(),
375 key=lambda t: 'z' + t[0] if t[1] else 'a' + t[0])).items():
376 self._lineno += 1
377 if children:
378 self.worksheet3.write(
379 self._lineno, level + 1,
380 result_map[job]['description'], self.format15)
381 if (
382 result_map[job]['category_status'] ==
383 IJobResult.OUTCOME_PASS
384 ):
385 self.worksheet3.write(
386 self._lineno, max_level + 2, 'PASS', self.format10)
387 elif (
388 result_map[job]['category_status'] ==
389 IJobResult.OUTCOME_FAIL
390 ):
391 self.worksheet3.write(
392 self._lineno, max_level + 2, 'FAIL', self.format11)
393 elif (
394 result_map[job]['category_status'] ==
395 IJobResult.OUTCOME_SKIP
396 ):
397 self.worksheet3.write(
398 self._lineno, max_level + 2, 'skip', self.format12)
399 if self.OPTION_WITH_DESCRIPTION in self._option_list:
400 self.worksheet4.write(
401 self._lineno, level + 1,
402 result_map[job]['description'], self.format15)
403 if level:
404 self.worksheet3.set_row(
405 self._lineno, 13, None, {'level': level})
406 if self.OPTION_WITH_DESCRIPTION in self._option_list:
407 self.worksheet4.set_row(
408 self._lineno, 13, None, {'level': level})
409 else:
410 self.worksheet3.set_row(self._lineno, 13)
411 if self.OPTION_WITH_DESCRIPTION in self._option_list:
412 self.worksheet4.set_row(self._lineno, 13)
413 self._write_job(children, result_map, max_level, level + 1)
414 else:
415 self.worksheet3.write(
416 self._lineno, max_level + 1, job,
417 self.format08 if self._lineno % 2 else self.format09)
418 if self.OPTION_WITH_DESCRIPTION in self._option_list:
419 link_cell = xl_rowcol_to_cell(self._lineno, max_level + 1)
420 self.worksheet3.write_url(
421 self._lineno, max_level + 1,
422 'internal:Test Descriptions!' + link_cell,
423 self.format08 if self._lineno % 2 else self.format09,
424 job)
425 self.worksheet4.write(
426 self._lineno, max_level + 1, job,
427 self.format08 if self._lineno % 2 else self.format09)
428 self.total += 1
429 if result_map[job]['outcome'] == IJobResult.OUTCOME_PASS:
430 self.worksheet3.write(
431 self._lineno, max_level, '✔', self.format10)
432 self.worksheet3.write(
433 self._lineno, max_level + 2, 'PASS', self.format10)
434 self.total_pass += 1
435 elif result_map[job]['outcome'] == IJobResult.OUTCOME_FAIL:
436 self.worksheet3.write(
437 self._lineno, max_level, '✘', self.format11)
438 self.worksheet3.write(
439 self._lineno, max_level + 2, 'FAIL', self.format11)
440 self.total_fail += 1
441 elif result_map[job]['outcome'] == IJobResult.OUTCOME_SKIP:
442 self.worksheet3.write(
443 self._lineno, max_level, '-', self.format12)
444 self.worksheet3.write(
445 self._lineno, max_level + 2, 'skip', self.format12)
446 self.total_skip += 1
447 elif result_map[job]['outcome'] == \
448 IJobResult.OUTCOME_NOT_SUPPORTED:
449 self.worksheet3.write(
450 self._lineno, max_level, '-', self.format12)
451 self.worksheet3.write(
452 self._lineno, max_level + 2,
453 'not supported', self.format12)
454 self.total_skip += 1
455 else:
456 self.worksheet3.write(
457 self._lineno, max_level, '-', self.format12)
458 self.worksheet3.write(
459 self._lineno, max_level + 2, None, self.format12)
460 self.total_skip += 1
461 io_log = ' '
462 if result_map[job]['io_log']:
463 io_log = standard_b64decode(
464 result_map[job]['io_log'].encode()).decode(
465 'UTF-8').rstrip()
466 io_lines = len(io_log.splitlines()) - 1
467 desc_lines = len(result_map[job]['description'].splitlines())
468 desc_lines -= 1
469 self.worksheet3.write(
470 self._lineno, max_level + 3, io_log,
471 self.format16 if self._lineno % 2 else self.format17)
472 if self.OPTION_WITH_DESCRIPTION in self._option_list:
473 self.worksheet4.write(
474 self._lineno, max_level + 2,
475 result_map[job]['description'],
476 self.format16 if self._lineno % 2 else self.format17)
477 if level:
478 self.worksheet3.set_row(
479 self._lineno, 12 + 9.71 * io_lines,
480 None, {'level': level})
481 if self.OPTION_WITH_DESCRIPTION in self._option_list:
482 self.worksheet4.set_row(
483 self._lineno, 12 + 9.71 * desc_lines,
484 None, {'level': level})
485 else:
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches