Merge lp:~zyga/checkbox/readonly-results into lp:checkbox
- readonly-results
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~zyga/checkbox/readonly-results |
Merge into: | lp:checkbox |
Diff against target: |
4085 lines (+1820/-796) 28 files modified
checkbox-ng/checkbox_ng/commands/newcli.py (+10/-4) checkbox-ng/checkbox_ng/service.py (+13/-7) plainbox/plainbox/data/report/checkbox.css (+0/-261) plainbox/plainbox/data/report/checkbox.html (+260/-139) plainbox/plainbox/data/report/checkbox.js (+0/-16) plainbox/plainbox/data/report/hexr.xml (+149/-0) plainbox/plainbox/impl/commands/inv_check_config.py (+6/-3) plainbox/plainbox/impl/commands/inv_run.py (+49/-42) plainbox/plainbox/impl/exporter/hexr.py (+138/-0) plainbox/plainbox/impl/exporter/html.py (+0/-2) plainbox/plainbox/impl/exporter/jinja2.py (+9/-0) plainbox/plainbox/impl/exporter/test_hexr.py (+535/-0) plainbox/plainbox/impl/exporter/text.py (+17/-3) plainbox/plainbox/impl/pod.py (+131/-8) plainbox/plainbox/impl/providers/special.py (+2/-2) plainbox/plainbox/impl/providers/stubbox/units/jobs/representative.pxu (+98/-0) plainbox/plainbox/impl/resource.py (+4/-0) plainbox/plainbox/impl/result.py (+104/-61) plainbox/plainbox/impl/runner.py (+90/-85) plainbox/plainbox/impl/session/jobs.py (+59/-13) plainbox/plainbox/impl/session/resume.py (+4/-7) plainbox/plainbox/impl/session/suspend.py (+4/-5) plainbox/plainbox/impl/session/test_jobs.py (+11/-0) plainbox/plainbox/impl/test_pod.py (+76/-112) plainbox/plainbox/impl/test_result.py (+44/-0) plainbox/setup.py (+4/-0) providers/plainbox-provider-checkbox/whitelists/smoke.whitelist (+0/-26) support/install-deb-dependencies (+3/-0) |
To merge this branch: | bzr merge lp:~zyga/checkbox/readonly-results |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Checkbox Developers | Pending | ||
Review via email: mp+259093@code.launchpad.net |
Commit message
Description of the change
This is a work in progress.
I want it out so that I can see the the diff and history and iterate on it,.
- 3777. By Zygmunt Krynicki
-
support: run dist-upgrade after installing deps
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3778. By Zygmunt Krynicki
-
plainbox:
commands: check_config: produce more useful output Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3779. By Zygmunt Krynicki
-
plainbox:pod: fix several PEP257 issues
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3780. By Zygmunt Krynicki
-
plainbox:pod: add unset variants for type assignment filters
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3781. By Zygmunt Krynicki
-
plainbox:pod: filter-out UNSET from as_dict()
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3782. By Zygmunt Krynicki
-
plainbox:pod: fix PEP257 issues
Some docstrings were rewritten entirely.
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3783. By Zygmunt Krynicki
-
plainbox:pod: add Field.change_
notifier This patch adds a new decorator that can be used similarly to
@foo.setter (when foo is a @property-decorated method).This can simplify all fields that need to have a custom notification
method. All such methods can be implemented _after_ all of the field
definitions now.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3784. By Zygmunt Krynicki
-
plainbox:result: fix many PEP257 and some PEP8 issues
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3785. By Zygmunt Krynicki
-
plainbox:result: warn about modification of existing results
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3786. By Zygmunt Krynicki
-
plainbox:result: add JobResultBuilder
This patch adds a new class that assists in building appropriate
JobResult objects.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3787. By Zygmunt Krynicki
-
plainbox:result: add _JobReturnBase.
unseal( ) Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3788. By Zygmunt Krynicki
-
plainbox:runner: fix many PEP257 issues
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3789. By Zygmunt Krynicki
-
plainbox:runner: use JobResultBuilder everywhere
Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3790. By Zygmunt Krynicki
-
plainbox:
commands: run: use JobResultBuilder to handle mutable results Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3791. By Zygmunt Krynicki
-
checkbox-
ng:service: use JobResultBuilder Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3792. By Zygmunt Krynicki
-
plainbox:resource: make it possible to iterate over resources
Iterating over resources behaves just like iterating over dictionaries
(keys are returned). This is useful to access resource elements in Jinja
templates. In the past we just accessed ._data directly but this is a
hack and we should get rid of it.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3793. By Zygmunt Krynicki
-
plainbox:stubbox: add set of representative jobs
This patch adds a number of jobs that validate and don't do anything.
Each job has an identifier that looks like
"representative/plugin/ {plugin} ". The goal of this collection is to simplify unit tests that traditionally
re-created everything using python APIs (which is both fragile and more
verbose).Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3794. By Zygmunt Krynicki
-
plainbox:
providers: special: allow passing keyword args to get_stubbox() The provider constructor seems to call the old validate API. Since this
is pretty annoying (all the incorrect things the old validation says) I
wanted to have a way to disable that by passing validate=False,
check=True. This is a quick stop-gap measure. In reality we want to
purge all the old validation code.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3795. By Zygmunt Krynicki
-
plainbox:
session: jobs: fix PEP257 issues Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3796. By Zygmunt Krynicki
-
plainbox:
session: jobs: keep track of result history Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3797. By Zygmunt Krynicki
-
plainbox:
session: suspend: fix PEP257 issue Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3798. By Zygmunt Krynicki
-
plainbox:
session: suspend: store full result history Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3799. By Zygmunt Krynicki
-
plainbox:
session: result: replay the full result history Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3800. By Zygmunt Krynicki
-
plainbox:
commands: run: print session identifier on startup Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3801. By Zygmunt Krynicki
-
checkbox-
ng:commands: newcli: allow re-run to be done indefinitely Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3802. By Zygmunt Krynicki
-
checkbox-
ng:commands: newcli: don't reset last outcome when re-running jobs Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3803. By Zygmunt Krynicki
-
remove shit from smoke whitelist
- 3804. By Zygmunt Krynicki
-
plainbox:
exporter: jinja: add support for customizing Environment The environment is how you control all of jinja's behavior. Currently
the environment is hidden in the initializer of the base jinja exporter
class. By having a method where we can customize the environment
_before_ a template is loaded we can add anything (additional tests and
filters). After a template is initialized, those changes don't affect
the template.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3805. By Zygmunt Krynicki
-
plainbox:
exporter: hexr: add HEXR exporter This patch adds a new exporter (HEXR) that is intended to replace our
current "xml" exporter. The schema is specific to the Canonical HEXR
application so the name reflects that.Note that due to the syntax used, this exporter requires Jinja2 >= 2.7.
The dependency wasn't there before so I've added it to install_requires.Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3806. By Zygmunt Krynicki
-
plainbox:
exporter: text: fix PEP257 issue Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3807. By Zygmunt Krynicki
-
plainbox:
exporter: text: display result history Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3808. By Zygmunt Krynicki
-
plainbox:
exporter: html: fix broken substitution on tr_outcome Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3809. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove useless OOM object Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3810. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove useless newlines Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3811. By Zygmunt Krynicki
-
plainbox:
exporter: html: don't spell the long namespace over and over Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3812. By Zygmunt Krynicki
-
plainbox:
exporter: html: use a variable for resource_map Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3813. By Zygmunt Krynicki
-
plainbox:
exporter: html: use a variable for job_state_map Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3814. By Zygmunt Krynicki
-
plainbox:
exporter: html: inline checkbox.css Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3815. By Zygmunt Krynicki
-
plainbox:
exporter: html: inline checkbox.js Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3816. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove unused css rules Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3817. By Zygmunt Krynicki
-
plainbox:
exporter: html: fix some inconsistent css syntax Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3818. By Zygmunt Krynicki
-
plainbox:
exporter: html: don't use deprecated -vendor properties Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3819. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove commented-out rule Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3820. By Zygmunt Krynicki
-
plainbox:
report: html: simplify html structure + result history [WIP] Signed-off-by: Zygmunt Krynicki <email address hidden>
Unmerged revisions
- 3820. By Zygmunt Krynicki
-
plainbox:
report: html: simplify html structure + result history [WIP] Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3819. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove commented-out rule Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3818. By Zygmunt Krynicki
-
plainbox:
exporter: html: don't use deprecated -vendor properties Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3817. By Zygmunt Krynicki
-
plainbox:
exporter: html: fix some inconsistent css syntax Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3816. By Zygmunt Krynicki
-
plainbox:
exporter: html: remove unused css rules Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3815. By Zygmunt Krynicki
-
plainbox:
exporter: html: inline checkbox.js Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3814. By Zygmunt Krynicki
-
plainbox:
exporter: html: inline checkbox.css Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3813. By Zygmunt Krynicki
-
plainbox:
exporter: html: use a variable for job_state_map Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3812. By Zygmunt Krynicki
-
plainbox:
exporter: html: use a variable for resource_map Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3811. By Zygmunt Krynicki
-
plainbox:
exporter: html: don't spell the long namespace over and over Signed-off-by: Zygmunt Krynicki <email address hidden>
Preview Diff
1 | === modified file 'checkbox-ng/checkbox_ng/commands/newcli.py' |
2 | --- checkbox-ng/checkbox_ng/commands/newcli.py 2015-04-17 10:43:20 +0000 |
3 | +++ checkbox-ng/checkbox_ng/commands/newcli.py 2015-05-14 11:57:31 +0000 |
4 | @@ -207,7 +207,11 @@ |
5 | self.print_estimated_duration() |
6 | self.run_all_selected_jobs() |
7 | if self.is_interactive: |
8 | - self.maybe_rerun_jobs() |
9 | + while True: |
10 | + if self.maybe_rerun_jobs(): |
11 | + continue |
12 | + else: |
13 | + break |
14 | self.export_and_send_results() |
15 | if SessionMetaData.FLAG_INCOMPLETE in self.metadata.flags: |
16 | print(self.C.header("Session Complete!", "GREEN")) |
17 | @@ -517,7 +521,7 @@ |
18 | rerun_candidates.append(job) |
19 | # bail-out early if no job qualifies for rerunning |
20 | if not rerun_candidates: |
21 | - return |
22 | + return False |
23 | tree = SelectableJobTreeNode.create_tree( |
24 | self.manager.state, rerun_candidates) |
25 | # deselect all by default |
26 | @@ -526,10 +530,12 @@ |
27 | wanted_set = frozenset(tree.selection) |
28 | if not wanted_set: |
29 | # nothing selected - nothing to run |
30 | - return |
31 | + return False |
32 | rerun_job_list = [job for job in self.manager.state.run_list |
33 | if job in wanted_set] |
34 | # reset outcome of jobs that are selected for re-running |
35 | for job in wanted_set: |
36 | - self.manager.state.job_state_map[job.id].result.outcome = None |
37 | + from plainbox.impl.result import MemoryJobResult |
38 | + self.manager.state.job_state_map[job.id].result = MemoryJobResult({}) |
39 | self.run_all_selected_jobs() |
40 | + return True |
41 | |
42 | === modified file 'checkbox-ng/checkbox_ng/service.py' |
43 | --- checkbox-ng/checkbox_ng/service.py 2015-05-06 10:18:36 +0000 |
44 | +++ checkbox-ng/checkbox_ng/service.py 2015-05-14 11:57:31 +0000 |
45 | @@ -37,7 +37,7 @@ |
46 | raise SystemExit(_("DBus parts require 'funcsigs' from pypi.")) |
47 | from plainbox.abc import IJobResult |
48 | from plainbox.impl.job import JobDefinition |
49 | -from plainbox.impl.result import MemoryJobResult |
50 | +from plainbox.impl.result import JobResultBuilder |
51 | from plainbox.impl.secure.qualifiers import select_jobs |
52 | from plainbox.impl.session import JobState |
53 | from plainbox.vendor import extcmd |
54 | @@ -1372,6 +1372,7 @@ |
55 | # A result object we got from running the command OR the result this |
56 | # job used to have before. It should be always published on the bus. |
57 | self._result = None |
58 | + self._result_builder = None |
59 | # A future for the result each time we're waiting for the command to |
60 | # finish. Gets reset to None after the command is done executing. |
61 | self._result_future = None |
62 | @@ -1447,16 +1448,20 @@ |
63 | _("But the job is not manual, it is %s"), |
64 | self.native.job.plugin) |
65 | # Create a new result object |
66 | - self._result = MemoryJobResult({ |
67 | - 'outcome': outcome, |
68 | - 'comments': comments |
69 | - }) |
70 | + self._result_builder = JobResultBuilder() |
71 | + self._result_builder.outcome = outcome |
72 | + self._result_builder.comments = comments |
73 | + self._result = self._result_builder.seal() |
74 | # Add the new result object to the bus |
75 | self._session_wrapper.add_result(self._result) |
76 | else: |
77 | + assert self._result_builder is not None |
78 | # Set the values as requested |
79 | - self._result.outcome = outcome |
80 | - self._result.comments = comments |
81 | + self._result_builder.outcome = outcome |
82 | + self._result_builder.comments = comments |
83 | + self._result = self._result_builder.seal() |
84 | + # Add the new result object to the bus |
85 | + self._session_wrapper.add_result(self._result) |
86 | # Notify the application that the result is ready. This has to be |
87 | # done unconditionally each time this method called. |
88 | self.JobResultAvailable( |
89 | @@ -1548,6 +1553,7 @@ |
90 | self._session_wrapper.remove_result(self._result) |
91 | # Unpack the result from the future |
92 | self._result = result_future.result() |
93 | + self._result_builder = self._result.unseal() |
94 | # Add the new result object to the session wrapper (and to the bus) |
95 | self._session_wrapper.add_result(self._result) |
96 | # Reset the future so that RunCommand() can run the job again |
97 | |
98 | === removed file 'plainbox/plainbox/data/report/checkbox.css' |
99 | --- plainbox/plainbox/data/report/checkbox.css 2015-04-09 16:57:49 +0000 |
100 | +++ plainbox/plainbox/data/report/checkbox.css 1970-01-01 00:00:00 +0000 |
101 | @@ -1,261 +0,0 @@ |
102 | -body { |
103 | - font-family: "Ubuntu Beta", "Bitstream Vera Sans", DejaVu Sans, Tahoma, sans-serif; |
104 | - color: #333; |
105 | - background: white; |
106 | - font-size: 12px; |
107 | - line-height: 14px; |
108 | - margin: 0px; |
109 | - padding: 0px; |
110 | -} |
111 | -#container { |
112 | - background: #f7f6f5; |
113 | - margin: 0px auto 20px; |
114 | - padding: 0px; |
115 | - width: 976px; |
116 | -} |
117 | -#container-inner { |
118 | - background-color: #dfdcd9; |
119 | -} |
120 | -#header, #container-inner { |
121 | - -moz-border-radius: 0px 0px 5px 5px; |
122 | - -webkit-border-bottom-left-radius: 5px; |
123 | - -webkit-border-bottom-right-radius: 5px; |
124 | - -moz-box-shadow: #bbb 0px 0px 5px; |
125 | - -webkit-box-shadow: #bbb 0px 0px 5px; |
126 | -} |
127 | -#header { |
128 | - background: #dd4814 top left repeat-x; |
129 | - height: 64px; |
130 | - margin: 0px; |
131 | - padding: 0px; |
132 | - position: relative; |
133 | -} |
134 | - |
135 | -#menu-search { |
136 | - height: 40px; |
137 | - margin: 0 16px; |
138 | -} |
139 | - |
140 | -#title { |
141 | - padding: 28px 24px; |
142 | -} |
143 | - |
144 | -#content { |
145 | - /*padding: 32px 80px 32px 80px;*/ |
146 | - padding: 32px 240px 32px 60px; |
147 | - margin: 0 16px 16px; |
148 | - width: 644px; |
149 | - background-color: #fff; |
150 | - -moz-border-radius: 4px; |
151 | - -webkit-border-radius: 4px; |
152 | -} |
153 | -#end-content { |
154 | - clear: both; |
155 | -} |
156 | - |
157 | -#content-panel { |
158 | - width: 446px; |
159 | - margin: 0px 0px 0px 0px; |
160 | - padding: 8px 8px 32px 8px; |
161 | - background-color: #fff; |
162 | - -moz-border-radius: 4px; |
163 | - -webkit-border-radius: 4px; |
164 | -} |
165 | - |
166 | -#copyright { |
167 | - background-position: 803px 40px; |
168 | - background-repeat: no-repeat; |
169 | - text-align: center; |
170 | - margin: 0 16px; |
171 | - padding: 40px 0 0 0; |
172 | - height: 32px; |
173 | -} |
174 | -#copyright p { |
175 | - color: #aea79f; |
176 | - font-size: 10px; |
177 | - line-height: 14px; |
178 | - margin: 2px 0; |
179 | -} |
180 | - |
181 | -#footer { |
182 | - padding-top: 16px; |
183 | -} |
184 | -#footer * { |
185 | - font-size: 10px; |
186 | - line-height: 14px; |
187 | -} |
188 | -#footer p { |
189 | - margin: 0; |
190 | - padding-bottom: 3px; |
191 | - border-bottom: 1px dotted #aea79f; |
192 | -} |
193 | -#footer p.footer-title { |
194 | - font-weight: bold; |
195 | -} |
196 | -#footer .footer-div { |
197 | - width: 144px; |
198 | - float: left; |
199 | - margin-left: 16px; |
200 | -} |
201 | -#footer .last-div { |
202 | - margin-right: 16px; |
203 | -} |
204 | -#footer ul { |
205 | - list-style: none; |
206 | - margin: 0; |
207 | - padding: 0; |
208 | -} |
209 | -#footer li { |
210 | - margin: 0; |
211 | - padding: 3px 0; |
212 | - border-bottom: 1px dotted #aea79f; |
213 | -} |
214 | - |
215 | -h1, h2, h3, h4, h5 { |
216 | - padding: 0; |
217 | - margin: 0; |
218 | - font-weight: normal; |
219 | -} |
220 | -h1 { |
221 | - font-size: 36px; |
222 | - line-height: 40px; |
223 | - color: #dd4814; |
224 | -} |
225 | -h2 { |
226 | - font-size: 24px; |
227 | - line-height: 28px; |
228 | - margin-bottom: 8px; |
229 | -} |
230 | -h3 { |
231 | - font-size: 16px; |
232 | - line-height: 20px; |
233 | - margin-bottom: 8px; |
234 | -} |
235 | -h3.link-other { |
236 | - color: #333; |
237 | -} |
238 | -h3.link-services { |
239 | - color: #fff; |
240 | -} |
241 | -h4 { |
242 | - font-size: 12px; |
243 | - line-height: 14px; |
244 | -} |
245 | -h4.partners { |
246 | - color: #333; |
247 | - font-size: 16px; |
248 | - line-height: 20px; |
249 | -} |
250 | -h5 { |
251 | - color: #333; |
252 | - font-size: 10px; |
253 | - line-height: 14px; |
254 | -} |
255 | -h1 span.grey, h2 span.grey, h1 span, h2 span{ |
256 | - color: #aea79f; |
257 | -} |
258 | -p { |
259 | - font-size: 12px; |
260 | - line-height: 14px; |
261 | - margin-bottom: 8px; |
262 | -} |
263 | -strong { |
264 | - font-weight: bold; |
265 | -} |
266 | - |
267 | -a { |
268 | - color: #333; |
269 | - text-decoration: none; |
270 | -} |
271 | -a:hover { |
272 | - color: #dd4814; |
273 | - text-decoration: underline; |
274 | -} |
275 | -div.footer-div:hover a, div#content:hover a { |
276 | - color: #dd4814; |
277 | - text-decoration: none; |
278 | -} |
279 | -div.footer-div:hover a:hover, div#content:hover a:hover { |
280 | - color: #dd4814; |
281 | - text-decoration: underline; |
282 | -} |
283 | - |
284 | -ul { |
285 | - margin-bottom: 16px; |
286 | -} |
287 | -ul li { |
288 | - margin-bottom: 8px; |
289 | - line-height: 14px; |
290 | -} |
291 | -ul li:last-child { |
292 | - margin-bottom: 0px; |
293 | -} |
294 | - |
295 | -p.call-to-action { |
296 | - color: #333; |
297 | -} |
298 | -p.case-study { |
299 | - color: #333; |
300 | -} |
301 | -p.highlight { |
302 | - font-size: 16px; |
303 | - line-height: 20px; |
304 | -} |
305 | -p.introduction { |
306 | - color: #333; |
307 | - font-size: 16px; |
308 | - line-height: 20px; |
309 | -} |
310 | -p.services { |
311 | - color: #fff; |
312 | -} |
313 | -p.small-text { |
314 | - color: #333; |
315 | - font-size: 10px; |
316 | -} |
317 | - |
318 | -/* Clearing floats without extra markup |
319 | -Based on How To Clear Floats Without Structural Markup by PiE |
320 | -[http://www.positioniseverything.net/easyclearing.html] */ |
321 | -.clearfix:after { |
322 | - content: "."; |
323 | - display: block; |
324 | - height: 0; |
325 | - clear: both; |
326 | - visibility: hidden; |
327 | -} |
328 | -.clearfix { |
329 | - -moz-border-radius: 5px 5px 5px 5px; |
330 | - -webkit-border-bottom-top-radius: 5px; |
331 | - -webkit-border-bottom-left-radius: 5px; |
332 | - -webkit-border-bottom-bottom-radius: 5px; |
333 | - -webkit-border-bottom-right-radius: 5px; |
334 | - -moz-box-shadow: #bbb 0px 0px 5px; |
335 | - -webkit-box-shadow: #bbb 0px 0px 5px; |
336 | - display: inline-block; |
337 | -} /* for IE/Mac */ |
338 | -th |
339 | -{ |
340 | - text-align: left; |
341 | -} |
342 | -td |
343 | -{ |
344 | - margin: 0; |
345 | - padding-bottom: 3px; |
346 | - border-bottom: 1px dotted #aea79f; |
347 | - font-size: 10px; |
348 | - line-height: 14px; |
349 | -} |
350 | -.resultimg |
351 | -{ |
352 | - height: 12px; |
353 | -} |
354 | -.disclosureimg |
355 | -{ |
356 | - height: .75em; |
357 | - vertical-align: middle; |
358 | -} |
359 | -.data |
360 | -{ |
361 | - display: none; |
362 | -} |
363 | |
364 | === modified file 'plainbox/plainbox/data/report/checkbox.html' |
365 | --- plainbox/plainbox/data/report/checkbox.html 2015-05-04 08:08:53 +0000 |
366 | +++ plainbox/plainbox/data/report/checkbox.html 2015-05-14 11:57:31 +0000 |
367 | @@ -1,153 +1,274 @@ |
368 | +{%- set ns = '2013.com.canonical.certification::' -%} |
369 | +{%- set state = manager.default_device_context.state -%} |
370 | +{%- set resource_map = state.resource_map -%} |
371 | +{%- set job_state_map = state.job_state_map -%} |
372 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> |
373 | <html> |
374 | <head> |
375 | <title>System Testing Report</title> |
376 | <style type="text/css"> |
377 | - {% include "checkbox.css" %} |
378 | + html { |
379 | + font-family: "Ubuntu Beta", "Bitstream Vera Sans", DejaVu Sans, Tahoma, sans-serif; |
380 | + color: #333; |
381 | + background: white; |
382 | + font-size: 12px; |
383 | + line-height: 14px; |
384 | + margin: 0px; |
385 | + padding: 0px; |
386 | + } |
387 | + body { |
388 | + background: #f7f6f5; |
389 | + margin: 0px auto 20px; |
390 | + padding: 0px; |
391 | + width: 976px; |
392 | + background-color: #dfdcd9; |
393 | + border-radius: 0px 0px 5px 5px; |
394 | + box-shadow: #bbb 0px 0px 5px; |
395 | + } |
396 | + #title { |
397 | + padding: 28px 24px; |
398 | + } |
399 | + #content { |
400 | + padding: 32px 240px 32px 60px; |
401 | + margin: 0 16px 16px; |
402 | + width: 644px; |
403 | + background-color: #fff; |
404 | + border-radius: 4px; |
405 | + } |
406 | + h1, h2, h3, h4, h5 { |
407 | + padding: 0; |
408 | + margin: 0; |
409 | + font-weight: normal; |
410 | + } |
411 | + h1 { |
412 | + font-size: 36px; |
413 | + line-height: 40px; |
414 | + color: #dd4814; |
415 | + } |
416 | + h2 { |
417 | + font-size: 24px; |
418 | + line-height: 28px; |
419 | + margin-bottom: 8px; |
420 | + } |
421 | + h3 { |
422 | + font-size: 16px; |
423 | + line-height: 20px; |
424 | + margin-bottom: 8px; |
425 | + } |
426 | + h4 { |
427 | + font-size: 12px; |
428 | + line-height: 14px; |
429 | + } |
430 | + h5 { |
431 | + color: #333; |
432 | + font-size: 10px; |
433 | + line-height: 14px; |
434 | + } |
435 | + h1 span.grey, h2 span.grey, h1 span, h2 span{ |
436 | + color: #aea79f; |
437 | + } |
438 | + p { |
439 | + font-size: 12px; |
440 | + line-height: 14px; |
441 | + margin-bottom: 8px; |
442 | + } |
443 | + pre { |
444 | + color: gray; |
445 | + } |
446 | + a { |
447 | + color: #333; |
448 | + text-decoration: none; |
449 | + } |
450 | + a:hover { |
451 | + color: #dd4814; |
452 | + text-decoration: underline; |
453 | + } |
454 | + div#content:hover a { |
455 | + color: #dd4814; |
456 | + text-decoration: none; |
457 | + } |
458 | + div#content:hover a:hover { |
459 | + color: #dd4814; |
460 | + text-decoration: underline; |
461 | + } |
462 | + ul { |
463 | + margin-bottom: 16px; |
464 | + } |
465 | + ul li { |
466 | + margin-bottom: 8px; |
467 | + line-height: 14px; |
468 | + } |
469 | + ul li:last-child { |
470 | + margin-bottom: 0px; |
471 | + } |
472 | + /* Clearing floats without extra markup |
473 | + Based on How To Clear Floats Without Structural Markup by PiE |
474 | + [http://www.positioniseverything.net/easyclearing.html] */ |
475 | + .clearfix:after { |
476 | + content: "."; |
477 | + display: block; |
478 | + height: 0; |
479 | + clear: both; |
480 | + visibility: hidden; |
481 | + } |
482 | + .clearfix { |
483 | + border-radius: 5px 5px 5px 5px; |
484 | + box-shadow: #bbb 0px 0px 5px; |
485 | + display: inline-block; |
486 | + } /* for IE/Mac */ |
487 | + th { |
488 | + text-align: left; |
489 | + } |
490 | + td { |
491 | + margin: 0; |
492 | + padding-bottom: 3px; |
493 | + border-bottom: 1px dotted #aea79f; |
494 | + font-size: 10px; |
495 | + line-height: 14px; |
496 | + vertical-align: top; |
497 | + } |
498 | + .data { |
499 | + display: none; |
500 | + } |
501 | </style> |
502 | <script type="text/javascript"> |
503 | - {% include "checkbox.js" %} |
504 | + function showHide(what) { |
505 | + var heading = document.getElementById(what); |
506 | + var contents = document.getElementById(what + "-contents"); |
507 | + var headingcontents = heading.innerHTML; |
508 | + var newcontents; |
509 | + |
510 | + if (contents.style.display != "block") { |
511 | + newcontents = headingcontents.replace(/[^\x00-\x80]/g, "▼"); |
512 | + contents.style.display = "block"; |
513 | + } else { |
514 | + newcontents = headingcontents.replace(/[^\x00-\x80]/g, "▶"); |
515 | + contents.style.display = "none"; |
516 | + } |
517 | + |
518 | + heading.innerHTML = newcontents; |
519 | + } |
520 | </script> |
521 | </head> |
522 | <body> |
523 | - <div id="container"> |
524 | - <div id="container-inner"> |
525 | - |
526 | - <div id="title"> |
527 | - <h1>System Testing<span class="grey"> Report</span></h1> |
528 | - </div> |
529 | - <div id="content" class="clearfix"> |
530 | - This report was created using {{ client_name }} {{ client_version }} on {{ timestamp }} |
531 | - </div> |
532 | - {% if "2013.com.canonical.certification::lsb" in manager.state.resource_map %} |
533 | - <div id="content" class="clearfix"> |
534 | - <h2>Software Information</h2> |
535 | - <p>{{ manager.state.resource_map["2013.com.canonical.certification::lsb"][0]["description"] }}</p> |
536 | - </div> |
537 | - {% endif %} |
538 | - |
539 | - {% if manager.state.get_certification_status_map() %} |
540 | - <div id="content" class="clearfix"> |
541 | - <h2 id="questions">Certification Status - Blockers</h2> |
542 | - <table style="width: 700px"> |
543 | - <thead> |
544 | - <tr> |
545 | - <th>Test ID</th> |
546 | - <th>Result</th> |
547 | - </tr> |
548 | - </thead> |
549 | - <tbody> |
550 | - {% for job_id, job_state in manager.state.get_certification_status_map()|dictsort %} |
551 | - <tr> |
552 | - <td>{{ job_state.job.tr_summary() }}</td> |
553 | - <td style='font-weight: bold; color: {{ OMM[job_state.result.outcome].color_hex }}'>{{ OMM[job_state.result.outcome].tr_outcome|replace("job ", "") }}</td> |
554 | - </tr> |
555 | - {% endfor %} |
556 | - </tbody> |
557 | - </table> |
558 | - </div> |
559 | - {% endif %} |
560 | - |
561 | - {% if manager.state.get_certification_status_map(certification_status_filter=('non-blocker',)) %} |
562 | - <div id="content" class="clearfix"> |
563 | - <h2 id="questions">Certification Status - Non Blockers</h2> |
564 | - <table style="width: 700px"> |
565 | - <thead> |
566 | - <tr> |
567 | - <th>Test ID</th> |
568 | - <th>Result</th> |
569 | - </tr> |
570 | - </thead> |
571 | - <tbody> |
572 | - {% for job_id, job_state in manager.state.get_certification_status_map(certification_status_filter=('non-blocker',))|dictsort %} |
573 | - <tr> |
574 | - <td>{{ job_state.job.tr_summary() }}</td> |
575 | - <td style='font-weight: bold; color: {{ OMM[job_state.result.outcome].color_hex }}'>{{ OMM[job_state.result.outcome].tr_outcome|replace("job ", "") }}</td> |
576 | - </tr> |
577 | - {% endfor %} |
578 | - </tbody> |
579 | - </table> |
580 | - </div> |
581 | - {% endif %} |
582 | - |
583 | - <div id="content" class="clearfix"> |
584 | - <h2 id="questions">Tests Performed</h2> |
585 | - <table style="width: 824px"> |
586 | - <thead> |
587 | - <tr> |
588 | - <th>Test ID</th> |
589 | - <th>Result</th> |
590 | - <th>Certification status</th> |
591 | - <th>Comment</th> |
592 | - </tr> |
593 | - </thead> |
594 | - <tbody> |
595 | - {% for job_id, job_state in manager.state.job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin not in ("resource", "local", "attachment") %} |
596 | - <tr> |
597 | - <td>{{ job_state.job.tr_summary() }}</td> |
598 | - <td style='font-weight: bold; color: {{ OMM[job_state.result.outcome].color_hex }}'>{{ OMM[job_state.result.outcome].tr_outcome|replace("job ", "") }}</td> |
599 | - <td>{{ job_state.effective_certification_status }}</td> |
600 | - {% if job_state.result.comments != None %} |
601 | - <td>{{ job_state.result.comments }}</td> |
602 | - {% else %} |
603 | - {% if job_state.result.io_log_as_flat_text != "" %} |
604 | - <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
605 | - <pre>{{ job_state.result.io_log_as_flat_text }}</pre> |
606 | - </div> |
607 | - </td> |
608 | - {% else %} |
609 | - <td> </td> |
610 | - {% endif %} |
611 | - {% endif %} |
612 | - </tr> |
613 | - {% endfor %} |
614 | - </tbody> |
615 | - </table> |
616 | - </div> |
617 | - {% if "2013.com.canonical.certification::requirements" in manager.state.resource_map %} |
618 | - <div id="content" class="clearfix"> |
619 | - <h2>Requirements documents</h2> |
620 | - <ul> |
621 | - {% for doc in manager.state.resource_map["2013.com.canonical.certification::requirements"] %} |
622 | - <li><a href="{{ doc["link"] }}">{{ doc["name"] }}</a></li> |
623 | - {% endfor %} |
624 | - </ul> |
625 | - </div> |
626 | - {% endif %} |
627 | - <div id="content" class="clearfix"> |
628 | - <h2>Log Files and Environment Information</h2> |
629 | - <div id="packages-contents"> |
630 | - {% if "2013.com.canonical.certification::package" in manager.state.resource_map %} |
631 | - <span onClick="showHide('package');"><h3><span id="package" style="color: #dd4814;">▶</span> |
632 | - packages installed</h3></span> |
633 | - <div class="data" id="package-contents" style="overflow: auto;"> |
634 | - <table> |
635 | - <thead> |
636 | - <tr> |
637 | - <th>Name</th> |
638 | - <th>Version</th> |
639 | - </tr> |
640 | - </thead> |
641 | - <tbody> |
642 | - {% for package in manager.state.resource_map["2013.com.canonical.certification::package"] %} |
643 | - <tr> |
644 | - <td>{{ package["name"] }}</td> |
645 | - <td>{{ package["version"] }}</td> |
646 | - </tr> |
647 | - {% endfor %} |
648 | - </tbody> |
649 | - </table> |
650 | - </div> |
651 | - {% endif %} |
652 | - {% for job_id, job_state in manager.state.job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "attachment" %} |
653 | - <span onClick="showHide('att{{ loop.index }}');"><h3><span id="att{{ loop.index }}" style="color: #dd4814;">▶</span> |
654 | - {{ job_state.job.partial_id|replace("_attachment", "") }}</h3></span> |
655 | - <div class="data" id="att{{ loop.index }}-contents" style="overflow: auto;"> |
656 | - <pre>{{ job_state.result.io_log_as_text_attachment }}</pre> |
657 | - </div> |
658 | + <div id="title"> |
659 | + <h1>System Testing<span class="grey"> Report</span></h1> |
660 | + </div> |
661 | + <div id="content" class="clearfix"> |
662 | + This report was created using {{ client_name }} {{ client_version }} on {{ timestamp }} |
663 | + </div> |
664 | + {% if ns ~ "lsb" in resource_map %} |
665 | + <div id="content" class="clearfix"> |
666 | + <h2>Software Information</h2> |
667 | + <p>{{ resource_map[ns ~ "lsb"][0]["description"] }}</p> |
668 | + </div> |
669 | + {% endif %} |
670 | + {% if manager.state.failed_blockers_map %} |
671 | + <div id="content" class="clearfix"> |
672 | + <h2 id="questions">Certification Status - Blockers</h2> |
673 | + <table style="width: 700px"> |
674 | + <thead> |
675 | + <tr> |
676 | + <th>Test ID</th> |
677 | + <th>Result</th> |
678 | + </tr> |
679 | + </thead> |
680 | + <tbody> |
681 | + {% for job_id, job_state in manager.state.get_certification_status_map()|dictsort %} |
682 | + <tr> |
683 | + <td>{{ job_state.job.tr_summary() }}</td> |
684 | + <td style='font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
685 | + </tr> |
686 | + {% endfor %} |
687 | + </tbody> |
688 | + </table> |
689 | + </div> |
690 | + {% endif %} |
691 | + {% if manager.state.failed_non_blockers_map %} |
692 | + <div id="content" class="clearfix"> |
693 | + <h2 id="questions">Certification Status - Non Blockers</h2> |
694 | + <table style="width: 700px"> |
695 | + <thead> |
696 | + <tr> |
697 | + <th>Test ID</th> |
698 | + <th>Result</th> |
699 | + </tr> |
700 | + </thead> |
701 | + <tbody> |
702 | + {% for job_id, job_state in manager.state.get_certification_status_map(certification_status_filter=('non-blocker',))|dictsort %} |
703 | + <tr> |
704 | + <td>{{ job_state.job.tr_summary() }}</td> |
705 | + <td style='font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
706 | + </tr> |
707 | + {% endfor %} |
708 | + </tbody> |
709 | + </table> |
710 | + </div> |
711 | + {% endif %} |
712 | + <div id="content" class="clearfix"> |
713 | + <h2 id="questions">Tests Performed</h2> |
714 | + <table style="width: 824px"> |
715 | + <thead> |
716 | + <tr> |
717 | + <th>Test ID</th> |
718 | + <th>Result</th> |
719 | + <th>Certification status</th> |
720 | + <th>Comments</th> |
721 | + </tr> |
722 | + </thead> |
723 | + <tbody> |
724 | + {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin not in ("resource", "local", "attachment") %} |
725 | + {%- for result in job_state.result_history %} |
726 | + <tr> |
727 | + <td>{{ job_state.job.tr_summary() }}{% if loop.index > 1 %} (run {{ loop.index }}){% endif %}</td> |
728 | + <td style='font-weight: bold; color: {{ result.outcome_meta().color_hex }}'>{{ result.outcome_meta().tr_label }}</td> |
729 | + {%- if loop.index0 == 0 %} |
730 | + <td rowspan="{{ 2 * job_state.result_history|length }}">{{ job_state.effective_certification_status }}</td> |
731 | + {%- endif %} |
732 | + <td>{{ result.comments|default("", true) }}</td> |
733 | + </tr> |
734 | + <tr> |
735 | + <td colspan="4"> |
736 | + <pre style="overflow: auto; width: 80em;">{{ result.io_log_as_flat_text }}</pre> |
737 | + </td> |
738 | + </tr> |
739 | + {%- endfor %} |
740 | + {%- endfor %} |
741 | + </tbody> |
742 | + </table> |
743 | + </div> |
744 | + <div id="content" class="clearfix"> |
745 | + <h2>Log Files and Environment Information</h2> |
746 | + <div id="packages-contents"> |
747 | + {% if ns ~ "package" in resource_map %} |
748 | + <span onClick="showHide('package');"><h3><span id="package" style="color: #dd4814;">▶</span> |
749 | + packages installed</h3></span> |
750 | + <div class="data" id="package-contents" style="overflow: auto;"> |
751 | + <table> |
752 | + <thead> |
753 | + <tr> |
754 | + <th>Name</th> |
755 | + <th>Version</th> |
756 | + </tr> |
757 | + </thead> |
758 | + <tbody> |
759 | + {% for package in resource_map[ns ~ "package"] %} |
760 | + <tr> |
761 | + <td>{{ package["name"] }}</td> |
762 | + <td>{{ package["version"] }}</td> |
763 | + </tr> |
764 | {% endfor %} |
765 | - </div> |
766 | - </div> |
767 | + </tbody> |
768 | + </table> |
769 | + </div> |
770 | + {% endif %} |
771 | + {% for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "attachment" %} |
772 | + <span onClick="showHide('att{{ loop.index }}');"><h3><span id="att{{ loop.index }}" style="color: #dd4814;">▶</span> |
773 | + {{ job_state.job.partial_id|replace("_attachment", "") }}</h3></span> |
774 | + <div class="data" id="att{{ loop.index }}-contents" style="overflow: auto;"> |
775 | + <pre>{{ job_state.result.io_log_as_text_attachment }}</pre> |
776 | + </div> |
777 | + {% endfor %} |
778 | </div> |
779 | </div> |
780 | </body> |
781 | |
782 | === removed file 'plainbox/plainbox/data/report/checkbox.js' |
783 | --- plainbox/plainbox/data/report/checkbox.js 2015-04-09 16:57:49 +0000 |
784 | +++ plainbox/plainbox/data/report/checkbox.js 1970-01-01 00:00:00 +0000 |
785 | @@ -1,16 +0,0 @@ |
786 | -function showHide(what) { |
787 | - var heading = document.getElementById(what); |
788 | - var contents = document.getElementById(what + "-contents"); |
789 | - var headingcontents = heading.innerHTML; |
790 | - var newcontents; |
791 | - |
792 | - if (contents.style.display != "block") { |
793 | - newcontents = headingcontents.replace(/[^\x00-\x80]/g, "▼"); |
794 | - contents.style.display = "block"; |
795 | - } else { |
796 | - newcontents = headingcontents.replace(/[^\x00-\x80]/g, "▶"); |
797 | - contents.style.display = "none"; |
798 | - } |
799 | - |
800 | - heading.innerHTML = newcontents; |
801 | -} |
802 | |
803 | === added file 'plainbox/plainbox/data/report/hexr.xml' |
804 | --- plainbox/plainbox/data/report/hexr.xml 1970-01-01 00:00:00 +0000 |
805 | +++ plainbox/plainbox/data/report/hexr.xml 2015-05-14 11:57:31 +0000 |
806 | @@ -0,0 +1,149 @@ |
807 | +{%- set ns = '2013.com.canonical.certification::' -%} |
808 | +{%- set state = manager.default_device_context.state -%} |
809 | +{%- set job_state_map = state.job_state_map -%} |
810 | +{%- set hexr_outcome_list = OUTCOME_METADATA_MAP.values()|selectattr('hexr_xml_allowed')|sort(attribute='hexr_xml_order') -%} |
811 | +<?xml version="1.0"?> |
812 | +<system version="1.0"> |
813 | + <context> |
814 | + {%- for job_id in job_state_map|sort %} |
815 | + {%- set job_state = job_state_map[job_id] %} |
816 | + {%- if job_state.job.id|strip_ns not in ("dmi_attachment", "sysfs_attachment", "udev_attachment") and job_state.result.outcome %} |
817 | + <info command="{{ job_state.job.id|strip_ns }}">{{ job_state.result.io_log_as_text_attachment }}</info> |
818 | + {%- endif %} |
819 | + {%- endfor %} |
820 | + </context> |
821 | + <hardware> |
822 | + {%- if ns ~ 'dmi_attachment' in state.job_state_map %} |
823 | + {%- set dmi_attachment = state.job_state_map[ns ~ 'dmi_attachment'].result.io_log_as_text_attachment %} |
824 | + <dmi>{{ dmi_attachment }}</dmi> |
825 | + {%- else %} |
826 | + <!-- the dmi_attachment job is not available, not producing the <dmi> section --> |
827 | + {%- endif %} |
828 | + {%- if ns ~ 'sysfs_attachment' in state.job_state_map %} |
829 | + {%- set sysfs_attachment = state.job_state_map[ns ~ 'sysfs_attachment'].result.io_log_as_text_attachment %} |
830 | + <sysfs-attributes>{{ sysfs_attachment }}</sysfs-attributes> |
831 | + {%- else %} |
832 | + <!-- the sysfs_attachment job is not available, not producing the <sysfs-attributes> tag --> |
833 | + {%- endif %} |
834 | + {%- if ns ~ 'udev_attachment' in state.job_state_map %} |
835 | + {%- set udev_attachment = state.job_state_map[ns ~ 'udev_attachment'].result.io_log_as_text_attachment %} |
836 | + <udev>{{ udev_attachment }}</udev> |
837 | + {%- else %} |
838 | + <!-- the udev_attachment job is not available, not producing the <udev> tag --> |
839 | + {%- endif %} |
840 | + {%- if ns ~ 'cpuinfo' in state.resource_map %} |
841 | + {%- set processor_resource = state.resource_map[ns ~ 'cpuinfo'][0] %} |
842 | + {#- FIXME: The <processors> section is quite broken by design. #} |
843 | + {#- Yes, it does copy the data for the 0th CPU $count times. #} |
844 | + <processors> |
845 | + {%- for dummy_index in range(processor_resource.count|int) %} |
846 | + <processor id="{{ loop.index0 }}" name="{{ loop.index0 }}"> |
847 | + {%- for key in processor_resource|sort %} |
848 | + <property name="{{ key }}" type="str">{{ processor_resource[key] }}</property> |
849 | + {%- endfor %} |
850 | + </processor> |
851 | + {%- endfor %} |
852 | + </processors> |
853 | + {%- else %} |
854 | + <!-- cpuinfo resource is not available, not producing the <processors> section --> |
855 | + {%- endif %} |
856 | + </hardware> |
857 | + <questions> |
858 | + {%- for job_id in job_state_map|sort %} |
859 | + {%- set job_state = job_state_map[job_id] %} |
860 | + {%- set job = job_state.job %} |
861 | + {%- set result = job_state.result %} |
862 | + {%- if job.plugin not in ("resource", "local", "attachment") and job_state.result.outcome %} |
863 | + <question{{ { |
864 | + 'name': job.id|strip_ns, |
865 | + 'certification_status': job_state.certification_status, |
866 | + }|sorted_xmlattr }}> |
867 | + <answer type="multiple_choice">{{ result.outcome_meta().hexr_xml_mapping }}</answer> |
868 | + {#- FIXME: Yes the <answer_choices> section is useless #} |
869 | + <answer_choices> |
870 | + {%- for outcome_info in hexr_outcome_list %} |
871 | + <value type="str">{{ outcome_info.hexr_xml_mapping }}</value> |
872 | + {%- endfor %} |
873 | + </answer_choices> |
874 | + {#- FIXME: yes this does munges comments and I/O log #} |
875 | + <comment> |
876 | + {%- if result.comments -%} |
877 | + {{ result.comments }} |
878 | + {%- else -%} |
879 | + {{ result.io_log_as_flat_text }} |
880 | + {%- endif -%} |
881 | + </comment> |
882 | + </question> |
883 | + {%- endif %} |
884 | + {%- endfor %} |
885 | + </questions> |
886 | + <software> |
887 | + {%- if ns ~ 'lsb' in state.resource_map %} |
888 | + {%- set lsb_resource = state.resource_map[ns ~ 'lsb'][0] %} |
889 | + <lsbrelease> |
890 | + {%- for key in lsb_resource|sort %} |
891 | + <property name="{{ key }}" type="str">{{ lsb_resource[key] }}</property> |
892 | + {%- endfor %} |
893 | + </lsbrelease> |
894 | + {%- else %} |
895 | + <!-- lsb resource is not available, not producing the <lsbrelease> tag --> |
896 | + {%- endif %} |
897 | + {%- if ns ~ 'package' in state.resource_map %} |
898 | + {%- set package_resource_list = state.resource_map[ns ~ 'package'] %} |
899 | + <packages> |
900 | + {%- for package_resource in package_resource_list %} |
901 | + <package id="{{ loop.index0 }}" name="{{ package_resource.name }}"> |
902 | + {%- for key in package_resource|reject('is_name')|sort %} |
903 | + <property name="{{ key }}" type="str">{{ package_resource[key] }}</property> |
904 | + {%- endfor %} |
905 | + </package> |
906 | + {%- endfor %} |
907 | + </packages> |
908 | + {%- else %} |
909 | + <!-- package resource is not available, not producing the <packages> tag --> |
910 | + {%- endif %} |
911 | + {%- if ns ~ 'requirements' in state.resource_map %} |
912 | + {%- set requirement_resource_list = state.resource_map[ns ~ 'requirements'] %} |
913 | + <requirements> |
914 | + {%- for requirement_resource in requirement_resource_list %} |
915 | + <requirement id=" {{ loop.index0 }}" name="{{ requirement_resource.name }}"> |
916 | + {%- for key in requirement_resource|reject('is_name')|sort %} |
917 | + <property name="{{ key }}" type="str">{{ requirement_resource[key] }}</property> |
918 | + {%- endfor %} |
919 | + </requirement> |
920 | + {%- endfor %} |
921 | + </requirements> |
922 | + {%- else %} |
923 | + <!-- requirements resource is not available, not producing the <requirements> tag --> |
924 | + {%- endif %} |
925 | + </software> |
926 | + <summary> |
927 | + {#- FIXME: with all the <property> tags those should not be custom #} |
928 | + <client name="{{ client_name }}" version="{{ client_version }}"/> |
929 | + <date_created value="{{ timestamp }}"/> |
930 | + {%- if ns ~ 'dpkg' in state.resource_map %} |
931 | + {%- set dpkg_resource = state.resource_map[ns ~ 'dpkg'][0] %} |
932 | + <architecture value="{{ dpkg_resource.architecture }}"/> |
933 | + {%- else %} |
934 | + <!-- dpkg resource is not available, not producing the <architecture> tag --> |
935 | + {%- endif %} |
936 | + {%- if ns ~ 'lsb' in state.resource_map %} |
937 | + {#- NOTE: lsb_resource is computed earlier, if the condition above holds #} |
938 | + <distribution value="{{ lsb_resource.distributor_id }}"/> |
939 | + <distroseries value="{{ lsb_resource.release }}"/> |
940 | + {%- else %} |
941 | + <!-- lsb resource is not available, not producing <distribution> and <distroseries> tags --> |
942 | + {%- endif %} |
943 | + {%- if ns ~ 'uname' in state.resource_map %} |
944 | + {%- set uname_resource = state.resource_map[ns ~ 'uname'][0] %} |
945 | + <kernel-release value="{{ uname_resource.release }}"/> |
946 | + {%- else %} |
947 | + <!-- uname resource is not available, not producing the <kernel-release> tag --> |
948 | + {%- endif %} |
949 | + {#- FIXME: yes, this is just hard-coded non-sense #} |
950 | + <private value="False"/> |
951 | + <contactable value="False"/> |
952 | + <live_cd value="False"/> |
953 | + <system_id value="{{ system_id }}"/> |
954 | + </summary> |
955 | +</system> |
956 | |
957 | === modified file 'plainbox/plainbox/impl/commands/inv_check_config.py' |
958 | --- plainbox/plainbox/impl/commands/inv_check_config.py 2014-11-18 17:20:58 +0000 |
959 | +++ plainbox/plainbox/impl/commands/inv_check_config.py 2015-05-14 11:57:31 +0000 |
960 | @@ -43,14 +43,17 @@ |
961 | print(_(" - {0} (not present)").format(filename)) |
962 | print(_("Variables:")) |
963 | for variable in self.config.Meta.variable_list: |
964 | + print(" [{0}]".format(variable.section)) |
965 | print(" {0}={1}".format( |
966 | variable.name, |
967 | variable.__get__(self.config, self.config.__class__))) |
968 | print(_("Sections:")) |
969 | for section in self.config.Meta.section_list: |
970 | - print(" {0}={1}".format( |
971 | - section.name, |
972 | - section.__get__(self.config, self.config.__class__))) |
973 | + print(" [{0}]".format(section.name)) |
974 | + section_value = section.__get__(self.config, self.config.__class__) |
975 | + if section_value: |
976 | + for key, value in sorted(section_value.items()): |
977 | + print(" {0}={1}".format(key, value)) |
978 | if self.config.problem_list: |
979 | print(_("Problems:")) |
980 | for problem in self.config.problem_list: |
981 | |
982 | === modified file 'plainbox/plainbox/impl/commands/inv_run.py' |
983 | --- plainbox/plainbox/impl/commands/inv_run.py 2015-05-12 16:09:40 +0000 |
984 | +++ plainbox/plainbox/impl/commands/inv_run.py 2015-05-14 11:57:31 +0000 |
985 | @@ -44,6 +44,7 @@ |
986 | from plainbox.impl.depmgr import DependencyDuplicateError |
987 | from plainbox.impl.exporter import ByteStringStreamTranslator |
988 | from plainbox.impl.exporter import get_all_exporters |
989 | +from plainbox.impl.result import JobResultBuilder |
990 | from plainbox.impl.result import MemoryJobResult |
991 | from plainbox.impl.result import tr_outcome |
992 | from plainbox.impl.runner import JobRunner |
993 | @@ -626,6 +627,11 @@ |
994 | # Connect the on_job_added signal. We use it to mark the test loop |
995 | # for re-execution and to update the list of desired jobs. |
996 | self.state.on_job_added.connect(self.on_job_added) |
997 | + finally: |
998 | + print(_("Session ID: {0}").format( |
999 | + self.C.YELLOW(self._manager.storage.id) |
1000 | + )) |
1001 | + |
1002 | |
1003 | def create_runner(self): |
1004 | """ |
1005 | @@ -741,10 +747,7 @@ |
1006 | estimated_time -= job.estimated_duration |
1007 | |
1008 | def run_single_job(self, job): |
1009 | - job_start_time = time.time() |
1010 | self.run_single_job_with_ui(job, self.get_ui_for_job(job)) |
1011 | - job_state = self.state.job_state_map[job.id] |
1012 | - job_state.result.execution_duration = time.time() - job_start_time |
1013 | |
1014 | def get_ui_for_job(self, job): |
1015 | if self.ns.dont_suppress_output is False and job.plugin in ( |
1016 | @@ -754,6 +757,7 @@ |
1017 | return NormalUI(self.C.c, show_cmd_output=True) |
1018 | |
1019 | def run_single_job_with_ui(self, job, ui): |
1020 | + job_start_time = time.time() |
1021 | job_state = self.state.job_state_map[job.id] |
1022 | ui.considering_job(job, job_state) |
1023 | if job_state.can_start(): |
1024 | @@ -761,18 +765,21 @@ |
1025 | self.metadata.running_job_name = job.id |
1026 | self.manager.checkpoint() |
1027 | ui.started_running(job, job_state) |
1028 | - job_result = self._run_single_job_with_ui_loop(job, job_state, ui) |
1029 | + result_builder = self._run_single_job_with_ui_loop(job, job_state, ui) |
1030 | + assert result_builder is not None |
1031 | + result_builder.execution_duration = time.time() - job_start_time |
1032 | + job_result = result_builder.seal() |
1033 | self.metadata.running_job_name = None |
1034 | self.manager.checkpoint() |
1035 | ui.finished_running(job, job_state, job_result) |
1036 | else: |
1037 | - job_result = MemoryJobResult({ |
1038 | - 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED, |
1039 | - 'comments': job_state.get_readiness_description() |
1040 | - }) |
1041 | + result_builder = JobResultBuilder( |
1042 | + outcome=IJobResult.OUTCOME_NOT_SUPPORTED, |
1043 | + comments=job_state.get_readiness_description(), |
1044 | + execution_duration = time.time() - job_start_time) |
1045 | + job_result = result_builder.seal() |
1046 | ui.job_cannot_start(job, job_state, job_result) |
1047 | - if job_result is not None: |
1048 | - self.state.update_job_result(job, job_result) |
1049 | + self.state.update_job_result(job, job_result) |
1050 | ui.finished(job, job_state, job_result) |
1051 | |
1052 | def _run_single_job_with_ui_loop(self, job, job_state, ui): |
1053 | @@ -788,8 +795,9 @@ |
1054 | cmd = ui.wait_for_interaction_prompt(job) |
1055 | if cmd == 'run' or cmd is None: |
1056 | ui.notify_about_steps(job) |
1057 | - job_result = self.runner.run_job( |
1058 | - job, job_state, self.config, ui) |
1059 | + result_builder = self.runner.run_job( |
1060 | + job, job_state, self.config, ui |
1061 | + ).unseal() |
1062 | elif cmd == 'comment': |
1063 | new_comment = input(self.C.BLUE( |
1064 | _('Please enter your comments:') + '\n')) |
1065 | @@ -797,36 +805,37 @@ |
1066 | comments += new_comment + '\n' |
1067 | continue |
1068 | elif cmd == 'skip': |
1069 | - job_result = MemoryJobResult({ |
1070 | - 'outcome': IJobResult.OUTCOME_SKIP, |
1071 | - 'comments': _("Explicitly skipped before" |
1072 | - " execution") |
1073 | - }) |
1074 | + result_builder = JobResultBuilder( |
1075 | + outcome=IJobResult.OUTCOME_SKIP, |
1076 | + comments=_("Explicitly skipped before" |
1077 | + " execution")) |
1078 | if comments != "": |
1079 | - job_result.comments = comments |
1080 | + result_builder.comments = comments |
1081 | break |
1082 | elif cmd == 'quit': |
1083 | raise SystemExit() |
1084 | else: |
1085 | - job_result = self.runner.run_job( |
1086 | - job, job_state, self.config, ui) |
1087 | + result_builder = self.runner.run_job( |
1088 | + job, job_state, self.config, ui |
1089 | + ).unseal() |
1090 | else: |
1091 | if 'noreturn' in job.get_flag_set(): |
1092 | ui.noreturn_job() |
1093 | - job_result = self.runner.run_job( |
1094 | - job, job_state, self.config, ui) |
1095 | + result_builder = self.runner.run_job( |
1096 | + job, job_state, self.config, ui |
1097 | + ).unseal() |
1098 | if (self.is_interactive and |
1099 | - job_result.outcome == IJobResult.OUTCOME_UNDECIDED): |
1100 | + result_builder.outcome == IJobResult.OUTCOME_UNDECIDED): |
1101 | try: |
1102 | if comments != "": |
1103 | - job_result.comments = comments |
1104 | + result_builder.comments = comments |
1105 | ui.notify_about_verification(job) |
1106 | - job_result = self._interaction_callback( |
1107 | - self.runner, job, job_result, self.config) |
1108 | + self._interaction_callback( |
1109 | + self.runner, job, result_builder, self.config) |
1110 | except ReRunJob: |
1111 | continue |
1112 | break |
1113 | - return job_result |
1114 | + return result_builder |
1115 | |
1116 | def export_and_send_results(self): |
1117 | # Get a stream with exported session data. |
1118 | @@ -861,7 +870,7 @@ |
1119 | def _pick_action_cmd(self, action_list, prompt=None): |
1120 | return ActionUI(action_list, prompt, self._color).run() |
1121 | |
1122 | - def _interaction_callback(self, runner, job, result, config, |
1123 | + def _interaction_callback(self, runner, job, result_builder, config, |
1124 | prompt=None, allowed_outcome=None): |
1125 | if prompt is None: |
1126 | prompt = _("Select an outcome or an action: ") |
1127 | @@ -890,42 +899,40 @@ |
1128 | if job.command is not None: |
1129 | allowed_actions.append( |
1130 | Action('r', _('re-run this job'), 're-run')) |
1131 | - if result.return_code is not None: |
1132 | - if result.return_code == 0: |
1133 | + if result_builder.return_code is not None: |
1134 | + if result_builder.return_code == 0: |
1135 | suggested_outcome = IJobResult.OUTCOME_PASS |
1136 | else: |
1137 | suggested_outcome = IJobResult.OUTCOME_FAIL |
1138 | allowed_actions.append( |
1139 | Action('', _('set suggested outcome [{0}]').format( |
1140 | tr_outcome(suggested_outcome)), 'set-suggested')) |
1141 | - while result.outcome not in allowed_outcome: |
1142 | + while result_builder.outcome not in allowed_outcome: |
1143 | print(_("Please decide what to do next:")) |
1144 | - print(" " + _("outcome") + ": {0}".format(self.C.result(result))) |
1145 | - if result.comments is None: |
1146 | + print(" " + _("outcome") + ": {0}".format( |
1147 | + self.C.result(result_builder.seal()))) |
1148 | + if result_builder.comments is None: |
1149 | print(" " + _("comments") + ": {0}".format( |
1150 | C_("none comment", "none"))) |
1151 | else: |
1152 | print(" " + _("comments") + ": {0}".format( |
1153 | - self.C.CYAN(result.comments, bright=False))) |
1154 | + self.C.CYAN(result_builder.comments, bright=False))) |
1155 | cmd = self._pick_action_cmd(allowed_actions) |
1156 | if cmd == 'set-pass': |
1157 | - result.outcome = IJobResult.OUTCOME_PASS |
1158 | + result_builder.outcome = IJobResult.OUTCOME_PASS |
1159 | elif cmd == 'set-fail': |
1160 | - result.outcome = IJobResult.OUTCOME_FAIL |
1161 | + result_builder.outcome = IJobResult.OUTCOME_FAIL |
1162 | elif cmd == 'set-skip' or cmd is None: |
1163 | - result.outcome = IJobResult.OUTCOME_SKIP |
1164 | + result_builder.outcome = IJobResult.OUTCOME_SKIP |
1165 | elif cmd == 'set-suggested': |
1166 | - result.outcome = suggested_outcome |
1167 | + result_builder.outcome = suggested_outcome |
1168 | elif cmd == 'set-comments': |
1169 | - if result.comments is None: |
1170 | - result.comments = "" |
1171 | new_comment = input(self.C.BLUE( |
1172 | _('Please enter your comments:') + '\n')) |
1173 | if new_comment: |
1174 | - result.comments += new_comment + '\n' |
1175 | + result_builder.add_comment(new_comment) |
1176 | elif cmd == 're-run': |
1177 | raise ReRunJob |
1178 | - return result |
1179 | |
1180 | def _update_desired_job_list(self, desired_job_list): |
1181 | problem_list = self.state.update_desired_job_list(desired_job_list) |
1182 | |
1183 | === added file 'plainbox/plainbox/impl/exporter/hexr.py' |
1184 | --- plainbox/plainbox/impl/exporter/hexr.py 1970-01-01 00:00:00 +0000 |
1185 | +++ plainbox/plainbox/impl/exporter/hexr.py 2015-05-14 11:57:31 +0000 |
1186 | @@ -0,0 +1,138 @@ |
1187 | +# This file is part of Checkbox. |
1188 | +# |
1189 | +# Copyright 2015 Canonical Ltd. |
1190 | +# Written by: |
1191 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
1192 | +# |
1193 | +# Checkbox is free software: you can redistribute it and/or modify |
1194 | +# it under the terms of the GNU General Public License version 3, |
1195 | +# as published by the Free Software Foundation. |
1196 | +# |
1197 | +# Checkbox is distributed in the hope that it will be useful, |
1198 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1199 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1200 | +# GNU General Public License for more details. |
1201 | +# |
1202 | +# You should have received a copy of the GNU General Public License |
1203 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
1204 | + |
1205 | +"""Exporter for creating the XML structure expected by HEXR.""" |
1206 | + |
1207 | +from datetime import datetime |
1208 | + |
1209 | +from jinja2 import Markup |
1210 | +from jinja2 import Undefined |
1211 | +from jinja2 import environmentfilter |
1212 | +from jinja2 import escape |
1213 | + |
1214 | +from plainbox import __version__ as version |
1215 | +from plainbox.impl.exporter.jinja2 import Jinja2SessionStateExporter |
1216 | +from plainbox.impl.result import OUTCOME_METADATA_MAP |
1217 | + |
1218 | + |
1219 | +#: Name-space prefix for Canonical Certification |
1220 | +CERTIFICATION_NS = '2013.com.canonical.certification::' |
1221 | + |
1222 | + |
1223 | +@environmentfilter |
1224 | +def do_sorted_xmlattr(_environment, d, autospace=True): |
1225 | + """A version of xmlattr filter that sorts attributes.""" |
1226 | + rv = ' '.join( |
1227 | + '%s="%s"' % (escape(key), escape(value)) |
1228 | + for key, value in sorted(d.items()) |
1229 | + if value is not None and not isinstance(value, Undefined) |
1230 | + ) |
1231 | + if autospace and rv: |
1232 | + rv = ' ' + rv |
1233 | + if _environment.autoescape: |
1234 | + rv = Markup(rv) |
1235 | + return rv |
1236 | + |
1237 | + |
1238 | +@environmentfilter |
1239 | +def do_strip_ns(_environment, unit_id, ns=CERTIFICATION_NS): |
1240 | + """Remove the namespace part of the identifier.""" |
1241 | + if unit_id.startswith(ns): |
1242 | + rv = unit_id[len(ns):] |
1243 | + else: |
1244 | + rv = unit_id |
1245 | + rv = escape(rv) |
1246 | + if _environment.autoescape: |
1247 | + rv = Markup(rv) |
1248 | + return rv |
1249 | + |
1250 | + |
1251 | +def do_is_name(text): |
1252 | + """A filter for checking if something is equal to "name".""" |
1253 | + return text == 'name' |
1254 | + |
1255 | + |
1256 | +class HEXRExporter(Jinja2SessionStateExporter): |
1257 | + |
1258 | + """ |
1259 | + Exporter for creating HEXR XML documents. |
1260 | + |
1261 | + This exporter takes the whole session and creates a XML document |
1262 | + containing a subset of the data. It is applicable for submission to HEXR |
1263 | + and ``C3`` (the certification website). It's also really bad. |
1264 | + |
1265 | + The format is hardwired to require certain jobs. They are: |
1266 | + |
1267 | + - 2013.com.canonical.certification::package (Optional) |
1268 | + - 2013.com.canonical.certification::uname (Optional) |
1269 | + - 2013.com.canonical.certification::lsb (Mandatory) |
1270 | + - 2013.com.canonical.certification::cpuinfo (Mandatory) |
1271 | + - 2013.com.canonical.certification::dpkg (Mandatory) |
1272 | + - 2013.com.canonical.certification::dmi_attachment (Mandatory) |
1273 | + - 2013.com.canonical.certification::sysfs_attachment (Mandatory) |
1274 | + - 2013.com.canonical.certification::udev_attachment (Mandatory) |
1275 | + |
1276 | + .. note:: |
1277 | + The exporter won't misbehave if those are not available but the server |
1278 | + side component will most likely crash or reject the resulting document. |
1279 | + """ |
1280 | + |
1281 | + def __init__(self, option_list=None, system_id="", timestamp=None, |
1282 | + client_version=None, client_name='plainbox'): |
1283 | + """Initialize the exporter with stuff that exporters need.""" |
1284 | + super().__init__("hexr.xml", option_list) |
1285 | + self._system_id = system_id |
1286 | + # Generate a time-stamp if needed |
1287 | + if timestamp is None: |
1288 | + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") |
1289 | + self._timestamp = timestamp |
1290 | + # Use current version unless told otherwise |
1291 | + if client_version is None: |
1292 | + client_version = "{}.{}.{}".format(*version[:3]) |
1293 | + self._client_version = client_version |
1294 | + # Remember client name |
1295 | + self._client_name = client_name |
1296 | + |
1297 | + def customize_environment(self, env): |
1298 | + """Register filters and tests custom to the HEXR exporter.""" |
1299 | + env.autoescape = True |
1300 | + env.filters['sorted_xmlattr'] = do_sorted_xmlattr |
1301 | + env.filters['strip_ns'] = do_strip_ns |
1302 | + env.tests['is_name'] = do_is_name |
1303 | + |
1304 | + def dump_from_session_manager(self, manager, stream): |
1305 | + """ |
1306 | + Extract data from session_manager and dump it into the stream. |
1307 | + |
1308 | + :param session_manager: |
1309 | + SessionManager instance that manages session to be exported by |
1310 | + this exporter |
1311 | + :param stream: |
1312 | + Byte stream to write to. |
1313 | + |
1314 | + """ |
1315 | + data = { |
1316 | + 'OUTCOME_METADATA_MAP': OUTCOME_METADATA_MAP, |
1317 | + 'client_name': self._client_name, |
1318 | + 'client_version': self._client_version, |
1319 | + 'manager': manager, |
1320 | + 'options': self.option_list, |
1321 | + 'system_id': self._system_id, |
1322 | + 'timestamp': self._timestamp, |
1323 | + } |
1324 | + self.dump(data, stream) |
1325 | |
1326 | === modified file 'plainbox/plainbox/impl/exporter/html.py' |
1327 | --- plainbox/plainbox/impl/exporter/html.py 2015-05-10 18:38:53 +0000 |
1328 | +++ plainbox/plainbox/impl/exporter/html.py 2015-05-14 11:57:31 +0000 |
1329 | @@ -33,7 +33,6 @@ |
1330 | |
1331 | from plainbox import __version__ as version |
1332 | from plainbox.impl.exporter.jinja2 import Jinja2SessionStateExporter |
1333 | -from plainbox.impl.result import OUTCOME_METADATA_MAP as OMM |
1334 | |
1335 | |
1336 | class HTMLSessionStateExporter(Jinja2SessionStateExporter): |
1337 | @@ -75,7 +74,6 @@ |
1338 | data = { |
1339 | 'manager': session_manager, |
1340 | 'options': self.option_list, |
1341 | - 'OMM': OMM, |
1342 | 'timestamp': self._timestamp, |
1343 | 'client_version': self._client_version, |
1344 | 'client_name': self._client_name |
1345 | |
1346 | === modified file 'plainbox/plainbox/impl/exporter/jinja2.py' |
1347 | --- plainbox/plainbox/impl/exporter/jinja2.py 2015-04-09 16:57:49 +0000 |
1348 | +++ plainbox/plainbox/impl/exporter/jinja2.py 2015-05-14 11:57:31 +0000 |
1349 | @@ -55,6 +55,7 @@ |
1350 | paths.extend(extra_paths) |
1351 | loader = FileSystemLoader(paths) |
1352 | env = Environment(loader=loader) |
1353 | + self.customize_environment(env) |
1354 | |
1355 | def include_file(name): |
1356 | # This helper function insert static files literally into Jinja |
1357 | @@ -64,6 +65,14 @@ |
1358 | self.template = env.get_template(jinja2_template) |
1359 | env.globals['include_file'] = include_file |
1360 | |
1361 | + def customize_environment(self, env): |
1362 | + """ |
1363 | + Customize the jinja2 Environment object. |
1364 | + |
1365 | + By default this method does nothing. Override it in your subclass to |
1366 | + define any extra filters, tests or other things that you may require. |
1367 | + """ |
1368 | + |
1369 | def dump(self, data, stream): |
1370 | """ |
1371 | Render report using jinja2 and dump it to stream. |
1372 | |
1373 | === added file 'plainbox/plainbox/impl/exporter/test_hexr.py' |
1374 | --- plainbox/plainbox/impl/exporter/test_hexr.py 1970-01-01 00:00:00 +0000 |
1375 | +++ plainbox/plainbox/impl/exporter/test_hexr.py 2015-05-14 11:57:31 +0000 |
1376 | @@ -0,0 +1,535 @@ |
1377 | +# This file is part of Checkbox. |
1378 | +# |
1379 | +# Copyright 2015 Canonical Ltd. |
1380 | +# Written by: |
1381 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
1382 | +# |
1383 | +# Checkbox is free software: you can redistribute it and/or modify |
1384 | +# it under the terms of the GNU General Public License version 3, |
1385 | +# as published by the Free Software Foundation. |
1386 | +# |
1387 | +# Checkbox is distributed in the hope that it will be useful, |
1388 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1389 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1390 | +# GNU General Public License for more details. |
1391 | +# |
1392 | +# You should have received a copy of the GNU General Public License |
1393 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
1394 | + |
1395 | +"""Tests for the hexr exporter.""" |
1396 | + |
1397 | +from io import BytesIO |
1398 | +from unittest import TestCase |
1399 | +from xml.etree import ElementTree |
1400 | + |
1401 | +from plainbox.impl.exporter.hexr import CERTIFICATION_NS |
1402 | +from plainbox.impl.exporter.hexr import HEXRExporter |
1403 | +from plainbox.impl.providers.special import get_stubbox |
1404 | +from plainbox.impl.resource import Resource |
1405 | +from plainbox.impl.result import MemoryJobResult |
1406 | +from plainbox.impl.session import SessionManager |
1407 | +from plainbox.impl.unit.job import JobDefinition |
1408 | + |
1409 | + |
1410 | +class HexrExporterTests(TestCase): |
1411 | + |
1412 | + """Tests for HEXRExporter.""" |
1413 | + |
1414 | + maxDiff = None |
1415 | + |
1416 | + def setUp(self): |
1417 | + """Common initialization.""" |
1418 | + self.exporter = HEXRExporter( |
1419 | + system_id='SYSTEM_ID', timestamp='TIMESTAMP', |
1420 | + client_version='CLIENT_VERSION', client_name='CLIENT_NAME') |
1421 | + self.manager = SessionManager.create() |
1422 | + self.manager.add_local_device_context() |
1423 | + |
1424 | + def _populate_session(self): |
1425 | + self._make_representative_jobs() |
1426 | + self._make_cert_resources() |
1427 | + self._make_cert_attachments() |
1428 | + |
1429 | + def _make_representative_jobs(self): |
1430 | + # Add all of the jobs from representative.pxu so that we don't have to |
1431 | + # create verbose fakes. Each job gets a simple passing result. |
1432 | + state = self.manager.default_device_context.state |
1433 | + stubbox = get_stubbox(validate=False, check=True) |
1434 | + for job in stubbox.job_list: |
1435 | + if not job.partial_id.startswith('representative/plugin/'): |
1436 | + continue |
1437 | + state.add_unit(job) |
1438 | + result = self._make_result_for(job) |
1439 | + state.update_job_result(job, result) |
1440 | + # Add a comment to one job (the last one) |
1441 | + result.comments = 'COMMENTS' |
1442 | + |
1443 | + def _make_result_for(self, job): |
1444 | + data = { |
1445 | + 'outcome': 'pass', |
1446 | + 'comments': None |
1447 | + } |
1448 | + if job.plugin == 'local': |
1449 | + pass |
1450 | + elif job.plugin == 'resource': |
1451 | + pass |
1452 | + else: |
1453 | + data['io_log'] = ( |
1454 | + (0, 'stdout', b'IO-LOG-STDOUT\n'), |
1455 | + (1, 'stderr', b'IO-LOG-STDERR\n') |
1456 | + ) |
1457 | + return MemoryJobResult(data) |
1458 | + |
1459 | + def _make_cert_resources(self): |
1460 | + # Create some specific resources that this exporter relies on. The |
1461 | + # corresponding jobs are _not_ loaded but this is irrelevant. |
1462 | + state = self.manager.default_device_context.state |
1463 | + ns = CERTIFICATION_NS |
1464 | + state.set_resource_list(ns + 'cpuinfo', [Resource({ |
1465 | + 'PROP-1': 'VALUE-1', |
1466 | + 'PROP-2': 'VALUE-2', |
1467 | + 'count': '2', # NOTE: this has to be a number :/ |
1468 | + })]) |
1469 | + state.set_resource_list(ns + 'dpkg', [Resource({ |
1470 | + 'architecture': 'dpkg.ARCHITECTURE', |
1471 | + })]) |
1472 | + state.set_resource_list(ns + 'lsb', [Resource({ |
1473 | + 'codename': 'lsb.CODENAME', |
1474 | + 'description': 'lsb.DESCRIPTION', |
1475 | + 'release': 'lsb.RELEASE', |
1476 | + 'distributor_id': 'lsb.DISTRIBUTOR_ID', |
1477 | + })]) |
1478 | + state.set_resource_list(ns + 'uname', [Resource({ |
1479 | + 'release': 'uname.RELEASE', |
1480 | + })]) |
1481 | + state.set_resource_list(ns + 'package', [Resource({ |
1482 | + 'name': 'package.0.NAME', |
1483 | + 'version': 'package.0.VERSION', |
1484 | + }), Resource({ |
1485 | + 'name': 'package.1.NAME', |
1486 | + 'version': 'package.1.VERSION', |
1487 | + })]) |
1488 | + state.set_resource_list(ns + 'requirements', [Resource({ |
1489 | + 'name': 'requirement.0.NAME', |
1490 | + 'link': 'requirement.0.LINK', |
1491 | + }), Resource({ |
1492 | + 'name': 'requirement.1.NAME', |
1493 | + 'link': 'requirement.1.LINK', |
1494 | + })]) |
1495 | + |
1496 | + def _make_cert_attachments(self): |
1497 | + state = self.manager.default_device_context.state |
1498 | + partial_id_list = ['dmi_attachment', 'sysfs_attachment', |
1499 | + 'udev_attachment'] |
1500 | + for partial_id in partial_id_list: |
1501 | + job = JobDefinition({ |
1502 | + 'id': CERTIFICATION_NS + partial_id, |
1503 | + 'plugin': 'attachment' |
1504 | + }) |
1505 | + result = MemoryJobResult({ |
1506 | + 'io_log': ( |
1507 | + (0, 'stdout', 'STDOUT-{}\n'.format( |
1508 | + partial_id).encode('utf-8')), |
1509 | + (1, 'stderr', 'STDERR-{}\n'.format( |
1510 | + partial_id).encode('utf-8'))), |
1511 | + }) |
1512 | + state.add_unit(job) |
1513 | + state.update_job_result(job, result) |
1514 | + |
1515 | + def _inject_evil_input(self): |
1516 | + evil = '"\'<&>' |
1517 | + self.exporter._system_id = evil |
1518 | + self.exporter._timestamp = evil |
1519 | + self.exporter._client_name = evil |
1520 | + self.exporter._client_version = evil |
1521 | + state = self.manager.default_device_context.state |
1522 | + for resource_id in state.resource_map: |
1523 | + resource_list = state.resource_map[resource_id] |
1524 | + for resource in resource_list: |
1525 | + for key in resource: |
1526 | + if resource_id.endswith('cpuinfo') and key == 'count': |
1527 | + # don't change resources for the <hardware> section |
1528 | + continue |
1529 | + resource[key] = evil |
1530 | + new_job_state_map = {} |
1531 | + for index, job_id in enumerate(sorted(state.job_state_map)): |
1532 | + job_state = state.job_state_map[job_id] |
1533 | + if (job_state.job.partial_id.endswith('_attachment') |
1534 | + or job_state.job.partial_id == 'cpuinfo'): |
1535 | + # don't change attachments for the <hardware> section |
1536 | + evil_id = job_id |
1537 | + else: |
1538 | + evil_id = '{}-{}-{}'.format(evil, index, job_state.job.plugin) |
1539 | + # NOTE: using private API |
1540 | + job_state.job._data['id'] = evil_id |
1541 | + job_state.result.comments = evil |
1542 | + # NOTE: using private API |
1543 | + job_state.result._data['_io_log'] = ( |
1544 | + (0, 'stdout', evil.encode("UTF-8")), |
1545 | + ) |
1546 | + new_job_state_map[evil_id] = job_state |
1547 | + # NOTE: using private API |
1548 | + state._job_state_map = new_job_state_map |
1549 | + |
1550 | + def tearDown(self): |
1551 | + """Common teardown.""" |
1552 | + self.manager.destroy() |
1553 | + |
1554 | + def test_smoke(self): |
1555 | + """The XML document has the right data in the right spot.""" |
1556 | + self._populate_session() |
1557 | + stream = BytesIO() |
1558 | + self.exporter.dump_from_session_manager(self.manager, stream) |
1559 | + smoke_actual = stream.getvalue().decode("utf-8") |
1560 | + self.assertMultiLineEqual(_smoke_expected, smoke_actual) |
1561 | + |
1562 | + def test_without_any_data(self): |
1563 | + """The XML document can be produced without any data in the session.""" |
1564 | + stream = BytesIO() |
1565 | + self.exporter.dump_from_session_manager(self.manager, stream) |
1566 | + empty_actual = stream.getvalue().decode("utf-8") |
1567 | + self.assertMultiLineEqual(_empty_expected, empty_actual) |
1568 | + |
1569 | + def test_escaping(self): |
1570 | + """Evil input doesn't break the correctness of the XML document.""" |
1571 | + self._populate_session() |
1572 | + self._inject_evil_input() |
1573 | + stream = BytesIO() |
1574 | + self.exporter.dump_from_session_manager(self.manager, stream) |
1575 | + evil_actual = stream.getvalue().decode("utf-8") |
1576 | + self.assertMultiLineEqual(_evil_expected, evil_actual) |
1577 | + |
1578 | + def test_xml_parsability(self): |
1579 | + """Each produced output can be parsed with an XML parser.""" |
1580 | + stream1 = BytesIO(_smoke_expected.encode("utf-8")) |
1581 | + ElementTree.parse(stream1) |
1582 | + stream2 = BytesIO(_empty_expected.encode("utf-8")) |
1583 | + ElementTree.parse(stream2) |
1584 | + stream3 = BytesIO(_evil_expected.encode("utf-8")) |
1585 | + ElementTree.parse(stream3) |
1586 | + |
1587 | + |
1588 | +_smoke_expected = """\ |
1589 | +<?xml version="1.0"?> |
1590 | +<system version="1.0"> |
1591 | + <context> |
1592 | + <info command="2013.com.canonical.plainbox::representative/plugin/attachment">IO-LOG-STDOUT |
1593 | +</info> |
1594 | + <info command="2013.com.canonical.plainbox::representative/plugin/local"></info> |
1595 | + <info command="2013.com.canonical.plainbox::representative/plugin/manual">IO-LOG-STDOUT |
1596 | +</info> |
1597 | + <info command="2013.com.canonical.plainbox::representative/plugin/qml">IO-LOG-STDOUT |
1598 | +</info> |
1599 | + <info command="2013.com.canonical.plainbox::representative/plugin/resource"></info> |
1600 | + <info command="2013.com.canonical.plainbox::representative/plugin/shell">IO-LOG-STDOUT |
1601 | +</info> |
1602 | + <info command="2013.com.canonical.plainbox::representative/plugin/user-interact">IO-LOG-STDOUT |
1603 | +</info> |
1604 | + <info command="2013.com.canonical.plainbox::representative/plugin/user-interact-verify">IO-LOG-STDOUT |
1605 | +</info> |
1606 | + <info command="2013.com.canonical.plainbox::representative/plugin/user-verify">IO-LOG-STDOUT |
1607 | +</info> |
1608 | + </context> |
1609 | + <hardware> |
1610 | + <dmi>STDOUT-dmi_attachment |
1611 | +</dmi> |
1612 | + <sysfs-attributes>STDOUT-sysfs_attachment |
1613 | +</sysfs-attributes> |
1614 | + <udev>STDOUT-udev_attachment |
1615 | +</udev> |
1616 | + <processors> |
1617 | + <processor id="0" name="0"> |
1618 | + <property name="count" type="str">2</property> |
1619 | + <property name="PROP-1" type="str">VALUE-1</property> |
1620 | + <property name="PROP-2" type="str">VALUE-2</property> |
1621 | + </processor> |
1622 | + <processor id="1" name="1"> |
1623 | + <property name="count" type="str">2</property> |
1624 | + <property name="PROP-1" type="str">VALUE-1</property> |
1625 | + <property name="PROP-2" type="str">VALUE-2</property> |
1626 | + </processor> |
1627 | + </processors> |
1628 | + </hardware> |
1629 | + <questions> |
1630 | + <question name="2013.com.canonical.plainbox::representative/plugin/manual"> |
1631 | + <answer type="multiple_choice">pass</answer> |
1632 | + <answer_choices> |
1633 | + <value type="str">none</value> |
1634 | + <value type="str">pass</value> |
1635 | + <value type="str">fail</value> |
1636 | + <value type="str">skip</value> |
1637 | + </answer_choices> |
1638 | + <comment>IO-LOG-STDOUT |
1639 | +IO-LOG-STDERR |
1640 | +</comment> |
1641 | + </question> |
1642 | + <question name="2013.com.canonical.plainbox::representative/plugin/qml"> |
1643 | + <answer type="multiple_choice">pass</answer> |
1644 | + <answer_choices> |
1645 | + <value type="str">none</value> |
1646 | + <value type="str">pass</value> |
1647 | + <value type="str">fail</value> |
1648 | + <value type="str">skip</value> |
1649 | + </answer_choices> |
1650 | + <comment>IO-LOG-STDOUT |
1651 | +IO-LOG-STDERR |
1652 | +</comment> |
1653 | + </question> |
1654 | + <question name="2013.com.canonical.plainbox::representative/plugin/shell"> |
1655 | + <answer type="multiple_choice">pass</answer> |
1656 | + <answer_choices> |
1657 | + <value type="str">none</value> |
1658 | + <value type="str">pass</value> |
1659 | + <value type="str">fail</value> |
1660 | + <value type="str">skip</value> |
1661 | + </answer_choices> |
1662 | + <comment>IO-LOG-STDOUT |
1663 | +IO-LOG-STDERR |
1664 | +</comment> |
1665 | + </question> |
1666 | + <question name="2013.com.canonical.plainbox::representative/plugin/user-interact"> |
1667 | + <answer type="multiple_choice">pass</answer> |
1668 | + <answer_choices> |
1669 | + <value type="str">none</value> |
1670 | + <value type="str">pass</value> |
1671 | + <value type="str">fail</value> |
1672 | + <value type="str">skip</value> |
1673 | + </answer_choices> |
1674 | + <comment>IO-LOG-STDOUT |
1675 | +IO-LOG-STDERR |
1676 | +</comment> |
1677 | + </question> |
1678 | + <question name="2013.com.canonical.plainbox::representative/plugin/user-interact-verify"> |
1679 | + <answer type="multiple_choice">pass</answer> |
1680 | + <answer_choices> |
1681 | + <value type="str">none</value> |
1682 | + <value type="str">pass</value> |
1683 | + <value type="str">fail</value> |
1684 | + <value type="str">skip</value> |
1685 | + </answer_choices> |
1686 | + <comment>IO-LOG-STDOUT |
1687 | +IO-LOG-STDERR |
1688 | +</comment> |
1689 | + </question> |
1690 | + <question name="2013.com.canonical.plainbox::representative/plugin/user-verify"> |
1691 | + <answer type="multiple_choice">pass</answer> |
1692 | + <answer_choices> |
1693 | + <value type="str">none</value> |
1694 | + <value type="str">pass</value> |
1695 | + <value type="str">fail</value> |
1696 | + <value type="str">skip</value> |
1697 | + </answer_choices> |
1698 | + <comment>COMMENTS</comment> |
1699 | + </question> |
1700 | + </questions> |
1701 | + <software> |
1702 | + <lsbrelease> |
1703 | + <property name="codename" type="str">lsb.CODENAME</property> |
1704 | + <property name="description" type="str">lsb.DESCRIPTION</property> |
1705 | + <property name="distributor_id" type="str">lsb.DISTRIBUTOR_ID</property> |
1706 | + <property name="release" type="str">lsb.RELEASE</property> |
1707 | + </lsbrelease> |
1708 | + <packages> |
1709 | + <package id="0" name="package.0.NAME"> |
1710 | + <property name="version" type="str">package.0.VERSION</property> |
1711 | + </package> |
1712 | + <package id="1" name="package.1.NAME"> |
1713 | + <property name="version" type="str">package.1.VERSION</property> |
1714 | + </package> |
1715 | + </packages> |
1716 | + <requirements> |
1717 | + <requirement id=" 0" name="requirement.0.NAME"> |
1718 | + <property name="link" type="str">requirement.0.LINK</property> |
1719 | + </requirement> |
1720 | + <requirement id=" 1" name="requirement.1.NAME"> |
1721 | + <property name="link" type="str">requirement.1.LINK</property> |
1722 | + </requirement> |
1723 | + </requirements> |
1724 | + </software> |
1725 | + <summary> |
1726 | + <client name="CLIENT_NAME" version="CLIENT_VERSION"/> |
1727 | + <date_created value="TIMESTAMP"/> |
1728 | + <architecture value="dpkg.ARCHITECTURE"/> |
1729 | + <distribution value="lsb.DISTRIBUTOR_ID"/> |
1730 | + <distroseries value="lsb.RELEASE"/> |
1731 | + <kernel-release value="uname.RELEASE"/> |
1732 | + <private value="False"/> |
1733 | + <contactable value="False"/> |
1734 | + <live_cd value="False"/> |
1735 | + <system_id value="SYSTEM_ID"/> |
1736 | + </summary> |
1737 | +</system>""" |
1738 | + |
1739 | + |
1740 | +_empty_expected = """\ |
1741 | +<?xml version="1.0"?> |
1742 | +<system version="1.0"> |
1743 | + <context> |
1744 | + </context> |
1745 | + <hardware> |
1746 | + <!-- the dmi_attachment job is not available, not producing the <dmi> section --> |
1747 | + <!-- the sysfs_attachment job is not available, not producing the <sysfs-attributes> tag --> |
1748 | + <!-- the udev_attachment job is not available, not producing the <udev> tag --> |
1749 | + <!-- cpuinfo resource is not available, not producing the <processors> section --> |
1750 | + </hardware> |
1751 | + <questions> |
1752 | + </questions> |
1753 | + <software> |
1754 | + <!-- lsb resource is not available, not producing the <lsbrelease> tag --> |
1755 | + <!-- package resource is not available, not producing the <packages> tag --> |
1756 | + <!-- requirements resource is not available, not producing the <requirements> tag --> |
1757 | + </software> |
1758 | + <summary> |
1759 | + <client name="CLIENT_NAME" version="CLIENT_VERSION"/> |
1760 | + <date_created value="TIMESTAMP"/> |
1761 | + <!-- dpkg resource is not available, not producing the <architecture> tag --> |
1762 | + <!-- lsb resource is not available, not producing <distribution> and <distroseries> tags --> |
1763 | + <!-- uname resource is not available, not producing the <kernel-release> tag --> |
1764 | + <private value="False"/> |
1765 | + <contactable value="False"/> |
1766 | + <live_cd value="False"/> |
1767 | + <system_id value="SYSTEM_ID"/> |
1768 | + </summary> |
1769 | +</system>""" |
1770 | + |
1771 | +_escaped_evil_text = '"'<&>' |
1772 | +_evil_expected = """\ |
1773 | +<?xml version="1.0"?> |
1774 | +<system version="1.0"> |
1775 | + <context> |
1776 | + <info command="2013.com.canonical.plainbox::"'<&>-10-user-interact-verify">IO-LOG-STDOUT |
1777 | +</info> |
1778 | + <info command="2013.com.canonical.plainbox::"'<&>-11-user-verify">IO-LOG-STDOUT |
1779 | +</info> |
1780 | + <info command="2013.com.canonical.plainbox::"'<&>-3-attachment">IO-LOG-STDOUT |
1781 | +</info> |
1782 | + <info command="2013.com.canonical.plainbox::"'<&>-4-local"></info> |
1783 | + <info command="2013.com.canonical.plainbox::"'<&>-5-manual">IO-LOG-STDOUT |
1784 | +</info> |
1785 | + <info command="2013.com.canonical.plainbox::"'<&>-6-qml">IO-LOG-STDOUT |
1786 | +</info> |
1787 | + <info command="2013.com.canonical.plainbox::"'<&>-7-resource"></info> |
1788 | + <info command="2013.com.canonical.plainbox::"'<&>-8-shell">IO-LOG-STDOUT |
1789 | +</info> |
1790 | + <info command="2013.com.canonical.plainbox::"'<&>-9-user-interact">IO-LOG-STDOUT |
1791 | +</info> |
1792 | + </context> |
1793 | + <hardware> |
1794 | + <dmi>STDOUT-dmi_attachment |
1795 | +</dmi> |
1796 | + <sysfs-attributes>STDOUT-sysfs_attachment |
1797 | +</sysfs-attributes> |
1798 | + <udev>STDOUT-udev_attachment |
1799 | +</udev> |
1800 | + <processors> |
1801 | + <processor id="0" name="0"> |
1802 | + <property name="count" type="str">2</property> |
1803 | + <property name="PROP-1" type="str">{evil}</property> |
1804 | + <property name="PROP-2" type="str">{evil}</property> |
1805 | + </processor> |
1806 | + <processor id="1" name="1"> |
1807 | + <property name="count" type="str">2</property> |
1808 | + <property name="PROP-1" type="str">{evil}</property> |
1809 | + <property name="PROP-2" type="str">{evil}</property> |
1810 | + </processor> |
1811 | + </processors> |
1812 | + </hardware> |
1813 | + <questions> |
1814 | + <question name="2013.com.canonical.plainbox::"'<&>-10-user-interact-verify"> |
1815 | + <answer type="multiple_choice">pass</answer> |
1816 | + <answer_choices> |
1817 | + <value type="str">none</value> |
1818 | + <value type="str">pass</value> |
1819 | + <value type="str">fail</value> |
1820 | + <value type="str">skip</value> |
1821 | + </answer_choices> |
1822 | + <comment>"'<&></comment> |
1823 | + </question> |
1824 | + <question name="2013.com.canonical.plainbox::"'<&>-11-user-verify"> |
1825 | + <answer type="multiple_choice">pass</answer> |
1826 | + <answer_choices> |
1827 | + <value type="str">none</value> |
1828 | + <value type="str">pass</value> |
1829 | + <value type="str">fail</value> |
1830 | + <value type="str">skip</value> |
1831 | + </answer_choices> |
1832 | + <comment>"'<&></comment> |
1833 | + </question> |
1834 | + <question name="2013.com.canonical.plainbox::"'<&>-5-manual"> |
1835 | + <answer type="multiple_choice">pass</answer> |
1836 | + <answer_choices> |
1837 | + <value type="str">none</value> |
1838 | + <value type="str">pass</value> |
1839 | + <value type="str">fail</value> |
1840 | + <value type="str">skip</value> |
1841 | + </answer_choices> |
1842 | + <comment>"'<&></comment> |
1843 | + </question> |
1844 | + <question name="2013.com.canonical.plainbox::"'<&>-6-qml"> |
1845 | + <answer type="multiple_choice">pass</answer> |
1846 | + <answer_choices> |
1847 | + <value type="str">none</value> |
1848 | + <value type="str">pass</value> |
1849 | + <value type="str">fail</value> |
1850 | + <value type="str">skip</value> |
1851 | + </answer_choices> |
1852 | + <comment>"'<&></comment> |
1853 | + </question> |
1854 | + <question name="2013.com.canonical.plainbox::"'<&>-8-shell"> |
1855 | + <answer type="multiple_choice">pass</answer> |
1856 | + <answer_choices> |
1857 | + <value type="str">none</value> |
1858 | + <value type="str">pass</value> |
1859 | + <value type="str">fail</value> |
1860 | + <value type="str">skip</value> |
1861 | + </answer_choices> |
1862 | + <comment>"'<&></comment> |
1863 | + </question> |
1864 | + <question name="2013.com.canonical.plainbox::"'<&>-9-user-interact"> |
1865 | + <answer type="multiple_choice">pass</answer> |
1866 | + <answer_choices> |
1867 | + <value type="str">none</value> |
1868 | + <value type="str">pass</value> |
1869 | + <value type="str">fail</value> |
1870 | + <value type="str">skip</value> |
1871 | + </answer_choices> |
1872 | + <comment>"'<&></comment> |
1873 | + </question> |
1874 | + </questions> |
1875 | + <software> |
1876 | + <lsbrelease> |
1877 | + <property name="codename" type="str">{evil}</property> |
1878 | + <property name="description" type="str">{evil}</property> |
1879 | + <property name="distributor_id" type="str">{evil}</property> |
1880 | + <property name="release" type="str">{evil}</property> |
1881 | + </lsbrelease> |
1882 | + <packages> |
1883 | + <package id="0" name="{evil}"> |
1884 | + <property name="version" type="str">{evil}</property> |
1885 | + </package> |
1886 | + <package id="1" name="{evil}"> |
1887 | + <property name="version" type="str">{evil}</property> |
1888 | + </package> |
1889 | + </packages> |
1890 | + <requirements> |
1891 | + <requirement id=" 0" name="{evil}"> |
1892 | + <property name="link" type="str">{evil}</property> |
1893 | + </requirement> |
1894 | + <requirement id=" 1" name="{evil}"> |
1895 | + <property name="link" type="str">{evil}</property> |
1896 | + </requirement> |
1897 | + </requirements> |
1898 | + </software> |
1899 | + <summary> |
1900 | + <client name="{evil}" version="{evil}"/> |
1901 | + <date_created value="{evil}"/> |
1902 | + <architecture value="{evil}"/> |
1903 | + <distribution value="{evil}"/> |
1904 | + <distroseries value="{evil}"/> |
1905 | + <kernel-release value="{evil}"/> |
1906 | + <private value="False"/> |
1907 | + <contactable value="False"/> |
1908 | + <live_cd value="False"/> |
1909 | + <system_id value="{evil}"/> |
1910 | + </summary> |
1911 | +</system>""".format(evil=_escaped_evil_text) |
1912 | |
1913 | === modified file 'plainbox/plainbox/impl/exporter/text.py' |
1914 | --- plainbox/plainbox/impl/exporter/text.py 2015-03-31 08:38:35 +0000 |
1915 | +++ plainbox/plainbox/impl/exporter/text.py 2015-05-14 11:57:31 +0000 |
1916 | @@ -25,15 +25,15 @@ |
1917 | |
1918 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
1919 | """ |
1920 | +from plainbox.i18n import gettext as _ |
1921 | from plainbox.impl.commands.inv_run import Colorizer |
1922 | from plainbox.impl.exporter import SessionStateExporterBase |
1923 | from plainbox.impl.result import outcome_meta |
1924 | |
1925 | |
1926 | class TextSessionStateExporter(SessionStateExporterBase): |
1927 | - """ |
1928 | - Human-readable session state exporter. |
1929 | - """ |
1930 | + |
1931 | + """Human-readable session state exporter.""" |
1932 | |
1933 | def __init__(self, option_list=None, color=None): |
1934 | super().__init__(option_list) |
1935 | @@ -55,9 +55,23 @@ |
1936 | outcome_meta(state.result.outcome).color_ansi |
1937 | ), state.job.tr_summary(), |
1938 | ).encode("UTF-8")) |
1939 | + if len(state.result_history) > 1: |
1940 | + stream.write(_(" history: {0}\n").format( |
1941 | + ', '.join( |
1942 | + self.C.custom( |
1943 | + result.outcome_meta().tr_outcome, |
1944 | + result.outcome_meta().color_ansi) |
1945 | + for result in state.result_history) |
1946 | + ).encode("UTF-8")) |
1947 | else: |
1948 | stream.write( |
1949 | "{:^15}: {}\n".format( |
1950 | state.result.tr_outcome(), |
1951 | state.job.tr_summary(), |
1952 | ).encode("UTF-8")) |
1953 | + if state.result_history: |
1954 | + print(_("History:"), ', '.join( |
1955 | + self.C.custom( |
1956 | + result.outcome_meta().unicode_sigil, |
1957 | + result.outcome_meta().color_ansi) |
1958 | + for result in state.result_history)) |
1959 | |
1960 | === modified file 'plainbox/plainbox/impl/pod.py' |
1961 | --- plainbox/plainbox/impl/pod.py 2015-04-16 20:49:41 +0000 |
1962 | +++ plainbox/plainbox/impl/pod.py 2015-05-14 11:57:31 +0000 |
1963 | @@ -1,3 +1,4 @@ |
1964 | +# encoding: utf-8 |
1965 | # This file is part of Checkbox. |
1966 | # |
1967 | # Copyright 2012-2015 Canonical Ltd. |
1968 | @@ -75,7 +76,7 @@ |
1969 | |
1970 | class _Singleton: |
1971 | |
1972 | - """ A simple object()-like singleton that has a more useful repr(). """ |
1973 | + """A simple object()-like singleton that has a more useful repr().""" |
1974 | |
1975 | def __repr__(self): |
1976 | return self.__class__.__name__ |
1977 | @@ -202,7 +203,7 @@ |
1978 | |
1979 | def __init__(self, doc=None, type=None, initial=None, initial_fn=None, |
1980 | notify=False, notify_fn=None, assign_filter_list=None): |
1981 | - """ Initialize (define) a new POD field. """ |
1982 | + """Initialize (define) a new POD field.""" |
1983 | self.__doc__ = dedent(doc) if doc is not None else None |
1984 | self.type = type |
1985 | self.initial = initial |
1986 | @@ -224,13 +225,47 @@ |
1987 | self.counter = self.__class__._counter |
1988 | self.__class__._counter += 1 |
1989 | |
1990 | + @property |
1991 | + def change_notifier(self): |
1992 | + """ |
1993 | + Decorator for changing the change notification function. |
1994 | + |
1995 | + This decorator can be used to define all the fields in one block and |
1996 | + all the notification function in another block. It helps to make the |
1997 | + code easier to read. |
1998 | + |
1999 | + Example:: |
2000 | + |
2001 | + >>> class Person(POD): |
2002 | + ... name = Field() |
2003 | + ... |
2004 | + ... @name.change_notifier |
2005 | + ... def _name_changed(self, old, new): |
2006 | + ... print("changed from {!r} to {!r}".format(old, new)) |
2007 | + >>> person = Person() |
2008 | + changed from UNSET to None |
2009 | + >>> person.name = "bob" |
2010 | + changed from None to 'bob' |
2011 | + |
2012 | + .. note:: |
2013 | + Keep in mind that the decorated function is converted to a signal |
2014 | + automatically. The name of the function is also irrelevant, the POD |
2015 | + core automatically creates signals that have consistent names of |
2016 | + ``on_{field}_changed()``. |
2017 | + """ |
2018 | + def decorator(fn): |
2019 | + self.notify = True |
2020 | + self.notify_fn = fn |
2021 | + return fn |
2022 | + return decorator |
2023 | + |
2024 | def __repr__(self): |
2025 | - """ Get a debugging representation of a field. """ |
2026 | + """Get a debugging representation of a field.""" |
2027 | return "<{} name:{!r}>".format(self.__class__.__name__, self.name) |
2028 | |
2029 | @property |
2030 | def is_mandatory(self) -> bool: |
2031 | - """ Flag indicating if the field needs a mandatory initializer. """ |
2032 | + """Flag indicating if the field needs a mandatory initializer.""" |
2033 | return self.initial is MANDATORY |
2034 | |
2035 | def gain_name(self, name: str) -> None: |
2036 | @@ -328,7 +363,7 @@ |
2037 | @total_ordering |
2038 | class PODBase: |
2039 | |
2040 | - """ Base class for POD-like classes. """ |
2041 | + """Base class for POD-like classes.""" |
2042 | |
2043 | field_list = [] |
2044 | namedtuple_cls = namedtuple('PODBase', '') |
2045 | @@ -386,7 +421,7 @@ |
2046 | setattr(self, field.name, field_value) |
2047 | |
2048 | def __repr__(self): |
2049 | - """ Get a debugging representation of a POD object. """ |
2050 | + """Get a debugging representation of a POD object.""" |
2051 | return "{}({})".format( |
2052 | self.__class__.__name__, |
2053 | ', '.join([ |
2054 | @@ -428,10 +463,16 @@ |
2055 | ]) |
2056 | |
2057 | def as_dict(self) -> dict: |
2058 | - """ Return the data in this POD as a dictionary. """ |
2059 | + """ |
2060 | + Return the data in this POD as a dictionary. |
2061 | + |
2062 | + .. note:: |
2063 | + UNSET values are not added to the dictionary. |
2064 | + """ |
2065 | return { |
2066 | field.name: getattr(self, field.name) |
2067 | for field in self.__class__.field_list |
2068 | + if getattr(self, field.name) is not UNSET |
2069 | } |
2070 | |
2071 | |
2072 | @@ -457,7 +498,7 @@ |
2073 | self.field_origin_map = {} # field name -> defining class name |
2074 | |
2075 | def inspect_cls_for_decorator(self, cls: type) -> None: |
2076 | - """ Analyze a bare POD class. """ |
2077 | + """Analyze a bare POD class.""" |
2078 | self.inspect_base_classes(cls.__bases__) |
2079 | self.inspect_namespace(cls.__dict__, cls.__name__) |
2080 | |
2081 | @@ -727,6 +768,39 @@ |
2082 | typed = type_check_assign_filter |
2083 | |
2084 | |
2085 | +@modify_field_docstring( |
2086 | + "unset or type-checked (value must be of type {field.type.__name__})") |
2087 | +def unset_or_type_check_assign_filter( |
2088 | + instance: POD, field: Field, old: "Any", new: "Any") -> "Any": |
2089 | + """ |
2090 | + An assign filter that type-checks the value according to the field type. |
2091 | + |
2092 | + .. note:: |
2093 | + This filter allows (passes through) the special ``UNSET`` value as-is. |
2094 | + |
2095 | + The field must have a valid python type object stored in the .type field. |
2096 | + |
2097 | + :param instance: |
2098 | + A subclass of :class:`POD` that contains ``field`` |
2099 | + :param field: |
2100 | + The :class:`Field` being assigned to |
2101 | + :param old: |
2102 | + The current value of the field |
2103 | + :param new: |
2104 | + The proposed value of the field |
2105 | + :returns: |
2106 | + ``new``, as-is |
2107 | + :raises TypeError: |
2108 | + if ``new`` is not an instance of ``field.type`` |
2109 | + """ |
2110 | + if new is UNSET: |
2111 | + return new |
2112 | + return type_check_assign_filter(instance, field, old, new) |
2113 | + |
2114 | + |
2115 | +unset_or_typed = unset_or_type_check_assign_filter |
2116 | + |
2117 | + |
2118 | class sequence_type_check_assign_filter: |
2119 | |
2120 | """ |
2121 | @@ -781,6 +855,55 @@ |
2122 | typed.sequence = sequence_type_check_assign_filter |
2123 | |
2124 | |
2125 | +class unset_or_sequence_type_check_assign_filter(typed.sequence): |
2126 | + |
2127 | + """ |
2128 | + Assign filter for typed sequences. |
2129 | + |
2130 | + .. note:: |
2131 | + This filter allows (passes through) the special ``UNSET`` value as-is. |
2132 | + |
2133 | + An assign filter for typed sequences (lists or tuples) that must contain an |
2134 | + object of the given type. |
2135 | + """ |
2136 | + |
2137 | + @property |
2138 | + def field_docstring_ext(self) -> str: |
2139 | + return ( |
2140 | + "unset or type-checked sequence (items must be of type {})" |
2141 | + ).format(self.item_type.__name__) |
2142 | + |
2143 | + def __call__( |
2144 | + self, instance: POD, field: Field, old: "Any", new: "Any" |
2145 | + ) -> "Any": |
2146 | + """ |
2147 | + An assign filter that type-checks the value of all sequence elements. |
2148 | + |
2149 | + .. note:: |
2150 | + This filter allows (passes through) the special ``UNSET`` value |
2151 | + as-is. |
2152 | + |
2153 | + :param instance: |
2154 | + A subclass of :class:`POD` that contains ``field`` |
2155 | + :param field: |
2156 | + The :class:`Field` being assigned to |
2157 | + :param old: |
2158 | + The current value of the field |
2159 | + :param new: |
2160 | + The proposed value of the field |
2161 | + :returns: |
2162 | + ``new``, as-is |
2163 | + :raises TypeError: |
2164 | + if ``new`` is not an instance of ``field.type`` |
2165 | + """ |
2166 | + if new is UNSET: |
2167 | + return new |
2168 | + return super().__call__(instance, field, old, new) |
2169 | + |
2170 | + |
2171 | +unset_or_typed.sequence = unset_or_sequence_type_check_assign_filter |
2172 | + |
2173 | + |
2174 | @modify_field_docstring("unique elements (sequence elements cannot repeat)") |
2175 | def unique_elements_assign_filter( |
2176 | instance: POD, field: Field, old: "Any", new: "Any") -> "Any": |
2177 | |
2178 | === modified file 'plainbox/plainbox/impl/providers/special.py' |
2179 | --- plainbox/plainbox/impl/providers/special.py 2015-03-30 17:36:35 +0000 |
2180 | +++ plainbox/plainbox/impl/providers/special.py 2015-05-14 11:57:31 +0000 |
2181 | @@ -52,8 +52,8 @@ |
2182 | return stubbox_def |
2183 | |
2184 | |
2185 | -def get_stubbox(): |
2186 | - return Provider1.from_definition(get_stubbox_def(), secure=False) |
2187 | +def get_stubbox(**kwargs): |
2188 | + return Provider1.from_definition(get_stubbox_def(), secure=False, **kwargs) |
2189 | |
2190 | |
2191 | def get_categories_def(): |
2192 | |
2193 | === added file 'plainbox/plainbox/impl/providers/stubbox/units/jobs/representative.pxu' |
2194 | --- plainbox/plainbox/impl/providers/stubbox/units/jobs/representative.pxu 1970-01-01 00:00:00 +0000 |
2195 | +++ plainbox/plainbox/impl/providers/stubbox/units/jobs/representative.pxu 2015-05-14 11:57:31 +0000 |
2196 | @@ -0,0 +1,98 @@ |
2197 | +# Definitions of jobs that are useful for testing. Whenever you need a scenario |
2198 | +# for testing that involves realistic jobs and you don't want to painstakingly |
2199 | +# define them manually just load stubbox and get all jobs matching the pattern |
2200 | +# 'representative/plugin/.*' |
2201 | + |
2202 | +# NOTE:: all of the jobs below can be simplified to a template once static |
2203 | +# resources are available. |
2204 | + |
2205 | +id: representative/plugin/shell |
2206 | +_summary: Job with plugin=shell |
2207 | +_description: Job with plugin=shell |
2208 | +plugin: shell |
2209 | +flags: preserve-locale |
2210 | +command: true; |
2211 | +estimated_duration: 0.1 |
2212 | +category_id: plugin-representative |
2213 | + |
2214 | +id: representative/plugin/resource |
2215 | +_summary: Job with plugin=resource |
2216 | +_description: Job with plugin=resource |
2217 | +plugin: resource |
2218 | +flags: preserve-locale |
2219 | +command: |
2220 | + echo "key_a: value-a-1" |
2221 | + echo "key_b: value-b-1" |
2222 | + echo |
2223 | + echo "key_a: value-a-2" |
2224 | + echo "key_b: value-b-2" |
2225 | +estimated_duration: 0.1 |
2226 | +category_id: plugin-representative |
2227 | + |
2228 | +id: representative/plugin/local |
2229 | +_summary: Job with plugin=local |
2230 | +_description: Job with plugin=local |
2231 | +plugin: local |
2232 | +flags: preserve-locale |
2233 | +command: : |
2234 | +estimated_duration: 0.1 |
2235 | +category_id: plugin-representative |
2236 | + |
2237 | +id: representative/plugin/attachment |
2238 | +_summary: Job with plugin=attachment |
2239 | +_description: Job with plugin=attachment |
2240 | +plugin: attachment |
2241 | +flags: preserve-locale |
2242 | +command: |
2243 | + echo "Line 1" |
2244 | + echo "Line 2" |
2245 | + echo "Line 3 (last)" |
2246 | +estimated_duration: 0.1 |
2247 | +category_id: plugin-representative |
2248 | + |
2249 | +id: representative/plugin/user-interact |
2250 | +_summary: Job with plugin=user-interact |
2251 | +_description: Job with plugin=user-interact |
2252 | +plugin: user-interact |
2253 | +flags: preserve-locale |
2254 | +command: |
2255 | + echo "(interaction)" |
2256 | +estimated_duration: 30 |
2257 | +category_id: plugin-representative |
2258 | + |
2259 | +id: representative/plugin/user-verify |
2260 | +_summary: Job with plugin=user-verify |
2261 | +_description: Job with plugin=user-verify |
2262 | +plugin: user-verify |
2263 | +flags: preserve-locale |
2264 | +command: |
2265 | + echo "(verification)" |
2266 | +estimated_duration: 30 |
2267 | +category_id: plugin-representative |
2268 | + |
2269 | +id: representative/plugin/user-interact-verify |
2270 | +_summary: Job with plugin=user-interact-verify |
2271 | +_description: Job with plugin=user-interact-verify |
2272 | +plugin: user-interact-verify |
2273 | +flags: preserve-locale |
2274 | +command: |
2275 | + echo "(interaction)" |
2276 | + echo "(verification)" |
2277 | +estimated_duration: 30 |
2278 | +category_id: plugin-representative |
2279 | + |
2280 | +id: representative/plugin/manual |
2281 | +_summary: Job with plugin=manual |
2282 | +_description: Job with plugin=manual |
2283 | +plugin: manual |
2284 | +estimated_duration: 1 |
2285 | +category_id: plugin-representative |
2286 | + |
2287 | +id: representative/plugin/qml |
2288 | +_summary: Job with plugin=qml |
2289 | +_description: Job with plugin=qml |
2290 | +plugin: qml |
2291 | +qml_file: qml-simple.qml |
2292 | +flags: preserve-locale |
2293 | +estimated_duration: 10 |
2294 | +category_id: plugin-representative |
2295 | |
2296 | === modified file 'plainbox/plainbox/impl/resource.py' |
2297 | --- plainbox/plainbox/impl/resource.py 2014-09-13 11:37:17 +0000 |
2298 | +++ plainbox/plainbox/impl/resource.py 2015-05-14 11:57:31 +0000 |
2299 | @@ -88,6 +88,10 @@ |
2300 | data = {} |
2301 | object.__setattr__(self, '_data', data) |
2302 | |
2303 | + def __iter__(self): |
2304 | + data = object.__getattribute__(self, '_data') |
2305 | + return iter(data) |
2306 | + |
2307 | def __setattr__(self, attr, value): |
2308 | if attr.startswith("_"): |
2309 | raise AttributeError(attr) |
2310 | |
2311 | === modified file 'plainbox/plainbox/impl/result.py' |
2312 | --- plainbox/plainbox/impl/result.py 2015-04-16 20:49:41 +0000 |
2313 | +++ plainbox/plainbox/impl/result.py 2015-05-14 11:57:31 +0000 |
2314 | @@ -1,13 +1,13 @@ |
2315 | +# encoding: utf-8 |
2316 | # This file is part of Checkbox. |
2317 | # |
2318 | -# Copyright 2012 Canonical Ltd. |
2319 | +# Copyright 2012-2015 Canonical Ltd. |
2320 | # Written by: |
2321 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2322 | # |
2323 | # Checkbox is free software: you can redistribute it and/or modify |
2324 | # it under the terms of the GNU General Public License version 3, |
2325 | # as published by the Free Software Foundation. |
2326 | - |
2327 | # |
2328 | # Checkbox is distributed in the hope that it will be useful, |
2329 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
2330 | @@ -18,6 +18,8 @@ |
2331 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2332 | |
2333 | """ |
2334 | +Implementation of job result (test result) classes. |
2335 | + |
2336 | :mod:`plainbox.impl.result` -- job result |
2337 | ========================================= |
2338 | |
2339 | @@ -25,19 +27,21 @@ |
2340 | :class:`MemoryJobResult` and :class:`DiskJobResult`. |
2341 | """ |
2342 | |
2343 | -from collections import namedtuple |
2344 | import base64 |
2345 | import codecs |
2346 | import gzip |
2347 | +import inspect |
2348 | import io |
2349 | import json |
2350 | import logging |
2351 | -import inspect |
2352 | import re |
2353 | +from collections import namedtuple |
2354 | |
2355 | from plainbox.abc import IJobResult |
2356 | from plainbox.i18n import gettext as _ |
2357 | from plainbox.i18n import pgettext as C_ |
2358 | +from plainbox.impl import pod |
2359 | +from plainbox.impl.decorators import raises |
2360 | from plainbox.vendor import morris |
2361 | |
2362 | logger = logging.getLogger("plainbox.result") |
2363 | @@ -209,34 +213,82 @@ |
2364 | |
2365 | |
2366 | def tr_outcome(outcome): |
2367 | - """ |
2368 | - Get the translated value of ``OUTCOME_`` constant |
2369 | - """ |
2370 | + """Get the translated value of ``OUTCOME_`` constant.""" |
2371 | return OUTCOME_METADATA_MAP[outcome].tr_outcome |
2372 | |
2373 | |
2374 | def outcome_color_hex(outcome): |
2375 | - """ |
2376 | - Get the hexadecimal "#RRGGBB" color that represents this outcome |
2377 | - """ |
2378 | + """Get the hexadecimal "#RRGGBB" color that represents this outcome.""" |
2379 | return OUTCOME_METADATA_MAP[outcome].color_hex |
2380 | |
2381 | |
2382 | def outcome_color_ansi(outcome): |
2383 | - """ |
2384 | - Get an ANSI escape sequence that represents this outcome |
2385 | - """ |
2386 | + """Get an ANSI escape sequence that represents this outcome.""" |
2387 | return OUTCOME_METADATA_MAP[outcome].color_ansi |
2388 | |
2389 | |
2390 | def outcome_meta(outcome): |
2391 | - """ |
2392 | - Get the OutcomeMetadata object associated with this outcome. |
2393 | - """ |
2394 | + """Get the OutcomeMetadata object associated with this outcome.""" |
2395 | return OUTCOME_METADATA_MAP[outcome] |
2396 | |
2397 | |
2398 | +class JobResultBuilder(pod.POD): |
2399 | + |
2400 | + """A builder for job result objects.""" |
2401 | + |
2402 | + outcome = pod.Field( |
2403 | + 'outcome of a test', |
2404 | + str, pod.UNSET, assign_filter_list=[pod.unset_or_typed]) |
2405 | + execution_duration = pod.Field( |
2406 | + 'time of test execution', |
2407 | + float, pod.UNSET, assign_filter_list=[pod.unset_or_typed]) |
2408 | + comments = pod.Field( |
2409 | + 'comments from the test operator', |
2410 | + str, pod.UNSET, assign_filter_list=[pod.unset_or_typed]) |
2411 | + return_code = pod.Field( |
2412 | + 'return code from the (optional) test process', |
2413 | + int, pod.UNSET, assign_filter_list=[pod.unset_or_typed]) |
2414 | + io_log = pod.Field( |
2415 | + 'history of the I/O log of the (optional) test process', |
2416 | + list, pod.UNSET, assign_filter_list=[ |
2417 | + pod.unset_or_typed, pod.unset_or_typed.sequence(tuple)]) |
2418 | + io_log_filename = pod.Field( |
2419 | + 'path to a structured I/O log file of the (optional) test process', |
2420 | + str, pod.UNSET, assign_filter_list=[pod.unset_or_typed]) |
2421 | + |
2422 | + def add_comment(self, comment): |
2423 | + """ |
2424 | + Add a new comment. |
2425 | + |
2426 | + The comment is safely combined with any prior comments. |
2427 | + """ |
2428 | + if self.comments is pod.UNSET: |
2429 | + self.comments = comments |
2430 | + else: |
2431 | + self.comments += '\n' + comment |
2432 | + |
2433 | + @raises(ValueError) |
2434 | + def seal(self): |
2435 | + """ |
2436 | + Seal the current state of the builder and create a new result. |
2437 | + |
2438 | + :returns: |
2439 | + A new MemoryJobResult or DiskJobResult with all the data |
2440 | + :raises ValueError: |
2441 | + If both io_log and io_log_filename were used. |
2442 | + """ |
2443 | + if not (self.io_log_filename is pod.UNSET or self.io_log is pod.UNSET): |
2444 | + raise ValueError( |
2445 | + "you can use only io_log or io_log_filename at a time") |
2446 | + if self.io_log_filename is not pod.UNSET: |
2447 | + cls = DiskJobResult |
2448 | + else: |
2449 | + cls = MemoryJobResult |
2450 | + return cls(self.as_dict()) |
2451 | + |
2452 | + |
2453 | class _JobResultBase(IJobResult): |
2454 | + |
2455 | """ |
2456 | Base class for :`IJobResult` implementations. |
2457 | |
2458 | @@ -245,7 +297,7 @@ |
2459 | |
2460 | def __init__(self, data): |
2461 | """ |
2462 | - Initialize a new result with the specified data |
2463 | + Initialize a new result with the specified data. |
2464 | |
2465 | Data is a dictionary that can hold arbitrary values. At least some |
2466 | values are explicitly used, such as 'outcome', 'comments' and |
2467 | @@ -259,6 +311,10 @@ |
2468 | key: value for key, value in data.items() |
2469 | if value is not None and value != []} |
2470 | |
2471 | + def unseal(self): |
2472 | + """Create a new job result builder from the data in this result.""" |
2473 | + return JobResultBuilder(**self._data) |
2474 | + |
2475 | def __eq__(self, other): |
2476 | if not isinstance(other, _JobResultBase): |
2477 | return NotImplemented |
2478 | @@ -275,9 +331,7 @@ |
2479 | |
2480 | @morris.signal |
2481 | def on_outcome_changed(self, old, new): |
2482 | - """ |
2483 | - Signal sent when ``outcome`` property value is changed |
2484 | - """ |
2485 | + """Signal sent when ``outcome`` property value is changed.""" |
2486 | |
2487 | @property |
2488 | def outcome(self): |
2489 | @@ -292,55 +346,46 @@ |
2490 | |
2491 | @outcome.setter |
2492 | def outcome(self, new): |
2493 | + logger.warning("writing to result.outcome is deprecated!") |
2494 | old = self.outcome |
2495 | if old != new: |
2496 | self._data['outcome'] = new |
2497 | self.on_outcome_changed(old, new) |
2498 | |
2499 | def tr_outcome(self): |
2500 | - """ |
2501 | - Get the translated value of the outcome |
2502 | - """ |
2503 | + """Get the translated value of the outcome.""" |
2504 | return tr_outcome(self.outcome) |
2505 | |
2506 | def outcome_color_hex(self): |
2507 | - """ |
2508 | - Get the hexadecimal "#RRGGBB" color that represents this outcome |
2509 | - """ |
2510 | + """Get the hexadecimal "#RRGGBB" color that represents this outcome.""" |
2511 | return outcome_color_hex(self.outcome) |
2512 | |
2513 | def outcome_color_ansi(self): |
2514 | - """ |
2515 | - Get an ANSI escape sequence that represents this outcome |
2516 | - """ |
2517 | + """Get an ANSI escape sequence that represents this outcome.""" |
2518 | return outcome_color_ansi(self.outcome) |
2519 | |
2520 | def outcome_meta(self): |
2521 | - """ |
2522 | - Get the OutcomeMetadata object associated with this outcome. |
2523 | - """ |
2524 | + """Get the OutcomeMetadata object associated with this outcome.""" |
2525 | return outcome_meta(self.outcome) |
2526 | |
2527 | @property |
2528 | def execution_duration(self): |
2529 | - """ |
2530 | - The amount of time in seconds it took to run this job. |
2531 | - """ |
2532 | + """The amount of time in seconds it took to run this job.""" |
2533 | return self._data.get('execution_duration') |
2534 | |
2535 | @execution_duration.setter |
2536 | def execution_duration(self, elapsed): |
2537 | + logger.warning("writing to result.execution_duration is deprecated!") |
2538 | self._data['execution_duration'] = elapsed |
2539 | |
2540 | @property |
2541 | def comments(self): |
2542 | - """ |
2543 | - comments of the test operator |
2544 | - """ |
2545 | + """Get the comments of the test operator.""" |
2546 | return self._data.get('comments') |
2547 | |
2548 | @comments.setter |
2549 | def comments(self, new): |
2550 | + logger.warning("writing to result.comments is deprecated!") |
2551 | old = self.comments |
2552 | if old != new: |
2553 | self._data['comments'] = new |
2554 | @@ -348,7 +393,7 @@ |
2555 | |
2556 | def append_comments(self, comments): |
2557 | """ |
2558 | - Append new comments to the result |
2559 | + Append new comments to the result. |
2560 | |
2561 | :param comments: |
2562 | The comments to append |
2563 | @@ -365,15 +410,11 @@ |
2564 | |
2565 | @morris.signal |
2566 | def on_comments_changed(self, old, new): |
2567 | - """ |
2568 | - Signal sent when ``comments`` property value is changed |
2569 | - """ |
2570 | + """Signal sent when ``comments`` property value is changed.""" |
2571 | |
2572 | @property |
2573 | def return_code(self): |
2574 | - """ |
2575 | - return code of the command associated with the job, if any |
2576 | - """ |
2577 | + """return code of the command associated with the job, if any.""" |
2578 | return self._data.get('return_code') |
2579 | |
2580 | @property |
2581 | @@ -383,6 +424,8 @@ |
2582 | @property |
2583 | def io_log_as_flat_text(self): |
2584 | """ |
2585 | + Perform a lossly conversion from the binary I/O log to text. |
2586 | + |
2587 | Convert the I/O log to a text string, replacing non unicode characters |
2588 | with U+FFFD, the REPLACEMENT CHARACTER. |
2589 | |
2590 | @@ -413,6 +456,8 @@ |
2591 | @property |
2592 | def io_log_as_text_attachment(self): |
2593 | """ |
2594 | + Perform a conversion of the binary I/O log to text, if possible. |
2595 | + |
2596 | Convert the I/O log to text attachment, if possible, otherwise return |
2597 | an empty string. |
2598 | |
2599 | @@ -437,7 +482,7 @@ |
2600 | @property |
2601 | def is_hollow(self): |
2602 | """ |
2603 | - flag that indicates if the result is hollow |
2604 | + flag that indicates if the result is hollow. |
2605 | |
2606 | Hollow results may have been created but hold no data at all. |
2607 | Hollow results are also tentatively deprecated, once we have some |
2608 | @@ -451,6 +496,7 @@ |
2609 | |
2610 | |
2611 | class MemoryJobResult(_JobResultBase): |
2612 | + |
2613 | """ |
2614 | A :class:`IJobResult` that keeps IO logs in memory. |
2615 | |
2616 | @@ -472,8 +518,9 @@ |
2617 | |
2618 | |
2619 | class GzipFile(gzip.GzipFile): |
2620 | + |
2621 | """ |
2622 | - Subclass of GzipFile that works around missing read1() on python3.2 |
2623 | + Subclass of GzipFile that works around missing read1() on python3.2. |
2624 | |
2625 | See: http://bugs.python.org/issue10791 |
2626 | """ |
2627 | @@ -483,6 +530,7 @@ |
2628 | |
2629 | |
2630 | class DiskJobResult(_JobResultBase): |
2631 | + |
2632 | """ |
2633 | A :class:`IJobResult` that keeps IO logs on disk. |
2634 | |
2635 | @@ -495,9 +543,7 @@ |
2636 | |
2637 | @property |
2638 | def io_log_filename(self): |
2639 | - """ |
2640 | - pathname of the file containing serialized IO log records |
2641 | - """ |
2642 | + """pathname of the file containing serialized IO log records.""" |
2643 | return self._data.get("io_log_filename") |
2644 | |
2645 | def get_io_log(self): |
2646 | @@ -519,9 +565,8 @@ |
2647 | |
2648 | |
2649 | class IOLogRecordWriter: |
2650 | - """ |
2651 | - Class for writing :class:`IOLogRecord` instances to a text stream |
2652 | - """ |
2653 | + |
2654 | + """Class for writing :class:`IOLogRecord` instances to a text stream.""" |
2655 | |
2656 | def __init__(self, stream): |
2657 | self.stream = stream |
2658 | @@ -530,9 +575,7 @@ |
2659 | self.stream.close() |
2660 | |
2661 | def write_record(self, record): |
2662 | - """ |
2663 | - Write an :class:`IOLogRecord` to the stream. |
2664 | - """ |
2665 | + """Write an :class:`IOLogRecord` to the stream.""" |
2666 | text = json.dumps([ |
2667 | record[0], record[1], |
2668 | base64.standard_b64encode(record[2]).decode("ASCII")], |
2669 | @@ -545,9 +588,8 @@ |
2670 | |
2671 | |
2672 | class IOLogRecordReader: |
2673 | - """ |
2674 | - Class for streaming :class`IOLogRecord` instances from a text stream |
2675 | - """ |
2676 | + |
2677 | + """Class for streaming :class`IOLogRecord` instances from a text stream.""" |
2678 | |
2679 | def __init__(self, stream): |
2680 | self.stream = stream |
2681 | @@ -572,8 +614,9 @@ |
2682 | |
2683 | def __iter__(self): |
2684 | """ |
2685 | - Iterate over the entire stream generating subsequent |
2686 | - :class:`IOLogRecord` entries. |
2687 | + Iterate over the entire stream generating subsequent records. |
2688 | + |
2689 | + This method generates subsequent :class:`IOLogRecord` entries. |
2690 | """ |
2691 | while True: |
2692 | record = self.read_record() |
2693 | |
2694 | === modified file 'plainbox/plainbox/impl/runner.py' |
2695 | --- plainbox/plainbox/impl/runner.py 2015-04-16 20:49:41 +0000 |
2696 | +++ plainbox/plainbox/impl/runner.py 2015-05-14 11:57:31 +0000 |
2697 | @@ -1,13 +1,13 @@ |
2698 | +# encoding: utf-8 |
2699 | # This file is part of Checkbox. |
2700 | # |
2701 | -# Copyright 2012, 2013 Canonical Ltd. |
2702 | +# Copyright 2012-2015 Canonical Ltd. |
2703 | # Written by: |
2704 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
2705 | # |
2706 | # Checkbox is free software: you can redistribute it and/or modify |
2707 | # it under the terms of the GNU General Public License version 3, |
2708 | # as published by the Free Software Foundation. |
2709 | - |
2710 | # |
2711 | # Checkbox is distributed in the hope that it will be useful, |
2712 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
2713 | @@ -18,6 +18,8 @@ |
2714 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2715 | |
2716 | """ |
2717 | +Definition of JobRunner class. |
2718 | + |
2719 | :mod:`plainbox.impl.runner` -- job runner |
2720 | ========================================= |
2721 | |
2722 | @@ -36,14 +38,12 @@ |
2723 | import sys |
2724 | import time |
2725 | |
2726 | -from plainbox.vendor import extcmd |
2727 | - |
2728 | -from plainbox.abc import IJobRunner, IJobResult |
2729 | +from plainbox.abc import IJobResult, IJobRunner |
2730 | from plainbox.i18n import gettext as _ |
2731 | -from plainbox.impl.result import DiskJobResult |
2732 | from plainbox.impl.result import IOLogRecord |
2733 | from plainbox.impl.result import IOLogRecordWriter |
2734 | -from plainbox.impl.result import MemoryJobResult |
2735 | +from plainbox.impl.result import JobResultBuilder |
2736 | +from plainbox.vendor import extcmd |
2737 | from plainbox.vendor import morris |
2738 | |
2739 | |
2740 | @@ -51,23 +51,19 @@ |
2741 | |
2742 | |
2743 | def slugify(_string): |
2744 | - """ |
2745 | - Slugify - like Django does for URL - transform a random string to a valid |
2746 | - slug that can be later used in filenames |
2747 | - """ |
2748 | + """Transform any string to onet that can be used in filenames.""" |
2749 | valid_chars = frozenset( |
2750 | "-_.{}{}".format(string.ascii_letters, string.digits)) |
2751 | return ''.join(c if c in valid_chars else '_' for c in _string) |
2752 | |
2753 | |
2754 | class IOLogRecordGenerator(extcmd.DelegateBase): |
2755 | - """ |
2756 | - Delegate for extcmd that generates io_log entries. |
2757 | - """ |
2758 | + |
2759 | + """Delegate for extcmd that generates io_log entries.""" |
2760 | |
2761 | def on_begin(self, args, kwargs): |
2762 | """ |
2763 | - Internal method of extcmd.DelegateBase |
2764 | + Internal method of extcmd.DelegateBase. |
2765 | |
2766 | Called when a command is being invoked. |
2767 | |
2768 | @@ -77,7 +73,7 @@ |
2769 | |
2770 | def on_line(self, stream_name, line): |
2771 | """ |
2772 | - Internal method of extcmd.DelegateBase |
2773 | + Internal method of extcmd.DelegateBase. |
2774 | |
2775 | Creates a new IOLogRecord and passes it to :meth:`on_new_record()`. |
2776 | Maintains a timestamp of the last message so that approximate delay |
2777 | @@ -92,7 +88,7 @@ |
2778 | @morris.signal |
2779 | def on_new_record(self, record): |
2780 | """ |
2781 | - Internal signal method of :class:`IOLogRecordGenerator` |
2782 | + Internal signal method of :class:`IOLogRecordGenerator`. |
2783 | |
2784 | Called when a new record is generated and needs to be processed. |
2785 | """ |
2786 | @@ -101,6 +97,7 @@ |
2787 | |
2788 | |
2789 | class CommandOutputWriter(extcmd.DelegateBase): |
2790 | + |
2791 | """ |
2792 | Delegate for extcmd that writes output to a file on disk. |
2793 | |
2794 | @@ -120,7 +117,7 @@ |
2795 | |
2796 | def on_begin(self, args, kwargs): |
2797 | """ |
2798 | - Internal method of extcmd.DelegateBase |
2799 | + Internal method of extcmd.DelegateBase. |
2800 | |
2801 | Called when a command is being invoked |
2802 | """ |
2803 | @@ -129,7 +126,7 @@ |
2804 | |
2805 | def on_end(self, returncode): |
2806 | """ |
2807 | - Internal method of extcmd.DelegateBase |
2808 | + Internal method of extcmd.DelegateBase. |
2809 | |
2810 | Called when a command finishes running |
2811 | """ |
2812 | @@ -138,7 +135,7 @@ |
2813 | |
2814 | def on_abnormal_end(self, signal_num): |
2815 | """ |
2816 | - Internal method of extcmd.DelegateBase |
2817 | + Internal method of extcmd.DelegateBase. |
2818 | |
2819 | Called when a command abnormally finishes running |
2820 | """ |
2821 | @@ -147,7 +144,7 @@ |
2822 | |
2823 | def on_line(self, stream_name, line): |
2824 | """ |
2825 | - Internal method of extcmd.DelegateBase |
2826 | + Internal method of extcmd.DelegateBase. |
2827 | |
2828 | Called for each line of output. |
2829 | """ |
2830 | @@ -158,6 +155,7 @@ |
2831 | |
2832 | |
2833 | class FallbackCommandOutputPrinter(extcmd.DelegateBase): |
2834 | + |
2835 | """ |
2836 | Delegate for extcmd that prints all output to stdout. |
2837 | |
2838 | @@ -166,11 +164,19 @@ |
2839 | """ |
2840 | |
2841 | def __init__(self, prompt): |
2842 | + """Initialize a new fallback command output printer.""" |
2843 | self._prompt = prompt |
2844 | self._lineno = collections.defaultdict(int) |
2845 | self._abort = False |
2846 | |
2847 | def on_line(self, stream_name, line): |
2848 | + """ |
2849 | + Internal method of extcmd.DelegateBase. |
2850 | + |
2851 | + Called for each line of output. Normally each line is just printed |
2852 | + (assuming UTF-8 encoding) If decoding fails for any reason that and all |
2853 | + subsequent lines are ignored. |
2854 | + """ |
2855 | if self._abort: |
2856 | return |
2857 | self._lineno[stream_name] += 1 |
2858 | @@ -183,8 +189,9 @@ |
2859 | |
2860 | |
2861 | class JobRunnerUIDelegate(extcmd.DelegateBase): |
2862 | + |
2863 | """ |
2864 | - Delegate for extcmd that delegates extcmd events to IJobRunnerUI |
2865 | + Delegate for extcmd that delegates extcmd events to IJobRunnerUI. |
2866 | |
2867 | The file itself is only opened once on_begin() gets called by extcmd. This |
2868 | makes it safe to instantiate this without worrying about dangling |
2869 | @@ -197,7 +204,7 @@ |
2870 | |
2871 | def __init__(self, ui=None): |
2872 | """ |
2873 | - Initialize the JobRunnerUIDelegate |
2874 | + Initialize the JobRunnerUIDelegate. |
2875 | |
2876 | :param ui: |
2877 | (optional) an instnace of IJobRunnerUI to delegate events to |
2878 | @@ -206,7 +213,7 @@ |
2879 | |
2880 | def on_begin(self, args, kwargs): |
2881 | """ |
2882 | - Internal method of extcmd.DelegateBase |
2883 | + Internal method of extcmd.DelegateBase. |
2884 | |
2885 | Called when a command is being invoked |
2886 | """ |
2887 | @@ -215,7 +222,7 @@ |
2888 | |
2889 | def on_end(self, returncode): |
2890 | """ |
2891 | - Internal method of extcmd.DelegateBase |
2892 | + Internal method of extcmd.DelegateBase. |
2893 | |
2894 | Called when a command finishes running |
2895 | """ |
2896 | @@ -224,7 +231,7 @@ |
2897 | |
2898 | def on_abnormal_end(self, signal_num): |
2899 | """ |
2900 | - Internal method of extcmd.DelegateBase |
2901 | + Internal method of extcmd.DelegateBase. |
2902 | |
2903 | Called when a command abnormally finishes running |
2904 | |
2905 | @@ -236,7 +243,7 @@ |
2906 | |
2907 | def on_line(self, stream_name, line): |
2908 | """ |
2909 | - Internal method of extcmd.DelegateBase |
2910 | + Internal method of extcmd.DelegateBase. |
2911 | |
2912 | Called for each line of output. |
2913 | """ |
2914 | @@ -245,7 +252,7 @@ |
2915 | |
2916 | def on_chunk(self, stream_name, chunk): |
2917 | """ |
2918 | - Internal method of extcmd.DelegateBase |
2919 | + Internal method of extcmd.DelegateBase. |
2920 | |
2921 | Called for each chunk of output. |
2922 | """ |
2923 | @@ -254,8 +261,9 @@ |
2924 | |
2925 | |
2926 | class JobRunner(IJobRunner): |
2927 | + |
2928 | """ |
2929 | - Runner for jobs - executes jobs and produces results |
2930 | + Runner for jobs - executes jobs and produces results. |
2931 | |
2932 | The runner is somewhat de-coupled from jobs and session. It still carries |
2933 | all checkbox-specific logic about the various types of plugins. |
2934 | @@ -333,7 +341,7 @@ |
2935 | @property |
2936 | def log_leftovers(self): |
2937 | """ |
2938 | - flag controlling if leftover files should be logged |
2939 | + flag controlling if leftover files should be logged. |
2940 | |
2941 | If you wish to connect a custom handler to :meth:`on_leftover_files()` |
2942 | then it is advisable to set this property to False so that leftover |
2943 | @@ -345,6 +353,7 @@ |
2944 | |
2945 | @log_leftovers.setter |
2946 | def log_leftovers(self, value): |
2947 | + """setter for log_leftovers property.""" |
2948 | self._log_leftovers = value |
2949 | |
2950 | def get_warm_up_sequence(self, job_list): |
2951 | @@ -375,7 +384,7 @@ |
2952 | |
2953 | def run_job(self, job, job_state, config=None, ui=None): |
2954 | """ |
2955 | - Run the specified job an return the result |
2956 | + Run the specified job an return the result. |
2957 | |
2958 | :param job: |
2959 | A JobDefinition to run |
2960 | @@ -412,10 +421,10 @@ |
2961 | try: |
2962 | runner = getattr(self, func_name) |
2963 | except AttributeError: |
2964 | - return MemoryJobResult({ |
2965 | - 'outcome': IJobResult.OUTCOME_NOT_IMPLEMENTED, |
2966 | - 'comment': _('This type of job is not supported'), |
2967 | - }) |
2968 | + return JobResultBuilder( |
2969 | + outcome=IJobResult.OUTCOME_NOT_IMPLEMENTED, |
2970 | + comments=_('This type of job is not supported') |
2971 | + ).seal() |
2972 | else: |
2973 | if self._dry_run and job.plugin not in self._DRY_RUN_PLUGINS: |
2974 | return self._get_dry_run_result(job) |
2975 | @@ -428,7 +437,7 @@ |
2976 | |
2977 | def run_shell_job(self, job, job_state, config): |
2978 | """ |
2979 | - Method called to run a job with plugin field equal to 'shell' |
2980 | + Method called to run a job with plugin field equal to 'shell'. |
2981 | |
2982 | The 'shell' job implements the following scenario: |
2983 | |
2984 | @@ -448,11 +457,11 @@ |
2985 | if job.plugin != "shell": |
2986 | # TRANSLATORS: please keep 'plugin' untranslated |
2987 | raise ValueError(_("bad job plugin value")) |
2988 | - return self._just_run_command(job, job_state, config) |
2989 | + return self._just_run_command(job, job_state, config).seal() |
2990 | |
2991 | def run_attachment_job(self, job, job_state, config): |
2992 | """ |
2993 | - Method called to run a job with plugin field equal to 'attachment' |
2994 | + Method called to run a job with plugin field equal to 'attachment'. |
2995 | |
2996 | The 'attachment' job implements the following scenario: |
2997 | |
2998 | @@ -473,11 +482,11 @@ |
2999 | if job.plugin != "attachment": |
3000 | # TRANSLATORS: please keep 'plugin' untranslated |
3001 | raise ValueError(_("bad job plugin value")) |
3002 | - return self._just_run_command(job, job_state, config) |
3003 | + return self._just_run_command(job, job_state, config).seal() |
3004 | |
3005 | def run_resource_job(self, job, job_state, config): |
3006 | """ |
3007 | - Method called to run a job with plugin field equal to 'resource' |
3008 | + Method called to run a job with plugin field equal to 'resource'. |
3009 | |
3010 | The 'resource' job implements the following scenario: |
3011 | |
3012 | @@ -499,11 +508,11 @@ |
3013 | if job.plugin != "resource": |
3014 | # TRANSLATORS: please keep 'plugin' untranslated |
3015 | raise ValueError(_("bad job plugin value")) |
3016 | - return self._just_run_command(job, job_state, config) |
3017 | + return self._just_run_command(job, job_state, config).seal() |
3018 | |
3019 | def run_local_job(self, job, job_state, config): |
3020 | """ |
3021 | - Method called to run a job with plugin field equal to 'local' |
3022 | + Method called to run a job with plugin field equal to 'local'. |
3023 | |
3024 | The 'local' job implements the following scenario: |
3025 | |
3026 | @@ -525,11 +534,11 @@ |
3027 | if job.plugin != "local": |
3028 | # TRANSLATORS: please keep 'plugin' untranslated |
3029 | raise ValueError(_("bad job plugin value")) |
3030 | - return self._just_run_command(job, job_state, config) |
3031 | + return self._just_run_command(job, job_state, config).seal() |
3032 | |
3033 | def run_manual_job(self, job, job_state, config): |
3034 | """ |
3035 | - Method called to run a job with plugin field equal to 'manual' |
3036 | + Method called to run a job with plugin field equal to 'manual'. |
3037 | |
3038 | The 'manual' job implements the following scenario: |
3039 | |
3040 | @@ -551,11 +560,11 @@ |
3041 | if job.plugin != "manual": |
3042 | # TRANSLATORS: please keep 'plugin' untranslated |
3043 | raise ValueError(_("bad job plugin value")) |
3044 | - return MemoryJobResult({'outcome': IJobResult.OUTCOME_UNDECIDED}) |
3045 | + return JobResultBuilder(outcome=IJobResult.OUTCOME_UNDECIDED).seal() |
3046 | |
3047 | def run_user_interact_job(self, job, job_state, config): |
3048 | """ |
3049 | - Method called to run a job with plugin field equal to 'user-interact' |
3050 | + Method called to run a job with plugin field equal to 'user-interact'. |
3051 | |
3052 | The 'user-interact' job implements the following scenario: |
3053 | |
3054 | @@ -592,11 +601,11 @@ |
3055 | if job.plugin != "user-interact": |
3056 | # TRANSLATORS: please keep 'plugin' untranslated |
3057 | raise ValueError(_("bad job plugin value")) |
3058 | - return self._just_run_command(job, job_state, config) |
3059 | + return self._just_run_command(job, job_state, config).seal() |
3060 | |
3061 | def run_user_verify_job(self, job, job_state, config): |
3062 | """ |
3063 | - Method called to run a job with plugin field equal to 'user-verify' |
3064 | + Method called to run a job with plugin field equal to 'user-verify'. |
3065 | |
3066 | The 'user-verify' job implements the following scenario: |
3067 | |
3068 | @@ -637,15 +646,14 @@ |
3069 | # TRANSLATORS: please keep 'plugin' untranslated |
3070 | raise ValueError(_("bad job plugin value")) |
3071 | # Run the command |
3072 | - result_cmd = self._just_run_command(job, job_state, config) |
3073 | + result_builder = self._just_run_command(job, job_state, config) |
3074 | # Maybe ask the user |
3075 | - result_cmd.outcome = IJobResult.OUTCOME_UNDECIDED |
3076 | - return result_cmd |
3077 | + result_builder.outcome = IJobResult.OUTCOME_UNDECIDED |
3078 | + return result_builder.seal() |
3079 | |
3080 | def run_user_interact_verify_job(self, job, job_state, config): |
3081 | """ |
3082 | - Method called to run a job with plugin field equal to |
3083 | - 'user-interact-verify' |
3084 | + Method for running jobs with plugin equal to 'user-interact-verify'. |
3085 | |
3086 | The 'user-interact-verify' job implements the following scenario: |
3087 | |
3088 | @@ -686,14 +694,14 @@ |
3089 | # TRANSLATORS: please keep 'plugin' untranslated |
3090 | raise ValueError(_("bad job plugin value")) |
3091 | # Run the command |
3092 | - result_cmd = self._just_run_command(job, job_state, config) |
3093 | + result_builder = self._just_run_command(job, job_state, config) |
3094 | # Maybe ask the user |
3095 | - result_cmd.outcome = IJobResult.OUTCOME_UNDECIDED |
3096 | - return result_cmd |
3097 | + result_builder.outcome = IJobResult.OUTCOME_UNDECIDED |
3098 | + return result_builder.seal() |
3099 | |
3100 | def run_qml_job(self, job, job_state, config): |
3101 | """ |
3102 | - Method called to run a job with plugin field equal to 'qml' |
3103 | + Method called to run a job with plugin field equal to 'qml'. |
3104 | |
3105 | The 'qml' job implements the following scenario: |
3106 | |
3107 | @@ -713,10 +721,10 @@ |
3108 | try: |
3109 | ctrl = self._get_ctrl_for_job(job) |
3110 | except LookupError: |
3111 | - return MemoryJobResult({ |
3112 | - 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED, |
3113 | - 'comment': _('No suitable execution controller is available)'), |
3114 | - }) |
3115 | + return JobResultBuilder( |
3116 | + outcome=IJobResult.OUTCOME_NOT_SUPPORTED, |
3117 | + comments=_('No suitable execution controller is available)') |
3118 | + ).seal() |
3119 | # Run the embedded command |
3120 | start_time = time.time() |
3121 | delegate, io_log_gen = self._prepare_io_handling(job, config) |
3122 | @@ -755,12 +763,12 @@ |
3123 | else: |
3124 | outcome = IJobResult.OUTCOME_FAIL |
3125 | # Create a result object and return it |
3126 | - return DiskJobResult({ |
3127 | - 'outcome': outcome, |
3128 | - 'return_code': return_code, |
3129 | - 'io_log_filename': record_path, |
3130 | - 'execution_duration': execution_duration |
3131 | - }) |
3132 | + return JobResultBuilder( |
3133 | + outcome=outcome, |
3134 | + return_code=return_code, |
3135 | + io_log_filename=record_path, |
3136 | + execution_duration=execution_duration |
3137 | + ).seal() |
3138 | |
3139 | def _get_dry_run_result(self, job): |
3140 | """ |
3141 | @@ -769,26 +777,24 @@ |
3142 | Returns a result that is used when running in dry-run mode (where we |
3143 | don't really test anything) |
3144 | """ |
3145 | - return MemoryJobResult({ |
3146 | - 'outcome': IJobResult.OUTCOME_SKIP, |
3147 | - 'comments': _("Job skipped in dry-run mode") |
3148 | - }) |
3149 | + return JobResultBuilder( |
3150 | + outcome=IJobResult.OUTCOME_SKIP, |
3151 | + comments=_("Job skipped in dry-run mode") |
3152 | + ).seal() |
3153 | |
3154 | def _just_run_command(self, job, job_state, config): |
3155 | """ |
3156 | Internal method of JobRunner. |
3157 | |
3158 | - Runs the command embedded in the job and returns the DiskJobResult that |
3159 | - describes the result. If the command cannot be executed it returns |
3160 | - a MemoryJobResult instead. |
3161 | + Runs the command embedded in the job and returns a JobResultBuilder |
3162 | + that describes the result. |
3163 | """ |
3164 | try: |
3165 | ctrl = self._get_ctrl_for_job(job) |
3166 | except LookupError: |
3167 | - return MemoryJobResult({ |
3168 | - 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED, |
3169 | - 'comment': _('No suitable execution controller is available)'), |
3170 | - }) |
3171 | + return JobResultBuilder( |
3172 | + outcome=IJobResult.OUTCOME_NOT_SUPPORTED, |
3173 | + comments=_('No suitable execution controller is available)')) |
3174 | # Run the embedded command |
3175 | start_time = time.time() |
3176 | return_code, record_path = self._run_command( |
3177 | @@ -802,12 +808,11 @@ |
3178 | else: |
3179 | outcome = IJobResult.OUTCOME_FAIL |
3180 | # Create a result object and return it |
3181 | - return DiskJobResult({ |
3182 | - 'outcome': outcome, |
3183 | - 'return_code': return_code, |
3184 | - 'io_log_filename': record_path, |
3185 | - 'execution_duration': execution_duration |
3186 | - }) |
3187 | + return JobResultBuilder( |
3188 | + outcome=outcome, |
3189 | + return_code=return_code, |
3190 | + io_log_filename=record_path, |
3191 | + execution_duration=execution_duration) |
3192 | |
3193 | def _prepare_io_handling(self, job, config): |
3194 | ui_io_delegate = self._command_io_delegate |
3195 | @@ -912,7 +917,7 @@ |
3196 | |
3197 | def _get_ctrl_for_job(self, job): |
3198 | """ |
3199 | - Get the execution controller most applicable to run this job |
3200 | + Get the execution controller most applicable to run this job. |
3201 | |
3202 | :param job: |
3203 | A job definition to run |
3204 | |
3205 | === modified file 'plainbox/plainbox/impl/session/jobs.py' |
3206 | --- plainbox/plainbox/impl/session/jobs.py 2015-04-09 13:20:17 +0000 |
3207 | +++ plainbox/plainbox/impl/session/jobs.py 2015-05-14 11:57:31 +0000 |
3208 | @@ -16,6 +16,8 @@ |
3209 | # You should have received a copy of the GNU General Public License |
3210 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
3211 | """ |
3212 | +Job State. |
3213 | + |
3214 | :mod:`plainbox.impl.session.jobs` -- jobs state handling |
3215 | ======================================================== |
3216 | |
3217 | @@ -39,8 +41,9 @@ |
3218 | |
3219 | |
3220 | class InhibitionCause(IntEnum): |
3221 | + |
3222 | """ |
3223 | - There are four possible not-ready causes: |
3224 | + There are four possible not-ready causes. |
3225 | |
3226 | UNDESIRED: |
3227 | This job was not selected to run in this session |
3228 | @@ -58,6 +61,7 @@ |
3229 | FAILED_RESOURCE: |
3230 | This job has a resource requirement that evaluated to a false value |
3231 | """ |
3232 | + |
3233 | UNDESIRED = 0 |
3234 | PENDING_DEP = 1 |
3235 | FAILED_DEP = 2 |
3236 | @@ -68,6 +72,8 @@ |
3237 | def cause_convert_assign_filter( |
3238 | instance: pod.POD, field: pod.Field, old: "Any", new: "Any") -> "Any": |
3239 | """ |
3240 | + Assign filter for for JobReadinessInhibitor.cause. |
3241 | + |
3242 | Custom assign filter for the JobReadinessInhibitor.cause field that |
3243 | produces a very specific error message. |
3244 | """ |
3245 | @@ -78,6 +84,7 @@ |
3246 | |
3247 | |
3248 | class JobReadinessInhibitor(pod.POD): |
3249 | + |
3250 | """ |
3251 | Class representing the cause of a job not being ready to execute. |
3252 | |
3253 | @@ -122,6 +129,7 @@ |
3254 | Provides additional context for the problem caused by a failing |
3255 | resource expression. |
3256 | """ |
3257 | + |
3258 | # XXX: PENDING_RESOURCE is not strict, there are multiple states that are |
3259 | # clumped here which is something I don't like. A resource may be still |
3260 | # "pending" as in PENDING_DEP (it has not ran yet) or it could have ran but |
3261 | @@ -176,11 +184,13 @@ |
3262 | ).format(self.cause.name)) |
3263 | |
3264 | def __repr__(self): |
3265 | + """Get a custom debugging representation of an inhibitor.""" |
3266 | return "<{} cause:{} related_job:{!r} related_expression:{!r}>".format( |
3267 | self.__class__.__name__, self.cause.name, self.related_job, |
3268 | self.related_expression) |
3269 | |
3270 | def __str__(self): |
3271 | + """Get a human-readable text representation of an inhibitor.""" |
3272 | if self.cause == InhibitionCause.UNDESIRED: |
3273 | # TRANSLATORS: as in undesired job |
3274 | return _("undesired") |
3275 | @@ -211,7 +221,10 @@ |
3276 | |
3277 | |
3278 | class OverridableJobField(pod.Field): |
3279 | + |
3280 | """ |
3281 | + A custom Field for modeling job values that can be overridden. |
3282 | + |
3283 | A readable-writable field that has a special initial value ``JOB_VALUE`` |
3284 | which is interpreted as "load this value from the corresponding job |
3285 | definition". |
3286 | @@ -222,11 +235,13 @@ |
3287 | |
3288 | def __init__(self, job_field, doc=None, type=None, notify=False, |
3289 | assign_filter_list=None): |
3290 | + """Initialize a new overridable job field.""" |
3291 | super().__init__( |
3292 | doc, type, JOB_VALUE, None, notify, assign_filter_list) |
3293 | self.job_field = job_field |
3294 | |
3295 | def __get__(self, instance, owner): |
3296 | + """Get an overriden (if any) value of an overridable job field.""" |
3297 | value = super().__get__(instance, owner) |
3298 | if value is JOB_VALUE: |
3299 | return getattr(instance.job, self.job_field) |
3300 | @@ -235,22 +250,28 @@ |
3301 | |
3302 | |
3303 | def job_assign_filter(instance, field, old_value, new_value): |
3304 | - # FIXME: This setter should not exist. job attribute should be |
3305 | - # read-only. This is a temporary kludge to get session restoring |
3306 | - # over DBus working. Once a solution that doesn't involve setting |
3307 | - # a JobState's job attribute is implemented, please remove this |
3308 | - # awful method. |
3309 | + """ |
3310 | + A custom setter for the JobState.job. |
3311 | + |
3312 | + .. warning:: |
3313 | + This setter should not exist. job attribute should be read-only. This |
3314 | + is a temporary kludge to get session restoring over DBus working. Once |
3315 | + a solution that doesn't involve setting a JobState's job attribute is |
3316 | + implemented, please remove this awful method. |
3317 | + """ |
3318 | return new_value |
3319 | |
3320 | |
3321 | def job_via_assign_filter(instance, field, old_value, new_value): |
3322 | + """A custom setter for JobState.via_job.""" |
3323 | if (old_value is not pod.UNSET and not isinstance(new_value, JobDefinition) |
3324 | - and not new_value is None): |
3325 | + and new_value is not None): |
3326 | raise TypeError("via_job must be the actual job, not the checksum") |
3327 | return new_value |
3328 | |
3329 | |
3330 | class JobState(pod.POD): |
3331 | + |
3332 | """ |
3333 | Class representing the state of a job in a session. |
3334 | |
3335 | @@ -284,6 +305,11 @@ |
3336 | initial_fn=lambda: MemoryJobResult({}), |
3337 | notify=True) |
3338 | |
3339 | + result_history = pod.Field( |
3340 | + doc="a tuple of result_history of the associated job", |
3341 | + type=tuple, initial=(), notify=True, |
3342 | + assign_filter_list=[pod.typed, pod.typed.sequence(IJobResult)]) |
3343 | + |
3344 | via_job = pod.Field( |
3345 | doc="the parent job definition", |
3346 | type=JobDefinition, |
3347 | @@ -299,16 +325,36 @@ |
3348 | doc="the effective certification status of this job", |
3349 | type=str) |
3350 | |
3351 | + # NOTE: the `result` property just exposes the last result from the |
3352 | + # `result_history` tuple above. The API is used everywhere so it should not |
3353 | + # be broken in any way but the way forward is the sequence stored in |
3354 | + # `result_history`. |
3355 | + # |
3356 | + # The one particularly annoying part of this implementation is that each |
3357 | + # job state always has at least one result. Even if there was no testing |
3358 | + # done yet. This OUTCOME_NONE result needs to be filtered out at various |
3359 | + # times. I think it would be better if we could not have it in the |
3360 | + # sequence-based API anymore. Otherwise each test will have two |
3361 | + # result_history (more if you count things like resuming a session). |
3362 | + |
3363 | + @result.change_notifier |
3364 | + def _result_changed(self, old, new): |
3365 | + # Don't track the initial assignment over UNSET |
3366 | + if old is pod.UNSET: |
3367 | + return |
3368 | + assert new != old |
3369 | + assert isinstance(new, IJobResult) |
3370 | + if new.is_hollow: |
3371 | + return |
3372 | + logger.debug("Appending result %r to history: %r", new, self.result_history) |
3373 | + self.result_history += (new,) |
3374 | + |
3375 | def can_start(self): |
3376 | - """ |
3377 | - Quickly check if the associated job can run right now. |
3378 | - """ |
3379 | + """Quickly check if the associated job can run right now.""" |
3380 | return len(self.readiness_inhibitor_list) == 0 |
3381 | |
3382 | def get_readiness_description(self): |
3383 | - """ |
3384 | - Get a human readable description of the current readiness state |
3385 | - """ |
3386 | + """Get a human readable description of the current readiness state.""" |
3387 | if self.readiness_inhibitor_list: |
3388 | return _("job cannot be started: {}").format( |
3389 | ", ".join((str(inhibitor) |
3390 | |
3391 | === modified file 'plainbox/plainbox/impl/session/resume.py' |
3392 | --- plainbox/plainbox/impl/session/resume.py 2015-04-13 18:20:44 +0000 |
3393 | +++ plainbox/plainbox/impl/session/resume.py 2015-05-14 11:57:31 +0000 |
3394 | @@ -707,13 +707,10 @@ |
3395 | result = self._build_JobResult( |
3396 | result_repr, self.flags, self.location) |
3397 | result_list.append(result) |
3398 | - # Show the _LAST_ result to the session. Currently we only store one |
3399 | - # result but showing the most recent (last) result should be good |
3400 | - # in general. |
3401 | - if len(result_list) > 0: |
3402 | - logger.debug( |
3403 | - _("calling update_job_result(%r, %r)"), job, result_list[-1]) |
3404 | - session.update_job_result(job, result_list[-1]) |
3405 | + # Replay each result, one by one |
3406 | + for result in result_list: |
3407 | + logger.debug(_("calling update_job_result(%r, %r)"), job, result) |
3408 | + session.update_job_result(job, result) |
3409 | |
3410 | @classmethod |
3411 | def _restore_SessionState_desired_job_list(cls, session, session_repr): |
3412 | |
3413 | === modified file 'plainbox/plainbox/impl/session/suspend.py' |
3414 | --- plainbox/plainbox/impl/session/suspend.py 2015-04-13 13:58:00 +0000 |
3415 | +++ plainbox/plainbox/impl/session/suspend.py 2015-05-14 11:57:31 +0000 |
3416 | @@ -238,7 +238,7 @@ |
3417 | } |
3418 | |
3419 | def _repr_JobResult(self, obj, session_dir): |
3420 | - """ Compute the representation of one of IJobResult subclasses. """ |
3421 | + """Compute the representation of one of IJobResult subclasses.""" |
3422 | if isinstance(obj, DiskJobResult): |
3423 | return self._repr_DiskJobResult(obj, session_dir) |
3424 | elif isinstance(obj, MemoryJobResult): |
3425 | @@ -510,11 +510,10 @@ |
3426 | if not state.result.is_hollow or state.job.id in id_run_list |
3427 | }, |
3428 | "results": { |
3429 | - # Currently we store only one result but we may store |
3430 | - # more than that in a later version. |
3431 | - state.job.id: [self._repr_JobResult(state.result, session_dir)] |
3432 | + state.job.id: [self._repr_JobResult(result, session_dir) |
3433 | + for result in state.result_history] |
3434 | for state in obj.job_state_map.values() |
3435 | - if not state.result.is_hollow |
3436 | + if len(state.result_history) > 0 |
3437 | }, |
3438 | "desired_job_list": [ |
3439 | job.id for job in obj.desired_job_list |
3440 | |
3441 | === modified file 'plainbox/plainbox/impl/session/test_jobs.py' |
3442 | --- plainbox/plainbox/impl/session/test_jobs.py 2015-04-09 13:20:17 +0000 |
3443 | +++ plainbox/plainbox/impl/session/test_jobs.py 2015-05-14 11:57:31 +0000 |
3444 | @@ -140,6 +140,7 @@ |
3445 | def test_smoke(self): |
3446 | self.assertIsNotNone(self.job_state.result) |
3447 | self.assertIs(self.job_state.result.outcome, IJobResult.OUTCOME_NONE) |
3448 | + self.assertEqual(self.job_state.result_history, ()) |
3449 | self.assertEqual(self.job_state.readiness_inhibitor_list, [ |
3450 | UndesiredJobReadinessInhibitor]) |
3451 | self.assertEqual(self.job_state.effective_category_id, |
3452 | @@ -164,6 +165,16 @@ |
3453 | self.job_state.result = result |
3454 | self.assertIs(self.job_state.result, result) |
3455 | |
3456 | + def test_result_history_keeps_track_of_result_changes(self): |
3457 | + # XXX: this example will fail if subsequent results are identical |
3458 | + self.assertEqual(self.job_state.result_history, ()) |
3459 | + result1 = make_job_result(outcome='fail') |
3460 | + self.job_state.result = result1 |
3461 | + self.assertEqual(self.job_state.result_history, (result1,)) |
3462 | + result2 = make_job_result(outcome='pass') |
3463 | + self.job_state.result = result2 |
3464 | + self.assertEqual(self.job_state.result_history, (result1, result2)) |
3465 | + |
3466 | def test_setting_result_fires_signal(self): |
3467 | """ |
3468 | verify that assigning state.result fires the on_result_changed signal |
3469 | |
3470 | === modified file 'plainbox/plainbox/impl/test_pod.py' |
3471 | --- plainbox/plainbox/impl/test_pod.py 2015-01-19 12:33:13 +0000 |
3472 | +++ plainbox/plainbox/impl/test_pod.py 2015-05-14 11:57:31 +0000 |
3473 | @@ -15,6 +15,9 @@ |
3474 | # |
3475 | # You should have received a copy of the GNU General Public License |
3476 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
3477 | + |
3478 | +"""Tests for the plainbox.impl.pod module.""" |
3479 | + |
3480 | from doctest import DocTestSuite |
3481 | from unittest import TestCase |
3482 | |
3483 | @@ -30,24 +33,36 @@ |
3484 | |
3485 | |
3486 | def load_tests(loader, tests, ignore): |
3487 | + """ |
3488 | + Protocol for loading unit tests. |
3489 | + |
3490 | + This function ensures that doctests are executed as well. |
3491 | + """ |
3492 | tests.addTests(DocTestSuite('plainbox.impl.pod')) |
3493 | return tests |
3494 | |
3495 | |
3496 | class SingletonTests(TestCase): |
3497 | |
3498 | + """Tests for several singleton objects.""" |
3499 | + |
3500 | def test_MANDATORY_repr(self): |
3501 | + """MANDATORY.repr() returns "MANDATORY".""" |
3502 | self.assertEqual(repr(MANDATORY), "MANDATORY") |
3503 | |
3504 | def test_UNSET_repr(self): |
3505 | + """UNSET.repr() returns "UNSET".""" |
3506 | self.assertEqual(repr(UNSET), "UNSET") |
3507 | |
3508 | |
3509 | class FieldTests(TestCase): |
3510 | |
3511 | + """Tests for the Field class.""" |
3512 | + |
3513 | FIELD_CLS = Field |
3514 | |
3515 | def setUp(self): |
3516 | + """Common set-up code.""" |
3517 | self.doc = "doc" # not a mock because it gets set to __doc__ |
3518 | self.type = mock.Mock(name='type') |
3519 | self.initial = mock.Mock(name='initial') |
3520 | @@ -58,18 +73,14 @@ |
3521 | self.owner = mock.Mock(name='owner') |
3522 | |
3523 | def test_initializer(self): |
3524 | - """ |
3525 | - Field initializer properly stored all attributes |
3526 | - """ |
3527 | + """.__init__() stored data correctly.""" |
3528 | self.assertEqual(self.field.__doc__, self.doc) |
3529 | self.assertEqual(self.field.type, self.type) |
3530 | self.assertEqual(self.field.initial, self.initial) |
3531 | self.assertEqual(self.field.initial_fn, self.initial_fn) |
3532 | |
3533 | def test_gain_name(self): |
3534 | - """ |
3535 | - Using gain_name() sets three extra attributes |
3536 | - """ |
3537 | + """.gain_name() sets three extra attributes.""" |
3538 | self.assertIsNone(self.field.name) |
3539 | self.assertIsNone(self.field.instance_attr) |
3540 | self.assertIsNone(self.field.signal_name) |
3541 | @@ -79,31 +90,23 @@ |
3542 | self.assertEqual(self.field.signal_name, "on_abcd_changed") |
3543 | |
3544 | def test_repr(self): |
3545 | - """ |
3546 | - Field has a working repr() method |
3547 | - """ |
3548 | + """.repr() works as expected.""" |
3549 | self.field.gain_name("field") |
3550 | self.assertEqual(repr(self.field), "<Field name:'field'>") |
3551 | |
3552 | def test_is_mandatory(self): |
3553 | - """ |
3554 | - Fields with the initial value of MANDATORY are mandatory |
3555 | - """ |
3556 | + """.is_mandatory looks for initial value of MANDATORY.""" |
3557 | self.field.initial = None |
3558 | self.assertFalse(self.field.is_mandatory) |
3559 | self.field.initial = MANDATORY |
3560 | self.assertTrue(self.field.is_mandatory) |
3561 | |
3562 | def test_cls_reads(self): |
3563 | - """ |
3564 | - Accessing fields on via the class exposes the field object itself |
3565 | - """ |
3566 | + """.__get__() returns the field if accessed via a class.""" |
3567 | self.assertIs(self.field.__get__(None, self.owner), self.field) |
3568 | |
3569 | def test_obj_reads(self): |
3570 | - """ |
3571 | - Accessing fields via an object reads data from the object |
3572 | - """ |
3573 | + """.__get__() reads POD data if accessed via an object.""" |
3574 | # Reading the field requires the field to know its name |
3575 | self.field.gain_name("field") |
3576 | self.assertEqual( |
3577 | @@ -111,18 +114,14 @@ |
3578 | self.instance._field) |
3579 | |
3580 | def test_obj_writes(self): |
3581 | - """ |
3582 | - Writing fields via an object writes data to the object |
3583 | - """ |
3584 | + """.__set__() writes POD data.""" |
3585 | # Writing the field requires the field to know its name |
3586 | self.field.gain_name("field") |
3587 | self.field.__set__(self.instance, "data") |
3588 | self.assertEqual(self.instance._field, "data") |
3589 | |
3590 | def test_obj_writes_fires_notification(self): |
3591 | - """ |
3592 | - Writing fields via an object triggers notification, if enabled |
3593 | - """ |
3594 | + """.__set__() fires change notifications.""" |
3595 | # Let's enable notification and set the name so that the field knows |
3596 | # what to do when it gets set. Let's set the instance data to "old" to |
3597 | # track the actual change. |
3598 | @@ -135,9 +134,7 @@ |
3599 | self.instance.on_field_changed.assert_called_with("old", "new") |
3600 | |
3601 | def test_obj_writes_uses_assign_chain(self): |
3602 | - """ |
3603 | - Writing fields via an object uses the assign filter list |
3604 | - """ |
3605 | + """.__set__() uses the assign filter list.""" |
3606 | # Let's enable the assign filter composed out of two functions |
3607 | # and set some data using the field. |
3608 | fn1 = mock.Mock() |
3609 | @@ -153,9 +150,7 @@ |
3610 | self.assertEqual(self.instance._field, fn2()) |
3611 | |
3612 | def test_alter_cls_without_notification(self): |
3613 | - """ |
3614 | - Using alter_cls() when notification is disabled does nothing |
3615 | - """ |
3616 | + """.alter_cls() doesn't do anything if notify is False.""" |
3617 | cls = mock.Mock(name='cls') |
3618 | del cls.on_field_changed |
3619 | self.field.notify = False |
3620 | @@ -164,9 +159,7 @@ |
3621 | self.assertFalse(hasattr(cls, "on_field_changed")) |
3622 | |
3623 | def test_alter_cls_with_notification(self): |
3624 | - """ |
3625 | - Using alter_cls() when notification is enabled creates a signal |
3626 | - """ |
3627 | + """.alter_cls() adds a change signal if notify is True.""" |
3628 | cls = mock.Mock(name='cls') |
3629 | del cls.on_field_changed |
3630 | cls.__name__ = "Klass" |
3631 | @@ -180,7 +173,10 @@ |
3632 | |
3633 | class FieldCollectionTests(TestCase): |
3634 | |
3635 | + """Tests for the _FieldCollection class.""" |
3636 | + |
3637 | def setUp(self): |
3638 | + """Common set-up code.""" |
3639 | self.foo = Field() |
3640 | self.bar = Field() |
3641 | self.ns = { |
3642 | @@ -192,13 +188,12 @@ |
3643 | self.fc = _FieldCollection() |
3644 | |
3645 | def set_field_names(self): |
3646 | + """Set names of the foo and bar fields.""" |
3647 | self.foo.gain_name('foo') |
3648 | self.bar.gain_name('bar') |
3649 | |
3650 | def test_add_field_builds_field_list(self): |
3651 | - """ |
3652 | - .add_field() appends new fields to field_list |
3653 | - """ |
3654 | + """.add_field() appends new fields to field_list.""" |
3655 | # because we're not calling inspect_namespace() which does that |
3656 | self.set_field_names() |
3657 | self.fc.add_field(self.foo, 'cls') |
3658 | @@ -207,9 +202,7 @@ |
3659 | self.assertEqual(self.fc.field_list, [self.foo, self.bar]) |
3660 | |
3661 | def test_add_field_builds_field_origin_map(self): |
3662 | - """ |
3663 | - .add_field() builds and maintains field_origin_map |
3664 | - """ |
3665 | + """.add_field() builds and maintains field_origin_map.""" |
3666 | # because we're not calling inspect_namespace() which does that |
3667 | self.set_field_names() |
3668 | self.fc.add_field(self.foo, 'cls') |
3669 | @@ -219,9 +212,7 @@ |
3670 | self.fc.field_origin_map, {'foo': 'cls', 'bar': 'cls'}) |
3671 | |
3672 | def test_add_field_detects_clashes(self): |
3673 | - """ |
3674 | - .add_Field() detects field clashes and raises TypeError |
3675 | - """ |
3676 | + """.add_Field() detects field clashes and raises TypeError.""" |
3677 | foo_clash = Field() |
3678 | foo_clash.name = 'foo' |
3679 | # because we're not calling inspect_namespace() which does that |
3680 | @@ -232,10 +223,7 @@ |
3681 | self.fc.add_field(foo_clash, 'other_cls') |
3682 | |
3683 | def test_inspect_base_classes_calls_add_field(self): |
3684 | - """ |
3685 | - .inspect_base_classes() calls add_field() on each Field found |
3686 | - """ |
3687 | - |
3688 | + """.inspect_base_classes() calls add_field() on each Field found.""" |
3689 | class Base1(POD): |
3690 | foo = Field() |
3691 | bar = Field() |
3692 | @@ -255,18 +243,14 @@ |
3693 | ]) |
3694 | |
3695 | def test_inspect_namespace_calls_add_field(self): |
3696 | - """ |
3697 | - .inspect_namespace() calls add_field() on each Field |
3698 | - """ |
3699 | + """.inspect_namespace() calls add_field() on each Field.""" |
3700 | with mock.patch.object(self.fc, 'add_field') as mock_add_field: |
3701 | self.fc.inspect_namespace(self.ns, 'cls') |
3702 | mock_add_field.assert_has_call(self.foo, 'cls') |
3703 | mock_add_field.assert_has_call(self.bar, 'cls') |
3704 | |
3705 | def test_inspect_namespace_sets_field_name(self): |
3706 | - """ |
3707 | - .inspect_namespace() sets the .name attribute of each field. |
3708 | - """ |
3709 | + """.inspect_namespace() sets .name of each field.""" |
3710 | self.assertIsNone(self.foo.name) |
3711 | self.assertIsNone(self.bar.name) |
3712 | fc = _FieldCollection() |
3713 | @@ -275,9 +259,7 @@ |
3714 | self.assertEqual(self.bar.name, 'bar') |
3715 | |
3716 | def test_inspect_namespace_sets_field_instance_attr(self): |
3717 | - """ |
3718 | - .inspect_namespace() sets the .instance_attr attribute of each field. |
3719 | - """ |
3720 | + """.inspect_namespace() sets .instance_attr of each field.""" |
3721 | self.assertIsNone(self.foo.instance_attr) |
3722 | self.assertIsNone(self.bar.instance_attr) |
3723 | fc = _FieldCollection() |
3724 | @@ -285,13 +267,21 @@ |
3725 | self.assertEqual(self.foo.instance_attr, '_foo') |
3726 | self.assertEqual(self.bar.instance_attr, '_bar') |
3727 | |
3728 | + def test_notifier(self): |
3729 | + """@field.change_notifier changes the notify function.""" |
3730 | + @self.foo.change_notifier |
3731 | + def on_foo_changed(pod, old, new): |
3732 | + pass |
3733 | + self.assertTrue(self.foo.notify) |
3734 | + self.assertEqual(self.foo.notify_fn, on_foo_changed) |
3735 | + |
3736 | |
3737 | class PODTests(TestCase): |
3738 | |
3739 | + """Tests for the POD class.""" |
3740 | + |
3741 | def test_field_list(self): |
3742 | - """ |
3743 | - Test that PODMeta correctly set up the field_list attribute |
3744 | - """ |
3745 | + """.field_list is set by PODMeta.""" |
3746 | m = mock.Mock() |
3747 | |
3748 | class T(POD): |
3749 | @@ -302,9 +292,7 @@ |
3750 | self.assertEqual(T.field_list, [T.f1, T.f2, T.f3]) |
3751 | |
3752 | def test_namedtuple_cls(self): |
3753 | - """ |
3754 | - Test that PODMeta correctly set up the namedtuple_cls attribute |
3755 | - """ |
3756 | + """Check that .namedtuple_cls is set up by PODMeta.""" |
3757 | m = mock.Mock() |
3758 | |
3759 | class T(POD): |
3760 | @@ -318,9 +306,7 @@ |
3761 | self.assertIsInstance(T.namedtuple_cls.f3, property) |
3762 | |
3763 | def test_initializer_positional_arguments(self): |
3764 | - """ |
3765 | - Test initializer operation with positional arguments |
3766 | - """ |
3767 | + """.__init__() works correctly with positional arguments.""" |
3768 | m = mock.Mock() |
3769 | |
3770 | class T(POD): |
3771 | @@ -339,9 +325,7 @@ |
3772 | self.assertEqual(T(1, 2, 3).f3, 3) |
3773 | |
3774 | def test_initializer_keyword_arguments(self): |
3775 | - """ |
3776 | - Test initializer operation with positional arguments |
3777 | - """ |
3778 | + """.__init__() works correctly with keyword arguments.""" |
3779 | m = mock.Mock() |
3780 | |
3781 | class T(POD): |
3782 | @@ -363,9 +347,7 @@ |
3783 | self.assertEqual(T(f1=1, f2=2, f3=3).f3, 3) |
3784 | |
3785 | def test_initializer_mandatory_arguments(self): |
3786 | - """ |
3787 | - Test initializer's response to mishandling of MANDATORY fields |
3788 | - """ |
3789 | + """.__init__() understands MANDATORY fields.""" |
3790 | class T(POD): |
3791 | m1 = Field(initial=MANDATORY) |
3792 | m2 = Field(initial=MANDATORY) |
3793 | @@ -384,9 +366,7 @@ |
3794 | T(m1=1) |
3795 | |
3796 | def test_initializer_default_arguments(self): |
3797 | - """ |
3798 | - Test initializer's response to default values |
3799 | - """ |
3800 | + """.__init__() understands initial (default) field values.""" |
3801 | class T(POD): |
3802 | f = Field(initial=42) |
3803 | self.assertEqual(T().f, 42) |
3804 | @@ -394,9 +374,7 @@ |
3805 | self.assertEqual(T(f=1).f, 1) |
3806 | |
3807 | def test_initializer_duplicate_field_value(self): |
3808 | - """ |
3809 | - Test that double initialization is not permitted |
3810 | - """ |
3811 | + """.__init__() prevents double-initialization.""" |
3812 | class T(POD): |
3813 | f = Field() |
3814 | with self.assertRaisesRegex( |
3815 | @@ -404,9 +382,7 @@ |
3816 | T(1, f=2) |
3817 | |
3818 | def test_initializer_unknown_field(self): |
3819 | - """ |
3820 | - Test that initializing unknown fields is not permitted |
3821 | - """ |
3822 | + """.__init__() prevents initializing unknown fields.""" |
3823 | class T(POD): |
3824 | pass |
3825 | with self.assertRaisesRegex(TypeError, "too many arguments"): |
3826 | @@ -415,9 +391,7 @@ |
3827 | T(f=1) |
3828 | |
3829 | def test_smoke(self): |
3830 | - """ |
3831 | - Test that a simple Person POD can be used to demonstrate basic features |
3832 | - """ |
3833 | + """Check that basic POD behavior works okay.""" |
3834 | class Person(POD): |
3835 | name = Field() |
3836 | age = Field() |
3837 | @@ -452,10 +426,16 @@ |
3838 | self.assertEqual( |
3839 | repr(joe), "Employee(name='Joe', age=42, salary=1000)") |
3840 | |
3841 | + def test_as_dict_filters_out_UNSET(self): |
3842 | + class P(POD): |
3843 | + f = Field() |
3844 | + |
3845 | + p = P() |
3846 | + p.f = UNSET |
3847 | + self.assertEqual(p.as_dict(), {}) |
3848 | + |
3849 | def test_notifications(self): |
3850 | - """ |
3851 | - Test that change notifications get sent |
3852 | - """ |
3853 | + """.on_{field}_changed() gets fired by field modification.""" |
3854 | class T(POD): |
3855 | f = Field(notify=True) |
3856 | |
3857 | @@ -471,7 +451,7 @@ |
3858 | field_callback.assert_called_with(None, 1) |
3859 | |
3860 | def test_pod_inheritance(self): |
3861 | - |
3862 | + """Check that PODs can be subclassed and new fields can be added.""" |
3863 | class B(POD): |
3864 | f1 = Field(notify=True) |
3865 | |
3866 | @@ -485,14 +465,11 @@ |
3867 | self.assertEqual(D.field_list, [B.f1, D.f2]) |
3868 | |
3869 | def test_pod_ordering(self): |
3870 | - """ |
3871 | - POD comparison doesn't care about the field names |
3872 | - """ |
3873 | - |
3874 | + """Check that comparison among single POD class works okay.""" |
3875 | class A(POD): |
3876 | a = Field() |
3877 | |
3878 | - B = A # easier to understand subsequent testds |
3879 | + B = A # easier to understand subsequent tests |
3880 | self.assertTrue(A(1) == B(1)) |
3881 | self.assertTrue(A(1) != B(0)) |
3882 | self.assertTrue(A(0) < B(1)) |
3883 | @@ -501,10 +478,7 @@ |
3884 | self.assertTrue(A(1) <= B(1)) |
3885 | |
3886 | def test_pod_ordering_tricky1(self): |
3887 | - """ |
3888 | - POD comparison doesn't care about actual classes |
3889 | - """ |
3890 | - |
3891 | + """Check that comparison among different POD classes works okay.""" |
3892 | class A(POD): |
3893 | f = Field() |
3894 | |
3895 | @@ -519,10 +493,7 @@ |
3896 | self.assertTrue(A(1) <= B(1)) |
3897 | |
3898 | def test_pod_ordering_tricky2(self): |
3899 | - """ |
3900 | - POD comparison doesn't care about the field names |
3901 | - """ |
3902 | - |
3903 | + """Check that comparison doesn't care about field names.""" |
3904 | class A(POD): |
3905 | a = Field() |
3906 | |
3907 | @@ -537,10 +508,7 @@ |
3908 | self.assertTrue(A(1) <= B(1)) |
3909 | |
3910 | def test_pod_ordering_other_types(self): |
3911 | - """ |
3912 | - POD comparison understands other types and is not equal to them |
3913 | - """ |
3914 | - |
3915 | + """Check that comparison between POD and not-POD types doesn't work.""" |
3916 | class A(POD): |
3917 | f = Field() |
3918 | |
3919 | @@ -551,10 +519,10 @@ |
3920 | |
3921 | class AssignFilterTests(TestCase): |
3922 | |
3923 | + """Tests for assignment filters.""" |
3924 | + |
3925 | def test_read_only_assign_filter(self): |
3926 | - """ |
3927 | - The read_only_assign_filter works as designed |
3928 | - """ |
3929 | + """The read_only_assign_filter works as designed.""" |
3930 | instance = mock.Mock(name='instance') |
3931 | instance.__class__.__name__ = 'cls' |
3932 | field = mock.Mock(name='field') |
3933 | @@ -569,9 +537,7 @@ |
3934 | read_only_assign_filter(instance, field, old, new) |
3935 | |
3936 | def test_type_convert_assign_filter(self): |
3937 | - """ |
3938 | - The type_convert_assign_filter works as designed |
3939 | - """ |
3940 | + """The type_convert_assign_filter works as designed.""" |
3941 | instance = mock.Mock(name='instance') |
3942 | old = mock.Mock(name='old') |
3943 | field = mock.Mock(name='field') |
3944 | @@ -585,9 +551,7 @@ |
3945 | type_convert_assign_filter(instance, field, old, 'hello?') |
3946 | |
3947 | def test_type_check_assign_filter(self): |
3948 | - """ |
3949 | - The type_convert_assign_filter works as designed |
3950 | - """ |
3951 | + """The type_convert_assign_filter works as designed.""" |
3952 | instance = mock.Mock(name='instance') |
3953 | instance.__class__.__name__ = 'cls' |
3954 | old = mock.Mock(name='old') |
3955 | |
3956 | === modified file 'plainbox/plainbox/impl/test_result.py' |
3957 | --- plainbox/plainbox/impl/test_result.py 2015-03-30 19:25:56 +0000 |
3958 | +++ plainbox/plainbox/impl/test_result.py 2015-05-14 11:57:31 +0000 |
3959 | @@ -33,6 +33,7 @@ |
3960 | from plainbox.impl.result import IOLogRecord |
3961 | from plainbox.impl.result import IOLogRecordReader |
3962 | from plainbox.impl.result import IOLogRecordWriter |
3963 | +from plainbox.impl.result import JobResultBuilder |
3964 | from plainbox.impl.result import MemoryJobResult |
3965 | from plainbox.impl.testing_utils import make_io_log |
3966 | |
3967 | @@ -150,6 +151,7 @@ |
3968 | }) |
3969 | self.assertEqual(result.io_log_as_text_attachment, 'foo') |
3970 | |
3971 | + |
3972 | class IOLogRecordWriterTests(TestCase): |
3973 | |
3974 | _RECORD = IOLogRecord(0.123, 'stdout', b'some\ndata') |
3975 | @@ -180,3 +182,45 @@ |
3976 | reader = IOLogRecordReader(stream) |
3977 | record_list = list(reader) |
3978 | self.assertEqual(record_list, [self._RECORD]) |
3979 | + |
3980 | + |
3981 | +class JobResultBuildeTests(TestCase): |
3982 | + |
3983 | + def test_smoke_hollow(self): |
3984 | + self.assertTrue(JobResultBuilder().seal().is_hollow) |
3985 | + |
3986 | + def test_smoke_memory(self): |
3987 | + builder = JobResultBuilder() |
3988 | + builder.comments = 'it works' |
3989 | + builder.execution_duration = 0.1 |
3990 | + builder.io_log = [(0, 'stdout', b'ok\n')] |
3991 | + builder.outcome = 'pass' |
3992 | + builder.return_code = 0 |
3993 | + result = builder.seal() |
3994 | + self.assertEqual(result.comments, "it works") |
3995 | + self.assertEqual(result.execution_duration, 0.1) |
3996 | + self.assertEqual(result.io_log, ( |
3997 | + IOLogRecord(delay=0, stream_name='stdout', data=b'ok\n'),)) |
3998 | + self.assertEqual(result.outcome, "pass") |
3999 | + self.assertEqual(result.return_code, 0) |
4000 | + |
4001 | + def test_smoke_disk(self): |
4002 | + builder = JobResultBuilder() |
4003 | + builder.comments = 'it works' |
4004 | + builder.execution_duration = 0.1 |
4005 | + builder.io_log_filename = 'log' |
4006 | + builder.outcome = 'pass' |
4007 | + builder.return_code = 0 |
4008 | + result = builder.seal() |
4009 | + self.assertEqual(result.comments, "it works") |
4010 | + self.assertEqual(result.execution_duration, 0.1) |
4011 | + self.assertEqual(result.io_log_filename, 'log') |
4012 | + self.assertEqual(result.outcome, "pass") |
4013 | + self.assertEqual(result.return_code, 0) |
4014 | + |
4015 | + def test_io_log_clash(self): |
4016 | + builder = JobResultBuilder() |
4017 | + builder.io_log = [(0, 'stout', b'hi')] |
4018 | + builder.io_log_filename = 'log' |
4019 | + with self.assertRaises(ValueError): |
4020 | + builder.seal() |
4021 | |
4022 | === modified file 'plainbox/setup.py' |
4023 | --- plainbox/setup.py 2015-05-10 18:38:53 +0000 |
4024 | +++ plainbox/setup.py 2015-05-14 11:57:31 +0000 |
4025 | @@ -70,6 +70,9 @@ |
4026 | 'Topic :: System :: Benchmark', |
4027 | 'Topic :: Utilities', |
4028 | ], |
4029 | + install_requires=[ |
4030 | + 'Jinja2 >= 2.7', |
4031 | + ], |
4032 | extras_require={ |
4033 | 'XLSX': 'XlsxWriter >= 0.3', |
4034 | 'XML': 'lxml >= 2.3', |
4035 | @@ -89,6 +92,7 @@ |
4036 | 'xlsx=plainbox.impl.exporter.xlsx:XLSXSessionStateExporter [XLSX]', |
4037 | 'xml=plainbox.impl.exporter.xml:XMLSessionStateExporter [XML]', |
4038 | 'html=plainbox.impl.exporter.html:HTMLSessionStateExporter [XML]', |
4039 | + 'hexr=plainbox.impl.exporter.hexr:HEXRExporter', |
4040 | ], |
4041 | 'plainbox.buildsystem': [ |
4042 | 'make=plainbox.impl.buildsystems:MakefileBuildSystem', |
4043 | |
4044 | === modified file 'providers/plainbox-provider-checkbox/whitelists/smoke.whitelist' |
4045 | --- providers/plainbox-provider-checkbox/whitelists/smoke.whitelist 2014-03-20 14:11:44 +0000 |
4046 | +++ providers/plainbox-provider-checkbox/whitelists/smoke.whitelist 2015-05-14 11:57:31 +0000 |
4047 | @@ -1,27 +1,1 @@ |
4048 | -## This is an example whitelist to start from. |
4049 | -## To use, copy this file and add the jobs you want to run to the copy. Delete |
4050 | -## these comments. DO NOT delete the first 9 jobs in tis file. They are resource |
4051 | -## gathering jobs and are necessary to do any testing at all. |
4052 | -# Resource Jobs (listed in jobs/resource.txt) |
4053 | -cpuinfo |
4054 | -cdimage |
4055 | -dmi |
4056 | -dpkg |
4057 | -efi |
4058 | -environment |
4059 | -gconf |
4060 | -lsb |
4061 | -meminfo |
4062 | -module |
4063 | -package |
4064 | -device |
4065 | -uname |
4066 | -# Smoke test cases |
4067 | -__smoke__ |
4068 | -smoke/true |
4069 | -smoke/false |
4070 | -smoke/dependency/good |
4071 | -smoke/dependency/bad |
4072 | -smoke/requirement/good |
4073 | -smoke/requirement/bad |
4074 | smoke/manual |
4075 | |
4076 | === modified file 'support/install-deb-dependencies' |
4077 | --- support/install-deb-dependencies 2014-04-02 13:19:06 +0000 |
4078 | +++ support/install-deb-dependencies 2015-05-14 11:57:31 +0000 |
4079 | @@ -54,3 +54,6 @@ |
4080 | apt-get install --quiet --quiet --yes $dpkgs_to_install_list |
4081 | fi |
4082 | fi |
4083 | + |
4084 | +# Update to latest packages, this is a bit annoying but this is what it takes |
4085 | +sudo apt-get dist-upgrade --yes |