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_coreand every public header underinclude/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-failuregreen 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-coveragepreset;tools/coverage.shproducing a summary, an HTML report and a machine-readable file; a floor check that exits non-zero when anysrc/**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 newtests/test_gl_errors.cpp) - Modify:
src/gl/loader.cpp,src/render/vertex_array.cpp,src/gl/handle.cpponly 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— thekNoLoaderand "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 underexamples/, 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, indocs/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_scalerule, 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/sumsqexact 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
Controllergesture table exactly as implemented, live follow with relative zoom and mouse panning, and theXFollow/set_x_rangereinterpretation with its clamp. -
styling — every
Stylefield with its default and whether it is logical or device, theui_scalerule, the grid classes, the crosshair dash, and theLegend/CrosshairStyleknobs. -
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/*.mdonly 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"