109 lines
4.5 KiB
Bash
109 lines
4.5 KiB
Bash
|
|
#!/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
|