Merge ~sylvain-pineau/checkbox-ng:fix-1817029 into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- fix-1817029
- Merge into master
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 42acde10e7671d996a14519149c8477b206f689c |
Merged at revision: | 0763920b80b0509e907973793b23c33ebb3b7d0f |
Proposed branch: | ~sylvain-pineau/checkbox-ng:fix-1817029 |
Merge into: | checkbox-ng:master |
Diff against target: |
304 lines (+68/-23) 6 files modified
checkbox_ng/launcher/merge_reports.py (+21/-4) plainbox/impl/exporter/jinja2.py (+2/-4) plainbox/impl/providers/exporters/data/checkbox.html (+7/-7) plainbox/impl/providers/exporters/data/checkbox.json (+9/-1) plainbox/impl/providers/exporters/data/multi-page.html (+8/-7) plainbox/impl/session/state.py (+21/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Pierre Equoy | Approve | ||
Checkbox Developers | Pending | ||
Review via email: mp+369818@code.launchpad.net |
Commit message
Description of the change
Fixes the linked bug.
This MR is mostly based on Pierre's https:/
The only difference is the category mapping used to render html reports (category_
Bonus: a Fix to dkms_info_json to remove looging recorded on sdterr and messing up the json validation.
Sylvain Pineau (sylvain-pineau) wrote : | # |
Pierre Equoy (pieq) wrote : | # |
Tested the latest fix like so:
1. checkbox-cli, select 18.04 test plan, then a few jobs from it (e.g. optical/detect ...)
2. during the execution, mark one of the manual jobs as Failed
→ You get a submission (let's call it sub1.tar.xz)
3. re-launch checkbox-cli, select the same test plans and the same few jobs
4. during the execution, mark the job you marked as Failed in the previous run as Passed
→ You get a submission (let's call it sub2.tar.xz)
5. Let's try to merge one submission with itself to make sure we get the same results:
$ checkbox-cli merge-submissions -o /tmp/selfmerge.
→ Both HTML and JSON files are the same in selfmerge.tar.xz and sub1.tar.xz (OK)
6. Now let's merge 2 different submissions to make sure the feature actually works as expected :)
$ checkbox-cli merge-submissions -o /tmp/selfmerge.
→ Since I have selected exactly the same jobs for sub1 and sub2, and marked the job as Passed in sub2, selfmerge.tar.xz and sub2.tar.xz HTML and json files are identical (OK)
7. Let's merge the submissions, but in reverse order:
$ checkbox-cli merge-submissions -o /tmp/selfmerge.
This time, selfmerge.tar.xz HTML and json files are similar to the ones in sub1.tar.xz (OK)
+1 to land this.
Thanks a lot Sylvain !
(I've noticed the submission title disappearance, but I think we'll live without it :))
Preview Diff
1 | diff --git a/checkbox_ng/launcher/merge_reports.py b/checkbox_ng/launcher/merge_reports.py |
2 | index 99fecaf..2eed566 100644 |
3 | --- a/checkbox_ng/launcher/merge_reports.py |
4 | +++ b/checkbox_ng/launcher/merge_reports.py |
5 | @@ -37,6 +37,10 @@ from plainbox.impl.unit.category import CategoryUnit |
6 | from plainbox.impl.unit.job import JobDefinition |
7 | |
8 | |
9 | +#: Name-space prefix for Canonical Certification |
10 | +CERTIFICATION_NS = 'com.canonical.certification::' |
11 | + |
12 | + |
13 | class MergeReports(Command): |
14 | name = 'merge-reports' |
15 | |
16 | @@ -58,6 +62,10 @@ class MergeReports(Command): |
17 | for result in data['results']: |
18 | result['plugin'] = 'shell' # Required so default to shell |
19 | result['summary'] = result['name'] |
20 | + # 'id' field in json file only contains partial id |
21 | + result['id'] = result.get('full_id', result['id']) |
22 | + if "::" not in result['id']: |
23 | + result['id'] = CERTIFICATION_NS + result['id'] |
24 | if mode == "list": |
25 | self.job_list.append(JobDefinition(result)) |
26 | elif mode == "dict": |
27 | @@ -65,6 +73,10 @@ class MergeReports(Command): |
28 | for result in data['resource-results']: |
29 | result['plugin'] = 'resource' |
30 | result['summary'] = result['name'] |
31 | + # 'id' field in json file only contains partial id |
32 | + result['id'] = result.get('full_id', result['id']) |
33 | + if "::" not in result['id']: |
34 | + result['id'] = CERTIFICATION_NS + result['id'] |
35 | if mode == "list": |
36 | self.job_list.append(JobDefinition(result)) |
37 | elif mode == "dict": |
38 | @@ -72,6 +84,10 @@ class MergeReports(Command): |
39 | for result in data['attachment-results']: |
40 | result['plugin'] = 'attachment' |
41 | result['summary'] = result['name'] |
42 | + # 'id' field in json file only contains partial id |
43 | + result['id'] = result.get('full_id', result['id']) |
44 | + if "::" not in result['id']: |
45 | + result['id'] = CERTIFICATION_NS + result['id'] |
46 | if mode == "list": |
47 | self.job_list.append(JobDefinition(result)) |
48 | elif mode == "dict": |
49 | @@ -98,7 +114,8 @@ class MergeReports(Command): |
50 | keepends=True)) |
51 | ] |
52 | result = MemoryJobResult({ |
53 | - 'outcome': job.get_record_value('status'), |
54 | + 'outcome': job.get_record_value('outcome', |
55 | + job.get_record_value('status')), |
56 | 'comments': job.get_record_value('comments'), |
57 | 'execution_duration': job.get_record_value('duration'), |
58 | 'io_log': io_log, |
59 | @@ -111,12 +128,12 @@ class MergeReports(Command): |
60 | new_resource_list.append(resource) |
61 | if not new_resource_list: |
62 | new_resource_list = [Resource({})] |
63 | - state.set_resource_list( |
64 | - "com.canonical.certification::" + job.id, |
65 | - new_resource_list) |
66 | + state.set_resource_list(job.id, new_resource_list) |
67 | job_state = state.job_state_map[job.id] |
68 | job_state.effective_category_id = job.get_record_value( |
69 | 'category_id', 'com.canonical.plainbox::uncategorised') |
70 | + job_state.effective_certification_status = job.get_record_value( |
71 | + 'certification_status', 'unspecified') |
72 | |
73 | def _create_exporter(self, exporter_id): |
74 | exporter_map = {} |
75 | diff --git a/plainbox/impl/exporter/jinja2.py b/plainbox/impl/exporter/jinja2.py |
76 | index 59d7b3b..c6dcfc8 100644 |
77 | --- a/plainbox/impl/exporter/jinja2.py |
78 | +++ b/plainbox/impl/exporter/jinja2.py |
79 | @@ -49,10 +49,8 @@ CERTIFICATION_NS = 'com.canonical.certification::' |
80 | @environmentfilter |
81 | def do_strip_ns(_environment, unit_id, ns=CERTIFICATION_NS): |
82 | """Remove the namespace part of the identifier.""" |
83 | - if unit_id.startswith(ns): |
84 | - rv = unit_id[len(ns):] |
85 | - else: |
86 | - rv = unit_id |
87 | + # com.my.namespace::category/job-id → category/job-id |
88 | + rv = unit_id.split("::")[-1] |
89 | rv = escape(rv) |
90 | if _environment.autoescape: |
91 | rv = Markup(rv) |
92 | diff --git a/plainbox/impl/providers/exporters/data/checkbox.html b/plainbox/impl/providers/exporters/data/checkbox.html |
93 | index 88ecb47..9c80c81 100644 |
94 | --- a/plainbox/impl/providers/exporters/data/checkbox.html |
95 | +++ b/plainbox/impl/providers/exporters/data/checkbox.html |
96 | @@ -2,7 +2,7 @@ |
97 | {%- set state = manager.default_device_context.state -%} |
98 | {%- set resource_map = state.resource_map -%} |
99 | {%- set job_state_map = state.job_state_map -%} |
100 | -{%- set category_map = state.category_map -%} |
101 | +{%- set category_map = state.category_map_lite -%} |
102 | {%- set category_outcome_map = state.category_outcome_map -%} |
103 | {%- set resource_global_outcome = state.resource_global_outcome -%} |
104 | {%- set attachment_global_outcome = state.attachment_global_outcome -%} |
105 | @@ -177,7 +177,7 @@ |
106 | <tbody> |
107 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.effective_category_id == cat_id and job_state.job.plugin not in ("resource", "attachment") %} |
108 | <tr> |
109 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
110 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
111 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
112 | {%- if job_state.effective_certification_status != "unspecified" %} |
113 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
114 | @@ -208,9 +208,9 @@ |
115 | </thead> |
116 | <tbody> |
117 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "resource" %} |
118 | - {%- if job_state.job.partial_id == "package" %} |
119 | + {%- if job_id|strip_ns == "package" %} |
120 | <tr> |
121 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
122 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
123 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
124 | {%- if job_state.effective_certification_status != "unspecified" %} |
125 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
126 | @@ -226,7 +226,7 @@ |
127 | </tr> |
128 | {%- else %} |
129 | <tr> |
130 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
131 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
132 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
133 | {%- if job_state.effective_certification_status != "unspecified" %} |
134 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
135 | @@ -258,7 +258,7 @@ |
136 | <tbody> |
137 | {%- endif %} |
138 | <tr> |
139 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
140 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
141 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
142 | {%- if job_state.effective_certification_status != "unspecified" %} |
143 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
144 | @@ -329,7 +329,7 @@ |
145 | {%- endfor %} |
146 | {%- endfor %} |
147 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "resource" %} |
148 | - {%- if job_state.result.io_log_as_flat_text != "" and job_state.job.partial_id != "package" %} |
149 | + {%- if job_state.result.io_log_as_flat_text != "" and job_id|strip_ns != "package" %} |
150 | <div class="jqm-demos ui-page" tabindex="0" data-url="resource-{{ loop.index }}" id="resource-{{ loop.index }}-log" data-role="page"> |
151 | <div data-role="header" class="jqm-header"> |
152 | <h1>{{ job_state.job.tr_summary() }}</h1> |
153 | diff --git a/plainbox/impl/providers/exporters/data/checkbox.json b/plainbox/impl/providers/exporters/data/checkbox.json |
154 | index 7f40ade..3c8e204 100644 |
155 | --- a/plainbox/impl/providers/exporters/data/checkbox.json |
156 | +++ b/plainbox/impl/providers/exporters/data/checkbox.json |
157 | @@ -46,11 +46,13 @@ |
158 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin not in ("resource", "attachment") %} |
159 | { |
160 | "id": "{{ job_id|strip_ns }}", |
161 | + "full_id": "{{ job_id }}", |
162 | "name": "{{ job_state.job.tr_summary() }}", |
163 | "certification_status": "{{ job_state.effective_certification_status }}", |
164 | "category": "{{ category_map[job_state.effective_category_id] }}", |
165 | "category_id": "{{ job_state.effective_category_id }}", |
166 | "status": "{{ job_state.result.outcome_meta().hexr_mapping }}", |
167 | + "outcome": "{{ job_state.result.outcome }}", |
168 | "comments": {{ job_state.result.comments | jsonify | safe }}, |
169 | "io_log": {{ job_state.result.io_log_as_flat_text | jsonify | safe }}, |
170 | "type": "test", |
171 | @@ -63,10 +65,13 @@ |
172 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "resource" %} |
173 | { |
174 | "id": "{{ job_id|strip_ns }}", |
175 | + "full_id": "{{ job_id }}", |
176 | "name": "{{ job_state.job.tr_summary() }}", |
177 | "certification_status": "{{ job_state.effective_certification_status }}", |
178 | "category": "{{ category_map[job_state.effective_category_id] }}", |
179 | + "category_id": "{{ job_state.effective_category_id }}", |
180 | "status": "{{ job_state.result.outcome_meta().hexr_mapping }}", |
181 | + "outcome": "{{ job_state.result.outcome }}", |
182 | "comments": {{ job_state.result.comments | jsonify | safe }}, |
183 | "io_log": {{ job_state.result.io_log_as_flat_text | jsonify | safe }}, |
184 | "type": "test", |
185 | @@ -79,10 +84,13 @@ |
186 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "attachment" %} |
187 | { |
188 | "id": "{{ job_id|strip_ns }}", |
189 | + "full_id": "{{ job_id }}", |
190 | "name": "{{ job_state.job.tr_summary() }}", |
191 | "certification_status": "{{ job_state.effective_certification_status }}", |
192 | "category": "{{ category_map[job_state.effective_category_id] }}", |
193 | + "category_id": "{{ job_state.effective_category_id }}", |
194 | "status": "{{ job_state.result.outcome_meta().hexr_mapping }}", |
195 | + "outcome": "{{ job_state.result.outcome }}", |
196 | "comments": {{ job_state.result.comments | jsonify | safe }}, |
197 | "io_log": {{ job_state.result.io_log_as_text_attachment | jsonify | safe }}, |
198 | "duration": {{ job_state.result.execution_duration if job_state.result.execution_duration else 0}} |
199 | @@ -95,7 +103,7 @@ |
200 | {%- endfor %} |
201 | } |
202 | {%- if ns ~ 'dkms_info_json' in state.job_state_map and state.job_state_map[ns ~ 'dkms_info_json'].result.outcome == 'pass' %}, |
203 | -{%- set dkms_info_json = state.job_state_map[ns ~ 'dkms_info_json'].result.io_log_as_text_attachment %} |
204 | +{%- set dkms_info_json = '{' + state.job_state_map[ns ~ 'dkms_info_json'].result.io_log_as_text_attachment.split('{', 1)[-1] %} |
205 | "dkms_info": {{ dkms_info_json | indent(4, false) | safe }} |
206 | {%- endif %} |
207 | {%- if ns ~ 'udev_json' in state.job_state_map and state.job_state_map[ns ~ 'udev_json'].result.outcome == 'pass' %}, |
208 | diff --git a/plainbox/impl/providers/exporters/data/multi-page.html b/plainbox/impl/providers/exporters/data/multi-page.html |
209 | index 6e511b4..aee86ec 100644 |
210 | --- a/plainbox/impl/providers/exporters/data/multi-page.html |
211 | +++ b/plainbox/impl/providers/exporters/data/multi-page.html |
212 | @@ -65,10 +65,11 @@ |
213 | {% for manager in manager_list %} |
214 | {% set managerloop = loop %} |
215 | {%- set metadata = manager.state.metadata %} |
216 | +{%- set ns = 'com.canonical.certification::' -%} |
217 | {%- set state = manager.default_device_context.state -%} |
218 | {%- set resource_map = state.resource_map -%} |
219 | {%- set job_state_map = state.job_state_map -%} |
220 | -{%- set category_map = state.category_map -%} |
221 | +{%- set category_map = state.category_map_lite -%} |
222 | {%- set category_outcome_map = state.category_outcome_map -%} |
223 | {%- set resource_global_outcome = state.resource_global_outcome -%} |
224 | {%- set attachment_global_outcome = state.attachment_global_outcome -%} |
225 | @@ -224,7 +225,7 @@ |
226 | <tbody> |
227 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.effective_category_id == cat_id and job_state.job.plugin not in ("resource", "attachment") %} |
228 | <tr> |
229 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
230 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
231 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
232 | {%- if job_state.effective_certification_status != "unspecified" %} |
233 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
234 | @@ -255,9 +256,9 @@ |
235 | </thead> |
236 | <tbody> |
237 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "resource" %} |
238 | - {%- if job_state.job.partial_id == "package" %} |
239 | + {%- if job_id|strip_ns == "package" %} |
240 | <tr> |
241 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
242 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
243 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
244 | {%- if job_state.effective_certification_status != "unspecified" %} |
245 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
246 | @@ -273,7 +274,7 @@ |
247 | </tr> |
248 | {%- else %} |
249 | <tr> |
250 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
251 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
252 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
253 | {%- if job_state.effective_certification_status != "unspecified" %} |
254 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
255 | @@ -305,7 +306,7 @@ |
256 | <tbody> |
257 | {%- endif %} |
258 | <tr> |
259 | - <td data-filtertext="{{ job_state.job.partial_id }}" style='width:35%'>{{ job_state.job.partial_id }}</td> |
260 | + <td data-filtertext="{{ job_id|strip_ns }}" style='width:35%'>{{ job_id|strip_ns }}</td> |
261 | <td style='width:10%; font-weight: bold; color: {{ job_state.result.outcome_meta().color_hex }}'>{{ job_state.result.outcome_meta().tr_label }}</td> |
262 | {%- if job_state.effective_certification_status != "unspecified" %} |
263 | <td style='width:10%'>{{ job_state.effective_certification_status }}</td> |
264 | @@ -386,7 +387,7 @@ |
265 | <!-- Resources I/O log pages --> |
266 | |
267 | {%- for job_id, job_state in job_state_map|dictsort if job_state.result.outcome != None and job_state.job.plugin == "resource" %} |
268 | -{%- if job_state.result.io_log_as_flat_text != "" and job_state.job.partial_id != "package" %} |
269 | +{%- if job_state.result.io_log_as_flat_text != "" and job_id|strip_ns != "package" %} |
270 | <div class="jqm-demos ui-page" tabindex="0" data-url="resource-{{ managerloop.index }}-{{ loop.index }}" id="resource-{{ managerloop.index }}-{{ loop.index }}-log" data-role="page"> |
271 | <div data-role="header" class="jqm-header"> |
272 | <h1>{{ job_state.job.tr_summary() }}</h1> |
273 | diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py |
274 | index bd7bdef..833bbb8 100644 |
275 | --- a/plainbox/impl/session/state.py |
276 | +++ b/plainbox/impl/session/state.py |
277 | @@ -1246,6 +1246,27 @@ class SessionState: |
278 | wanted_category_ids = frozenset({ |
279 | job_state.effective_category_id |
280 | for job_state in self.job_state_map.values() |
281 | + if job_state.result.outcome != None |
282 | + }) |
283 | + return { |
284 | + unit.id: unit.tr_name() |
285 | + for unit in self.unit_list |
286 | + if unit.Meta.name == 'category' |
287 | + and unit.id in wanted_category_ids |
288 | + } |
289 | + |
290 | + @property |
291 | + def category_map_lite(self): |
292 | + """ |
293 | + Map from category id to their corresponding translated names but |
294 | + without taking into account resources and attachments jobs. |
295 | + |
296 | + Meant to be used to generate the HTML reports where resources and |
297 | + attachments are presented in different sections. |
298 | + """ |
299 | + wanted_category_ids = frozenset({ |
300 | + job_state.effective_category_id |
301 | + for job_state in self.job_state_map.values() |
302 | if job_state.result.outcome != None and |
303 | job_state.job.plugin not in ("resource", "attachment") |
304 | }) |
if "::" not in result['id']:
The fallback to result['id'] actually makes thsi code compatible with old submissions (e.g the one linked in the bug report: https:/ /certification. canonical. com/hardware/ 201812- 26755/submissio n/138731/)