Some checks failed
repo hygiene / hygiene (push) Has been cancelled
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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Finish the widget-in-cell catalog with the two deferred controls, each a plain value plus a render flag (same convention as checkbox/slider/Markdown). - Dropdown: CellStyle.choices (a serialized, pipe-escaped list) makes a cell cycle through its choices on click. The toolbar "Drop" button parses the selected cell's value as a "Low|Med|High" list into choices (undoable SetChoices) and selects the first entry; a dropdown renders its value with a trailing ▾ affordance. dropdown.rs holds next_choice / parse_choice_list. - Button: CellStyle.button makes a cell a button whose value is a TARGET[+N] action spec (A1 ref + optional signed step, default +1). Clicking increments the target cell's numeric value through set_cell (undoable, recalculates dependents) — the spreadsheet-native counterpart of the reference's "+10" boost. button.rs holds parse_button_spec / button_step / format_step_value; the cell renders as a raised box with the spec centred. Toolbar "Btn" toggles the flag. Both flags ride in the render cache so cached cells stay styled, and both commands route through WorkbookCommand (apply + apply_command + dirty marking). Choices serialize after the markdown/slider/button flags; older files default to off/empty. Engine: 491 lib tests (+4) + integration. UI controllers: 142 tests (+8, button 5 + dropdown 4). Coverage: engine 96.59%, ui-controllers 99.45% (floors 96); button.rs and dropdown.rs at 100%.
284 lines
13 KiB
Bash
Executable file
284 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# LLVM source-coverage for the Spreadsheet engine and UI controller logic.
|
|
#
|
|
# Everything lives in one temporary directory — toolchain, cargo caches, crate
|
|
# copies, build artefacts, profiles and the report — and it is removed on any
|
|
# exit path. Nothing is installed into the host, and the repository's own
|
|
# `target/` is untouched.
|
|
#
|
|
# Usage:
|
|
# ./tools/test-spreadsheet-coverage.sh # engine + ui
|
|
# COVERAGE_TARGET=engine ./tools/test-spreadsheet-coverage.sh
|
|
# COVERAGE_TARGET=ui ./tools/test-spreadsheet-coverage.sh
|
|
# KEEP_COVERAGE=1 ./tools/test-spreadsheet-coverage.sh # keeps the report
|
|
#
|
|
# `KEEP_COVERAGE=1` additionally writes an uncovered-line listing, which is
|
|
# what makes "add tests for uncovered branches" a directed activity rather
|
|
# than guesswork.
|
|
|
|
set -Eeuo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
COVERAGE_TARGET="${COVERAGE_TARGET:-all}"
|
|
KEEP_COVERAGE="${KEEP_COVERAGE:-0}"
|
|
ENGINE_FLOOR="${ENGINE_FLOOR:-96}"
|
|
UI_FLOOR="${UI_FLOOR:-96}"
|
|
|
|
case "$COVERAGE_TARGET" in
|
|
all | engine | ui) ;;
|
|
*)
|
|
echo "error: COVERAGE_TARGET must be all, engine or ui" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
# Two constraints pull against each other here.
|
|
#
|
|
# `/tmp` is a small tmpfs on some hosts and an instrumented build plus a
|
|
# toolchain will not fit, so the default must be somewhere roomier. But the
|
|
# work directory must *not* sit inside the repository: Cargo would then treat
|
|
# the copied crates as workspace members and refuse to build them.
|
|
#
|
|
# `$HOME/.cache` satisfies both — same roomy filesystem, outside the
|
|
# workspace. A caller-supplied TMPDIR wins, and is assumed to be sane.
|
|
DEFAULT_TMP="${HOME:-/var/tmp}/.cache/nigig-coverage"
|
|
mkdir -p "${TMPDIR:-$DEFAULT_TMP}"
|
|
WORK="$(mktemp -d "${TMPDIR:-$DEFAULT_TMP}/spreadsheet-coverage.XXXXXXXX")"
|
|
|
|
cleanup() {
|
|
local status=$?
|
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
echo "coverage environment retained: $WORK" >&2
|
|
else
|
|
rm -rf -- "$WORK"
|
|
# Only remove the default parent, and only when we created it and it
|
|
# is empty: a caller-supplied TMPDIR is not ours to delete.
|
|
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
|
|
|
|
TOOLCHAIN="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
"$ROOT/rust-toolchain.toml" | head -n 1)"
|
|
if [[ -z "$TOOLCHAIN" ]]; then
|
|
echo "error: could not read channel from rust-toolchain.toml" >&2
|
|
exit 2
|
|
fi
|
|
|
|
export RUSTUP_HOME="$WORK/rustup"
|
|
export CARGO_HOME="$WORK/cargo"
|
|
export CARGO_TARGET_DIR="$WORK/target"
|
|
export PATH="$CARGO_HOME/bin:$PATH"
|
|
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
|
export CARGO_INCREMENTAL=0
|
|
export LLVM_PROFILE_FILE="$WORK/profiles/%p-%m.profraw"
|
|
# `-C opt-level=0` keeps line mapping honest: optimisation merges and drops
|
|
# lines, which makes a coverage report describe a program nobody wrote.
|
|
export RUSTFLAGS="-C instrument-coverage -C codegen-units=1 -C opt-level=0"
|
|
mkdir -p "$WORK/profiles"
|
|
|
|
echo "creating isolated coverage toolchain ($TOOLCHAIN) in $WORK" >&2
|
|
curl --fail --location --proto '=https' --tlsv1.2 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
|
|
|
|
LLVM_BIN="$RUSTUP_HOME/toolchains/$TOOLCHAIN-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin"
|
|
|
|
# ── Exclusions (step 4) ──────────────────────────────────────────────────────
|
|
#
|
|
# Two categories are excluded, for different reasons.
|
|
#
|
|
# 1. **Not our code.** Registry and git dependencies, the Rust standard
|
|
# library, and the copy path itself. Including them measures other
|
|
# people's tests.
|
|
#
|
|
# 2. **Generated Makepad DSL and platform startup.** `script_mod! { ... }`
|
|
# expands to widget-registration code that no unit test can reach without
|
|
# a live `Cx` and a window, and `src/bin/` is the desktop entry point.
|
|
# Counting them does not report "untested logic", it reports "logic that
|
|
# cannot be reached from a test", which drags the number down while
|
|
# telling you nothing actionable. They are excluded by *file*, and the
|
|
# files that hold them are named here rather than pattern-matched, so
|
|
# adding a new one is a deliberate act.
|
|
#
|
|
# **This exclusion has to be audited, not trusted.** `grid.rs` is 2,700
|
|
# lines and only its last ~30 are `script_mod!`; of its 59 functions, 36
|
|
# took no `cx`, no `Event` and no `Scope`. Those were pure arithmetic —
|
|
# hit testing, cell rectangles, frozen panes, autofill handles — hidden
|
|
# behind a file-level exclusion and carrying no tests at all. They now
|
|
# live in `geometry.rs`, which is measured, and `grid.rs` delegates to
|
|
# it. An exclusion that quietly grows to cover real logic is worse than
|
|
# no exclusion, because the number stays green while the coverage goes
|
|
# away. Before widening this list, check what is actually inside.
|
|
#
|
|
# Everything else in the UI crate — the controller modules — is measured.
|
|
# Only foreign code is filtered. The copied crates live *under* the work
|
|
# directory, so matching the work path here would exclude the sources being
|
|
# measured — which is exactly what an earlier version of this script did,
|
|
# reporting a confident 0%.
|
|
IGNORE_COMMON='(/cargo/registry|/cargo/git|/rustc/)'
|
|
# grid.rs, ui.rs and workspace.rs are the three files carrying `script_mod!`.
|
|
# Their *testable* logic belongs in a measured module — see geometry.rs —
|
|
# rather than behind this exclusion.
|
|
IGNORE_UI="$IGNORE_COMMON"'|spreadsheet-ui/src/(grid|ui|workspace|trend_chart|virtual_grid)\.rs|spreadsheet-ui/src/bin/'
|
|
|
|
# Collect every instrumented test binary as `-object` arguments.
|
|
#
|
|
# One binary is not enough, and the shortfall is silent. Cargo builds each
|
|
# integration test into its *own* executable, so measuring only the lib-test
|
|
# binary discards everything `tests/` exercised. That is not a rounding
|
|
# error: `persistence.rs` reported 41.77% with 14 of its 17 functions
|
|
# apparently never called, while `tests/sync_flow.rs` was calling
|
|
# `save_spreadsheet_state` and `load_saved_spreadsheet_state` on every run
|
|
# and passing. The functions were covered; the report was reading the wrong
|
|
# object. Same class of defect as the two already fixed in this script.
|
|
collect_objects() {
|
|
local pattern="$1"
|
|
OBJECTS=()
|
|
local bin first=1
|
|
while IFS= read -r bin; do
|
|
# llvm-cov takes the first binary positionally and the rest through
|
|
# `-object`. Passing every one as `-object` makes it read the first
|
|
# *source* path as the positional binary and fail with "not a valid
|
|
# object file", so the split matters.
|
|
if (( first )); then
|
|
OBJECTS+=("$bin")
|
|
first=0
|
|
else
|
|
OBJECTS+=(-object "$bin")
|
|
fi
|
|
done < <(find "$CARGO_TARGET_DIR/debug/deps" -maxdepth 1 -type f \
|
|
-executable ! -name '*.d' ! -name '*.so' -name "$pattern" | sort)
|
|
}
|
|
|
|
report_for() {
|
|
local label="$1" profdata="$2" ignore="$3" floor="$4"
|
|
shift 4
|
|
local sources=("$@")
|
|
|
|
echo >&2
|
|
echo "── $label ─────────────────────────────────────────────" >&2
|
|
"$LLVM_BIN/llvm-cov" report "${OBJECTS[@]}" \
|
|
-instr-profile="$profdata" \
|
|
-ignore-filename-regex="$ignore" \
|
|
"${sources[@]}"
|
|
|
|
local covered
|
|
covered="$("$LLVM_BIN/llvm-cov" export "${OBJECTS[@]}" \
|
|
-instr-profile="$profdata" \
|
|
-ignore-filename-regex="$ignore" \
|
|
-summary-only "${sources[@]}" \
|
|
| grep -o '"lines":{[^}]*}' | tail -1 \
|
|
| grep -o '"percent":[0-9.]*' | cut -d: -f2)"
|
|
covered="${covered%%.*}"
|
|
if [[ -z "$covered" ]]; then
|
|
echo "error: could not read $label line coverage" >&2
|
|
exit 1
|
|
fi
|
|
if (( covered < floor )); then
|
|
echo "FAIL: $label line coverage ${covered}% is below the ${floor}% floor" >&2
|
|
exit 1
|
|
fi
|
|
echo "$label line coverage ${covered}% meets the ${floor}% floor" >&2
|
|
}
|
|
|
|
uncovered_listing() {
|
|
local label="$1" profdata="$2" ignore="$3"
|
|
shift 3
|
|
"$LLVM_BIN/llvm-cov" show "${OBJECTS[@]}" \
|
|
-instr-profile="$profdata" \
|
|
-ignore-filename-regex="$ignore" \
|
|
"$@" \
|
|
| awk '
|
|
# `llvm-cov show` prints a bare path line before each file. Match
|
|
# any line that is not a numbered source line and ends in `.rs`
|
|
# or `.rs:` — an earlier version required a trailing colon and
|
|
# silently produced an empty listing.
|
|
/^[^ 0-9]/ && /\.rs:?$/ { file = $0; sub(/:$/, "", file); next }
|
|
/^ *[0-9]+\| *0\|/ { print file ": " $0 }
|
|
' > "$WORK/uncovered-$label.txt" || true
|
|
echo "uncovered lines: $WORK/uncovered-$label.txt" >&2
|
|
}
|
|
|
|
# ── Engine ───────────────────────────────────────────────────────────────────
|
|
if [[ "$COVERAGE_TARGET" == "all" || "$COVERAGE_TARGET" == "engine" ]]; then
|
|
ENGINE="$WORK/spreadsheet-engine"
|
|
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-engine" "$ENGINE"
|
|
|
|
# `--all-targets` picks up tests/sync_flow.rs as well as the unit tests.
|
|
cargo test --manifest-path "$ENGINE/Cargo.toml" --all-targets
|
|
|
|
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw \
|
|
-o "$WORK/engine.profdata"
|
|
# Every engine test binary: the lib tests plus each file in tests/.
|
|
collect_objects '*'
|
|
if [[ "${#OBJECTS[@]}" -eq 0 ]]; then
|
|
echo "error: no instrumented engine test binaries were produced" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A directory is accepted as a source filter only with a single
|
|
# `-object`; with several, llvm-cov rejects it. Expand to the files.
|
|
ENGINE_SOURCES=()
|
|
while IFS= read -r f; do
|
|
ENGINE_SOURCES+=("$f")
|
|
done < <(find "$ENGINE/src" -name '*.rs' | sort)
|
|
|
|
report_for "engine" "$WORK/engine.profdata" \
|
|
"$IGNORE_COMMON" "$ENGINE_FLOOR" "${ENGINE_SOURCES[@]}"
|
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
uncovered_listing "engine" "$WORK/engine.profdata" \
|
|
"$IGNORE_COMMON" "${ENGINE_SOURCES[@]}"
|
|
fi
|
|
fi
|
|
|
|
# ── UI controller logic ──────────────────────────────────────────────────────
|
|
if [[ "$COVERAGE_TARGET" == "all" || "$COVERAGE_TARGET" == "ui" ]]; then
|
|
# Makepad's Linux backend needs native libraries to link a test binary.
|
|
bash "$ROOT/tools/makepad-native-libs.sh" --check
|
|
|
|
rm -f "$WORK/profiles"/*.profraw
|
|
mkdir -p "$WORK/spreadsheet"
|
|
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-engine" \
|
|
"$WORK/spreadsheet/spreadsheet-engine"
|
|
cp -a "$ROOT/crates/apps/spreadsheet/spreadsheet-ui" \
|
|
"$WORK/spreadsheet/spreadsheet-ui"
|
|
UI="$WORK/spreadsheet/spreadsheet-ui"
|
|
|
|
# Measure the headless controller/unit suite only. The makepad-test
|
|
# integration binary requires a supported Studio runtime and is kept in
|
|
# the separate spreadsheet-ui test target.
|
|
cargo test --manifest-path "$UI/Cargo.toml" --lib
|
|
|
|
"$LLVM_BIN/llvm-profdata" merge -sparse "$WORK/profiles"/*.profraw \
|
|
-o "$WORK/ui.profdata"
|
|
collect_objects '*'
|
|
if [[ "${#OBJECTS[@]}" -eq 0 ]]; then
|
|
echo "error: no instrumented UI test binaries were produced" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Only the controller modules are measured; see the exclusion note above.
|
|
report_for "ui-controllers" "$WORK/ui.profdata" \
|
|
"$IGNORE_UI" "$UI_FLOOR" \
|
|
"$UI/src/button.rs" "$UI/src/chart.rs" "$UI/src/checkbox.rs" "$UI/src/clipboard.rs" "$UI/src/dropdown.rs" "$UI/src/edit.rs" "$UI/src/event_router.rs" \
|
|
"$UI/src/formula_bar.rs" \
|
|
"$UI/src/geometry.rs" "$UI/src/input.rs" "$UI/src/market.rs" "$UI/src/markdown.rs" "$UI/src/model.rs" "$UI/src/render_cache.rs" \
|
|
"$UI/src/selection.rs" "$UI/src/slider.rs" "$UI/src/sort_state.rs" "$UI/src/sparkline.rs" "$UI/src/text_measure.rs" "$UI/src/zoom.rs"
|
|
if [[ "$KEEP_COVERAGE" == "1" ]]; then
|
|
uncovered_listing "ui" "$WORK/ui.profdata" "$IGNORE_UI" \
|
|
"$UI/src/button.rs" "$UI/src/chart.rs" "$UI/src/checkbox.rs" "$UI/src/clipboard.rs" "$UI/src/dropdown.rs" "$UI/src/edit.rs" "$UI/src/event_router.rs" \
|
|
"$UI/src/formula_bar.rs" \
|
|
"$UI/src/geometry.rs" "$UI/src/input.rs" "$UI/src/market.rs" "$UI/src/markdown.rs" "$UI/src/model.rs" "$UI/src/render_cache.rs" \
|
|
"$UI/src/selection.rs" "$UI/src/slider.rs" "$UI/src/sort_state.rs" "$UI/src/sparkline.rs" "$UI/src/text_measure.rs" "$UI/src/zoom.rs"
|
|
fi
|
|
fi
|
|
|
|
echo >&2
|
|
echo "spreadsheet coverage run complete" >&2
|