nigig-org/tools/test-doc-workspace-coverage.sh
arena-agent 949cf24189 test(doc): doc-workspace coverage harness with enforced floors
tools/test-doc-workspace-coverage.sh measures line+region coverage of
the doc module's pure layer the same way the CAD gate does: it copies
the dependency-free sources (advanced_json, crdt_bridge,
mobile_gesture, persistence, projection_layout, projection_session,
and the collaboration/, editing/, layout/, model/, plugins/ trees)
plus tests_pure.rs into a temporary host-only crate with the real
module path, satisfies the five makepad-math symbols the pure layer
uses through a 30-line makepad-widgets shim, runs the suite under
-C instrument-coverage with a toolchain it installs itself, and
enforces a total floor plus a per-file floor for every instrumented
file. The per-file floors are the point: a lone total waves through
the silent loss of one whole file's tests.

Measurement moved from a 28.55% line baseline to 96.76% (6170 lines)
with the tranche in the parent commit; floors sit a few points under
per file, except persistence.rs (55%), whose three write-path entry
points write into the host's real application-data directory and are
covered through their path-injected seams instead -- the honest
exclusions, the exact table, and the two defect fixes this drive
surfaced (ReplaceBlockRange validation order, dead
RgaText::visit_children) are written down in the module's new
COVERAGE.md.

Everything the script touches -- pinned toolchain, cargo home, target
dir, fetched Makepad tree, profraw data -- lives under one mktemp dir
removed by a shell trap on every exit path; nothing lands in the repo
or $HOME unless KEEP_COVERAGE=1 is set for a debugging run.
DOC_WS_COVERAGE_REPORT_ONLY=1 measures without gating.
2026-08-17 10:25:27 +00:00

404 lines
15 KiB
Bash
Executable file

