The renderer¶
SpaRenderer (otto.coverage.renderer.spa_renderer) is the reporter’s only
renderer: the report otto cov report emits is the covapp single-page app.
It replaced a server-rendered Jinja HtmlRenderer, which was deleted —
module, templates, and report.css — once the SPA became the reporter’s
renderer.
SpaRenderer.render does two things, in order:
Copy the bundle.
make webbuilds a second Vite app (web/src/covapp/) intosrc/otto/_webassets/covapp/— a classic-script IIFE bundle with no ES modules, no inline scripts, and only relative asset paths, so it boots the same fromfile://, a CI artifacts browser, or behind Jenkins’ minimal CSP. That directory isn’t committed; it’s built bymake weband wheel-embedded (make wheel-checkasserts the bundle’sindex.htmlis present in the built wheel).SpaRenderercopies it into the report directory as-is (sourcemaps excluded — kept out of every emitted report, used only by the TS coverage fold in CI). A checkout that skippedmake webhas no bundle to copy:_copy_bundlewarns, namesmake webas the fix, and still emits the data chunks rather than failing the whole report.Emit the data chunks (
otto.coverage.renderer.spa_data, pure Python) —cov_data/index.js, one classic-script assignment towindow.__OTTO_COV__carrying the report-wide payload: config (thresholds, tier colors/labels, state colors), the run table and its per-run contribution rollup (run_contrib: lines credited, lines revoked, and the top files per run), and a directory tree with per-directory rollup stats precomputed in Python (hit/total per stat type, per-tier breakdowns, stale/aging/ excluded flag counts) so the frontend never re-aggregates the whole store client-side. Alongside it, onecov_data/files/<mangled-path>.jsclassic-script chunk per source file, each callingwindow.__OTTO_COV_FILE__({...})with that file’s annotated source and per-line hit/branch/state/run data. Chunks load lazily on navigation to a file page, not up front, so page-load cost stays constant regardless of report size. Every index and file chunk carries the same stamp (a UTC timestamp plus a short random suffix, freshly generated perrender()call) and the sameOTTO_COV_DATA_FORMATconstant — the frontend’sEXPECTED_DATA_FORMATmust match it exactly (otto.coverage.renderer.spa_data.OTTO_COV_DATA_FORMATand the TypeScript constant are bumped together or never). A stamp mismatch between the index and a stale cached chunk, or a format the running bundle doesn’t recognize, renders a “this report needs to be regenerated” guard screen instead of a wrong or partial report.
Chunk ids come from mangle_path over the file’s canonical path, never
its display path, so --prefix changes what a report shows without changing
what any of its files are called.
Exclusion display stays render-time, not store-time, for the same reason it
always has: a single-valued LineRecord.state can’t express “excluded
always wins” over covered/stale/aging, so the reporter never bakes
state == "excluded" into the store — it only forwards the configured
extra marker strings. emit_chunks re-scans each file’s source for
exclusion markers and annotates FileRecord.excluded_lines on the store as
a side effect of building that file’s chunk. This is why otto cov report
calls store.save(...) for store.json after renderer.render(store)
returns — saving first would miss every file’s excluded-line list.