feat(e2e): render direct/recorder/debug/tcplogger/stress sections in the unified report

Extends report_build.py with build_by_kind() (per-scenario-kind pass/fail
rollup) and a ported build_stress()/stress_headline()/stress_plots() (scaling
curves per stress axis), wires both into the headline KPIs, regression
tracking, and report_data.json. E2E_Report.typ renders the four new
per-kind tables plus a Stress Tests section (per-axis case tables + scaling
plots, gracefully degrading to placeholders when a kind/stress data is
absent). run_e2e.sh now passes --stress-results so the report actually
receives real stress data instead of silently omitting it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-07-01 21:57:11 +02:00
parent b65ac06ce2
commit 8337d678be
4 changed files with 235 additions and 5 deletions
+73
View File
@@ -323,6 +323,79 @@ MARTe2 processes, plus sustained client throughput (recorded samples ÷ duration
#v(6pt)
]
// ── per-kind sections (direct/recorder/debug/tcplogger) ─────────────────────
// Raw scenario records here come straight from results.json (not e2e.scenarios'
// reshaping), so they only carry id/kind/status/known_issue/metrics — no
// waveform-fidelity breakdown (already covered, where applicable, in the
// Scenarios section above for chain-kind scenarios; these kinds run through
// dedicated non-chain harnesses in run_e2e.sh).
#let kind_table(title, block) = {
[= #title]
[#block.n_pass/#block.n_total passed.]
v(4pt)
if block.n_total == 0 {
text(fill: neutral)[_No scenarios of this kind in this run._]
} else {
table(
columns: (1.4fr, 0.8fr, 2fr),
align: (left, center, left),
stroke: 0.4pt + rgb("#d0d7de"),
inset: 5pt,
table.header([*Scenario*], [*Status*], [*Known issue*]),
..block.scenarios.map(s => (
[#s.id],
status_badge(s.status),
if s.at("known_issue", default: none) != none {
text(size: 8pt, fill: rgb("#7d4e00"))[#s.known_issue]
} else { text(fill: neutral)[—] },
)).flatten()
)
}
}
#kind_table("Direct Round-Trip (UDPStreamer UDPStreamerClient)", data.direct)
#kind_table("Recorder (BinaryRecorder disk output)", data.recorder)
#kind_table("Debug Service E2E", data.debug)
#kind_table("TCPLogger E2E", data.tcplogger)
// ── stress tests ─────────────────────────────────────────────────────────────
= Stress Tests
#if data.stress == none [
_Not run this session._
] else [
#align(center, status_badge(data.stress.overall) + h(8pt) + text(size: 11pt)[
#hl.at("stress_pass", default: 0) passed ·
#hl.at("stress_fail", default: 0) failed
])
#v(4pt)
#for (axis, cases) in data.stress.by_axis [
== Axis: #raw(axis)
#table(
columns: (0.8fr, 0.8fr, 1fr, 1fr, 1fr, 1fr),
align: (right, center, right, right, right, right),
stroke: 0.4pt + rgb("#d0d7de"),
inset: 4pt,
table.header([*Level*], [*Status*], [*Hub RSS (MB)*], [*MARTe RSS (MB)*],
[*Zoom p50 (ms)*], [*Zoom p95 (ms)*]),
..cases.map(c => (
[#c.at("level", default: "n/a")],
status_badge(c.at("status", default: "?")),
fnum(c.at("hub_rss_mb", default: none), digits: 1),
fnum(c.at("marte_rss_mb", default: none), digits: 1),
fnum(c.at("zoom_p50_ms", default: none), digits: 1),
fnum(c.at("zoom_p95_ms", default: none), digits: 1),
)).flatten()
)
]
#if data.stress_plots.len() > 0 [
#v(4pt)
== Scaling curves
#grid(columns: 2, gutter: 8pt,
..data.stress_plots.map(p => image(p, width: 100%))
)
]
]
// ── trend plots ──────────────────────────────────────────────────────────────
#if data.trend_plots.len() > 0 [
= Trends over runs