ClickHouse/tests/ci/report.py
2023-01-06 06:28:51 +03:00

373 lines
12 KiB
Python

# -*- coding: utf-8 -*-
import os
import datetime
### BEST FRONTEND PRACTICES BELOW
HTML_BASE_TEST_TEMPLATE = """
<!DOCTYPE html>
<html>
<style>
.gradient {{
background-image: linear-gradient(90deg, #8F8, #F88);
background-size: 100%;
background-repeat: repeat;
background-clip: text;
-webkit-text-fill-color: transparent;
-moz-text-fill-color: transparent;
-webkit-background-clip: text;
-moz-background-clip: text;
}}
html {{ font-family: "DejaVu Sans", "Noto Sans", Arial, sans-serif; background: linear-gradient(180deg, hsl(190deg, 90%, 10%), hsl(190deg, 90%, 0%)); color: white; }}
h1 {{ margin-left: 10px; }}
th, td {{ border: 0; padding: 5px 10px 5px 10px; text-align: left; vertical-align: top; line-height: 1.5; background: hsl(190deg, 90%, 15%); }}
th {{ background: hsl(180deg, 90%, 15%); }}
a {{ color: white; text-decoration: none; }}
a:hover, a:active {{ color: #F40; text-decoration: none; }}
table {{ border: 0; box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 1); }}
p.links a {{ padding: 5px; margin: 3px; background: hsl(190deg, 90%, 20%); line-height: 2.5; white-space: nowrap; box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 1); }}
p.links a:hover {{ background: hsl(190deg, 100%, 50%); color: black; }}
th {{ cursor: pointer; }}
tr:hover {{ filter: brightness(120%); }}
.failed {{ cursor: pointer; }}
.failed-content {{ display: none; }}
#fish {{ display: none; float: right; position: relative; top: -20em; right: 10vw; margin-bottom: -20em; width: 30vh; filter: brightness(3%); z-index: -1; }}
</style>
<title>{title}</title>
</head>
<body>
<div class="main">
<h1><span class="gradient">{header}</span></h1>
<p class="links">
<a href="{raw_log_url}">{raw_log_name}</a>
<a href="{commit_url}">Commit</a>
{additional_urls}
<a href="{task_url}">Task (github actions)</a>
<a href="{job_url}">Job (github actions)</a>
</p>
{test_part}
<img id="fish" src="https://presentations.clickhouse.com/images/fish.png" />
<script type="text/javascript">
/// Straight from https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript
const getCellValue = (tr, idx) => {{
var classes = tr.classList;
var elem = tr;
if (classes.contains("failed-content") || classes.contains("failed-content.open"))
elem = tr.previousElementSibling;
return elem.children[idx].innerText || elem.children[idx].textContent;
}}
const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {{
const table = th.closest('table');
Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
.sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
.forEach(tr => table.appendChild(tr) );
}})));
Array.from(document.getElementsByClassName("failed")).forEach(tr => tr.addEventListener('click', function() {{
var content = this.nextElementSibling;
content.classList.toggle("failed-content.open");
content.classList.toggle("failed-content");
}}));
if (document.body.clientHeight > 3000) {{
document.getElementById('fish').style.display = 'block';
}}
</script>
</body>
</html>
"""
HTML_TEST_PART = """
<table>
<tr>
{headers}
</tr>
{rows}
</table>
"""
BASE_HEADERS = ["Test name", "Test status"]
class ReportColorTheme:
class ReportColor:
yellow = "#FFB400"
red = "#F00"
green = "#0A0"
blue = "#00B4FF"
default = (ReportColor.green, ReportColor.red, ReportColor.yellow)
bugfixcheck = (ReportColor.yellow, ReportColor.blue, ReportColor.blue)
def _format_header(header, branch_name, branch_url=None):
result = " ".join([w.capitalize() for w in header.split(" ")])
result = result.replace("Clickhouse", "ClickHouse")
result = result.replace("clickhouse", "ClickHouse")
if "ClickHouse" not in result:
result = "ClickHouse " + result
result += " for "
if branch_url:
result += f'<a href="{branch_url}">{branch_name}</a>'
else:
result += branch_name
return result
def _get_status_style(status, colortheme=None):
ok_statuses = ("OK", "success", "PASSED")
fail_statuses = ("FAIL", "failure", "error", "FAILED", "Timeout")
if colortheme is None:
colortheme = ReportColorTheme.default
style = "font-weight: bold;"
if status in ok_statuses:
style += f"color: {colortheme[0]};"
elif status in fail_statuses:
style += f"color: {colortheme[1]};"
else:
style += f"color: {colortheme[2]};"
return style
def _get_html_url_name(url):
if isinstance(url, str):
return os.path.basename(url).replace("%2B", "+").replace("%20", " ")
if isinstance(url, tuple):
return url[1].replace("%2B", "+").replace("%20", " ")
return None
def _get_html_url(url):
href = None
name = None
if isinstance(url, str):
href, name = url, _get_html_url_name(url)
if isinstance(url, tuple):
href, name = url[0], _get_html_url_name(url)
if href and name:
return f'<a href="{href}">{_get_html_url_name(url)}</a>'
return ""
def create_test_html_report(
header,
test_result,
raw_log_url,
task_url,
job_url,
branch_url,
branch_name,
commit_url,
additional_urls=None,
with_raw_logs=False,
statuscolors=None,
):
if additional_urls is None:
additional_urls = []
if test_result:
rows_part = ""
num_fails = 0
has_test_time = False
has_test_logs = False
if with_raw_logs:
# Display entires with logs at the top (they correspond to failed tests)
test_result.sort(key=lambda result: len(result) <= 3)
for result in test_result:
test_name = result[0]
test_status = result[1]
test_logs = None
test_time = None
if len(result) > 2:
test_time = result[2]
has_test_time = True
if len(result) > 3:
test_logs = result[3]
has_test_logs = True
row = "<tr>"
is_fail = test_status in ("FAIL", "FLAKY")
if is_fail and with_raw_logs and test_logs is not None:
row = '<tr class="failed">'
row += "<td>" + test_name + "</td>"
style = _get_status_style(test_status, colortheme=statuscolors)
# Allow to quickly scroll to the first failure.
is_fail_id = ""
if is_fail:
num_fails = num_fails + 1
is_fail_id = 'id="fail' + str(num_fails) + '" '
row += f'<td {is_fail_id}style="{style}">{test_status}</td>'
if test_time is not None:
row += "<td>" + test_time + "</td>"
if test_logs is not None and not with_raw_logs:
test_logs_html = "<br>".join([_get_html_url(url) for url in test_logs])
row += "<td>" + test_logs_html + "</td>"
row += "</tr>"
rows_part += row
if test_logs is not None and with_raw_logs:
row = '<tr class="failed-content">'
# TODO: compute colspan too
row += '<td colspan="3"><pre>' + test_logs + "</pre></td>"
row += "</tr>"
rows_part += row
headers = BASE_HEADERS
if has_test_time:
headers.append("Test time, sec.")
if has_test_logs and not with_raw_logs:
headers.append("Logs")
headers_html = "".join(["<th>" + h + "</th>" for h in headers])
test_part = HTML_TEST_PART.format(headers=headers_html, rows=rows_part)
else:
test_part = ""
additional_html_urls = " ".join(
[_get_html_url(url) for url in sorted(additional_urls, key=_get_html_url_name)]
)
raw_log_name = os.path.basename(raw_log_url)
if "?" in raw_log_name:
raw_log_name = raw_log_name.split("?")[0]
result = HTML_BASE_TEST_TEMPLATE.format(
title=_format_header(header, branch_name),
header=_format_header(header, branch_name, branch_url),
raw_log_name=raw_log_name,
raw_log_url=raw_log_url,
task_url=task_url,
job_url=job_url,
test_part=test_part,
branch_name=branch_name,
commit_url=commit_url,
additional_urls=additional_html_urls,
)
return result
HTML_BASE_BUILD_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: "DejaVu Sans", "Noto Sans", Arial, sans-serif; background: #EEE; }}
h1 {{ margin-left: 10px; }}
th, td {{ border: 0; padding: 5px 10px 5px 10px; text-align: left; vertical-align: top; line-height: 1.5; background-color: #FFF;
border: 0; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 8px 25px -5px rgba(0, 0, 0, 0.1); }}
a {{ color: #06F; text-decoration: none; }}
a:hover, a:active {{ color: #F40; text-decoration: underline; }}
table {{ border: 0; }}
.main {{ margin: auto; }}
p.links a {{ padding: 5px; margin: 3px; background: #FFF; line-height: 2; white-space: nowrap; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 8px 25px -5px rgba(0, 0, 0, 0.1); }}
tr:hover td {{filter: brightness(95%);}}
</style>
<title>{title}</title>
</head>
<body>
<div class="main">
<h1>{header}</h1>
<table>
<tr>
<th>Compiler</th>
<th>Build type</th>
<th>Sanitizer</th>
<th>Status</th>
<th>Build log</th>
<th>Build time</th>
<th class="artifacts">Artifacts</th>
</tr>
{rows}
</table>
<p class="links">
<a href="{commit_url}">Commit</a>
<a href="{task_url}">Task (github actions)</a>
</p>
</body>
</html>
"""
LINK_TEMPLATE = '<a href="{url}">{text}</a>'
def create_build_html_report(
header,
build_results,
build_logs_urls,
artifact_urls_list,
task_url,
branch_url,
branch_name,
commit_url,
):
rows = ""
for (build_result, build_log_url, artifact_urls) in zip(
build_results, build_logs_urls, artifact_urls_list
):
row = "<tr>"
row += f"<td>{build_result.compiler}</td>"
if build_result.build_type:
row += f"<td>{build_result.build_type}</td>"
else:
row += "<td>relwithdebuginfo</td>"
if build_result.sanitizer:
row += f"<td>{build_result.sanitizer}</td>"
else:
row += "<td>none</td>"
if build_result.status:
style = _get_status_style(build_result.status)
row += f'<td style="{style}">{build_result.status}</td>'
else:
style = _get_status_style("error")
row += f'<td style="{style}">error</td>'
row += f'<td><a href="{build_log_url}">link</a></td>'
if build_result.elapsed_seconds:
delta = datetime.timedelta(seconds=build_result.elapsed_seconds)
else:
delta = "unknown" # type: ignore
row += f"<td>{delta}</td>"
links = ""
link_separator = "<br/>"
if artifact_urls:
for artifact_url in artifact_urls:
links += LINK_TEMPLATE.format(
text=_get_html_url_name(artifact_url), url=artifact_url
)
links += link_separator
if links:
links = links[: -len(link_separator)]
row += f"<td>{links}</td>"
row += "</tr>"
rows += row
return HTML_BASE_BUILD_TEMPLATE.format(
title=_format_header(header, branch_name),
header=_format_header(header, branch_name, branch_url),
rows=rows,
task_url=task_url,
branch_name=branch_name,
commit_url=commit_url,
)