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