Skip to content

Commit 7c0dfc2

Browse files
committed
Add an integration test suite using the file tape backend
Shell-based tests driven by automake (make check) covering mount and unmount, data roundtrips, truncate, rename, directory operations, xattrs, symlinks, large directories, and a tar roundtrip, plus a regression test for the unified scheduler data loss (#591): single multi-block writes of random data, content-compared while mounted and after a remount. Tests mount a file-backend volume from the build tree, so no installation or tape hardware is needed; they are skipped on hosts without /dev/fuse. tests/run-in-docker.sh builds and runs the suite in an Ubuntu container for development on non-Linux hosts. A small C helper drives renameat2() with RENAME_NOREPLACE and RENAME_EXCHANGE and ftruncate() on an open descriptor; renameat2 is Linux-only, so the helper reports the commands as unsupported elsewhere. Inode numbers must survive a remount, which verifies that the LTFS index UIDs are passed through (use_ino) on both FUSE versions. Three tests cover the FUSE request size, -o direct_io, and readdirplus. Their assertions are FUSE-version aware: on libfuse 2 the request-size test expects the big_writes limit and the readdirplus prefill is not asserted (it needs libfuse >= 3.17); the strict expectations activate with the FUSE 3 port. All pass on both APIs. The harness locates build artifacts in both the autotools and CMake layouts, so the same scripts run under make check and ctest, and the test list is computed from tests/t/ so other branches can add scripts without editing the registration.
1 parent 66f3b44 commit 7c0dfc2

23 files changed

Lines changed: 817 additions & 1 deletion

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ GTAGS
3232
TAGS
3333
tags
3434
*~
35+
# Files generated by make check
36+
tests/t/*.log
37+
tests/t/*.trs
38+
tests/test-suite.log
39+
build.log
40+
configure.log
41+
tests/helpers/fsops_helper

Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ACLOCAL_AMFLAGS = -I m4
3737

3838
nobase_pkginclude_HEADERS = config.h
3939

40-
SUBDIRS = messages src conf init.d man
40+
SUBDIRS = messages src conf init.d man tests
4141

4242
install-data-local: ltfs.pc
4343
if [ ! -d $(DESTDIR)$(libdir)/pkgconfig ]; then \

configure.ac

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,11 @@ dnl
604604
dnl Output files
605605
dnl
606606
AC_CONFIG_HEADERS([config.h])
607+
dnl Compute the integration test list at configure time so branches can
608+
dnl add tests/t/*.sh scripts without editing the build files.
609+
TESTS_LIST=`cd "$srcdir/tests" 2>/dev/null && ls t/*.sh 2>/dev/null | sort | tr '\n' ' '`
610+
AC_SUBST([TESTS_LIST])
611+
607612
AC_CONFIG_FILES([
608613
Makefile
609614
messages/Makefile
@@ -622,6 +627,7 @@ AC_CONFIG_FILES([
622627
src/iosched/Makefile
623628
src/kmi/Makefile
624629
src/utils/Makefile
630+
tests/Makefile
625631
ltfs.pc:ltfs.pc.in
626632
])
627633

tests/Makefile.am

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#
2+
# Integration tests for LTFS using the file tape backend.
3+
#
4+
# Run with: make check
5+
# The tests need a Linux host with /dev/fuse; elsewhere they are skipped.
6+
# On macOS use tests/run-in-docker.sh to run them in a Linux container.
7+
#
8+
9+
TEST_EXTENSIONS = .sh
10+
SH_LOG_COMPILER = $(SHELL)
11+
12+
AM_TESTS_ENVIRONMENT = \
13+
top_builddir='$(abs_top_builddir)' top_srcdir='$(abs_top_srcdir)'; \
14+
export top_builddir top_srcdir;
15+
16+
check_PROGRAMS = helpers/fsops_helper
17+
helpers_fsops_helper_SOURCES = helpers/fsops_helper.c
18+
19+
# The test list is computed at configure time (TESTS_LIST in configure.ac)
20+
# so feature branches can ship additional t/*.sh scripts without editing
21+
# the build files; ctest registers them the same way. GNU make functions
22+
# cannot be used here: automake runs with -Wall -Werror, which rejects
23+
# them as non-POSIX.
24+
TESTS = $(TESTS_LIST)
25+
26+
EXTRA_DIST = \
27+
lib/harness.sh \
28+
$(TESTS) \
29+
docker/Dockerfile \
30+
docker/icu-config \
31+
run-in-docker.sh

tests/docker/Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM ubuntu:24.04
2+
3+
ENV DEBIAN_FRONTEND=noninteractive
4+
5+
RUN apt-get -q update && apt-get -q -y install --no-install-recommends \
6+
build-essential automake autoconf libtool pkg-config \
7+
icu-devtools libicu-dev libxml2-dev uuid-dev libsnmp-dev \
8+
libfuse-dev fuse3 libfuse3-dev \
9+
attr file procps util-linux tar \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
# configure.ac probes icu-config, which modern ICU no longer ships.
13+
COPY icu-config /usr/local/bin/icu-config
14+
RUN chmod +x /usr/local/bin/icu-config
15+
16+
WORKDIR /ltfs

tests/docker/icu-config

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/sh
2+
# Minimal icu-config replacement for distributions that no longer ship it.
3+
case "$1" in
4+
--cppflags)
5+
pkg-config --cflags icu-uc ;;
6+
--ldflags)
7+
pkg-config --libs icu-uc icu-io ;;
8+
--version)
9+
pkg-config --modversion icu-uc ;;
10+
*)
11+
echo '' ;;
12+
esac

tests/helpers/fsops_helper.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* Test helper exercising syscalls that shell utilities do not reach
2+
* directly: renameat2() flags and ftruncate() on an open descriptor.
3+
*
4+
* Usage:
5+
* fsops_helper noreplace <old> <new> renameat2 with RENAME_NOREPLACE
6+
* fsops_helper exchange <a> <b> renameat2 with RENAME_EXCHANGE
7+
* fsops_helper ftruncate <file> <len> ftruncate an open fd, print new size
8+
*
9+
* Exit codes: 0 = success, 2 = syscall failed (errno printed), 3 = usage.
10+
*/
11+
#ifndef _GNU_SOURCE
12+
#define _GNU_SOURCE
13+
#endif
14+
#include <errno.h>
15+
#include <fcntl.h>
16+
#include <stdio.h>
17+
#include <stdlib.h>
18+
#include <string.h>
19+
#include <sys/stat.h>
20+
#include <unistd.h>
21+
22+
int main(int argc, char **argv)
23+
{
24+
if (argc != 4) {
25+
fprintf(stderr, "usage: %s noreplace|exchange|ftruncate <arg> <arg>\n",
26+
argv[0]);
27+
return 3;
28+
}
29+
30+
if (!strcmp(argv[1], "noreplace") || !strcmp(argv[1], "exchange")) {
31+
#ifdef __linux__
32+
unsigned int flags = !strcmp(argv[1], "noreplace") ?
33+
RENAME_NOREPLACE : RENAME_EXCHANGE;
34+
35+
if (renameat2(AT_FDCWD, argv[2], AT_FDCWD, argv[3], flags) < 0) {
36+
printf("%s\n", strerror(errno));
37+
return 2;
38+
}
39+
return 0;
40+
#else
41+
/* The integration tests only run on Linux; keep the helper
42+
* compiling on the other platforms. */
43+
fprintf(stderr, "rename flags are not supported on this platform\n");
44+
return 3;
45+
#endif
46+
}
47+
48+
if (!strcmp(argv[1], "ftruncate")) {
49+
struct stat st;
50+
off_t len = strtoll(argv[3], NULL, 10);
51+
int fd = open(argv[2], O_RDWR);
52+
53+
if (fd < 0 || ftruncate(fd, len) < 0 || fstat(fd, &st) < 0) {
54+
printf("%s\n", strerror(errno));
55+
return 2;
56+
}
57+
printf("%lld\n", (long long)st.st_size);
58+
close(fd);
59+
return 0;
60+
}
61+
62+
fprintf(stderr, "unknown command: %s\n", argv[1]);
63+
return 3;
64+
}

tests/lib/harness.sh

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Common helpers for LTFS integration tests.
2+
#
3+
# Tests run against the build tree (no installation needed) using the
4+
# file tape backend, which emulates a tape drive in a plain directory.
5+
#
6+
# A test script sources this file, calls ltfs_setup, performs its checks
7+
# under $MNT, then calls ltfs_finish (unmount + ltfsck). Cleanup of
8+
# mounts and temporary files is handled by an EXIT trap.
9+
10+
set -eu
11+
12+
SKIP=77
13+
14+
: "${top_builddir:?top_builddir must be set (run via make check)}"
15+
: "${top_srcdir:?top_srcdir must be set (run via make check)}"
16+
17+
LTFS_BIN="$top_builddir/src/ltfs"
18+
MKLTFS_BIN="$top_builddir/src/utils/mkltfs"
19+
LTFSCK_BIN="$top_builddir/src/utils/ltfsck"
20+
21+
# Extra mount options for the ltfs invocation; tests may override.
22+
LTFS_MOUNT_OPTS="${LTFS_MOUNT_OPTS:-}"
23+
24+
WORK=
25+
LTFS_PID=
26+
MNT=
27+
TAPE=
28+
29+
skip() {
30+
echo "SKIP: $*"
31+
exit "$SKIP"
32+
}
33+
34+
fail() {
35+
echo "FAIL: $*" >&2
36+
exit 1
37+
}
38+
39+
ltfs_check_env() {
40+
[ "$(uname -s)" = "Linux" ] || skip "FUSE integration tests only run on Linux"
41+
[ -e /dev/fuse ] || skip "/dev/fuse is not available"
42+
command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1 \
43+
|| [ "$(id -u)" = "0" ] || skip "fusermount is not available"
44+
[ -x "$LTFS_BIN" ] || fail "ltfs binary not found at $LTFS_BIN"
45+
[ -x "$MKLTFS_BIN" ] || fail "mkltfs binary not found at $MKLTFS_BIN"
46+
[ -x "$LTFSCK_BIN" ] || fail "ltfsck binary not found at $LTFSCK_BIN"
47+
}
48+
49+
_fusermount() {
50+
if command -v fusermount3 >/dev/null 2>&1; then
51+
fusermount3 "$@"
52+
elif command -v fusermount >/dev/null 2>&1; then
53+
fusermount "$@"
54+
else
55+
# root can unmount directly
56+
shift # drop -u
57+
umount "$@"
58+
fi
59+
}
60+
61+
ltfs_cleanup() {
62+
status=$?
63+
set +e
64+
if [ -n "$MNT" ] && mountpoint -q "$MNT" 2>/dev/null; then
65+
_fusermount -u "$MNT" 2>/dev/null || umount "$MNT" 2>/dev/null
66+
fi
67+
if [ -n "$LTFS_PID" ] && kill -0 "$LTFS_PID" 2>/dev/null; then
68+
# Give the daemon time to flush the index after unmount.
69+
for _ in $(seq 50); do
70+
kill -0 "$LTFS_PID" 2>/dev/null || break
71+
sleep 0.2
72+
done
73+
kill "$LTFS_PID" 2>/dev/null
74+
fi
75+
[ -n "$WORK" ] && rm -rf "$WORK"
76+
exit "$status"
77+
}
78+
79+
# Locate a built plugin .so, accepting both the autotools (libtool .libs/)
80+
# and the CMake (plain subdir) layouts.
81+
_find_plugin() {
82+
subdir=$1
83+
base=$2
84+
for cand in \
85+
"$top_builddir/src/$subdir/.libs/$base.so" \
86+
"$top_builddir/src/$subdir/$base.so"; do
87+
if [ -f "$cand" ]; then
88+
echo "$cand"
89+
return 0
90+
fi
91+
done
92+
fail "plugin $base.so not found under $top_builddir/src/$subdir"
93+
}
94+
95+
# Generate an ltfs.conf pointing at the plugins in the build tree.
96+
_write_config() {
97+
cat >"$WORK/ltfs.conf" <<EOF
98+
plugin tape file $(_find_plugin tape_drivers/generic/file libtape-file)
99+
plugin iosched unified $(_find_plugin iosched libiosched-unified)
100+
plugin iosched fcfs $(_find_plugin iosched libiosched-fcfs)
101+
plugin kmi flatfile $(_find_plugin kmi libkmi-flatfile)
102+
plugin kmi simple $(_find_plugin kmi libkmi-simple)
103+
default tape file
104+
default iosched unified
105+
default kmi none
106+
EOF
107+
}
108+
109+
# ltfs_setup: format a file-backend tape and mount it on $MNT.
110+
ltfs_setup() {
111+
ltfs_check_env
112+
113+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/ltfstest.XXXXXX")
114+
trap ltfs_cleanup EXIT INT TERM
115+
MNT="$WORK/mnt"
116+
TAPE="$WORK/tape"
117+
mkdir -p "$MNT" "$TAPE"
118+
119+
_write_config
120+
121+
"$MKLTFS_BIN" -i "$WORK/ltfs.conf" -e file -d "$TAPE" -n testvol -f \
122+
>"$WORK/mkltfs.log" 2>&1 || {
123+
cat "$WORK/mkltfs.log" >&2
124+
fail "mkltfs failed"
125+
}
126+
127+
# Run in the foreground so we keep the pid and the log.
128+
# shellcheck disable=SC2086
129+
"$LTFS_BIN" "$MNT" -o config_file="$WORK/ltfs.conf" -o tape_backend=file \
130+
-o devname="$TAPE" $LTFS_MOUNT_OPTS -f >"$WORK/ltfs.log" 2>&1 &
131+
LTFS_PID=$!
132+
133+
for _ in $(seq 150); do
134+
mountpoint -q "$MNT" && return 0
135+
kill -0 "$LTFS_PID" 2>/dev/null || break
136+
sleep 0.2
137+
done
138+
cat "$WORK/ltfs.log" >&2
139+
fail "ltfs did not mount within timeout"
140+
}
141+
142+
# ltfs_umount: unmount and wait for the daemon to flush and exit.
143+
ltfs_umount() {
144+
_fusermount -u "$MNT"
145+
for _ in $(seq 150); do
146+
kill -0 "$LTFS_PID" 2>/dev/null || { LTFS_PID=; return 0; }
147+
sleep 0.2
148+
done
149+
cat "$WORK/ltfs.log" >&2
150+
fail "ltfs daemon did not exit after unmount"
151+
}
152+
153+
# ltfs_fsck: verify the volume is consistent.
154+
# Exit codes follow fsck conventions: 0 = clean, 1 = corrected/treat as
155+
# success (e.g. MAM coherency update); anything else is a failure.
156+
ltfs_fsck() {
157+
rc=0
158+
"$LTFSCK_BIN" -i "$WORK/ltfs.conf" -e file "$TAPE" >"$WORK/ltfsck.log" 2>&1 || rc=$?
159+
if [ "$rc" -gt 1 ]; then
160+
cat "$WORK/ltfsck.log" >&2
161+
fail "ltfsck reported errors (exit $rc)"
162+
fi
163+
grep -q "Volume is consistent" "$WORK/ltfsck.log" || {
164+
cat "$WORK/ltfsck.log" >&2
165+
fail "ltfsck did not report a consistent volume"
166+
}
167+
}
168+
169+
# ltfs_finish: standard end of test (unmount + consistency check).
170+
ltfs_finish() {
171+
ltfs_umount
172+
ltfs_fsck
173+
}
174+
175+
# ltfs_is_fuse3: true when the ltfs binary is linked against libfuse 3.
176+
ltfs_is_fuse3() {
177+
{ ldd "$LTFS_BIN" 2>/dev/null || ldd "$top_builddir/src/.libs/ltfs" 2>/dev/null; } \
178+
| grep -q libfuse3
179+
}
180+
181+
# ltfs_remount: unmount and mount again (e.g. to verify persistence).
182+
ltfs_remount() {
183+
ltfs_umount
184+
# shellcheck disable=SC2086
185+
"$LTFS_BIN" "$MNT" -o config_file="$WORK/ltfs.conf" -o tape_backend=file \
186+
-o devname="$TAPE" $LTFS_MOUNT_OPTS -f >"$WORK/ltfs-remount.log" 2>&1 &
187+
LTFS_PID=$!
188+
for _ in $(seq 150); do
189+
mountpoint -q "$MNT" && return 0
190+
kill -0 "$LTFS_PID" 2>/dev/null || break
191+
sleep 0.2
192+
done
193+
cat "$WORK/ltfs-remount.log" >&2
194+
fail "ltfs did not remount within timeout"
195+
}

tests/run-in-docker.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
# Build LTFS and run the test suite inside a Linux container.
3+
# FUSE mounts require /dev/fuse and CAP_SYS_ADMIN in the container.
4+
#
5+
# Usage: tests/run-in-docker.sh [configure-options...]
6+
# e.g. tests/run-in-docker.sh --with-fuse2
7+
# Environment:
8+
# LTFS_DOCKER_SHELL=1 drop into an interactive shell instead of building
9+
10+
set -eu
11+
12+
top_srcdir=$(cd "$(dirname "$0")/.." && pwd)
13+
image=ltfs-test
14+
15+
docker build -q -t "$image" "$top_srcdir/tests/docker" >/dev/null
16+
17+
run_flags="--rm --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined"
18+
19+
if [ "${LTFS_DOCKER_SHELL:-0}" = "1" ]; then
20+
# shellcheck disable=SC2086
21+
exec docker run $run_flags -it -v "$top_srcdir:/ltfs" "$image" bash
22+
fi
23+
24+
# shellcheck disable=SC2086
25+
exec docker run $run_flags -v "$top_srcdir:/ltfs" "$image" sh -ec "
26+
./autogen.sh
27+
./configure --enable-icu-6x $*
28+
make -j\$(nproc)
29+
make check VERBOSE=1
30+
"

0 commit comments

Comments
 (0)