Files
plotter/docs/superpowers/plans/2026-09-28-plot-docs-and-coverage.md
T

13 KiB

plot — documentation and a coverage floor

Goal: Give the library a README and a user-facing documentation set, make the test coverage reproducible from one command, raise every library source above 80% line coverage, and add a floor check so it cannot silently regress.

Measured baseline (this is the starting point, not an estimate)

lcov over src/**, built with --coverage -O0 -g, running the full 274-case suite:

Aggregate 93.0% lines (3206/3447), 97.8% functions (404/413)
Files captured 23
Below 80% gl/loader.cpp — 65.4% lines, 77.8% functions
Near the line render/vertex_array.cpp 83.1%, gl/handle.cpp 83.6%
At or near 100% data/*, math/transform, plot/axis, plot/measure, render/embedded_mono_font
No instrumented lines core/status.cpp (19 non-comment lines), render/shaders.cpp (82, string literals only), text/stb_truetype_impl.cpp (0)
Adapters 2 files captured / 57 lines at 100% — the widget and host bodies need a window, so they are covered by the demo runs, not by unit tests; their testable seams (pointer anchors, row routing) are unit-tested

The uncovered lines are, in every case, error paths and "loader unavailable" branches — that is the honest description of the gap, and it is what the new tests must exercise.

Global Constraints

  • plot_core and every public header under include/plot/ stay free of third-party code, and the public API must not change to make something testable.
  • Public headers self-contained; the configure-time umbrella and self-containment checks keep passing.
  • C++20; our targets clean under -Wall -Wextra -Wpedantic -Wconversion -Wshadow.
  • No exceptions across the public API boundary.
  • The existing 274 cases keep passing; no assertion is relaxed.
  • Tests must fail toward a false alarm, never toward a silent pass. A coverage test that cannot fail is worse than none.
  • Never leave process-global GL state poisoned for later cases. plot::gl::load() is one-way and process-wide; tests that need the loader unavailable must run in a forked child (the suite already has that pattern) or restore exactly what they stubbed.
  • Documentation claims must be checked against the code, not written from memory: every API name, signature, default value and file path in the docs is verified before it is committed.
  • Commit as soon as the tree builds; several agents in this phase have died of turn exhaustion.
  • Every task ends with cmake --preset linux-debug && cmake --build build/debug && ctest --test-dir build/debug --output-on-failure green and warning-clean.

Task 1: Reproducible coverage, with a floor

Files:

  • Modify: CMakePresets.json, CMakeLists.txt
  • Create: tools/coverage.sh, tools/coverage_floor.py (or an equivalent small checker)
  • Create: docs/coverage.md (the measured table, regenerated by the script)

Interfaces:

  • Consumes: ctest, lcov, genhtml, the presets.

  • Produces: a linux-coverage preset; tools/coverage.sh producing a summary, an HTML report and a machine-readable file; a floor check that exits non-zero when any src/** file is below 80% lines or the aggregate is below the configured target.

  • Step 1: The preset

Add linux-coverage (Ninja, Debug, --coverage -O0 -g on compile and link, binaryDir build/cov). It must not affect the other presets, and PLOT_BUILD_TESTS=ON is required. Confirm cmake --preset linux-coverage && cmake --build build/cov && ctest --test-dir build/cov works from clean.

  • Step 2: One command

tools/coverage.sh runs the suite, captures with lcov, writes build/cov/coverage.info, generates build/cov/html/index.html with genhtml, prints the per-file table for src/**, and exits non-zero if the floor check fails. It must be idempotent: a second run without changes gives the same numbers (say how you guarantee that — stale .gcda files are the usual cause).

  • Step 3: The floor check

tools/coverage_floor.py reads the lcov file and fails if any file under src/ is below 80% lines, or the src/ aggregate is below 90%. It must be able to fail: prove it by temporarily raising the per-file floor above a file's actual coverage and showing the non-zero exit. State the numbers it enforces in docs/coverage.md so the contract is visible.

  • Step 4: Wire it so it cannot rot

Decide how the check is reachable: a coverage custom CMake target, a documented command, or a ctest case that only registers in the coverage preset. Whichever you choose, it must not slow or destabilise the normal debug/asan/release runs, and the README's build section (Task 4) must name the exact command. Report what you chose.

  • Step 5: Verify and commit

Report from a clean tree: the aggregate and per-file table, the floor check's exit status, and the deliberate-failure demonstration. Append the table to docs/coverage.md.

git add -A && git commit -m "test(coverage): a reproducible coverage preset, report and floor check"

Task 2: Close the error-path gaps in the GL layer

Files:

  • Modify: tests/test_gl_context.cpp, tests/test_render_smoke.cpp (or a new tests/test_gl_errors.cpp)
  • Modify: src/gl/loader.cpp, src/render/vertex_array.cpp, src/gl/handle.cpp only if a test requires a change, and then only a non-behavioural one (state it in the report)

Interfaces:

  • Consumes: the suite's existing stub-and-restore pattern for entry points (RefuseGetIntegerv, RefuseMapBufferRange, RefuseFenceSync), its fork-based death-test pattern, plot::gl::available(), caps().

  • Produces: tests, not new API. The public surface must not change.

  • Step 1: loader.cpp — the extension scanner and the missing-symbol path (28 uncovered lines)

The uncovered lines are has_token (the whole function, including the at_start/at_end boundary), has_extension's indexed path, the "no current context" return, and the post-failure PLOT_ASSERT. Reach has_token through the public path by stubbing glGetString so GL_VERSION reports a 2.1 context and GL_EXTENSIONS returns a crafted string — then assert caps().buffer_storage/persistent_map both ways: extension present (...GL_ARB_buffer_storage ...) and a prefix-only near-miss (GL_ARB_buffer_storageX, which must be false). That near-miss is the boundary the function exists for, so it is the assertion that matters. Reach the indexed path by stubbing glGetStringi to return nullptr and to return a matching name. For the missing-symbol path: run it in a forked child that calls load() with a stub loader returning nullptr for one required entry point, asserts load() returns false and available() is false, and exits with a status the parent checks. Never do this in the parent — load() is one-way and process-wide.

  • Step 2: handle.cpp — the kNoLoader and "returned 0" families (42 uncovered lines)

The uncovered lines are the failure returns for glCreateShader/glGenBuffers/glGenVertexArrays/glCreateProgram returning 0, the kNoLoader returns, the program-link-failure log path, and two early returns. Cover the "returned 0" family by stubbing the specific entry point to return 0 (save and restore it), then asserting the constructor returns the documented Status and leaves a usable object (id 0, destructor safe). Cover the kNoLoader family in the forked child from Step 1, where available() is genuinely false. Cover the link-failure log path by compiling a program whose fragment shader is valid GLSL but fails to link against the vertex shader (a mismatched varying/interface is the usual way) and asserting the failure is reported with a non-empty log.

  • Step 3: vertex_array.cpp — the failure and empty paths (10 uncovered lines)

The uncovered lines are the loader-unavailable returns, vao_.reset() on the failure path, and the instances_ = 0 early returns for an empty batch. Cover the empty-batch returns through the public staging API (stage nothing, upload, draw — assert nothing is drawn and no GL error), and the failure returns through the forked child and the stub pattern.

  • Step 4: Re-measure and report

Run tools/coverage.sh and report the new per-file table. Every file under src/ must be at or above 80% lines, and the aggregate at or above 90%; if either fails, keep going rather than lowering the floor. Also report the aggregate change from 93.0% and name any line you could not reach and why (an unreachable line is worth stating, not hiding).

  • Step 5: Commit
git add -A && git commit -m "test(gl): cover the loader's extension scanner and the handle and vertex-array failure paths"

Task 3: The user guide under docs/

Files:

  • Create: docs/getting-started.md, docs/architecture.md, docs/embedding.md, docs/data-and-performance.md, docs/interaction.md, docs/styling.md, docs/building-and-testing.md

Interfaces:

  • Consumes: the headers, the specs under docs/superpowers/specs/, the demos under examples/, the coverage table from Task 1.

  • Produces: prose documentation for a user of the library. The docs/superpowers/ tree stays as the design record and is not user documentation — say so once, in docs/architecture.md.

  • Step 1: Write the pages, verifying every claim

For each page, the claims must come from the code: read the header and the relevant src/ file before writing a sentence about it. Specifically:

  • getting-started — prerequisites, the build/install/consume commands, and a minimal GLFW program that opens a window and draws one trace. The program must be compiled as part of the verification (a scratch build is fine; do not add it to examples/ unless it duplicates nothing).

  • architecture — the layering (core/math → gl → data → render → plot → input), what each layer owns, the dependency rule, and where the design record lives.

  • embedding — one section per host (GLFW, ImGui, Qt6, GTK4), each with the exact initialize/paint/resize/event-translation steps the shipped adapter performs, the DPI/ui_scale rule, and the fact that the library never owns the context or the event loop. Include the per-host headless invocation used by the project's own verification.

  • data-and-performance — Series/ArraySeries/StreamSeries, the SPSC contract and what a producer may and may not do, the min/max LOD pyramid and why it makes cost O(pixels), the exact-vs-interpolated query distinction (reduce/sum/sumsq exact over a range; the crosshair interpolated), and the measured frame times with the standing caveat that they are llvmpipe under Xvfb.

  • interaction — cursors (two orientations), regions, measurement band, legend, crosshair, the Controller gesture table exactly as implemented, live follow with relative zoom and mouse panning, and the XFollow/set_x_range reinterpretation with its clamp.

  • styling — every Style field with its default and whether it is logical or device, the ui_scale rule, the grid classes, the crosshair dash, and the Legend/CrosshairStyle knobs.

  • building-and-testing — the presets, the options (PLOT_BUILD_*), how to run the suite, the headless demo modes, and the coverage command with the Task 1 floor.

  • Step 2: No invented API

Any symbol you mention must exist with that exact name and signature. Check by grepping the headers, and for the getting-started program by compiling it. Report the check you ran per page.

  • Step 3: Commit
git add -A && git commit -m "docs: a user guide covering getting started, architecture, embedding, data, interaction, styling and testing"

Task 4: The README, and a final accuracy pass

Files:

  • Create: README.md
  • Modify: docs/*.md only to fix anything the final pass finds wrong

Interfaces:

  • Consumes: everything above, including Task 3's pages and Task 1's coverage table.

  • Produces: the repository front door.

  • Step 1: The README

Keep it to what a newcomer needs, in this order: what the library is in two sentences; the four host integrations and the headline capability (real-time, high sample rate, O(pixels) rendering) with one measured number; a short feature list; build and install; a ~20-line minimal embedding example; a link per doc page under docs/; the test and coverage commands with the current numbers; licence and third-party provenance (Adwaita Mono under OFL 1.1, vendored stb_truetype v1.26, and the plot_core zero-dependency rule); and a short, honest limitations list. Do not market: no claims without a number or a file behind them.

  • Step 2: The final accuracy pass

Walk every code snippet in the README and in docs/ and compile or at least syntax-check it against the real headers. Fix what does not compile. Grep every mentioned symbol and confirm it exists. Report the list of symbols checked and any correction made.

  • Step 3: Commit
git add -A && git commit -m "docs: README with build, embedding and verification entry points"