nigig-org/TEST_BASELINE.md
andodeki cbce585eb4
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
perf(cad): complete 2D and 3D LOD -- Phase 5
The first Phase 5 tranche correctly reduced sub-pixel 2D outlines, but it stopped short of the plan's full level-of-detail phase. Complete the phase by applying the same measured three-pixel policy to the 3D path.

Visible 3D parts below the projected AABB threshold now use one shared unit-cube geometry with an axis-aligned world-bounds transform. Full-detail shape batches, proxy instance rows and proxy draw calls are reported separately. Wireframe and hidden-line overlays use the same policy, and selected or hovered parts remain full detail.

Extend the host-testable projection and proxy policy tests, frame benchmark, source guards and documentation. Keep the real GPU/window visual check explicit: compilation, policy arithmetic, submission structure and coverage pass here, but this environment cannot execute Makepad's draw submission.
2026-08-26 16:25:45 +00:00

233 lines
13 KiB
Markdown

# `nigig-build` test baseline
```
cargo test --locked -p nigig-build --lib
test result: ok. 1133 passed; 0 failed; 21 ignored
```
**The suite is green.** CI asserts a plain pass — any failing test fails the
build. Do not reintroduce a "no worse than N failures" threshold.
The 21 ignored are the CAD `profile_benchmarks` and release-only
merge tests, deliberately `#[ignore]`-d
because they are timing measurements rather than assertions. Run them with
`cargo test -- --ignored --nocapture`.
---
## History
| Stage | Result |
|---|---|
| Before any work | Crate did not compile (44 errors); no test had ever run |
| After Phase 0 | 634 passed / 17 failed |
| After Phase 1 | 679 passed / 17 failed |
| After baseline cleanup | 702 passed / 0 failed |
| After Phase 2 (security) | 720 passed / 0 failed |
| After Phase 3 (performance) | 728 passed / 0 failed |
| After Phase 4 (deletion) | 741 passed / 0 failed |
| After Phase 5.1 (generation-tracked store) | 750 passed / 0 failed |
| After the CAD engine coverage work | **1044 passed / 0 failed** |
Phase 4 removed 29 tests along with the dead `Legacy*` command system they
exercised. That is a *reduction in duplicated coverage*, not a regression:
every one of them tested code that no longer exists, and the equivalent
`Command`/`UndoRedoStack` paths keep their own tests. Verified by diffing
the full test-name list before and after.
The 17 failures were **not** regressions — they were pre-existing logic
defects that had been invisible because the test binary never built. Each
is described below, because the diagnoses are the useful part: roughly half
turned out to be wrong *tests* rather than wrong code, and telling those
apart required reading the production callers in every case.
---
## What the 17 were
### Genuine product bugs (10)
| Area | Defect |
|---|---|
| `cad::construction_geometry` | `next_polar_increment` used `position(..).unwrap_or(0)` then advanced, so an unrecognised increment returned 10 instead of restarting the cycle at 5. |
| `cad::construction_geometry` | `snap_to_polar_angle` returned its result unnormalised, so 370° snapped to 360° instead of 0°. Now wraps into `(-π, π]`, matching the `atan2` output the viewport feeds it. |
| `cad::construction_geometry` | `parse_coord_input` accepted `"3."` as 3.0. Rust parses it, but the user is mid-way through typing `3.5`, so the live preview committed a wrong-length segment on every keystroke. Trailing `.`/`e`/`+`/`-` are now treated as incomplete, and `"nan"`/`"inf"` are rejected. |
| `cad::viewport` | The viewport carried **inline copies** of both polar helpers, so neither fix above would have reached users. Both now call the shared functions. |
| `project_management::logic` | `would_create_cycle` walked the graph from the wrong end and tested the seed node before expanding, so it missed both direct (`1→2`, then link `2→1`) and indirect (`1→2→3`, then link `3→1`) cycles. Dependency cycles could be created through the UI. |
| `project_management::logic` | `get_over_allocated_assignees` keyed unassigned tasks on the empty string, so two overlapping *unassigned* tasks were reported as one over-booked person. |
| `project_management::persistence` | Serialization escaped `"` but not `\`, and deserialization used `trim_matches(.. '"')`, which stripped escaped quotes and left the escaping backslash behind. `Phase "Alpha"` round-tripped as `Phase "Alpha\`. Replaced with a proper escape/unescape pair covering `\ " \n \r \t`. |
| `cost_estimator::data_raw` | The default-rooms index used `HashMap::remove`, which consumes the entry. The same property type appears once per region, so the first region got its default rooms and the other two got none. |
| `cost_estimator::estimate_store` | Selections were normalised only in response to a user command, so a freshly constructed store kept `Estimate::default()`'s empty region — which matches nothing in the lookup index, leaving the property dropdown blank until the user touched a control. |
| `cost_estimator::screen_state` | `is_blocked` omitted `Initializing`, so the screen reported itself interactive while cost data was still loading. Now the exact complement of `is_ready`. |
| `doc-engine::crdt` | **Text atoms sorted ascending by id.** RGA requires siblings ordered *counter descending, actor ascending*: newest-nearest-the-anchor for correct sequential typing, actor tiebreak for deterministic convergence. Typing `!` after the `h` of `hi` produced `hi!` instead of `h!i`. Block ordering legitimately stays ascending and was left alone. |
Also fixed here: `CrdtDocument::to_json`, added in Phase 0, had never been
executed. `operations` is a `BTreeMap<OpId, _>` and JSON object keys must be
strings, so it failed at runtime with *"key must be a string"*. It now
serialises the log as an array of `[key, value]` pairs.
### Wrong tests (7)
Each was verified against the production caller before being changed.
| Test | Why it was wrong |
|---|---|
| `dde_buffer_incremental_parse` | Asserted `"5<4"` is "partial". It is a valid polar entry (5 units at 4°) and indistinguishable from `"5<0"`, which a neighbouring test asserts is **valid**. Rewritten; a separate test now covers genuinely incomplete input. |
| `logic::no_cycle` | Asserted the opposite of `cycle_direct` on a structurally identical graph — relabel the ids and they are the same call with contradictory expectations. Duplicate-link suppression is the caller's job (`dependencies.contains`), not this function's. |
| `history::undo_redo_roundtrip`, `history::multiple_undo_redo_cycle` | Modelled snapshots taken *after* each mutation; every production caller snapshots *before* (`self.snapshot(); self.tasks.push(..)`). Under the real protocol the old sequences pushed the current state, so undo popped the state you were already in. |
| `solar::calculate_lead_acid_battery` | Asserted `3.125` while the engine rounds every output to 2 dp. The test's own comment computed the unrounded intermediate. |
| `services::export_json_empty_rooms` | Wanted compact `"rooms": []`; the writer emitted a valid but awkward multi-line empty array. Fixed the writer — the test's preference was reasonable. |
| `types::*_inequal_to_*` (2, fixed in Phase 0) | Asserted `assert_ne!` between distinct newtypes — a comparison the type system now rejects at compile time, which is strictly stronger. |
---
## CAD engine line coverage
The pass/fail count above says the suite is green. It does not say what
the suite touches, and until this work nothing measured that.
A correction worth recording, because it cost six sessions. The reason
given for not measuring — including by me, repeatedly, in commit
messages and in this file — was that `cargo test -p nigig-build` needs
wayland, X11, GL, alsa and polkit and therefore "does not build in this
environment". That was never tested. It builds fine: in a plain
container, `bash tools/makepad-native-libs.sh --install` followed by
`cargo check --locked -p nigig-build --lib` takes 2m52s, and
`cargo test --locked -p nigig-build --lib` takes about five minutes cold
and under a second warm for 1133 tests.
`--test cad_integration` adds 154 more. `cargo fmt -p nigig-build
--check`, the lockfile gate and the mistyped-enum-variant gate all pass.
The install script existed the whole time, written for exactly this,
with a comment explaining that libasound and libpulse are needed to
*link* a test binary rather than merely to check it. Both this file's
"Reproducing" section and the review's "Required next commands" pointed
at it. An assumption made once and never retested is indistinguishable
from a fact, which is the same failure this file criticises the CAD
review for.
`tools/test-cad-coverage.sh` measures it, and is still the right tool for
the job: it needs no apt, no root and no desktop packages, it runs in
about 20 seconds warm against five minutes for the full crate, and it is
what the CI gate uses. The full-crate build is the thing to reach for
when a change touches the widget layer, which the harness cannot see. Nineteen of the CAD module's
thirty files are pure -- geometry, the scene graph, undo/redo, the
four exporters, file I/O -- and their only Makepad imports are the math
types, the CSG library and two log macros, all dependency-free Rust. The
script copies those into a host-only crate with the same module path,
runs them plus `tests/cad_integration.rs` under `-C instrument-coverage`,
and enforces a total floor and a per-file floor. The `cad-engine-coverage`
job in `.forgejo/workflows/nigig-build.yml` runs it on every push.
| File | Lines | Floor | Note |
|---|---|---|---|
| `batching.rs` | 100.00% | 99 | new — Phase 3 instanced batching |
| `cull.rs` | 100.00% | 99 | Phase 1 viewport culling |
| `nav_pad.rs` | 100.00% | 99 | |
| `render_budget.rs` | 99.75% | 99 | Phase 4 batching + Phase 5 2-D/3-D LOD budget |
| `lod.rs` | 99.68% | 99 | new — Phase 5 2-D/3-D sub-pixel policy |
| `scene_holder.rs` | 100.00% | 99 | |
| `section_shape.rs` | 100.00% | 99 | |
| `send_sync_audit.rs` | 100.00% | 99 | |
| `math.rs` | 99.60% | 97 | was 20.40%, no tests at all |
| `arch_svg.rs` | 99.02% | 97 | |
| `arch_stl.rs` | 98.88% | 98 | |
| `arch_gltf.rs` | 98.65% | 97 | |
| `cad_scene.rs` | 98.56% | 96 | |
| `constants.rs` | 98.51% | 95 | |
| `tools.rs` | 97.07% | 95 | was 0.00% — see below |
| `commands.rs` | 96.23% | 94 | |
| `construction_geometry.rs` | 95.88% | 95 | rest is `panic!` arms inside tests |
| `persistence.rs` | 94.51% | 90 | rest is the two real-data-dir wrappers |
| `exporters.rs` | 91.17% | 88 | rest is the save-dialog branch |
| `arch_pdf.rs` | 88.99% | 86 | rest is the printpdf emitter |
| **Total** | **97.39%** | **96** | 653 engine-coverage tests (499 unit + 154 integration); 1,133 full-crate unit tests |
Measured against the state of the tree when this table was written; the
floors, not the table, are what CI enforces.
### The other half: the widget layer, measured at last
The engine table above is 97% of the *smaller* half. Here is the rest,
from `./tools/test-cad-widget-coverage.sh` — the real crate, real tests,
`-C instrument-coverage`, no desktop required:
| File | Lines | Covered |
|---|---|---|
| `mod.rs` | 42 | 80.95% |
| `script_bindings.rs` | 724 | 71.27% |
| `viewport.rs` | 3,548 | 14.37% |
| `workspace.rs` | 2,462 | 14.18% |
| `viewport_input.rs` | 672 | **0.00%** |
| `viewport_render.rs` | 2,083 | **0.00%** |
| `workspace_actions.rs` | 690 | **0.00%** |
| `cad_editor_sheet.rs` | 224 | **0.00%** |
| `viewport_2d.rs` | 138 | **0.00%** |
| `code_editor.rs` | 67 | **0.00%** |
| **Total** | **10,637** | **13.25%** |
9,228 lines have never been executed by a test. Six files have never had
a single line run. Among them is `viewport_input.rs` — every click,
drag, modifier and keystroke the CAD editor handles — and
`viewport_render.rs`, every draw call.
Worth sitting with, because it reframes the engine work above: 1133
green tests and a 97% engine coexist with an input layer that no test
has ever touched. The two facts were never in tension; they were just
never on the same page.
No floors here yet, deliberately. A floor at 13% reads as a blessing.
The first person to write a real input test should set one behind them.
### What moving the number actually costs
The first attempt is worth recording, because it calibrates the rest.
`nav_pad.rs` extracted the navigation pad's hit-testing out of
`viewport_input.rs` into a pure module: 64 lines moved, now at 100% with
7 tests, and a one-pixel misalignment fixed on the way. The widget total
moved 13.15% -> 13.25%.
`viewport_input.rs` is still at 0.00%, and will stay there under this
strategy — extraction makes logic testable by moving it *out*, so the
handler shrinks rather than getting covered. At roughly 60 lines per
extraction, the remaining 672 lines of input handling are ten more of
these. That is a reasonable way to spend the effort, and the pixel bug
suggests the yield is real, but it will not produce a big number
quickly. Genuinely covering the handlers themselves means driving them
with an event loop — `makepad-test`, which `tests/cad_ui.rs` and
spreadsheet-ui already use — and that is a different piece of work.
### Which is the right tool
| | `test-cad-coverage.sh` | `test-cad-widget-coverage.sh` |
|---|---|---|
| Measures | the 15 pure engine files | the 10 widget files |
| Needs | nothing but network | the Makepad Linux packages, so root |
| Cold / warm | ~3 min / ~20 s | ~6 min / ~4 s |
| Gated in CI | yes, with per-file floors | not yet |
```bash
./tools/test-cad-coverage.sh # measure and enforce
KEEP_COVERAGE=1 ./tools/test-cad-coverage.sh # keep the uncovered-line list
# local loop: seed the caches once, then each run takes ~20s
export CAD_COV_TOOLCHAIN_HOME=~/.cache/cad-cov \
CAD_COV_TARGET_DIR=~/.cache/cad-cov/target
```
---
## Reproducing
```bash
sudo apt-get install -y pkg-config libwayland-dev libxcursor-dev \
libxrandr-dev libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
libpolkit-gobject-1-dev libpolkit-agent-1-dev libglib2.0-dev \
libssl-dev libsqlite3-dev libudev-dev libpulse-dev libxkbcommon-dev
cargo test --locked -p nigig-build --lib
cargo test --locked -p doc-engine
```
`libpulse-dev` and `libxkbcommon-dev` are link-time-only requirements —
`cargo check` passes without them, `cargo test` does not.