CI / shellcheck + selftests (pull_request) Successful in 1m23s
Two review findings on #13. THE STATED INVARIANT WAS NOT IN FORCE. The header claimed a `set -e` abort could never be mistaken for the `1` that means "measured, and the answer is mtime". It could not fire at all: `checksum_freshness_probe || probe_rc=$?` runs the left side with errexit suppressed, and that suppression propagates into the subshell. An unguarded step there fell through to `exit 0` and reported ACTIVE — a fourth outcome the header said was impossible. The guard audit was complete, so nothing was broken; the wrong mechanism had the credit, in a comment inviting the next editor to add an unguarded step and rely on it. The suggested fix — `set -e` as the first line of the subshell — does not work, and measured on bash 5.3 it makes things worse rather than not-better: set -e inside a subshell called via `||` or `if` still falls through: rc=0 set -e inside, called with errexit off at the site aborts, but with the FAILING COMMAND's status — `false` gives 1, which is exactly the value that means "mtime" So both halves are needed and neither is decoration: `set +e` around the call site, so the subshell can arm its own errexit at all, and `trap 'exit 2' ERR` inside it, so an abort lands on "not measured" instead of on an answer. The explicit `|| exit 2` guards stay as the first line of defence. All of that is now written down where the false claim was. Red-proven with one unguarded `false` between the `touch` and the second build, on a box where freshness is live: without the fix, mode `on` and 4 assertions — the reviewer's fourth outcome, reproduced; with it, mode `unmeasured`, the warning, 3 assertions; unmodified, mode `on` and 4. Recorded in the same block, since the next person to hit a `2` will ask: why it skips rather than fails. The gated scenario is the only thing here that depends on freshness mode, and failing would turn a statement about one machine into a red gate reading "the hardlink scheme is broken" across three consuming repos. What would change the answer is `2` becoming the everyday CI outcome, and run 2583 measures that it is not — gitdan-ci reports `1`. GPG. The two suites that commit in scratch repos inherited the developer's GLOBAL `commit.gpgsign`, so whether this gate passes could depend on their gpg agent — seen as a red `restore-mtimes-selftest.sh` caused by a full disk breaking gpg, in a suite with nothing to say about either. Pinned off locally, in the throwaway repos only: `git config commit.gpgsign false` beside the identity the scratch repo already sets, and `-c commit.gpgsign=false` on prune-cache-selftest's three commits, matching its existing `-c` style. Red-proven under a GIT_CONFIG_GLOBAL with gpgsign on and a nonexistent gpg program: without it `fatal: failed to write commit object`, with it both suites pass.
490 lines
21 KiB
Bash
Executable File
490 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression test for restore-mtimes.sh's per-target-dir watermark (issue
|
|
# The merge hazard: a branch that merges an older commit could silently reuse a stale
|
|
# build artifact and fail with an "impossible" compile error). Builds a
|
|
# minimal, throwaway two-crate cargo workspace in a scratch git repo whose
|
|
# history is shaped exactly like the real incident, then runs the ACTUAL
|
|
# `restore-mtimes.sh` sitting next to this file against it under real
|
|
# `cargo build` — not a simulation of the logic, the real script and a real
|
|
# compiler. Every scenario ends in a hard assertion; the first failure dumps
|
|
# the relevant log and exits non-zero, so a future change to restore-mtimes.sh
|
|
# or the actions' step order that reopens this hazard fails loudly here
|
|
# instead of surfacing weeks later as a confusing CI compile error.
|
|
#
|
|
# Run by hand:
|
|
# bash .gitea/scripts/restore-mtimes-selftest.sh
|
|
#
|
|
# Slow by design — this builds a throwaway cargo workspace
|
|
# and runs several real compiler invocations (~1-2 minutes), which is too
|
|
# slow to pay on every ordinary push. Re-run it by hand whenever
|
|
# restore-mtimes.sh's timestamp logic or the actions' cache/seed/watermark step
|
|
# order changes.
|
|
#
|
|
# Needs a working `cargo`/`rustc` on PATH. Does not touch this repo or its
|
|
# own target dir — everything happens under a `mktemp -d` scratch tree,
|
|
# removed on exit via the trap below regardless of outcome.
|
|
#
|
|
# What each scenario demonstrates, and why the "control" runs matter as much
|
|
# as the "fixed" ones (a scenario that always passes proves nothing):
|
|
#
|
|
# 1. NO WATERMARK, same-branch merge (control) — a feature branch builds
|
|
# once, then merges a commit that changed a dependency crate but was
|
|
# authored earlier in real time than that build. With no watermark to
|
|
# consult, restore-mtimes.sh falls back to its pre-watermark behaviour
|
|
# (git-log-only timestamps): asserts this reproduces the exact bug —
|
|
# the dependency crate is judged Fresh and reused stale, and the
|
|
# dependent crate fails to compile against it.
|
|
# 2. WATERMARK PRESENT, same state (fix) — identical setup, but this
|
|
# target dir's watermark names the pre-merge build. Asserts the
|
|
# dependency crate is correctly identified as changed and recompiles,
|
|
# and the build succeeds.
|
|
# 3. WATERMARK PRESENT, no further change (warm-path) — re-runs scenario
|
|
# 2's state with an up-to-date watermark and no new commits. Asserts
|
|
# NOTHING recompiles — the ordinary no-merge case restore-mtimes.sh
|
|
# exists for is unaffected by the watermark machinery.
|
|
# 4. SEEDED NAMESPACE, watermark stripped (control) — a brand-new branch
|
|
# namespace, seeded from scenario
|
|
# 2/3's now-warm target dir, then merges a THIRD lineage's older commit.
|
|
# With the inherited watermark file deliberately removed, asserts the
|
|
# same staleness bug reproduces on a namespace that has never itself
|
|
# run a build — proving the seed-snapshot path shares the hazard, not
|
|
# just long-lived branches.
|
|
# 5. SEEDED NAMESPACE, watermark inherited (fix) — identical to 4, but the
|
|
# watermark file rides along with the `cp -a` seed copy exactly as
|
|
# the cargo-cache action's seed step really does it.
|
|
# Asserts the dependency crate correctly recompiles on this namespace's
|
|
# very first run.
|
|
# 6. TWO CONSUMERS, ONE SHARED WATERMARK FILE, sequential jobs (control) —
|
|
# two jobs in one workflow can share a single per-ref
|
|
# $CARGO_TARGET_DIR, so one cache holds two independent cargo profile
|
|
# directories (say `debug/` built by a test job and `release/` built by a
|
|
# wasm build job). Modelling this as "both jobs read before either
|
|
# writes" is the wrong ordering: on a serial runner with no `needs:`
|
|
# between the two jobs, one job's watermark WRITE lands before the other
|
|
# job's own READ, within the same trigger. This scenario now runs restore-mtimes.sh TWICE IN
|
|
# SEQUENCE against ONE shared watermark file, with a real watermark
|
|
# write in between (job A builds debug/, succeeds, advances the shared
|
|
# watermark to HEAD; job B then reads that JUST-ADVANCED watermark).
|
|
# Asserts job A succeeds and job B reproduces the original bug —
|
|
# an empty HEAD..HEAD diff, full fallback to git-log timestamps, stale
|
|
# reuse in release/.
|
|
# 7. TWO CONSUMERS, TWO WATERMARK FILES, sequential jobs (fix) — identical
|
|
# ordering, but each job reads and writes its OWN watermark file
|
|
# (`CI_WATERMARK_FILE`), exactly as the cargo-cache action does. Job B's read is
|
|
# unaffected by job A's write because they touch different files.
|
|
# Asserts BOTH jobs correctly recompile the dependency and succeed.
|
|
set -euo pipefail
|
|
|
|
# Every assertion below reads cargo's own words out of a build log
|
|
# (`Compiling libdep`, `Fresh probe`). A CI image that forces colour splices an
|
|
# ANSI reset between the status word and the crate name, at which point every
|
|
# one of those greps silently stops matching and the suite reports the
|
|
# opposite of what happened — observed on gitdan-ci's runner image, where
|
|
# scenario 2 failed while the log it printed plainly showed `Compiling libdep`.
|
|
# Pin the format the assertions are written against.
|
|
export CARGO_TERM_COLOR=never
|
|
|
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
restore_mtimes="$script_dir/restore-mtimes.sh"
|
|
|
|
if [ ! -x "$restore_mtimes" ] && [ ! -f "$restore_mtimes" ]; then
|
|
echo "restore-mtimes-selftest: expected to find restore-mtimes.sh at $restore_mtimes" >&2
|
|
exit 1
|
|
fi
|
|
|
|
scratch=$(mktemp -d)
|
|
trap 'rm -rf "$scratch"' EXIT
|
|
repo="$scratch/wk"
|
|
target_feature="$scratch/target-feature"
|
|
target_newbranch="$scratch/target-newbranch"
|
|
target_multiprofile="$scratch/target-multiprofile"
|
|
|
|
pass_count=0
|
|
|
|
assert_log_has() {
|
|
local file="$1" pattern="$2" desc="$3"
|
|
if ! grep -q -- "$pattern" "$file"; then
|
|
echo "ASSERTION FAILED: $desc" >&2
|
|
echo " expected to find: $pattern" >&2
|
|
echo " --- full log ($file) ---" >&2
|
|
cat "$file" >&2
|
|
exit 1
|
|
fi
|
|
pass_count=$((pass_count + 1))
|
|
echo "PASS: $desc"
|
|
}
|
|
|
|
assert_log_lacks() {
|
|
local file="$1" pattern="$2" desc="$3"
|
|
if grep -q -- "$pattern" "$file"; then
|
|
echo "ASSERTION FAILED: $desc" >&2
|
|
echo " expected NOT to find: $pattern" >&2
|
|
echo " --- full log ($file) ---" >&2
|
|
cat "$file" >&2
|
|
exit 1
|
|
fi
|
|
pass_count=$((pass_count + 1))
|
|
echo "PASS: $desc"
|
|
}
|
|
|
|
# `actions/checkout` stamps every tracked file with wall-clock "now" on
|
|
# every run — simulate that before each restore-mtimes.sh pass, exactly as
|
|
# CI would see it, so this test exercises the script the same way CI does.
|
|
stamp_checkout_now() {
|
|
find . -path ./.git -prune -o -type f -print0 | xargs -0 touch
|
|
}
|
|
|
|
echo "=== building scratch workspace ==="
|
|
mkdir -p "$repo/crates/libdep/src" "$repo/crates/libuser/src"
|
|
cd "$repo"
|
|
git init -q
|
|
git config user.email test@example.com
|
|
git config user.name "restore-mtimes-selftest"
|
|
# Local to this mktemp'd throwaway repo. Without it the eight commits below
|
|
# inherit the developer's GLOBAL commit.gpgsign, which makes whether this gate
|
|
# passes depend on their gpg agent — observed as a red run caused by a full
|
|
# disk breaking gpg, in a suite that has nothing to say about either.
|
|
git config commit.gpgsign false
|
|
|
|
cat > Cargo.toml <<'EOF'
|
|
[workspace]
|
|
resolver = "2"
|
|
members = ["crates/libdep", "crates/libuser"]
|
|
EOF
|
|
cat > crates/libdep/Cargo.toml <<'EOF'
|
|
[package]
|
|
name = "libdep"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
EOF
|
|
cat > crates/libdep/src/lib.rs <<'EOF'
|
|
pub struct CheckIn;
|
|
|
|
impl CheckIn {
|
|
pub fn new() -> Self {
|
|
CheckIn
|
|
}
|
|
}
|
|
EOF
|
|
cat > crates/libuser/Cargo.toml <<'EOF'
|
|
[package]
|
|
name = "libuser"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[dependencies]
|
|
libdep = { path = "../libdep" }
|
|
EOF
|
|
cat > crates/libuser/src/lib.rs <<'EOF'
|
|
pub fn make() -> libdep::CheckIn {
|
|
libdep::CheckIn::new()
|
|
}
|
|
EOF
|
|
git add -A
|
|
GIT_AUTHOR_DATE="2023-01-01T00:00:00" GIT_COMMITTER_DATE="2023-01-01T00:00:00" \
|
|
git commit -q -m "base: CheckIn::new only"
|
|
base_sha=$(git rev-parse HEAD)
|
|
|
|
# dev's own future: adds with_intensity_opt, with an OLD committer date —
|
|
# analogous to a PR merged into dev before this branch's own build.
|
|
git switch -q -c dev
|
|
cat > crates/libdep/src/lib.rs <<'EOF'
|
|
pub struct CheckIn;
|
|
|
|
impl CheckIn {
|
|
pub fn new() -> Self {
|
|
CheckIn
|
|
}
|
|
|
|
pub fn with_intensity_opt(&self) -> Self {
|
|
CheckIn
|
|
}
|
|
}
|
|
EOF
|
|
git add -A
|
|
GIT_AUTHOR_DATE="2023-06-01T01:02:51" GIT_COMMITTER_DATE="2023-06-01T01:02:51" \
|
|
git commit -q -m "journal: add CheckIn::with_intensity_opt"
|
|
dev2_sha=$(git rev-parse HEAD)
|
|
|
|
# feature branch, forked BEFORE dev2 exists — does its own unrelated work,
|
|
# and gets its own real build (real wall-clock dep-info mtimes) before dev2
|
|
# is ever merged in.
|
|
git switch -q -c feature "$base_sha"
|
|
cat > crates/libuser/src/lib.rs <<'EOF'
|
|
pub fn make() -> libdep::CheckIn {
|
|
libdep::CheckIn::new()
|
|
}
|
|
|
|
pub fn unrelated_feature_work() -> u32 {
|
|
42
|
|
}
|
|
EOF
|
|
git add -A
|
|
git commit -q -m "feature: unrelated libuser work"
|
|
feat1_sha=$(git rev-parse HEAD)
|
|
|
|
echo "=== feature branch's own real build (real wall-clock artifact mtimes) ==="
|
|
CARGO_TARGET_DIR="$target_feature" cargo build --workspace --quiet
|
|
# Two pristine snapshots taken right after this one real build, before
|
|
# scenario 1's failed build attempt below partially rewrites fingerprints
|
|
# against it: one WITHOUT a watermark file (scenario 1's starting state) and
|
|
# one WITH (scenario 2's) — both need to restart from this same pre-merge
|
|
# baseline, not from whatever a failed build left behind, and scenario 1
|
|
# must never see a watermark or it isn't testing the control it claims to.
|
|
cp -a "$target_feature" "$target_feature.pristine-no-watermark"
|
|
echo "$feat1_sha" > "$target_feature/.ci-watermark-sha"
|
|
cp -a "$target_feature" "$target_feature.pristine-with-watermark"
|
|
|
|
# The commit below ("feature: use with_intensity_opt") gets a real "now"
|
|
# date — scenario 1 needs it to be unambiguously newer than the dep-info
|
|
# artifact just built above, in whole-second terms git commit timestamps
|
|
# use. This harness runs fast enough that, without a gap, the two could
|
|
# otherwise land in the same wall-clock second and make scenario 1 flaky.
|
|
# Real CI never runs this close together; this is purely a test-harness
|
|
# margin, matching the one before scenarios 4/5 below.
|
|
sleep 2
|
|
|
|
git merge -q dev -m "merge dev into feature"
|
|
cat > crates/libuser/src/lib.rs <<'EOF'
|
|
pub fn make() -> libdep::CheckIn {
|
|
libdep::CheckIn::new().with_intensity_opt()
|
|
}
|
|
|
|
pub fn unrelated_feature_work() -> u32 {
|
|
42
|
|
}
|
|
EOF
|
|
git add -A
|
|
git commit -q -m "feature: use with_intensity_opt"
|
|
feat2_sha=$(git rev-parse HEAD)
|
|
|
|
echo
|
|
echo "=== 1: NO WATERMARK, same-branch merge (control — expect the bug) ==="
|
|
rm -rf "$target_feature"
|
|
cp -a "$target_feature.pristine-no-watermark" "$target_feature"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_feature" bash "$restore_mtimes"
|
|
set +e
|
|
CARGO_TARGET_DIR="$target_feature" cargo build --workspace -v > "$scratch/run1.log" 2>&1
|
|
set -e
|
|
assert_log_has "$scratch/run1.log" "no method named .with_intensity_opt." \
|
|
"scenario 1: absent watermark reproduces the stale-reuse compile error"
|
|
|
|
echo
|
|
echo "=== 2: WATERMARK PRESENT, same state (fix — expect success) ==="
|
|
rm -rf "$target_feature"
|
|
cp -a "$target_feature.pristine-with-watermark" "$target_feature"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_feature" bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_feature" cargo build --workspace -v > "$scratch/run2.log" 2>&1
|
|
assert_log_lacks "$scratch/run2.log" "no method named" \
|
|
"scenario 2: watermark present — build succeeds, no stale reuse"
|
|
assert_log_has "$scratch/run2.log" "Compiling libdep" \
|
|
"scenario 2: libdep actually recompiled (not just Fresh-passthrough)"
|
|
|
|
echo
|
|
echo "=== 3: WATERMARK PRESENT, no further change (warm-path preserved) ==="
|
|
echo "$feat2_sha" > "$target_feature/.ci-watermark-sha"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_feature" bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_feature" cargo build --workspace -v > "$scratch/run3.log" 2>&1
|
|
assert_log_lacks "$scratch/run3.log" "Compiling" \
|
|
"scenario 3: unchanged re-run stays warm — nothing recompiles"
|
|
|
|
# Scenarios 4/5: a brand-new namespace seeded from feature's own (now warm)
|
|
# target dir, then merging a THIRD lineage's older change. `sleep` keeps the
|
|
# THIRD lineage's own "now"-dated commits a full second clear of the
|
|
# artifact just built above — git commit timestamps are whole-second, dep-info
|
|
# mtimes are nanosecond, and this harness runs fast enough that the two could
|
|
# otherwise land in the same second and make scenario 4's control flaky. Real
|
|
# CI never runs this close together; this is purely a test-harness margin.
|
|
sleep 2
|
|
|
|
git switch -q -c parallel2 "$dev2_sha"
|
|
cat > crates/libdep/src/lib.rs <<'EOF'
|
|
pub struct CheckIn;
|
|
|
|
impl CheckIn {
|
|
pub fn new() -> Self {
|
|
CheckIn
|
|
}
|
|
|
|
pub fn with_intensity_opt(&self) -> Self {
|
|
CheckIn
|
|
}
|
|
|
|
pub fn with_note(&self) -> Self {
|
|
CheckIn
|
|
}
|
|
}
|
|
EOF
|
|
git add -A
|
|
GIT_AUTHOR_DATE="2023-07-15T09:00:00" GIT_COMMITTER_DATE="2023-07-15T09:00:00" \
|
|
git commit -q -m "journal: add CheckIn::with_note"
|
|
|
|
# newbranch forks from feature at feat2_sha (scenario 2/3's fixed, warm
|
|
# state) and merges parallel2. Clean merge: newbranch already has dev2's
|
|
# with_intensity_opt via feature's earlier merge, parallel2 only adds
|
|
# with_note on top of that same ancestor.
|
|
git switch -q -c newbranch "$feat2_sha"
|
|
git merge -q parallel2 -m "merge parallel2 into newbranch"
|
|
sed -i 's/CheckIn::new()\.with_intensity_opt()/CheckIn::new().with_intensity_opt().with_note()/' \
|
|
crates/libuser/src/lib.rs
|
|
git add -A
|
|
git commit -q -m "newbranch: use with_note too"
|
|
|
|
echo
|
|
echo "=== 4: SEEDED NAMESPACE, watermark stripped (control — expect the bug) ==="
|
|
rm -rf "$target_newbranch"
|
|
cp -a "$target_feature" "$target_newbranch"
|
|
rm -f "$target_newbranch/.ci-watermark-sha"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_newbranch" bash "$restore_mtimes"
|
|
set +e
|
|
CARGO_TARGET_DIR="$target_newbranch" cargo build --workspace -v > "$scratch/run4.log" 2>&1
|
|
set -e
|
|
assert_log_has "$scratch/run4.log" "no method named .with_note." \
|
|
"scenario 4: a seeded namespace with no inherited watermark reproduces the same bug"
|
|
|
|
echo
|
|
echo "=== 5: SEEDED NAMESPACE, watermark inherited via cp -a (fix — expect success) ==="
|
|
rm -rf "$target_newbranch"
|
|
cp -a "$target_feature" "$target_newbranch"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_newbranch" bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_newbranch" cargo build --workspace -v > "$scratch/run5.log" 2>&1
|
|
assert_log_lacks "$scratch/run5.log" "no method named" \
|
|
"scenario 5: inherited watermark — build succeeds on the namespace's first run"
|
|
assert_log_has "$scratch/run5.log" "Compiling libdep" \
|
|
"scenario 5: libdep actually recompiled on the seeded namespace"
|
|
|
|
# Scenarios 6/7: a second job in the same workflow can share the
|
|
# `ci` job's per-branch $CARGO_TARGET_DIR namespace — `debug/` (the `ci`
|
|
# job's fmt/clippy/test steps) and `release/` (the `web` job's
|
|
# build-web-opt) sit side by side in one directory. The FIRST version of
|
|
# these scenarios ran restore-mtimes.sh ONCE, then built both profiles
|
|
# against its single output — modelling "both jobs read before either
|
|
# writes". Review caught that this is the wrong ordering: on a serial
|
|
# runner with no `needs:` between the two jobs, one job's watermark WRITE
|
|
# can land before the other job's own READ, on the very same trigger. These
|
|
# scenarios now run restore-mtimes.sh TWICE IN SEQUENCE, with a watermark
|
|
# write in between — the real ordering — and scenario 6 is required to
|
|
# reproduce the bug against a single shared watermark file before scenario 7
|
|
# demonstrates the fix (one watermark file per consumer). `target_feature`
|
|
# is already warm and consistent at feat2_sha from scenarios 2/3 (debug/
|
|
# built, watermark=feat2_sha); build release/ against the same namespace too
|
|
# so both profile dirs start warm, exactly as they would after both real
|
|
# jobs have run once against a branch.
|
|
echo
|
|
echo "=== building the release profile too, so both profile dirs start warm ==="
|
|
CARGO_TARGET_DIR="$target_feature" cargo build --workspace --release --quiet
|
|
cp -a "$target_feature" "$scratch/mp-pristine-shared-watermark"
|
|
cp -a "$target_feature" "$scratch/mp-pristine-split-watermarks"
|
|
cp "$scratch/mp-pristine-split-watermarks/.ci-watermark-sha" \
|
|
"$scratch/mp-pristine-split-watermarks/.ci-watermark-sha-wasm32"
|
|
|
|
# A third lineage, forked from dev2_sha (same shape as parallel2 above),
|
|
# adding a further method with an OLD committer date — analogous to a second,
|
|
# later dev commit that both real jobs would need to see as a real change.
|
|
sleep 2
|
|
git switch -q -c parallel3 "$dev2_sha"
|
|
cat > crates/libdep/src/lib.rs <<'EOF'
|
|
pub struct CheckIn;
|
|
|
|
impl CheckIn {
|
|
pub fn new() -> Self {
|
|
CheckIn
|
|
}
|
|
|
|
pub fn with_intensity_opt(&self) -> Self {
|
|
CheckIn
|
|
}
|
|
|
|
pub fn with_flag(&self) -> Self {
|
|
CheckIn
|
|
}
|
|
}
|
|
EOF
|
|
git add -A
|
|
GIT_AUTHOR_DATE="2023-08-01T09:00:00" GIT_COMMITTER_DATE="2023-08-01T09:00:00" \
|
|
git commit -q -m "journal: add CheckIn::with_flag"
|
|
|
|
# multiprofile forks from feat2_sha (feature's own fixed, warm point) and
|
|
# merges parallel3 — clean merge, same shape as newbranch/parallel2 above.
|
|
git switch -q -c multiprofile "$feat2_sha"
|
|
git merge -q parallel3 -m "merge parallel3 into multiprofile"
|
|
cat > crates/libuser/src/lib.rs <<'EOF'
|
|
pub fn make() -> libdep::CheckIn {
|
|
libdep::CheckIn::new().with_intensity_opt().with_flag()
|
|
}
|
|
|
|
pub fn unrelated_feature_work() -> u32 {
|
|
42
|
|
}
|
|
EOF
|
|
git add -A
|
|
git commit -q -m "multiprofile: use with_flag too"
|
|
mp_sha=$(git rev-parse HEAD)
|
|
|
|
echo
|
|
echo "=== 6: TWO CONSUMERS, ONE SHARED WATERMARK FILE, sequential jobs (control — expect the bug) ==="
|
|
# "Job A" (ci-equivalent, debug/ consumer) runs FIRST: its own checkout,
|
|
# its own restore-mtimes call, its own build, then its own watermark write —
|
|
# exactly the actions' real step order, just inlined here instead of split
|
|
# across two jobs.
|
|
rm -rf "$target_multiprofile"
|
|
cp -a "$scratch/mp-pristine-shared-watermark" "$target_multiprofile"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_multiprofile" bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_multiprofile" cargo build --workspace -v > "$scratch/run6-debug.log" 2>&1
|
|
assert_log_lacks "$scratch/run6-debug.log" "no method named" \
|
|
"scenario 6: job A (debug/, running first) builds cleanly off the true prior watermark"
|
|
assert_log_has "$scratch/run6-debug.log" "Compiling libdep" \
|
|
"scenario 6: job A actually recompiled libdep in debug/"
|
|
echo "$mp_sha" > "$target_multiprofile/.ci-watermark-sha"
|
|
|
|
# "Job B" (web-equivalent, release/ consumer) runs SECOND, same trigger, same
|
|
# HEAD, same $CARGO_TARGET_DIR, no `needs:` between them — the real ordering
|
|
# review caught. Its own checkout (stamp_checkout_now again, faithfully
|
|
# modelling a separate checkout), then its own restore-mtimes call, which
|
|
# reads the SAME watermark file job A just advanced to mp_sha: HEAD..mp_sha
|
|
# is an empty diff, so the override never fires and every path falls back to
|
|
# its plain git-log timestamp — including the backdated libdep change.
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_multiprofile" bash "$restore_mtimes"
|
|
set +e
|
|
CARGO_TARGET_DIR="$target_multiprofile" cargo build --workspace --release -v > "$scratch/run6-release.log" 2>&1
|
|
set -e
|
|
assert_log_has "$scratch/run6-release.log" "no method named .with_flag." \
|
|
"scenario 6: job B (release/, running second) reads the watermark job A just advanced, gets an empty diff, and reproduces the original stale-reuse bug"
|
|
|
|
echo
|
|
echo "=== 7: TWO CONSUMERS, TWO WATERMARK FILES, sequential jobs (fix — expect success) ==="
|
|
# Same sequential ordering as scenario 6, but each job reads and writes its
|
|
# OWN watermark file (CI_WATERMARK_FILE), exactly as the cargo-cache action does.
|
|
rm -rf "$target_multiprofile"
|
|
cp -a "$scratch/mp-pristine-split-watermarks" "$target_multiprofile"
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_multiprofile" bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_multiprofile" cargo build --workspace -v > "$scratch/run7-debug.log" 2>&1
|
|
assert_log_lacks "$scratch/run7-debug.log" "no method named" \
|
|
"scenario 7: job A (debug/) builds cleanly off its own watermark"
|
|
assert_log_has "$scratch/run7-debug.log" "Compiling libdep" \
|
|
"scenario 7: job A actually recompiled libdep in debug/"
|
|
echo "$mp_sha" > "$target_multiprofile/.ci-watermark-sha"
|
|
|
|
# Job B reads ITS OWN file (.ci-watermark-sha-wasm32), untouched by job A's
|
|
# write above — still feat2_sha, so the diff against mp_sha is exactly the
|
|
# real change set, not empty.
|
|
stamp_checkout_now
|
|
CARGO_TARGET_DIR="$target_multiprofile" CI_WATERMARK_FILE=.ci-watermark-sha-wasm32 \
|
|
bash "$restore_mtimes"
|
|
CARGO_TARGET_DIR="$target_multiprofile" cargo build --workspace --release -v > "$scratch/run7-release.log" 2>&1
|
|
assert_log_lacks "$scratch/run7-release.log" "no method named" \
|
|
"scenario 7: job B (release/) builds cleanly off its OWN watermark, unaffected by job A's write"
|
|
assert_log_has "$scratch/run7-release.log" "Compiling libdep" \
|
|
"scenario 7: job B actually recompiled libdep in release/"
|
|
|
|
echo
|
|
echo "ALL ${pass_count} ASSERTIONS PASSED"
|