diff --git a/tests/Makefile.am b/tests/Makefile.am index cb51a91..da4dc2d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,10 +8,11 @@ EXTRA_DIST = package.m4 testsuite.at $(TESTSUITE) $(TESTS_) -TESTS_ = lib_hash.at lib_prng.at +TESTS_ = lib_hash.at lib_prng.at lib_watcher.at # Helper programs for testing +check_HEADERS = check_lib.h check_PROGRAMS = if BOOKMARKFS_UTIL @@ -19,7 +20,7 @@ if BOOKMARKFS_UTIL check_bookmarkfs_util_CPPFLAGS = -I$(top_srcdir)/src check_bookmarkfs_util_LDADD = $(top_builddir)/src/libbookmarkfs_util.la - check_bookmarkfs_util_SOURCES = check_lib.c + check_bookmarkfs_util_SOURCES = check_lib.c check_watcher.c endif # BOOKMARKFS_UTIL # Autotest setup diff --git a/tests/check_lib.c b/tests/check_lib.c index 715eef8..154798a 100644 --- a/tests/check_lib.c +++ b/tests/check_lib.c @@ -22,6 +22,8 @@ # include "config.h" #endif +#include "check_lib.h" + #include #include #include @@ -55,6 +57,8 @@ dispatch_subcmds ( status = subcmd_hash(argc, argv); } else if (0 == strcmp("prng", cmd)) { status = subcmd_prng(argc, argv); + } else if (0 == strcmp("watcher", cmd)) { + status = check_watcher(argc, argv); } return status; } diff --git a/tests/check_lib.h b/tests/check_lib.h new file mode 100644 index 0000000..e95ccc5 --- /dev/null +++ b/tests/check_lib.h @@ -0,0 +1,38 @@ +/** + * bookmarkfs/tests/check_lib.h + * ---- + * + * Copyright (C) 2025 CismonX + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BOOKMARKFS_CHECK_LIB_H_ +#define BOOKMARKFS_CHECK_LIB_H_ + +#include "xstd.h" + +#define ASSERT_EXPR(cond, action_if_false) \ + if (!(cond)) { \ + log_printf("assertion (%s) failed", #cond); \ + action_if_false \ + } + +int +check_watcher ( + int argc, + char *argv[] +); + +#endif /* !defined(BOOKMARKFS_CHECK_LIB_H_) */ diff --git a/tests/check_watcher.c b/tests/check_watcher.c new file mode 100644 index 0000000..dcd0138 --- /dev/null +++ b/tests/check_watcher.c @@ -0,0 +1,184 @@ +/** +* bookmarkfs/tests/check_watcher.c +* ---- +* +* Copyright (C) 2025 CismonX +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include + +#include +#include + +#include "check_lib.h" +#include "frontend_util.h" +#include "sandbox.h" +#include "watcher.h" + +// Forward declaration start +static int do_check_watcher (int, uint32_t); +static void msecs_to_timespec (struct timespec *, unsigned long); +static int wait_for_watcher (struct watcher *, struct timespec const *, int); +// Forward declaration end + +static int +do_check_watcher ( + int dirfd, + uint32_t flags +) { +#define FILE1_NAME "foo.tmp" +#define FILE2_NAME "bar.tmp" + +#define ASSERT_EQ(val, expr) ASSERT_EXPR((val) == (expr), goto end;) +#define ASSERT_NE(val, expr) ASSERT_EXPR((val) != (expr), goto end;) + + struct watcher *w = watcher_create(dirfd, FILE1_NAME, flags); + if (w == NULL) { + return -1; + } + + unsigned long msecs = 100; + int tries = 5; + if (flags & WATCHER_FALLBACK) { + msecs = 2500; + tries = 2; + } + struct timespec ts; + msecs_to_timespec(&ts, msecs); + + int status = -1; + int fd = -1; + + fd = openat(dirfd, FILE1_NAME, O_WRONLY | O_CREAT, 0600); + ASSERT_NE(-1, fd); + // Lazy-init watcher. + ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); + + ASSERT_NE(-1, write(fd, "foo", 3)); + ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); + + bool check_truncate = true; +#if defined(__FreeBSD__) + // For kevent() EVFILT_VNODE, ftruncate() only triggers NOTE_ATTRIB, + // which we don't want to watch. + check_truncate = flags & WATCHER_FALLBACK; +#endif + if (check_truncate) { + ASSERT_EQ(0, ftruncate(fd, 0)); + ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); + } + + int fd2 = openat(dirfd, FILE2_NAME, O_WRONLY | O_CREAT, 0600); + ASSERT_NE(-1, fd2); + close(fd2); + + // FAN_DELETE_SELF won't fire if the watched file + // is still opened somewhere. + close(fd); + fd = -1; + ASSERT_EQ(0, renameat(dirfd, FILE2_NAME, dirfd, FILE1_NAME)); + ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); + + ASSERT_EQ(0, renameat(dirfd, FILE1_NAME, dirfd, FILE2_NAME)); + ASSERT_EQ(-ENOENT, wait_for_watcher(w, &ts, tries)); + + // If the watched file has gone, but managed to come back, + // the watcher should continue to work. + ASSERT_EQ(0, renameat(dirfd, FILE2_NAME, dirfd, FILE1_NAME)); + ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); + + ASSERT_EQ(0, unlinkat(dirfd, FILE1_NAME, 0)); + ASSERT_EQ(-ENOENT, wait_for_watcher(w, &ts, tries)); + + status = 0; + + end: + if (fd >= 0) { + close(fd); + } + unlinkat(dirfd, FILE1_NAME, 0); + unlinkat(dirfd, FILE2_NAME, 0); + watcher_destroy(w); + return status; +} + +static void +msecs_to_timespec ( + struct timespec *ts_buf, + unsigned long millisecs +) { + ts_buf->tv_sec = millisecs / 1000; + ts_buf->tv_nsec = (millisecs % 1000) * 1000000; +} + +static int +wait_for_watcher ( + struct watcher *w, + struct timespec const *ts, + int retry +) { + for (int i = 0; i < retry; ++i) { + clock_nanosleep(CLOCK_MONOTONIC, 0, ts, NULL); + + int status = watcher_poll(w); + if (status != -EAGAIN) { + return status; + } + } + return -ETIMEDOUT; +} + +int +check_watcher ( + int argc, + char *argv[] +) { + uint32_t flags = 0; +#if defined(__FreeBSD__) + // Do not enable sandbox on FreeBSD, + // since the watcher sandbox only grants read access to dirfd, + // and cap_enter() applies to the entire process. + flags |= SANDBOX_NOOP << WATCHER_SANDBOX_FLAGS_OFFSET; +#endif + + getopt_foreach(argc, argv, ":f") { + case 'f': + flags |= WATCHER_FALLBACK; + break; + + default: + return -1; + } + if (argc < 1) { + return -1; + } + argv += optind; + + int dirfd = open(argv[0], O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + return -1; + } + int status = do_check_watcher(dirfd, flags); + close(dirfd); + return status; +} diff --git a/tests/lib_watcher.at b/tests/lib_watcher.at new file mode 100644 index 0000000..d978497 --- /dev/null +++ b/tests/lib_watcher.at @@ -0,0 +1,25 @@ +dnl +dnl Copyright (C) 2025 CismonX +dnl +dnl Copying and distribution of this file, with or without modification, +dnl are permitted in any medium without royalty, +dnl provided the copyright notice and this notice are preserved. +dnl This file is offered as-is, without any warranty. +dnl + +AT_SETUP([util lib: file watcher]) +AT_KEYWORDS([lib watcher]) + +ATX_CHECK_LIB([ + args= + ATX_FEAT_IF([native-watcher], , [ + args="$args -f" + ]) + + tmpdir=./$(ath_fn_rand_u64_hex).tmp.d + + mkdir $tmpdir + check-bookmarkfs-util watcher $args $tmpdir +]) + +AT_CLEANUP diff --git a/tests/testsuite.at b/tests/testsuite.at index ebb4209..79c7c15 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -28,9 +28,9 @@ dnl dnl Conditionally run script depending on whether a feature is enabled. dnl m4_define([ATX_FEAT_IF], [ - if test -n "$ATX_FEAT_NAME($1)"; then + if test -n "$ATX_FEAT_NAME($1)"; then : $2 - else + else : $3 fi ]) @@ -47,7 +47,11 @@ m4_define([ATX_FEAT_PREREQ], [ ]) m4_define([ATX_CHECK_SIMPLE], [ - AT_CHECK([$1], , [ignore], [ignore]) + AT_CHECK([ + atx_line_start=$LINENO + trap 'echo "Error $? at line $(($LINENO-$atx_line_start))"; exit 1' ERR + $1 + ], , [ignore], [ignore]) ]) m4_define([ATX_CHECK_LIB], [ @@ -70,3 +74,4 @@ AT_TEST_HELPER_FN([rand_base64], , , [ AT_BANNER([The Utility Library]) m4_include([lib_hash.at]) m4_include([lib_prng.at]) +m4_include([lib_watcher.at])