From 1d5b5ab83af84db0f66283d4cf5931d16e71f17e Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sun, 24 May 2026 08:12:39 +1000 Subject: [PATCH] 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) --- Makefile.in | 34 ++++++++++++++++++++++++++++++++++ cleanup.c | 10 +++++++++- configure.ac | 26 ++++++++++++++++++++++++++ main.c | 5 +++++ syscall.c | 10 ++++++---- 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/Makefile.in b/Makefile.in index af9fbfb2..8f3b04c9 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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) diff --git a/cleanup.c b/cleanup.c index 0493fbbb..7f1864cc 100644 --- a/cleanup.c +++ b/cleanup.c @@ -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); } diff --git a/configure.ac b/configure.ac index 4062651d..4faab5fc 100644 --- a/configure.ac +++ b/configure.ac @@ -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) diff --git a/main.c b/main.c index 78f0b833..c54fd79b 100644 --- a/main.c +++ b/main.c @@ -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); diff --git a/syscall.c b/syscall.c index e317bccc..8579b075 100644 --- a/syscall.c +++ b/syscall.c @@ -33,7 +33,7 @@ #include #endif -#ifdef __linux__ +#if defined(__linux__) && defined(HAVE_OPENAT2) #include #include #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; }