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
121 lines
5.3 KiB
Bash
Executable File
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
|