#!/usr/bin/env bash
# Temporary LLVM source-coverage run for the doc workspace's pure logic.
#
# WHAT THIS COVERS
# ----------------
# The doc module (construction_frame/pages/workspace/doc) is ~20k lines,
# most of it widget code that needs live_design!, Cx and an event loop.
# But the module's CORE is dependency-free: the document model types,
# legacy layout engine pieces, collaboration glue, the projection
# layout tree (glyph/rect geometry, hit tests, selection handles), the
# projection session (save/load wire), advanced-node JSON, clipboard /
# style editing logic, persistence and the mobile gesture state machine.
# Those files import only the makepad-math types (DVec2/Rect/Vec4f,
# dvec2/vec4), doc-engine (pure Rust), serde and std. This harness
# copies them into a host-only crate that carries the SAME module path
# (`nigig_build::construction_frame::pages::workspace::doc::*`), so the
# sources compile byte-for-byte with no edits, no GUI, no windowing
# system and no platform startup — the CAD harness pattern, applied to
# the doc surface.
#
# The test driver is the crate's own tests_pure.rs: the doc test suite
# was split so that every dependency-free test lives there (widget
# runtime tests stay in tests.rs). The harness copies it byte-for-byte
# as `#[cfg(test)] mod tests_pure`, so the measurement is exactly the
# pure suite the lib run executes — single-sourced, no drift.
#
# WHAT IT EXCLUDES FROM THE REPORT (step 4 of the coverage plan)
# - the Makepad checkout -- generated/vendored upstream code
# - the cargo registry and git dirs -- third-party code
# - the rustc sysroot -- std
# - harness/src/lib.rs, shim/ -- the platform-startup stand-ins
# this script writes itself; scaffolding, not doc code, and counting
# it would flatter the number for no reason.
# Widget files (crdt_widget.rs, widgets/, render/,
# projection_renderer.rs, mod.rs) stay unmeasured here on purpose: they
# are gated by the crate's own lib suite in CI, and this script does not
# pretend to measure them.
#
# USAGE
# ./tools/test-doc-workspace-coverage.sh # run, enforce floors, clean up
# KEEP_COVERAGE=1 ./tools/test-doc-workspace-coverage.sh # keep env + uncovered lines
# DOC_WS_COVERAGE_REPORT_ONLY=1 ./tools/test-doc-workspace-coverage.sh # measure, don't gate
#
# Everything -- toolchain, cargo home, target dir, profraw data, the
# fetched Makepad tree and the report -- lives under a single mktemp
# directory a shell trap removes on success, failure, interrupt or
# termination. Nothing is written into the repository or $HOME.
set -Eeuo pipefail
IFS=$'\n\t'
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
# Floors, set a couple of points under today's measurement so ordinary
# refactoring does not trip them while a real loss of coverage does.
# A single total hides the failure this is meant to catch: losing every
# test in one file moves the total by a point or two and a lone number
# would wave that through. Lowering a floor is a reviewable edit here,
# not something to do quietly.
TOTAL_FLOOR="${DOC_WS_COVERAGE_TOTAL_FLOOR:-92}"
PER_FILE_FLOORS="${DOC_WS_COVERAGE_PER_FILE_FLOORS:-\
projection_layout.rs:95
projection_session.rs:95
mobile_gesture.rs:95
persistence.rs:55
advanced_json.rs:94
crdt_bridge.rs:91
collaboration/session.rs:95
collaboration/transport.rs:92
editing/commands.rs:89
editing/controller.rs:89
editing/history.rs:91
layout/advanced_layout.rs:90
layout/block_cache.rs:90
layout/block_layout.rs:90
layout/divider_layout.rs:90
layout/hit_test.rs:90
layout/image_layout.rs:90
layout/layout_engine.rs:90
layout/layout_tree.rs:90
layout/mod.rs:90
layout/page_cache.rs:90
layout/page_layout.rs:90
layout/table_layout.rs:90
model/advanced.rs:92
model/crdt.rs:93
model/crdt_advanced.rs:82
model/crdt_table.rs:92
model/document.rs:95
model/selection.rs:92
model/session.rs:92
model/style.rs:92
plugins/mod.rs:90}"
TOOLCHAIN="$(sed -n 's/^channel = "\(.*\)"/\1/p' "$ROOT/rust-toolchain.toml")"
HOST_TRIPLE="${DOC_WS_COV_HOST:-x86_64-unknown-linux-gnu}"
# Same TMPDIR reasoning as tools/test-spreadsheet-coverage.sh: a mktemp
# default under $HOME/.cache -- roomy, outside the workspace, and removed
# on every exit path.
DEFAULT_TMP="${HOME:-/var/tmp}/.cache/nigig-coverage"
mkdir -p "${TMPDIR:-$DEFAULT_TMP}"
WORK="$(mktemp -d "${TMPDIR:-$DEFAULT_TMP}/doc-workspace-coverage.XXXXXXXX")"
KEEP_COVERAGE="${KEEP_COVERAGE:-0}"
cleanup() {
local status=$?
if [[ "$KEEP_COVERAGE" == "1" ]]; then
echo "coverage environment retained: $WORK" >&2
else
rm -rf -- "$WORK"
rmdir "$DEFAULT_TMP" 2>/dev/null || true
rmdir "${HOME:-/var/tmp}/.cache" 2>/dev/null || true
echo "cleaned isolated coverage environment" >&2
fi
exit "$status"
}
trap cleanup EXIT HUP INT TERM
DOC="$ROOT/crates/apps/nigig-build/src/construction_frame/pages/workspace/doc"
MANIFEST="$ROOT/crates/apps/nigig-build/Cargo.toml"
# Root-level pure files. Subdirectories are copied whole (their mod.rs
# is pure too), listed in PURE_DIRS below. Adding a new pure module at
# the root means adding it here, otherwise it is silently unmeasured --
# the watchdog at the end of section 3 looks for exactly that drift.
ROOT_FILES=(
advanced_json.rs
crdt_bridge.rs
mobile_gesture.rs
persistence.rs
projection_layout.rs
projection_session.rs
)
PURE_DIRS=(
collaboration
editing
layout
model
plugins
)
# ---------------------------------------------------------------------------
# 1. Isolated toolchain with the coverage instrumentation components
# ---------------------------------------------------------------------------
export RUSTUP_HOME="$WORK/rustup"
export CARGO_HOME="$WORK/cargo"
export CARGO_TARGET_DIR="$WORK/target"
export PATH="$CARGO_HOME/bin:$PATH"
export LLVM_PROFILE_FILE="$WORK/profiles/%p-%m.profraw"
export RUSTFLAGS="-C instrument-coverage -C codegen-units=1 -C opt-level=0"
mkdir -p "$WORK/profiles"
curl --fail --location https://sh.rustup.rs -o "$WORK/rustup-init"
chmod 700 "$WORK/rustup-init"
"$WORK/rustup-init" -y --profile minimal --default-toolchain "$TOOLCHAIN" \
--component llvm-tools-preview --no-modify-path
# ---------------------------------------------------------------------------
# 2. makepad-math sources at the pinned rev (the only Makepad the pure
# files touch). Same sparse/blobless trick as tools/test-cad-coverage.sh:
# 29 MB and seconds instead of a 319 MB shallow clone.
# ---------------------------------------------------------------------------
MAKEPAD_REV="$(sed -n 's/.*makepad-widgets.*rev = "\([0-9a-f]\{40\}\)".*/\1/p' \
"$MANIFEST" | head -1)"
[[ -n "$MAKEPAD_REV" ]] || { echo "cannot read the pinned makepad rev"; exit 1; }
MAKEPAD="$WORK/makepad"
git init --quiet "$MAKEPAD"
git -C "$MAKEPAD" remote add origin https://gitdab.com/andodeki/makepad
# The closure of makepad-math's path dependencies: math -> micro_serde ->
# live_id, each with a proc-macro sibling.
git -C "$MAKEPAD" sparse-checkout set --cone \
libs/math libs/micro_serde libs/live_id libs/micro_proc_macro
git -C "$MAKEPAD" fetch --quiet --depth 1 --filter=blob:none origin "$MAKEPAD_REV"
git -C "$MAKEPAD" checkout --quiet FETCH_HEAD
for manifest in libs/math libs/micro_serde libs/micro_serde/derive \
libs/micro_proc_macro libs/live_id libs/live_id/id_macros; do
test -f "$MAKEPAD/$manifest/Cargo.toml" \
|| { echo "missing $manifest in the Makepad checkout"; exit 1; }
done
# ---------------------------------------------------------------------------
# 3. Assemble the host-only harness
# ---------------------------------------------------------------------------
HARNESS="$WORK/harness"
DEST="$HARNESS/src/construction_frame/pages/workspace/doc"
mkdir -p "$DEST" "$WORK/shim/src"
cat > "$WORK/shim/Cargo.toml" <<EOF
[package]
name = "makepad-widgets"
version = "0.1.0"
edition = "2021"
[lib]
name = "makepad_widgets"
path = "src/lib.rs"
[dependencies]
makepad-math = { path = "$MAKEPAD/libs/math" }
EOF
cat > "$WORK/shim/src/lib.rs" <<'EOF'
//! Host-only stand-in for `makepad_widgets`. The pure doc files take
//! only the math types from Makepad (DVec2/Rect/Vec4f, dvec2/vec4), so
//! the shim re-exports makepad-math and nothing from the GUI or
//! platform layer -- which is what keeps this build headless.
pub use makepad_math::*;
EOF
# doc-engine is a path dependency of the real crate and pure Rust, so
# the harness references a copy of the real thing (no lockfile of its
# own -- the workspace root owns the lock -- so it re-resolves serde,
# a two-crate registry hit).
mkdir -p "$WORK/doc-engine"
cp "$ROOT/crates/apps/doc/doc-engine/Cargo.toml" "$WORK/doc-engine/"
cp -r "$ROOT/crates/apps/doc/doc-engine/src" "$WORK/doc-engine/src"
cat > "$HARNESS/Cargo.toml" <<EOF
[package]
name = "doc-workspace-cov"
version = "0.0.0"
edition = "2021"
publish = false
# The lib is named nigig_build so the copied sources resolve their
# `crate::construction_frame::...` paths against this crate unchanged.
[lib]
name = "nigig_build"
path = "src/lib.rs"
[dependencies]
makepad-widgets = { path = "$WORK/shim" }
doc-engine = { path = "$WORK/doc-engine" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
EOF
cat > "$HARNESS/src/lib.rs" <<'EOF'
//! Coverage harness root. Scaffolding only -- excluded from the report.
/// Stand-in for `nigig_core::dir`, which pulls in the platform layer
/// (the real `app_data_dir` resolves ProjectDirs). Tests point the data
/// dir at temp space anyway; fall back to the system temp dir.
pub mod dir {
use std::path::PathBuf;
pub fn app_data_dir() -> PathBuf {
std::env::var_os("NIGIG_DOC_COV_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}
}
pub mod construction_frame {
pub mod pages {
pub mod workspace {
pub mod doc;
}
}
}
EOF
# The pure sources, placed at the same relative path as in the crate.
for f in "${ROOT_FILES[@]}"; do
cp "$DOC/$f" "$DEST/$f"
done
for d in "${PURE_DIRS[@]}"; do
cp -r "$DOC/$d" "$DEST/$d"
done
cp "$DOC/tests_pure.rs" "$DEST/tests_pure.rs"
# doc/mod.rs is widget-bound and cannot compile host-only, so the
# harness generates one that declares the pure subtree plus the test
# module. Every name comes from the file lists above, so the harness
# cannot drift away from the crate silently: a file removed from the
# crate fails the cp above, and a new pure file fails the watchdog below.
{
echo "//! Pure-logic view of the doc module. Generated by"
echo "//! tools/test-doc-workspace-coverage.sh -- do not edit."
for d in "${PURE_DIRS[@]}"; do
echo "pub mod $d;"
done
for f in "${ROOT_FILES[@]}"; do
echo "pub mod ${f%.rs};"
done
echo "#[cfg(test)]"
echo "mod tests_pure;"
} > "$DEST/mod.rs"
# Watchdog: a pure file in the crate that this harness does not measure.
is_pure() {
! grep -qE 'live_design!|impl Widget|&mut Cx|Live, LiveHook|use makepad_widgets::\*' "$1"
}
for f in "$DOC"/*.rs; do
base="$(basename "$f")"
case "$base" in
mod.rs|tests.rs|tests_pure.rs) continue ;;
esac
case " ${ROOT_FILES[*]} " in
*" $base "*) continue ;;
esac
if is_pure "$f"; then
echo "NOTE: $base has no widget markers but is not in ROOT_FILES." >&2
echo " If it is pure, add it so it gets measured." >&2
fi
done
# ---------------------------------------------------------------------------
# 4. Instrumented run: the copied pure suite (unit tests inside the
# sources' #[cfg(test)] blocks plus tests_pure.rs)
# ---------------------------------------------------------------------------
cargo test --manifest-path "$HARNESS/Cargo.toml" --lib
# ---------------------------------------------------------------------------
# 5. Report
# ---------------------------------------------------------------------------
LLVM_BIN="$RUSTUP_HOME/toolchains/$TOOLCHAIN-$HOST_TRIPLE/lib/rustlib/$HOST_TRIPLE/bin"
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw \
-o "$WORK/coverage.profdata"
mapfile -t BINS < <(find "$CARGO_TARGET_DIR/debug/deps" -maxdepth 1 -type f -executable \
-name 'nigig_build-*' ! -name '*.d')
[[ ${#BINS[@]} -gt 0 ]] || { echo "no instrumented test binaries found"; exit 1; }
OBJECTS=("${BINS[0]}")
for b in "${BINS[@]:1}"; do OBJECTS+=(-object "$b"); done
# Two independent exclusions (same reasoning as the CAD harness):
MAKEPAD_ESC="$(printf '%s' "$MAKEPAD" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')"
IGNORE="(/cargo/registry|/cargo/git|/rustc/|$MAKEPAD_ESC|/shim/|harness/src/lib\.rs)"
SOURCES=()
for f in "${ROOT_FILES[@]}"; do
SOURCES+=("$DEST/$f")
done
for d in "${PURE_DIRS[@]}"; do
while IFS= read -r f; do SOURCES+=("$f"); done < <(find "$DEST/$d" -name '*.rs' | sort)
done
SOURCES+=("$DEST/tests_pure.rs")
"$LLVM_BIN/llvm-cov" report "${OBJECTS[@]}" \
-instr-profile="$WORK/coverage.profdata" -ignore-filename-regex="$IGNORE" \
"${SOURCES[@]}"
if [[ "$KEEP_COVERAGE" == "1" ]]; then
"$LLVM_BIN/llvm-cov" show "${OBJECTS[@]}" \
-instr-profile="$WORK/coverage.profdata" -ignore-filename-regex="$IGNORE" \
"${SOURCES[@]}" \
| grep -E '^ *[0-9]+\| *0\|' > "$WORK/uncovered-lines.txt" || true
echo "uncovered line report: $WORK/uncovered-lines.txt" >&2
fi
# ---------------------------------------------------------------------------
# 6. Enforce the floors
# ---------------------------------------------------------------------------
"$LLVM_BIN/llvm-cov" export "${OBJECTS[@]}" \
-instr-profile="$WORK/coverage.profdata" -ignore-filename-regex="$IGNORE" \
"${SOURCES[@]}" > "$WORK/coverage.json"
if [[ "${DOC_WS_COVERAGE_REPORT_ONLY:-0}" == "1" ]]; then
echo 'report-only mode: the floors were not enforced'
exit 0
fi
python3 - "$WORK/coverage.json" "$TOTAL_FLOOR" "$PER_FILE_FLOORS" <<'PY'
import json, sys
path, total_floor, per_file = sys.argv[1], float(sys.argv[2]), sys.argv[3]
with open(path) as fh:
data = json.load(fh)
export = data["data"][0]
total = export["totals"]["lines"]["percent"]
measured = {f["filename"]: f["summary"]["lines"]["percent"] for f in export["files"]}
failures = []
if total < total_floor:
failures.append(f" total {total:.2f}% is below the floor of {total_floor:.2f}%")
for line in per_file.split():
if not line.strip():
continue
name, _, floor = line.rpartition(":")
floor = float(floor)
hits = [v for k, v in measured.items() if k.endswith("/doc/" + name)]
if not hits:
failures.append(
f" {name} has a floor but was not measured -- was it renamed, "
"deleted, or dropped from ROOT_FILES? A floor on a file that "
"is not measured silently protects nothing.")
continue
if hits[0] < floor:
failures.append(
f" {name} {hits[0]:.2f}% is below its floor of {floor:.2f}%")
if failures:
print("coverage floors not met:", file=sys.stderr)
print("\n".join(failures), file=sys.stderr)
print(
"\nEither the change removed tested code, or it added untested code.\n"
"Lowering a floor is a reviewable edit to tools/test-doc-workspace-coverage.sh,\n"
"not something to do quietly.",
file=sys.stderr,
)
sys.exit(1)
print(f"all coverage floors met (total {total:.2f}%)")
PY