carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1#!/bin/bash
2# Mutation checks for the scheduler's invariants (zero-downtime plan 5, task
3# 16). Each mutant rewrites one marked line (// rolloutsim:Mn) in a scratch
4# copy of the committed tree (git archive HEAD), never in the checkout, and
5# runs TestSimInvariants against it. A clean baseline run of the unmutated
6# copy comes first: a kill means nothing if the simulation fails anyway.
7#
8# A mutant whose invariant the simulation cannot reach names a unit test
9# instead (its third field "test:TestName"; zero-downtime flag readiness,
10# task 16 rule 2). Its run is that one test (-run '^TestName$'), and the
11# test gets a clean baseline run of its own on the unmutated copy too. Such
12# a mutant is the exception: each says in the list below why the simulation
13# cannot catch it.
14#
15# internal/rollout/testdata/rolloutsim/mutate.sh # 200 seeds
16# internal/rollout/testdata/rolloutsim/mutate.sh 300 # 300 seeds
17# internal/rollout/testdata/rolloutsim/mutate.sh 200 M3 # only M3
18#
19# The script runs at nice 19 (it re-executes itself under nice if it has to,
20# and warns if it cannot) and one run at a time with GOMAXPROCS=2 (the CPU
21# budget on shared boxes). Each go test run's timeout grows with the seeds,
22# SEEDS * 3 / 100 + 20 minutes (3000 seeds: 110 minutes); the header prints
23# it. A fixed 20 minutes was too short for 3000 seeds (round 1).
24#
25# The simulation's invariants are I1 to I10 (TestSimInvariants' doc). I9
26# and I10 run only on the seeds whose failure book is on disk, which the
27# simulation turns off when it runs as root (root writes into a read-only
28# directory, so it cannot fail a save): as root, M13 to M17 read MISSED.
29#
30# Execution integrity is recorded apart from detection. A run must build,
31# start, and end in its own --- PASS or --- FAIL line for the test it ran
32# (TestSimInvariants, or a unit-test mutant's own test), with no panic, no
33# timeout and no "rolloutsim: crash at seed" line (a panic of the simulation
34# or of the code under test, which the test recovers and reports apart from
35# violations). A run that breaks integrity fails the mutant's gate even when
36# it also reported the expected violation; that kill is still printed, as a
37# diagnostic.
38#
39# Verdicts:
40# CAUGHT the run executed intact and reported a violation of the
41# mutant's own invariant ("rolloutsim: I2 violated at seed",
42# any of I1 to I10), or, for a unit-test mutant, its own test
43# ended in --- FAIL
44# BROKEN the run panicked, timed out, crashed or never ran
45# MISSED the run passed, or reported only other invariants' violations
46# ERROR the marker is not on exactly one line, the substitution did not
47# apply, or the copy did not build
48# Each baseline (the simulation's, and one per unit test a selected mutant
49# names) is PASSED or, like a mutant, BROKEN or FAILED. The simulation's
50# baseline runs only when a selected mutant runs the simulation. Exit 1 on
51# anything but a PASSED baseline and CAUGHT for every mutant.
52#
53# A mutant named on the command line that is not in the list below is an
54# ERROR, reported before anything runs, and the script exits 1: a typo must
55# never pass as a selection that checked nothing.
56#
57# Scratch: one fresh directory per run, made with mktemp -d under $TMPDIR
58# (named zdd5-mut.XXXXXX) and printed first. When the run ends with a PASSED
59# baseline and every selected mutant CAUGHT, the whole directory is removed
60# on exit. On any other outcome (a failure, an interrupted run) only its tree
61# copy is removed, and its path is printed again: the logs stay there to be
62# read, and removed by hand after. The script never touches any other path.
63set -u
64if [ "$(nice)" -lt 19 ] && [ -z "${ROLLOUTSIM_RENICED:-}" ]; then
65 ROLLOUTSIM_RENICED=1 exec nice -n 19 "$0" "$@"
66fi
67if [ "$(nice)" -lt 19 ]; then
68 echo "warning: running at niceness $(nice), not 19" >&2
69fi
70SEEDS=${1:-200}
71shift $(($# > 0 ? 1 : 0))
72ONLY=" $* "
73# Each go test run's timeout, scaled with the seeds (3000 seeds: 110m).
74TIMEOUT_MIN=$((SEEDS * 3 / 100 + 20))
75
76# name|file|check|perl substitution applied to the marked line only. check
77# is the simulation invariant that must catch the mutant (I1 to I10), or
78# test:TestName for a unit-test mutant. The marker match is anchored (\b),
79# so rolloutsim:M1 never matches rolloutsim:M10. The substitution is the
80# last field, so it may contain "|".
81#
82# M7 to M12 are zero-downtime flag readiness task 16's:
83# M7 decideHoldRan's Blocks check on the marked build (Astra R3 N7): I1.
84# M8 ClearHoldRanBefore's sequence test (Astra R3 N6): I5, at a good
85# write delivered late, after a hold adoption failed on the host.
86# M9 preRetryLocked never reports a failure stale (ruling 191): I7.
87# M10 a Begin whose save failed runs the attempt anyway (F5): I8, on a
88# seed whose book is on disk, across a read-only window.
89# M11 failCohort halts the cohort on a PreRetry failure (plan review
90# finding 3): I7's cohort check.
91# M12 markLocked keeps the marker's Seq for another execution of the
92# same build (plan review finding 4). A unit test, by rule 2: the
93# simulation cannot reach it. The kill needs a good write whose
94# execution began while a marker of build X stood, then a second
95# execution of X marking the host before that write lands. A wake
96# planned under X's marker boots X as a first wake, whose intent
97# refuses every adoption of the host until it settles, so no second
98# run of X can start inside that window; only an ordinary wake of a
99# build the host served before, under another build's marker, leaves
100# it open, and that needs X installed again and adopted on a hold.
101#
102# M13 to M17 are zero-downtime flag readiness round 2 task 2's (the
103# crash-epoch hard limit), each on a seed whose book is on disk:
104# M13 PublishedEpoch ignores its dirty check: I10. A change made while
105# saves fail moves the generation in memory only; status publishes
106# it, and the last landed file holds another.
107# M14 markLocked's marker set, renewed or replaced moves no generation:
108# I9. A grant issued under X's hold-ran marker, then another hold run
109# of X on the host (a first wake under the marker, stopped before it
110# settles: the simulation's rewakeArm and rewakeRun) renews the marker
111# at the same generation, and the grant, delivered after, clears the
112# newer run's marker. (The plan's witness, X's marker replaced by a
113# failed hold run of Y and Y's marker then cleared by a pointer,
114# catches nothing: the pointer's clear, ClearHoldRanOnPointer, moves
115# the generation on its own line.)
116# M15 the reopen's seq keeps only the file's own seq, halved: I9. The
117# max over the saved generations and markers (highestSaved), which
118# would repair a lowered seq, is not taken, so the reopened book
119# hands out again generations it had published (each restart halves
120# it again), and a grant issued before a restart, still pending,
121# matches a restriction that a failure after it made, typically an
122# intent the restart reconciled.
123# M16 RetryHostAt skips the book-identity comparison: I9. A box rebuild
124# opens a new book whose generations start again from one, so a
125# grant for the old book, still pending, matches a restriction the
126# new one made. One of the two rarest kills (with M17): at 300 seeds
127# it rests on a few seeds (rebuilds are drawn 1 step in 150). So a
128# MISSED for M16 at 300 seeds or fewer is not yet a regression: run
129# it again at 3000 seeds (mutate.sh 3000 M16) before reading it as
130# one.
131# M17 a new record moves the app's generation only from zero (the
132# sequence still advances, so nothing else changes): I9. A new
133# record holds every host of the app (spec §8.4), so a grant issued
134# under X's hold-ran marker on h, then a failure of X on another
135# host that writes X's new record, then the grant delivered: it
136# clears the marker and exempts h from the new record. I9 counts a
137# new record as a fact of every host. The random schedule seldom
138# lines this up (a build with a marker and no record has seldom
139# failed anywhere), so the simulation's rewakeArm makes it: when X
140# has no record, the wake after the Retry is another instance's
141# first wake of X, made to fail. Like M16 it rests on
142# few seeds at 300, and the same rule holds: a MISSED at 300 seeds
143# or fewer is re-run at 3000 before it is read as a regression.
144# (Removing the touch outright leaves the generation zero when the
145# new record is the app's first restriction, so every retry is
146# refused and I7 catches it first, which is not the hole this
147# mutant is for.)
148MUTANTS='M1|queue.go|I2|s/if q\.running >= q\.concurrency \{/if false \&\& q.running >= q.concurrency {/
149M2|queue.go|I3|s/class := classRecover/class := classAdopt/
150M3|queue.go|I1|s/res, err = book\.Record\(rec\)/res, err = Recorded, nil/
151M4|queue.go|I4|s/h\.buildRev = q\.nextRev\(\)/_ = q.nextRev()/
152M5|wakeplan.go|I1|s/if hasGood \&\& in\.Book\.Blocks\(/if false \&\& in.Book.Blocks(/
153M6|wakeplan.go|I1|s/plan\.Fallback = \&good/plan.Fallback = nil/
154M7|wakeplan.go|I1|s/if hasGood \&\& in\.Book\.Blocks\(/if false \&\& hasGood \&\& in.Book.Blocks(/
155M8|failures.go|I5|s/cur\.Seq >= snap \{/cur.Seq > ^uint64(0) {/
156M9|failures.go|I7|s/if r\.Seq == 0 \{/if true {/
157M10|scheduler.go|I8|s/; err != nil \{/; errors.Is(err, ErrIntentStands) {/
158M11|queue.go|I7|s/if res == PreRetry \{/if false \&\& res == PreRetry {/
159M12|failures.go|test:TestAnotherExecutionOfTheMarkedBuildRenewsTheMarker|s/h\.Seq = b\.bumpLocked\(\)/if ok \&\& cur.Build == h.Build { h.Seq = cur.Seq } else { h.Seq = b.bumpLocked() }/
160M13|failures.go|I10|s/if b\.dirty \{/if false \&\& b.dirty {/
161M14|failures.go|I9|s/b\.touchLocked\(h\.Account, h\.App\)//
162M15|failures.go|I9|s/b\.seq = f\.highestSaved\(\)/b.seq = f.Seq \/ 2/
163M16|failures.go|I9|s/ \|\| book != b\.idLocked\(\)//
164M17|failures.go|I9|s/b\.touchLocked\(r\.Account, r\.App\)/if b.gens[genKey(r.Account, r.App)] == 0 { b.touchLocked(r.Account, r.App) } else { b.bumpLocked() }/'
165
166# Every name selected must be a mutant in the list, checked before any work.
167KNOWN=" $(printf '%s\n' "$MUTANTS" | cut -d'|' -f1 | tr '\n' ' ')"
168unknown=0
169for name in "$@"; do
170 if [[ "$KNOWN" != *" $name "* ]]; then
171 echo "ERROR $name: no such mutant (known:${KNOWN% })"
172 unknown=1
173 fi
174done
175[ $unknown -eq 0 ] || exit 1
176
177export GOMAXPROCS=2
178ROOT=$(git -C "$(dirname "$0")" rev-parse --show-toplevel) || exit 2
179SCRATCH=$(mktemp -d "${TMPDIR:?}/zdd5-mut.XXXXXX") || exit 2
180echo "scratch: $SCRATCH"
181# fail is the run's verdict so far; complete is set only once every
182# selected mutant has a verdict. The exit trap removes the scratch
183# directory whole only when both say the run passed.
184fail=0
185complete=0
186cleanup() {
187 if [ $complete -eq 1 ] && [ $fail -eq 0 ]; then
188 rm -rf "$SCRATCH"
189 else
190 rm -rf "$SCRATCH/tree" "$SCRATCH/orig" "$SCRATCH/gowork"
191 echo "scratch kept: $SCRATCH (logs in $SCRATCH/logs; remove it after reading)"
192 fi
193}
194trap cleanup EXIT
195P=internal/rollout
196# The default build cache is read-only inside a sandbox.
197if [ -z "${GOCACHE:-}" ] && [ ! -w "$(go env GOCACHE)" ]; then
198 export GOCACHE=$TMPDIR/gocache
199fi
200# An archive has no .git, so VCS stamping must be off.
201export GOFLAGS="${GOFLAGS:-} -buildvcs=false"
202mkdir -p "$SCRATCH/tree" "$SCRATCH/orig" "$SCRATCH/logs" "$SCRATCH/gowork" || exit 2
203export GOTMPDIR=$SCRATCH/gowork
204git -C "$ROOT" archive HEAD | tar -x -C "$SCRATCH/tree" || exit 2
205echo "tree: $(git -C "$ROOT" rev-parse --short HEAD)"
206echo "seeds: $SEEDS; timeout per run: ${TIMEOUT_MIN}m"
207cp -a "$SCRATCH/tree/$P/." "$SCRATCH/orig/" || exit 2
208
209# testOf CHECK: the test a mutant's run executes.
210testOf() {
211 case "$1" in
212 test:*) echo "${1#test:}" ;;
213 *) echo TestSimInvariants ;;
214 esac
215}
216
217# run LOG TEST: one go test call of TEST against the scratch tree, verbose
218# so that the test prints its --- PASS or --- FAIL line.
219run() {
220 (cd "$SCRATCH/tree" && ROLLOUT_SIM_SEEDS="$SEEDS" go test -v -p 1 -count=1 -timeout "${TIMEOUT_MIN}m" \
221 -run "^$2\$" ./$P/) >"$1" 2>&1
222}
223
224# integrity LOG TEST: empty when the run of TEST executed intact, else what
225# broke it.
226integrity() {
227 if grep -q '\[build failed\]\|\[setup failed\]' "$1"; then
228 echo BUILD
229 elif grep -q '^panic: test timed out' "$1"; then
230 echo TIMEOUT
231 elif grep -qE '^(panic: |fatal error: )' "$1"; then
232 echo PANIC
233 elif grep -q 'rolloutsim: crash at seed' "$1"; then
234 echo CRASH
235 elif ! grep -qE -- "--- (PASS|FAIL): $2 \(" "$1"; then
236 echo NOT-RUN
237 fi
238}
239
240# found LOG: the invariants the run reported violated, e.g. "I1 I3".
241found() {
242 grep -oE 'rolloutsim: I[0-9]+ violated at seed' "$1" | awk '{print $2}' | sort -u | tr '\n' ' ' | sed 's/ $//'
243}
244
245selected() {
246 [ "$ONLY" = " " ] || [[ "$ONLY" == *" $1 "* ]]
247}
248
249# The baselines: the simulation's when a selected mutant runs it, and each
250# unit test a selected mutant names, once.
251tests=$(while IFS='|' read -r name _ check _; do
252 selected "$name" && testOf "$check"
253done <<<"$MUTANTS" | awk '!seen[$0]++')
254for t in $tests; do
255 blog="$SCRATCH/logs/baseline-$t.log"
256 what="$t"
257 [ "$t" = TestSimInvariants ] && what="$SEEDS seeds"
258 run "$blog" "$t"
259 broke=$(integrity "$blog" "$t")
260 if [ -n "$broke" ]; then
261 echo "BROKEN baseline: $t: $broke; see $blog"
262 fail=1
263 elif grep -qF -- "--- FAIL: $t (" "$blog"; then
264 echo "FAILED baseline: $t failed on the unmutated tree (violations: $(found "$blog")); see $blog"
265 fail=1
266 else
267 echo "PASSED baseline: $what"
268 fi
269done
270
271while IFS='|' read -r name file inv sub; do
272 if ! selected "$name"; then
273 continue
274 fi
275 test=$(testOf "$inv")
276 marker="rolloutsim:$name"
277 src="$SCRATCH/tree/$P/$file"
278 # Each marker sits on exactly one line of the package's Go sources.
279 lines=$(cat "$SCRATCH/orig"/*.go | grep -cE "$marker\\b")
280 if [ "$lines" -ne 1 ]; then
281 echo "ERROR $name: $marker is on $lines lines of $P/*.go, not one"
282 fail=1
283 continue
284 fi
285 cp "$SCRATCH/orig/$file" "$src"
286 perl -pi -e "if (/\\Q$marker\\E\\b/) { $sub }" "$src"
287 if cmp -s "$SCRATCH/orig/$file" "$src"; then
288 echo "ERROR $name: the substitution did not apply to $file"
289 fail=1
290 continue
291 fi
292 mutated=$(grep -E "$marker\\b" "$src" | sed 's/^[[:space:]]*//')
293 log="$SCRATCH/logs/$name.log"
294 run "$log" "$test"
295 cp "$SCRATCH/orig/$file" "$src"
296 broke=$(integrity "$log" "$test")
297 caught=0
298 if [ "$test" = TestSimInvariants ]; then
299 got=$(found "$log")
300 [[ " $got " == *" $inv "* ]] && caught=1
301 detail="expects $inv; reported: ${got:-none}; line: $mutated"
302 else
303 got=passed
304 grep -qF -- "--- FAIL: $test (" "$log" && got=failed caught=1
305 detail="expects $test to fail; it $got; line: $mutated"
306 fi
307 if [ "$broke" = BUILD ]; then
308 echo "ERROR $name: the mutant did not build; $detail; see $log"
309 fail=1
310 elif [ -n "$broke" ]; then
311 kill="not caught"
312 [ $caught -eq 1 ] && kill="caught (diagnostic only)"
313 echo "BROKEN $name: execution integrity failed: $broke; $kill; $detail; see $log"
314 fail=1
315 elif [ $caught -eq 1 ]; then
316 echo "CAUGHT $name: $detail"
317 else
318 echo "MISSED $name: $detail"
319 fail=1
320 fi
321done <<<"$MUTANTS"
322complete=1
323exit $fail
324