On Wed, Sep 16, 2026 at 5:55 PM Luis Henriques <[email protected]> wrote:
>
> 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 requested.
>
> Signed-off-by: Luis Henriques <[email protected]>
> ---
>  .../selftests/filesystems/fuse/.gitignore     |   1 +
>  .../selftests/filesystems/fuse/Makefile       |   2 +
>  .../fuse/fuse_symlink_cache_test.c            | 163 ++++++++++++++++++
>  3 files changed, 166 insertions(+)
>  create mode 100644 
> tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
>
> diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore 
> b/tools/testing/selftests/filesystems/fuse/.gitignore
> index f7f3dd345a50..ebfe7133d811 100644
> --- a/tools/testing/selftests/filesystems/fuse/.gitignore
> +++ b/tools/testing/selftests/filesystems/fuse/.gitignore
> @@ -3,3 +3,4 @@ fuse_mnt
>  fusectl_test
>  write_extend_eof_test
>  fuse_acl_cache_test
> +fuse_symlink_cache_test
> diff --git a/tools/testing/selftests/filesystems/fuse/Makefile 
> b/tools/testing/selftests/filesystems/fuse/Makefile
> index 7744f796eb06..3a5a557dde7a 100644
> --- a/tools/testing/selftests/filesystems/fuse/Makefile
> +++ b/tools/testing/selftests/filesystems/fuse/Makefile
> @@ -5,6 +5,7 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
>  TEST_GEN_PROGS := fusectl_test
>  TEST_GEN_PROGS += write_extend_eof_test
>  TEST_GEN_PROGS += fuse_acl_cache_test
> +TEST_GEN_PROGS += fuse_symlink_cache_test
>  TEST_GEN_FILES := fuse_mnt
>
>  include ../../lib.mk
> @@ -28,5 +29,6 @@ $(OUTPUT)/fuse_mnt: CFLAGS += $(VAR_CFLAGS)
>  $(OUTPUT)/fuse_mnt: LDLIBS += $(VAR_LDLIBS)
>
>  $(OUTPUT)/fuse_acl_cache_test: fuse_common.c fuse_acl_cache_test.c
> +$(OUTPUT)/fuse_symlink_cache_test: fuse_common.c fuse_symlink_cache_test.c
>
>  EXTRA_CLEAN := fuse_common.o
> diff --git 
> a/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c 
> b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
> new file mode 100644
> index 000000000000..dd001736ebca
> --- /dev/null
> +++ b/tools/testing/selftests/filesystems/fuse/fuse_symlink_cache_test.c
> @@ -0,0 +1,163 @@
> +// 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 named
> + * 'file' and a symlink to that file named 'link'.  If symlink caching is
> + * disabled (i.e. FUSE_CAP_CACHE_SYMLINKS is reset during FUSE_INIT), 
> whenever
> + * the ->readlink() is executed to resolve 'link' a counter will be 
> incremented.
> + *
> + * If symlink caching is enabled (i.e. FUSE_CAP_CACHE_SYMLINKS is set during
> + * FUSE_INIT), resolving a symlink will only call into user-space the first
> + * time.
> + */
> +
> +#define FUSE_USE_VERSION 31
> +
> +#include <stdio.h>
> +#include <fuse_lowlevel.h>
> +
> +#include "kselftest_harness.h"
> +
> +#include "fuse_common.h"
> +
> +#define FILENAME "file"
> +#define FILE_INO 42
> +
> +#define LINKNAME "link"
> +#define LINK_INO 43
> +
> +#define TIMEOUT        86400.0f
> +
> +struct test_state {
> +       pthread_mutex_t lock;
> +       struct fuse_common_ctx ctx;
> +       int readlink_counter;
> +       bool cache;
> +} test_state = {
> +       .lock = PTHREAD_MUTEX_INITIALIZER,
> +};
> +
> +static void fs_init(void *userdata, struct fuse_conn_info *conn)
> +{
> +       pthread_mutex_lock(&test_state.lock);
> +       if (test_state.cache)
> +               fuse_set_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS);
> +       else
> +               fuse_unset_feature_flag(conn, FUSE_CAP_CACHE_SYMLINKS);
> +       pthread_mutex_unlock(&test_state.lock);
> +}
> +
> +static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
> +{
> +       struct fuse_entry_param e = {};
> +
> +       if (parent != FUSE_ROOT_ID ||
> +           (strcmp(name, FILENAME) != 0 && strcmp(name, LINKNAME) != 0)) {
> +               fuse_reply_err(req, ENOENT);
> +               return;
> +       }
> +
> +       if (!strcmp(name, FILENAME)) {
> +               e.ino = FILE_INO;
> +               e.attr.st_mode = S_IFREG | 0444;
> +               e.attr.st_nlink = 2;
> +       } else if (!strcmp(name, LINKNAME)) {
> +               e.ino = LINK_INO;
> +               e.attr.st_mode = S_IFLNK | 0444;
> +               e.attr.st_nlink = 1;
> +               e.attr.st_size = strlen(FILENAME);
> +       }
> +       e.attr_timeout = TIMEOUT;
> +       e.entry_timeout = TIMEOUT;
> +
> +       fuse_reply_entry(req, &e);
> +}
> +
> +static void fs_readlink(fuse_req_t req, fuse_ino_t ino)
> +{
> +       char buf[PATH_MAX];
> +       size_t sz = strlen(FILENAME);
> +
> +       if (ino != LINK_INO) {
> +               fuse_reply_err(req, ENOENT);
> +               return;
> +       }
> +
> +       memcpy(buf, FILENAME, sz);
> +       buf[sz] = '\0';
> +       pthread_mutex_lock(&test_state.lock);
> +       test_state.readlink_counter++;
> +       pthread_mutex_unlock(&test_state.lock);
> +
> +       fuse_reply_readlink(req, buf);
> +}
> +
> +static const struct fuse_lowlevel_ops fs_ops = {
> +       .init           = fs_init,
> +       .lookup         = fs_lookup,
> +       .readlink       = fs_readlink,
> +};
> +
> +FIXTURE(symlink_cache) {};
> +FIXTURE_VARIANT(symlink_cache)
> +{
> +       const bool cache;
> +};
> +FIXTURE_VARIANT_ADD(symlink_cache, symlinks_nocache)
> +{
> +       /* Variant with symlink cache disabled */
> +       .cache = false,
> +};
> +FIXTURE_VARIANT_ADD(symlink_cache, symlinks_cache)
> +{
> +       /* Variant with symlink cache enabled */
> +       .cache = true,
> +};
> +
> +FIXTURE_SETUP(symlink_cache)
> +{
> +       char err[MAX_ERR_MSG];
> +
> +       pthread_mutex_lock(&test_state.lock);
> +       test_state.readlink_counter = 0;
> +       test_state.cache = variant->cache;
> +
> +       if (fs_setup(&fs_ops, &test_state.ctx, err))
> +               SKIP(goto out, err);
> +out:
> +       pthread_mutex_unlock(&test_state.lock);
> +}
> +
> +FIXTURE_TEARDOWN(symlink_cache)
> +{
> +       pthread_mutex_lock(&test_state.lock);
> +       fs_teardown(&test_state.ctx);
> +       pthread_mutex_unlock(&test_state.lock);
> +}
> +

Here too, unless you have a good reason not to,
please put fuse_common_ctx in the test fixture object.

With that fix, feel free to add

Reviewed-by: Amir Goldstein <[email protected]>

Thanks,
Amir.

> +TEST_F(symlink_cache, test_symlink_cache)
> +{
> +       char pathname[PATH_MAX];
> +       char buf[PATH_MAX];
> +       ssize_t sz;
> +       int counter;
> +       int i;
> +
> +       sprintf(pathname, "%s/%s", test_state.ctx.mountpoint, LINKNAME);
> +       for (i = 0; i < 100; i++) {
> +               sz = readlink(pathname, buf, PATH_MAX);
> +               ASSERT_NE(sz, -1);
> +       }
> +       pthread_mutex_lock(&test_state.lock);
> +       counter = test_state.readlink_counter;
> +       pthread_mutex_unlock(&test_state.lock);
> +
> +       if (variant->cache) {
> +               ASSERT_EQ(counter, 1);
> +       } else {
> +               ASSERT_EQ(counter, 100);
> +       }
> +}
> +
> +TEST_HARNESS_MAIN

Reply via email to