diff --git a/ci/explicit-test-commands.d/815-affected-set-rows.cmd b/ci/explicit-test-commands.d/815-affected-set-rows.cmd new file mode 100644 index 0000000000..639899fb2a --- /dev/null +++ b/ci/explicit-test-commands.d/815-affected-set-rows.cmd @@ -0,0 +1,2 @@ +# FLOW-010 E5: affected-set tool rows (leaf+dependents vs cargo tree oracle, core/Cargo.lock/unknown -> ALL). +bash scripts/check_affected_set.sh diff --git a/ci/sections.yml b/ci/sections.yml index 4cc17744ef..edc32bb85a 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1951,6 +1951,11 @@ jobs: run: setsid --wait bash scripts/check_lockfile_current.sh - name: "lockfile guard must still turn RED (case table)" run: setsid --wait bash scripts/check_lockfile_current.sh --self-test + # FLOW-010 E5: runs cargo metadata/tree, so it lives in the cargo job, not guard-tree-steps. + - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" + run: setsid --wait bash scripts/check_affected_set.sh + - name: "Affected-set readers guard (every crate that reads other crates' files is listed; RED on a new one)" + run: setsid --wait bash scripts/check_affected_readers.sh && setsid --wait bash scripts/check_affected_readers.sh --self-test # #2640: the SAME defect class as the two steps above, one level up. The # dogfood release runner existed as two 1170-line copies whose divergence # was, in every hunk, a pin for a release verifier that one copy had and diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh new file mode 100755 index 0000000000..4604dbec14 --- /dev/null +++ b/scripts/affected_set.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# FLOW-010 E5: the diff -> the set of workspace packages whose tests it can change. +# +# scripts/affected_set.sh [--filter] [--diff-from FILE] [BASE [HEAD]] +# +# Prints one package per line, or the single word ALL. With --filter it prints the +# nextest filterset instead (`package(a) | package(b)`, or `all()` for ALL). +# Fail-safe: anything this tool cannot place in a package is ALL. +# crates//... the package + its reverse-dependency closure (cargo metadata) +# a CORE crate ALL (its closure is the workspace; naming it would only hide that) +# Cargo.toml of a crate ALL (a manifest edit changes features/deps, which no path can bound) +# docs/, root *.md, ci/ (sections.yml, explicit-test-commands.d/, ...), Cargo.lock, root Cargo.toml, +# rust-toolchain.toml, .cargo/, .github/, Makefile, any other path outside a crate +# ALL (tests read README.md, CLAUDE.md, ci/sections.yml and docs/ at run time) +# Any non-.rs file under crates// (README.md, *.json, *.toml, ...) is ALL: other crates' tests read them +# (aprender-core readme_contract.rs reads every crates/*/README.md; aprender-contracts reads aprender-serve/kernel-registry.json). +# An empty diff, or a diff git cannot compute (bad base: exit 128), is ALL: nothing was +# measured, so nothing may be skipped. +# git diff runs with --no-renames: a moved file is its old path AND its new path (a rename +# out of a crate would otherwise show only the destination and the source crate would be skipped). +# --diff-from reads the changed paths from FILE (tests). AFFECTED_META / AFFECTED_ROOT +# replace `cargo metadata` and the checkout root (tests; synthetic graphs). +set -euo pipefail +# CORE = ALL on touch. apr-cli is here because other crates' tests read its sources (aprender-core readme_contract.rs reads +# apr-cli/src/commands_enum.rs; spec_checklist_*, toyota_principles_tests). Reader edges are not in cargo metadata, so a +# crate whose source another crate reads at run time must be listed (aprender-quant: aprender-serve/capability.rs reads +# aprender-quant/src/ggml_type.rs). Any crates/*/build.rs is ALL: contracts/build_helper.rs reads crates/*/build.rs. +CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" +# READERS = crates whose tests read OTHER crates' files at run time (CARGO_MANIFEST_DIR/.., crates/, ../). CORE ones are ALL on touch, but that does not put them in a non-ALL set; every non-ALL set includes them, because the touched crate may be what they read. scripts/check_affected_readers.sh +# goes RED on a reader this list (or CORE) lacks. +READERS="${AFFECTED_READERS:-apr-cli apr-format aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop}" +filter=0 from="" +while [ $# -gt 0 ]; do + case "$1" in + --filter) filter=1; shift ;; + --diff-from) from="$2"; shift 2 ;; + *) break ;; + esac +done +case "$from" in *..*) echo "affected_set: refusing a --diff-from with \"..\": $from" >&2; exit 2 ;; esac # bashrs SEC010 +if [ -n "$from" ]; then files="$(cat "$from")" +else files="$(git diff --name-only --no-renames "${1:-origin/main}...${2:-HEAD}")" || files=""; fi +emit() { # $@ = package names, or ALL + if [ "${1:-}" = ALL ]; then [ "$filter" = 1 ] && echo 'all()' || echo ALL; return; fi + if [ "$filter" = 1 ]; then printf '%s\n' "$@" | sed 's/.*/package(&)/' | paste -sd'|' | sed 's/|/ | /g'; else printf '%s\n' "$@"; fi +} +[ -n "$files" ] || { emit ALL; exit 0; } +if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" +else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)"; fi +root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" +dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" || { emit ALL; exit 0; } +[ -n "$dirs" ] || { emit ALL; exit 0; } +touched="" +while IFS= read -r f; do + case "$f" in + '') continue ;; + crates/*/build.rs) emit ALL; exit 0 ;; + crates/*.rs) ;; + crates/*) emit ALL; exit 0 ;; + esac + # longest manifest dir that prefixes the path + pkg="$(awk -F'\t' -v f="$f" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" + [ -n "$pkg" ] || { emit ALL; exit 0; } + case " $CORE " in *" $pkg "*) emit ALL; exit 0 ;; esac + touched="$touched"$'\n'"$pkg" +done <<<"$files" +touched="$(printf '%s\n' "$touched" | grep -v '^$' | sort -u || true)" +[ -n "$touched" ] || { emit ALL; exit 0; } # nothing placed in a package: nothing may be skipped +# The closure, then aprender-core: its falsification_spec_v10 test scans every crate's src, so ANY non-ALL set includes it (and the READERS). +res="$(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" --arg rd "$READERS" ' + (.packages | map({name, deps: [.dependencies[].name]})) as $p + | def grow($s): ($s + [$p|.[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); + (.packages | map(.name)) as $m + | (($t | until(grow(.) == .; grow(.))) + ($rd | split(" ")) | unique) | .[] | select(. as $n | $m | index($n))' <<<"$meta")" || { emit ALL; exit 0; } +[ -n "$res" ] || { emit ALL; exit 0; } +mapfile -t out <<<"$res" +emit "${out[@]}" diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh new file mode 100755 index 0000000000..2da2e462e4 --- /dev/null +++ b/scripts/check_affected_readers.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# FLOW-010 E5: a crate whose Rust can reach OTHER crates' files at run time is a reader edge that cargo metadata +# cannot see. scripts/affected_set.sh must list it in READERS (the only list that puts a crate in a non-ALL set), +# or a change to the crate it reads would skip its tests. RED on a crate with any path-escape form and no entry. +# Over-approximates on purpose: a false positive only adds a crate to READERS (more tests run); a miss skips tests. +# Flagged in any .rs: a string holding `..`, parent(), ancestors(), current_dir, a "crates/" string or "crates", +# CARGO_WORKSPACE_DIR, workspace_path_or_skip, read_dir(...) naming MANIFEST/crates/root/workspace, format!(...) +# naming MANIFEST_DIR or crates/. Comment-only lines and files under a target/ path component are skipped. +# Usage: check_affected_readers.sh [crates-dir] | --self-test | --list +set -euo pipefail +SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" +# the exact file:line exemptions (a crate-wide exemption hides a later reader in the same crate) +ALLOW="${AFFECTED_READERS_ALLOW:-crates/aprender-bench-tokenizer/src/lib.rs:50}" # joins ../.. only for /tokenizer.json and /models/ +# crates/ dirs outside the workspace (excluded): a change there is unplaceable -> ALL, so they need no READERS entry +NONMEMBER="${AFFECTED_NONMEMBER:-facades}" +PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' +listed() { sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } +hits() { # $1 = crates dir; prints crate names + { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true + # file-level: CARGO_MANIFEST_DIR and .pop( anywhere in one file (rustfmt may wrap the let or split pop/push across lines) + for f in $(grep -rlE --include='*.rs' 'CARGO_MANIFEST_DIR' "$1" 2>/dev/null || true); do + grep -qE '\.pop[[:space:]]*\(' "$f" && echo "$f:0:pop-with-MANIFEST_DIR" + done; } \ + | grep -vE '^[^:]*/target/' | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' \ + | awk -F: -v allow=" $ALLOW " -v nm=" $NONMEMBER " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); if (index(nm, " " a[2] " ")) next; print a[2] }' | sort -u +} +nonmember_in_workspace() { # a NONMEMBER dir that cargo metadata lists is a member: its exemption would hide a reader + local m d + m="$({ if [ -n "${AFFECTED_META_JSON:-}" ]; then cat "$AFFECTED_META_JSON"; else cargo metadata --no-deps --format-version 1 --offline 2>/dev/null; fi; } || true)" + [ -n "$m" ] || return 0 + for d in $NONMEMBER; do + if printf '%s' "$m" | grep -qF "/crates/$d/"; then echo "RED: NONMEMBER dir crates/$d is a workspace member (cargo metadata lists it): drop it from NONMEMBER"; return 1; fi + done +} +check() { + nonmember_in_workspace || return 1 + local bad="" l c; l="$(listed)" + for c in $(hits "$1"); do printf '%s\n' "$l" | grep -qx "$c" || bad="$bad $c"; done + if [ -n "$bad" ]; then echo "RED: crates can reach other crates' files but are not in READERS (scripts/affected_set.sh):$bad"; return 1; fi + echo "ok: every crate with a path-escape form is in READERS" +} +if [ "${1:-}" = "--list" ]; then hits crates; exit 0; fi +if [ "${1:-}" = "--self-test" ]; then + t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT + SET="$(pwd)/$SET"; cd "$t" + fail=0 + fresh() { rm -rf -- "${t:?}/crates"; mkdir -p "$t/crates/$1/src"; } # one unlisted crate holding $2 + expect_red() { # name crate body + fresh "$2"; printf '%s\n' "fn f() { $3 }" >"$t/crates/$2/src/x.rs" + if (check crates >/dev/null 2>&1); then echo "FAIL self-test: not caught: $1"; fail=1; fi + } + expect_red 'join("crates")' zz-a 'let _ = root.join("crates");' + expect_red 'MANIFEST_DIR.parent().parent()' zz-b 'let _ = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent();' + expect_red 'root.join("crates/aprender-core")' zz-c 'let _ = root.join("crates/aprender-core");' + expect_red 'format! with ../../crates' zz-d 'let _ = format!("{}/../../crates/x", env!("CARGO_MANIFEST_DIR"));' + expect_red 'ancestors().nth(2)' zz-e 'let _ = here.ancestors().nth(2);' + expect_red 'read_dir("../")' zz-f 'let _ = std::fs::read_dir("../");' + expect_red 'current_dir().join("..")' zz-g 'let _ = std::env::current_dir().unwrap().join("..");' + expect_red 'relative read_to_string' zz-h 'let _ = std::fs::read_to_string("../../crates/x/src/lib.rs");' + expect_red 'MANIFEST_DIR.parent().join(apr-cli)' zz-i 'let _ = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("apr-cli");' + expect_red 'workspace_path_or_skip!' zz-j 'let _ = workspace_path_or_skip!("crates/x/..");' + expect_red 'read_dir(workspace_root)' zz-k 'let _ = std::fs::read_dir(workspace_root);' + expect_red 'CARGO_WORKSPACE_DIR' zz-l 'let _ = env!("CARGO_WORKSPACE_DIR");' + expect_red 'format! with MANIFEST_DIR' zz-m 'let _ = format!("{}/x", env!("CARGO_MANIFEST_DIR"));' + expect_red 'a *contracts*/*target* crate name' zz-contracts-target-new 'let _ = root.join("crates");' + expect_red 'a line that mentions target/contracts' zz-n 'let _ = root.join("crates"); /* target contracts docs/ */' + expect_red 'PathBuf pop then push' zz-p 'let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.pop(); p.push("aprender-core");' + expect_red 'Component::ParentDir' zz-q 'let _ = Component::ParentDir;' + expect_red 'git rev-parse --show-toplevel' zz-r 'let _ = Command::new("git").args(["rev-parse", "--show-toplevel"]);' + expect_red 'Path::parent(x)' zz-s 'let _ = Path::parent(&here);' + expect_red '.parent ()' zz-t 'let _ = here.parent ();' + expect_red 'a non-member dir is only exempt when named' facades-x 'let _ = here.ancestors();' + expect_red 'rustfmt-wrapped let + split pop/push' zz-u $'let mut m =\n PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n m.pop();\n m.push("aprender-core");' + expect_red 'spaced .pop (' zz-w $'let mut m = PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n m.pop ();' + printf '{"packages":[{"manifest_path":"/x/crates/facades/zz-evil/Cargo.toml"}]}' >"$t/meta.json"; fresh zz-v + if (AFFECTED_META_JSON="$t/meta.json" check crates >/dev/null 2>&1); then echo "FAIL self-test: a NONMEMBER dir listed by cargo metadata passed"; fail=1; fi + # the bench-tokenizer exemption is the one line, not the crate + fresh aprender-bench-tokenizer; printf 'fn f() {}\nfn g() { let _ = root.join("crates"); }\n' >"$t/crates/aprender-bench-tokenizer/src/lib.rs" + if (AFFECTED_READERS_ALLOW="crates/aprender-bench-tokenizer/src/lib.rs:1" check crates >/dev/null 2>&1); then echo "FAIL self-test: an allow entry exempted a different line"; fail=1; fi + # a listed reader, a comment-only mention and a target/ file are green + fresh aprender-serve; echo 'fn f() { let _ = root.join("crates"); }' >"$t/crates/aprender-serve/src/y.rs" + (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a listed reader was flagged"; fail=1; } + fresh zz-o; echo '// root.join("crates")' >"$t/crates/zz-o/src/y.rs" + (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a comment-only line was flagged"; fail=1; } + [ "$fail" -eq 0 ] && echo "ok: self-test" + exit "$fail" +fi +check "${1:-crates}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh new file mode 100755 index 0000000000..acb4d9c799 --- /dev/null +++ b/scripts/check_affected_set.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# FLOW-010 E5 RED rows for scripts/affected_set.sh. Oracle for the closure is +# `cargo tree -i` (a different code path from the tool's cargo-metadata jq). +set -euo pipefail +# the CI container runs as root over a host-owned checkout: git would refuse it (dubious ownership) +export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' +A=scripts/affected_set.sh +t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT +fail=0 +row() { # name want got + if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1"; echo " want: $2" | head -c 300; echo; echo " got: $3" | head -c 300; echo; fail=1; fi +} +run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } +core_list=" aprender-core aprender-compute aprender-contracts apr-cli aprender-quant " +readers="$(sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$A")" +meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) +members=$(jq -r '.packages[].name' <<<"$meta" | sort) +# a READERS entry that is not a workspace package is dropped by the tool; drop it from the oracle too +readers="$(printf '%s\n' $readers | grep -Fxf <(printf '%s\n' "$members") || true)" +# a non-core crate that has dependents, and a leaf with none +withdep="" leaf="" +for p in $members; do + case "$core_list" in *" $p "*) continue ;; esac + n=$(cargo tree --offline -i "$p" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//' | sort -u | grep -vc "^$p$" || true) + dir=$(jq -r --arg p "$p" '.packages[]|select(.name==$p)|.manifest_path' <<<"$meta" | sed "s|$(git rev-parse --show-toplevel)/||; s|/Cargo.toml||") + case "$dir" in crates/*) ;; *) continue ;; esac + [ -z "$withdep" ] && [ "$n" -gt 0 ] && [ "$n" -lt 10 ] && withdep="$p:$dir" + [ -z "$leaf" ] && [ "$n" -eq 0 ] && leaf="$p:$dir" + [ -n "$withdep" ] && [ -n "$leaf" ] && break +done +[ -n "$withdep" ] && [ -n "$leaf" ] || { echo "FAIL could not find a leaf and a crate with dependents"; exit 1; } +wp=${withdep%%:*}; wd=${withdep#*:}; lp=${leaf%%:*}; ld=${leaf#*:} +# every non-ALL set also carries aprender-core (its falsification_spec_v10 test scans every crate's src) +want=$( { cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//'; printf '%s\n' $readers; } | sort -u) +lc="$(printf '%s\n' "$lp" $readers | sort -u)" +row "crate with dependents ($wp) -> itself + dependents + aprender-core" "$want" "$(run "$wd/src/lib.rs" | sort -u)" +row "leaf crate ($lp) -> itself + aprender-core" "$lc" "$(run "$ld/src/lib.rs" | sort -u)" +row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" +row "aprender-quant source -> ALL (aprender-serve capability.rs reads aprender-quant/src/ggml_type.rs)" ALL "$(run crates/aprender-quant/src/ggml_type.rs)" +row "a crate's build.rs -> ALL (contracts build_helper.rs reads crates/*/build.rs)" ALL "$(run "$ld/build.rs")" +row "apr-cli source -> ALL (aprender-core tests read apr-cli/src/commands_enum.rs)" ALL "$(run crates/apr-cli/src/commands_enum.rs)" +row "--diff-from with .. is refused (SEC010)" "2" "$($A --diff-from ../x >/dev/null 2>&1; echo $?)" +row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" +row "unknown path -> ALL" ALL "$(run some/unknown/path.txt)" +row "a path whose dir only prefixes a crate dir (crates/aprender-train-old/) -> ALL (awk prefix needs the trailing /)" ALL "$(run crates/aprender-train-old/src/lib.rs)" +row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/whatever.sh)" +row "docs file -> ALL (tests read docs/ at run time)" ALL "$(run docs/x.md)" +row "root README.md -> ALL" ALL "$(run README.md)" +row "root CLAUDE.md -> ALL" ALL "$(run CLAUDE.md)" +row "ci/sections.yml -> ALL" ALL "$(run ci/sections.yml)" +row "ci/explicit-test-commands.d fragment -> ALL" ALL "$(run ci/explicit-test-commands.d/815-affected-set-rows.cmd)" +row "a crate's own Cargo.toml -> ALL" ALL "$(run "$ld/Cargo.toml")" +row "a .md inside a leaf crate -> ALL (aprender-core reads crates/*/README.md)" ALL "$(run "$ld/README.md")" +row "a .json inside a leaf crate -> ALL (kernel-registry.json is read by aprender-contracts)" ALL "$(run "$ld/kernel-registry.json")" +row "a .rs inside a leaf crate stays scoped (itself + aprender-core)" "$lc" "$(run "$ld/src/other.rs" | sort -u)" +row "--filter names the package and aprender-core" "$(printf 'package(%s)\n' $lc | LC_ALL=C sort | paste -sd'|' | sed 's/|/ | /g')" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" +row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" +# empty diff and a bad base are ALL (nothing measured, nothing skipped) +: >"$t/empty" +row "empty diff -> ALL" ALL "$($A --diff-from "$t/empty")" +row "bad base (git exit 128) -> ALL" ALL "$($A no-such-base-ref-5802 HEAD 2>/dev/null)" +# dev-dep counting: b depends on a ONLY as a dev-dependency; touching a must reach b. +cat >"$t/meta.json" <<'JSON' +{"packages":[ + {"name":"a","manifest_path":"/r/crates/a/Cargo.toml","dependencies":[]}, + {"name":"b","manifest_path":"/r/crates/b/Cargo.toml","dependencies":[{"name":"a","kind":"dev"}]}, + {"name":"c","manifest_path":"/r/crates/c/Cargo.toml","dependencies":[]}]} +JSON +row "dev-only edge is counted: touching a reaches b" "a +b" "$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r run crates/a/src/lib.rs | sort -u)" +row "an unrelated crate is not reached" "c" "$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r run crates/c/src/lib.rs)" +# mutant: a tool that drops dev edges misses b, and the dev-dep row must see it +sed 's/\[\.dependencies\[\]\.name\]/[.dependencies[]|select(.kind!="dev")|.name]/' "$A" >"$t/mut.sh" +if cmp -s "$A" "$t/mut.sh"; then echo "FAIL mutant did not apply"; fail=1 +else + got=$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r bash "$t/mut.sh" --diff-from <(echo crates/a/src/lib.rs) | sort -u) + row "MUTANT: ignoring dev edges is caught by the dev-dep row" "a" "$got" +fi +# --no-renames: moving a file out of crate a into crate c touches BOTH (git's rename detection would show only c). +rt="$t/repo"; mkdir -p "$rt/crates/a/src" "$rt/crates/b" "$rt/crates/c/src"; abs="$(cd "$(dirname "$A")" && pwd)/$(basename "$A")" +sed "s|/r/|$rt/|" "$t/meta.json" >"$t/meta-rt.json" +printf 'pub fn moved() -> u32 { 1 }\n// padding so git sees a rename, not a delete+add\n// padding\n' >"$rt/crates/a/src/x.rs" +# plumbing (write-tree/commit-tree) so the scratch repo runs no commit hooks +(cd "$rt" && git init -q && git add -A && b=$(GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git commit-tree "$(git write-tree)" -m base) && git mv crates/a/src/x.rs crates/c/src/x.rs && git update-ref HEAD "$b" && git add -A && GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git update-ref HEAD "$(GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git commit-tree "$(git write-tree)" -p "$b" -m move)") +mv_run() { (cd "$rt" && AFFECTED_META="$t/meta-rt.json" AFFECTED_ROOT="$rt" bash "$1" HEAD~1 HEAD | sort -u); } +row "rename a->c touches both ends (a, its dependent b, and c)" "a +b +c" "$(mv_run "$abs")" +sed 's/--name-only --no-renames/--name-only/' "$A" >"$t/mut-rn.sh" +if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 +else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi +sed '/^ crates\/\*\/build.rs)/d' "$A" >"$t/mut-build.sh" +if cmp -s "$A" "$t/mut-build.sh"; then echo "FAIL build.rs mutant did not apply"; fail=1 +else printf '%s\n' "$ld/build.rs" >"$t/db"; row "MUTANT: dropping the build.rs arm is caught by the build.rs row" "$lc" "$(bash "$t/mut-build.sh" --diff-from "$t/db" | sort -u || true)"; fi +# mutants: the skip list that used to exist (docs/*, *.md) must be caught by the ALL rows above. +sed 's#^ crates/\*.rs) ;;# docs/* | *.md) continue ;;\n crates/*.rs) ;;#' "$A" >"$t/mut-docs.sh" +if cmp -s "$A" "$t/mut-docs.sh"; then echo "FAIL docs-skip mutant did not apply"; fail=1 +else printf '%s\n%s\n' docs/x.md "$ld/src/lib.rs" >"$t/dd"; row "MUTANT: skipping docs/*.md is caught by the docs-ALL row" "$lc" "$(bash "$t/mut-docs.sh" --diff-from "$t/dd" | sort -u || true)"; fi +sed 's#^ crates/\*) emit ALL.*# crates/*) continue ;;#' "$A" >"$t/mut-toml.sh" +if cmp -s "$A" "$t/mut-toml.sh"; then echo "FAIL manifest mutant did not apply"; fail=1 +else printf '%s\n%s\n' "$ld/Cargo.toml" "$ld/src/lib.rs" >"$t/dt"; row "MUTANT: skipping non-.rs crate files is caught by the manifest row" "$lc" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | sort -u || true)"; fi +# CORE crates: each must give ALL, and dropping each from CORE must be seen (its dependents alone are not the workspace) +for c in aprender-compute aprender-contracts aprender-quant; do + cd_=$(jq -r --arg p "$c" '.packages[]|select(.name==$p)|.manifest_path' <<<"$meta" | sed "s|$(git rev-parse --show-toplevel)/||; s|/Cargo.toml||") + row "CORE crate $c -> ALL" ALL "$(run "$cd_/src/lib.rs")" + sed "s/ $c//" "$A" >"$t/mut-core.sh" + if cmp -s "$A" "$t/mut-core.sh"; then echo "FAIL core mutant ($c) did not apply"; fail=1 + else printf '%s\n' "$cd_/src/lib.rs" >"$t/dcore"; row "MUTANT: dropping $c from CORE is caught" "not-ALL" "$([ "$(bash "$t/mut-core.sh" --diff-from "$t/dcore" | head -n1)" != ALL ] && echo not-ALL || echo ALL)"; fi +done +# a 2-hop chain (c -> b -> a) and the multi-package --filter format, on a synthetic graph +cat >"$t/chain.json" <<'JSON' +{"packages":[ + {"name":"a","manifest_path":"/r/crates/a/Cargo.toml","dependencies":[]}, + {"name":"b","manifest_path":"/r/crates/b/Cargo.toml","dependencies":[{"name":"a","kind":null}]}, + {"name":"c","manifest_path":"/r/crates/c/Cargo.toml","dependencies":[{"name":"b","kind":null}]}, + {"name":"aprender-core","manifest_path":"/r/crates/core/Cargo.toml","dependencies":[]}]} +JSON +chain() { AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$1" --diff-from <(echo crates/a/src/lib.rs) | sort -u; } +row "2-hop chain a<-b<-c: touching a reaches b and c (+ aprender-core)" "a +aprender-core +b +c" "$(chain "$A")" +sed 's/until(grow(.) == .; grow(.))/grow(.)/' "$A" >"$t/mut-hop.sh" +if cmp -s "$A" "$t/mut-hop.sh"; then echo "FAIL 1-hop mutant did not apply"; fail=1 +else row "MUTANT: a 1-hop closure is caught by the 2-hop row" "a +aprender-core +b" "$(chain "$t/mut-hop.sh")"; fi +want_f="package(a) | package(aprender-core) | package(b) | package(c)" +row "multi-package --filter is ' | ' joined" "$want_f" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$A" --filter --diff-from <(echo crates/a/src/lib.rs))" +sed "s/ | sed 's\/|\/ | \/g'//" "$A" >"$t/mut-fmt.sh" +if cmp -s "$A" "$t/mut-fmt.sh"; then echo "FAIL filter-format mutant did not apply"; fail=1 +else row "MUTANT: an unspaced multi-package filter is caught" "package(a)|package(aprender-core)|package(b)|package(c)" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$t/mut-fmt.sh" --filter --diff-from <(echo crates/a/src/lib.rs))"; fi +# an unreadable cargo metadata is ALL, not an empty answer +echo 'not json' >"$t/bad.json" +row "unreadable metadata -> ALL" ALL "$(AFFECTED_META="$t/bad.json" bash "$A" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null)" +sed 's/ || { emit ALL; exit 0; }$//' "$A" >"$t/mut-jq.sh" +if cmp -s "$A" "$t/mut-jq.sh"; then echo "FAIL jq-failure mutant did not apply"; fail=1 +else row "MUTANT: dropping the jq-failure fallback is caught" "" "$(AFFECTED_META="$t/bad.json" bash "$t/mut-jq.sh" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null || true)"; fi +# READERS: each reader is in every non-ALL set (real tree), and dropping the READERS list is caught +for r in $readers; do + case " $(run "$ld/src/lib.rs" | tr '\n' ' ')" in *" $r "*) echo "ok reader $r is in a non-ALL set" ;; *) echo "FAIL reader $r missing from a non-ALL set"; fail=1 ;; esac +done +sed 's#^READERS=.*#READERS="aprender-core"#' "$A" >"$t/mut-readers.sh" +if cmp -s "$A" "$t/mut-readers.sh"; then echo "FAIL readers mutant did not apply"; fail=1 +else row "MUTANT: an emptied READERS list is caught" "$(printf '%s\n' aprender-core "$lp" | sort -u)" "$(printf '%s\n' "$ld/src/lib.rs" >"$t/drd"; bash "$t/mut-readers.sh" --diff-from "$t/drd" | sort -u)"; fi +# the res= jq fallback ALONE: metadata with no `dependencies` key makes that jq fail -> ALL +echo '{"packages":[{"name":"a","manifest_path":"/r/crates/a/Cargo.toml"}]}' >"$t/nodeps.json" +row "no dependencies key -> ALL (res= fallback)" ALL "$(AFFECTED_META="$t/nodeps.json" AFFECTED_ROOT=/r bash "$A" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null)" +sed '/<<<"$meta")" || { emit ALL; exit 0; }$/s/ || { emit ALL; exit 0; }$//' "$A" >"$t/mut-res.sh" +if cmp -s "$A" "$t/mut-res.sh"; then echo "FAIL res-fallback mutant did not apply"; fail=1 +else row "MUTANT: dropping only the res= fallback is caught" "" "$(AFFECTED_META="$t/nodeps.json" AFFECTED_ROOT=/r bash "$t/mut-res.sh" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null || true)"; fi +exit "$fail"