diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 32037557c7b4..75670bfaa79f 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -701,6 +701,8 @@ file .. fs + fuse + .. tmpfs .. .. diff --git a/tests/sys/fs/Makefile b/tests/sys/fs/Makefile index c82ee143a785..ec7ebdfc449b 100644 --- a/tests/sys/fs/Makefile +++ b/tests/sys/fs/Makefile @@ -7,6 +7,7 @@ TESTSDIR= ${TESTSBASE}/sys/fs TESTSRC= ${SRCTOP}/contrib/netbsd-tests/fs #TESTS_SUBDIRS+= nullfs # XXX: needs rump +TESTS_SUBDIRS+= fuse TESTS_SUBDIRS+= tmpfs ${PACKAGE}FILES+= h_funcs.subr diff --git a/tests/sys/fs/fuse/Makefile b/tests/sys/fs/fuse/Makefile new file mode 100644 index 000000000000..9bf734130ca0 --- /dev/null +++ b/tests/sys/fs/fuse/Makefile @@ -0,0 +1,47 @@ +# $FreeBSD$ + +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/fs/fuse + +ATF_TESTS_CXX+= getattr +ATF_TESTS_CXX+= lookup + +SRCS.getattr+= getattr.cc +SRCS.getattr+= getmntopts.c +SRCS.getattr+= mockfs.cc +SRCS.getattr+= utils.cc + +SRCS.lookup+= lookup.cc +SRCS.lookup+= getmntopts.c +SRCS.lookup+= mockfs.cc +SRCS.lookup+= utils.cc + +TEST_METADATA+= timeout=10 +TEST_METADATA+= required_user=root + +FUSEFS= ${.CURDIR:H:H:H:H}/sys/fs/fuse +MOUNT= ${.CURDIR:H:H:H:H}/sbin/mount +CFLAGS+= -I${.CURDIR:H:H:H} +CFLAGS+= -I${FUSEFS} +CFLAGS+= -I${MOUNT} +.PATH: ${MOUNT} + +LIBADD+= pthread +WARNS?= 6 +NO_WTHREAD_SAFETY= # GoogleTest fails Clang's thread safety check + +# Use googlemock from ports until after the import-googletest-1.8.1 branch +# merges to head. +CXXFLAGS+= -I/usr/local/include +CXXFLAGS+= -DGTEST_HAS_POSIX_RE=1 +CXXFLAGS+= -DGTEST_HAS_PTHREAD=1 +CXXFLAGS+= -DGTEST_HAS_STREAM_REDIRECTION=1 +CXXFLAGS+= -frtti +CXXFLAGS+= -std=c++14 +LDADD+= ${LOCALBASE}/lib/libgmock.a +LDADD+= ${LOCALBASE}/lib/libgtest.a +# Without -lpthread, gtest fails at _runtime_ with the error pthread_key_create(&key, &DeleteThreadLocalValue)failed with error 78 +LIBADD+= pthread + +.include diff --git a/tests/sys/fs/fuse/getattr.cc b/tests/sys/fs/fuse/getattr.cc new file mode 100644 index 000000000000..231d6c530f43 --- /dev/null +++ b/tests/sys/fs/fuse/getattr.cc @@ -0,0 +1,241 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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. + */ + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Getattr : public FuseTest {}; + +/* + * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */ +TEST_F(Getattr, DISABLED_attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const uint64_t generation = 13; + struct stat sb; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = ino; + out->body.entry.generation = generation; + })); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, attr); + out->body.attr.attr_valid = UINT64_MAX; + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | 0644; + })); + EXPECT_EQ(0, stat(FULLPATH, &sb)); + /* The second stat(2) should use cached attributes */ + EXPECT_EQ(0, stat(FULLPATH, &sb)); +} + +/* + * If getattr returns a finite but non-zero cache timeout, then we should + * discard the cached attributes and requery the daemon after the timeout + * period passes. + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */ +TEST_F(Getattr, attr_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const uint64_t generation = 13; + struct stat sb; + /* + * The timeout should be longer than the longest plausible time the + * daemon would take to complete a write(2) to /dev/fuse, but no longer. + */ + long timeout_ns = 250'000'000; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.entry_valid = UINT64_MAX; + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = ino; + out->body.entry.generation = generation; + })); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, attr); + out->body.attr.attr_valid_nsec = timeout_ns; + out->body.attr.attr_valid = UINT64_MAX; + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | 0644; + })); + EXPECT_EQ(0, stat(FULLPATH, &sb)); + usleep(2 * timeout_ns / 1000); + /* Timeout has expire. stat(2) should requery the daemon */ + EXPECT_EQ(0, stat(FULLPATH, &sb)); +} + +TEST_F(Getattr, enoent) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb; + const uint64_t ino = 42; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = 0100644; + out->body.entry.nodeid = ino; + })); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + out->header.error = -ENOENT; + out->header.len = sizeof(out->header); + })); + EXPECT_NE(0, stat(FULLPATH, &sb)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(Getattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const uint64_t generation = 13; + struct stat sb; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = ino; + out->body.entry.generation = generation; + })); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, attr); + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | 0644; + out->body.attr.attr.size = 1; + out->body.attr.attr.blocks = 2; + out->body.attr.attr.atime = 3; + out->body.attr.attr.mtime = 4; + out->body.attr.attr.ctime = 5; + out->body.attr.attr.atimensec = 6; + out->body.attr.attr.mtimensec = 7; + out->body.attr.attr.ctimensec = 8; + out->body.attr.attr.nlink = 9; + out->body.attr.attr.uid = 10; + out->body.attr.attr.gid = 11; + out->body.attr.attr.rdev = 12; + })); + + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + // fuse(4) does not _yet_ support inode generations + //EXPECT_EQ(generation, sb.st_gen); + + //st_birthtim and st_flags are not supported by protocol 7.8. They're + //only supported as OS-specific extensions to OSX. + //EXPECT_EQ(, sb.st_birthtim); + //EXPECT_EQ(, sb.st_flags); + + //FUSE can't set st_blksize until protocol 7.9 +} diff --git a/tests/sys/fs/fuse/lookup.cc b/tests/sys/fs/fuse/lookup.cc new file mode 100644 index 000000000000..6c24fdf500d5 --- /dev/null +++ b/tests/sys/fs/fuse/lookup.cc @@ -0,0 +1,258 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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 +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Lookup: public FuseTest {}; + +/* + * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */ +TEST_F(Lookup, DISABLED_attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.nodeid = ino; + out->body.entry.attr_valid = UINT64_MAX; + out->body.entry.attr.ino = ino; // Must match nodeid + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.attr.size = 1; + out->body.entry.attr.blocks = 2; + out->body.entry.attr.atime = 3; + out->body.entry.attr.mtime = 4; + out->body.entry.attr.ctime = 5; + out->body.entry.attr.atimensec = 6; + out->body.entry.attr.mtimensec = 7; + out->body.entry.attr.ctimensec = 8; + out->body.entry.attr.nlink = 9; + out->body.entry.attr.uid = 10; + out->body.entry.attr.gid = 11; + out->body.entry.attr.rdev = 12; + })); + /* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + // fuse(4) does not _yet_ support inode generations + //EXPECT_EQ(generation, sb.st_gen); + + //st_birthtim and st_flags are not supported by protocol 7.8. They're + //only supported as OS-specific extensions to OSX. + //EXPECT_EQ(, sb.st_birthtim); + //EXPECT_EQ(, sb.st_flags); + + //FUSE can't set st_blksize until protocol 7.9 +} + +/* + * If lookup returns a finite but non-zero cache timeout, then we should discard + * the cached attributes and requery the daemon. + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */ +TEST_F(Lookup, attr_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + /* + * The timeout should be longer than the longest plausible time the + * daemon would take to complete a write(2) to /dev/fuse, but no longer. + */ + long timeout_ns = 250'000'000; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.nodeid = ino; + out->body.entry.attr_valid_nsec = timeout_ns; + out->body.entry.attr.ino = ino; // Must match nodeid + out->body.entry.attr.mode = S_IFREG | 0644; + })); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, attr); + out->body.attr.attr.ino = ino; // Must match nodeid + out->body.attr.attr.mode = S_IFREG | 0644; + })); + + /* access(2) will issue a VOP_LOOKUP but not a VOP_GETATTR */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + usleep(2 * timeout_ns / 1000); + /* The cache has timed out; VOP_GETATTR should query the daemon*/ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); +} + +TEST_F(Lookup, enoent) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in->header.opcode == FUSE_LOOKUP); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + out->header.error = -ENOENT; + out->header.len = sizeof(out->header); + })); + EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +/* + * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs + * should use the cached inode rather than requery the daemon + */ +TEST_F(Lookup, entry_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.entry_valid = UINT64_MAX; + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = 14; + })); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + /* The second access(2) should use the cache */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * If lookup returns a finite but non-zero entry cache timeout, then we should + * discard the cached inode and requery the daemon + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */ +TEST_F(Lookup, DISABLED_entry_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + /* + * The timeout should be longer than the longest plausible time the + * daemon would take to complete a write(2) to /dev/fuse, but no longer. + */ + long timeout_ns = 250'000'000; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.entry_valid_nsec = timeout_ns; + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = 14; + })); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + usleep(2 * timeout_ns / 1000); + /* The cache has timed out; VOP_LOOKUP should query the daemon*/ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +TEST_F(Lookup, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_LOOKUP && + strcmp(in->body.lookup, RELPATH) == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, entry); + out->body.entry.attr.mode = S_IFREG | 0644; + out->body.entry.nodeid = 14; + })); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} diff --git a/tests/sys/fs/fuse/mockfs.cc b/tests/sys/fs/fuse/mockfs.cc new file mode 100644 index 000000000000..cab79529d533 --- /dev/null +++ b/tests/sys/fs/fuse/mockfs.cc @@ -0,0 +1,251 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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 + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "mntopts.h" // for build_iovec +} + +#include + +#include "mockfs.hh" + +using namespace testing; + +int verbosity = 0; +static sig_atomic_t quit = 0; + +const char* opcode2opname(uint32_t opcode) +{ + const int NUM_OPS = 39; + const char* table[NUM_OPS] = { + "Unknown (opcode 0)", + "FUSE_LOOKUP", + "FUSE_FORGET", + "FUSE_GETATTR", + "FUSE_SETATTR", + "FUSE_READLINK", + "FUSE_SYMLINK", + "Unknown (opcode 7)", + "FUSE_MKNOD", + "FUSE_MKDIR", + "FUSE_UNLINK", + "FUSE_RMDIR", + "FUSE_RENAME", + "FUSE_LINK", + "FUSE_OPEN", + "FUSE_READ", + "FUSE_WRITE", + "FUSE_STATFS", + "FUSE_RELEASE", + "Unknown (opcode 19)", + "FUSE_FSYNC", + "FUSE_SETXATTR", + "FUSE_GETXATTR", + "FUSE_LISTXATTR", + "FUSE_REMOVEXATTR", + "FUSE_FLUSH", + "FUSE_INIT", + "FUSE_OPENDIR", + "FUSE_READDIR", + "FUSE_RELEASEDIR", + "FUSE_FSYNCDIR", + "FUSE_GETLK", + "FUSE_SETLK", + "FUSE_SETLKW", + "FUSE_ACCESS", + "FUSE_CREATE", + "FUSE_INTERRUPT", + "FUSE_BMAP", + "FUSE_DESTROY" + }; + if (opcode >= NUM_OPS) + return ("Unknown (opcode > max)"); + else + return (table[opcode]); +} + +void sigint_handler(int __unused sig) { + quit = 1; +} + +MockFS::MockFS() { + struct iovec *iov = NULL; + int iovlen = 0; + char fdstr[15]; + + /* + * Kyua sets pwd to a testcase-unique tempdir; no need to use + * mkdtemp + */ + /* + * googletest doesn't allow ASSERT_ in constructors, so we must throw + * instead. + */ + if (mkdir("mountpoint" , 0644) && errno != EEXIST) + throw(std::system_error(errno, std::system_category(), + "Couldn't make mountpoint directory")); + + m_fuse_fd = open("/dev/fuse", O_RDWR); + if (m_fuse_fd < 0) + throw(std::system_error(errno, std::system_category(), + "Couldn't open /dev/fuse")); + sprintf(fdstr, "%d", m_fuse_fd); + + m_pid = getpid(); + + build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1); + build_iovec(&iov, &iovlen, "fspath", + __DECONST(void *, "mountpoint"), -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + build_iovec(&iov, &iovlen, "fd", fdstr, -1); + if (nmount(iov, iovlen, 0)) + throw(std::system_error(errno, std::system_category(), + "Couldn't mount filesystem")); + + // Setup default handler + ON_CALL(*this, process(_, _)) + .WillByDefault(Invoke(this, &MockFS::process_default)); + + init(); + if (pthread_create(&m_thr, NULL, service, (void*)this)) + throw(std::system_error(errno, std::system_category(), + "Couldn't Couldn't start fuse thread")); +} + +MockFS::~MockFS() { + pthread_kill(m_daemon_id, SIGUSR1); + // Closing the /dev/fuse file descriptor first allows unmount to + // succeed even if the daemon doesn't correctly respond to commands + // during the unmount sequence. + close(m_fuse_fd); + pthread_join(m_daemon_id, NULL); + ::unmount("mountpoint", MNT_FORCE); + rmdir("mountpoint"); +} + +void MockFS::init() { + mockfs_buf_in *in; + mockfs_buf_out out; + + in = (mockfs_buf_in*) malloc(sizeof(*in)); + ASSERT_TRUE(in != NULL); + + read_request(in); + ASSERT_EQ(FUSE_INIT, in->header.opcode); + + memset(&out, 0, sizeof(out)); + out.header.unique = in->header.unique; + out.header.error = 0; + out.body.init.major = FUSE_KERNEL_VERSION; + out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; + SET_OUT_HEADER_LEN(&out, init); + write(m_fuse_fd, &out, out.header.len); + + free(in); +} + +void MockFS::loop() { + mockfs_buf_in *in; + mockfs_buf_out out; + + in = (mockfs_buf_in*) malloc(sizeof(*in)); + ASSERT_TRUE(in != NULL); + while (!quit) { + bzero(in, sizeof(*in)); + bzero(&out, sizeof(out)); + read_request(in); + if (quit) + break; + if (verbosity > 0) { + printf("Got request %s\n", + opcode2opname(in->header.opcode)); + } + if ((pid_t)in->header.pid != m_pid) { + /* + * Reject any requests from unknown processes. Because + * we actually do mount a filesystem, plenty of + * unrelated system daemons may try to access it. + */ + process_default(in, &out); + } else { + process(in, &out); + } + if (in->header.opcode == FUSE_FORGET) { + /*Alone among the opcodes, FORGET expects no response*/ + continue; + } + ASSERT_TRUE(write(m_fuse_fd, &out, out.header.len) > 0 || + errno == EAGAIN) + << strerror(errno); + } + free(in); +} + +void MockFS::process_default(const mockfs_buf_in *in, mockfs_buf_out* out) { + out->header.unique = in->header.unique; + out->header.error = -EOPNOTSUPP; + out->header.len = sizeof(out->header); +} + +void MockFS::read_request(mockfs_buf_in *in) { + ssize_t res; + + res = read(m_fuse_fd, in, sizeof(*in)); + if (res < 0 && !quit) + perror("read"); + ASSERT_TRUE(res >= (ssize_t)sizeof(in->header) || quit); +} + +void* MockFS::service(void *pthr_data) { + MockFS *mock_fs = (MockFS*)pthr_data; + mock_fs->m_daemon_id = pthread_self(); + + quit = 0; + signal(SIGUSR1, sigint_handler); + + mock_fs->loop(); + + return (NULL); +} + +void MockFS::unmount() { + ::unmount("mountpoint", 0); +} diff --git a/tests/sys/fs/fuse/mockfs.hh b/tests/sys/fs/fuse/mockfs.hh new file mode 100644 index 000000000000..649c09811ba2 --- /dev/null +++ b/tests/sys/fs/fuse/mockfs.hh @@ -0,0 +1,131 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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 + +#include + +#include "fuse_kernel.h" +} + +#include + +#define SET_OUT_HEADER_LEN(out, variant) { \ + (out)->header.len = (sizeof((out)->header) + \ + sizeof((out)->body.variant)); \ +} + +extern int verbosity; + +union fuse_payloads_in { + fuse_forget_in forget; + fuse_init_in init; + char lookup[0]; + /* value is from fuse_kern_chan.c in fusefs-libs */ + uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)]; +}; + +struct mockfs_buf_in { + fuse_in_header header; + union fuse_payloads_in body; +}; + +union fuse_payloads_out { + fuse_init_out init; + fuse_entry_out entry; + fuse_attr_out attr; +}; + +struct mockfs_buf_out { + fuse_out_header header; + union fuse_payloads_out body; +}; + +/* + * Fake FUSE filesystem + * + * "Mounts" a filesystem to a temporary directory and services requests + * according to the programmed expectations. + * + * Operates directly on the fuse(4) kernel API, not the libfuse(3) user api. + */ +class MockFS { + public: + /* thread id of the fuse daemon thread */ + pthread_t m_daemon_id; + + private: + /* file descriptor of /dev/fuse control device */ + int m_fuse_fd; + + /* pid of the test process */ + pid_t m_pid; + + /* + * Thread that's running the mockfs daemon. + * + * It must run in a separate thread so it doesn't deadlock with the + * client test code. + */ + pthread_t m_thr; + + /* Initialize a session after mounting */ + void init(); + + /* Default request handler */ + void process_default(const mockfs_buf_in*, mockfs_buf_out*); + + /* Entry point for the daemon thread */ + static void* service(void*); + + /* Read, but do not process, a single request from the kernel */ + void read_request(mockfs_buf_in*); + + public: + /* Create a new mockfs and mount it to a tempdir */ + MockFS(); + virtual ~MockFS(); + + /* Process FUSE requests endlessly */ + void loop(); + + /* + * Request handler + * + * This method is expected to provide the response to each FUSE + * operation. Responses must be immediate (so this method can't be used + * for testing a daemon with queue depth > 1). Test cases must define + * each response using Googlemock expectations + */ + MOCK_METHOD2(process, void(const mockfs_buf_in*, mockfs_buf_out*)); + + /* Gracefully unmount */ + void unmount(); +}; diff --git a/tests/sys/fs/fuse/utils.cc b/tests/sys/fs/fuse/utils.cc new file mode 100644 index 000000000000..19b85cd3fd29 --- /dev/null +++ b/tests/sys/fs/fuse/utils.cc @@ -0,0 +1,57 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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. + */ + +#include +#include + +#include "mockfs.hh" + +static void usage(char* progname) { + fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname); + exit(2); +} + +int main(int argc, char **argv) { + int ch; + + ::testing::InitGoogleTest(&argc, argv); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbosity++; + break; + default: + usage(argv[0]); + break; + } + } + + return (RUN_ALL_TESTS()); +} diff --git a/tests/sys/fs/fuse/utils.hh b/tests/sys/fs/fuse/utils.hh new file mode 100644 index 000000000000..6923a5c95b79 --- /dev/null +++ b/tests/sys/fs/fuse/utils.hh @@ -0,0 +1,62 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * All rights reserved. + * + * 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. + */ + +#include + +#define GTEST_REQUIRE_KERNEL_MODULE(_mod_name) do { \ + if (modfind(_mod_name) == -1) { \ + printf("module %s could not be resolved: %s\n", \ + _mod_name, strerror(errno)); \ + /* + * TODO: enable GTEST_SKIP once GoogleTest 1.8.2 merges + * GTEST_SKIP() + */ \ + FAIL() << "Module " << _mod_name << " could not be resolved\n";\ + } \ +} while(0) + +class FuseTest : public ::testing::Test { + protected: + MockFS *m_mock = NULL; + + public: + void SetUp() { + GTEST_REQUIRE_KERNEL_MODULE("fuse"); + try { + m_mock = new MockFS{}; + } catch (std::system_error err) { + FAIL() << err.what(); + } + } + + void TearDown() { + if (m_mock) + delete m_mock; + } +};