#!/usr/bin/env bash # Regression test for cache-root.sh and the lineage rules in cache-lib.sh — # the fix for daniel/gitdan#60, where two jobs building the same ref for # different targets resolved to one CARGO_TARGET_DIR and serialised on Cargo's # exclusive build-directory lock. # # 1. NO LINEAGE CHANGES NOTHING — the effective root is the cache root byte # for byte, so every consumer that does not set a lineage keeps the exact # directories it already has on the volume. This is the whole of the # migration story for lublub, zemyna and emowheel's `ci` job, so it is # asserted rather than assumed. # 2. TWO LINEAGES, ONE REF, TWO TARGET DIRS — the bug itself. The two jobs # keep one cache key (they are the same ref) and still get directories # that are neither equal nor nested one inside the other, which is what # Cargo's per-directory lock needs in order not to serialise them. # 3. THE WHOLE PIPELINE MOVES TOGETHER — seed, publish and prune all operate # inside the lineage root. A pass run in one lineage must not evict, or # even see, a sibling lineage's caches or the flat layout's. # 4. BASE SEEDING IS PER LINEAGE — a PR branch layers over ITS OWN lineage's # base snapshot, not over whatever the flat root happens to hold. This is # the property that keeps a warm start for both jobs rather than one. # 5. A NAME NO READER CAN HANDLE IS REFUSED AT RESOLVE TIME — one assertion # per constraint, each named for the reader that imposes it. None of these # is a filesystem limit: every one of them produces a working directory # that some pass silently stops seeing, which is the failure mode this # whole scheme exists to avoid rather than to relocate. # 6. A PUBLISH THAT DISAGREES WITH ITS CONSUME STEP FAILS LOUDLY — the # footgun the lineage input introduces. cargo-cache-publish derives both # ends of the snapshot swap from its own `cache-root`, so a publish step # left at the default while its consume step nested would republish the # OTHER lineage's live target dir over that lineage's snapshot, silently. set -euo pipefail script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) . "$script_dir/cache-lib.sh" scratch=$(mktemp -d) trap 'rm -rf "$scratch"' EXIT root="$scratch/cache"; mkdir -p "$root" pass_count=0 fail() { echo "ASSERTION FAILED: $*" >&2; exit 1; } ok() { pass_count=$((pass_count + 1)); echo "PASS: $*"; } resolve() { bash "$script_dir/cache-root.sh" resolve "$@"; } # A tree that looks enough like a Cargo target dir for the pipeline scripts, # with a marker naming which lineage produced it — scenario 4 turns on reading # that marker back out of a seeded directory. make_tree() { local d="$1" marker="$2" mkdir -p "$d/debug/deps" "$d/debug/.fingerprint/x" echo "$marker" > "$d/debug/deps/libx.rlib" echo "$marker" > "$d/lineage-marker" echo "$marker" > "$d/debug/.fingerprint/x/dep-lib-x" } echo "=== 1. no lineage changes nothing ===" [ "$(resolve /cache)" = /cache ] || fail "an omitted lineage changed the root" [ "$(resolve /cache '')" = /cache ] || fail "an empty lineage changed the root" ok "no lineage resolves to the cache root unchanged" KEY=$(cache_key dev) [ "$(target_dir_for "$(resolve /cache '')" "$KEY")" = "/cache/target-$KEY" ] \ || fail "the flat target-dir name moved" [ "$(snapshot_dir_for "$(resolve /cache '')" "$KEY")" = "/cache/snapshot-$KEY" ] \ || fail "the flat snapshot name moved" ok "the flat layout's directory names are untouched" echo echo "=== 2. two lineages, one ref, two target dirs ===" HOST_ROOT=$(resolve /cache) WASM_ROOT=$(resolve /cache wasm32) [ "$WASM_ROOT" = /cache/wasm32 ] || fail "lineage root resolved to '$WASM_ROOT'" HOST_DIR=$(target_dir_for "$HOST_ROOT" "$KEY") WASM_DIR=$(target_dir_for "$WASM_ROOT" "$KEY") [ "$HOST_DIR" != "$WASM_DIR" ] || fail "both lineages resolved to $HOST_DIR" case "$WASM_DIR" in "$HOST_DIR"/*) fail "the wasm target dir sits inside the host one" ;; esac case "$HOST_DIR" in "$WASM_DIR"/*) fail "the host target dir sits inside the wasm one" ;; esac ok "one cache key ($KEY), two disjoint target dirs: $HOST_DIR and $WASM_DIR" echo echo "=== 3. the whole pipeline moves together ===" DEAD=$(cache_key feat/dead) lin="$root/wasm32" mkdir -p "$lin" make_tree "$root/target-$DEAD" flat make_tree "$root/native/target-$DEAD" native make_tree "$lin/target-$DEAD" wasm32 bash "$script_dir/seed-target-dir.sh" "$KEY" "" "$lin" tag-3 > "$scratch/seed3.log" 2>&1 \ || { cat "$scratch/seed3.log"; fail "seed inside a lineage root failed"; } [ -d "$lin/target-$KEY" ] || fail "seed did not create $lin/target-$KEY" [ -d "$root/target-$KEY" ] && fail "seed created a directory at the flat root as well" ok "seed creates its directory under the lineage root and nowhere else" make_tree "$lin/target-$KEY" wasm32 bash "$script_dir/publish-snapshot.sh" "$KEY" "$lin" tag-3 > "$scratch/pub3.log" 2>&1 \ || { cat "$scratch/pub3.log"; fail "publish inside a lineage root failed"; } [ -d "$lin/snapshot-$KEY" ] || fail "publish did not create $lin/snapshot-$KEY" [ -d "$root/snapshot-$KEY" ] && fail "publish created a snapshot at the flat root as well" ok "publish writes its snapshot under the lineage root and nowhere else" # Free space far below the threshold, so pass 2 evicts every eligible # directory it can see. What it can see is the point of the scenario. CACHE_LIVENESS=false CACHE_DF_OVERRIDE="1000000 1000" \ bash "$script_dir/prune-cache.sh" "$lin" "$lin/target-$KEY" 'dev main' 10 \ > "$scratch/prune3.log" 2>&1 || { cat "$scratch/prune3.log"; fail "prune inside a lineage root failed"; } [ -d "$lin/target-$DEAD" ] && { cat "$scratch/prune3.log"; fail "prune left its own lineage's evictable cache in place"; } [ -d "$root/target-$DEAD" ] || fail "prune reached out of its lineage and evicted the flat root's cache" [ -d "$root/native/target-$DEAD" ] || fail "prune reached into a sibling lineage and evicted its cache" ok "prune under disk pressure evicts inside its own lineage only" echo echo "=== 4. base seeding is per lineage ===" BASE=$(cache_key dev) PR=$(cache_key feat/pr) rm -rf "$lin" "$root/snapshot-$BASE" mkdir -p "$lin" make_tree "$root/snapshot-$BASE" flat-base make_tree "$lin/snapshot-$BASE" wasm32-base bash "$script_dir/seed-target-dir.sh" "$PR" "$BASE" "$lin" tag-4 > "$scratch/seed4.log" 2>&1 \ || { cat "$scratch/seed4.log"; fail "seeding a PR branch inside a lineage failed"; } [ -d "$lin/target-$PR" ] || fail "the PR branch's lineage target dir was not created" got=$(cat "$lin/target-$PR/lineage-marker") [ "$got" = wasm32-base ] || fail "the PR branch layered over '$got', not its own lineage's base snapshot" grep -q 'base snapshot' "$scratch/seed4.log" || { cat "$scratch/seed4.log"; fail "seed did not report a base-snapshot clone"; } ok "a PR branch layers over its own lineage's base snapshot ($got)" echo echo "=== 5. a name no reader can handle is refused ===" reject() { local lineage="$1" want="$2" desc="$3" out if out=$(resolve /cache "$lineage" 2>&1); then fail "lineage '$lineage' was accepted (resolved to '$out') — $desc" fi case "$out" in *"$want"*) ;; *) fail "lineage '$lineage' was rejected without naming '$want': $out" ;; esac ok "rejected '$lineage' — $desc" } reject 'a/b' 'single path component' "the arbiter walks a volume to depth 2" reject '.hidden' 'dot' "every dot-prefixed name under a cache root is leftover-contract territory" reject 'target-x' 'prune-cache.sh' "prune-cache.sh globs target-* at the cache root" reject 'snapshot-x' 'prune-cache.sh' "prune-cache.sh globs snapshot-* at the cache root" reject 'wasm 32' 'A-Za-z0-9._-' "a cache key is sanitised to that charset and a lineage sits beside one" reject 'release' 'CI_CACHE_NODESCEND_NAMES' "the arbiter never descends into a Cargo profile name" reject 'doc' 'CI_CACHE_NODESCEND_NAMES' "same, for the docs profile directory" reject 'lineage-deadbeef' 'per-branch cache directory' "the arbiter reads a hex-suffixed name as one cache dir" for good in wasm32 web android host wasm32.release lineage_2; do out=$(resolve /cache "$good") || fail "lineage '$good' was rejected: $out" [ "$out" = "/cache/$good" ] || fail "lineage '$good' resolved to '$out'" done ok "ordinary lineage names still resolve" echo echo "=== 6. a publish that disagrees with its consume step fails loudly ===" verify() { bash "$script_dir/cache-root.sh" verify "$@"; } verify /cache wasm32 /cache/wasm32 > /dev/null 2>&1 \ || fail "verify rejected a publish step that agrees with its consume step" verify /cache '' /cache > /dev/null 2>&1 \ || fail "verify rejected an unmigrated consumer's matching default pair" ok "verify accepts a publish step whose inputs match what the consume step exported" # The exact shape of the mistake: the consume step nested, the publish step # kept the default. Left unchecked this republishes the host lineage's live # target dir over the host lineage's snapshot, from the wasm job. if out=$(verify /cache '' /cache/wasm32 2>&1); then fail "verify accepted a publish step that resolved to /cache while the job exported /cache/wasm32" fi case "$out" in *'/cache/wasm32'*) ;; *) fail "the mismatch error does not quote what the consume step exported: $out" ;; esac case "$out" in *'SAME cache-root and cache-lineage'*) ;; *) fail "the mismatch error does not say what to do about it: $out" ;; esac ok "verify rejects a publish step that forgot the lineage, and says so" if verify /cache 'a/b' /cache/a/b > /dev/null 2>&1; then fail "verify accepted an invalid lineage as long as both sides agreed on it" fi ok "verify validates the lineage as well as comparing it" echo echo "cache-root-selftest: ${pass_count} assertions passed"