Asked for a plan to 100%. Four things had to be established first, because each one changes the plan's shape, and three of them contradict what I would have assumed. **There are already 148 widget tests and they have never run.** tests/ui.rs is 1,889 lines of #[makepad_test] tests driving the real app through TestApp/Selector. Line 1 imports a crate that is not a dependency, so the file has never compiled, and no CI job names it. Same defect the spreadsheet-ui suite documents about itself; same class as the nigig-email binary that had never been built. **They compile with a one-line manifest change, and they run.** Adding makepad-test as a dev-dependency produces a binary; under xvfb-run all 148 execute in 59 seconds without hanging. **All 148 fail at a known point.** The harness's child build of the app exits non-zero before startup: code 127 with no cargo on the child PATH, code 101 after fixing that — while `cargo check -p nigig-build --bins` passes. So the blocker is in how the harness invokes the child, not in the app, and spreadsheet-ui already documents the workaround. **#[coverage(off)] is unstable on 1.97.1.** There is no way to annotate a line as legitimately unreachable, so anything genuinely uncoverable has to be covered, moved to an excluded file, or subtracted openly. The plan puts unblocking those 148 tests first, because it is the cheapest large prize and because its outcome resizes everything after it. Engine cleanup runs in parallel since it is independent. Extraction work is explicitly held until Phase 0 reports, so nobody extracts logic the app-level tests already cover. It also argues against 100% as a target for the widget half. The 148 tests are mostly wait_visible(); they will move the number a long way while proving that widgets exist. Of the four real defects this work has found, three came from reading uncovered regions and asking why they were unreachable, not from driving a percentage. The plan targets 100% of what is worth executing and names the ~48 subtracted lines.
9.8 KiB
CAD test coverage: a phased plan to 100%
Written after measuring, not before. Every number here came from a run in this repository; the commands that produced them are named so you can reproduce or refute any of it.
Where we actually are
Two halves, measured by two tools, because one tool cannot see both.
| Half | Files | Lines | Covered | Tool |
|---|---|---|---|---|
| Engine | 15 | 13,752 | 97.16% | tools/test-cad-coverage.sh (gated in CI) |
| Widget | 10 | 10,637 | 13.25% | tools/test-cad-widget-coverage.sh (not gated) |
9,618 lines are uncovered. 96% of them are in the widget half, so that is where the plan spends its effort, and any plan that opens with engine polishing is optimising the wrong term.
The widget half, per file
| File | Lines | Covered |
|---|---|---|
viewport.rs |
3,548 | 14.37% |
workspace.rs |
2,462 | 14.18% |
viewport_render.rs |
2,083 | 0.00% |
script_bindings.rs |
724 | 71.27% |
workspace_actions.rs |
690 | 0.00% |
viewport_input.rs |
672 | 0.00% |
cad_editor_sheet.rs |
224 | 0.00% |
viewport_2d.rs |
138 | 0.00% |
code_editor.rs |
67 | 0.00% |
mod.rs |
42 | 80.95% |
Four findings that decide the plan's shape
1. There are already 148 widget tests, and they have never run.
tests/ui.rs is 1,889 lines of #[makepad_test] tests driving the real
app through TestApp/Selector. Line 1 is
use makepad_test::{...} — a crate that is not a dependency — so the
file has never compiled, and no CI job references it. This is the same
defect the spreadsheet-ui suite documents about itself, and the same
class as the nigig-email binary that had never been built.
2. They compile with a one-line manifest change, and they run.
Adding to crates/apps/nigig-build/Cargo.toml:
[dev-dependencies]
makepad-test = { git = "https://gitdab.com/andodeki/makepad", rev = "ecf5a572ab62a1c1598909971f602f99083671cc", package = "makepad-test" }
produces a test binary. Under xvfb-run -a, all 148 execute in 59
seconds. They do not hang and they do not crash the harness.
3. All 148 currently fail, at a known point. The harness spawns a
child build of the app under test, and that child exits non-zero
"before startup". With no cargo on the child's PATH the code is 127;
after symlinking cargo into /usr/local/bin it becomes 101 — a build
failure — even though cargo check -p nigig-build --bins passes
cleanly. So the remaining blocker is in how the harness invokes the
child build, not in the app. spreadsheet-ui hit the same wall and worked
around it by abandoning the #[makepad_test] attribute for
run_with_config, explicitly injecting PATH, CARGO, CARGO_HOME
and RUSTUP_HOME into the child environment. That workaround is the
first thing to try.
4. #[coverage(off)] is not available. It is still unstable on the
pinned 1.97.1 toolchain (error[E0658]). There is therefore no way to
annotate a line as legitimately unreachable. Anything genuinely
uncoverable must either be covered anyway, moved into a file that the
report excludes wholesale, or subtracted honestly from the target.
On the target itself
100% line coverage is reachable for the engine and is not, by itself, a good goal for the widget half. Two reasons, both concrete:
- Coverage counts lines executed, not assertions made. A test that
clicks every button and asserts nothing scores identically to one that
checks the result. The 148 existing tests are mostly
wait_visible()— they will move the number a long way while proving only that widgets exist. - Six sessions of engine work found four real defects. Three were found
by reading uncovered regions and asking why they were unreachable
(
mat4_inverse, the exporter dispatch arms, the nav-pad pixel), not by driving the percentage up. The number is a search strategy, not the product.
So the plan targets 100% of what is worth executing, and states plainly what is subtracted and why. The subtractions total ~50 lines.
Phase 0 — Unblock the existing suite (1–2 days)
The highest-leverage work in this document. 148 tests already written.
- Add the
makepad-testdev-dependency (diff above). Commit the lockfile change with it. - Fix the import on line 1 of
tests/ui.rs. - Diagnose the child build failure. Capture the child's stderr from
makepad_test's runtime; try the spreadsheet-ui approach ofrun_with_configwithPATH/CARGO/CARGO_HOME/RUSTUP_HOMEinjected, in place of the bare#[makepad_test]attribute. - Triage the failures that remain once the app launches. Expect two
populations: tests whose selectors have drifted from the current UI,
and tests that need
NIGIG_TEST_MODEand a loaded project (the file already hasrequire_project_loaded()for this).
Exit criteria: xvfb-run -a cargo test -p nigig-build --test ui
green, and a CI job running it. Record the pass count and the widget
coverage delta — both are the input to every later estimate here.
Expected yield: unknown, and deliberately not guessed. These tests touch app launch, workspace open/close, tab switching, the spreadsheet and solar panels. If they lift the widget half from 13% to 40% the rest of this plan is much shorter; if they lift it to 18%, Phase 2 grows. Measure before committing to Phases 2–4.
Phase 1 — Close the engine's remaining 390 lines (3–5 days)
Small, well-understood, and it keeps the gated number honest while Phase 0 is in flight.
| File | Uncovered | What it is | Verdict |
|---|---|---|---|
arch_pdf.rs |
152 | printpdf emitter: page furniture, dimension strings, title block | Cover with golden-byte tests over a fixed scene |
commands.rs |
56 | error arms and one #[ignore]d release-only test |
Cover, except the ignored one |
cad_scene.rs |
44 | defensive arms on builder invariants | Cover or delete the unreachable ones |
exporters.rs |
28 | ExportTarget::Prompt — raises a native save dialog |
Subtract (see below) |
construction_geometry.rs |
20 | other => panic!(...) arms inside tests |
Subtract: they execute only when a test fails |
arch_gltf.rs |
14 | GLB byte-layout edges | Cover |
tools.rs |
11 | tool-state edges | Cover |
persistence.rs |
10 | the two real-data-dir wrappers | Cover by pointing HOME/XDG_DATA_HOME at a temp dir |
| others | 55 | scattered | Cover |
Exit criteria: engine ≥ 99.5% with per-file floors raised to match, and the ~48 subtracted lines isolated so the number is honest — see Phase 3.
Phase 2 — Extract logic the harness can reach (2–3 weeks)
For widget code that Phase 0's app-level tests exercise shallowly or not
at all. The pattern is proven: nav_pad.rs moved 64 lines of hit-testing
out of a 630-line event handler into a pure module at 100%, and found a
one-pixel bug on the way.
Targets, in order of expected defect yield rather than size:
viewport_input.rs(672 lines) — snap resolution, direct-distance entry parsing, marquee window-vs-crossing, modifier decisions. All pure given a small state struct.workspace_actions.rs(690) — action dispatch and validation.viewport_2d.rs(138) — screen↔world projection is already pure maths wearing animpl CadViewportcoat.viewport_render.rs(2,083) — geometry generation is separable from the draw calls; the draw calls themselves are not worth extracting.
Calibration from the one data point we have: ~60 lines per extraction, roughly half a day each including tests. The handler does not get covered — it shrinks. Expect the widget percentage to move slowly; expect the defect yield to be the real return.
Exit criteria: the four files above under 200 lines of unextracted imperative glue each, every extracted module ≥ 95% with a floor.
Phase 3 — Make the subtractions explicit (2 days)
Because #[coverage(off)] is unavailable, the ~48 genuinely
unreachable lines have to be handled structurally:
- Move
ExportTarget::Prompt's dialog arm intoexporters_dialog.rs, excluded from the report by filename, with the reason in the file header. The arm becomes three lines that call into it. - Leave the in-test
panic!arms where they are and subtract them in the report generator, which already knows which files are test-only.
Exit criteria: tools/test-cad-coverage.sh prints both a raw and an
adjusted figure, and the adjusted one can legitimately be 100%. Anything
subtracted is listed by file and line count in TEST_BASELINE.md.
Phase 4 — Gate it (2 days)
- Add the widget coverage run to CI, report-only for one week, then with floors at measured-minus-one.
- Raise the engine floor to 99.
- Add the
--test uisuite tofull-crate-checkunder xvfb. - Fix
--test cost_estimatorand--test cost_estimator_ui, which still do not compile and are noted as such innigig-build.yml. Another 1,873 lines of tests nobody runs.
What this does not cover
- The 3D render path.
viewport_render.rs's draw calls can be executed but not meaningfully asserted without golden images, and golden-image testing over a GL context in CI is its own project. script_bindings.rs's VM integration beyond its current 71%. The remaining lines are the makepad script VM boundary.- Assertion quality. Nothing here prevents a test that executes a
line and checks nothing. The
--test uisuite in particular is visibility-heavy. Coverage will not tell you; review has to.
Order of work, and why
Phase 0 first because 148 written tests that have never run is a larger and cheaper prize than anything else on the list, and because its result resizes Phases 2–4. Phase 1 in parallel — it is independent, and it keeps the one gated number moving while Phase 0's outcome is unknown. Do not start Phase 2 before Phase 0 reports: extracting logic that the app-level tests already cover would be effort spent for no measurement.