// E2E_Report.typ — Streaming-chain E2E report. // // Renders report_data.json (produced by report_build.py) into a single PDF: // headline KPIs, per-field progression/regression vs the previous run, unit-test // suites, code coverage, performance (CPU/peak-RSS/throughput), per-scenario // waveform fidelity, trend plots, and embedded per-scenario waveform images. // // Compile from the directory holding report_data.json + the *.png artifacts: // typst compile E2E_Report.typ E2E_Report.pdf // Missing inputs degrade to "n/a" / skipped sections so a partial run still // renders. #let data = json("report_data.json") #let meta = data.meta #let e2e = data.e2e #let ut = data.unit_tests #let cov = data.coverage #let reg = data.regression #let hl = data.headline // ── helpers ────────────────────────────────────────────────────────────────── #let fmt(v, suffix: "") = if v == none { "n/a" } else { str(v) + suffix } #let fnum(v, digits: 3, suffix: "") = { if v == none { "n/a" } else { [#calc.round(v, digits: digits)] + suffix } } #let ok_color = rgb("#1a7f37") #let bad_color = rgb("#cf222e") #let neutral = rgb("#57606a") #let warn_color = rgb("#9a6700") // XFAIL — expected/known failure #let xpass_color = rgb("#8250df") // XPASS — stale marker, needs attention #let status_badge(s) = { let c = if s == "PASS" { ok_color } else if s == "FAIL" { bad_color } else if s == "XFAIL" { warn_color } else if s == "XPASS" { xpass_color } else { neutral } box(fill: c, inset: (x: 6pt, y: 2pt), radius: 3pt, text(fill: white, weight: "bold", size: 8pt)[#s]) } #let yesno(b) = { if b == true { text(fill: ok_color)[✓] } else if b == false { text(fill: bad_color)[✗] } else { text(fill: neutral)[—] } } // progression arrow for a regression row #let trend_arrow(r) = { if r.delta == none or r.delta == 0 { text(fill: neutral)[—] } else { let up = r.delta > 0 let good = r.better == true let c = if good { ok_color } else { bad_color } let arrow = if up { "▲" } else { "▼" } text(fill: c)[#arrow #calc.round(r.delta, digits: 4)] } } // ── page setup ─────────────────────────────────────────────────────────────── #set page( paper: "a4", margin: (x: 1.8cm, y: 1.8cm), header: align(right, text(size: 8pt, fill: neutral)[Streaming-chain E2E · #meta.git_sha]), footer: context align(center, text(size: 8pt, fill: neutral)[#counter(page).display("1 / 1", both: true)]), ) #set text(font: "DejaVu Sans", size: 9.5pt) #set heading(numbering: "1.1") #show heading.where(level: 1): it => block(above: 14pt, below: 8pt, text(size: 14pt, weight: "bold", it)) #show heading.where(level: 2): it => block(above: 10pt, below: 6pt, text(size: 11pt, weight: "bold", it)) // ── title ──────────────────────────────────────────────────────────────────── #align(center)[ #text(size: 20pt, weight: "bold")[MARTe2 Streaming-Chain E2E Report] \ #v(2pt) #text(size: 10pt, fill: neutral)[ #meta.timestamp · commit #raw(meta.git_sha) · target #meta.target · run \##data.history_len ] ] #v(6pt) #align(center, status_badge(e2e.overall) + h(8pt) + text(size: 11pt)[ E2E #hl.e2e_pass/#hl.e2e_total · Units #hl.unit_pass/#hl.unit_total #if hl.at("e2e_xfail", default: 0) > 0 [ · #hl.e2e_xfail xfail] #if hl.at("e2e_xpass", default: 0) > 0 [ · #text(fill: xpass_color)[#hl.e2e_xpass xpass!]] ]) // ── headline KPIs ──────────────────────────────────────────────────────────── #v(10pt) #let kpi(label, value) = box(width: 100%, inset: 8pt, radius: 4pt, fill: rgb("#f6f8fa"))[ #align(center)[ #text(size: 14pt, weight: "bold")[#value] \ #text(size: 8pt, fill: neutral)[#label] ] ] #grid(columns: 4, gutter: 6pt, kpi("E2E passed", [#hl.e2e_pass/#hl.e2e_total]), kpi("Unit passed", [#hl.unit_pass/#hl.unit_total]), kpi("Mean sine corr", fnum(hl.mean_corr, digits: 4)), kpi("Throughput", fnum(hl.mean_throughput_sps, digits: 0, suffix: " sp/s")), kpi("Python cov", fmt(hl.cov_python, suffix: "%")), kpi("Go cov", fmt(hl.cov_go, suffix: "%")), kpi("Mean peak RSS", fnum(hl.mean_peak_rss_mb, digits: 1, suffix: " MB")), kpi("Mean CPU", fnum(hl.mean_cpu_s, digits: 2, suffix: " s")), ) // ── progression / regression ───────────────────────────────────────────────── = Progression / regression #if data.is_first_run [ _First recorded run — baseline established; no previous run to compare against._ ] else [ Comparison against the previous run (#text(fill: ok_color)[▲] better, #text(fill: bad_color)[▼] worse, — unchanged/unavailable). #v(4pt) #table( columns: (1.6fr, 1fr, 1fr, 1.2fr), align: (left, right, right, right), stroke: 0.4pt + rgb("#d0d7de"), inset: 5pt, table.header([*Metric*], [*Current*], [*Previous*], [*Δ (trend)*]), ..reg.map(r => ( [#r.name], fnum(r.current, digits: 4), fnum(r.previous, digits: 4), trend_arrow(r), )).flatten() ) ] // ── unit tests ─────────────────────────────────────────────────────────────── = Unit tests #table( columns: (2fr, 0.8fr, 0.8fr, 0.8fr, 0.8fr, 0.8fr, 0.8fr), align: (left, center, right, right, right, right, right), stroke: 0.4pt + rgb("#d0d7de"), inset: 5pt, table.header([*Suite*], [*Status*], [*Total*], [*Pass*], [*Fail*], [*Skip*], [*Time (s)*]), ..ut.suites.map(s => ( [#s.name], if not s.avail { status_badge("SKIP") } else if s.ok { status_badge("PASS") } else { status_badge("FAIL") }, [#s.total], [#s.passed], [#s.failed], [#s.skipped], fnum(s.time_s, digits: 2), )).flatten(), table.cell(colspan: 2)[*Totals*], [#ut.totals.total], [#ut.totals.passed], [#ut.totals.failed], [#ut.totals.skipped], [], ) // ── coverage ───────────────────────────────────────────────────────────────── = Code coverage #table( columns: (1fr, 1fr, 2fr), align: (left, right, left), stroke: 0.4pt + rgb("#d0d7de"), inset: 5pt, table.header([*Language*], [*Coverage*], [*Source*]), ..cov.languages.map(l => ( [#l.name], if l.pct == none { text(fill: neutral)[n/a] } else { [#l.pct%] }, text(size: 8pt, fill: neutral)[#l.note], )).flatten() ) // per-file C++ detail — the streaming chain's most critical code, so the report // shows line coverage for every project C++ source (worst-covered first). #let cpp = cov.languages.find(l => l.name == "C++") #if cpp != none and cpp.at("files", default: ()).len() > 0 [ == C++ source detail #let cf = cpp.files #text(size: 8pt, fill: neutral)[ #cpp.at("lines_hit", default: 0) of #cpp.at("lines_found", default: 0) lines covered across #cf.len() project files (worst-covered first; green ≥75%, amber 50–75%, red under 50%). ] #v(4pt) #let covcell(p) = { let c = if p == none { neutral } else if p >= 75 { ok_color } else if p >= 50 { warn_color } else { bad_color } text(fill: c, weight: "bold")[#if p == none { "n/a" } else [#p%]] } #table( columns: (3.2fr, 1fr, 1fr, 1.1fr), align: (left, right, right, right), stroke: 0.4pt + rgb("#d0d7de"), inset: 4pt, table.header([*Source file*], [*Lines*], [*Hit*], [*Coverage*]), ..cf.map(f => ( text(size: 8pt)[#f.path], [#f.lines_found], [#f.lines_hit], covcell(f.pct), )).flatten() ) ] // ── performance ────────────────────────────────────────────────────────────── = Performance Per-scenario CPU time and peak resident memory (VmHWM) of the StreamHub and MARTe2 processes, plus sustained client throughput (recorded samples ÷ duration). #v(4pt) #table( columns: (1.8fr, 1fr, 1fr, 1fr, 1fr, 1.1fr), align: (left, right, right, right, right, right), stroke: 0.4pt + rgb("#d0d7de"), inset: 5pt, table.header([*Scenario*], [*Hub CPU (s)*], [*Hub RSS (MB)*], [*MARTe CPU (s)*], [*MARTe RSS (MB)*], [*Throughput (sp/s)*]), ..e2e.scenarios.map(sc => { let h = sc.perf.at("hub", default: (:)) let m = sc.perf.at("marte", default: (:)) ( [#sc.id], fnum(h.at("cpu_s", default: none), digits: 2), fnum(h.at("peak_rss_mb", default: none), digits: 1), fnum(m.at("cpu_s", default: none), digits: 2), fnum(m.at("peak_rss_mb", default: none), digits: 1), fnum(sc.throughput_sps, digits: 0), ) }).flatten() ) // ── per-scenario waveform fidelity ─────────────────────────────────────────── = Scenarios #for sc in e2e.scenarios [ == #sc.id #h(6pt) #status_badge(sc.status) #if sc.at("desc", default: none) != none [ #v(1pt) #text(size: 9pt, fill: neutral, style: "italic")[#sc.desc] #v(2pt) ] #if sc.at("known_issue", default: none) != none [ #v(2pt) #box(fill: rgb("#fff8c5"), inset: 5pt, radius: 3pt, width: 100%, text(size: 8pt, fill: rgb("#7d4e00"))[*Known issue:* #sc.known_issue]) ] #let rb = sc.rollup Client checks: live #yesno(rb.at("live_ok", default: none)) · zoom #yesno(rb.at("zoom_ok", default: none)) · window #yesno(rb.at("window_ok", default: none)) · trigger #yesno(rb.at("trigger_ok", default: none)) #if sc.live_frames != none [ · #sc.live_frames live frames] #v(3pt) #table( columns: (1.6fr, 0.6fr, 0.8fr, 0.8fr, 1fr, 0.9fr, 0.9fr, 0.8fr, 0.8fr), align: (left, center, left, left, right, right, right, center, center), stroke: 0.4pt + rgb("#d0d7de"), inset: 4pt, table.header([*Signal*], [*Pass*], [*Type*], [*Quant*], [*Max abs err*], [*Corr*], [*nRMSE*], [*Fidelity*], [*Shape*]), ..sc.signals.map(g => ( raw(g.key), yesno(g.pass), text(size: 8pt)[#g.type], text(size: 8pt)[#g.quant], fnum(g.max_abs_err, digits: 6), fnum(g.corr, digits: 4), fnum(g.nrmse, digits: 4), yesno(g.fidelity_ok), yesno(g.shape_ok), )).flatten() ) // zoom / window / trigger behavioural detail (real WS-driven results) #let zooms = sc.at("zoom", default: ()) #let win = sc.at("window", default: (:)) #let trigs = sc.at("trigger", default: ()) #if zooms.len() > 0 or win.len() > 0 [ #v(3pt) #text(size: 8.5pt, weight: "bold")[Zoom / window queries] #table( columns: (1fr, 1.2fr, 1fr, 0.8fr, 0.8fr), align: (left, left, right, right, center), stroke: 0.4pt + rgb("#d0d7de"), inset: 4pt, table.header([*Query*], [*Signal*], [*Span (s)*], [*Pts*], [*OK*]), ..zooms.enumerate().map(((i, z)) => { let r = z.at("range", default: (0, 0)) ( [zoom #str(i + 1)], raw(z.at("key", default: "")), fnum(r.at(1) - r.at(0), digits: 4), [#z.at("returned", default: 0)], yesno(z.at("inrange", default: none)), ) }).flatten(), ..(if win.len() > 0 and win.at("returned", default: 0) > 0 {( [window #fnum(win.at("windowSec", default: 0), digits: 2)], raw(win.at("key", default: "")), fnum(win.at("span", default: 0), digits: 4), [#win.at("returned", default: 0)], yesno(win.at("ok", default: none)), )} else {()}), ) ] #if trigs.len() > 0 [ #v(3pt) #text(size: 8.5pt, weight: "bold")[Trigger captures] #h(4pt) #text(size: 8pt, fill: neutral)[(signal #raw(trigs.at(0).at("key", default: "")))] #table( columns: (1fr, 1fr, 0.8fr, 1fr, 1fr, 0.8fr, 0.8fr), align: (left, left, center, right, right, center, center), stroke: 0.4pt + rgb("#d0d7de"), inset: 4pt, table.header([*Edge*], [*Mode*], [*Fired*], [*Pre/Post (s)*], [*Capture pts*], [*Win OK*], [*Edge OK*]), ..trigs.map(t => ( text(size: 8pt)[#t.at("edge", default: "")], text(size: 8pt)[#t.at("mode", default: "")], yesno(t.at("fired", default: none)), text(size: 8pt)[#fnum(t.at("preSec", default: 0), digits: 3) / #fnum(t.at("postSec", default: 0), digits: 3)], [#t.at("capturePts", default: 0)], yesno(t.at("windowOk", default: none)), yesno(t.at("edgeOk", default: none)), )).flatten() ) ] // embed the waveform overview if report_build.py found one in the artifacts #if sc.at("wave_img", default: none) != none [ #v(3pt) #align(center, image(sc.wave_img, width: 92%)) ] #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 #grid(columns: 2, gutter: 8pt, ..data.trend_plots.map(p => image(p, width: 100%)) ) ]