Merge ~sylvain-pineau/checkbox-ng:fix-1817029 into checkbox-ng:master

Proposed by Sylvain Pineau
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)
Reviewer Review Type Date Requested Status
Pierre Equoy Approve
Checkbox Developers Pending
Review via email: mp+369818@code.launchpad.net

Description of the change

Fixes the linked bug.

This MR is mostly based on Pierre's https://code.launchpad.net/~pieq/checkbox-ng/+git/checkbox-ng/+merge/367634

The only difference is the category mapping used to render html reports (category_map_lite).

Bonus: a Fix to dkms_info_json to remove looging recorded on sdterr and messing up the json validation.

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

                result['id'] = result.get('full_id', result['id'])
                if "::" not in result['id']:
                    result['id'] = CERTIFICATION_NS + 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/submission/138731/)

Revision history for this message
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.tar.xz sub1.tar.xz sub1.tar.xz

→ 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.tar.xz sub1.tar.xz sub2.tar.xz

→ 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.tar.xz sub2.tar.xz sub1.tar.xz

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 :))

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_ng/launcher/merge_reports.py b/checkbox_ng/launcher/merge_reports.py
2index 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 = {}
75diff --git a/plainbox/impl/exporter/jinja2.py b/plainbox/impl/exporter/jinja2.py
76index 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)
92diff --git a/plainbox/impl/providers/exporters/data/checkbox.html b/plainbox/impl/providers/exporters/data/checkbox.html
93index 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>
153diff --git a/plainbox/impl/providers/exporters/data/checkbox.json b/plainbox/impl/providers/exporters/data/checkbox.json
154index 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' %},
208diff --git a/plainbox/impl/providers/exporters/data/multi-page.html b/plainbox/impl/providers/exporters/data/multi-page.html
209index 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>
273diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py
274index 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 })

Subscribers

People subscribed via source and target branches