#!/usr/bin/env bash # Usage: scripts/check-progression.sh # 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 }" 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