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:
# `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)
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)
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);
}
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)
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);
#include <sys/syscall.h>
#endif
-#ifdef __linux__
+#if defined(__linux__) && defined(HAVE_OPENAT2)
#include <sys/syscall.h>
#include <linux/openat2.h>
#endif
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;
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;
}