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