aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/mockfs.hh
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs/mockfs.hh')
-rw-r--r--tests/sys/fs/fusefs/mockfs.hh446
1 files changed, 446 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh
new file mode 100644
index 000000000000..f98a5337c9d1
--- /dev/null
+++ b/tests/sys/fs/fusefs/mockfs.hh
@@ -0,0 +1,446 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2019 The FreeBSD Foundation
+ *
+ * This software was developed by BFF Storage Systems, LLC under sponsorship
+ * from the FreeBSD Foundation.
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ */
+
+extern "C" {
+#include <sys/types.h>
+
+#include <pthread.h>
+
+#include "fuse_kernel.h"
+}
+
+#include <unordered_set>
+
+#include <gmock/gmock.h>
+
+#define TIME_T_MAX (std::numeric_limits<time_t>::max())
+
+/*
+ * A pseudo-fuse errno used indicate that a fuse operation should have no
+ * response, at least not immediately
+ */
+#define FUSE_NORESPONSE 9999
+
+#define SET_OUT_HEADER_LEN(out, variant) { \
+ (out).header.len = (sizeof((out).header) + \
+ sizeof((out).body.variant)); \
+}
+
+/*
+ * Create an expectation on FUSE_LOOKUP and return it so the caller can set
+ * actions.
+ *
+ * This must be a macro instead of a method because EXPECT_CALL returns a type
+ * with a deleted constructor.
+ */
+#define EXPECT_LOOKUP(parent, path) \
+ EXPECT_CALL(*m_mock, process( \
+ ResultOf([=](auto in) { \
+ return (in.header.opcode == FUSE_LOOKUP && \
+ in.header.nodeid == (parent) && \
+ strcmp(in.body.lookup, (path)) == 0); \
+ }, Eq(true)), \
+ _) \
+ )
+
+extern int verbosity;
+
+/*
+ * The maximum that a test case can set max_write, limited by the buffer
+ * supplied when reading from /dev/fuse. This limitation is imposed by
+ * fusefs-libs, but not by the FUSE protocol.
+ */
+const uint32_t max_max_write = 0x20000;
+
+
+/* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */
+struct fuse_create_out {
+ struct fuse_entry_out entry;
+ struct fuse_open_out open;
+};
+
+/* Protocol 7.8 version of struct fuse_attr */
+struct fuse_attr_7_8
+{
+ uint64_t ino;
+ uint64_t size;
+ uint64_t blocks;
+ uint64_t atime;
+ uint64_t mtime;
+ uint64_t ctime;
+ uint32_t atimensec;
+ uint32_t mtimensec;
+ uint32_t ctimensec;
+ uint32_t mode;
+ uint32_t nlink;
+ uint32_t uid;
+ uint32_t gid;
+ uint32_t rdev;
+};
+
+/* Protocol 7.8 version of struct fuse_attr_out */
+struct fuse_attr_out_7_8
+{
+ uint64_t attr_valid;
+ uint32_t attr_valid_nsec;
+ uint32_t dummy;
+ struct fuse_attr_7_8 attr;
+};
+
+/* Protocol 7.8 version of struct fuse_entry_out */
+struct fuse_entry_out_7_8 {
+ uint64_t nodeid; /* Inode ID */
+ uint64_t generation; /* Inode generation: nodeid:gen must
+ be unique for the fs's lifetime */
+ uint64_t entry_valid; /* Cache timeout for the name */
+ uint64_t attr_valid; /* Cache timeout for the attributes */
+ uint32_t entry_valid_nsec;
+ uint32_t attr_valid_nsec;
+ struct fuse_attr_7_8 attr;
+};
+
+/* Output struct for FUSE_CREATE for protocol 7.8 servers */
+struct fuse_create_out_7_8 {
+ struct fuse_entry_out_7_8 entry;
+ struct fuse_open_out open;
+};
+
+/* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */
+struct fuse_init_out_7_22 {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t max_readahead;
+ uint32_t flags;
+ uint16_t max_background;
+ uint16_t congestion_threshold;
+ uint32_t max_write;
+};
+
+union fuse_payloads_in {
+ fuse_access_in access;
+ fuse_bmap_in bmap;
+ /*
+ * In fusefs-libs 3.4.2 and below the buffer size is fixed at 0x21000
+ * minus the header sizes. fusefs-libs 3.4.3 (and FUSE Protocol 7.29)
+ * add a FUSE_MAX_PAGES option that allows it to be greater.
+ *
+ * See fuse_kern_chan.c in fusefs-libs 2.9.9 and below, or
+ * FUSE_DEFAULT_MAX_PAGES_PER_REQ in fusefs-libs 3.4.3 and above.
+ */
+ uint8_t bytes[
+ max_max_write + 0x1000 - sizeof(struct fuse_in_header)
+ ];
+ fuse_copy_file_range_in copy_file_range;
+ fuse_create_in create;
+ fuse_fallocate_in fallocate;
+ fuse_flush_in flush;
+ fuse_fsync_in fsync;
+ fuse_fsync_in fsyncdir;
+ fuse_forget_in forget;
+ fuse_getattr_in getattr;
+ fuse_interrupt_in interrupt;
+ fuse_lk_in getlk;
+ fuse_getxattr_in getxattr;
+ fuse_init_in init;
+ fuse_link_in link;
+ fuse_listxattr_in listxattr;
+ char lookup[0];
+ fuse_lseek_in lseek;
+ fuse_mkdir_in mkdir;
+ fuse_mknod_in mknod;
+ fuse_open_in open;
+ fuse_open_in opendir;
+ fuse_read_in read;
+ fuse_read_in readdir;
+ fuse_release_in release;
+ fuse_release_in releasedir;
+ fuse_rename_in rename;
+ char rmdir[0];
+ fuse_setattr_in setattr;
+ fuse_setxattr_in setxattr;
+ fuse_lk_in setlk;
+ fuse_lk_in setlkw;
+ char unlink[0];
+ fuse_write_in write;
+};
+
+struct mockfs_buf_in {
+ fuse_in_header header;
+ union fuse_payloads_in body;
+};
+
+union fuse_payloads_out {
+ fuse_attr_out attr;
+ fuse_attr_out_7_8 attr_7_8;
+ fuse_bmap_out bmap;
+ fuse_create_out create;
+ fuse_create_out_7_8 create_7_8;
+ /*
+ * The protocol places no limits on the size of bytes. Choose
+ * a size big enough for anything we'll test.
+ */
+ uint8_t bytes[0x40000];
+ fuse_entry_out entry;
+ fuse_entry_out_7_8 entry_7_8;
+ fuse_lk_out getlk;
+ fuse_getxattr_out getxattr;
+ fuse_init_out init;
+ fuse_init_out_7_22 init_7_22;
+ fuse_lseek_out lseek;
+ /* The inval_entry structure should be followed by the entry's name */
+ fuse_notify_inval_entry_out inval_entry;
+ fuse_notify_inval_inode_out inval_inode;
+ /* The store structure should be followed by the data to store */
+ fuse_notify_store_out store;
+ fuse_listxattr_out listxattr;
+ fuse_open_out open;
+ fuse_statfs_out statfs;
+ /*
+ * The protocol places no limits on the length of the string. This is
+ * merely convenient for testing.
+ */
+ char str[80];
+ fuse_write_out write;
+};
+
+struct mockfs_buf_out {
+ fuse_out_header header;
+ union fuse_payloads_out body;
+ /* the expected errno of the write to /dev/fuse */
+ int expected_errno;
+
+ /* Default constructor: zero everything */
+ mockfs_buf_out() {
+ memset(this, 0, sizeof(*this));
+ }
+};
+
+/* A function that can be invoked in place of MockFS::process */
+typedef std::function<void (const mockfs_buf_in& in,
+ std::vector<std::unique_ptr<mockfs_buf_out>> &out)>
+ProcessMockerT;
+
+/*
+ * Helper function used for setting an error expectation for any fuse operation.
+ * The operation will return the supplied error
+ */
+ProcessMockerT ReturnErrno(int error);
+
+/* Helper function used for returning negative cache entries for LOOKUP */
+ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid);
+
+/* Helper function used for returning a single immediate response */
+ProcessMockerT ReturnImmediate(
+ std::function<void(const mockfs_buf_in& in,
+ struct mockfs_buf_out &out)> f);
+
+/* How the daemon should check /dev/fuse for readiness */
+enum poll_method {
+ BLOCKING,
+ SELECT,
+ POLL,
+ KQ
+};
+
+/*
+ * Fake FUSE filesystem
+ *
+ * "Mounts" a filesystem to a temporary directory and services requests
+ * according to the programmed expectations.
+ *
+ * Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api.
+ */
+class MockFS {
+ /*
+ * thread id of the fuse daemon thread
+ *
+ * It must run in a separate thread so it doesn't deadlock with the
+ * client test code.
+ */
+ pthread_t m_daemon_id;
+
+ /* file descriptor of /dev/fuse control device */
+ volatile int m_fuse_fd;
+
+ /* The minor version of the kernel API that this mock daemon targets */
+ uint32_t m_kernel_minor_version;
+
+ int m_kq;
+
+ /*
+ * If nonzero, the maximum size in bytes of a read that the kernel will
+ * send to the server.
+ */
+ int m_maxread;
+
+ /* The max_readahead file system option */
+ uint32_t m_maxreadahead;
+
+ /* pid of the test process */
+ pid_t m_pid;
+
+ /* Every "unique" value of a fuse ticket seen so far */
+ std::unique_ptr<std::unordered_set<uint64_t>> m_uniques;
+
+ /* Method the daemon should use for I/O to and from /dev/fuse */
+ enum poll_method m_pm;
+
+ /* Timestamp granularity in nanoseconds */
+ unsigned m_time_gran;
+
+ void audit_request(const mockfs_buf_in &in, ssize_t buflen);
+ void debug_request(const mockfs_buf_in&, ssize_t buflen);
+ void debug_response(const mockfs_buf_out&);
+
+ /* Initialize a session after mounting */
+ void init(uint32_t flags);
+
+ /* Is pid from a process that might be involved in the test? */
+ bool pid_ok(pid_t pid);
+
+ /* Default request handler */
+ void process_default(const mockfs_buf_in&,
+ std::vector<std::unique_ptr<mockfs_buf_out>>&);
+
+ /* Entry point for the daemon thread */
+ static void* service(void*);
+
+ /*
+ * Read, but do not process, a single request from the kernel
+ *
+ * @param in Return storage for the FUSE request
+ * @param res Return value of read(2). If positive, the amount of
+ * data read from the fuse device.
+ */
+ void read_request(mockfs_buf_in& in, ssize_t& res);
+
+ public:
+ /* Write a single response back to the kernel */
+ void write_response(const mockfs_buf_out &out);
+
+ /* pid of child process, for two-process test cases */
+ pid_t m_child_pid;
+
+ /* Maximum size of a FUSE_WRITE write */
+ uint32_t m_maxwrite;
+
+ /*
+ * Number of events that were available from /dev/fuse after the last
+ * kevent call. Only valid when m_pm = KQ.
+ */
+ int m_nready;
+
+ /* Tell the daemon to shut down ASAP */
+ bool m_quit;
+
+ /* Tell the daemon that the server might forcibly unmount us */
+ bool m_expect_unmount;
+
+ /* Create a new mockfs and mount it to a tempdir */
+ MockFS(int max_read, int max_readahead, bool allow_other,
+ bool default_permissions, bool push_symlinks_in, bool ro,
+ enum poll_method pm, uint32_t flags,
+ uint32_t kernel_minor_version, uint32_t max_write, bool async,
+ bool no_clusterr, unsigned time_gran, bool nointr,
+ bool noatime, const char *fsname, const char *subtype,
+ bool no_auto_init);
+
+ virtual ~MockFS();
+
+ /* Kill the filesystem daemon without unmounting the filesystem */
+ void kill_daemon();
+
+ /* Wait until the daemon thread terminates */
+ void join_daemon();
+
+ /* Process FUSE requests endlessly */
+ void loop();
+
+ /*
+ * Send an asynchronous notification to invalidate a directory entry.
+ * Similar to libfuse's fuse_lowlevel_notify_inval_entry
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param parent Parent directory's inode number
+ * @param name name of dirent to invalidate
+ * @param namelen size of name, including the NUL
+ * @param expected_errno The error that write() should return
+ */
+ int notify_inval_entry(ino_t parent, const char *name, size_t namelen,
+ int expected_errno = 0);
+
+ /*
+ * Send an asynchronous notification to invalidate an inode's cached
+ * data and/or attributes. Similar to libfuse's
+ * fuse_lowlevel_notify_inval_inode.
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param ino File's inode number
+ * @param off offset at which to begin invalidation. A
+ * negative offset means to invalidate attributes
+ * only.
+ * @param len Size of region of data to invalidate. 0 means
+ * to invalidate all cached data.
+ */
+ int notify_inval_inode(ino_t ino, off_t off, ssize_t len);
+
+ /*
+ * Send an asynchronous notification to store data directly into an
+ * inode's cache. Similar to libfuse's fuse_lowlevel_notify_store.
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param ino File's inode number
+ * @param off Offset at which to store data
+ * @param data Pointer to the data to cache
+ * @param len Size of data
+ */
+ int notify_store(ino_t ino, off_t off, const void* data, ssize_t size);
+
+ /*
+ * Request handler
+ *
+ * This method is expected to provide the responses to each FUSE
+ * operation. For an immediate response, push one buffer into out.
+ * For a delayed response, push nothing. For an immediate response
+ * plus a delayed response to an earlier operation, push two bufs.
+ * Test cases must define each response using Googlemock expectations
+ */
+ MOCK_METHOD2(process, void(const mockfs_buf_in&,
+ std::vector<std::unique_ptr<mockfs_buf_out>>&));
+
+ /* Gracefully unmount */
+ void unmount();
+};