diff --git a/tests/scripts/defines.sh b/tests/scripts/defines.sh
index 12102393bb..40b0d467c1 100755
--- a/tests/scripts/defines.sh
+++ b/tests/scripts/defines.sh
@@ -66,6 +66,10 @@ case "$SCHEMADIR" in
.*) ABS_SCHEMADIR="$TESTWD/$SCHEMADIR" ;;
*) ABS_SCHEMADIR="$SCHEMADIR" ;;
esac
+case "$SRCDIR" in
+.*) ABS_SRCDIR="$TESTWD/$SRCDIR" ;;
+*) ABS_SRCDIR="$SRCDIR" ;;
+esac
DBDIR1A=$TESTDIR/db.1.a
DBDIR1B=$TESTDIR/db.1.b
@@ -182,6 +186,23 @@ SLURPLOG=$TESTDIR/slurp.log
CONFIGPWF=$TESTDIR/configpw
+# wrappers (valgrind, gdb, environment variables, etc.)
+if [ -n "$WRAPPER" ]; then
+ : # skip
+elif [ "$SLAPD_COMMON_WRAPPER" = gdb ]; then
+ WRAPPER="$ABS_SRCDIR/scripts/grandchild_wrapper.py gdb -nx -x $ABS_SRCDIR/scripts/gdb.py -batch-silent -return-child-result --args"
+elif [ "$SLAPD_COMMON_WRAPPER" = valgrind ]; then
+ WRAPPER="valgrind --log-file=$TESTDIR/valgrind.%p.log --fullpath-after=`dirname $ABS_SRCDIR` --keep-debuginfo=yes --leak-check=full"
+elif [ "$SLAPD_COMMON_WRAPPER" = "valgrind-errstop" ]; then
+ WRAPPER="valgrind --log-file=$TESTDIR/valgrind.%p.log --vgdb=yes --vgdb-error=1"
+elif [ "$SLAPD_COMMON_WRAPPER" = vgdb ]; then
+ WRAPPER="valgrind --log-file=$TESTDIR/valgrind.%p.log --vgdb=yes --vgdb-error=0"
+fi
+
+if [ -n "$WRAPPER" ]; then
+ SLAPD_WRAPPER="$TESTWD/../libtool --mode=execute env $WRAPPER"
+fi
+
# args
SASLARGS="-Q"
TOOLARGS="-x $LDAP_TOOLARGS"
@@ -193,11 +214,11 @@ CONFDIRSYNC=$SRCDIR/scripts/confdirsync.sh
MONITORDATA=$SRCDIR/scripts/monitor_data.sh
-SLAPADD="$TESTWD/../servers/slapd/slapd -Ta -d 0 $LDAP_VERBOSE"
-SLAPCAT="$TESTWD/../servers/slapd/slapd -Tc -d 0 $LDAP_VERBOSE"
-SLAPINDEX="$TESTWD/../servers/slapd/slapd -Ti -d 0 $LDAP_VERBOSE"
-SLAPMODIFY="$TESTWD/../servers/slapd/slapd -Tm -d 0 $LDAP_VERBOSE"
-SLAPPASSWD="$TESTWD/../servers/slapd/slapd -Tpasswd"
+SLAPADD="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -Ta -d 0 $LDAP_VERBOSE"
+SLAPCAT="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -Tc -d 0 $LDAP_VERBOSE"
+SLAPINDEX="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -Ti -d 0 $LDAP_VERBOSE"
+SLAPMODIFY="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -Tm -d 0 $LDAP_VERBOSE"
+SLAPPASSWD="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -Tpasswd"
unset DIFF_OPTIONS
# NOTE: -u/-c is not that portable...
@@ -205,8 +226,8 @@ DIFF="diff -i"
CMP="diff -i"
BCMP="diff -iB"
CMPOUT=/dev/null
-SLAPD="$TESTWD/../servers/slapd/slapd -s0"
-LLOADD="$TESTWD/../servers/lloadd/lloadd -s0"
+SLAPD="$SLAPD_WRAPPER $TESTWD/../servers/slapd/slapd -s0"
+LLOADD="$SLAPD_WRAPPER $TESTWD/../servers/lloadd/lloadd -s0"
LDAPPASSWD="$CLIENTDIR/ldappasswd $TOOLARGS"
LDAPSASLSEARCH="$CLIENTDIR/ldapsearch $SASLARGS $TOOLPROTO $LDAP_TOOLARGS -LLL"
LDAPSASLWHOAMI="$CLIENTDIR/ldapwhoami $SASLARGS $LDAP_TOOLARGS"
diff --git a/tests/scripts/gdb.py b/tests/scripts/gdb.py
new file mode 100644
index 0000000000..d8229dea4c
--- /dev/null
+++ b/tests/scripts/gdb.py
@@ -0,0 +1,85 @@
+# $OpenLDAP$
+## This work is part of OpenLDAP Software .
+##
+## Copyright 2020-2021 The OpenLDAP Foundation.
+## All rights reserved.
+##
+## Redistribution and use in source and binary forms, with or without
+## modification, are permitted only as authorized by the OpenLDAP
+## Public License.
+##
+## A copy of this license is available in the file LICENSE in the
+## top-level directory of the distribution or, alternatively, at
+## .
+"""
+This GDB script sets up the debugger to run the program and see if it finishes
+of its own accord or is terminated by a signal (like SIGABRT/SIGSEGV). In the
+latter case, it saves a full backtrace and core file.
+
+These signals are considered part of normal operation and will not trigger the
+above handling:
+- SIGPIPE: normal in a networked environmnet
+- SIGHUP: normally used to tell a process to shut down
+"""
+
+import os
+import os.path
+
+import gdb
+
+
+def format_program(inferior=None, thread=None):
+ "Format program name and p(t)id"
+
+ if thread:
+ inferior = thread.inferior
+ elif inferior is None:
+ inferior = gdb.selected_inferior()
+
+ try:
+ name = os.path.basename(inferior.progspace.filename)
+ except AttributeError: # inferior has died already
+ name = "unknown"
+
+ if thread:
+ pid = ".".join(tid for tid in thread.ptid if tid)
+ else:
+ pid = inferior.pid
+
+ return "{}.{}".format(name, pid)
+
+
+def stop_handler(event):
+ "Inferior stopped on a signal, record core, backtrace and exit"
+
+ if not isinstance(event, gdb.SignalEvent):
+ # Ignore breakpoints
+ return
+
+ thread = event.inferior_thread
+
+ identifier = format_program(thread=thread)
+ prefix = os.path.expandvars("${TESTDIR}/") + identifier
+
+ if event.stop_signal == "SIGHUP":
+ # TODO: start a timer to catch shutdown issues/deadlocks
+ gdb.execute("continue")
+ return
+
+ gdb.execute('generate-core-file {}.core'.format(prefix))
+
+ with open(prefix + ".backtrace", "w") as bt_file:
+ backtrace = gdb.execute("thread apply all backtrace full",
+ to_string=True)
+ bt_file.write(backtrace)
+
+ gdb.execute("continue")
+
+
+# We or we could allow the runner to disable randomisation
+gdb.execute("set disable-randomization off")
+
+gdb.execute("handle SIGPIPE noprint")
+gdb.execute("handle SIGINT pass")
+gdb.events.stop.connect(stop_handler)
+gdb.execute("run")
diff --git a/tests/scripts/grandchild_wrapper.py b/tests/scripts/grandchild_wrapper.py
new file mode 100755
index 0000000000..700222d9e8
--- /dev/null
+++ b/tests/scripts/grandchild_wrapper.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+# $OpenLDAP$
+## This work is part of OpenLDAP Software .
+##
+## Copyright 2020-2021 The OpenLDAP Foundation.
+## All rights reserved.
+##
+## Redistribution and use in source and binary forms, with or without
+## modification, are permitted only as authorized by the OpenLDAP
+## Public License.
+##
+## A copy of this license is available in the file LICENSE in the
+## top-level directory of the distribution or, alternatively, at
+## .
+"""
+Running slapd under GDB in our testsuite, KILLPIDS would record gdb's PID
+rather than slapd's. When we want the server to shut down, SIGHUP is sent to
+KILLPIDS but GDB cannot handle being signalled directly and the entire thing is
+terminated immediately. There might be tests that rely on slapd being given the
+chance to shut down gracefully, to do this, we need to make sure the signal is
+actually sent to slapd.
+
+This script attempts to address this shortcoming in our test suite, serving as
+the front for gdb/other wrappers, catching SIGHUPs and redirecting them to the
+oldest living grandchild. The way we start up gdb, that process should be
+slapd, our intended target.
+
+This requires the pgrep utility provided by the procps package on Debian
+systems.
+"""
+
+import asyncio
+import os
+import signal
+import sys
+
+
+async def signal_to_grandchild(child):
+ # Get the first child, that should be the one we're after
+ pgrep = await asyncio.create_subprocess_exec(
+ "pgrep", "-o", "--parent", str(child.pid),
+ stdout=asyncio.subprocess.PIPE)
+
+ stdout, _ = await pgrep.communicate()
+ if not stdout:
+ return
+
+ grandchild = [int(pid) for pid in stdout.split()][0]
+
+ os.kill(grandchild, signal.SIGHUP)
+
+
+def sighup_handler(child):
+ asyncio.create_task(signal_to_grandchild(child))
+
+
+async def main(args=None):
+ if args is None:
+ args = sys.argv[1:]
+
+ child = await asyncio.create_subprocess_exec(*args)
+
+ # If we got a SIGHUP before we got the child fully started, there's no
+ # point signalling anyway
+ loop = asyncio.get_running_loop()
+ loop.add_signal_handler(signal.SIGHUP, sighup_handler, child)
+
+ raise SystemExit(await child.wait())
+
+
+if __name__ == '__main__':
+ asyncio.run(main())