# 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 ```sh ./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 need `live_design!`, a `Cx`, and an event loop; they are gated by the full `nigig-build` build/test CI jobs instead. The split is deliberate: `tests_pure.rs` contains everything that can run host-only, and both files are compiled into the crate's normal test suite. * **`persistence.rs` write paths** — `save_doc_state` / `save_doc_state_as` / `load_saved_doc_state` resolve the host's real application-data directory and unconditionally write into it. They are thin wrappers over `save_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 why `persistence.rs` keeps a lower floor (55%) — do not lower it further without a genuine new exclusion. * **Defensive guards** — cycle guards in RGA/block-order traversal (`seen.insert` `continue` arms), unreachable block-id parse failures inside `layout_projection` (engine ids always contain `actor:counter`), and the `unreachable` canvas-text serialization arm's sibling rejections in `advanced_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) 1. `editing/commands.rs::ReplaceBlockRange` validated the explicit `block_ids` length **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. 2. `model/crdt.rs::visit_children` was dead code (a String-collecting duplicate of `visit_atoms` with no callers). Removed. 3. Multi-peer atom-id collisions: two `CrdtMetadata::default()` peers mint identical `AtomId`s, so a concurrent insert silently resurrects instead of inserting. The sync test now assigns distinct `document.crdt.local_actor`s — matching how real peers must be provisioned — and pins the convergent `"hi!"` exchange.