#!/usr/bin/env bash # # pre-push: build libakstdlib and run the test harnesses before anything leaves # this machine. # # Install (once per clone): # # git config core.hooksPath .githooks # # Runs by default, a few seconds all in: # # * default build + ctest # * ASan/UBSan build + ctest # # Opt in to the slow harness (~25 minutes -- it rebuilds and re-runs the whole # suite once per mutant): # # AKSL_HOOK_MUTATION=1 git push # # Bypass everything (git's own escape hatch): # # git push --no-verify # # Builds go under .git/aksl-prepush so the hook never disturbs whatever is in # your own build/ directory. Override with AKSL_HOOK_BUILD_DIR if you want them # somewhere else. set -u # Keep in sync with the --threshold in .gitea/workflows/ci.yaml, so a push that # would fail CI fails here first. MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-40}" ZERO_SHA=0000000000000000000000000000000000000000 root=$(git rev-parse --show-toplevel) || exit 1 cd "$root" || exit 1 # git feeds us one line per ref being pushed. A push that only *deletes* refs # has no commits to test, so there is nothing to do. An empty stdin (nothing to # push) lands here too, which is equally fine to skip. has_updates=0 while read -r _local_ref local_sha _remote_ref _remote_sha; do if [ "$local_sha" != "$ZERO_SHA" ]; then has_updates=1 fi done if [ "$has_updates" -eq 0 ]; then exit 0 fi if [ ! -f deps/libakerror/CMakeLists.txt ]; then echo "pre-push: deps/libakerror is empty. Run:" >&2 echo " git submodule update --init --recursive" >&2 exit 1 fi builddir="${AKSL_HOOK_BUILD_DIR:-$(git rev-parse --git-dir)/aksl-prepush}" mkdir -p "$builddir" || exit 1 logfile="$builddir/last.log" # Run a step quietly; on failure, dump what it said and abort the push. run() { if ! "$@" > "$logfile" 2>&1; then echo >&2 echo "pre-push: FAILED: $*" >&2 echo "---------------------------------------------------------------" >&2 cat "$logfile" >&2 echo "---------------------------------------------------------------" >&2 echo "pre-push: push aborted. Use 'git push --no-verify' to override." >&2 exit 1 fi } echo "pre-push: default build + ctest" run cmake -S . -B "$builddir/default" run cmake --build "$builddir/default" run ctest --test-dir "$builddir/default" --output-on-failure echo "pre-push: sanitizer build + ctest" run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON run cmake --build "$builddir/asan" run ctest --test-dir "$builddir/asan" --output-on-failure if [ "${AKSL_HOOK_MUTATION:-0}" = "1" ]; then echo "pre-push: mutation testing, threshold ${MUTATION_THRESHOLD}% (this takes a while)" # Deliberately not wrapped in run(): this one is slow enough that you want # to watch it make progress. if ! python3 scripts/mutation_test.py \ --target src/stdlib.c \ --threshold "$MUTATION_THRESHOLD"; then echo >&2 echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2 echo "pre-push: use 'git push --no-verify' to override." >&2 exit 1 fi fi echo "pre-push: OK" exit 0