Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions .github/actions/setup-rust-build-cache/tests/fixture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ fi
grep -Fxq 'fixture-id=417-1' "$WORK_DIR/seed/output" || fail 'seed does not export fixture identity'
[[ "$(grep -c 'cargo build' "$ACTION_DIR/tests/assert-compiler-reuse.sh")" == 1 ]] || fail 'reuse probe primes its own cache'
if grep -q 'cargo clean' "$ACTION_DIR/tests/assert-compiler-reuse.sh"; then fail 'reuse probe accepts same-job rebuilds'; fi
workflow="$ACTION_DIR/../../workflows/deploy-action.yml"
workflow="${CACHE_WORKFLOW_FILE:-$ACTION_DIR/../../workflows/deploy-action.yml}"
seed=$(yq -r '.jobs."cache-restore-seed".steps[] | select(.name == "Seed native and WASM compiler outputs") | .run' "$workflow")
# shellcheck disable=SC2016 # Assert the literal shell command in workflow metadata.
[[ "$seed" == *'cargo build --locked --lib --target "$host"'* && "$seed" == *'cargo build --locked --lib --target wasm32-wasip1'* ]] || fail 'seed job does not compile both targets'
[[ "$seed" == 'bash .github/actions/setup-rust-build-cache/tests/seed-compiler-cache.sh' ]] || fail 'seed does not verify compiler publication'
for job in cache-restore-seed cache-restore-smoke cache-new-revision-smoke; do
JOB_ID="$job" yq -e '.jobs[strenv(JOB_ID)].steps[] | select(.name == "Install native and WASM compiler targets") | .run == "bash .github/actions/setup-rust-build-cache/tests/setup-toolchain.sh"' "$workflow" >/dev/null || fail "$job does not share its Rust toolchain"
done
Expand All @@ -41,7 +40,62 @@ for job in cache-restore-smoke cache-new-revision-smoke; do
actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].steps[] | select(.id == "fixture") | .env.CACHE_FIXTURE_ID' "$workflow")
[[ "$actual" == "$expected_identity" ]] || fail "$job does not reuse the seed fixture identity"
done
[[ "$(yq -r '.jobs."cache-restore-seed".steps[] | select(.id == "cache") | .with."save-target"' "$workflow")" == true ]] || fail 'seed does not opt into saving'
[[ "$(yq -r '.jobs."cache-restore-seed".steps[] | select(.id == "cache") | .with."save-target"' "$workflow")" == false ]] || fail 'seed relies on unverified post-job saving'
for job in fixture-release store-free-release; do
actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].needs // [] | length' "$workflow")
[[ "$actual" == 0 ]] || fail "$job depends on cache probe availability"
done
# Exercise both publication surfaces against their actual workflow wiring.
for kind in source target; do
if [[ "$kind" == source ]]; then
expected_path=$(yq -r '.runs.steps[] | select(.id == "cargo-sources") | .with.path' "$ACTION_DIR/action.yml")
else
# shellcheck disable=SC2016 # Literal GitHub output reference.
expected_path='${{ steps.cache.outputs.cargo-target-dir }}'
fi
expected_key="\${{ steps.cache.outputs.$kind-cache-key }}"
retry_condition="steps.$kind-verify-1.outputs.cache-hit != 'true'"
for step in "$kind-save-1" "$kind-verify-1" "$kind-save-2" "$kind-verify-2"; do
actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with.path' "$workflow")
[[ "$actual" == "$expected_path" ]] || fail "$step uses a different cache path"
actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with.key' "$workflow")
[[ "$actual" == "$expected_key" ]] || fail "$step uses a different cache key"
actual=$(STEP_ID="$step" yq -r '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .if // ""' "$workflow")
if [[ "$step" == *-2 ]]; then
[[ "$actual" == "$retry_condition" ]] || fail "$step retries without a first-attempt miss"
else
[[ -z "$actual" ]] || fail "$step skips the initial publication attempt"
fi
done
for attempt in 1 2; do
STEP_ID="$kind-save-$attempt" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .uses == "actions/cache/save@v5"' "$workflow" >/dev/null || fail 'missing explicit cache save'
STEP_ID="$kind-verify-$attempt" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with."lookup-only" == true' "$workflow" >/dev/null || fail 'cache publication is not verified'
done
STEP_ID="$kind-verify-2" yq -e '.jobs."cache-restore-seed".steps[] | select(.id == strenv(STEP_ID)) | .with."fail-on-cache-miss" == true' "$workflow" >/dev/null || fail 'exhausted publication retries stay green'
# The action can return successfully before honoring fail-on-cache-miss.
# Require actual exact-hit output, including when availability checks fail.
publication_step=".jobs.\"cache-restore-seed\".steps[] | select(.name == \"Require exact $kind seed publication\")"
publication_assertion=$(yq -r "$publication_step | .run" "$workflow")
[[ -n "$publication_assertion" && "$publication_assertion" != null ]] || fail 'missing exact publication assertion'
first_hit="\${{ steps.$kind-verify-1.outputs.cache-hit }}"
second_hit="\${{ steps.$kind-verify-2.outputs.cache-hit }}"
[[ "$(yq -r "$publication_step | .env.CACHE_SEED_FIRST_HIT" "$workflow")" == "$first_hit" ]] || fail 'publication assertion ignores first lookup output'
[[ "$(yq -r "$publication_step | .env.CACHE_SEED_SECOND_HIT" "$workflow")" == "$second_hit" ]] || fail 'publication assertion ignores second lookup output'
[[ "$(yq -r "$publication_step | .if // \"\"" "$workflow")" == '' ]] || fail 'publication assertion can be skipped after successful lookup steps'
[[ "$(yq -r "$publication_step | .\"continue-on-error\" // false" "$workflow")" == false ]] || fail 'publication assertion failure is ignored'
for first in '' false true partial; do
for second in '' false true partial; do
status=0
CACHE_SEED_FIRST_HIT="$first" CACHE_SEED_SECOND_HIT="$second" \
bash -e -o pipefail -c "$publication_assertion" >"$WORK_DIR/publication.log" 2>&1 || status=$?
if [[ "$first" == true || "$second" == true ]]; then
[[ "$status" == 0 ]] || fail 'publication assertion rejects a verified exact hit'
else
[[ "$status" != 0 ]] || fail "publication assertion accepts unverified outputs '$first'/'$second'"
fi
done
done
done
for job in fixture-release store-free-release; do
actual=$(JOB_ID="$job" yq -r '.jobs[strenv(JOB_ID)].steps[] | select(.uses == "./.github/actions/setup-rust-build-cache") | .with."cache-target"' "$workflow")
[[ "$actual" == false ]] || fail "$job enables full target caching for a disposable fixture"
Expand Down
62 changes: 62 additions & 0 deletions .github/actions/setup-rust-build-cache/tests/publication.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Mutate the workflow itself so contract checks cannot merely test a detached if.
set -euo pipefail
ACTION_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/edgezero-cache-publication-test.XXXXXX")
trap 'rm -rf -- "$WORK_DIR"' EXIT
# shellcheck source=common.sh
source "$ACTION_DIR/tests/common.sh"
workflow="$ACTION_DIR/../../workflows/deploy-action.yml"

reject_mutation() {
local name="$1" expression="$2" diagnostic="$3" status=0
yq "$expression" "$workflow" >"$WORK_DIR/workflow.yml"
CACHE_WORKFLOW_FILE="$WORK_DIR/workflow.yml" bash "$ACTION_DIR/tests/fixture.sh" >"$WORK_DIR/log" 2>&1 || status=$?
[[ "$status" == 1 ]] || fail "workflow guard accepted mutation: $name (exit $status)"
grep -Fq "$diagnostic" "$WORK_DIR/log" || {
cat "$WORK_DIR/log" >&2
fail "mutation failed for an unrelated reason: $name"
}
}

for kind in source target; do
publication=".jobs.\"cache-restore-seed\".steps[] | select(.name == \"Require exact $kind seed publication\")"
reject_mutation 'missing final gate' \
".jobs.\"cache-restore-seed\".steps |= map(select(.name != \"Require exact $kind seed publication\"))" \
'missing exact publication assertion'
reject_mutation 'hardcoded first hit' \
"($publication | .env.CACHE_SEED_FIRST_HIT) = \"true\"" \
'publication assertion ignores first lookup output'
reject_mutation 'hardcoded second hit' \
"($publication | .env.CACHE_SEED_SECOND_HIT) = \"true\"" \
'publication assertion ignores second lookup output'
reject_mutation 'skipped final gate' \
"($publication | .if) = \"false\"" \
'publication assertion can be skipped'
reject_mutation 'ignored final failure' \
"($publication | .\"continue-on-error\") = true" \
'publication assertion failure is ignored'
reject_mutation 'gate always succeeds' \
"($publication | .run) = \"true\"" \
'publication assertion accepts unverified outputs'
reject_mutation 'gate always fails' \
"($publication | .run) = \"false\"" \
'publication assertion rejects a verified exact hit'
for step in "$kind-save-1" "$kind-verify-1" "$kind-save-2" "$kind-verify-2"; do
for input in path key; do
reject_mutation "$step wrong $input" \
"(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .with.$input) = \"wrong\"" \
"$step uses a different cache $input"
done
if [[ "$step" == *-2 ]]; then
reject_mutation "$step wrong retry condition" \
"(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .if) = \"false\"" \
"$step retries without a first-attempt miss"
else
reject_mutation "$step skipped initial attempt" \
"(.jobs.\"cache-restore-seed\".steps[] | select(.id == \"$step\") | .if) = \"false\"" \
"$step skips the initial publication attempt"
fi
done
done
printf 'source and target publication workflow mutation contracts passed\n'
2 changes: 2 additions & 0 deletions .github/actions/setup-rust-build-cache/tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,5 @@ printf 'setup-rust-build-cache action tests passed\n'
bash "$ACTION_DIR/tests/revision.sh"
bash "$ACTION_DIR/tests/review.sh"
bash "$ACTION_DIR/tests/fixture.sh"
bash "$ACTION_DIR/tests/publication.sh"
bash "$ACTION_DIR/tests/seed.sh"
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

# Publish each target before downstream jobs test first-build compiler reuse.
fixture="${GITHUB_WORKSPACE:?}/cache-restore-fixture"
target_dir=$(realpath "${CARGO_TARGET_DIR:?}")
expected="$(realpath "${RUNNER_TEMP:?}")/edgezero-rust-cache/cache-restore-smoke-v2/target"
[[ "$target_dir" == "$expected" ]] || {
printf '::error::compiler seed must use its confined fixture target directory\n' >&2
exit 1
}
cd "$fixture"
host=$(rustc -vV | sed -n 's/^host: //p')
[[ "$host" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || {
printf '::error::rustc must report a safe host target component\n' >&2
exit 1
}
for target in "$host" wasm32-wasip1; do
for attempt in 1 2 3; do
# Cargo must call sccache again after a failed publication, rather than
# reporting locally fresh outputs as a successful cache seed.
debug_dir="$target_dir/$target/debug"
[[ ! -L "$target_dir/$target" && ! -L "$debug_dir" ]] || {
printf '::error::compiler seed cleanup must not follow target symlinks\n' >&2
exit 1
}
rm -rf -- "$debug_dir"
sccache --zero-stats
cargo build --locked --lib --target "$target"
stats=$(sccache --show-stats --stats-format json)
printf 'Compiler seed for %s (attempt %s): %s\n' "$target" "$attempt" "$stats"
if jq -e '((.stats.cache_hits.counts.Rust // 0) + (.stats.cache_writes // 0)) > 0 and (.stats.cache_errors.counts.Rust // 0) == 0 and (.stats.cache_read_errors // 0) == 0 and (.stats.cache_write_errors // 0) == 0' <<<"$stats" >/dev/null; then
break
fi
if [[ "$attempt" == 3 ]]; then
printf '::error::failed to publish compiler cache for %s after three attempts\n' "$target" >&2
exit 1
fi
printf '::warning::compiler cache seed failed; retrying after backoff\n'
sleep "$((attempt * 60))"
done
done
127 changes: 127 additions & 0 deletions .github/actions/setup-rust-build-cache/tests/seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
ACTION_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/edgezero-cache-seed-test.XXXXXX")
trap 'rm -rf -- "$WORK_DIR"' EXIT
# shellcheck source=common.sh
source "$ACTION_DIR/tests/common.sh"
mkdir -p "$WORK_DIR/bin" "$WORK_DIR/workspace/cache-restore-fixture" "$WORK_DIR/runner"
export GITHUB_WORKSPACE="$WORK_DIR/workspace" RUNNER_TEMP="$WORK_DIR/runner"
export CARGO_TARGET_DIR="$RUNNER_TEMP/edgezero-rust-cache/cache-restore-smoke-v2/target"
export PROBE_LOG="$WORK_DIR/calls" PROBE_COUNT="$WORK_DIR/count"
cat >"$WORK_DIR/bin/rustc" <<'MOCK'
#!/usr/bin/env bash
printf 'host: %s\n' "${PROBE_HOST:-test-host}"
MOCK
cat >"$WORK_DIR/bin/sleep" <<'MOCK'
#!/usr/bin/env bash
printf 'sleep %s\n' "$*" >>"$PROBE_LOG"
MOCK
cat >"$WORK_DIR/bin/cargo" <<'MOCK'
#!/usr/bin/env bash
printf 'build %s\n' "$*" >>"$PROBE_LOG"
[[ "${PROBE_MODE:-}" != compile-failure ]] || exit 42
[[ "$PROBE_MODE" != compile-wasm-failure || "${*: -1}" != wasm32-wasip1 ]] || exit 42
[[ ! -d "$CARGO_TARGET_DIR/${*: -1}/debug" ]] || exit 43
mkdir -p "$CARGO_TARGET_DIR/${*: -1}/debug"
MOCK
cat >"$WORK_DIR/bin/sccache" <<'MOCK'
#!/usr/bin/env bash
if [[ "$1" == --zero-stats ]]; then
printf 'zero\n' >>"$PROBE_LOG"
exit 0
fi
[[ "$1" == --show-stats ]] || exit 2
count=$(cat "$PROBE_COUNT")
printf '%s\n' "$((count + 1))" >"$PROBE_COUNT"
if [[ "$PROBE_MODE" == permanent ||
( "$PROBE_MODE" == transient && "$count" == 0 ) ||
( "$PROBE_MODE" == wasm-transient && "$count" == 1 ) ||
( "$PROBE_MODE" == both-transient && ( "$count" == 0 || "$count" == 2 ) ) ||
( "$PROBE_MODE" == wasm-permanent && "$count" -ge 1 ) ]]; then
printf '{"stats":{"cache_write_errors":1,"cache_writes":0}}\n'
elif [[ "$PROBE_MODE" == read-error ]]; then
printf '{"stats":{"cache_writes":1,"cache_read_errors":1}}\n'
elif [[ "$PROBE_MODE" == compile-cache-error ]]; then
printf '{"stats":{"cache_hits":{"counts":{"Rust":1}},"cache_errors":{"counts":{"Rust":1}}}}\n'
elif [[ "$PROBE_MODE" == malformed ]]; then
printf 'not-json\n'
elif [[ "$PROBE_MODE" == empty ]]; then
printf '{"stats":{}}\n'
elif [[ "$PROBE_MODE" == hit ]]; then
printf '{"stats":{"cache_hits":{"counts":{"Rust":1}}}}\n'
else
printf '{"stats":{"cache_writes":1}}\n'
fi
MOCK
chmod +x "$WORK_DIR/bin/"*
export PATH="$WORK_DIR/bin:$PATH"
run_seed() {
rm -rf "$CARGO_TARGET_DIR"
mkdir -p "$CARGO_TARGET_DIR"
printf '0\n' >"$PROBE_COUNT"
: >"$PROBE_LOG"
PROBE_MODE="$1" bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1
}
run_seed transient || fail 'compiler seed does not recover from a transient write failure'
[[ "$(grep -c '^build ' "$PROBE_LOG")" == 3 ]] || fail 'retry does not rebuild fresh outputs'
[[ "$(grep -c '^sleep ' "$PROBE_LOG")" == 1 ]] || fail 'transient error does not back off'
run_seed hit || fail 'existing compiler hit is not accepted as published'
[[ "$(grep -c '^build ' "$PROBE_LOG")" == 2 ]] || fail 'successful seed does not build both targets'
grep -q -- '--target test-host$' "$PROBE_LOG" || fail 'successful seed omits native target'
grep -q -- '--target wasm32-wasip1$' "$PROBE_LOG" || fail 'successful seed omits WASM target'
[[ "$(grep -c '^zero$' "$PROBE_LOG")" == 2 ]] || fail 'successful seed does not reset stats for each target'
for mode in wasm-transient both-transient; do
run_seed "$mode" || fail "$mode does not recover"
expected=3
if [[ "$mode" == both-transient ]]; then expected=4; fi
[[ "$(grep -c '^build ' "$PROBE_LOG")" == "$expected" ]] || fail "$mode does not rebuild every attempt"
[[ "$(grep -c '^zero$' "$PROBE_LOG")" == "$expected" ]] || fail "$mode does not reset every attempt"
done
for mode in permanent empty compile-failure compile-wasm-failure wasm-permanent read-error compile-cache-error malformed; do
if run_seed "$mode"; then fail "accepted unsuccessful compiler seeding: $mode"; fi
case "$mode" in
compile-failure) expected=1 ;;
compile-wasm-failure) expected=2 ;;
wasm-permanent) expected=4 ;;
*) expected=3 ;;
esac
[[ "$(grep -c '^build ' "$PROBE_LOG")" == "$expected" ]] || fail "$mode violates per-target retry limits"
[[ "$(grep -c '^zero$' "$PROBE_LOG")" == "$expected" ]] || fail "$mode uses stale stats"
done
# Cleanup must never follow restored symlinks outside the fixture root.
for target in test-host wasm32-wasip1; do
for component in namespace debug; do
rm -rf -- "$CARGO_TARGET_DIR" "$WORK_DIR/outside"
mkdir -p "$CARGO_TARGET_DIR" "$WORK_DIR/outside/debug"
printf keep >"$WORK_DIR/outside/debug/marker"
if [[ "$component" == namespace ]]; then
ln -s "$WORK_DIR/outside" "$CARGO_TARGET_DIR/$target"
else
mkdir -p "$CARGO_TARGET_DIR/$target"
ln -s "$WORK_DIR/outside/debug" "$CARGO_TARGET_DIR/$target/debug"
fi
status=0
PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1 || status=$?
[[ -f "$WORK_DIR/outside/debug/marker" ]] || fail "cleanup escaped through $target $component symlink"
[[ "$status" != 0 ]] || fail "accepted $target $component symlink"
grep -Fq 'compiler seed cleanup must not follow target symlinks' "$WORK_DIR/log" || fail 'symlink case failed for an unrelated reason'
done
done
rm -rf -- "$CARGO_TARGET_DIR"
mkdir -p "$CARGO_TARGET_DIR" "$WORK_DIR/runner/edgezero-rust-cache/debug"
printf keep >"$WORK_DIR/runner/edgezero-rust-cache/debug/marker"
if PROBE_HOST=../.. PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1; then
fail 'accepted traversal in compiler host'
fi
[[ -f "$WORK_DIR/runner/edgezero-rust-cache/debug/marker" ]] || fail 'removed output through host traversal'
grep -Fq 'rustc must report a safe host target component' "$WORK_DIR/log" || fail 'traversal case failed for an unrelated reason'
# A mistaken directory must never be removed by retry cleanup.
export CARGO_TARGET_DIR="$WORK_DIR/outside"
mkdir -p "$CARGO_TARGET_DIR"
printf keep >"$CARGO_TARGET_DIR/marker"
if PROBE_MODE=hit bash "$ACTION_DIR/tests/seed-compiler-cache.sh" >"$WORK_DIR/log" 2>&1; then
fail 'accepted unconfined compiler seed directory'
fi
[[ -f "$CARGO_TARGET_DIR/marker" ]] || fail 'removed unconfined output'
printf 'compiler seed backoff contracts passed\n'
Loading
Loading