Skip to content

Commit e2939b9

Browse files
authored
feat(e2e): run the Kubernetes e2e suite on cargo-nextest with machine- and human-readable reports (#3344)
* feat(e2e): run kubernetes suite on cargo-nextest with JUnit/HTML reports Switch e2e:kubernetes (and all its variants) from `cargo test` to `cargo nextest run` for per-test process isolation and output consistent with the other nextest-based CI runs. - Add a dedicated `e2e-kubernetes` nextest profile with a JUnit report and a generous slow-timeout (60s flag, 5-min terminate) suited to live-cluster tests; kept separate from `ci` so its JUnit path and timeouts don't affect the workspace run. - Pin `--target-dir` for the run so the profile's relative JUnit path resolves to the repo-root results/ regardless of any inherited CARGO_TARGET_DIR (nextest ignores absolute JUnit paths). - Render the JUnit XML to a standalone HTML report via xsltproc and a committed XSLT stylesheet (best-effort; never masks the test exit code). - Name each report via `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`), used verbatim for both the `results/<name>.{xml,html}` filenames and the HTML heading. Tasks that invoke the script multiple times in one run set a distinct name per invocation so the reports no longer clobber the single fixed path: the credential-driver runs write results/e2e-kubernetes-secrets.xml and -vault.xml, and e2e:kubernetes:agent-sandbox-versions writes results/e2e-kubernetes-agent-sandbox-v1beta1.xml and -v1alpha1.xml. - Declare cargo-nextest in mise [tools] so the task runs without the Nix shell. - Ignore the results/ output directory. The results/ reports do not leak information. They are gitignored and no workflow uploads them as artifacts, so they stay on the ephemeral CI runner and are discarded when it is torn down. The HTML template renders only test names, status, timings, and failure messages (no captured stdout/stderr). Moving from `cargo test -- --nocapture` to nextest's captured, failure-only output also reduces what lands in the retained, viewable console logs. Signed-off-by: Jorge Garcia Oncins <jgarciao@redhat.com> * chore(e2e): revert per-lane report names for agent-sandbox-versions The agent-sandbox-versions task runs two lanes sequentially against the same cluster: v0.5.0 (v1beta1 storage version) then v0.4.6 (v1alpha1). On a reused cluster the second lane fails when kubectl applies the older CRD, because Kubernetes refuses to drop v1beta1 from spec.versions while it remains in status.storedVersions (the storage-version downgrade guardrail). This is a pre-existing issue with the v0.4.6 lane, unrelated to the nextest reporting work. The per-lane OPENSHELL_E2E_REPORT_NAME additions do not address that downgrade failure, so revert them to keep this PR scoped to the nextest change. Agent Sandbox 0.4.x is also superseded (1.0.0 is published); dropping or bumping the v1alpha1 lane is left as a follow-up. Signed-off-by: Jorge Garcia Oncins <jgarciao@redhat.com> --------- Signed-off-by: Jorge Garcia Oncins <jgarciao@redhat.com>
1 parent c5a8c4d commit e2939b9

6 files changed

Lines changed: 239 additions & 18 deletions

File tree

.config/nextest.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@ final-status-level = "fail"
88
failure-output = "immediate-final"
99
# Print slow test names after 30 seconds and terminate them after two minutes.
1010
slow-timeout = { period = "30s", terminate-after = 4 }
11+
12+
[profile.e2e-kubernetes]
13+
fail-fast = false
14+
status-level = "slow"
15+
final-status-level = "fail"
16+
failure-output = "immediate-final"
17+
# Flag slow tests after 60s and terminate them after five minutes.
18+
slow-timeout = { period = "60s", terminate-after = 5 }
19+
20+
# Relative to the profile store dir (`e2e/rust/target/nextest/e2e-kubernetes/`).
21+
[profile.e2e-kubernetes.junit]
22+
path = "../../../../../results/e2e-kubernetes.xml"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
/target/
77
e2e/rust/target/
88
target/
9+
10+
# Test result artifacts (JUnit XML, etc.)
11+
/results/
912
debug/
1013
release/
1114

