Skip to content

Commit 76d5d9e

Browse files
committed
kirk: Add results JSON to HTML convertor script
Signed-off-by: Cyril Hrubis <chrubis@suse.cz> Reviewed-by: Petr Vorel <pvorel@suse.cz> Reviewed-by: Andrea Cervesato <andrea.cervesato@suse.com>
1 parent e0d6a2d commit 76d5d9e

File tree

1 file changed

+335
-0
lines changed

1 file changed

+335
-0
lines changed

utils/json2html.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0-or-later
3+
# Copyright (c) 2025 Cyril Hrubis <chrubis@suse.cz>
4+
"""
5+
This script parses JSON results from kirk and produces a HTML page.
6+
"""
7+
import re
8+
import os
9+
import json
10+
import argparse
11+
from datetime import timedelta
12+
from html import escape
13+
14+
_HTML_HEADER="""<html>
15+
<head>
16+
<meta charset=\"UTF-8\">
17+
<title>LTP results</title>
18+
<style>
19+
body {
20+
background-color: #eee;
21+
font: 80% \"Helvetica\";
22+
text-align: center;
23+
}
24+
table {border-collapse: collapse;}
25+
th, td {
26+
border-bottom: 1px solid #888;
27+
text-align: right;
28+
padding-top: 0.1em;
29+
padding-bottom: 0.1em;
30+
padding-left: 0.5em;
31+
padding-right: 0.5em;
32+
}
33+
th {
34+
border-top: 1px solid #888;
35+
background-color: #ccc;
36+
}
37+
tr {
38+
border-left: 1px solid #888;
39+
border-right: 1px solid #888;
40+
}
41+
hr {border-top: 1px solid #888; border-bottom: 0px;}
42+
td:hover.rtime {background-color: #ccf}
43+
td:hover.pass {background-color: #9f9}
44+
td:hover.fail {background-color: #f99}
45+
td:hover.brok {background-color: #f99}
46+
td:hover.skip {background-color: #ff9}
47+
td:hover.warn {background-color: #f9f}
48+
td.rtime {background-color: #aaf; text-align: center;}
49+
td.pass {background-color: #7f7; text-align: center;}
50+
td.fail {background-color: #f77; text-align: center;}
51+
td.brok {background-color: #f77; text-align: center;}
52+
td.skip {background-color: #ff7; text-align: center;}
53+
td.warn {background-color: #f7f; text-align: center;}
54+
th.id, td.id {text-align: left; width: 15em;}
55+
th:hover {background-color: #bbb}
56+
tr:hover.info {background-color: #eee}
57+
tr:hover.pass {background-color: #9f9}
58+
tr:hover.fail {background-color: #f99}
59+
tr:hover.brok {background-color: #f99}
60+
tr:hover.skip {background-color: #ff9}
61+
tr:hover.warn {background-color: #f9f}
62+
tr.info {background-color: #ddd; text-align: left;}
63+
tr.pass {background-color: #7f7}
64+
tr.fail {background-color: #f77}
65+
tr.brok {background-color: #f77}
66+
tr.skip {background-color: #ff7}
67+
tr.warn {background-color: #f7f}
68+
tr.hidden1 {display: none}
69+
tr.hidden2 {display: none}
70+
tr.hidden3 {display: none}
71+
tr.logs {background-color: #bbb;}
72+
tr:hover.logs {background-color: #ccc;}
73+
td.logs {text-align: left}
74+
table.hidden {display: none}
75+
</style>
76+
<script type=\"text/javascript\">
77+
function toggle_visibility(element, class_id) {
78+
var table = document.getElementById(\"results\");
79+
table.classList.add(\"hidden\");
80+
if (element.checked) {
81+
for (var i = 1; table.rows[i]; i+=2) {
82+
if (table.rows[i].classList.contains(class_id)) {
83+
table.rows[i].classList.add(\"hidden1\");
84+
table.rows[i+1].classList.add(\"hidden1\");
85+
}
86+
}
87+
} else {
88+
for (var i = 1; table.rows[i]; i+=2) {
89+
if (table.rows[i].classList.contains(class_id)) {
90+
table.rows[i].classList.remove(\"hidden1\");
91+
table.rows[i+1].classList.remove(\"hidden1\");
92+
}
93+
}
94+
}
95+
table.classList.remove(\"hidden\");
96+
}
97+
function filter_by_id(substr) {
98+
var table = document.getElementById(\"results\");
99+
table.classList.add(\"hidden\");
100+
for (var i = 1; table.rows[i]; i+=2) {
101+
if (table.rows[i].cells[0].innerText.includes(substr)) {
102+
table.rows[i].classList.remove(\"hidden2\");
103+
table.rows[i+1].classList.remove(\"hidden2\");
104+
} else {
105+
table.rows[i].classList.add(\"hidden2\");
106+
table.rows[i+1].classList.add(\"hidden2\");
107+
}
108+
}
109+
table.classList.remove(\"hidden\");
110+
}
111+
function cmp_asc(row1, row2, cell_id) {
112+
var h1 = row1.cells[cell_id].innerHTML;
113+
var h2 = row2.cells[cell_id].innerHTML;
114+
if (cell_id == 0) return h1 < h2
115+
return parseFloat(h1) < parseFloat(h2);
116+
}
117+
function cmp_desc(row1, row2, cell_id) {
118+
var h1 = row1.cells[cell_id].innerHTML;
119+
var h2 = row2.cells[cell_id].innerHTML;
120+
if (cell_id == 0) return h1 > h2
121+
return parseFloat(h1) > parseFloat(h2);
122+
}
123+
function sort(cmp, cell_id) {
124+
var table = document.getElementById(\"results\");
125+
table.classList.add(\"hidden\");
126+
for (var i = 3; table.rows[i]; i+=2) {
127+
var l = 1, r = i, m;
128+
while (r - l > 2) {
129+
/* Find odd table row in the middle */
130+
m = (r - l)/2 + l + ((((r - l)/2) % 2) ? 1 : 0);
131+
if (cmp(table.rows[i], table.rows[m], cell_id))
132+
r = m;
133+
else
134+
l = m;
135+
}
136+
m = cmp(table.rows[l], table.rows[i], cell_id) ? r : l;
137+
if (i == m)
138+
continue;
139+
var rowi1 = table.rows[i];
140+
var rowi2 = table.rows[i+1];
141+
var row = table.rows[m];
142+
rowi1.parentNode.insertBefore(rowi1, row);
143+
rowi2.parentNode.insertBefore(rowi2, row);
144+
}
145+
table.classList.remove(\"hidden\");
146+
}
147+
function sort_by(cell_id) {
148+
var table = document.getElementById(\"results\");
149+
var id_col = table.rows[0].cells[cell_id].innerHTML;
150+
if (id_col.endsWith(\"\\u2191\")) {
151+
sort(cmp_desc, cell_id);
152+
table.rows[0].cells[cell_id].innerHTML = id_col.slice(0, -1) + \"\\u2193\";
153+
} else {
154+
sort(cmp_asc, cell_id);
155+
table.rows[0].cells[cell_id].innerHTML = id_col.slice(0, -1) + \"\\u2191\";
156+
}
157+
}
158+
</script>
159+
</head>
160+
<body>
161+
<div style=\"display: inline-block\">
162+
<center>
163+
<h1>LTP Results</h1>"""
164+
165+
_HTML_FOOTER=""" </center>
166+
</div>
167+
<script type=\"text/javascript\">
168+
var table = document.getElementById(\"results\");
169+
for (var i = 1; table.rows[i]; i++) {
170+
table.rows[i].onclick = function() {
171+
if (this.classList.contains(\"logs\")) {
172+
this.classList.add(\"hidden3\");
173+
} else {
174+
var next_row = this.parentNode.rows[this.rowIndex + 1];
175+
if (next_row.classList.contains(\"hidden3\"))
176+
next_row.classList.remove(\"hidden3\");
177+
else
178+
next_row.classList.add(\"hidden3\");
179+
}
180+
}
181+
}
182+
</script>
183+
</body>
184+
</html>"""
185+
186+
def _generate_environment(environment):
187+
"""
188+
Generates HTML environment table.
189+
"""
190+
out = []
191+
out.append(" <table width=\"100%\">")
192+
out.append(" <tr>")
193+
out.append(" <th colspan=\"2\" style=\"text-align: center\">Environment information</th>")
194+
out.append(" </tr>")
195+
196+
print("\n".join(out))
197+
198+
for key in environment:
199+
out = []
200+
201+
out.append(" <tr class=\"info\">")
202+
out.append(f" <td>{key}</td>")
203+
out.append(f" <td>{environment[key]}</td>")
204+
out.append(" </tr>")
205+
206+
print("\n".join(out))
207+
208+
print(" </table>")
209+
210+
def _generate_stats(stats):
211+
"""
212+
Generates HTML overall statistics.
213+
"""
214+
215+
out = []
216+
out.append(" <table width=\"100%\">")
217+
out.append(" <tr>")
218+
out.append(" <th colspan=\"6\" style=\"text-align: center\">Overall results</th>")
219+
out.append(" </tr>")
220+
out.append(" <tr>")
221+
out.append(f" <td class=\"rtime\">Runtime: {str(timedelta(seconds=stats['runtime']))}</td>")
222+
out.append(f" <td class=\"pass\">Passed: {stats['passed']}</td>")
223+
out.append(f" <td class=\"skip\">Skipped: {stats['skipped']}</td>")
224+
out.append(f" <td class=\"fail\">Failed: {stats['failed']}</td>")
225+
out.append(f" <td class=\"brok\">Broken: {stats['broken']}</td>")
226+
out.append(f" <td class=\"warn\">Warnings: {stats['warnings']}</td>")
227+
out.append(" </tr>")
228+
out.append(" </table>")
229+
230+
print("\n".join(out))
231+
232+
_RESULT_TABLE_HEADER=""" <div style=\"background-color: #ccc\">
233+
<hr>
234+
<input type=\"checkbox\" onchange=\"toggle_visibility(this, 'pass')\"> Hide Passed
235+
<input type=\"checkbox\" onchange=\"toggle_visibility(this, 'skip')\"> Hide Skipped
236+
Filter by ID: <input type=\"text\" onkeyup=\"filter_by_id(this.value)\">
237+
<hr>
238+
</div>
239+
<table id=\"results\" style=\"cursor: pointer\">
240+
<tr>
241+
<th onclick=\"sort_by(0)\" class=\"id\">Test ID &#8597;</th>
242+
<th onclick=\"sort_by(1)\">Duration &#8597;</th>
243+
<th onclick=\"sort_by(2)\">Passes &#8597;</th>
244+
<th onclick=\"sort_by(3)\">Skips &#8597;</th>
245+
<th onclick=\"sort_by(4)\">Fails &#8597;</th>
246+
<th onclick=\"sort_by(5)\">Broken &#8597;</th>
247+
<th onclick=\"sort_by(6)\">Warns &#8597;</th>
248+
</tr>"""
249+
250+
def _generate_results(results):
251+
"""
252+
generates html result table.
253+
"""
254+
print(_RESULT_TABLE_HEADER)
255+
256+
for res in results:
257+
overall = 'pass'
258+
259+
test = res['test'];
260+
261+
if test['failed'] > 0:
262+
overall = 'fail'
263+
elif test['broken'] > 0:
264+
overall = 'brok'
265+
elif test['warnings'] > 0:
266+
overall = 'warn'
267+
elif test['skipped'] > 0:
268+
overall = 'skip'
269+
270+
out = []
271+
272+
out.append(f" <tr class=\"{overall}\">")
273+
out.append(f" <td class=\"id\">{res['test_fqn']}</td>")
274+
out.append(f" <td>{'%.2f' % test['duration']}</td>")
275+
out.append(f" <td>{test['passed']}</td>")
276+
out.append(f" <td>{test['skipped']}</td>")
277+
out.append(f" <td>{test['failed']}</td>")
278+
out.append(f" <td>{test['broken']}</td>")
279+
out.append(f" <td>{test['warnings']}</td>")
280+
out.append(" </tr>")
281+
out.append(" <tr class=\"logs hidden3\">")
282+
out.append(" <td class=\"logs\" colspan=\"8\">")
283+
out.append(" <pre>")
284+
out.append(escape(test['log']) + " </pre>")
285+
out.append(" </td>")
286+
out.append(" </tr>")
287+
288+
print("\n".join(out))
289+
290+
print(" </table>")
291+
292+
def _generate_html(results_path):
293+
"""
294+
Generates HTML results.
295+
"""
296+
print(_HTML_HEADER)
297+
298+
with open(results_path, 'r', encoding="utf-8") as file:
299+
results = json.load(file)
300+
301+
_generate_environment(results['environment']);
302+
_generate_stats(results['stats']);
303+
_generate_results(results['results']);
304+
305+
print(_HTML_FOOTER)
306+
307+
def _file_exists(filepath):
308+
"""
309+
Check if the given file path exists.
310+
"""
311+
if not os.path.isfile(filepath):
312+
raise argparse.ArgumentTypeError(
313+
f"The file '{filepath}' does not exist.")
314+
return filepath
315+
316+
def run():
317+
"""
318+
Entry point of the script.
319+
"""
320+
parser = argparse.ArgumentParser(
321+
description="Script to generate simple HTML result table.")
322+
323+
parser.add_argument(
324+
'-r',
325+
'--results',
326+
type=_file_exists,
327+
required=True,
328+
help='kirk results.json file location')
329+
330+
args = parser.parse_args()
331+
332+
_generate_html(args.results)
333+
334+
if __name__ == "__main__":
335+
run()

0 commit comments

Comments
 (0)