Publish EUnit/CT HTML reports to ci-reports host.
Refs EventHub/EventHubBack#62
This commit is contained in:
+33
-3
@@ -114,12 +114,42 @@ jobs:
|
|||||||
.
|
.
|
||||||
|
|
||||||
- name: Run unit tests (EUnit)
|
- name: Run unit tests (EUnit)
|
||||||
run: >
|
id: eunit
|
||||||
docker run --rm eventhub-tests:latest
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p ci-out/eunit ci-out/eunit-html
|
||||||
|
set +e
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/ci-out/eunit:/app/logs/test/eunit" \
|
||||||
|
eventhub-tests:latest \
|
||||||
rebar3 eunit --sname ci_eunit --verbose
|
rebar3 eunit --sname ci_eunit --verbose
|
||||||
|
EUNIT_RC=$?
|
||||||
|
set -e
|
||||||
|
python3 scripts/junit-xml-to-html.py ci-out/eunit -o ci-out/eunit-html --title "EUnit CI" || true
|
||||||
|
exit "${EUNIT_RC}"
|
||||||
|
|
||||||
- name: Run API tests (local CT)
|
- name: Run API tests (local CT)
|
||||||
run: docker run --rm eventhub-tests:latest
|
id: ct
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p ci-out/ct
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/ci-out/ct:/app/logs/test/ct" \
|
||||||
|
eventhub-tests:latest
|
||||||
|
|
||||||
|
- name: Publish HTML reports (ci-reports)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUN_ID="${GITHUB_RUN_NUMBER:-local}"
|
||||||
|
mkdir -p /var/eventhub/ci-reports || sudo mkdir -p /var/eventhub/ci-reports || true
|
||||||
|
if [[ -d ci-out/eunit-html ]] && compgen -G 'ci-out/eunit-html/*' >/dev/null; then
|
||||||
|
bash scripts/publish-ci-report.sh EventHubBack "${RUN_ID}" eunit ci-out/eunit-html || true
|
||||||
|
fi
|
||||||
|
if [[ -d ci-out/ct ]] && compgen -G 'ci-out/ct/*' >/dev/null; then
|
||||||
|
bash scripts/publish-ci-report.sh EventHubBack "${RUN_ID}" ct ci-out/ct || true
|
||||||
|
fi
|
||||||
|
bash scripts/prune-ci-reports.sh || true
|
||||||
|
|
||||||
- name: Push tested eventhub image
|
- name: Push tested eventhub image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ erl_crash.dump
|
|||||||
.rebar
|
.rebar
|
||||||
logs
|
logs
|
||||||
data
|
data
|
||||||
|
ci-out
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
rebar3.crashdump
|
rebar3.crashdump
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
autoindex on;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* /allure/ {
|
||||||
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,19 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|
||||||
|
# ================== CI HTML reports (local runner) ==================
|
||||||
|
ci-reports:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
volumes:
|
||||||
|
- "${CI_REPORTS_HOST_DIR:-/var/eventhub/ci-reports}:/usr/share/nginx/html:ro"
|
||||||
|
- "./ci-reports/nginx.conf:/etc/nginx/conf.d/default.conf:ro"
|
||||||
|
networks:
|
||||||
|
- eventhub-net
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: any
|
||||||
|
|
||||||
# ================== Admin UI ==================
|
# ================== Admin UI ==================
|
||||||
admin-ui:
|
admin-ui:
|
||||||
image: eventhub-admin-ui:latest
|
image: eventhub-admin-ui:latest
|
||||||
|
|||||||
@@ -116,6 +116,17 @@ http:
|
|||||||
tls: true
|
tls: true
|
||||||
service: "client-ui-service"
|
service: "client-ui-service"
|
||||||
|
|
||||||
|
ci-reports:
|
||||||
|
rule: "Host(`ci-reports.dev.eventhub.local`)"
|
||||||
|
entryPoints: ["web"]
|
||||||
|
middlewares: ["redirect-to-https"]
|
||||||
|
service: "ci-reports"
|
||||||
|
ci-reports-secure:
|
||||||
|
rule: "Host(`ci-reports.dev.eventhub.local`)"
|
||||||
|
entryPoints: ["websecure"]
|
||||||
|
tls: true
|
||||||
|
service: "ci-reports"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
failover:
|
failover:
|
||||||
@@ -172,3 +183,8 @@ http:
|
|||||||
loadbalancer:
|
loadbalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://client-ui:80"
|
- url: "http://client-ui:80"
|
||||||
|
|
||||||
|
ci-reports:
|
||||||
|
loadbalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://ci-reports:80"
|
||||||
|
|||||||
@@ -49,6 +49,11 @@
|
|||||||
{verbose, true} % Print more info to console
|
{verbose, true} % Print more info to console
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{eunit_opts, [
|
||||||
|
verbose,
|
||||||
|
{report, {eunit_surefire, [{dir, "logs/test/eunit"}]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
{ct_compile_opts, [
|
{ct_compile_opts, [
|
||||||
{i, "include"}, % Include directory
|
{i, "include"}, % Include directory
|
||||||
{d, 'DEBUG'} % Define macros
|
{d, 'DEBUG'} % Define macros
|
||||||
|
|||||||
@@ -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
|
fi
|
||||||
|
|
||||||
|
CT_OUT="${CT_REPORT_DIR:-$(pwd)/ci-out/ct-${STAND}}"
|
||||||
|
mkdir -p "${CT_OUT}"
|
||||||
|
|
||||||
|
set +e
|
||||||
docker run --rm --network=host \
|
docker run --rm --network=host \
|
||||||
${docker_add_hosts[@]+"${docker_add_hosts[@]}"} \
|
${docker_add_hosts[@]+"${docker_add_hosts[@]}"} \
|
||||||
|
-v "${CT_OUT}:/app/logs/test/ct" \
|
||||||
-e CT_MODE=remote \
|
-e CT_MODE=remote \
|
||||||
-e "API_HOST=${API_HOST}" \
|
-e "API_HOST=${API_HOST}" \
|
||||||
-e "ADMIN_API_HOST=${ADMIN_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_EMAIL=${ADMIN_SUPPORT_EMAIL:-}" \
|
||||||
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
|
-e "ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-}" \
|
||||||
"${IMAGE}"
|
"${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}) ==="
|
echo "=== API CT passed (${STAND}) ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user