Merge lp:~zyga/checkbox/result-history into lp:checkbox
- result-history
- Merge into trunk
Proposed by
Zygmunt Krynicki
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 3824 |
Merged at revision: | 3816 |
Proposed branch: | lp:~zyga/checkbox/result-history |
Merge into: | lp:checkbox |
Diff against target: |
585 lines (+157/-30) 12 files modified
checkbox-ng/checkbox_ng/service.py (+1/-1) plainbox/plainbox/data/report/checkbox.html (+25/-0) plainbox/plainbox/impl/exporter/test_text.py (+1/-1) plainbox/plainbox/impl/exporter/text.py (+17/-3) 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/test-data/html-exporter/with_both_certification_status.html (+14/-0) plainbox/plainbox/test-data/html-exporter/with_certification_blocker.html (+7/-0) plainbox/plainbox/test-data/html-exporter/with_certification_non_blocker.html (+7/-0) plainbox/plainbox/test-data/html-exporter/without_certification_status.html (+7/-0) |
To merge this branch: | bzr merge lp:~zyga/checkbox/result-history |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau | Approve | ||
Review via email: mp+260457@code.launchpad.net |
Commit message
Description of the change
56b7b43 checkbox-
17c2aea plainbox:
f23ccc2 plainbox:
284f0b8 plainbox:
98647a1 plainbox:
d2282fb plainbox:
19e21e2 plainbox:
8b389d0 plainbox:
3f0f87d plainbox:
To post a comment you must log in.
lp:~zyga/checkbox/result-history
updated
- 3823. By Zygmunt Krynicki
-
plainbox:
exporter: text: display result history Signed-off-by: Zygmunt Krynicki <email address hidden>
- 3824. By Zygmunt Krynicki
-
plainbox:
exporter: html: display result history Signed-off-by: Zygmunt Krynicki <email address hidden>
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'checkbox-ng/checkbox_ng/service.py' |
2 | --- checkbox-ng/checkbox_ng/service.py 2015-05-28 10:04:37 +0000 |
3 | +++ checkbox-ng/checkbox_ng/service.py 2015-05-28 11:25:44 +0000 |
4 | @@ -528,7 +528,7 @@ |
5 | return self.native.comments or "" |
6 | |
7 | @comments.setter |
8 | - def comments(self, value): |
9 | + def comments(self, new_value): |
10 | """ |
11 | set comments to a new value |
12 | """ |
13 | |
14 | === modified file 'plainbox/plainbox/data/report/checkbox.html' |
15 | --- plainbox/plainbox/data/report/checkbox.html 2015-05-19 17:56:35 +0000 |
16 | +++ plainbox/plainbox/data/report/checkbox.html 2015-05-28 11:25:44 +0000 |
17 | @@ -127,6 +127,9 @@ |
18 | font-size: 10px; |
19 | line-height: 14px; |
20 | } |
21 | + tr.historic-run td:first-child { |
22 | + padding-left: 2em; |
23 | + } |
24 | .data { |
25 | display: none; |
26 | } |
27 | @@ -217,6 +220,7 @@ |
28 | <th>Test ID</th> |
29 | <th>Result</th> |
30 | <th>Certification status</th> |
31 | + <th>Run</th> |
32 | <th>Comment</th> |
33 | </tr> |
34 | </thead> |
35 | @@ -226,6 +230,7 @@ |
36 | <td>{{ job_state.job.tr_summary() }}</td> |
37 | <td style='font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
38 | <td>{{ job_state.effective_certification_status }}</td> |
39 | + <td>{{ job_state.result_history|length }}</td> |
40 | {%- if job_state.result.comments != None %} |
41 | <td>{{ job_state.result.comments }}</td> |
42 | {%- else %} |
43 | @@ -239,6 +244,26 @@ |
44 | {%- endif %} |
45 | {%- endif %} |
46 | </tr> |
47 | + {%- for result in job_state.result_history[:-1] %} |
48 | + <tr class='historic-run'> |
49 | + <td>{{ job_state.job.tr_summary() }}</td> |
50 | + <td style='font-weight: bold; color: {{ result.outcome_meta().color_hex }}'>{{ result.outcome_meta().tr_label }}</td> |
51 | + <td></td> |
52 | + <td>{{ loop.index }}</td> |
53 | + {%- if result.comments != None %} |
54 | + <td>{{ result.comments }}</td> |
55 | + {%- else %} |
56 | + {%- if result.io_log_as_flat_text != "" %} |
57 | + <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
58 | + <pre>{{ result.io_log_as_flat_text }}</pre> |
59 | + </div> |
60 | + </td> |
61 | + {%- else %} |
62 | + <td> </td> |
63 | + {%- endif %} |
64 | + {%- endif %} |
65 | + </tr> |
66 | + {%- endfor %} |
67 | {%- endfor %} |
68 | </tbody> |
69 | </table> |
70 | |
71 | === modified file 'plainbox/plainbox/impl/exporter/test_text.py' |
72 | --- plainbox/plainbox/impl/exporter/test_text.py 2014-11-11 06:06:55 +0000 |
73 | +++ plainbox/plainbox/impl/exporter/test_text.py 2015-05-28 11:25:44 +0000 |
74 | @@ -44,7 +44,7 @@ |
75 | data = mock.Mock( |
76 | run_list=[job], |
77 | job_state_map={ |
78 | - job.id: mock.Mock(result=result, job=job) |
79 | + job.id: mock.Mock(result=result, job=job, result_history=()) |
80 | } |
81 | ) |
82 | stream = BytesIO() |
83 | |
84 | === modified file 'plainbox/plainbox/impl/exporter/text.py' |
85 | --- plainbox/plainbox/impl/exporter/text.py 2015-03-31 08:38:35 +0000 |
86 | +++ plainbox/plainbox/impl/exporter/text.py 2015-05-28 11:25:44 +0000 |
87 | @@ -25,15 +25,15 @@ |
88 | |
89 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
90 | """ |
91 | +from plainbox.i18n import gettext as _ |
92 | from plainbox.impl.commands.inv_run import Colorizer |
93 | from plainbox.impl.exporter import SessionStateExporterBase |
94 | from plainbox.impl.result import outcome_meta |
95 | |
96 | |
97 | class TextSessionStateExporter(SessionStateExporterBase): |
98 | - """ |
99 | - Human-readable session state exporter. |
100 | - """ |
101 | + |
102 | + """Human-readable session state exporter.""" |
103 | |
104 | def __init__(self, option_list=None, color=None): |
105 | super().__init__(option_list) |
106 | @@ -55,9 +55,23 @@ |
107 | outcome_meta(state.result.outcome).color_ansi |
108 | ), state.job.tr_summary(), |
109 | ).encode("UTF-8")) |
110 | + if len(state.result_history) > 1: |
111 | + stream.write(_(" history: {0}\n").format( |
112 | + ', '.join( |
113 | + self.C.custom( |
114 | + result.outcome_meta().tr_outcome, |
115 | + result.outcome_meta().color_ansi) |
116 | + for result in state.result_history) |
117 | + ).encode("UTF-8")) |
118 | else: |
119 | stream.write( |
120 | "{:^15}: {}\n".format( |
121 | state.result.tr_outcome(), |
122 | state.job.tr_summary(), |
123 | ).encode("UTF-8")) |
124 | + if state.result_history: |
125 | + print(_("History:"), ', '.join( |
126 | + self.C.custom( |
127 | + result.outcome_meta().unicode_sigil, |
128 | + result.outcome_meta().color_ansi) |
129 | + for result in state.result_history)) |
130 | |
131 | === modified file 'plainbox/plainbox/impl/session/jobs.py' |
132 | --- plainbox/plainbox/impl/session/jobs.py 2015-04-09 13:20:17 +0000 |
133 | +++ plainbox/plainbox/impl/session/jobs.py 2015-05-28 11:25:44 +0000 |
134 | @@ -16,6 +16,8 @@ |
135 | # You should have received a copy of the GNU General Public License |
136 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
137 | """ |
138 | +Job State. |
139 | + |
140 | :mod:`plainbox.impl.session.jobs` -- jobs state handling |
141 | ======================================================== |
142 | |
143 | @@ -39,8 +41,9 @@ |
144 | |
145 | |
146 | class InhibitionCause(IntEnum): |
147 | + |
148 | """ |
149 | - There are four possible not-ready causes: |
150 | + There are four possible not-ready causes. |
151 | |
152 | UNDESIRED: |
153 | This job was not selected to run in this session |
154 | @@ -58,6 +61,7 @@ |
155 | FAILED_RESOURCE: |
156 | This job has a resource requirement that evaluated to a false value |
157 | """ |
158 | + |
159 | UNDESIRED = 0 |
160 | PENDING_DEP = 1 |
161 | FAILED_DEP = 2 |
162 | @@ -68,6 +72,8 @@ |
163 | def cause_convert_assign_filter( |
164 | instance: pod.POD, field: pod.Field, old: "Any", new: "Any") -> "Any": |
165 | """ |
166 | + Assign filter for for JobReadinessInhibitor.cause. |
167 | + |
168 | Custom assign filter for the JobReadinessInhibitor.cause field that |
169 | produces a very specific error message. |
170 | """ |
171 | @@ -78,6 +84,7 @@ |
172 | |
173 | |
174 | class JobReadinessInhibitor(pod.POD): |
175 | + |
176 | """ |
177 | Class representing the cause of a job not being ready to execute. |
178 | |
179 | @@ -122,6 +129,7 @@ |
180 | Provides additional context for the problem caused by a failing |
181 | resource expression. |
182 | """ |
183 | + |
184 | # XXX: PENDING_RESOURCE is not strict, there are multiple states that are |
185 | # clumped here which is something I don't like. A resource may be still |
186 | # "pending" as in PENDING_DEP (it has not ran yet) or it could have ran but |
187 | @@ -176,11 +184,13 @@ |
188 | ).format(self.cause.name)) |
189 | |
190 | def __repr__(self): |
191 | + """Get a custom debugging representation of an inhibitor.""" |
192 | return "<{} cause:{} related_job:{!r} related_expression:{!r}>".format( |
193 | self.__class__.__name__, self.cause.name, self.related_job, |
194 | self.related_expression) |
195 | |
196 | def __str__(self): |
197 | + """Get a human-readable text representation of an inhibitor.""" |
198 | if self.cause == InhibitionCause.UNDESIRED: |
199 | # TRANSLATORS: as in undesired job |
200 | return _("undesired") |
201 | @@ -211,7 +221,10 @@ |
202 | |
203 | |
204 | class OverridableJobField(pod.Field): |
205 | + |
206 | """ |
207 | + A custom Field for modeling job values that can be overridden. |
208 | + |
209 | A readable-writable field that has a special initial value ``JOB_VALUE`` |
210 | which is interpreted as "load this value from the corresponding job |
211 | definition". |
212 | @@ -222,11 +235,13 @@ |
213 | |
214 | def __init__(self, job_field, doc=None, type=None, notify=False, |
215 | assign_filter_list=None): |
216 | + """Initialize a new overridable job field.""" |
217 | super().__init__( |
218 | doc, type, JOB_VALUE, None, notify, assign_filter_list) |
219 | self.job_field = job_field |
220 | |
221 | def __get__(self, instance, owner): |
222 | + """Get an overriden (if any) value of an overridable job field.""" |
223 | value = super().__get__(instance, owner) |
224 | if value is JOB_VALUE: |
225 | return getattr(instance.job, self.job_field) |
226 | @@ -235,22 +250,28 @@ |
227 | |
228 | |
229 | def job_assign_filter(instance, field, old_value, new_value): |
230 | - # FIXME: This setter should not exist. job attribute should be |
231 | - # read-only. This is a temporary kludge to get session restoring |
232 | - # over DBus working. Once a solution that doesn't involve setting |
233 | - # a JobState's job attribute is implemented, please remove this |
234 | - # awful method. |
235 | + """ |
236 | + A custom setter for the JobState.job. |
237 | + |
238 | + .. warning:: |
239 | + This setter should not exist. job attribute should be read-only. This |
240 | + is a temporary kludge to get session restoring over DBus working. Once |
241 | + a solution that doesn't involve setting a JobState's job attribute is |
242 | + implemented, please remove this awful method. |
243 | + """ |
244 | return new_value |
245 | |
246 | |
247 | def job_via_assign_filter(instance, field, old_value, new_value): |
248 | + """A custom setter for JobState.via_job.""" |
249 | if (old_value is not pod.UNSET and not isinstance(new_value, JobDefinition) |
250 | - and not new_value is None): |
251 | + and new_value is not None): |
252 | raise TypeError("via_job must be the actual job, not the checksum") |
253 | return new_value |
254 | |
255 | |
256 | class JobState(pod.POD): |
257 | + |
258 | """ |
259 | Class representing the state of a job in a session. |
260 | |
261 | @@ -284,6 +305,11 @@ |
262 | initial_fn=lambda: MemoryJobResult({}), |
263 | notify=True) |
264 | |
265 | + result_history = pod.Field( |
266 | + doc="a tuple of result_history of the associated job", |
267 | + type=tuple, initial=(), notify=True, |
268 | + assign_filter_list=[pod.typed, pod.typed.sequence(IJobResult)]) |
269 | + |
270 | via_job = pod.Field( |
271 | doc="the parent job definition", |
272 | type=JobDefinition, |
273 | @@ -299,16 +325,36 @@ |
274 | doc="the effective certification status of this job", |
275 | type=str) |
276 | |
277 | + # NOTE: the `result` property just exposes the last result from the |
278 | + # `result_history` tuple above. The API is used everywhere so it should not |
279 | + # be broken in any way but the way forward is the sequence stored in |
280 | + # `result_history`. |
281 | + # |
282 | + # The one particularly annoying part of this implementation is that each |
283 | + # job state always has at least one result. Even if there was no testing |
284 | + # done yet. This OUTCOME_NONE result needs to be filtered out at various |
285 | + # times. I think it would be better if we could not have it in the |
286 | + # sequence-based API anymore. Otherwise each test will have two |
287 | + # result_history (more if you count things like resuming a session). |
288 | + |
289 | + @result.change_notifier |
290 | + def _result_changed(self, old, new): |
291 | + # Don't track the initial assignment over UNSET |
292 | + if old is pod.UNSET: |
293 | + return |
294 | + assert new != old |
295 | + assert isinstance(new, IJobResult) |
296 | + if new.is_hollow: |
297 | + return |
298 | + logger.debug("Appending result %r to history: %r", new, self.result_history) |
299 | + self.result_history += (new,) |
300 | + |
301 | def can_start(self): |
302 | - """ |
303 | - Quickly check if the associated job can run right now. |
304 | - """ |
305 | + """Quickly check if the associated job can run right now.""" |
306 | return len(self.readiness_inhibitor_list) == 0 |
307 | |
308 | def get_readiness_description(self): |
309 | - """ |
310 | - Get a human readable description of the current readiness state |
311 | - """ |
312 | + """Get a human readable description of the current readiness state.""" |
313 | if self.readiness_inhibitor_list: |
314 | return _("job cannot be started: {}").format( |
315 | ", ".join((str(inhibitor) |
316 | |
317 | === modified file 'plainbox/plainbox/impl/session/resume.py' |
318 | --- plainbox/plainbox/impl/session/resume.py 2015-04-13 18:20:44 +0000 |
319 | +++ plainbox/plainbox/impl/session/resume.py 2015-05-28 11:25:44 +0000 |
320 | @@ -707,13 +707,10 @@ |
321 | result = self._build_JobResult( |
322 | result_repr, self.flags, self.location) |
323 | result_list.append(result) |
324 | - # Show the _LAST_ result to the session. Currently we only store one |
325 | - # result but showing the most recent (last) result should be good |
326 | - # in general. |
327 | - if len(result_list) > 0: |
328 | - logger.debug( |
329 | - _("calling update_job_result(%r, %r)"), job, result_list[-1]) |
330 | - session.update_job_result(job, result_list[-1]) |
331 | + # Replay each result, one by one |
332 | + for result in result_list: |
333 | + logger.debug(_("calling update_job_result(%r, %r)"), job, result) |
334 | + session.update_job_result(job, result) |
335 | |
336 | @classmethod |
337 | def _restore_SessionState_desired_job_list(cls, session, session_repr): |
338 | |
339 | === modified file 'plainbox/plainbox/impl/session/suspend.py' |
340 | --- plainbox/plainbox/impl/session/suspend.py 2015-04-13 13:58:00 +0000 |
341 | +++ plainbox/plainbox/impl/session/suspend.py 2015-05-28 11:25:44 +0000 |
342 | @@ -238,7 +238,7 @@ |
343 | } |
344 | |
345 | def _repr_JobResult(self, obj, session_dir): |
346 | - """ Compute the representation of one of IJobResult subclasses. """ |
347 | + """Compute the representation of one of IJobResult subclasses.""" |
348 | if isinstance(obj, DiskJobResult): |
349 | return self._repr_DiskJobResult(obj, session_dir) |
350 | elif isinstance(obj, MemoryJobResult): |
351 | @@ -510,11 +510,10 @@ |
352 | if not state.result.is_hollow or state.job.id in id_run_list |
353 | }, |
354 | "results": { |
355 | - # Currently we store only one result but we may store |
356 | - # more than that in a later version. |
357 | - state.job.id: [self._repr_JobResult(state.result, session_dir)] |
358 | + state.job.id: [self._repr_JobResult(result, session_dir) |
359 | + for result in state.result_history] |
360 | for state in obj.job_state_map.values() |
361 | - if not state.result.is_hollow |
362 | + if len(state.result_history) > 0 |
363 | }, |
364 | "desired_job_list": [ |
365 | job.id for job in obj.desired_job_list |
366 | |
367 | === modified file 'plainbox/plainbox/impl/session/test_jobs.py' |
368 | --- plainbox/plainbox/impl/session/test_jobs.py 2015-04-09 13:20:17 +0000 |
369 | +++ plainbox/plainbox/impl/session/test_jobs.py 2015-05-28 11:25:44 +0000 |
370 | @@ -140,6 +140,7 @@ |
371 | def test_smoke(self): |
372 | self.assertIsNotNone(self.job_state.result) |
373 | self.assertIs(self.job_state.result.outcome, IJobResult.OUTCOME_NONE) |
374 | + self.assertEqual(self.job_state.result_history, ()) |
375 | self.assertEqual(self.job_state.readiness_inhibitor_list, [ |
376 | UndesiredJobReadinessInhibitor]) |
377 | self.assertEqual(self.job_state.effective_category_id, |
378 | @@ -164,6 +165,16 @@ |
379 | self.job_state.result = result |
380 | self.assertIs(self.job_state.result, result) |
381 | |
382 | + def test_result_history_keeps_track_of_result_changes(self): |
383 | + # XXX: this example will fail if subsequent results are identical |
384 | + self.assertEqual(self.job_state.result_history, ()) |
385 | + result1 = make_job_result(outcome='fail') |
386 | + self.job_state.result = result1 |
387 | + self.assertEqual(self.job_state.result_history, (result1,)) |
388 | + result2 = make_job_result(outcome='pass') |
389 | + self.job_state.result = result2 |
390 | + self.assertEqual(self.job_state.result_history, (result1, result2)) |
391 | + |
392 | def test_setting_result_fires_signal(self): |
393 | """ |
394 | verify that assigning state.result fires the on_result_changed signal |
395 | |
396 | === modified file 'plainbox/plainbox/test-data/html-exporter/with_both_certification_status.html' |
397 | --- plainbox/plainbox/test-data/html-exporter/with_both_certification_status.html 2015-05-19 17:56:35 +0000 |
398 | +++ plainbox/plainbox/test-data/html-exporter/with_both_certification_status.html 2015-05-28 11:25:44 +0000 |
399 | @@ -123,6 +123,9 @@ |
400 | font-size: 10px; |
401 | line-height: 14px; |
402 | } |
403 | + tr.historic-run td:first-child { |
404 | + padding-left: 2em; |
405 | + } |
406 | .data { |
407 | display: none; |
408 | } |
409 | @@ -203,6 +206,7 @@ |
410 | <th>Test ID</th> |
411 | <th>Result</th> |
412 | <th>Certification status</th> |
413 | + <th>Run</th> |
414 | <th>Comment</th> |
415 | </tr> |
416 | </thead> |
417 | @@ -211,6 +215,7 @@ |
418 | <td>job 1</td> |
419 | <td style='font-weight: bold; color: #DC3912'>failed</td> |
420 | <td>blocker</td> |
421 | + <td>1</td> |
422 | <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
423 | <pre>FATAL ERROR |
424 | </pre> |
425 | @@ -221,16 +226,25 @@ |
426 | <td>job 2</td> |
427 | <td style='font-weight: bold; color: #DC3912'>failed</td> |
428 | <td>non-blocker</td> |
429 | + <td>2</td> |
430 | <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
431 | <pre>FATAL ERROR |
432 | </pre> |
433 | </div> |
434 | </td> |
435 | </tr> |
436 | + <tr class='historic-run'> |
437 | + <td>job 2</td> |
438 | + <td style='font-weight: bold; color: #6AA84F'>passed</td> |
439 | + <td></td> |
440 | + <td>1</td> |
441 | + <td>blah blah</td> |
442 | + </tr> |
443 | <tr> |
444 | <td>job 3</td> |
445 | <td style='font-weight: bold; color: #FF9900'>skipped</td> |
446 | <td>unspecified</td> |
447 | + <td>1</td> |
448 | <td>No such device</td> |
449 | </tr> |
450 | </tbody> |
451 | |
452 | === modified file 'plainbox/plainbox/test-data/html-exporter/with_certification_blocker.html' |
453 | --- plainbox/plainbox/test-data/html-exporter/with_certification_blocker.html 2015-05-19 17:56:35 +0000 |
454 | +++ plainbox/plainbox/test-data/html-exporter/with_certification_blocker.html 2015-05-28 11:25:44 +0000 |
455 | @@ -123,6 +123,9 @@ |
456 | font-size: 10px; |
457 | line-height: 14px; |
458 | } |
459 | + tr.historic-run td:first-child { |
460 | + padding-left: 2em; |
461 | + } |
462 | .data { |
463 | display: none; |
464 | } |
465 | @@ -186,6 +189,7 @@ |
466 | <th>Test ID</th> |
467 | <th>Result</th> |
468 | <th>Certification status</th> |
469 | + <th>Run</th> |
470 | <th>Comment</th> |
471 | </tr> |
472 | </thead> |
473 | @@ -194,6 +198,7 @@ |
474 | <td>job 1</td> |
475 | <td style='font-weight: bold; color: #DC3912'>failed</td> |
476 | <td>blocker</td> |
477 | + <td>1</td> |
478 | <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
479 | <pre>FATAL ERROR |
480 | </pre> |
481 | @@ -204,12 +209,14 @@ |
482 | <td>job 2</td> |
483 | <td style='font-weight: bold; color: #6AA84F'>passed</td> |
484 | <td>unspecified</td> |
485 | + <td>1</td> |
486 | <td>blah blah</td> |
487 | </tr> |
488 | <tr> |
489 | <td>job 3</td> |
490 | <td style='font-weight: bold; color: #FF9900'>skipped</td> |
491 | <td>unspecified</td> |
492 | + <td>1</td> |
493 | <td>No such device</td> |
494 | </tr> |
495 | </tbody> |
496 | |
497 | === modified file 'plainbox/plainbox/test-data/html-exporter/with_certification_non_blocker.html' |
498 | --- plainbox/plainbox/test-data/html-exporter/with_certification_non_blocker.html 2015-05-19 17:56:35 +0000 |
499 | +++ plainbox/plainbox/test-data/html-exporter/with_certification_non_blocker.html 2015-05-28 11:25:44 +0000 |
500 | @@ -123,6 +123,9 @@ |
501 | font-size: 10px; |
502 | line-height: 14px; |
503 | } |
504 | + tr.historic-run td:first-child { |
505 | + padding-left: 2em; |
506 | + } |
507 | .data { |
508 | display: none; |
509 | } |
510 | @@ -184,6 +187,7 @@ |
511 | <th>Test ID</th> |
512 | <th>Result</th> |
513 | <th>Certification status</th> |
514 | + <th>Run</th> |
515 | <th>Comment</th> |
516 | </tr> |
517 | </thead> |
518 | @@ -192,6 +196,7 @@ |
519 | <td>job 1</td> |
520 | <td style='font-weight: bold; color: #DC3912'>failed</td> |
521 | <td>non-blocker</td> |
522 | + <td>1</td> |
523 | <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
524 | <pre>FATAL ERROR |
525 | </pre> |
526 | @@ -202,12 +207,14 @@ |
527 | <td>job 2</td> |
528 | <td style='font-weight: bold; color: #6AA84F'>passed</td> |
529 | <td>unspecified</td> |
530 | + <td>1</td> |
531 | <td>blah blah</td> |
532 | </tr> |
533 | <tr> |
534 | <td>job 3</td> |
535 | <td style='font-weight: bold; color: #FF9900'>skipped</td> |
536 | <td>unspecified</td> |
537 | + <td>1</td> |
538 | <td>No such device</td> |
539 | </tr> |
540 | </tbody> |
541 | |
542 | === modified file 'plainbox/plainbox/test-data/html-exporter/without_certification_status.html' |
543 | --- plainbox/plainbox/test-data/html-exporter/without_certification_status.html 2015-05-19 17:56:35 +0000 |
544 | +++ plainbox/plainbox/test-data/html-exporter/without_certification_status.html 2015-05-28 11:25:44 +0000 |
545 | @@ -123,6 +123,9 @@ |
546 | font-size: 10px; |
547 | line-height: 14px; |
548 | } |
549 | + tr.historic-run td:first-child { |
550 | + padding-left: 2em; |
551 | + } |
552 | .data { |
553 | display: none; |
554 | } |
555 | @@ -167,6 +170,7 @@ |
556 | <th>Test ID</th> |
557 | <th>Result</th> |
558 | <th>Certification status</th> |
559 | + <th>Run</th> |
560 | <th>Comment</th> |
561 | </tr> |
562 | </thead> |
563 | @@ -175,6 +179,7 @@ |
564 | <td>job 1</td> |
565 | <td style='font-weight: bold; color: #DC3912'>failed</td> |
566 | <td>unspecified</td> |
567 | + <td>1</td> |
568 | <td><div style="vertical-align: middle; width: 420px; overflow: auto;"> |
569 | <pre>FATAL ERROR |
570 | </pre> |
571 | @@ -185,12 +190,14 @@ |
572 | <td>job 2</td> |
573 | <td style='font-weight: bold; color: #6AA84F'>passed</td> |
574 | <td>unspecified</td> |
575 | + <td>1</td> |
576 | <td>blah blah</td> |
577 | </tr> |
578 | <tr> |
579 | <td>job 3</td> |
580 | <td style='font-weight: bold; color: #FF9900'>skipped</td> |
581 | <td>unspecified</td> |
582 | + <td>1</td> |
583 | <td>No such device</td> |
584 | </tr> |
585 | </tbody> |
tested with both gui and cli using the smoke testplan. all the results are available in the report.
+1