test: add tests for the file watcher

This commit is contained in:
CismonX 2025-03-01 11:23:42 +08:00
parent 0b7b46be9c
commit 3a0f435572
No known key found for this signature in database
GPG key ID: 3094873E29A482FB
6 changed files with 262 additions and 5 deletions

View file

@ -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

View file

@ -22,6 +22,8 @@
# include "config.h"
#endif
#include "check_lib.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
@ -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;
}

38
tests/check_lib.h Normal file
View file

@ -0,0 +1,38 @@
/**
* bookmarkfs/tests/check_lib.h
* ----
*
* Copyright (C) 2025 CismonX <admin@cismon.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#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_) */

184
tests/check_watcher.c Normal file
View file

@ -0,0 +1,184 @@
/**
* bookmarkfs/tests/check_watcher.c
* ----
*
* Copyright (C) 2025 CismonX <admin@cismon.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
#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;
}

25
tests/lib_watcher.at Normal file
View file

@ -0,0 +1,25 @@
dnl
dnl Copyright (C) 2025 CismonX <admin@cismon.net>
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

View file

@ -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])