diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..a5a4700f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test Suite + +on: [push, pull_request] + +jobs: + test: + name: Build and test + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + fuse: [fuse3, fuse2] + + steps: + - name: Set up Git repository + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get -q update + sudo apt-get -q -y install --no-install-recommends \ + build-essential automake autoconf libtool pkg-config \ + icu-devtools libicu-dev libxml2-dev uuid-dev libsnmp-dev \ + libfuse-dev libfuse3-dev attr + + - name: Build + run: | + ./autogen.sh + ./configure ${{ matrix.fuse == 'fuse2' && '--with-fuse2' || '' }} + make -j"$(nproc)" + + - name: Run tests + run: make check VERBOSE=1 diff --git a/.gitignore b/.gitignore index 295a99da..612b0d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,8 @@ GTAGS TAGS tags *~ +# Files generated by make check +tests/t/*.log +tests/t/*.trs +tests/test-suite.log +tests/helpers/fsops_helper diff --git a/Makefile.am b/Makefile.am index f7cec8cd..b743ee10 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,7 @@ ACLOCAL_AMFLAGS = -I m4 nobase_pkginclude_HEADERS = config.h EXTRA_DIST = VERSION -SUBDIRS = messages src conf init.d man +SUBDIRS = messages src conf init.d man tests install-data-local: ltfs.pc if [ ! -d $(DESTDIR)$(libdir)/pkgconfig ]; then \ diff --git a/README.md b/README.md index ca8ef1f9..ae6a4879 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,12 @@ make install | 8.0 | i386 | OK | | 7.2 | amd64 | OK | +## Running the test suite + +`make check` runs the integration tests. They mount LTFS through FUSE on a tape that the file backend emulates in a local directory, so no tape hardware is needed. + +The tests need Linux with `/dev/fuse` and are skipped on other systems. On macOS, `tests/run-in-docker.sh [configure-options...]` builds LTFS and runs the tests in an Ubuntu container. + ## Contributing Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. diff --git a/configure.ac b/configure.ac index 3e4ec856..55c7977b 100644 --- a/configure.ac +++ b/configure.ac @@ -619,6 +619,11 @@ dnl dnl Output files dnl AC_CONFIG_HEADERS([config.h]) +dnl Compute the integration test list at configure time so branches can +dnl add tests/t/*.sh scripts without editing the build files. +TESTS_LIST=`cd "$srcdir/tests" 2>/dev/null && ls t/*.sh 2>/dev/null | sort | tr '\n' ' '` +AC_SUBST([TESTS_LIST]) + AC_CONFIG_FILES([ Makefile messages/Makefile @@ -637,6 +642,7 @@ AC_CONFIG_FILES([ src/iosched/Makefile src/kmi/Makefile src/utils/Makefile + tests/Makefile ltfs.pc:ltfs.pc.in ]) diff --git a/src/libltfs/arch/ltfs_arch_ops.h b/src/libltfs/arch/ltfs_arch_ops.h index 242d6dc8..097b70c6 100644 --- a/src/libltfs/arch/ltfs_arch_ops.h +++ b/src/libltfs/arch/ltfs_arch_ops.h @@ -151,7 +151,7 @@ extern "C" { #define arch_sscanf sscanf - #define arch_open( descriptor_ptr, filename_ptr, open_flg, share_flg, unused) do{ *descriptor_ptr = open(filename_ptr, open_flg, share_flg); }while(0) + #define arch_open( descriptor_ptr, filename_ptr, open_flg, share_flg, perm) do{ *descriptor_ptr = open(filename_ptr, open_flg, perm); }while(0) #define arch_fopen(file, mode, file_ptr) do {file_ptr = fopen(file, mode);}while(0) diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 00000000..692294c9 --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,62 @@ +# +# +# OO_Copyright_BEGIN +# +# +# Copyright 2010, 2020 IBM Corp. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# +# OO_Copyright_END +# + +# +# Integration tests for LTFS using the file tape backend. +# +# Run with: make check +# The tests need a Linux host with /dev/fuse; elsewhere they are skipped. +# On macOS use tests/run-in-docker.sh to run them in a Linux container. +# + +TEST_EXTENSIONS = .sh +SH_LOG_COMPILER = $(SHELL) + +AM_TESTS_ENVIRONMENT = \ + top_builddir='$(abs_top_builddir)' top_srcdir='$(abs_top_srcdir)'; \ + export top_builddir top_srcdir; + +check_PROGRAMS = helpers/fsops_helper +helpers_fsops_helper_SOURCES = helpers/fsops_helper.c + +# TESTS_LIST is set by configure from tests/t/*.sh; GNU make functions +# cannot be used because automake runs with -Wall -Werror. +TESTS = $(TESTS_LIST) + +EXTRA_DIST = \ + lib/harness.sh \ + $(TESTS) \ + docker/Dockerfile \ + run-in-docker.sh diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 00000000..3801cece --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get -q update && apt-get -q -y install --no-install-recommends \ + build-essential automake autoconf libtool pkg-config \ + icu-devtools libicu-dev libxml2-dev uuid-dev libsnmp-dev \ + libfuse-dev fuse3 libfuse3-dev \ + attr file procps util-linux tar \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build diff --git a/tests/helpers/fsops_helper.c b/tests/helpers/fsops_helper.c new file mode 100644 index 00000000..8f1c9485 --- /dev/null +++ b/tests/helpers/fsops_helper.c @@ -0,0 +1,158 @@ +/* +** +** OO_Copyright_BEGIN +** +** +** Copyright 2010, 2025 IBM Corp. All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +** ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +** LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +** CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +** SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +** INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +** CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +** ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +** POSSIBILITY OF SUCH DAMAGE. +** +** +** OO_Copyright_END +*/ + +/* Test helper exercising syscalls that shell utilities do not reach + * directly: renameat2() flags, ftruncate() on an open descriptor, mmap() + * and utimensat() with UTIME_OMIT. + * + * Usage: + * fsops_helper noreplace renameat2 with RENAME_NOREPLACE + * fsops_helper exchange renameat2 with RENAME_EXCHANGE + * fsops_helper ftruncate ftruncate an open fd, print new size + * fsops_helper mmap map the file shared and writable + * fsops_helper atime set atime, UTIME_OMIT for mtime + * fsops_helper mtime set mtime, UTIME_OMIT for atime + * + * Exit codes: 0 = success, 2 = syscall failed (strerror printed on stdout), + * 3 = usage, 4 = not supported on this platform. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +static int usage(const char *prog) +{ + fprintf(stderr, "usage: %s noreplace|exchange|ftruncate|mmap|atime|mtime \n", + prog); + return 3; +} + +static int parse_len(const char *arg, long long *len) +{ + char *end; + + *len = strtoll(arg, &end, 10); + return end != arg && *end == '\0'; +} + +/* Print the error before close() can change errno. */ +static int syscall_failed(int fd) +{ + printf("%s\n", strerror(errno)); + if (fd >= 0) + close(fd); + return 2; +} + +int main(int argc, char **argv) +{ + if (argc != 4) + return usage(argv[0]); + + if (!strcmp(argv[1], "noreplace") || !strcmp(argv[1], "exchange")) { + /* renameat2() and RENAME_* need glibc 2.28 or later. */ +#if defined(__linux__) && defined(RENAME_NOREPLACE) + unsigned int flags = !strcmp(argv[1], "noreplace") ? + RENAME_NOREPLACE : RENAME_EXCHANGE; + + if (renameat2(AT_FDCWD, argv[2], AT_FDCWD, argv[3], flags) < 0) + return syscall_failed(-1); + return 0; +#else + fprintf(stderr, "rename flags are not supported on this platform\n"); + return 4; +#endif + } + + if (!strcmp(argv[1], "ftruncate")) { + struct stat st; + long long len; + int fd; + + if (!parse_len(argv[3], &len)) + return usage(argv[0]); + fd = open(argv[2], O_RDWR); + if (fd < 0) + return syscall_failed(-1); + if (ftruncate(fd, len) < 0 || fstat(fd, &st) < 0) + return syscall_failed(fd); + printf("%lld\n", (long long)st.st_size); + close(fd); + return 0; + } + + if (!strcmp(argv[1], "mmap")) { + long long len; + void *addr; + int fd; + + if (!parse_len(argv[3], &len)) + return usage(argv[0]); + fd = open(argv[2], O_RDWR); + if (fd < 0) + return syscall_failed(-1); + addr = mmap(NULL, (size_t)len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) + return syscall_failed(fd); + munmap(addr, (size_t)len); + close(fd); + return 0; + } + + if (!strcmp(argv[1], "atime") || !strcmp(argv[1], "mtime")) { + struct timespec ts[2] = { { 0, UTIME_OMIT }, { 0, UTIME_OMIT } }; + int i = !strcmp(argv[1], "mtime"); + long long sec; + + if (!parse_len(argv[3], &sec)) + return usage(argv[0]); + ts[i].tv_sec = (time_t)sec; + ts[i].tv_nsec = 0; + if (utimensat(AT_FDCWD, argv[2], ts, 0) < 0) + return syscall_failed(-1); + return 0; + } + + fprintf(stderr, "unknown command: %s\n", argv[1]); + return 3; +} diff --git a/tests/lib/harness.sh b/tests/lib/harness.sh new file mode 100644 index 00000000..7d38689f --- /dev/null +++ b/tests/lib/harness.sh @@ -0,0 +1,227 @@ +# Common helpers for LTFS integration tests. +# +# Tests run against the build tree (no installation needed) using the +# file tape backend, which emulates a tape drive in a plain directory. +# +# A test script sources this file, calls ltfs_setup, performs its checks +# under $MNT, then calls ltfs_finish (unmount + ltfsck). Cleanup of +# mounts and temporary files is handled by an EXIT trap. + +set -eu + +# Error texts are compared literally. +export LC_ALL=C + +SKIP=77 + +: "${top_builddir:?top_builddir must be set (run via make check)}" +: "${top_srcdir:?top_srcdir must be set (run via make check)}" + +LTFS_BIN="$top_builddir/src/ltfs" +MKLTFS_BIN="$top_builddir/src/utils/mkltfs" +LTFSCK_BIN="$top_builddir/src/utils/ltfsck" +HELPER="$top_builddir/tests/helpers/fsops_helper" + +# Extra mount options for the ltfs invocation; tests may override. +LTFS_MOUNT_OPTS="${LTFS_MOUNT_OPTS:-}" + +WORK= +LTFS_PID= +MNT= +TAPE= + +skip() { + echo "SKIP: $*" + exit "$SKIP" +} + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# helper_fails : the fsops_helper +# syscall must fail with exactly that error text. +helper_fails() { + [ -x "$HELPER" ] || fail "fsops_helper not built" + expected=$1 + shift + rc=0 + out=$("$HELPER" "$@") || rc=$? + [ "$rc" -eq 4 ] && skip "fsops_helper $* is not supported on this platform" + [ "$rc" -eq 2 ] || fail "fsops_helper $*: exit code $rc, output: $out" + [ "$out" = "$expected" ] || fail "fsops_helper $*: expected '$expected', got '$out'" +} + +ltfs_check_env() { + [ "$(uname -s)" = "Linux" ] || skip "FUSE integration tests only run on Linux" + [ -e /dev/fuse ] || skip "/dev/fuse is not available" + command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1 \ + || [ "$(id -u)" = "0" ] || skip "fusermount is not available" + [ -x "$LTFS_BIN" ] || fail "ltfs binary not found at $LTFS_BIN" + [ -x "$MKLTFS_BIN" ] || fail "mkltfs binary not found at $MKLTFS_BIN" + [ -x "$LTFSCK_BIN" ] || fail "ltfsck binary not found at $LTFSCK_BIN" +} + +_fusermount() { + if command -v fusermount3 >/dev/null 2>&1; then + fusermount3 "$@" + elif command -v fusermount >/dev/null 2>&1; then + fusermount "$@" + else + # root can unmount directly + shift # drop -u + umount "$@" + fi +} + +ltfs_cleanup() { + status=$? + trap '' INT TERM + set +e + if [ -n "$MNT" ] && mountpoint -q "$MNT" 2>/dev/null; then + _fusermount -u "$MNT" 2>/dev/null || _fusermount -u -z "$MNT" 2>/dev/null \ + || umount -l "$MNT" 2>/dev/null + fi + if [ -n "$LTFS_PID" ] && kill -0 "$LTFS_PID" 2>/dev/null; then + # Give the daemon time to flush the index after unmount. + for _ in $(seq 50); do + kill -0 "$LTFS_PID" 2>/dev/null || break + sleep 0.2 + done + kill "$LTFS_PID" 2>/dev/null + for _ in $(seq 25); do + kill -0 "$LTFS_PID" 2>/dev/null || break + sleep 0.2 + done + kill -9 "$LTFS_PID" 2>/dev/null + wait "$LTFS_PID" + fi + [ -n "$WORK" ] && rm -rf "$WORK" + exit "$status" +} + +# Locate a built plugin .so, accepting both the autotools (libtool .libs/) +# and the CMake (plain subdir) layouts. +_find_plugin() { + subdir=$1 + base=$2 + for cand in \ + "$top_builddir/src/$subdir/.libs/$base.so" \ + "$top_builddir/src/$subdir/$base.so"; do + if [ -f "$cand" ]; then + echo "$cand" + return 0 + fi + done + fail "plugin $base.so not found under $top_builddir/src/$subdir" +} + +# Generate an ltfs.conf pointing at the plugins in the build tree. +_write_config() { + tape_file=$(_find_plugin tape_drivers/generic/file libtape-file) + iosched_unified=$(_find_plugin iosched libiosched-unified) + iosched_fcfs=$(_find_plugin iosched libiosched-fcfs) + kmi_flatfile=$(_find_plugin kmi libkmi-flatfile) + kmi_simple=$(_find_plugin kmi libkmi-simple) + cat >"$WORK/ltfs.conf" <"$WORK/mkltfs.log" 2>&1 || { + cat "$WORK/mkltfs.log" >&2 + fail "mkltfs failed" + } + + # Run in the foreground so we keep the pid and the log. + # shellcheck disable=SC2086 + "$LTFS_BIN" "$MNT" -o config_file="$WORK/ltfs.conf" -o tape_backend=file \ + -o devname="$TAPE" $LTFS_MOUNT_OPTS -f >"$WORK/ltfs.log" 2>&1 & + LTFS_PID=$! + + for _ in $(seq 150); do + mountpoint -q "$MNT" && return 0 + kill -0 "$LTFS_PID" 2>/dev/null || break + sleep 0.2 + done + cat "$WORK/ltfs.log" >&2 + fail "ltfs did not mount within timeout" +} + +# ltfs_umount: unmount and wait for the daemon to flush and exit. +ltfs_umount() { + _fusermount -u "$MNT" + for _ in $(seq 150); do + kill -0 "$LTFS_PID" 2>/dev/null || { LTFS_PID=; return 0; } + sleep 0.2 + done + cat "$WORK/ltfs.log" >&2 + fail "ltfs daemon did not exit after unmount" +} + +# ltfs_fsck: verify the volume is consistent. +# ltfsck exits 1 (LTFSCK_CORRECTED) after a successful check, so 0 and 1 +# are success; anything else is a failure. +ltfs_fsck() { + rc=0 + "$LTFSCK_BIN" -i "$WORK/ltfs.conf" -e file "$TAPE" >"$WORK/ltfsck.log" 2>&1 || rc=$? + if [ "$rc" -gt 1 ]; then + cat "$WORK/ltfsck.log" >&2 + fail "ltfsck reported errors (exit $rc)" + fi + grep -q "Volume is consistent" "$WORK/ltfsck.log" || { + cat "$WORK/ltfsck.log" >&2 + fail "ltfsck did not report a consistent volume" + } +} + +# ltfs_finish: standard end of test (unmount + consistency check). +ltfs_finish() { + ltfs_umount + ltfs_fsck +} + +# ltfs_is_fuse3: true when the ltfs binary is linked against libfuse 3. +ltfs_is_fuse3() { + { ldd "$LTFS_BIN" 2>/dev/null || ldd "$top_builddir/src/.libs/ltfs" 2>/dev/null; } \ + | grep -q libfuse3 +} + +# ltfs_remount: unmount and mount again (e.g. to verify persistence). +ltfs_remount() { + ltfs_umount + # shellcheck disable=SC2086 + "$LTFS_BIN" "$MNT" -o config_file="$WORK/ltfs.conf" -o tape_backend=file \ + -o devname="$TAPE" $LTFS_MOUNT_OPTS -f >"$WORK/ltfs-remount.log" 2>&1 & + LTFS_PID=$! + for _ in $(seq 150); do + mountpoint -q "$MNT" && return 0 + kill -0 "$LTFS_PID" 2>/dev/null || break + sleep 0.2 + done + cat "$WORK/ltfs-remount.log" >&2 + fail "ltfs did not remount within timeout" +} diff --git a/tests/run-in-docker.sh b/tests/run-in-docker.sh new file mode 100755 index 00000000..1d0ac4c0 --- /dev/null +++ b/tests/run-in-docker.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Build LTFS and run the test suite inside a Linux container. +# FUSE mounts require /dev/fuse and CAP_SYS_ADMIN in the container. +# +# Usage: tests/run-in-docker.sh [configure-options...] +# e.g. tests/run-in-docker.sh --enable-fast +# Environment: +# LTFS_DOCKER_SHELL=1 drop into an interactive shell instead of building +# (the source tree is mounted read-only at /src) + +set -eu + +top_srcdir=$(cd "$(dirname "$0")/.." && pwd) +image=ltfs-test + +docker build -q -t "$image" "$top_srcdir/tests/docker" >/dev/null + +run_flags="--rm --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor=unconfined" + +if [ "${LTFS_DOCKER_SHELL:-0}" = "1" ]; then + # shellcheck disable=SC2086 + exec docker run $run_flags -it -v "$top_srcdir:/src:ro" "$image" bash +fi + +# Build from a copy of the source files so the host tree stays untouched. +cd "$top_srcdir" +# shellcheck disable=SC2086 +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git ls-files -z --cached --others --exclude-standard \ + | COPYFILE_DISABLE=1 tar --null -T - -cf - +else + COPYFILE_DISABLE=1 tar -cf - . +fi | docker run $run_flags -i "$image" sh -ec ' + mkdir -p /build && cd /build && tar -xf - + ./autogen.sh + ./configure "$@" + make -j"$(nproc)" + make check VERBOSE=1 + ' sh "$@" diff --git a/tests/t/00-mount-umount.sh b/tests/t/00-mount-umount.sh new file mode 100755 index 00000000..072c18da --- /dev/null +++ b/tests/t/00-mount-umount.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Format, mount, unmount, fsck: the volume must be consistent and empty. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +mountpoint -q "$MNT" || fail "mountpoint not active" +[ -z "$(ls -A "$MNT")" ] || fail "freshly formatted volume is not empty" + +df -P "$MNT" | grep -q "$MNT" || fail "statfs does not report the volume" + +ltfs_finish +echo "PASS" diff --git a/tests/t/01-io-roundtrip.sh b/tests/t/01-io-roundtrip.sh new file mode 100755 index 00000000..0b87f24c --- /dev/null +++ b/tests/t/01-io-roundtrip.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Write files of various sizes, verify contents before and after remount. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +mkdir "$MNT/data" + +# Small, block-sized, and multi-megabyte files +dd if=/dev/urandom of="$WORK/small" bs=1234 count=1 status=none +dd if=/dev/urandom of="$WORK/medium" bs=512K count=1 status=none +dd if=/dev/urandom of="$WORK/large" bs=1M count=8 status=none + +# Expected contents after the overwrite and the append below +cp "$WORK/small" "$WORK/small.expect" +printf 'rewrite' | dd of="$WORK/small.expect" bs=1 seek=10 conv=notrunc status=none +cat "$WORK/medium" "$WORK/small" >"$WORK/medium.expect" + +for f in small medium large; do + cp "$WORK/$f" "$MNT/data/$f" +done + +( cd "$WORK" && sha256sum small medium large >"$WORK/sums" ) +( cd "$MNT/data" && sha256sum -c "$WORK/sums" >/dev/null ) \ + || fail "checksum mismatch while mounted" + +# Overwrite in place and append +printf 'rewrite' | dd of="$MNT/data/small" bs=1 seek=10 conv=notrunc status=none +cat "$WORK/small" >>"$MNT/data/medium" + +ltfs_remount + +cmp -s "$WORK/small.expect" "$MNT/data/small" || fail "small differs after remount" +cmp -s "$WORK/medium.expect" "$MNT/data/medium" || fail "medium differs after remount" +cmp -s "$WORK/large" "$MNT/data/large" || fail "large differs after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/02-truncate.sh b/tests/t/02-truncate.sh new file mode 100755 index 00000000..f02c7d77 --- /dev/null +++ b/tests/t/02-truncate.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Truncate by path (shrink and extend) and verify content and size. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +dd if=/dev/urandom of="$WORK/source" bs=1M count=2 status=none +head -c 1048576 "$WORK/source" >"$WORK/head" +cp "$WORK/source" "$MNT/file" + +truncate -s 1M "$MNT/file" +[ "$(stat -c %s "$MNT/file")" -eq 1048576 ] || fail "shrink truncate size" +cmp -s "$WORK/head" "$MNT/file" || fail "content changed by shrink truncate" + +truncate -s 3M "$MNT/file" +[ "$(stat -c %s "$MNT/file")" -eq 3145728 ] || fail "extend truncate size" +head -c 1048576 "$MNT/file" | cmp -s "$WORK/head" - || fail "content changed by extend truncate" + +# Extended region must read back as zeros +tail -c 2097152 "$MNT/file" >"$WORK/tail" +head -c 2097152 /dev/zero >"$WORK/zeros" +cmp -s "$WORK/tail" "$WORK/zeros" || fail "extended region not zero-filled" + +truncate -s 0 "$MNT/file" +[ "$(stat -c %s "$MNT/file")" -eq 0 ] || fail "truncate to zero" + +ltfs_finish +echo "PASS" diff --git a/tests/t/03-rename.sh b/tests/t/03-rename.sh new file mode 100755 index 00000000..88783a75 --- /dev/null +++ b/tests/t/03-rename.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Rename files and directories, including overwriting an existing target. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +echo one >"$MNT/a" +echo two >"$MNT/b" +mkdir "$MNT/dir1" +echo nested >"$MNT/dir1/inner" + +# Simple rename +mv "$MNT/a" "$MNT/a2" +[ "$(cat "$MNT/a2")" = "one" ] || fail "simple rename content" +[ ! -e "$MNT/a" ] || fail "source still present after rename" + +# Overwriting rename +mv "$MNT/a2" "$MNT/b" +[ "$(cat "$MNT/b")" = "one" ] || fail "overwriting rename content" + +# Directory rename +mv "$MNT/dir1" "$MNT/dir2" +[ "$(cat "$MNT/dir2/inner")" = "nested" ] || fail "directory rename content" + +# mv -n must not overwrite (rename(2) with RENAME_NOREPLACE on fuse3) +echo keep >"$MNT/target" +echo other >"$MNT/source" +mv -n "$MNT/source" "$MNT/target" 2>/dev/null || true +[ "$(cat "$MNT/target")" = "keep" ] || fail "mv -n overwrote existing target" + +ltfs_remount +[ "$(cat "$MNT/b")" = "one" ] || fail "rename result lost after remount" +[ "$(cat "$MNT/dir2/inner")" = "nested" ] || fail "dir rename lost after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/04-dirs-unlink.sh b/tests/t/04-dirs-unlink.sh new file mode 100755 index 00000000..2f15c487 --- /dev/null +++ b/tests/t/04-dirs-unlink.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# mkdir/rmdir/unlink semantics, including unlink of an open file. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +mkdir -p "$MNT/d1/d2/d3" +[ -d "$MNT/d1/d2/d3" ] || fail "nested mkdir" + +echo data >"$MNT/d1/f" +rmdir "$MNT/d1" 2>/dev/null && fail "rmdir of non-empty directory succeeded" + +rm "$MNT/d1/f" +[ ! -e "$MNT/d1/f" ] || fail "unlink" + +rmdir "$MNT/d1/d2/d3" "$MNT/d1/d2" "$MNT/d1" +[ ! -e "$MNT/d1" ] || fail "rmdir chain" + +# Unlink while open: file content must stay readable through the open fd, +# and no .fuse_hidden* litter may remain (hard_remove semantics). +echo openme >"$MNT/openfile" +exec 3<"$MNT/openfile" +rm "$MNT/openfile" +[ ! -e "$MNT/openfile" ] || fail "unlink of open file did not remove the name" +ls -a "$MNT" | grep -q '\.fuse_hidden' && fail ".fuse_hidden file left behind" +IFS= read -r line <&3 || fail "open file unreadable after unlink" +[ "$line" = "openme" ] || fail "open file reads '$line' after unlink" +exec 3<&- + +ltfs_finish +echo "PASS" diff --git a/tests/t/05-xattr.sh b/tests/t/05-xattr.sh new file mode 100755 index 00000000..e04bc6ca --- /dev/null +++ b/tests/t/05-xattr.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Extended attribute set/get/list/remove, persistence across remount. +. "${top_srcdir}/tests/lib/harness.sh" + +command -v setfattr >/dev/null 2>&1 || skip "attr tools not installed" + +ltfs_setup + +echo content >"$MNT/file" + +setfattr -n user.test1 -v value1 "$MNT/file" +setfattr -n user.test2 -v value2 "$MNT/file" + +[ "$(getfattr --only-values -n user.test1 "$MNT/file")" = "value1" ] \ + || fail "getxattr value" + +listing=$(getfattr "$MNT/file") || fail "listxattr failed" +echo "$listing" | grep -q user.test1 || fail "listxattr missing user.test1" +echo "$listing" | grep -q user.test2 || fail "listxattr missing user.test2" + +setfattr -x user.test2 "$MNT/file" +listing=$(getfattr "$MNT/file") || fail "listxattr failed after removexattr" +echo "$listing" | grep -q user.test2 && fail "removexattr: user.test2 still present" +echo "$listing" | grep -q user.test1 || fail "removexattr also removed user.test1" + +ltfs_remount +[ "$(getfattr --only-values -n user.test1 "$MNT/file")" = "value1" ] \ + || fail "xattr lost after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/06-symlink.sh b/tests/t/06-symlink.sh new file mode 100755 index 00000000..8adeabfe --- /dev/null +++ b/tests/t/06-symlink.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Symlink creation, readlink, and persistence. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +echo target-content >"$MNT/target" +ln -s target "$MNT/link" + +[ -L "$MNT/link" ] || fail "symlink not created" +[ "$(readlink "$MNT/link")" = "target" ] || fail "readlink value" +[ "$(cat "$MNT/link")" = "target-content" ] || fail "read through symlink" + +ln -s /nonexistent/absolute "$MNT/dangling" +[ "$(readlink "$MNT/dangling")" = "/nonexistent/absolute" ] || fail "dangling symlink" + +ltfs_remount +[ "$(readlink "$MNT/link")" = "target" ] || fail "symlink lost after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/07-large-dir.sh b/tests/t/07-large-dir.sh new file mode 100755 index 00000000..3bf003fd --- /dev/null +++ b/tests/t/07-large-dir.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# readdir on a directory with many entries; attributes must match stat. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +mkdir "$MNT/big" +i=0 +while [ $i -lt 500 ]; do + printf '%d' "$i" >"$MNT/big/f$i" + i=$((i + 1)) +done + +count=$(ls "$MNT/big" | wc -l) +[ "$count" -eq 500 ] || fail "expected 500 entries, got $count" + +# Attributes from listing (readdirplus on fuse3) must match per-file stat +ls_size=$(ls -l "$MNT/big/f123" | awk '{print $5}') +stat_size=$(stat -c %s "$MNT/big/f123") +[ "$ls_size" = "$stat_size" ] || fail "listing size != stat size" +[ "$stat_size" -eq 3 ] || fail "unexpected file size" + +ltfs_remount +count=$(ls "$MNT/big" | wc -l) +[ "$count" -eq 500 ] || fail "entries lost after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/08-tar-roundtrip.sh b/tests/t/08-tar-roundtrip.sh new file mode 100755 index 00000000..e95a5f67 --- /dev/null +++ b/tests/t/08-tar-roundtrip.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Copy a small tree onto the volume, tar it back, and compare checksums. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +# Build a source tree with a few subdirectories and binary files +mkdir -p "$WORK/tree/sub1/sub2" +dd if=/dev/urandom of="$WORK/tree/a.bin" bs=100K count=1 status=none +dd if=/dev/urandom of="$WORK/tree/sub1/b.bin" bs=300K count=1 status=none +dd if=/dev/urandom of="$WORK/tree/sub1/sub2/c.bin" bs=700K count=1 status=none +echo "text file" >"$WORK/tree/readme.txt" + +cp -r "$WORK/tree" "$MNT/tree" + +( cd "$WORK/tree" && find . -type f -exec sha256sum {} + | sort ) >"$WORK/src.sums" +( cd "$MNT/tree" && find . -type f -exec sha256sum {} + | sort ) >"$WORK/dst.sums" +cmp -s "$WORK/src.sums" "$WORK/dst.sums" || fail "tree copy checksum mismatch" + +# tar from the volume and extract elsewhere +tar -C "$MNT" -cf "$WORK/vol.tar" tree +mkdir "$WORK/extract" +tar -C "$WORK/extract" -xf "$WORK/vol.tar" +( cd "$WORK/extract/tree" && find . -type f -exec sha256sum {} + | sort ) >"$WORK/tar.sums" +cmp -s "$WORK/src.sums" "$WORK/tar.sums" || fail "tar roundtrip checksum mismatch" + +ltfs_finish +echo "PASS" diff --git a/tests/t/09-rename-flags-ftruncate.sh b/tests/t/09-rename-flags-ftruncate.sh new file mode 100755 index 00000000..d4e63db6 --- /dev/null +++ b/tests/t/09-rename-flags-ftruncate.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# rename(2) flag semantics, ftruncate on an open fd, and inode stability. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +[ -x "$HELPER" ] || fail "fsops_helper not built" + +echo one >"$MNT/a" +echo two >"$MNT/b" + +# RENAME_NOREPLACE with an existing target must fail and leave it intact +helper_fails "File exists" noreplace "$MNT/a" "$MNT/b" +[ "$(cat "$MNT/b")" = "two" ] || fail "target changed by failed RENAME_NOREPLACE" +[ "$(cat "$MNT/a")" = "one" ] || fail "source changed by failed RENAME_NOREPLACE" + +# RENAME_EXCHANGE is not supported by LTFS; it must fail without touching data +helper_fails "Invalid argument" exchange "$MNT/a" "$MNT/b" +[ "$(cat "$MNT/a")" = "one" ] || fail "source changed by failed RENAME_EXCHANGE" +[ "$(cat "$MNT/b")" = "two" ] || fail "target changed by failed RENAME_EXCHANGE" + +if ltfs_is_fuse3; then + # With a free target, RENAME_NOREPLACE must succeed (fuse2 cannot + # express rename flags at all, so this branch is fuse3-only) + "$HELPER" noreplace "$MNT/a" "$MNT/c" || fail "RENAME_NOREPLACE to free target failed" + [ "$(cat "$MNT/c")" = "one" ] || fail "content lost by RENAME_NOREPLACE" + [ ! -e "$MNT/a" ] || fail "source still present after RENAME_NOREPLACE" + mv "$MNT/c" "$MNT/a" +fi + +# ftruncate through an open descriptor +dd if=/dev/urandom of="$MNT/file" bs=1M count=1 status=none +out=$("$HELPER" ftruncate "$MNT/file" 12345) || fail "ftruncate failed: $out" +[ "$out" = "12345" ] || fail "ftruncate reported size $out" +[ "$(stat -c %s "$MNT/file")" -eq 12345 ] || fail "size after ftruncate" + +# Inode numbers come from the LTFS index (use_ino) and must survive a remount +ino_before=$(stat -c %i "$MNT/file") +ltfs_remount +ino_after=$(stat -c %i "$MNT/file") +[ "$ino_before" = "$ino_after" ] || fail "inode changed across remount ($ino_before -> $ino_after)" + +ltfs_finish +echo "PASS" diff --git a/tests/t/10-request-size.sh b/tests/t/10-request-size.sh new file mode 100755 index 00000000..6783430b --- /dev/null +++ b/tests/t/10-request-size.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Verify the FUSE request sizes that reach the daemon. FUSE 3 builds +# negotiate 1 MiB requests (max_write/max_pages); FUSE 2 is limited to +# 128 KiB with big_writes. +. "${top_srcdir}/tests/lib/harness.sh" + +# DEBUG3 logging prints "FUSE write '...' (offset=..., count=...)" +LTFS_MOUNT_OPTS="-o verbose=6" + +ltfs_setup + +dd if=/dev/zero of="$MNT/big" bs=1M count=8 conv=fsync status=none + +# O_DIRECT reads bypass the readahead window, so the application's +# request size reaches the daemon (split at the negotiated maximum) +err=$(dd if="$MNT/big" of=/dev/null bs=1M iflag=direct status=none 2>&1) || { + case $err in + *"failed to open"*"Invalid argument"* | *"failed to open"*"Invalid input"*) + skip "O_DIRECT is not supported on this kernel" ;; + esac + fail "O_DIRECT read failed: $err" +} + +ltfs_finish + +max_req() { + grep "FUSE $1" "$WORK/ltfs.log" | grep -o 'count=[0-9]*' | \ + cut -d= -f2 | sort -n | tail -1 +} + +write_max=$(max_req write) +read_max=$(max_req read) +echo "largest write request: ${write_max:-none}, largest read request: ${read_max:-none}" + +[ -n "$write_max" ] || fail "no write requests logged" + +if ltfs_is_fuse3; then + [ "$write_max" -ge 524288 ] || fail "write requests capped at $write_max bytes" + [ "$read_max" -ge 524288 ] || fail "read requests capped at $read_max bytes" +else + # big_writes raises the FUSE 2 limit to 128 KiB + [ "$write_max" -ge 131072 ] || fail "write requests capped at $write_max bytes" +fi + +echo "PASS" diff --git a/tests/t/11-direct-io.sh b/tests/t/11-direct-io.sh new file mode 100755 index 00000000..da7fd3c6 --- /dev/null +++ b/tests/t/11-direct-io.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# -o direct_io: data integrity without the page cache, large requests +# even for buffered application I/O, and graceful mmap failure. +. "${top_srcdir}/tests/lib/harness.sh" + +LTFS_MOUNT_OPTS="-o direct_io -o verbose=6" + +ltfs_setup + +# Data integrity through the direct path, including odd sizes +dd if=/dev/urandom of="$WORK/data" bs=37k count=9 status=none +cp "$WORK/data" "$MNT/data" +cmp -s "$WORK/data" "$MNT/data" || fail "data mismatch while mounted" + +# mmap is not available on direct-I/O files; it must fail cleanly +helper_fails "No such device" mmap "$MNT/data" 4096 + +# Buffered writes from the application reach the daemon at the +# application's block size (no page-cache splitting) +dd if=/dev/zero of="$MNT/big" bs=1M count=4 status=none + +ltfs_remount +cmp -s "$WORK/data" "$MNT/data" || fail "data mismatch after remount" + +ltfs_finish + +if ltfs_is_fuse3; then + write_max=$(grep "FUSE write" "$WORK/ltfs.log" | grep -o 'count=[0-9]*' | \ + cut -d= -f2 | sort -n | tail -1) + echo "largest write request: $write_max" + [ "$write_max" -ge 524288 ] || fail "direct writes capped at $write_max bytes" +fi + +echo "PASS" diff --git a/tests/t/12-readdirplus.sh b/tests/t/12-readdirplus.sh new file mode 100755 index 00000000..7f2cc36f --- /dev/null +++ b/tests/t/12-readdirplus.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# readdirplus: listing a directory must return correct attributes and +# must not trigger a getattr request per entry. +. "${top_srcdir}/tests/lib/harness.sh" + +LTFS_MOUNT_OPTS="-o verbose=6" + +NFILES=100 + +ltfs_check_env +ltfs_is_fuse3 || skip "readdirplus needs a FUSE 3 build" + +ltfs_setup + +mkdir "$MNT/big" +i=0 +while [ $i -lt $NFILES ]; do + head -c $((i + 1)) /dev/zero >"$MNT/big/f$i" + i=$((i + 1)) +done + +# Remount so the listing below runs against a cold kernel cache +ltfs_remount + +# Attributes reported by the listing must match per-file stat +ls -l "$MNT/big" >"$WORK/listing" +for n in 0 57 99; do + ls_size=$(awk -v f="f$n" '$NF == f {print $5}' "$WORK/listing") + [ "$ls_size" = "$((n + 1))" ] || fail "listing reports size $ls_size for f$n" + stat_size=$(stat -c %s "$MNT/big/f$n") + [ "$stat_size" = "$((n + 1))" ] || fail "stat reports size $stat_size for f$n" +done + +ltfs_finish + +# "FUSE getattr/fgetattr" debug lines from the remounted instance show how +# many attribute requests the listing needed +getattrs=$(grep -c "FUSE f*getattr" "$WORK/ltfs-remount.log" || true) +echo "getattr requests during ls -l of $NFILES files: $getattrs" + +# readdirplus delivers attributes with the listing; without it the +# kernel issues one getattr (via lookup) per entry. The prefill is +# only effective with libfuse >= 3.17 (verified there; libfuse 3.14 +# never sends READDIRPLUS to the high-level API), so the strict +# assertion is gated on the runtime library version. +ver=$(fusermount3 -V 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1) +maj=${ver%%.*} +min=${ver#*.} +if [ "${maj:-0}" -gt 3 ] || { [ "${maj:-0}" -eq 3 ] && [ "${min:-0}" -ge 17 ]; }; then + [ "$getattrs" -lt $((NFILES / 2)) ] \ + || fail "expected readdirplus to suppress per-entry getattr, saw $getattrs" +else + echo "libfuse ${ver:-unknown}: readdirplus prefill not asserted (verified on >= 3.17)" +fi + +echo "PASS" diff --git a/tests/t/13-large-write-integrity.sh b/tests/t/13-large-write-integrity.sh new file mode 100755 index 00000000..c3631096 --- /dev/null +++ b/tests/t/13-large-write-integrity.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Data integrity for single write() calls larger than the tape block size. +# +# A single large write exercises the I/O scheduler's request-splitting: the +# write must be broken into block-sized cache entries without dropping the +# tail. dd with a large bs issues one write() syscall per block, so with the +# FUSE 3 default max_write of 1 MiB (> the 512 KiB tape block) the daemon +# receives multi-block writes in a single request. Content is random and +# verified byte-for-byte, before and after a remount. +. "${top_srcdir}/tests/lib/harness.sh" + +ltfs_setup + +# One write() each, all larger than the block size, at offset 0 and beyond +for mb in 1 4 8; do + dd if=/dev/urandom of="$WORK/src$mb" bs=${mb}M count=1 status=none + dd if="$WORK/src$mb" of="$MNT/f$mb" bs=${mb}M count=1 conv=fsync status=none + cmp -s "$WORK/src$mb" "$MNT/f$mb" || fail "${mb} MiB single write corrupted while mounted" +done + +# Overwrite the second half of the 4 MiB file with one 2 MiB write at a +# block-aligned non-zero offset (exercises the insert-into-list path). +dd if=/dev/urandom of="$WORK/mid" bs=2M count=1 status=none +dd if="$WORK/mid" of="$MNT/f4" bs=2M count=1 seek=1 conv=fsync,notrunc status=none +head -c 2097152 "$WORK/src4" >"$WORK/f4.expect" +cat "$WORK/mid" >>"$WORK/f4.expect" +cmp -s "$WORK/f4.expect" "$MNT/f4" || fail "large write at non-zero offset corrupted" + +ltfs_remount + +cmp -s "$WORK/src8" "$MNT/f8" || fail "8 MiB write corrupted after remount" +cmp -s "$WORK/f4.expect" "$MNT/f4" || fail "offset write corrupted after remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/14-overwrite-truncate.sh b/tests/t/14-overwrite-truncate.sh new file mode 100755 index 00000000..42237e50 --- /dev/null +++ b/tests/t/14-overwrite-truncate.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Overwriting a file with shorter content must truncate it (O_TRUNC). +. "${top_srcdir}/tests/lib/harness.sh" + +# check_file +check_file() { + size=$(stat -c %s "$MNT/$1") + content=$(cat "$MNT/$1") + [ "$size" -eq ${#2} ] && [ "$content" = "$2" ] \ + || fail "$1 $3: expected '$2' (${#2} bytes), got '$content' ($size bytes)" +} + +ltfs_setup + +printf 0123456789 >"$MNT/redirect" +printf abc >"$MNT/redirect" + +printf 0123456789 >"$WORK/long" +printf xyz >"$WORK/short" +cp "$WORK/long" "$MNT/copy" +cp "$WORK/short" "$MNT/copy" + +printf 0123456789 >"$MNT/append" +printf abc >>"$MNT/append" + +check_file redirect abc "after overwrite by redirection" +check_file copy xyz "after overwrite by cp" +check_file append 0123456789abc "after append" + +ltfs_remount + +check_file redirect abc "after overwrite by redirection and remount" +check_file copy xyz "after overwrite by cp and remount" +check_file append 0123456789abc "after append and remount" + +ltfs_finish +echo "PASS" diff --git a/tests/t/15-timestamps.sh b/tests/t/15-timestamps.sh new file mode 100755 index 00000000..cb1892e8 --- /dev/null +++ b/tests/t/15-timestamps.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# touch must store the given date, or the current time, on new and existing +# files; an update of one time must keep the other. +. "${top_srcdir}/tests/lib/harness.sh" + +DATE=2020-01-01T00:00:00 +DATE_A=2021-02-03T04:05:06 +DATE_M=2022-03-04T05:06:07 + +# check_now : the stat field is within 5 minutes of $now +check_now() { + t=$(stat -c "$2" "$MNT/$1") + [ $((t - now)) -ge -300 ] && [ $((t - now)) -le 300 ] \ + || fail "$1: $3 is $t, expected the current time $now" +} + +# check_times : the file has exactly these times +check_times() { + atime=$(stat -c %X "$MNT/$1") + mtime=$(stat -c %Y "$MNT/$1") + [ "$atime" -eq "$2" ] && [ "$mtime" -eq "$3" ] \ + || fail "$1: atime $atime, mtime $mtime after $4, expected $2 and $3" +} + +ltfs_setup + +date_epoch=$(date -d "$DATE" +%s) +date_a_epoch=$(date -d "$DATE_A" +%s) +date_m_epoch=$(date -d "$DATE_M" +%s) + +printf data >"$MNT/dated" +printf data >"$MNT/existing" +touch -d "$DATE" "$MNT/dated" "$MNT/existing" +for f in dated existing; do + check_times "$f" "$date_epoch" "$date_epoch" "touch -d $DATE" +done + +now=$(date +%s) +touch "$MNT/existing" +check_now existing %Y "mtime after touch of an existing file" +check_now existing %X "atime after touch of an existing file" + +touch "$MNT/new" +check_now new %Y "mtime of a file created by touch" + +# Set one time and pass UTIME_OMIT for the other, which touch -a and -m do +# not always do. libfuse 2 drops such updates because LTFS does not set +# flag_utime_omit_ok. +if ltfs_is_fuse3; then + [ -x "$HELPER" ] || fail "fsops_helper not built" + printf data >"$MNT/partial" + touch -d "$DATE" "$MNT/partial" + "$HELPER" atime "$MNT/partial" "$date_a_epoch" || fail "partial: atime update failed" + check_times partial "$date_a_epoch" "$date_epoch" "an atime-only update" + "$HELPER" mtime "$MNT/partial" "$date_m_epoch" || fail "partial: mtime update failed" + check_times partial "$date_a_epoch" "$date_m_epoch" "an mtime-only update" +fi + +existing_mtime=$(stat -c %Y "$MNT/existing") +existing_atime=$(stat -c %X "$MNT/existing") +new_mtime=$(stat -c %Y "$MNT/new") + +ltfs_remount + +[ "$(stat -c %Y "$MNT/dated")" -eq "$date_epoch" ] || fail "dated: mtime changed by remount" +[ "$(stat -c %Y "$MNT/existing")" -eq "$existing_mtime" ] || fail "existing: mtime changed by remount" +[ "$(stat -c %X "$MNT/existing")" -eq "$existing_atime" ] || fail "existing: atime changed by remount" +[ "$(stat -c %Y "$MNT/new")" -eq "$new_mtime" ] || fail "new: mtime changed by remount" +if ltfs_is_fuse3; then + check_times partial "$date_a_epoch" "$date_m_epoch" "remount" +fi + +ltfs_finish +echo "PASS" diff --git a/tests/t/16-listing-during-writes.sh b/tests/t/16-listing-during-writes.sh new file mode 100755 index 00000000..8bfb9615 --- /dev/null +++ b/tests/t/16-listing-during-writes.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# Writing and closing files while a large directory is listed must not deadlock. +. "${top_srcdir}/tests/lib/harness.sh" + +NENTRIES=1000 +NWRITES=300 + +# stop_deadlocked : stop the workers, detach the mount and fail +stop_deadlocked() { + kill "$LS_PID" "$WR_PID" 2>/dev/null || true + # A stat of the mount point would block on the deadlocked daemon + # too; detach the mount so that the cleanup goes on to kill it. + _fusermount -u -z "$MNT" 2>/dev/null || umount -l "$MNT" 2>/dev/null || true + fail "$1 did not finish within 120 s (deadlock)" +} + +# wait_pid : wait up to 120 s, in 0.2 s steps, for the process to end +wait_pid() { + n=0 + while kill -0 "$1" 2>/dev/null; do + [ $n -lt 600 ] || stop_deadlocked "$2" + sleep 0.2 + n=$((n + 1)) + done +} + +ltfs_setup + +mkdir "$MNT/big" "$MNT/w" +i=0 +while [ $i -lt $NENTRIES ]; do + : >"$MNT/big/e$i" + i=$((i + 1)) +done + +while [ ! -e "$WORK/stop" ]; do + ls -l "$MNT/big" >/dev/null +done & +LS_PID=$! + +( + i=0 + while [ $i -lt $NWRITES ]; do + head -c 100000 /dev/urandom >"$MNT/w/f$i" + i=$((i + 1)) + done +) & +WR_PID=$! + +wait_pid "$WR_PID" "writing files while listing" +: >"$WORK/stop" +wait_pid "$LS_PID" "listing after the writes" +wait "$LS_PID" || fail "listing failed while files were written" +wait "$WR_PID" || fail "writing files failed while listing" + +ltfs_finish +echo "PASS"