#!/usr/bin/env bash # Isolated, self-cleaning Rust test runner for Nigig's pure domain crate. # # This deliberately tests a copied crate outside the workspace: the top-level # workspace currently has sibling path dependencies (Makepad/Robius) that are # not included in a standalone checkout. It installs Rust, Cargo caches, the # crate copy and all build artifacts in one temporary directory, then deletes # that directory on success, failure, Ctrl-C or termination. # # Usage: # ./tools/test-rust-clean.sh # RUST_TOOLCHAIN=1.85.0 ./tools/test-rust-clean.sh # TEST_TARGET=platform ./tools/test-rust-clean.sh # TEST_TARGET=pdf ./tools/test-rust-clean.sh # TEST_TARGET=pdf-ui ./tools/test-rust-clean.sh # needs GUI system libs # TEST_TARGET=valhalla ./tools/test-rust-clean.sh # KEEP_TEST_ENV=1 ./tools/test-rust-clean.sh # debugging only; prints path # # It does not modify source files, Cargo.lock, ~/.cargo, ~/.rustup or the # workspace target directory. set -Eeuo pipefail IFS=$'\n\t' REPO_ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" DOMAIN_DIR="$REPO_ROOT/crates/nigig-pay-domain" STORAGE_DIR="$REPO_ROOT/crates/nigig-pay-storage" PLATFORM_DIR="$REPO_ROOT/crates/nigig-pay-platform" # The platform crate's Android adapter is an optional feature, so its USSD # path is exercised separately from the default (portable) test run. USSD_DIR="$REPO_ROOT/crates/robius-ussd" VALHALLA_DIR="$REPO_ROOT/crates/apps/valhalla" VALHALLA_CRATES=(valhalla-core valhalla-cost valhalla-path valhalla-search valhalla-match valhalla-narrative valhalla-elevation valhalla-service valhalla-makepad valhalla-builder valhalla-diff) TEST_TARGET="${TEST_TARGET:-domain}" SPREADSHEET_DIR="$REPO_ROOT/crates/apps/spreadsheet/spreadsheet-engine" SPREADSHEET_UI_DIR="$REPO_ROOT/crates/apps/spreadsheet/spreadsheet-ui" PDF_DIR="$REPO_ROOT/crates/apps/pdf" # The PDF crates form a layered stack (cos <- document <- graphics) wired by # relative path dependencies, so all three are copied together and each is # tested in dependency order. pdf-makepad is excluded on purpose: it needs # makepad_draw, which is not available in a standalone checkout. PDF_CRATES=(pdf-cos pdf-document pdf-graphics) # pdf-makepad additionally needs Makepad and system GUI libraries, so it is # a separate target rather than part of the default pdf run. PDF_UI_CRATES=(pdf-cos pdf-document pdf-graphics pdf-makepad) # Read the toolchain from rust-toolchain.toml so the runner can never drift # from the repository's declared version. Override only deliberately. declared_toolchain() { sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \ "$REPO_ROOT/rust-toolchain.toml" | head -n 1 } TOOLCHAIN="${RUST_TOOLCHAIN:-$(declared_toolchain)}" if [[ -z "$TOOLCHAIN" ]]; then echo "error: could not read channel from rust-toolchain.toml" >&2 exit 2 fi KEEP_TEST_ENV="${KEEP_TEST_ENV:-0}" CHECK_FORMAT="${CHECK_FORMAT:-1}" if [[ ! -f "$DOMAIN_DIR/Cargo.toml" ]]; then echo "error: expected pure domain crate at $DOMAIN_DIR" >&2 exit 2 fi if [[ "$TEST_TARGET" != "domain" && "$TEST_TARGET" != "storage" \ && "$TEST_TARGET" != "platform" \ && "$TEST_TARGET" != "spreadsheet" && "$TEST_TARGET" != "spreadsheet-ui" \ && "$TEST_TARGET" != "spreadsheet-ui-check" \ && "$TEST_TARGET" != "spreadsheet-ui-unit" \ && "$TEST_TARGET" != "pdf" && "$TEST_TARGET" != "pdf-ui" \ && "$TEST_TARGET" != "valhalla" ]]; then echo "error: TEST_TARGET must be domain, storage, platform, spreadsheet," >&2 echo " spreadsheet-ui," >&2 echo " pdf, pdf-ui, or valhalla" >&2 exit 2 fi if [[ "$TEST_TARGET" == "pdf" ]]; then for crate in "${PDF_CRATES[@]}"; do if [[ ! -f "$PDF_DIR/$crate/Cargo.toml" ]]; then echo "error: expected PDF crate at $PDF_DIR/$crate" >&2 exit 2 fi done fi if [[ "$TEST_TARGET" == "valhalla" ]]; then for crate in "${VALHALLA_CRATES[@]}"; do if [[ ! -f "$VALHALLA_DIR/$crate/Cargo.toml" ]]; then echo "error: expected Valhalla crate at $VALHALLA_DIR/$crate" >&2 exit 2 fi done fi if [[ "$TEST_TARGET" == "spreadsheet" && ! -f "$SPREADSHEET_DIR/Cargo.toml" ]]; then echo "error: expected spreadsheet engine crate at $SPREADSHEET_DIR" >&2 exit 2 fi if [[ ("$TEST_TARGET" == "spreadsheet-ui" || "$TEST_TARGET" == "spreadsheet-ui-check" || "$TEST_TARGET" == "spreadsheet-ui-unit") && ! -f "$SPREADSHEET_UI_DIR/Cargo.toml" ]]; then echo "error: expected spreadsheet UI crate at $SPREADSHEET_UI_DIR" >&2 exit 2 fi if [[ "$TEST_TARGET" == "storage" && ! -f "$STORAGE_DIR/Cargo.toml" ]]; then echo "error: expected storage crate at $STORAGE_DIR" >&2 exit 2 fi if [[ "$TEST_TARGET" == "platform" && ! -f "$PLATFORM_DIR/Cargo.toml" ]]; then echo "error: expected platform crate at $PLATFORM_DIR" >&2 exit 2 fi mkdir -p "$HOME/tmp" WORK_DIR="$(mktemp -d "${TMPDIR:-$HOME/tmp}/nigig-rust-test.XXXXXXXX")" cleanup() { local status=$? if [[ "$KEEP_TEST_ENV" == "1" ]]; then echo "test environment retained for debugging: $WORK_DIR" >&2 else rm -rf -- "$WORK_DIR" echo "cleaned isolated Rust test environment" >&2 fi exit "$status" } trap cleanup EXIT HUP INT TERM export RUSTUP_HOME="$WORK_DIR/rustup" export CARGO_HOME="$WORK_DIR/cargo" export CARGO_TARGET_DIR="$WORK_DIR/target" export PATH="$CARGO_HOME/bin:$PATH" export CARGO_NET_GIT_FETCH_WITH_CLI=true export CARGO_INCREMENTAL=0 export RUSTFLAGS="-C strip=debuginfo" RUSTUP_INIT="$WORK_DIR/rustup-init" echo "creating isolated Rust toolchain ($TOOLCHAIN) in $WORK_DIR" >&2 curl --fail --location --proto '=https' --tlsv1.2 \ https://sh.rustup.rs -o "$RUSTUP_INIT" chmod 700 "$RUSTUP_INIT" "$RUSTUP_INIT" -y --profile minimal --default-toolchain "$TOOLCHAIN" \ --component rustfmt --component clippy --no-modify-path # Avoid workspace resolution: copy only the selected pure crate(s) and test # their manifest. Storage needs the sibling domain crate for its relative path. TEST_CRATE="$WORK_DIR/nigig-pay-domain" cp -a "$DOMAIN_DIR" "$TEST_CRATE" if [[ "$TEST_TARGET" == "storage" ]]; then TEST_CRATE="$WORK_DIR/nigig-pay-storage" cp -a "$STORAGE_DIR" "$TEST_CRATE" elif [[ "$TEST_TARGET" == "platform" ]]; then # robius-ussd is an *optional* dependency, but Cargo still resolves its # path at manifest-read time, so it has to travel with the crate. TEST_CRATE="$WORK_DIR/nigig-pay-platform" cp -a "$PLATFORM_DIR" "$TEST_CRATE" cp -a "$USSD_DIR" "$WORK_DIR/robius-ussd" elif [[ "$TEST_TARGET" == "spreadsheet" ]]; then TEST_CRATE="$WORK_DIR/spreadsheet-engine" cp -a "$SPREADSHEET_DIR" "$TEST_CRATE" elif [[ "$TEST_TARGET" == "spreadsheet-ui" || "$TEST_TARGET" == "spreadsheet-ui-check" || "$TEST_TARGET" == "spreadsheet-ui-unit" ]]; then # Preserve the UI crate's sibling engine path dependency. mkdir -p "$WORK_DIR/spreadsheet" cp -a "$SPREADSHEET_DIR" "$WORK_DIR/spreadsheet/spreadsheet-engine" cp -a "$SPREADSHEET_UI_DIR" "$WORK_DIR/spreadsheet/spreadsheet-ui" TEST_CRATE="$WORK_DIR/spreadsheet/spreadsheet-ui" elif [[ "$TEST_TARGET" == "pdf" ]]; then for crate in "${PDF_CRATES[@]}"; do cp -a "$PDF_DIR/$crate" "$WORK_DIR/$crate" done # The regression corpus sits beside the crates and is referenced by a # relative path, so it has to come along or the corpus tests cannot # find their fixtures. mkdir -p "$WORK_DIR/tests" cp -a "$PDF_DIR/tests/corpus" "$WORK_DIR/tests/corpus" elif [[ "$TEST_TARGET" == "pdf-ui" ]]; then for crate in "${PDF_UI_CRATES[@]}"; do cp -a "$PDF_DIR/$crate" "$WORK_DIR/$crate" done mkdir -p "$WORK_DIR/tests" cp -a "$PDF_DIR/tests/corpus" "$WORK_DIR/tests/corpus" elif [[ "$TEST_TARGET" == "valhalla" ]]; then for crate in "${VALHALLA_CRATES[@]}"; do cp -a "$VALHALLA_DIR/$crate" "$WORK_DIR/$crate" done TEST_CRATE="$WORK_DIR/valhalla-makepad" fi cargo --version rustc --version if [[ "$TEST_TARGET" == "spreadsheet-ui" || "$TEST_TARGET" == "spreadsheet-ui-check" || "$TEST_TARGET" == "spreadsheet-ui-unit" ]]; then # Makepad's Linux backend uses native Wayland/X11 libraries. Keep this # preflight separate from Rust/Cargo caches; the runner only cleans its # isolated temporary environment and never removes host packages. "$REPO_ROOT/tools/makepad-native-libs.sh" --check fi # Run tests if [[ "$TEST_TARGET" == "domain" || "$TEST_TARGET" == "storage" \ || "$TEST_TARGET" == "platform" ]]; then if [[ ! -f "$TEST_CRATE/Cargo.lock" ]]; then echo "error: $TEST_TARGET crate has no checked-in Cargo.lock" >&2 exit 2 fi cargo test --manifest-path "$TEST_CRATE/Cargo.toml" --all-targets --locked elif [[ "$TEST_TARGET" == "pdf" ]]; then for crate in "${PDF_CRATES[@]}"; do echo "--- testing $crate" >&2 cargo test --manifest-path "$WORK_DIR/$crate/Cargo.toml" --all-targets done elif [[ "$TEST_TARGET" == "pdf-ui" ]]; then for crate in "${PDF_UI_CRATES[@]}"; do echo "--- testing $crate" >&2 cargo test --manifest-path "$WORK_DIR/$crate/Cargo.toml" --all-targets done elif [[ "$TEST_TARGET" == "spreadsheet-ui-check" ]]; then cargo check --manifest-path "$TEST_CRATE/Cargo.toml" --lib --bins elif [[ "$TEST_TARGET" == "spreadsheet-ui-unit" ]]; then cargo test --manifest-path "$TEST_CRATE/Cargo.toml" --lib elif [[ "$TEST_TARGET" == "valhalla" ]]; then for crate in "${VALHALLA_CRATES[@]}"; do echo "--- testing $crate" >&2 cargo test --manifest-path "$WORK_DIR/$crate/Cargo.toml" --all-targets done else cargo test --manifest-path "$TEST_CRATE/Cargo.toml" --all-targets fi # Formatting is a source-quality check, not a formatter: never rewrite files. if [[ "$CHECK_FORMAT" == "1" && "$TEST_TARGET" == "pdf-ui" ]]; then for crate in "${PDF_UI_CRATES[@]}"; do cargo fmt --manifest-path "$WORK_DIR/$crate/Cargo.toml" -- --check done elif [[ "$CHECK_FORMAT" == "1" && "$TEST_TARGET" == "pdf" ]]; then for crate in "${PDF_CRATES[@]}"; do cargo fmt --manifest-path "$WORK_DIR/$crate/Cargo.toml" -- --check done elif [[ "$CHECK_FORMAT" == "1" && "$TEST_TARGET" == "valhalla" ]]; then for crate in "${VALHALLA_CRATES[@]}"; do cargo fmt --manifest-path "$WORK_DIR/$crate/Cargo.toml" -- --check done elif [[ "$CHECK_FORMAT" == "1" ]]; then cargo fmt --manifest-path "$TEST_CRATE/Cargo.toml" -- --check else echo "format check skipped (CHECK_FORMAT=$CHECK_FORMAT)" >&2 fi if [[ "$TEST_TARGET" == "domain" || "$TEST_TARGET" == "storage" \ || "$TEST_TARGET" == "platform" ]]; then cargo clippy --manifest-path "$TEST_CRATE/Cargo.toml" --all-targets --locked -- -D warnings fi # The PDF stack parses untrusted input, so a warning there is a real defect # class (silent truncation, unwrap on malformed bytes). Gate it on warnings. if [[ "$TEST_TARGET" == "pdf" || "$TEST_TARGET" == "pdf-ui" ]]; then if [[ "$TEST_TARGET" == "pdf-ui" ]]; then clippy_crates=("${PDF_UI_CRATES[@]}") else clippy_crates=("${PDF_CRATES[@]}") fi for crate in "${clippy_crates[@]}"; do cargo clippy --manifest-path "$WORK_DIR/$crate/Cargo.toml" \ --all-targets -- -D warnings done fi if [[ "$TEST_TARGET" == "platform" ]]; then # The Android adapter compiles for the host even though it only *runs* on # a device, so a mismapped provider error is caught here rather than on a # phone. cargo test --manifest-path "$TEST_CRATE/Cargo.toml" --locked --features ussd cargo clippy --manifest-path "$TEST_CRATE/Cargo.toml" --all-targets --locked \ --features ussd -- -D warnings # Review item 5.4: MockGateway fabricates confirmations. A release build # that enables it must fail to compile. This asserts the guard is real # rather than trusting the comment above it. if cargo build --manifest-path "$TEST_CRATE/Cargo.toml" --locked \ --release --features mock >/dev/null 2>&1; then echo "error: the mock gateway compiled into a release build" >&2 exit 1 fi echo "mock gateway is correctly refused in release builds" >&2 fi if [[ "$TEST_TARGET" == "storage" ]]; then cargo test --manifest-path "$TEST_CRATE/Cargo.toml" --locked --features sqlcipher cargo clippy --manifest-path "$TEST_CRATE/Cargo.toml" --all-targets --locked \ --features sqlcipher -- -D warnings fi if [[ "$TEST_TARGET" == "domain" ]]; then cargo bench --manifest-path "$TEST_CRATE/Cargo.toml" --locked fi echo "isolated $TEST_TARGET tests passed" >&2