4.7 KiB
Doc-workspace source coverage
tools/test-doc-workspace-coverage.sh measures line/region coverage for the
pure (widget-free) part of this module and enforces floors in CI
(.forgejo/workflows/nigig-build.yml, job doc-workspace-coverage). The same
suite also runs uninstrumented inside cargo test -p nigig-build --lib, so
every test executes twice: once in the real crate and once in the harness.
How it works
The script copies the dependency-free sources — advanced_json.rs,
crdt_bridge.rs, mobile_gesture.rs, persistence.rs,
projection_layout.rs, projection_session.rs, and the collaboration/,
editing/, layout/, model/, plugins/ trees — plus tests_pure.rs
byte-for-byte into a temporary host-only crate with the real module path. A 30
line makepad-widgets shim (re-exporting makepad_math plus the log! /
error! macros) satisfies the only Makepad symbols the pure layer uses
(DVec2, Rect, Vec4f, dvec2, vec4).
doc-engine is included as a path dependency because the pure projection code
sits on top of it.
The harness then installs its own pinned toolchain with
llvm-tools-preview into the same temp dir, runs the suite under
-C instrument-coverage, and enforces:
- a total line floor (
DOC_WS_COVERAGE_TOTAL_FLOOR), and - a per-file floor for every instrumented source
(
DOC_WS_COVERAGE_PER_FILE_FLOORS).
The per-file floors are the point: deleting one file's whole test section
moves the total by a point or two and a lone number would wave that through.
Lowering a floor is a reviewable edit to the script, not something to do
quietly. Everything — toolchain, cargo home, target dir, fetched Makepad
tree, profraw data — is removed by a shell trap on every exit path; nothing
is written into the repository or $HOME unless KEEP_COVERAGE=1 is set.
Running it
./tools/test-doc-workspace-coverage.sh # gated run (CI)
DOC_WS_COVERAGE_REPORT_ONLY=1 ./tools/test-doc-workspace-coverage.sh # measure only
KEEP_COVERAGE=1 ./tools/test-doc-workspace-coverage.sh # keep uncovered-lines.txt + report
Latest measurement (2026-08-17)
| File | Lines covered |
|---|---|
| TOTAL | 96.76% (6170 lines) |
| advanced_json.rs | 98.02% |
| crdt_bridge.rs | 95.45% |
| mobile_gesture.rs | 100% |
| persistence.rs | 65.85% (see exclusions) |
| projection_layout.rs | 98.52% |
| projection_session.rs | 100% |
| collaboration/* | 100% |
| editing/commands.rs | 93.39% |
| editing/controller.rs | 93.39% |
| editing/history.rs | 95.65% |
| layout/* | 94–100% |
| model/* | 91–100% |
| plugins/mod.rs | 100% |
What is intentionally not measured
- The widget layer —
mod.rs,crdt_widget.rs,widgets/,render/,projection_renderer.rs,tests.rs. These needlive_design!, aCx, and an event loop; they are gated by the fullnigig-buildbuild/test CI jobs instead. The split is deliberate:tests_pure.rscontains everything that can run host-only, and both files are compiled into the crate's normal test suite. persistence.rswrite paths —save_doc_state/save_doc_state_as/load_saved_doc_stateresolve the host's real application-data directory and unconditionally write into it. They are thin wrappers oversave_doc_state_to/load_saved_doc_state_with, which the tests drive with explicit temp paths; the wrappers themselves are covered by the widget-runtime tests on device. This is whypersistence.rskeeps a lower floor (55%) — do not lower it further without a genuine new exclusion.- Defensive guards — cycle guards in RGA/block-order traversal
(
seen.insertcontinuearms), unreachable block-id parse failures insidelayout_projection(engine ids always containactor:counter), and theunreachablecanvas-text serialization arm's sibling rejections inadvanced_json.rs. They exist for invariant defense; there is no honest public-API path that reaches them.
Defects found by this coverage drive (fixed with tests)
editing/commands.rs::ReplaceBlockRangevalidated the explicitblock_idslength after draining blocks out of the document, so a malformed command destroyed content before reporting failure. Validation now happens before any mutation; a test pins that a rejected replace leaves blocks and ids untouched.model/crdt.rs::visit_childrenwas dead code (a String-collecting duplicate ofvisit_atomswith no callers). Removed.- Multi-peer atom-id collisions: two
CrdtMetadata::default()peers mint identicalAtomIds, so a concurrent insert silently resurrects instead of inserting. The sync test now assigns distinctdocument.crdt.local_actors — matching how real peers must be provisioned — and pins the convergent"hi!"exchange.