Skip to content

Commit 1c3078e

Browse files
committed
test(runtime): a proven mismatch must not go green on a re-run
The Linux closure verdict is cached against the artifact stat and the RuntimeBinding contract hash, and caching a FAILURE is the half worth pinning: an unchanged artifact that was proven wrong is still wrong, and reporting success the second time tells the user a problem went away when nothing about the program changed. It is easy to lose by accident, because the project fast path exists to skip work when nothing changed — including, unless something stops it, the check that would have failed. `validated_artifact_snapshot` stops it by refusing to engage unless every stored verdict is a PASS; that rule had no test. 209 poisons one stored verdict without touching a source file and requires the next build to fail with the recorded diagnostics, requires `mcpp self doctor` to explain it from storage, and then requires a rebuilt artifact to be re-validated — a mismatch is cached, not a sentence, or the first real failure would wedge the project until someone deleted target/.
1 parent afba6f0 commit 1c3078e

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
# requires: elf gcc python3
3+
# 209_runtime_verdict_never_reruns_green.sh — a proven runtime mismatch must
4+
# not disappear when you simply run the build again.
5+
#
6+
# The Linux runtime closure check (Rule A/B) runs after the link, and its
7+
# verdict is cached against the artifact's stat plus the RuntimeBinding
8+
# contract hash. Caching a FAILURE is the interesting half: an unchanged
9+
# artifact that was proven wrong is still wrong, and a build system that
10+
# reports success on the second invocation has told the user the problem went
11+
# away when nothing about the program changed.
12+
#
13+
# It would be easy to get wrong by accident, because the project-level fast
14+
# path exists precisely to skip work when nothing changed — including, if
15+
# nobody stopped it, the check that would have failed. So the fast path is only
16+
# allowed to engage when every stored verdict is a PASS, and this test pins
17+
# that: poison one stored verdict and the very next `mcpp build` must fail
18+
# without touching a single source file.
19+
#
20+
# The other half matters too: the verdict is cached, not permanent. Change the
21+
# program and it is re-derived from the new artifact.
22+
set -euo pipefail
23+
24+
TMP=$(mktemp -d)
25+
trap 'rm -rf "$TMP"' EXIT
26+
27+
cd "$TMP"
28+
mkdir -p app/src
29+
cat > app/mcpp.toml <<'EOF'
30+
[package]
31+
name = "app"
32+
version = "0.1.0"
33+
EOF
34+
cat > app/src/main.cpp <<'EOF'
35+
int main() { return 0; }
36+
EOF
37+
38+
cd app
39+
"$MCPP" build > build.log 2>&1 || { cat build.log; echo "initial build failed"; exit 1; }
40+
41+
CACHE=$(find target -name '.mcpp-runtime-verdicts.json' | head -1)
42+
if [[ -z "$CACHE" ]]; then
43+
echo "SKIP: no glibc runtime binding on this host (no stored verdicts)"
44+
exit 0
45+
fi
46+
47+
# Every artifact must have been validated and recorded as passing, or the rest
48+
# of this test is asserting against the wrong starting state.
49+
python3 - "$CACHE" <<'PY'
50+
import json, sys
51+
doc = json.load(open(sys.argv[1]))
52+
artifacts = doc.get("artifacts") or {}
53+
assert artifacts, "no artifacts recorded in the verdict cache"
54+
bad = {k: v.get("status") for k, v in artifacts.items() if v.get("status") != "pass"}
55+
assert not bad, f"expected every artifact to pass on a clean build, got {bad}"
56+
print(f"stored verdicts: {len(artifacts)} pass")
57+
PY
58+
59+
# ── 1. a stored mismatch survives a no-op re-run ────────────────────
60+
# The fingerprint is left untouched: this is exactly the state a real Rule A/B
61+
# failure leaves behind, and exactly the state the fast path would otherwise
62+
# treat as "nothing to do".
63+
python3 - "$CACHE" <<'PY'
64+
import json, sys
65+
path = sys.argv[1]
66+
doc = json.load(open(path))
67+
for entry in doc["artifacts"].values():
68+
entry["status"] = "proven_mismatch"
69+
entry["diagnostics"] = ["synthetic mismatch recorded by 209"]
70+
json.dump(doc, open(path, "w"), indent=2)
71+
PY
72+
73+
if "$MCPP" build > rerun.log 2>&1; then
74+
cat rerun.log
75+
echo "FAIL: a stored proven mismatch went green on a plain re-run"
76+
exit 1
77+
fi
78+
grep -q "runtime closure validation failed" rerun.log || {
79+
cat rerun.log
80+
echo "FAIL: the re-run failed, but not for the recorded runtime mismatch"
81+
exit 1
82+
}
83+
grep -q "synthetic mismatch recorded by 209" rerun.log || {
84+
cat rerun.log
85+
echo "FAIL: the stored diagnostics were not reported to the user"
86+
exit 1
87+
}
88+
89+
# ── 2. `mcpp self doctor` explains the stored verdict without re-probing ──
90+
doctor_out=$("$MCPP" self doctor 2>&1 || true)
91+
printf '%s\n' "$doctor_out" | grep -qi "mismatch" || {
92+
printf '%s\n' "$doctor_out"
93+
echo "FAIL: doctor did not surface the stored runtime mismatch"
94+
exit 1
95+
}
96+
97+
# ── 3. cached, not permanent ────────────────────────────────────────
98+
# A changed artifact gets a fresh verdict. Otherwise the first real mismatch
99+
# would wedge the project forever and the only cure would be deleting target/.
100+
cat > src/main.cpp <<'EOF'
101+
int main() { return 0; }
102+
// touched so the artifact is relinked and the verdict is re-derived
103+
EOF
104+
"$MCPP" build > recovered.log 2>&1 || {
105+
cat recovered.log
106+
echo "FAIL: a rebuilt artifact must be re-validated, not stay condemned"
107+
exit 1
108+
}
109+
110+
python3 - "$CACHE" <<'PY'
111+
import json, sys
112+
doc = json.load(open(sys.argv[1]))
113+
statuses = {k: v.get("status") for k, v in (doc.get("artifacts") or {}).items()}
114+
assert statuses, "verdict cache lost its artifacts after the rebuild"
115+
assert all(s == "pass" for s in statuses.values()), statuses
116+
print("re-derived verdicts:", statuses)
117+
PY
118+
119+
echo "OK"

0 commit comments

Comments
 (0)