Publish EUnit/CT HTML reports to ci-reports host.
Refs EventHub/EventHubBack#62
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert Surefire/JUnit XML (eunit_surefire) into a simple HTML report."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_suites(paths: list[Path]) -> list[ET.Element]:
|
||||
suites: list[ET.Element] = []
|
||||
for path in paths:
|
||||
try:
|
||||
root = ET.parse(path).getroot()
|
||||
except ET.ParseError as exc:
|
||||
print(f"skip {path}: {exc}", file=sys.stderr)
|
||||
continue
|
||||
if root.tag == "testsuite":
|
||||
suites.append(root)
|
||||
elif root.tag == "testsuites":
|
||||
suites.extend(list(root.findall("testsuite")))
|
||||
return suites
|
||||
|
||||
|
||||
def render(suites: list[ET.Element], title: str) -> str:
|
||||
total = failures = errors = skipped = 0
|
||||
rows: list[str] = []
|
||||
for suite in suites:
|
||||
s_name = suite.attrib.get("name", "suite")
|
||||
total += int(suite.attrib.get("tests", "0") or 0)
|
||||
failures += int(suite.attrib.get("failures", "0") or 0)
|
||||
errors += int(suite.attrib.get("errors", "0") or 0)
|
||||
skipped += int(suite.attrib.get("skipped", "0") or 0)
|
||||
for case in suite.findall("testcase"):
|
||||
name = case.attrib.get("name", "?")
|
||||
classname = case.attrib.get("classname", s_name)
|
||||
time_s = case.attrib.get("time", "")
|
||||
fail = case.find("failure")
|
||||
err = case.find("error")
|
||||
skip = case.find("skipped")
|
||||
if fail is not None:
|
||||
status, detail = "FAIL", fail.attrib.get("message") or (fail.text or "")
|
||||
elif err is not None:
|
||||
status, detail = "ERROR", err.attrib.get("message") or (err.text or "")
|
||||
elif skip is not None:
|
||||
status, detail = "SKIP", skip.attrib.get("message") or ""
|
||||
else:
|
||||
status, detail = "PASS", ""
|
||||
cls = status.lower()
|
||||
rows.append(
|
||||
"<tr class=\"{cls}\"><td>{status}</td><td>{suite}</td><td>{case}</td>"
|
||||
"<td>{time}</td><td><pre>{detail}</pre></td></tr>".format(
|
||||
cls=html.escape(cls),
|
||||
status=html.escape(status),
|
||||
suite=html.escape(classname),
|
||||
case=html.escape(name),
|
||||
time=html.escape(time_s),
|
||||
detail=html.escape(detail.strip()[:4000]),
|
||||
)
|
||||
)
|
||||
|
||||
ok = total - failures - errors - skipped
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{html.escape(title)}</title>
|
||||
<style>
|
||||
body {{ font-family: system-ui, sans-serif; margin: 1.5rem; }}
|
||||
.summary span {{ margin-right: 1rem; }}
|
||||
.pass {{ color: #0a7; }}
|
||||
.fail, .error {{ color: #c22; }}
|
||||
.skip {{ color: #a80; }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin-top: 1rem; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: .4rem .6rem; vertical-align: top; text-align: left; }}
|
||||
th {{ background: #f4f4f4; }}
|
||||
pre {{ white-space: pre-wrap; margin: 0; font-size: 12px; }}
|
||||
tr.fail, tr.error {{ background: #fff5f5; }}
|
||||
tr.pass {{ background: #f5fff8; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{html.escape(title)}</h1>
|
||||
<p class="summary">
|
||||
<span>tests: <b>{total}</b></span>
|
||||
<span class="pass">pass: <b>{ok}</b></span>
|
||||
<span class="fail">fail: <b>{failures}</b></span>
|
||||
<span class="error">error: <b>{errors}</b></span>
|
||||
<span class="skip">skip: <b>{skipped}</b></span>
|
||||
</p>
|
||||
<table>
|
||||
<thead><tr><th>Status</th><th>Suite</th><th>Case</th><th>Time</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
{''.join(rows) if rows else '<tr><td colspan="5">No testcases</td></tr>'}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("xml_dir", type=Path, help="Directory with TEST-*.xml")
|
||||
ap.add_argument("-o", "--out", type=Path, required=True, help="Output HTML file or dir")
|
||||
ap.add_argument("--title", default="EUnit report")
|
||||
args = ap.parse_args()
|
||||
|
||||
xml_files = sorted(args.xml_dir.glob("TEST-*.xml")) + sorted(args.xml_dir.glob("*.xml"))
|
||||
# de-dupe
|
||||
seen = set()
|
||||
unique: list[Path] = []
|
||||
for p in xml_files:
|
||||
if p.resolve() in seen:
|
||||
continue
|
||||
seen.add(p.resolve())
|
||||
unique.append(p)
|
||||
|
||||
suites = load_suites(unique)
|
||||
out = args.out
|
||||
if out.suffix.lower() != ".html":
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
out = out / "index.html"
|
||||
else:
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(render(suites, args.title), encoding="utf-8")
|
||||
print(f"wrote {out} ({len(suites)} suites, {len(unique)} xml files)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Удаляет каталоги отчётов старше CI_REPORTS_KEEP_DAYS (default 14).
|
||||
# Usage: prune-ci-reports.sh [/var/eventhub/ci-reports]
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${1:-${CI_REPORTS_DIR:-/var/eventhub/ci-reports}}"
|
||||
KEEP_DAYS="${CI_REPORTS_KEEP_DAYS:-14}"
|
||||
|
||||
if [[ ! -d "${ROOT}" ]]; then
|
||||
echo "ci-reports: ${ROOT} отсутствует — пропуск"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ci-reports prune: root=${ROOT} keep_days=${KEEP_DAYS}"
|
||||
find "${ROOT}" -mindepth 2 -maxdepth 2 -type d -mtime "+${KEEP_DAYS}" -print -exec rm -rf {} + 2>/dev/null || true
|
||||
echo "ci-reports prune: done"
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish HTML CI report tree to local runner host and print HTTPS URL.
|
||||
# Usage: publish-ci-report.sh <repo> <run_id> <kind> <src_dir>
|
||||
# repo: EventHubBack | EventHubFront | …
|
||||
# run_id: usually GITHUB_RUN_NUMBER or sha
|
||||
# kind: eunit | ct | allure | e2e-ift | e2e-stage | …
|
||||
# src_dir: directory with HTML to copy
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${1:?repo}"
|
||||
RUN_ID="${2:?run_id}"
|
||||
KIND="${3:?kind}"
|
||||
SRC="${4:?src_dir}"
|
||||
|
||||
REPORTS_DIR="${CI_REPORTS_DIR:-/var/eventhub/ci-reports}"
|
||||
STAND="${RUNNER_STAND:-}"
|
||||
|
||||
detect_stand() {
|
||||
if [[ -n "${CI_REPORTS_BASE_URL:-}" ]]; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${STAND}" ]]; then
|
||||
echo "${STAND}"
|
||||
return 0
|
||||
fi
|
||||
if getent hosts ci-reports.ift.eventhub.local >/dev/null 2>&1 \
|
||||
|| getent ahostsv4 ci-reports.ift.eventhub.local >/dev/null 2>&1; then
|
||||
# Prefer ift if this host resolves ift reports (IFT runner DNS)
|
||||
if [[ -f /opt/eventhub-ift/.env ]] || [[ -d /opt/eventhub-ift ]]; then
|
||||
echo "ift"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ -d /opt/eventhub-ift ]]; then
|
||||
echo "ift"
|
||||
return 0
|
||||
fi
|
||||
echo "dev"
|
||||
}
|
||||
|
||||
if [[ -n "${CI_REPORTS_BASE_URL:-}" ]]; then
|
||||
BASE_URL="${CI_REPORTS_BASE_URL%/}"
|
||||
else
|
||||
STAND="$(detect_stand)"
|
||||
BASE_URL="https://ci-reports.${STAND}.eventhub.local"
|
||||
fi
|
||||
|
||||
if [[ ! -d "${SRC}" ]]; then
|
||||
echo "publish-ci-report: src missing: ${SRC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST="${REPORTS_DIR}/${REPO}/${RUN_ID}/${KIND}"
|
||||
mkdir -p "${REPORTS_DIR}/${REPO}/${RUN_ID}"
|
||||
rm -rf "${DEST}"
|
||||
mkdir -p "${DEST}"
|
||||
cp -a "${SRC}/." "${DEST}/"
|
||||
|
||||
# Convenience: CT puts index under ct_run.*; expose DEST/index.html
|
||||
if [[ ! -f "${DEST}/index.html" ]]; then
|
||||
CT_INDEX="$(find "${DEST}" -type f -name index.html 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "${CT_INDEX}" ]]; then
|
||||
REL="${CT_INDEX#"${DEST}/"}"
|
||||
cat > "${DEST}/index.html" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="refresh" content="0; url=${REL}"/>
|
||||
<title>CT report</title>
|
||||
</head><body>
|
||||
<p><a href="${REL}">Open Common Test report</a></p>
|
||||
</body></html>
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Refresh run index
|
||||
INDEX="${REPORTS_DIR}/${REPO}/${RUN_ID}/index.html"
|
||||
{
|
||||
echo "<!DOCTYPE html>"
|
||||
echo "<html lang=\"ru\"><head><meta charset=\"utf-8\"/><title>${REPO} #${RUN_ID}</title>"
|
||||
echo "<style>body{font-family:system-ui,sans-serif;margin:2rem}a{display:block;margin:.4rem 0}</style>"
|
||||
echo "</head><body>"
|
||||
echo "<h1>${REPO} — run ${RUN_ID}</h1>"
|
||||
echo "<ul>"
|
||||
for d in "${REPORTS_DIR}/${REPO}/${RUN_ID}"/*/; do
|
||||
[[ -d "${d}" ]] || continue
|
||||
name="$(basename "${d}")"
|
||||
if [[ -f "${d}index.html" ]]; then
|
||||
echo "<li><a href=\"./${name}/index.html\">${name}</a></li>"
|
||||
else
|
||||
echo "<li><a href=\"./${name}/\">${name}/</a></li>"
|
||||
fi
|
||||
done
|
||||
echo "</ul></body></html>"
|
||||
} > "${INDEX}"
|
||||
|
||||
URL="${BASE_URL}/${REPO}/${RUN_ID}/${KIND}/"
|
||||
INDEX_URL="${BASE_URL}/${REPO}/${RUN_ID}/"
|
||||
echo "Report: ${URL}"
|
||||
echo "Report index: ${INDEX_URL}"
|
||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||
{
|
||||
echo "### Test report"
|
||||
echo "- [${KIND}](${URL})"
|
||||
echo "- [index](${INDEX_URL})"
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
@@ -114,8 +114,13 @@ if [[ "${have_image}" -ne 1 ]]; then
|
||||
.
|
||||
fi
|
||||
|
||||
CT_OUT="${CT_REPORT_DIR:-$(pwd)/ci-out/ct-${STAND}}"
|
||||
mkdir -p "${CT_OUT}"
|
||||
|
||||
set +e
|
||||
docker run --rm --network=host \
|
||||
${docker_add_hosts[@]+"${docker_add_hosts[@]}"} \
|
||||
-v "${CT_OUT}:/app/logs/test/ct" \
|
||||
-e CT_MODE=remote \
|
||||
-e "API_HOST=${API_HOST}" \
|
||||
-e "ADMIN_API_HOST=${ADMIN_API_HOST}" \
|
||||
@@ -130,5 +135,18 @@ docker run --rm --network=host \
|
||||
-e "ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-}" \
|
||||
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
|
||||
"${IMAGE}"
|
||||
CT_RC=$?
|
||||
set -e
|
||||
|
||||
if [[ -d "${CT_OUT}" ]] && compgen -G "${CT_OUT}/*" >/dev/null; then
|
||||
RUN_ID="${GITHUB_RUN_NUMBER:-local}"
|
||||
mkdir -p /var/eventhub/ci-reports 2>/dev/null || true
|
||||
bash scripts/publish-ci-report.sh EventHubBack "${RUN_ID}" "e2e-${STAND}" "${CT_OUT}" || true
|
||||
fi
|
||||
|
||||
if [[ "${CT_RC}" -ne 0 ]]; then
|
||||
echo "=== API CT FAILED (${STAND}) rc=${CT_RC} ===" >&2
|
||||
exit "${CT_RC}"
|
||||
fi
|
||||
|
||||
echo "=== API CT passed (${STAND}) ==="
|
||||
|
||||
Reference in New Issue
Block a user