aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/scripts/mutate.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/mutate.sh')
-rwxr-xr-xscripts/mutate.sh52
1 files changed, 52 insertions, 0 deletions
diff --git a/scripts/mutate.sh b/scripts/mutate.sh
new file mode 100755
index 0000000..e3132de
--- /dev/null
+++ b/scripts/mutate.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# Mutation check: prove a test can fail. Replaces an exact snippet in one
+# source file (must match exactly once), runs the tests, and ALWAYS restores
+# the file. Exit 0 = mutant killed (some test failed, as it should); 1 =
+# mutant SURVIVED (tests too weak); 3 = mutant doesn't build (invalid —
+# replace a statement with `{ }` instead of deleting a loop body line).
+#
+# Usage (from the repo whose tests must catch it — a game root, or this lib's root):
+# <path>/mutate.sh <file> <old-snippet> <new-snippet>
+# e.g. scripts/mutate.sh src/MyGame.Core/Loop/InputLatch.cs \
+# '_jump |= sample.JumpPressed;' '_jump = sample.JumpPressed;'
+# <path>/mutate.sh <file> <old> <new> -- <test command...>
+# runs that command instead of the whole `dotnet test` (a filtered dotnet test, or any other runner: python
+# unittest, ...); a dotnet command keeps the build / [FAIL] parsing, any other is killed by a non-zero exit.
+set -uo pipefail
+
+file=$1 old=$2 new=$3
+shift 3
+cmd=(dotnet test --nologo)
+if [[ ${1:-} == -- ]]; then
+ shift
+ cmd=("$@")
+fi
+backup=$(mktemp)
+cp "$file" "$backup"
+trap 'cp "$backup" "$file"; rm -f "$backup"' EXIT
+
+python3 - "$file" "$old" "$new" <<'EOF' || exit 2
+import sys
+path, old, new = sys.argv[1:]
+text = open(path, encoding="utf-8").read()
+n = text.count(old)
+if n != 1:
+ sys.exit(f"snippet matches {n} times in {path} (need exactly 1)")
+open(path, "w", encoding="utf-8").write(text.replace(old, new))
+EOF
+
+result=$("${cmd[@]}" 2>&1)
+status=$?
+if [[ ${cmd[0]} != dotnet ]]; then
+ if (( status != 0 )); then
+ echo "KILLED (exit $status):"; tail -3 <<<"$result"; exit 0
+ fi
+ echo "SURVIVED: no test failed — strengthen the tests"; exit 1
+fi
+if grep -q "error CS" <<<"$result"; then
+ echo "INVALID: mutant doesn't build"; grep -m3 "error CS" <<<"$result"; exit 3
+fi
+if grep -q "Failed!" <<<"$result"; then
+ echo "KILLED by:"; grep "\[FAIL\]" <<<"$result" | sed -E 's/^\[xUnit[^]]*\] +//; s/ \[FAIL\]$//' | sort -u; exit 0
+fi
+echo "SURVIVED: no test failed — strengthen the tests"; exit 1