Re: [RFC PATCH v1 4/5] selftests/filesystems: add fuse symlink caching test
Luis Henriques <[email protected]> Tue, 21 Jul 2026 13:58:12 +0100
| Newsgroups | dev.linux.lists.fuse-devel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest |
|---|---|
| Message-ID | <[email protected]> |
Amir Goldstein <[email protected]> writes: > On Wed, Jul 8, 2026 at 3:11=E2=80=AFPM Luis Henriques <[email protected]> w= rote: >> >> This patch adds a simple test that allows to verify that, when resolving= a >> symlink, user-space is called only the first time when caching is enabled >> or, if caching is disabled, every time the symlink resolution is request= ed. >> >> Signed-off-by: Luis Henriques <[email protected]> >> --- >> .../selftests/filesystems/fuse/.gitignore | 1 + >> .../selftests/filesystems/fuse/Makefile | 6 +- >> .../selftests/filesystems/fuse/symlink_fs.c | 115 ++++++++++++++++++ >> .../filesystems/fuse/symlink_test.sh | 52 ++++++++ >> 4 files changed, 173 insertions(+), 1 deletion(-) >> create mode 100644 tools/testing/selftests/filesystems/fuse/symlink_fs.c >> create mode 100755 tools/testing/selftests/filesystems/fuse/symlink_tes= t.sh >> >> diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools= /testing/selftests/filesystems/fuse/.gitignore >> index 3e72e742d08e..cfdc5ca3ded4 100644 >> --- a/tools/testing/selftests/filesystems/fuse/.gitignore >> +++ b/tools/testing/selftests/filesystems/fuse/.gitignore >> @@ -1,3 +1,4 @@ >> # SPDX-License-Identifier: GPL-2.0-only >> fuse_mnt >> fusectl_test >> +symlink_fs >> diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/t= esting/selftests/filesystems/fuse/Makefile >> index 422cd1b1688d..342da0878006 100644 >> --- a/tools/testing/selftests/filesystems/fuse/Makefile >> +++ b/tools/testing/selftests/filesystems/fuse/Makefile >> @@ -3,7 +3,8 @@ >> CFLAGS +=3D -Wall -O2 -g $(KHDR_INCLUDES) >> >> TEST_GEN_PROGS :=3D fusectl_test >> -TEST_GEN_FILES :=3D fuse_mnt >> +TEST_PROGS :=3D symlink_test.sh >> +TEST_GEN_FILES :=3D fuse_mnt symlink_fs >> >> include ../../lib.mk >> >> @@ -19,3 +20,6 @@ endif >> >> $(OUTPUT)/fuse_mnt: CFLAGS +=3D $(VAR_CFLAGS) >> $(OUTPUT)/fuse_mnt: LDLIBS +=3D $(VAR_LDLIBS) >> + >> +$(OUTPUT)/symlink_fs: CFLAGS +=3D $(VAR_CFLAGS) >> +$(OUTPUT)/symlink_fs: LDLIBS +=3D $(VAR_LDLIBS) >> diff --git a/tools/testing/selftests/filesystems/fuse/symlink_fs.c b/too= ls/testing/selftests/filesystems/fuse/symlink_fs.c >> new file mode 100644 >> index 000000000000..1aee26c91b3f >> --- /dev/null >> +++ b/tools/testing/selftests/filesystems/fuse/symlink_fs.c >> @@ -0,0 +1,115 @@ >> +// SPDX-License-Identifier: GPL-2.0 >> +/* >> + * Simple filesystem to test FUSE symlink cache >> + * >> + * This is a simple FUSE filesystem that contains two objects: a file n= amed >> + * 'file' and a symlink to that file named 'link'. Whenever the ->read= link() is >> + * executed to resolve 'link' a counter will be incremented. A ->read(= ) to any >> + * filesystem object will return the value in this counter. >> + * >> + * A '--cache' argument will allow to enable symlink caching (disabled = by >> + * default). This means that, if caching is enabled, resolving a symli= nk will >> + * only call into user-space the first time. >> + */ >> + >> +#define FUSE_USE_VERSION 31 >> + >> +#include <fuse.h> >> +#include <stdio.h> >> +#include <string.h> >> +#include <errno.h> >> +#include <fcntl.h> >> +#include <stddef.h> >> +#include <assert.h> >> + >> +#define FILE "file" >> +#define LINK "link" >> + >> +static struct options { >> + int cache_symlinks; >> +} options; >> + >> +static const struct fuse_opt option_spec[] =3D { >> + { "--cache", offsetof(struct options, cache_symlinks), 1 }, >> + FUSE_OPT_END >> +}; >> + >> +static int readlink_counter =3D 0; >> + >> +static void *symlink_init(struct fuse_conn_info *conn, struct fuse_conf= ig *cfg) >> +{ >> + if (options.cache_symlinks) >> + fuse_set_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS); >> + >> + return NULL; >> +} >> + >> +static int symlink_getattr(const char *path, struct stat *stbuf, >> + struct fuse_file_info *fi) >> +{ >> + int res =3D 0; >> + >> + memset(stbuf, 0, sizeof(struct stat)); >> + if (strcmp(path, "/") =3D=3D 0) { >> + stbuf->st_mode =3D S_IFDIR | 0755; >> + stbuf->st_nlink =3D 2; >> + } else if (strcmp(path + 1, FILE) =3D=3D 0) { >> + char data[64]; >> + >> + stbuf->st_mode =3D S_IFREG | 0444; >> + stbuf->st_nlink =3D 1; >> + stbuf->st_size =3D sprintf(data, "%d\n", readlink_counte= r); >> + } else if (strcmp(path + 1, LINK) =3D=3D 0) { >> + stbuf->st_mode =3D S_IFLNK | 0444; >> + stbuf->st_nlink =3D 1; >> + stbuf->st_size =3D strlen(FILE); >> + } else >> + res =3D -ENOENT; >> + >> + return res; >> +} >> + >> +static int symlink_readlink(const char *path, char *buf, size_t size) >> +{ >> + if (strcmp(path + 1, LINK) !=3D 0) >> + return -ENOENT; >> + >> + memcpy(buf, FILE, strlen(FILE)); >> + readlink_counter++; >> + >> + return 0; >> +} >> + >> +static int symlink_read(const char *path, char *buf, size_t sz, off_t o= ff, >> + struct fuse_file_info *fi) >> +{ >> + char data[64]; >> + int len; >> + >> + len =3D sprintf(data, "%d\n", readlink_counter); >> + memcpy(buf, data, len); >> + >> + return len; >> +} >> + >> +static const struct fuse_operations symlink_oper =3D { >> + .init =3D symlink_init, >> + .getattr =3D symlink_getattr, >> + .readlink =3D symlink_readlink, >> + .read =3D symlink_read, >> +}; >> + >> +int main(int argc, char *argv[]) >> +{ >> + int ret; >> + struct fuse_args args =3D FUSE_ARGS_INIT(argc, argv); >> + >> + options.cache_symlinks =3D 0; >> + if (fuse_opt_parse(&args, &options, option_spec, NULL) =3D=3D -1) >> + return 1; >> + >> + ret =3D fuse_main(args.argc, args.argv, &symlink_oper, NULL); >> + fuse_opt_free_args(&args); >> + >> + return ret; >> +} >> diff --git a/tools/testing/selftests/filesystems/fuse/symlink_test.sh b/= tools/testing/selftests/filesystems/fuse/symlink_test.sh >> new file mode 100755 >> index 000000000000..546541c1920e >> --- /dev/null >> +++ b/tools/testing/selftests/filesystems/fuse/symlink_test.sh >> @@ -0,0 +1,52 @@ >> +#!/bin/sh >> +# SPDX-License-Identifier: GPL-2.0 >> + >> +exit_cleanup() >> +{ >> + fusermount -u ./mnt >> + rmdir ./mnt >> +} >> + >> +assert_value () >> +{ >> + if [ $1 -ne $2 ]; then >> + echo "FAILED" >> + echo $3 >> + exit 1 >> + fi >> +} >> + >> +set -e >> + >> +trap exit_cleanup EXIT >> + >> +mkdir -p mnt >> + >> +./symlink_fs ./mnt >> + >> +echo -n "Testing symlink without cached: " >> + >> +# When symlink caching is disabled every access to a symlink is expecte= d to >> +# result in a call to user-space >> +for i in $(seq 1 10); do >> + readlink ./mnt/link > /dev/null >> +done >> + >> +res=3D$(cat ./mnt/file) >> +assert_value $res $i "Got $res, expected $i" >> +echo "PASSED" >> + >> +fusermount -u ./mnt >> + >> +./symlink_fs --cache ./mnt >> + >> +echo -n "Testing symlink with cache: " >> + >> +# With caching enabled, there will only be a single call into user-space >> +for i in $(seq 0 10); do >> + readlink ./mnt/link > /dev/null >> +done >> + >> +res=3D$(cat ./mnt/file) >> +assert_value 1 $res "Got $ res, expected 1" >> +echo "PASSED" >> > > As we discussed, consider doing this as a single c test program, > but not a must as you wish. Sure, next iteration will have this test as a stand-alone c program. Cheers, --=20 Lu=C3=ADs