Adds .forgejo/workflows/cad.yml owning cad-core, cad-ui and shared tooling: non-empty source-root gates, exact ignored-test budget (20), empty-fixture proof, locked cargo check/test/clippy/fmt. Guards every stale nigig-build/.../workspace/cad scan and both coverage harnesses to fail closed (exit 1 with move pointer) until BUILD-00 removes/retargets them; an empty grep scan is never green. Verified: sun 14/14 + measure 19/19 standalone (DVec3 shim); coverage harnesses exit 1; stale-guard loop fail=0; git diff --check clean.
598 lines
23 KiB
Bash
Executable file
598 lines
23 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Temporary LLVM source-coverage run for the nigig-build CAD engine.
|
|
#
|
|
# WHAT THIS COVERS
|
|
# ----------------
|
|
# The CAD module currently has 31 Rust files. Twenty of them are the
|
|
# *engine*: pure geometry, scene graph, undo/redo, exporters and file I/O
|
|
# whose only Makepad imports are math types, the CSG library and two log
|
|
# macros. Those twenty engine files are copied here into a host-only crate
|
|
# that carries the SAME module path as the real crate
|
|
# (`nigig_build::construction_frame::pages::workspace::cad::*`), so the
|
|
# sources compile byte-for-byte with no edits, no GUI, no windowing system
|
|
# and no platform startup.
|
|
#
|
|
# The other ten (mod.rs, viewport*.rs, workspace*.rs, script_bindings,
|
|
# cad_editor_sheet and code_editor) are widget code: they need
|
|
# `live_design!`, `Cx`, `Widget` and a real event loop. They are gated by
|
|
# the `full-crate-check` job in .forgejo/workflows/nigig-build.yml, not by
|
|
# this script, and this script does not pretend to measure them. The pure
|
|
# `profile_benchmarks.rs` file is included only in CAD_BENCH=1 mode so its
|
|
# ignored timing tests do not distort the coverage report.
|
|
#
|
|
# 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/, picker/ -- the platform-startup stand-ins
|
|
# this script writes itself; they are scaffolding, not CAD code, and
|
|
# counting them would flatter (or deflate) the number for no reason.
|
|
#
|
|
# USAGE
|
|
# ./tools/test-cad-coverage.sh # run, report, clean up
|
|
# KEEP_COVERAGE=1 ./tools/test-cad-coverage.sh # keep env + uncovered lines
|
|
#
|
|
# By default everything -- toolchain, cargo home, target dir, profraw
|
|
# data, the fetched Makepad tree and the report -- lives under a single
|
|
# mktemp directory that a shell trap removes on success, failure,
|
|
# interrupt or termination. Nothing is written into the repository or
|
|
# $HOME. That is the mode CI runs, and it is the only mode whose result
|
|
# is reproducible from nothing.
|
|
#
|
|
# Three opt-in knobs make the measure-edit-measure loop bearable
|
|
# locally, where a cold run spends ~90s installing a toolchain and ~60s
|
|
# compiling printpdf before it measures anything:
|
|
#
|
|
# CAD_COV_MAKEPAD=/path/to/makepad reuse a Makepad checkout
|
|
# CAD_COV_TOOLCHAIN_HOME=/path reuse RUSTUP_HOME + CARGO_HOME
|
|
# (expects $path/rustup and
|
|
# $path/cargo; installs into
|
|
# them once if absent)
|
|
# CAD_COV_TARGET_DIR=/path reuse the build cache
|
|
#
|
|
# A reused directory is NOT deleted by the trap -- it lives outside
|
|
# $WORK by definition, and silently deleting a path the caller named
|
|
# would be a surprise. Set none of them and the run is hermetic.
|
|
#
|
|
# # first run seeds the cache, later runs take seconds
|
|
# export CAD_COV_TOOLCHAIN_HOME=~/.cache/cad-cov CAD_COV_TARGET_DIR=~/.cache/cad-cov/target
|
|
#
|
|
# BENCHMARK MODE
|
|
#
|
|
# CAD_BENCH=1 ./tools/test-cad-coverage.sh
|
|
#
|
|
# Same harness, one extra file (profile_benchmarks.rs), built --release
|
|
# with no instrumentation, running the `#[ignore]`-d benchmarks. This is
|
|
# the host-only equivalent of
|
|
#
|
|
# cargo test -p nigig-build cad::profile_benchmarks -- --nocapture --test-threads=1
|
|
#
|
|
# which REVIEWS/PAY_CAD_IMPLEMENTATION_STATUS.md listed as blocked on "no
|
|
# Cargo toolchain available". Coverage instrumentation is deliberately
|
|
# off here: -C instrument-coverage -C opt-level=0 makes every number
|
|
# meaningless. Compare the output against BENCH_BASELINE.md.
|
|
set -Eeuo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cad-coverage.XXXXXXXX")"
|
|
KEEP_COVERAGE="${KEEP_COVERAGE:-0}"
|
|
|
|
# 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 persistence.rs moves the total by under two points.
|
|
#
|
|
# The low floors are the honest ones, not the aspirational ones:
|
|
# arch_pdf -- a large emitter whose remaining gaps are
|
|
# byte-layout paths reached only by a real
|
|
# PDF consumer.
|
|
# exporters -- the ExportTarget::Prompt arm needs a
|
|
# windowing system.
|
|
TOTAL_FLOOR="${CAD_COVERAGE_TOTAL_FLOOR:-96}"
|
|
PER_FILE_FLOORS="${CAD_COVERAGE_PER_FILE_FLOORS:-\
|
|
math.rs:97
|
|
section_shape.rs:99
|
|
construction_geometry.rs:95
|
|
constants.rs:95
|
|
cad_scene.rs:96
|
|
commands.rs:94
|
|
scene_holder.rs:99
|
|
arch_stl.rs:98
|
|
arch_svg.rs:97
|
|
arch_gltf.rs:97
|
|
arch_pdf.rs:86
|
|
exporters.rs:88
|
|
persistence.rs:90
|
|
send_sync_audit.rs:99
|
|
tools.rs:95
|
|
nav_pad.rs:99
|
|
render_budget.rs:99
|
|
cull.rs:99
|
|
lod.rs:99
|
|
batching.rs:99}"
|
|
TOOLCHAIN="$(sed -n 's/^channel = "\(.*\)"/\1/p' "$ROOT/rust-toolchain.toml")"
|
|
HOST_TRIPLE="${CAD_COV_HOST:-x86_64-unknown-linux-gnu}"
|
|
|
|
cleanup() {
|
|
local status=$?
|
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
echo "coverage environment retained: $WORK" >&2
|
|
else
|
|
rm -rf "$WORK"
|
|
echo "cleaned isolated coverage environment" >&2
|
|
fi
|
|
exit "$status"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
CAD="$ROOT/crates/apps/nigig-build/src/construction_frame/pages/workspace/cad"
|
|
MANIFEST="$ROOT/crates/apps/nigig-build/Cargo.toml"
|
|
|
|
# CORE-00: the CAD engine no longer lives under nigig-build. Fail closed
|
|
# with a pointer instead of cryptic `cp` errors or, worse, an
|
|
# accidentally-green empty run. Full retarget to
|
|
# crates/apps/cad/{cad-core,cad-ui} is tracked under BUILD-00/UI-00.
|
|
if [[ ! -d "$CAD" ]]; then
|
|
echo "ERROR: CAD source root $CAD no longer exists." >&2
|
|
echo " The engine moved to crates/apps/cad/cad-core/src and" >&2
|
|
echo " crates/apps/cad/cad-ui/src; this harness still copies from the" >&2
|
|
echo " removed nigig-build/.../workspace/cad tree." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The engine files, in dependency order for a human reader. Adding a new
|
|
# pure module to the CAD directory means adding it here too, otherwise it
|
|
# is silently unmeasured -- so the script checks for that at the end.
|
|
ENGINE_FILES=(
|
|
math.rs
|
|
section_shape.rs
|
|
construction_geometry.rs
|
|
constants.rs
|
|
cad_scene.rs
|
|
commands.rs
|
|
scene_holder.rs
|
|
arch_stl.rs
|
|
arch_svg.rs
|
|
arch_gltf.rs
|
|
arch_pdf.rs
|
|
exporters.rs
|
|
persistence.rs
|
|
send_sync_audit.rs
|
|
tools.rs
|
|
nav_pad.rs
|
|
render_budget.rs
|
|
cull.rs
|
|
lod.rs
|
|
batching.rs
|
|
)
|
|
|
|
# profile_benchmarks.rs is engine code too -- its only Makepad imports
|
|
# are the math types -- but it is measured separately: the benchmarks are
|
|
# `#[ignore]`-d, so counting their lines would report the file as mostly
|
|
# uncovered and say nothing useful.
|
|
BENCH_MODE="${CAD_BENCH:-0}"
|
|
if [[ "$BENCH_MODE" == "1" ]]; then
|
|
ENGINE_FILES+=(profile_benchmarks.rs)
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Isolated toolchain with the coverage instrumentation components
|
|
# ---------------------------------------------------------------------------
|
|
TOOLCHAIN_HOME="${CAD_COV_TOOLCHAIN_HOME:-$WORK}"
|
|
export RUSTUP_HOME="$TOOLCHAIN_HOME/rustup"
|
|
export CARGO_HOME="$TOOLCHAIN_HOME/cargo"
|
|
export CARGO_TARGET_DIR="${CAD_COV_TARGET_DIR:-$WORK/target}"
|
|
export PATH="$CARGO_HOME/bin:$PATH"
|
|
export LLVM_PROFILE_FILE="$WORK/profiles/%p-%m.profraw"
|
|
if [[ "$BENCH_MODE" == "1" ]]; then
|
|
# No instrumentation and no -O0: a benchmark built that way measures
|
|
# the instrumentation.
|
|
export RUSTFLAGS=""
|
|
else
|
|
export RUSTFLAGS="-C instrument-coverage -C codegen-units=1 -C opt-level=0"
|
|
fi
|
|
mkdir -p "$WORK/profiles"
|
|
|
|
if [[ -x "$CARGO_HOME/bin/cargo" ]]; then
|
|
echo "reusing toolchain: $CARGO_HOME" >&2
|
|
else
|
|
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
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Makepad sources for makepad-math + makepad-csg, at the pinned rev
|
|
# ---------------------------------------------------------------------------
|
|
# Only these two libraries are needed. Both are pure Rust with no system
|
|
# dependencies -- that is the whole reason the engine can be measured
|
|
# without a desktop stack.
|
|
if [[ -n "${CAD_COV_MAKEPAD:-}" ]]; then
|
|
MAKEPAD="$CAD_COV_MAKEPAD"
|
|
echo "reusing Makepad checkout: $MAKEPAD" >&2
|
|
else
|
|
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
|
|
# Sparse + blobless + depth 1: three directories instead of the whole
|
|
# fork. 29 MB and two seconds, against 319 MB for a plain shallow
|
|
# checkout of a repository that is mostly shaders, fonts and demos.
|
|
# The closure of path dependencies, not just the two crates named
|
|
# above: math -> micro_serde -> live_id, each with a proc-macro
|
|
# sibling. A missing one fails at manifest-read time, not at compile
|
|
# time, so it is worth listing them explicitly.
|
|
git -C "$MAKEPAD" sparse-checkout set --cone \
|
|
libs/math libs/csg 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
|
|
fi
|
|
# The full path-dependency closure of makepad-math + makepad-csg,
|
|
# resolved from the manifests rather than guessed:
|
|
# math -> micro_serde -> {derive, live_id -> id_macros}, and
|
|
# micro_serde's derive -> micro_proc_macro; csg -> six csg_* siblings.
|
|
for manifest in libs/math libs/csg/csg libs/csg/csg_math libs/csg/csg_mesh \
|
|
libs/csg/csg_boolean libs/csg/csg_exact libs/csg/csg_primitives \
|
|
libs/csg/csg_sdf 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"
|
|
mkdir -p "$HARNESS/src/construction_frame/pages/workspace/cad" \
|
|
"$HARNESS/tests" "$WORK/shim/src" "$WORK/picker/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" }
|
|
makepad-csg = { path = "$MAKEPAD/libs/csg/csg" }
|
|
EOF
|
|
|
|
cat > "$WORK/shim/src/lib.rs" <<'EOF'
|
|
//! Host-only stand-in for `makepad_widgets`. It re-exports exactly what
|
|
//! the CAD engine files take from Makepad -- the math types, the CSG
|
|
//! library and the two log macros -- and nothing from the GUI or
|
|
//! platform layer, which is what keeps this build headless.
|
|
pub use makepad_csg;
|
|
pub use makepad_math;
|
|
pub use makepad_math::*;
|
|
|
|
#[macro_export]
|
|
macro_rules! log {
|
|
($($arg:tt)*) => {{ let _ = format_args!($($arg)*); }};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! error {
|
|
($($arg:tt)*) => {{ let _ = format_args!($($arg)*); }};
|
|
}
|
|
EOF
|
|
|
|
cat > "$WORK/picker/Cargo.toml" <<'EOF'
|
|
[package]
|
|
name = "robius-file-picker"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[lib]
|
|
name = "robius_file_picker"
|
|
path = "src/lib.rs"
|
|
EOF
|
|
|
|
cat > "$WORK/picker/src/lib.rs" <<'EOF'
|
|
//! Headless stand-in for `robius-file-picker`. A save dialog cannot be
|
|
//! raised without a windowing system, so `save_data` reports the
|
|
//! unsupported condition rather than pretending a file was written.
|
|
//! Nothing in the suite asserts against this behaviour: the
|
|
//! `ExportTarget::Prompt` arm of `exporters::deliver_bytes` is dialog
|
|
//! bound and is reported as uncovered, which is the honest result.
|
|
use std::io;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Default)]
|
|
pub struct FileDialog {
|
|
title: String,
|
|
file_name: String,
|
|
}
|
|
|
|
impl FileDialog {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
pub fn set_title(mut self, title: impl Into<String>) -> Self {
|
|
self.title = title.into();
|
|
self
|
|
}
|
|
pub fn set_file_name(mut self, name: impl Into<String>) -> Self {
|
|
self.file_name = name.into();
|
|
self
|
|
}
|
|
pub fn save_data<F>(self, _bytes: Vec<u8>, _cb: F) -> Result<(), io::Error>
|
|
where
|
|
F: FnOnce(Result<Option<PathBuf>, io::Error>) + Send + 'static,
|
|
{
|
|
let _ = (&self.title, &self.file_name);
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"no windowing system in the coverage harness",
|
|
))
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "$HARNESS/Cargo.toml" <<EOF
|
|
[package]
|
|
name = "cad-engine-cov"
|
|
version = "0.0.0"
|
|
edition = "2021"
|
|
publish = false
|
|
|
|
# The lib is named nigig_build so tests/cad_integration.rs -- the real
|
|
# integration suite, copied in unmodified -- resolves its \`nigig_build::\`
|
|
# paths against this crate.
|
|
[lib]
|
|
name = "nigig_build"
|
|
path = "src/lib.rs"
|
|
|
|
[dependencies]
|
|
makepad-widgets = { path = "$WORK/shim" }
|
|
makepad-csg = { path = "$MAKEPAD/libs/csg/csg" }
|
|
robius-file-picker = { path = "$WORK/picker" }
|
|
serde_json = "1"
|
|
rayon = "1"
|
|
printpdf = "0.7"
|
|
time = "0.3"
|
|
EOF
|
|
|
|
cat > "$HARNESS/src/lib.rs" <<'EOF'
|
|
//! Coverage harness root. Scaffolding only -- excluded from the report.
|
|
pub use makepad_csg;
|
|
|
|
/// Stand-in for `nigig_core::dir`, which pulls in the platform layer.
|
|
pub mod dir {
|
|
use std::path::PathBuf;
|
|
pub fn app_data_dir() -> PathBuf {
|
|
std::env::var_os("NIGIG_CAD_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 cad;
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
DEST="$HARNESS/src/construction_frame/pages/workspace/cad"
|
|
for f in "${ENGINE_FILES[@]}"; do
|
|
cp "$CAD/$f" "$DEST/$f"
|
|
done
|
|
cp "$ROOT/crates/apps/nigig-build/tests/cad_integration.rs" "$HARNESS/tests/"
|
|
|
|
{
|
|
echo "//! Engine-only view of the CAD module. Generated by"
|
|
echo "//! tools/test-cad-coverage.sh -- do not edit."
|
|
for f in "${ENGINE_FILES[@]}"; do
|
|
echo "pub mod ${f%.rs};"
|
|
done
|
|
# The real mod.rs re-exports the scene types so `use super::*` in a
|
|
# child module reaches them. tools.rs relies on that for PartKind.
|
|
echo "pub(crate) use cad_scene::*;"
|
|
echo "pub(crate) use math::*;"
|
|
# ...and the Makepad math types the real mod.rs pulls in, so a child
|
|
# module's `use super::*` reaches DVec2 the way it does in production.
|
|
echo "pub(crate) use makepad_widgets::{DVec2, Mat4f, Vec3f, Vec4f};"
|
|
} > "$DEST/mod.rs"
|
|
|
|
# The real mod.rs is widget-bound and cannot compile host-only, but it
|
|
# also declares a handful of PLAIN types -- ordinary structs and enums
|
|
# with ordinary derives -- that the engine files and the integration
|
|
# suite use. Those are EXTRACTED from the real file at run time rather
|
|
# than copied here, so the harness cannot drift away from the crate: if a
|
|
# declaration changes shape, this run compiles the new shape or fails.
|
|
#
|
|
# tools.rs is the reason the list is longer than two. It is 253 lines of
|
|
# pure tool-state logic (tool cycling, work-plane axis mapping, inclined
|
|
# UCS maths) whose own header comment claims its types must live in
|
|
# mod.rs because "they use #[derive] macros that need the script_mod!
|
|
# context". They do not: every one of them derives Clone/Copy/Debug/
|
|
# PartialEq and nothing else, and none is inside the script_mod! block.
|
|
# The real obstacle to moving them out is narrower -- DrawingState,
|
|
# SnapSettings and InclinedPlane have private fields that mod.rs and
|
|
# viewport.rs read directly, so a move needs those widened first. That
|
|
# is a refactor for someone with a compiler for the widget layer;
|
|
# mirroring the declarations here measures the logic today without
|
|
# touching production code at all.
|
|
python3 - "$CAD/mod.rs" "$DEST/mod.rs" <<'PY'
|
|
import re, sys
|
|
src, dest = sys.argv[1], sys.argv[2]
|
|
text = open(src, encoding="utf-8").read()
|
|
# (name, kind) pairs. Order matters only for readability.
|
|
WANTED = [
|
|
("ViewMode", "enum"),
|
|
("SelectionMode", "enum"),
|
|
("CadTool", "enum"),
|
|
("WorkPlane", "enum"),
|
|
("AxisLock", "enum"),
|
|
("RefPlane", "struct"),
|
|
("InclinedPlane", "struct"),
|
|
("DrawingState", "struct"),
|
|
("SnapSettings", "struct"),
|
|
]
|
|
out = ["\n// Extracted verbatim from the crate's mod.rs at run time. Everything",
|
|
"// here is a plain data declaration; the rest of mod.rs is widget code.",
|
|
"// Visibility is widened to pub(crate) so the mirrored module tree can",
|
|
"// see them from the same relative position the real one does.\n"]
|
|
for name, kind in WANTED:
|
|
m = re.search(
|
|
r"(#\[derive\([^)]*\)\]\n)?(?:pub(?:\(crate\))? )?%s %s \{.*?\n\}" % (kind, name),
|
|
text, re.S)
|
|
if not m:
|
|
sys.exit(f"ERROR: {kind} {name} not found in {src}; the harness is out of date")
|
|
decl = m.group(0)
|
|
decl = re.sub(r"(?m)^(#\[derive[^\n]*\]\n)?(pub(?:\(crate\))? )?(%s %s)" % (kind, name),
|
|
lambda mm: (mm.group(1) or "") + "pub " + mm.group(3), decl, count=1)
|
|
out.append(decl + "\n")
|
|
open(dest, "a", encoding="utf-8").write("\n".join(out))
|
|
PY
|
|
|
|
# A new pure module added to cad/ but not to ENGINE_FILES would be
|
|
# unmeasured and nobody would notice. Say so loudly.
|
|
for f in "$CAD"/*.rs; do
|
|
base="$(basename "$f")"
|
|
# Membership by explicit loop: `${ENGINE_FILES[*]}` joins on the first
|
|
# character of IFS, which this script sets to a newline, so the obvious
|
|
# `case " ${ENGINE_FILES[*]} " in *" $base "*)` never matches.
|
|
listed=0
|
|
for known in "${ENGINE_FILES[@]}"; do
|
|
[[ "$known" == "$base" ]] && { listed=1; break; }
|
|
done
|
|
[[ "$listed" == "1" ]] && continue
|
|
# Measured by CAD_BENCH=1 instead; see the note on ENGINE_FILES.
|
|
[[ "$base" == "profile_benchmarks.rs" ]] && continue
|
|
if ! grep -qE 'live_design!|impl Widget|&mut Cx|Live, LiveHook|use makepad_widgets::\*' "$f"; then
|
|
echo "NOTE: $base has no widget markers but is not in ENGINE_FILES." >&2
|
|
echo " If it is pure, add it so it gets measured." >&2
|
|
fi
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Instrumented run: unit tests (in-file #[cfg(test)]) + integration suite
|
|
# ---------------------------------------------------------------------------
|
|
if [[ "$BENCH_MODE" == "1" ]]; then
|
|
echo
|
|
echo "=== CAD benchmarks (release, host-only). Compare against BENCH_BASELINE.md."
|
|
echo "=== Absolute numbers are machine-dependent; the ratios are the signal."
|
|
echo
|
|
cargo test --release --manifest-path "$HARNESS/Cargo.toml" --lib -- \
|
|
--ignored --nocapture --test-threads=1 profile_benchmarks
|
|
exit 0
|
|
fi
|
|
|
|
cargo test --manifest-path "$HARNESS/Cargo.toml" --all-targets
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Report
|
|
# ---------------------------------------------------------------------------
|
|
LLVM_BIN="$RUSTUP_HOME/toolchains/$TOOLCHAIN-$HOST_TRIPLE/lib/rustlib/$HOST_TRIPLE/bin"
|
|
# A reused toolchain home might have been installed without the
|
|
# coverage components. Say which component is missing rather than
|
|
# failing on a "no such file" three lines later.
|
|
if [[ ! -x "$LLVM_BIN/llvm-profdata" ]]; then
|
|
echo "ERROR: llvm-tools-preview is missing from $RUSTUP_HOME." >&2
|
|
echo " rustup component add llvm-tools-preview --toolchain $TOOLCHAIN" >&2
|
|
exit 1
|
|
fi
|
|
"$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-*' -o -name 'cad_integration-*' \) ! -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, because either one alone leaks:
|
|
# - the regex drops third-party/generated code wherever it was built
|
|
# from (the Makepad checkout can live outside $WORK when
|
|
# CAD_COV_MAKEPAD is set, so its real path is spliced in);
|
|
# - the explicit source list means only the twenty engine files and
|
|
# the integration suite contribute to TOTAL, so the number cannot be
|
|
# diluted by scaffolding that happens to sit under $WORK.
|
|
MAKEPAD_ESC="$(printf '%s' "$MAKEPAD" | sed 's/[][\.^$*+?(){}|\/]/\\&/g')"
|
|
IGNORE="(/cargo/registry|/cargo/git|/rustc/|$MAKEPAD_ESC|/shim/|/picker/|harness/src/lib\.rs)"
|
|
|
|
SOURCES=()
|
|
for f in "${ENGINE_FILES[@]}"; do
|
|
SOURCES+=("$DEST/$f")
|
|
done
|
|
SOURCES+=("$HARNESS/tests/cad_integration.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 [[ "${CAD_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("/cad/" + name)]
|
|
if not hits:
|
|
failures.append(
|
|
f" {name} has a floor but was not measured -- was it renamed, "
|
|
"deleted, or dropped from ENGINE_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-cad-coverage.sh,\n"
|
|
"not something to do quietly.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
print(f"all coverage floors met (total {total:.2f}%)")
|
|
PY
|