build: add gcov coverage and --disable-openat2 knobs for the test suite

Two test-coverage build knobs (both behaviour-neutral by default):

  --enable-coverage  appends '--coverage -fprofile-update=atomic -O0' and adds
                     a 'make coverage' target (whole suite, run serially, then
                     gcovr HTML with branch + decision coverage). rsync forks
                     and its children exit without running the gcov atexit
                     flush -- the generator via its SIGUSR1 handler
                     (_exit_cleanup) and the receiver via the SIGUSR2 handler
                     -- so under GCOV_COVERAGE we call __gcov_dump() at both, or
                     receiver.c/generator.c record no coverage at all.

  --disable-openat2  gates the Linux openat2(RESOLVE_BENEATH) sites in syscall.c
                     on HAVE_OPENAT2 (defined by default), so disabling it forces
                     the portable per-component O_NOFOLLOW resolver to run as the
                     primary on ordinary Linux -- exercising and
                     coverage-counting that fallback tier without a pre-5.6
                     kernel. NOTE: coordinate with the parallel syscall.c
                     path-resolution restructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Tridgell
2026-05-24 08:12:39 +10:00
parent b0ba699031
commit 1d5b5ab83a
5 changed files with 80 additions and 5 deletions

View File

@@ -280,6 +280,8 @@ clean: cleantests
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
git-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \
*.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp
rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda
rm -rf coverage
.PHONY: cleantests
cleantests:
@@ -324,6 +326,15 @@ test: check
# `make check CHECK_J=1` (serial) or any other value.
CHECK_J = 8
# Parallelism for `make coverage`. Defaults to the same as CHECK_J: the
# coverage build sets -fprofile-update=atomic (atomic in-memory counters) and
# gcc's libgcov serializes the per-source .gcda read-modify-write merge with a
# file lock, so concurrent rsync processes (incl. the forked sender/generator/
# receiver) accumulate exactly -- verified by a count-linearity check (a hot
# line accumulates identically at -j1 and -P16). Override with
# `make coverage COVERAGE_J=1` if your libgcov does not lock .gcda merges.
COVERAGE_J = $(CHECK_J)
.PHONY: check
check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(CHECK_J)
@@ -336,6 +347,29 @@ check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(CHECK_J) --protocol=30
# Whole-suite gcov coverage report (HTML, with branch + decision coverage).
# Requires a build configured with --enable-coverage and the `gcovr` tool
# (pip install gcovr). Runs the suite in parallel (COVERAGE_J, default CHECK_J):
# this is safe because the coverage build uses -fprofile-update=atomic and
# libgcov locks the per-source .gcda during its merge, so concurrent rsync
# processes accumulate exactly (see COVERAGE_J above). Use COVERAGE_J=1 if your
# toolchain's libgcov does not lock .gcda merges.
.PHONY: coverage
coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
@case '$(CFLAGS)' in *--coverage*) ;; \
*) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac
@command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; }
find . -name '*.gcda' -delete
@rc=0; $(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT) -j $(COVERAGE_J) || rc=$$?; \
rm -rf coverage && mkdir -p coverage; \
gcovr --root $(srcdir) --branches --decisions --print-summary \
--html-details -o coverage/index.html . || exit $$?; \
echo "Coverage report written to coverage/index.html"; \
if test $$rc != 0; then \
echo "*** test suite FAILED (status $$rc) -- coverage report still written above"; \
fi; \
exit $$rc
wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h
wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS)

View File

@@ -269,8 +269,16 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
break;
}
if (called_from_signal_handler)
if (called_from_signal_handler) {
#ifdef GCOV_COVERAGE
/* _exit() bypasses the gcov atexit flush; rsync's generator (and
* other processes) normally finish via the signal handler, so
* without this they would write no .gcda. Harmless otherwise. */
extern void __gcov_dump(void);
__gcov_dump();
#endif
_exit(exit_code);
}
exit(exit_code);
}

View File

@@ -82,6 +82,32 @@ if test x"$enable_profile" = x"yes"; then
CFLAGS="$CFLAGS -pg"
fi
dnl Coverage build (gcov) for `make coverage`. NOTE: --enable-profile above is
dnl gprof (-pg) and is NOT coverage. -O0 keeps branch coverage meaningful;
dnl -fprofile-update=atomic keeps the shared .gcda counters correct while the
dnl suite runs many rsync processes in parallel.
AC_ARG_ENABLE(coverage,
AS_HELP_STRING([--enable-coverage],[build with gcov instrumentation for `make coverage`]))
if test x"$enable_coverage" = x"yes"; then
CFLAGS="$CFLAGS --coverage -fprofile-update=atomic -O0"
CXXFLAGS="$CXXFLAGS --coverage -fprofile-update=atomic -O0"
LDFLAGS="$LDFLAGS --coverage"
AC_DEFINE([GCOV_COVERAGE], 1,
[Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), which bypasses the gcov atexit handler, so without this no .gcda is written for the receiver/generator/daemon-worker processes.])
fi
dnl openat2(RESOLVE_BENEATH) is used on Linux 5.6+ for the secure resolver.
dnl --disable-openat2 forces the portable per-component O_NOFOLLOW fallback to
dnl run as the primary resolver on ordinary Linux, so that tier is exercised
dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral
dnl by default (the knob only REMOVES a tier when explicitly disabled).
AC_ARG_ENABLE(openat2,
AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)]))
if test x"$enable_openat2" != x"no"; then
AC_DEFINE([HAVE_OPENAT2], 1,
[Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])
fi
AC_MSG_CHECKING([if md2man can create manpages])
if test x"$ac_cv_path_PYTHON3" = x; then
AC_MSG_RESULT(no - python3 not found)

5
main.c
View File

@@ -1618,6 +1618,11 @@ static void sigusr2_handler(UNUSED(int val))
if (!am_server)
output_summary();
close_all();
#ifdef GCOV_COVERAGE
/* The receiver child is killed here via SIGUSR2 and exits with _exit(),
* bypassing the gcov atexit flush; without this it writes no .gcda. */
{ extern void __gcov_dump(void); __gcov_dump(); }
#endif
if (got_xfer_error)
_exit(RERR_PARTIAL);
_exit(0);

View File

@@ -33,7 +33,7 @@
#include <sys/syscall.h>
#endif
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
#include <sys/syscall.h>
#include <linux/openat2.h>
#endif
@@ -1691,7 +1691,7 @@ static int path_has_dotdot_component(const char *path)
return 0;
}
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
{
struct open_how how;
@@ -1791,11 +1791,13 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
return -1;
}
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
{
int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
/* ENOSYS = kernel < 5.6 doesn't have the syscall even though
* glibc/kernel-headers do; fall through to the portable path. */
* glibc/kernel-headers do; fall through to the portable path.
* (Built unconditionally unless --disable-openat2, which forces
* the portable resolver below so that tier is exercised.) */
if (fd != -1 || errno != ENOSYS)
return fd;
}