Begin a fuse(4) test suite

It only tests the kernel portion of fuse, not the userspace portion (which
comes from sysutils/fusefs-libs).  The kernel-userspace interface is
de-facto standardized, and this test suite seeks to validate FreeBSD's
implementation.

It uses GoogleMock to substitute for a userspace daemon and validate the
kernel's behavior in response to filesystem access.  GoogleMock is
convenient because it can validate the order, number, and arguments of each
operation, and return canned responses.

But that also means that the test suite must use GoogleTest, since
GoogleMock is incompatible with atf-c++ and atf.test.mk does not allow C++
programs to use atf-c.

This commit adds the first 10 test cases out of an estimated 130 total.

PR:		235775, 235773
Sponsored by:	The FreeBSD Foundation
This commit is contained in:
Alan Somers 2019-03-01 23:53:05 +00:00
parent c02ccc7e44
commit 44154e682a
9 changed files with 1050 additions and 0 deletions

View File

@ -701,6 +701,8 @@
file
..
fs
fuse
..
tmpfs
..
..

View File

@ -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

View File

@ -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 <bsd.test.mk>

View File

@ -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
}

258
tests/sys/fs/fuse/lookup.cc Normal file
View File

@ -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 <unistd.h>
}
#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);
}

251
tests/sys/fs/fuse/mockfs.cc Normal file
View File

@ -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 <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include "mntopts.h" // for build_iovec
}
#include <gtest/gtest.h>
#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);
}

131
tests/sys/fs/fuse/mockfs.hh Normal file
View File

@ -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 <sys/types.h>
#include <pthread.h>
#include "fuse_kernel.h"
}
#include <gmock/gmock.h>
#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();
};

View File

@ -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 <gtest/gtest.h>
#include <unistd.h>
#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());
}

View File

@ -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 <sys/module.h>
#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;
}
};