Files
libakgl/scripts/memcheck.sh
Andrew Kesterson 5560edf410
All checks were successful
libakgl CI Build / cmake_build (push) Successful in 8m54s
libakgl CI Build / performance (push) Successful in 9m48s
libakgl CI Build / memory_check (push) Successful in 14m39s
libakgl CI Build / mutation_test (push) Successful in 27m24s
Exclude the two shell-script tests from the memcheck run
The memory job's first-ever complete valgrind pass on the runner reported
every real suite and all three example games clean -- and 414 findings in
docs_examples and docs_screenshots. Those two tests are bash scripts that
drive gcc and run the compiled snippets as children; valgrind wraps the test
command and does not trace children, so all 414 were /bin/bash's own
by-design leaks and not one byte of libakgl. Memchecking them proves nothing
the suites and example games do not already prove as real binaries under the
same valgrind run.

memcheck.sh now excludes exactly those two by name, with the reasoning in a
comment beside the exclusion -- per the suppressions file's own rule that
nothing gets quieted without a stated reason. Verified locally: full run over
the RelWithDebInfo tree, every suite and example under valgrind, zero
findings, exit 0.

Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 15:12:39 -04:00

121 lines
5.3 KiB
Bash
Executable File

#!/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.
#
# docs_examples and docs_screenshots are excluded, and the exclusion deserves
# its reasons stated: those two tests are *shell scripts* that drive gcc and
# run the compiled snippets as child processes. Valgrind wraps the test
# command, does not trace children, and so spends the whole test memchecking
# /bin/bash -- 413 of bash's own by-design leaks on the first CI run that got
# this far, and not one byte of libakgl. The snippet programs themselves are
# ordinary akgl consumers whose code paths the suites and example games
# already cover below, under valgrind, as real binaries. A caller's own -E
# overrides this one (ctest takes the last), which is fine: a narrowed run
# chose its scope on purpose.
set +e
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure \
-E '^(docs_examples|docs_screenshots)$' "$@"
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