#!/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" < "$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" < "$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