Last active
November 16, 2022 01:49
-
-
Save sshnaidm/7d69c2c27008f5570ff9a2b773fc8062 to your computer and use it in GitHub Desktop.
Junit to HTML in a nice way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import codecs | |
import re | |
from jinja2 import Template | |
from xml.sax import saxutils | |
from junitparser import JUnitXml | |
HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<title>{{ title }}</title> | |
<meta name="generator" content="{{ generator }}"/> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> | |
{{ stylesheet }} | |
</head> | |
<body> | |
<script language="javascript" type="text/javascript"><!-- | |
output_list = Array(); | |
/* level - 0:Summary; 1:Failed; 2:All */ | |
function showCase(level) { | |
trs = document.getElementsByTagName("tr"); | |
for (var i = 0; i < trs.length; i++) { | |
tr = trs[i]; | |
id = tr.id; | |
if (id.substr(0,2) == 'ft') { | |
if (level < 1) { | |
tr.className = 'hiddenRow'; | |
} | |
else { | |
tr.className = ''; | |
} | |
} | |
if (id.substr(0,2) == 'pt') { | |
if (level > 1) { | |
tr.className = ''; | |
} | |
else { | |
tr.className = 'hiddenRow'; | |
} | |
} | |
} | |
} | |
function showClassDetail(cid, count) { | |
var id_list = Array(count); | |
var toHide = 1; | |
for (var i = 0; i < count; i++) { | |
tid0 = 't' + cid.substr(1) + '.' + (i+1); | |
tid = 'f' + tid0; | |
tr = document.getElementById(tid); | |
if (!tr) { | |
tid = 'p' + tid0; | |
tr = document.getElementById(tid); | |
} | |
id_list[i] = tid; | |
if (tr.className) { | |
toHide = 0; | |
} | |
} | |
for (var i = 0; i < count; i++) { | |
tid = id_list[i]; | |
if (toHide) { | |
document.getElementById('div_'+tid).style.display = 'none' | |
document.getElementById(tid).className = 'hiddenRow'; | |
} | |
else { | |
document.getElementById(tid).className = ''; | |
} | |
} | |
} | |
function showTestDetail(div_id){ | |
var details_div = document.getElementById(div_id) | |
var displayState = details_div.style.display | |
// alert(displayState) | |
if (displayState != 'block' ) { | |
displayState = 'block' | |
details_div.style.display = 'block' | |
} | |
else { | |
details_div.style.display = 'none' | |
} | |
} | |
function html_escape(s) { | |
s = s.replace(/&/g,'&'); | |
s = s.replace(/</g,'<'); | |
s = s.replace(/>/g,'>'); | |
return s; | |
} | |
/* obsoleted by detail in <div> | |
function showOutput(id, name) { | |
var w = window.open("", //url | |
name, | |
"resizable,scrollbars,status,width=800,height=450"); | |
d = w.document; | |
d.write("<pre>"); | |
d.write(html_escape(output_list[id])); | |
d.write("\n"); | |
d.write("<a href='javascript:window.close()'>close</a>\n"); | |
d.write("</pre>\n"); | |
d.close(); | |
} | |
*/ | |
--></script> | |
{{ heading }} | |
{{ report }} | |
{{ rending }} | |
</body> | |
</html> | |
""" | |
STYLESHEET_TMPL = """ | |
<style type="text/css" media="screen"> | |
body { font-family: verdana, arial, helvetica, sans-serif; | |
font-size: 80%; } | |
table { font-size: 110%; width: 100%;} | |
pre { font-size: 80%; } | |
/* -- heading -------------------------------------------------------------- */ | |
h1 { | |
font-size: 26pt; | |
color: gray; | |
} | |
.heading { | |
margin-top: 0ex; | |
margin-bottom: 1ex; | |
} | |
.heading .attribute { | |
margin-top: 1ex; | |
margin-bottom: 0; | |
} | |
.heading .description { | |
margin-top: 4ex; | |
margin-bottom: 6ex; | |
} | |
/* -- css div popup -------------------------------------------------------- */ | |
a.popup_link { | |
} | |
a.popup_link:hover { | |
color: red; | |
} | |
.popup_window { | |
display: none; | |
overflow-x: scroll; | |
/*border: solid #627173 1px; */ | |
padding: 10px; | |
background-color: #E6E6D6; | |
font-family: "Ubuntu Mono", "Lucida Console", "Courier New", monospace; | |
text-align: left; | |
font-size: 10pt; | |
} | |
} | |
/* -- report --------------------------------------------------------------- */ | |
# show_detail_line { | |
margin-top: 3ex; | |
margin-bottom: 1ex; | |
} | |
# result_table { | |
width: 100%; | |
border-collapse: collapse; | |
border: 1px solid #777; | |
} | |
# header_row { | |
font-weight: bold; | |
color: white; | |
background-color: #777; | |
} | |
# result_table td { | |
border: 1px solid #777; | |
padding: 2px; | |
} | |
# total_row { font-weight: bold; } | |
.passClass { background-color: #6c6; font-weight: bold; font-size: 120%;} | |
.skipClass { background-color: #bababa; font-weight: bold; font-size: 120%;} | |
.failClass { background-color: #c60; font-weight: bold; font-size: 120%;} | |
.errorClass { background-color: #c00; font-weight: bold; font-size: 120%;} | |
.passCase { color: black; background-color: #c6ffc2; } | |
.failCase { color: #763b00; font-weight: bold; background-color: #ffc2c8; } | |
.errorCase { color: #c00; font-weight: bold;} | |
.skipCase { color: #0068df; font-weight: bold; background-color: #e1e1e1; } | |
.hiddenRow { display: none; } | |
.testcase { margin-left: 2em; } | |
td.testname {width: 40%} | |
td.small {width: 40px} | |
/* -- ending --------------------------------------------------------------- */ | |
# ending { | |
} | |
</style> | |
""" | |
HEADING_TMPL = """<div class='heading'> | |
<h1>{{ title }}</h1> | |
{{ parameters }} | |
<p class='description'>{{ description }}</p> | |
</div> | |
""" | |
HEADING_ATTRIBUTE_TMPL = """ | |
<p class='attribute'><strong>{{ name }}:</strong> {{ value }}</p> | |
""" | |
REPORT_TMPL = """ | |
<p id='show_detail_line'>Show | |
<a href='javascript:showCase(0)'>Summary</a> | |
<a href='javascript:showCase(1)'>Failed</a> | |
<a href='javascript:showCase(2)'>All</a> | |
</p> | |
<table id='result_table'> | |
<colgroup> | |
<col align='left' /> | |
<col align='right' /> | |
<col align='right' /> | |
<col align='right' /> | |
<col align='right' /> | |
<col align='right' /> | |
<col align='right' /> | |
<col align='right' /> | |
</colgroup> | |
<tr id='header_row'> | |
<td>Test Group/Test case</td> | |
<td>Count</td> | |
<td>Pass</td> | |
<td>Fail</td> | |
<td>Error</td> | |
<td>Skip</td> | |
<td>View</td> | |
<td> </td> | |
</tr> | |
{{ test_list }} | |
<tr id='total_row'> | |
<td>Total</td> | |
<td>{{ count }}</td> | |
<td>{{ Pass }}</td> | |
<td>{{ fail }}</td> | |
<td>{{ error }}</td> | |
<td>{{ skip }}</td> | |
<td> </td> | |
<td> </td> | |
</tr> | |
</table> | |
""" | |
REPORT_CLASS_TMPL = r""" | |
<tr class='{{ style }}'> | |
<td class="testname">{{ desc }}</td> | |
<td class="small">{{ count }}</td> | |
<td class="small">{{ Pass }}</td> | |
<td class="small">{{ fail }}</td> | |
<td class="small">{{ error }}</td> | |
<td class="small">{{ skip }}</td> | |
<td class="small"><a href="javascript:showClassDetail('{{ cid }}',{{ count }})" | |
>Detail</a></td> | |
<td> </td> | |
</tr> | |
""" | |
REPORT_TEST_WITH_OUTPUT_TMPL = r""" | |
<tr id='{{ tid }}' class='{{ Class }}'> | |
<td class='{{ style }}'><div class='testcase'>{{ desc }}</div></td> | |
<td colspan='7' align='left'> | |
<!--css div popup start--> | |
<a class="popup_link" onfocus='this.blur();' | |
href="javascript:showTestDetail('div_{{ tid }}')" > | |
{{ status }}</a> | |
<div id='div_{{ tid }}' class="popup_window"> | |
<div style='text-align: right; color:red;cursor:pointer'> | |
<a onfocus='this.blur();' | |
onclick="document.getElementById('div_{{ tid }}').style.display = 'none' " > | |
[x]</a> | |
</div> | |
<pre> | |
{{ script }} | |
</pre> | |
</div> | |
<!--css div popup end--> | |
</td> | |
</tr> | |
""" | |
REPORT_TEST_NO_OUTPUT_TMPL = r""" | |
<tr id='{{ tid }}' class='{{ Class }}'> | |
<td class='{{ style }}'><div class='testcase'>{{ desc }}</div></td> | |
<td colspan='6' align='center'>{{ status }}</td> | |
</tr> | |
""" # variables: (tid, Class, style, desc, status) | |
REPORT_TEST_OUTPUT_TMPL = r""" | |
{{ id }}: {{ output }} | |
""" | |
ENDING_TMPL = """<div id='ending'> </div>""" | |
DEFAULT_TITLE = 'CNF Test Report' | |
DEFAULT_DESCRIPTION = '' | |
def getReportAttributes(test_data): | |
"""Return report attributes as a list of (name, value).""" | |
status = [] | |
if test_data['success_count']: | |
status.append('Pass %s' % test_data['success_count']) | |
if test_data['failure_count']: | |
status.append('Failure %s' % test_data['failure_count']) | |
if test_data['error_count']: | |
status.append('Error %s' % test_data['error_count']) | |
if test_data['skip_count']: | |
status.append('Skip %s' % test_data['skip_count']) | |
if status: | |
status = ' '.join(status) | |
else: | |
status = 'none' | |
return [ | |
('Status', status), | |
] | |
def generate_heading(test_data): | |
report_attrs = getReportAttributes(test_data) | |
a_lines = [] | |
for name, value in report_attrs: | |
line = Template(HEADING_ATTRIBUTE_TMPL).render( | |
name=saxutils.escape(name), | |
value=saxutils.escape(value), | |
) | |
a_lines.append(line) | |
heading = Template(HEADING_TMPL).render( | |
title=saxutils.escape(Template(DEFAULT_TITLE).render()), | |
parameters=''.join(a_lines), | |
description=saxutils.escape(Template(DEFAULT_DESCRIPTION).render()), | |
) | |
return heading | |
def generate_report_test(rows, tid, cid, test): | |
status = "error" | |
test_txt = "" | |
if test.is_passed: | |
status = "passed" | |
test_txt = test.name | |
elif test.is_skipped: | |
status = "skipped" | |
test_txt = (test.result[0].text or '') if test.result else test.name | |
elif test.result and test.result[0].type == 'Failure': | |
status = "failed" | |
test_txt = test.result[0].text or '' | |
has_output = bool(test.system_out or test.system_err or test_txt) | |
tid = "t%s.%s" % (cid + 1, tid + 1) | |
tid = "p%s" % tid if status in ('passed', 'skipped') else "f%s" % tid | |
name = test.name | |
desc = name | |
# tmpl = (has_output and REPORT_TEST_WITH_OUTPUT_TMPL or REPORT_TEST_NO_OUTPUT_TMPL) | |
tmpl = REPORT_TEST_WITH_OUTPUT_TMPL | |
try: | |
output = saxutils.escape( | |
(test.system_out or '') + (test.system_err or '') + test_txt) | |
# We expect to get this exception in python2. | |
except UnicodeDecodeError: | |
e = codecs.decode(test.system_err or '', 'utf-8') | |
o = codecs.decode(test.system_out or '', 'utf-8') | |
tt = codecs.decode(test_txt or '', 'utf-8') | |
output = saxutils.escape(o + e + tt) | |
script = Template(REPORT_TEST_OUTPUT_TMPL).render( | |
id=tid, | |
output=output, | |
) | |
row = Template(tmpl).render( | |
tid=tid, | |
Class=((status in ['skipped', 'passed']) and 'hiddenRow' or 'none'), | |
style=(status == 'error' and 'errorCase' or | |
(status == 'failed' and 'failCase' or | |
(status == 'skipped' and 'skipCase' or | |
(status == 'passed' and 'passCase' or | |
'none')))), | |
desc=desc, | |
script=script, | |
status=status, | |
) | |
rows.append(row) | |
if not has_output: | |
return | |
def generate_report(test_data, xml): | |
rfe_sub = re.compile(r"\[r[fe][fe]_id:[^\]]+\]") | |
clac = re.compile(r"^(\[[^\]]+\])+") | |
# Groups tests by Feature name - [sriov], [pao], etc | |
clasd_tests = {'unknown': []} | |
for c in xml: | |
name = c.name | |
if clac.search(name): | |
cl_type = clac.search(name).group() | |
if 'ref_id' in cl_type or 'rfe_id' in cl_type: | |
cl_type = rfe_sub.sub("", cl_type) | |
if cl_type not in clasd_tests: | |
clasd_tests[cl_type] = [c] | |
else: | |
clasd_tests[cl_type].append(c) | |
else: | |
clasd_tests['unknown'].append(c) | |
rows = [] | |
for cid, t_class in enumerate(list(clasd_tests.keys())): | |
tests = clasd_tests[t_class] | |
desc = "%s tests suite" % t_class.capitalize() | |
pa = [] | |
fa = [] | |
sk = [] | |
er = [] | |
for t in tests: | |
if t.is_passed: | |
pa.append(t) | |
elif t.is_skipped: | |
sk.append(t) | |
elif t.result and t.result[0].type == 'Failure': | |
fa.append(t) | |
else: | |
er.append(t) | |
ne, nf, ns, np = len(er), len(fa), len(sk), len(pa) | |
all_skipped = len(er) + len(fa) + len(sk) + len(pa) == len(sk) | |
rows.append( | |
Template(REPORT_CLASS_TMPL).render( | |
style=(ne > 0 and 'errorClass' | |
or nf > 0 and 'failClass' | |
or all_skipped and 'skipClass' | |
or 'passClass'), | |
desc=desc, | |
count=np + nf + ne + ns, | |
Pass=np, | |
fail=nf, | |
error=ne, | |
skip=ns, | |
cid='c%s' % (cid + 1), | |
)) | |
for tid, t in enumerate(tests): | |
generate_report_test(rows, tid, cid, t) | |
report = Template(REPORT_TMPL).render( | |
test_list=''.join(rows), | |
count=str(test_data['success_count'] + test_data['failure_count'] + | |
test_data['error_count'] + test_data['skip_count']), | |
Pass=str(test_data['success_count']), | |
fail=str(test_data['failure_count']), | |
error=str(test_data['error_count']), | |
skip=str(test_data['skip_count']), | |
) | |
return report | |
def get_stat(xml): | |
res = { | |
'success_count': 0, | |
'failure_count': 0, | |
'error_count': 0, | |
'skip_count': 0, | |
} | |
# for suite in xml: | |
for t in xml: | |
if t.is_passed: | |
res['success_count'] += 1 | |
elif t.is_skipped: | |
res['skip_count'] += 1 | |
elif t.result and t.result[0].type == 'Failure': | |
res['failure_count'] += 1 | |
else: | |
res['error_count'] += 1 | |
return res | |
def main(): | |
parser = argparse.ArgumentParser( | |
description="Extract tasks from a playbook." | |
) | |
parser.add_argument( | |
"--output", | |
"-o", | |
help="Output file. Default: cnf_result.html", | |
default="cnf_result.html", | |
) | |
parser.add_argument( | |
"files", | |
nargs="+", | |
help="Files to extract tests from.", | |
) | |
args = parser.parse_args() | |
all_xml = JUnitXml.fromfile(args.files[0]) | |
for i in args.files[1:]: | |
all_xml += JUnitXml.fromfile(i) | |
data = get_stat(all_xml) | |
html_template = Template(HTML_TMPL) | |
html = html_template.render( | |
title=DEFAULT_TITLE, | |
generator="j2html", | |
stylesheet=Template(STYLESHEET_TMPL).render(), | |
heading=generate_heading(data), | |
report=generate_report(data, all_xml), | |
ending=Template(ENDING_TMPL).render(), | |
) | |
with open(args.output, "wb") as f: | |
f.write(html.encode('utf8')) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment