#!/bin/bash # # Run the existing CTest suites under valgrind and report what leaked. # # scripts/memcheck.sh check every suite in ./build # scripts/memcheck.sh -R tilemap check only the suites matching a regex # scripts/memcheck.sh -LE perf skip the perf suites # # Anything after the build directory is handed straight to ctest, so every # selection flag ctest understands works here. # # Environment: # AKGL_BUILD_DIR build tree to check (default: build) # AKGL_MEMCHECK_LOG directory for the per-suite valgrind logs # (default: $AKGL_BUILD_DIR/Testing/Temporary) # # Exit status: 0 when valgrind found nothing, 1 when it found something, 2 on a # usage or environment error. This is the part `ctest -T memcheck` will not do # on its own -- it records defects and still exits 0, which is no use as a gate. set -eu AKGL_BUILD_DIR="${AKGL_BUILD_DIR:-build}" function memcheck_die() { echo "memcheck: $*" >&2 exit 2 } # The suite runs headless on purpose. Loading the real GPU stack costs thousands # of unfixable findings inside the vendor driver, and none of them are libakgl's. # A suite that sets these hints itself is unaffected; the ones that do not -- # tests/tilemap.c and tests/sprite.c -- pick them up from here. export SDL_VIDEO_DRIVER="${SDL_VIDEO_DRIVER:-dummy}" export SDL_RENDER_DRIVER="${SDL_RENDER_DRIVER:-software}" export SDL_AUDIO_DRIVER="${SDL_AUDIO_DRIVER:-dummy}" command -v valgrind >/dev/null 2>&1 || memcheck_die "valgrind not found" command -v ctest >/dev/null 2>&1 || memcheck_die "ctest not found" [ -f "$AKGL_BUILD_DIR/CMakeCache.txt" ] || memcheck_die "$AKGL_BUILD_DIR is not a configured build tree" AKGL_MEMCHECK_LOG="${AKGL_MEMCHECK_LOG:-$AKGL_BUILD_DIR/Testing/Temporary}" # Old logs would be counted as this run's findings. CTest numbers them by test # id, so a run narrowed with -R leaves the others behind. rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log # The run itself is allowed to fail: a suite that valgrind makes slow enough to # trip an assertion still produced a log worth reading, and the log is what # decides the exit status below. set +e ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure "$@" AKGL_CTEST_STATUS=$? set -e # What counts as a finding. "Definitely lost" is a leak nobody can argue with; # the invalid-access lines are memcheck telling us the library read or wrote # outside an allocation, which is worse than a leak and easier to fix. # # "are definitely lost", not "definitely lost": the LEAK SUMMARY block ends every # clean log with "definitely lost: 0 bytes in 0 blocks", and matching that # reported a finding in all twenty-three suites, including the ones that never # call malloc. AKGL_MEMCHECK_PATTERN='are definitely lost|Invalid read|Invalid write|Invalid free|Mismatched free|Source and destination overlap|depends on uninitialised|Use of uninitialised' total=0 echo echo "== valgrind findings by suite ==" for log in "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log; do [ -f "$log" ] || continue # CTest writes the test name into the log's first line as the command it ran. suite=$(grep -m1 -oE 'akgl_test_[a-z_]+' "$log" || basename "$log") count=$(grep -cE "$AKGL_MEMCHECK_PATTERN" "$log" || true) total=$((total + count)) if [ "$count" -gt 0 ]; then printf '%-24s %3d finding(s) %s\n' "$suite" "$count" "$log" fi done if [ "$total" -eq 0 ]; then echo "none" echo # A clean valgrind run and a failing test are different things: a suite can # assert its way to a non-zero exit without leaking a byte, and saying # "clean" and returning 0 for that would hide it. if [ "$AKGL_CTEST_STATUS" -ne 0 ]; then echo "memcheck: valgrind found nothing, but ctest exited $AKGL_CTEST_STATUS -- a suite failed on its own terms" >&2 exit 1 fi echo "memcheck: clean ($AKGL_MEMCHECK_LOG)" exit 0 fi echo echo "== library frames in those stacks, most frequent first ==" # Keyed on the function name rather than the file, because src/physics.c and # tests/physics.c are both "physics.c" in a valgrind frame and only one of them # is the library. Anything a caller can reach is an akgl_ symbol by convention, # which makes the prefix a usable filter; a static helper appears under whatever # akgl_ function called it, one frame further down. grep -hoE 'by 0x[0-9A-Fa-f]+: akgl_[A-Za-z0-9_]+ \([a-z_]+\.c:[0-9]+\)' \ "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log 2>/dev/null | sed -E 's/^by 0x[0-9A-Fa-f]+: //' | sort | uniq -c | sort -rn | head -20 echo echo "memcheck: $total finding(s); full stacks in $AKGL_MEMCHECK_LOG" >&2 exit 1