Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
117 lines
4.8 KiB
Bash
Executable File
117 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Usage: scripts/check-progression.sh <js|scala|fsharp>
|
|
# Progression checkpoint: for each module N (that has tests in the track), build a temp copy where src/ holds the
|
|
# reference solutions of modules <= N and the untouched student stubs for the rest, then run the tests of modules
|
|
# M01..MN. All must pass: this proves a student who has finished module N has a green suite up to N, and that
|
|
# no earlier solution secretly depends on a later one. The optional module 07b sits between 07 and 08.
|
|
set -uo pipefail
|
|
track="${1:?usage: check-progression.sh <js|scala|fsharp>}"
|
|
repo="$(cd "$(dirname "$0")/.." && pwd)"
|
|
export PATH="$HOME/.dotnet:$HOME/.local/bin:$PATH"
|
|
|
|
# Module -> files (relative to src/ and solutions/) that the module introduces. A directory entry copies it whole.
|
|
files_for() {
|
|
case "$track:$1" in
|
|
js:01) echo "util.js todo.js" ;;
|
|
js:02) echo "model.js" ;;
|
|
js:03) echo "rules.js" ;;
|
|
js:04) echo "random.js spawn.js state.js" ;;
|
|
js:05) echo "game.js" ;;
|
|
js:06) echo "view.js" ;;
|
|
js:07) echo "io.js" ;;
|
|
js:07b) echo "safeio.js" ;; # optional module 7b
|
|
js:08) echo "stream.js" ;;
|
|
js:09) echo "input.js app.js runtime.js browser.js main.js" ;;
|
|
js:10) echo "replay.js" ;; # also edits game/input/stream/app/io/runtime/browser, already included by m05-m09
|
|
js:11) echo "pong" ;;
|
|
js:12) echo "check.js" ;;
|
|
js:13) echo "life" ;;
|
|
scala:01) echo "Util.scala" ;;
|
|
scala:02) echo "Model.scala" ;;
|
|
scala:03) echo "Rules.scala" ;;
|
|
scala:04) echo "Random.scala Spawn.scala State.scala" ;;
|
|
scala:05) echo "Game.scala" ;;
|
|
scala:06) echo "View.scala" ;;
|
|
scala:07) echo "IO.scala" ;;
|
|
scala:07b) echo "SafeIO.scala" ;;
|
|
scala:08) echo "Stream.scala" ;;
|
|
scala:09) echo "Input.scala App.scala Runtime.scala" ;;
|
|
scala:10) echo "Replay.scala" ;;
|
|
scala:11) echo "pong" ;;
|
|
scala:12) echo "Check.scala" ;;
|
|
scala:13) echo "life" ;;
|
|
fsharp:01) echo "Util.fs" ;; # Snake.fsproj lists every file from the start and is identical in both trees
|
|
fsharp:02) echo "Model.fs" ;;
|
|
fsharp:03) echo "Rules.fs" ;;
|
|
fsharp:04) echo "Random.fs Spawn.fs State.fs" ;;
|
|
fsharp:05) echo "Game.fs" ;;
|
|
fsharp:06) echo "View.fs" ;;
|
|
fsharp:07) echo "IO.fs" ;;
|
|
fsharp:07b) echo "SafeIO.fs" ;;
|
|
fsharp:08) echo "Stream.fs" ;;
|
|
fsharp:09) echo "Input.fs App.fs Runtime.fs" ;;
|
|
fsharp:10) echo "Replay.fs" ;;
|
|
fsharp:11) echo "Pong" ;;
|
|
fsharp:12) echo "Check.fs" ;;
|
|
fsharp:13) echo "Life" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
case "$track" in
|
|
js) has() { compgen -G "$repo/js/test/m$1-*.test.js" >/dev/null; } ;;
|
|
scala) has() { compgen -G "$repo/scala/test/M$1*.test.scala" >/dev/null; } ;;
|
|
fsharp) has() { compgen -G "$repo/fsharp/tests/M$1*.fs" >/dev/null; } ;;
|
|
*) echo "unknown track: $track" >&2; exit 2 ;;
|
|
esac
|
|
|
|
# Modules in course order. "07b" (optional) comes after 07: order is position in this list, not numeric value.
|
|
modules="01 02 03 04 05 06 07 07b 08 09 10 11 12 13"
|
|
|
|
# Scala's `./test.sh M07` (munit glob "*M07*") and F#'s (Expecto --filter-test-list, a substring) also select the
|
|
# M07b tests, unlike JS's "m07-*" glob. Before module 07b is reached its tests must not run, so delete them from
|
|
# the temp copy (and, for F#, their line in tests/Snake.Tests.fsproj; Expecto finds test lists by attribute).
|
|
drop_tests() {
|
|
case "$track" in
|
|
scala) rm -f "$tmp"/scala/test/M"$1"*.test.scala ;;
|
|
fsharp) for f in "$tmp"/fsharp/tests/M"$1"*.fs; do
|
|
[ -e "$f" ] || continue
|
|
rm -f "$f"; sed -i "/Include=\"$(basename "$f")\"/d" "$tmp/fsharp/tests/Snake.Tests.fsproj"
|
|
done ;;
|
|
esac
|
|
}
|
|
|
|
status=0
|
|
for n in $modules; do
|
|
has "$n" || continue
|
|
tmp="$(mktemp -d)"
|
|
tar -C "$repo" --exclude=.git --exclude=node_modules --exclude=bin --exclude=obj \
|
|
--exclude=.scala-build --exclude=.bsp --exclude=.superpowers -cf - . | tar -C "$tmp" -xf -
|
|
reached=""; past=0
|
|
for m in $modules; do
|
|
if [ $past -eq 1 ]; then # a later module: drop its tests if a reached module's tag is a prefix of its tag
|
|
for r in $reached; do [[ "$m" == "$r"?* ]] && drop_tests "$m"; done
|
|
continue
|
|
fi
|
|
for f in $(files_for "$m"); do
|
|
[ -e "$tmp/$track/solutions/$f" ] || continue # module not (yet) present in this track
|
|
rm -rf "$tmp/$track/src/$f"
|
|
cp -r "$tmp/$track/solutions/$f" "$tmp/$track/src/$f"
|
|
done
|
|
reached="$reached $m"
|
|
[ "$m" = "$n" ] && past=1
|
|
done
|
|
ok=1
|
|
for m in $reached; do
|
|
has "$m" || continue
|
|
if ! out="$(cd "$tmp/$track" && ./test.sh "M$m" 2>&1)"; then
|
|
echo "checkpoint M$n: FAIL (M$m tests fail with solutions up to M$n)"
|
|
echo "$out" | tail -40
|
|
ok=0; status=1; break
|
|
fi
|
|
done
|
|
[ $ok -eq 1 ] && echo "checkpoint M$n: ok (M01..M$n pass)"
|
|
rm -rf "$tmp"
|
|
done
|
|
exit $status
|