]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
build: add gcov coverage and --disable-openat2 knobs for the test suite
authorAndrew Tridgell <andrew@tridgell.net>
Sat, 23 May 2026 22:12:39 +0000 (08:12 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Sun, 24 May 2026 02:31:52 +0000 (12:31 +1000)
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>
Makefile.in
cleanup.c
configure.ac
main.c
syscall.c

index af9fbfb28e26f5ed6205d11127713172789e249c..8f3b04c9d2031267a27c48b3a177f4088979f473 100644 (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)
index 0493fbbb1ed8f37190a97a1577341ad6bc26cf07..7f1864ccbcf3c10c7fdb25a761659eae59ae7867 100644 (file)
--- 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);
 }
 
index 4062651df49b37cdea90f3fee03455c9e8dd74f6..4faab5fcb210cd9decc9c05264da7d3bab825157 100644 (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)
diff --git a/main.c b/main.c
index 78f0b8331bdc1aac5ce6183badf5bfe267836ec2..c54fd79bc994e167c7ee723de5817c07ec54e6d3 100644 (file)
--- 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);
index e317bccc308431ebae70681b77689ecdd64fbdd0..8579b075fa165210821e2071e77f87a9ed268780 100644 (file)
--- a/syscall.c
+++ b/syscall.c
@@ -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;
        }