e2e/rust/e2e-kubernetes.sh

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
# via `server.hostGatewayIP`. Targeting a cluster where the test host is
1515
# unreachable from pods? Set OPENSHELL_E2E_KUBERNETES_FEATURES=e2e to drop the
1616
# alias-dependent tests entirely.
17+
#
18+
# Results: `run_suite` writes a JUnit + HTML report under `results/`. Set
19+
# `OPENSHELL_E2E_REPORT_NAME` to name it per run when invoking this script repeatedly.
1720

1821
set -euo pipefail
1922

@@ -24,6 +27,25 @@ source "${ROOT}/e2e/support/conformance.sh"
2427

2528
E2E_FEATURES="${OPENSHELL_E2E_KUBERNETES_FEATURES-e2e,e2e-host-gateway,e2e-kubernetes}"
2629

30+
# Fixed output path of the `e2e-kubernetes` nextest profile (`.config/nextest.toml`).
31+
JUNIT_XML="${ROOT}/results/e2e-kubernetes.xml"
32+
33+
# Render a sibling HTML report from a JUnit XML. Best-effort: failures only warn.
34+
render_html() {
35+
local xml="${1:-${JUNIT_XML}}"
36+
local title="${2:-e2e-kubernetes}"
37+
[ -f "${xml}" ] || return 0
38+
local html="${xml%.xml}.html"
39+
if command -v xsltproc >/dev/null 2>&1; then
40+
xsltproc --stringparam title "${title}" \
41+
"${ROOT}/scripts/junit-to-html.xsl" "${xml}" >"${html}" \
42+
&& echo "HTML report: ${html}" \
43+
|| echo "WARNING: failed to render HTML report from ${xml}" >&2
44+
else
45+
echo "WARNING: xsltproc not found; skipping HTML report (${xml} still written)" >&2
46+
fi
47+
}
48+
2749
# Docker and Podman build their local gateway and CLI together in the shared
2850
# gateway wrapper. Kubernetes consumes published gateway images, so only its
2951
# local CLI needs to be built when CI has not supplied a prebuilt one.
@@ -50,9 +72,19 @@ run_conformance() {
5072
e2e_run_openshell_conformance "Kubernetes"
5173
}
5274

75+
# `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`) names the report
76+
# `results/<name>.{xml,html}` and its heading, so repeated runs do not clobber.
5377
run_suite() {
78+
local name="${OPENSHELL_E2E_REPORT_NAME:-e2e-kubernetes}"
79+
local report="${ROOT}/results/${name}.xml"
80+
local status=0
5481
"${ROOT}/e2e/with-kube-gateway.sh" \
55-
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}"
82+
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}" || status=$?
83+
if [ "${report}" != "${JUNIT_XML}" ]; then
84+
mv -f "${JUNIT_XML}" "${report}" 2>/dev/null || true
85+
fi
86+
render_html "${report}" "${name}"
87+
return "${status}"
5688
}
5789

5890
run_e2e() {
@@ -61,24 +93,27 @@ run_e2e() {
6193
return 0
6294
fi
6395

64-
cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
96+
# Pin `--target-dir` so the profile `junit.path` resolves regardless of `CARGO_TARGET_DIR`.
97+
cargo nextest run --profile e2e-kubernetes \
98+
--config-file "${ROOT}/.config/nextest.toml" \
99+
--target-dir "${ROOT}/e2e/rust/target" \
100+
--manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
65101
--features "${E2E_FEATURES}" \
66-
--no-fail-fast \
67-
${test_filter[@]+"${test_filter[@]}"} \
68-
-- --nocapture
102+
${test_filter[@]+"${test_filter[@]}"}
69103
}
70104

71105
if [ "${1:-}" = "${RUN_WITH_GATEWAY_COMMAND}" ]; then
72106
run_e2e
73107
exit 0
74108
fi
75109

110+
# Credential-driver mode: run once per storage backend, each with its own report.
76111
if [ "${OPENSHELL_E2E_CREDENTIAL_DRIVERS:-0}" = "1" ] \
77112
&& [ -z "${OPENSHELL_E2E_CREDENTIAL_DRIVER:-}" ]; then
78-
OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets run_suite
79-
OPENSHELL_E2E_CREDENTIAL_DRIVER=vault run_suite
80-
exit 0
113+
OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets \
114+
OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-secrets run_suite
115+
OPENSHELL_E2E_CREDENTIAL_DRIVER=vault \
116+
OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-vault run_suite
117+
else
118+
run_suite
81119
fi
82-
83-
exec "${ROOT}/e2e/with-kube-gateway.sh" \
84-
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}"

mise.lock

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ k3d = { version = "5.8.3", os = ["macos"] }
4444
"github:EmbarkStudios/cargo-deny" = { version = "0.20.2", version_prefix = "" }
4545
zig = "0.14.1"
4646
"github:rust-secure-code/cargo-auditable" = "0.7.5"
47+
"github:nextest-rs/nextest" = { version = "0.9.143", version_prefix = "cargo-nextest-" }
4748
"github:rust-cross/cargo-zigbuild" = "0.22.3"
48-
"github:nextest-rs/nextest" = { version = "cargo-nextest-0.9.143", os = ["windows"] }
4949
"npm:markdownlint-cli2" = "0.22.0"
5050

5151
[tools."github:mozilla/sccache"]

scripts/junit-to-html.xsl

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
3+
<!-- SPDX-License-Identifier: Apache-2.0 -->
4+
<!--
5+
Convert a nextest JUnit report into a standalone HTML report (XSLT 1.0).
6+
7+
Usage:
8+
xsltproc junit-to-html.xsl report.xml > report.html
9+
xsltproc -stringparam title "my suite report" junit-to-html.xsl report.xml > report.html
10+
11+
Renders a summary plus a per-suite table with pass/fail/skip rows. The report
12+
heading is the `title` parameter (default "Test report").
13+
-->
14+
<xsl:stylesheet version="1.0"
15+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
16+
<xsl:output method="html" indent="yes" encoding="UTF-8"
17+
doctype-system="about:legacy-compat"/>
18+
19+
<!-- Report heading; override via the xsltproc `title` string parameter. -->
20+
<xsl:param name="title" select="'Test report'"/>
21+
22+
<xsl:template match="/testsuites">
23+
<html lang="en">
24+
<head>
25+
<meta charset="UTF-8"/>
26+
<title><xsl:value-of select="$title"/></title>
27+
<style>
28+
:root {
29+
--bg: #f6f7f9; --card: #fff; --ink: #1f2328; --muted: #656d76;
30+
--border: #d7dbe0; --pass: #1a7f37; --fail: #cf222e; --skip: #9a6700;
31+
--pass-bg: #eafbe7; --fail-bg: #fbecec; --skip-bg: #fdf6e3;
32+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
33+
}
34+
* { box-sizing: border-box; }
35+
body {
36+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
37+
margin: 0; padding: 2rem clamp(1rem, 4vw, 3rem); color: var(--ink);
38+
background: var(--bg); line-height: 1.45;
39+
}
40+
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
41+
h2 {
42+
font-family: var(--mono); font-size: 0.95rem; font-weight: 600;
43+
margin: 2rem 0 0; color: var(--ink);
44+
}
45+
h2 .count { color: var(--muted); font-weight: 400; }
46+
47+
.summary { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0 0 0.5rem; }
48+
.summary .card {
49+
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
50+
padding: 0.5rem 0.9rem; min-width: 5.5rem;
51+
}
52+
.summary .card .label {
53+
display: block; font-size: 0.7rem; text-transform: uppercase;
54+
letter-spacing: 0.04em; color: var(--muted);
55+
}
56+
.summary .card .value { font-size: 1.35rem; font-weight: 700; }
57+
.card.c-fail .value { color: var(--fail); }
58+
.card.c-skip .value { color: var(--skip); }
59+
.card.c-fail.zero .value, .card.c-skip.zero .value { color: var(--muted); }
60+
61+
table {
62+
border-collapse: collapse; width: 100%; margin-top: 0.6rem; table-layout: fixed;
63+
background: var(--card); border: 1px solid var(--border);
64+
border-radius: 8px; overflow: hidden;
65+
}
66+
th, td {
67+
text-align: left; padding: 0.5rem 0.75rem;
68+
border-bottom: 1px solid var(--border); vertical-align: top;
69+
}
70+
tbody tr:last-child td { border-bottom: none; }
71+
td:first-child { font-family: var(--mono); font-size: 0.85rem; overflow-wrap: anywhere; }
72+
th {
73+
position: sticky; top: 0; background: #eef1f4; z-index: 1;
74+
font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
75+
color: var(--muted);
76+
}
77+
tbody tr:nth-child(even) { background: #fafbfc; }
78+
tbody tr:hover { background: #eef4ff; }
79+
tr.failed, tr.failed:nth-child(even) { background: var(--fail-bg); }
80+
tr.skipped, tr.skipped:nth-child(even) { background: var(--skip-bg); }
81+
td.time { font-family: var(--mono); font-size: 0.85rem; color: var(--muted); }
82+
83+
.badge {
84+
display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px;
85+
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em;
86+
}
87+
.status-pass { background: var(--pass-bg); color: var(--pass); }
88+
.status-fail { background: var(--fail-bg); color: var(--fail); }
89+
.status-skip { background: var(--skip-bg); color: var(--skip); }
90+
pre {
91+
margin: 0.4rem 0 0; padding: 0.5rem 0.6rem; white-space: pre-wrap;
92+
color: var(--fail); background: #fff; border: 1px solid var(--border);
93+
border-radius: 6px; font-size: 0.8rem;
94+
}
95+
</style>
96+
</head>
97+
<body>
98+
<h1><xsl:value-of select="$title"/></h1>
99+
<div class="summary">
100+
<div class="card">
101+
<span class="label">Total</span>
102+
<span class="value"><xsl:value-of select="@tests"/></span>
103+
</div>
104+
<div class="card c-fail">
105+
<xsl:if test="@failures = 0"><xsl:attribute name="class">card c-fail zero</xsl:attribute></xsl:if>
106+
<span class="label">Failures</span>
107+
<span class="value"><xsl:value-of select="@failures"/></span>
108+
</div>
109+
<div class="card c-fail">
110+
<xsl:if test="@errors = 0"><xsl:attribute name="class">card c-fail zero</xsl:attribute></xsl:if>
111+
<span class="label">Errors</span>
112+
<span class="value"><xsl:value-of select="@errors"/></span>
113+
</div>
114+
<div class="card c-skip">
115+
<xsl:if test="sum(testsuite/@skipped) = 0"><xsl:attribute name="class">card c-skip zero</xsl:attribute></xsl:if>
116+
<span class="label">Skipped</span>
117+
<span class="value"><xsl:value-of select="sum(testsuite/@skipped)"/></span>
118+
</div>
119+
<div class="card">
120+
<span class="label">Time</span>
121+
<span class="value"><xsl:value-of select="@time"/>s</span>
122+
</div>
123+
</div>
124+
<xsl:for-each select="testsuite">
125+
<h2><xsl:value-of select="@name"/>
126+
<xsl:text> </xsl:text>
127+
<span class="count">(<xsl:value-of select="count(testcase)"/> tests, <xsl:value-of select="@time"/>s)</span>
128+
</h2>
129+
<table>
130+
<colgroup>
131+
<col/>
132+
<col style="width: 6rem;"/>
133+
<col style="width: 6rem;"/>
134+
</colgroup>
135+
<thead>
136+
<tr><th>Test</th><th>Status</th><th>Time</th></tr>
137+
</thead>
138+
<tbody>
139+
<xsl:for-each select="testcase">
140+
<xsl:choose>
141+
<xsl:when test="failure or error">
142+
<tr class="failed">
143+
<td><xsl:value-of select="@name"/>
144+
<pre><xsl:value-of select="failure | error"/></pre>
145+
</td>
146+
<td><span class="badge status-fail">FAIL</span></td>
147+
<td class="time"><xsl:value-of select="@time"/>s</td>
148+
</tr>
149+
</xsl:when>
150+
<xsl:when test="skipped">
151+
<tr class="skipped">
152+
<td><xsl:value-of select="@name"/></td>
153+
<td><span class="badge status-skip">SKIP</span></td>
154+
<td class="time"><xsl:value-of select="@time"/>s</td>
155+
</tr>
156+
</xsl:when>
157+
<xsl:otherwise>
158+
<tr>
159+
<td><xsl:value-of select="@name"/></td>
160+
<td><span class="badge status-pass">PASS</span></td>
161+
<td class="time"><xsl:value-of select="@time"/>s</td>
162+
</tr>
163+
</xsl:otherwise>
164+
</xsl:choose>
165+
</xsl:for-each>
166+
</tbody>
167+
</table>
168+
</xsl:for-each>
169+
</body>
170+
</html>
171+
</xsl:template>
172+
</xsl:stylesheet>

0 commit comments

Comments
 (0)