From owner-svn-src-projects@freebsd.org Fri Mar 8 19:01:33 2019 Return-Path: Delivered-To: svn-src-projects@mailman.ysv.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mailman.ysv.freebsd.org (Postfix) with ESMTP id 826CB1526295 for ; Fri, 8 Mar 2019 19:01:33 +0000 (UTC) (envelope-from asomers@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (4096 bits) client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "Let's Encrypt Authority X3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 32BFC872FD; Fri, 8 Mar 2019 19:01:33 +0000 (UTC) (envelope-from asomers@FreeBSD.org) Received: from repo.freebsd.org (repo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:0]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 2758E2F5FD; Fri, 8 Mar 2019 19:01:33 +0000 (UTC) (envelope-from asomers@FreeBSD.org) Received: from repo.freebsd.org ([127.0.1.37]) by repo.freebsd.org (8.15.2/8.15.2) with ESMTP id x28J1XNv061655; Fri, 8 Mar 2019 19:01:33 GMT (envelope-from asomers@FreeBSD.org) Received: (from asomers@localhost) by repo.freebsd.org (8.15.2/8.15.2/Submit) id x28J1W7l061651; Fri, 8 Mar 2019 19:01:32 GMT (envelope-from asomers@FreeBSD.org) Message-Id: <201903081901.x28J1W7l061651@repo.freebsd.org> X-Authentication-Warning: repo.freebsd.org: asomers set sender to asomers@FreeBSD.org using -f From: Alan Somers Date: Fri, 8 Mar 2019 19:01:32 +0000 (UTC) To: src-committers@freebsd.org, svn-src-projects@freebsd.org Subject: svn commit: r344930 - projects/fuse2/tests/sys/fs/fuse X-SVN-Group: projects X-SVN-Commit-Author: asomers X-SVN-Commit-Paths: projects/fuse2/tests/sys/fs/fuse X-SVN-Commit-Revision: 344930 X-SVN-Commit-Repository: base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Rspamd-Queue-Id: 32BFC872FD X-Spamd-Bar: -- Authentication-Results: mx1.freebsd.org X-Spamd-Result: default: False [-2.96 / 15.00]; local_wl_from(0.00)[FreeBSD.org]; NEURAL_HAM_MEDIUM(-1.00)[-1.000,0]; NEURAL_HAM_SHORT(-0.96)[-0.961,0]; ASN(0.00)[asn:11403, ipnet:2610:1c1:1::/48, country:US]; NEURAL_HAM_LONG(-1.00)[-1.000,0] X-BeenThere: svn-src-projects@freebsd.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "SVN commit messages for the src " projects" tree" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 08 Mar 2019 19:01:34 -0000 Author: asomers Date: Fri Mar 8 19:01:31 2019 New Revision: 344930 URL: https://svnweb.freebsd.org/changeset/base/344930 Log: fuse(4): add tests for FUSE_WRITE and FUSE_RELEASE And a few definitions needed for upcoming FUSE_READ tests Sponsored by: The FreeBSD Foundation Added: projects/fuse2/tests/sys/fs/fuse/release.cc (contents, props changed) projects/fuse2/tests/sys/fs/fuse/write.cc (contents, props changed) Modified: projects/fuse2/tests/sys/fs/fuse/Makefile projects/fuse2/tests/sys/fs/fuse/mockfs.cc projects/fuse2/tests/sys/fs/fuse/mockfs.hh Modified: projects/fuse2/tests/sys/fs/fuse/Makefile ============================================================================== --- projects/fuse2/tests/sys/fs/fuse/Makefile Fri Mar 8 18:59:37 2019 (r344929) +++ projects/fuse2/tests/sys/fs/fuse/Makefile Fri Mar 8 19:01:31 2019 (r344930) @@ -13,12 +13,14 @@ ATF_TESTS_CXX+= mkdir ATF_TESTS_CXX+= mknod ATF_TESTS_CXX+= open ATF_TESTS_CXX+= readlink +ATF_TESTS_CXX+= release ATF_TESTS_CXX+= rename ATF_TESTS_CXX+= rmdir ATF_TESTS_CXX+= setattr ATF_TESTS_CXX+= statfs ATF_TESTS_CXX+= symlink ATF_TESTS_CXX+= unlink +ATF_TESTS_CXX+= write SRCS.access+= access.cc SRCS.access+= getmntopts.c @@ -65,6 +67,11 @@ SRCS.readlink+= mockfs.cc SRCS.readlink+= readlink.cc SRCS.readlink+= utils.cc +SRCS.release+= getmntopts.c +SRCS.release+= mockfs.cc +SRCS.release+= release.cc +SRCS.release+= utils.cc + SRCS.rename+= getmntopts.c SRCS.rename+= mockfs.cc SRCS.rename+= rename.cc @@ -95,6 +102,11 @@ SRCS.unlink+= mockfs.cc SRCS.unlink+= unlink.cc SRCS.unlink+= utils.cc +SRCS.write+= getmntopts.c +SRCS.write+= mockfs.cc +SRCS.write+= write.cc +SRCS.write+= utils.cc + # TODO: drastically increase timeout after test development is mostly complete TEST_METADATA+= timeout=10 @@ -105,7 +117,7 @@ CFLAGS+= -I${FUSEFS} CFLAGS+= -I${MOUNT} .PATH: ${MOUNT} -LIBADD+= pthread +LIBADD+= util pthread WARNS?= 6 NO_WTHREAD_SAFETY= # GoogleTest fails Clang's thread safety check Modified: projects/fuse2/tests/sys/fs/fuse/mockfs.cc ============================================================================== --- projects/fuse2/tests/sys/fs/fuse/mockfs.cc Fri Mar 8 18:59:37 2019 (r344929) +++ projects/fuse2/tests/sys/fs/fuse/mockfs.cc Fri Mar 8 19:01:31 2019 (r344930) @@ -33,8 +33,10 @@ extern "C" { #include #include #include +#include #include +#include #include #include #include @@ -57,44 +59,44 @@ 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", + "LOOKUP", + "FORGET", + "GETATTR", + "SETATTR", + "READLINK", + "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", + "MKNOD", + "MKDIR", + "UNLINK", + "RMDIR", + "RENAME", + "LINK", + "OPEN", + "READ", + "WRITE", + "STATFS", + "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" + "FSYNC", + "SETXATTR", + "GETXATTR", + "LISTXATTR", + "REMOVEXATTR", + "FLUSH", + "INIT", + "OPENDIR", + "READDIR", + "RELEASEDIR", + "FSYNCDIR", + "GETLK", + "SETLK", + "SETLKW", + "ACCESS", + "CREATE", + "INTERRUPT", + "BMAP", + "DESTROY" }; if (opcode >= NUM_OPS) return ("Unknown (opcode > max)"); @@ -133,12 +135,30 @@ void sigint_handler(int __unused sig) { void debug_fuseop(const mockfs_buf_in *in) { - printf("%s ino=%lu", opcode2opname(in->header.opcode), + printf("%-11s ino=%2lu", opcode2opname(in->header.opcode), in->header.nodeid); + if (verbosity > 1) { + printf(" uid=%5u gid=%5u pid=%5u unique=%lu len=%u", + in->header.uid, in->header.gid, in->header.pid, + in->header.unique, in->header.len); + } switch (in->header.opcode) { case FUSE_LOOKUP: printf(" %s", in->body.lookup); break; + case FUSE_OPEN: + printf(" flags=%#x mode=%#o", + in->body.open.flags, in->body.open.mode); + break; + case FUSE_READ: + printf(" offset=%lu size=%u", in->body.read.offset, + in->body.read.size); + break; + case FUSE_WRITE: + printf(" offset=%lu size=%u flags=%u", + in->body.write.offset, in->body.write.size, + in->body.write.write_flags); + break; default: break; } @@ -201,22 +221,37 @@ MockFS::~MockFS() { void MockFS::init() { mockfs_buf_in *in; - mockfs_buf_out out; + mockfs_buf_out *out; in = (mockfs_buf_in*) malloc(sizeof(*in)); ASSERT_TRUE(in != NULL); + out = (mockfs_buf_out*) malloc(sizeof(*out)); + ASSERT_TRUE(out != 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); + 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; + /* + * The default max_write is set to this formula in libfuse, though + * individual filesystems can lower it. The "- 4096" was added in + * commit 154ffe2, with the commit message "fix". + */ + uint32_t default_max_write = 32 * getpagesize() + 0x1000 - 4096; + /* For testing purposes, it should be distinct from MAXPHYS */ + m_max_write = MIN(default_max_write, MAXPHYS / 2); + out->body.init.max_write = m_max_write; + + /* Default max_readahead is UINT_MAX, though it can be lowered */ + out->body.init.max_readahead = UINT_MAX; + SET_OUT_HEADER_LEN(out, init); + write(m_fuse_fd, out, out->header.len); + free(in); } @@ -246,15 +281,15 @@ void MockFS::loop() { break; if (verbosity > 0) debug_fuseop(in); - if ((pid_t)in->header.pid != m_pid) { + if (pid_ok((pid_t)in->header.pid)) { + process(in, &out); + } else { /* * 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*/ @@ -265,6 +300,27 @@ void MockFS::loop() { << strerror(errno); } free(in); +} + +bool MockFS::pid_ok(pid_t pid) { + if (pid == m_pid) { + return (true); + } else { + struct kinfo_proc *ki; + bool ok = false; + + ki = kinfo_getproc(pid); + if (ki == NULL) + return (false); + /* + * Allow access by the aio daemon processes so that our tests + * can use aio functions + */ + if (0 == strncmp("aiod", ki->ki_comm, 4)) + ok = true; + free(ki); + return (ok); + } } void MockFS::process_default(const mockfs_buf_in *in, mockfs_buf_out* out) { Modified: projects/fuse2/tests/sys/fs/fuse/mockfs.hh ============================================================================== --- projects/fuse2/tests/sys/fs/fuse/mockfs.hh Fri Mar 8 18:59:37 2019 (r344929) +++ projects/fuse2/tests/sys/fs/fuse/mockfs.hh Fri Mar 8 19:01:31 2019 (r344930) @@ -80,10 +80,13 @@ union fuse_payloads_in { fuse_mkdir_in mkdir; fuse_mknod_in mknod; fuse_open_in open; + fuse_read_in read; + fuse_release_in release; fuse_rename_in rename; char rmdir[0]; fuse_setattr_in setattr; char unlink[0]; + fuse_write_in write; }; struct mockfs_buf_in { @@ -94,6 +97,8 @@ struct mockfs_buf_in { union fuse_payloads_out { fuse_attr_out attr; fuse_create_out create; + /* The protocol places no limits on the size of bytes */ + uint8_t bytes[0x2000]; fuse_entry_out entry; fuse_init_out init; fuse_open_out open; @@ -103,6 +108,7 @@ union fuse_payloads_out { * merely convenient for testing. */ char str[80]; + fuse_write_out write; }; struct mockfs_buf_out { @@ -110,12 +116,16 @@ struct mockfs_buf_out { union fuse_payloads_out body; }; +/* A function that can be invoked in place of MockFS::process */ +typedef std::function +ProcessMockerT; + /* * Helper function used for setting an error expectation for any fuse operation. * The operation will return the supplied error */ -std::function -ReturnErrno(int error); +ProcessMockerT ReturnErrno(int error); /* Helper function used for returning negative cache entries for LOOKUP */ std::function @@ -130,7 +140,6 @@ ReturnNegativeCache(const struct timespec *entry_valid * Operates directly on the fuse(4) kernel API, not the libfuse(3) user api. */ class MockFS { - public: /* * thread id of the fuse daemon thread * @@ -139,7 +148,6 @@ class MockFS { */ pthread_t m_daemon_id; - private: /* file descriptor of /dev/fuse control device */ int m_fuse_fd; @@ -149,6 +157,9 @@ class MockFS { /* Initialize a session after mounting */ void init(); + /* 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*, mockfs_buf_out*); @@ -159,6 +170,9 @@ class MockFS { void read_request(mockfs_buf_in*); public: + /* Maximum size of a FUSE_WRITE write */ + uint32_t m_max_write; + /* Create a new mockfs and mount it to a tempdir */ MockFS(); virtual ~MockFS(); Added: projects/fuse2/tests/sys/fs/fuse/release.cc ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ projects/fuse2/tests/sys/fs/fuse/release.cc Fri Mar 8 19:01:31 2019 (r344930) @@ -0,0 +1,199 @@ +/*- + * 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 "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Release: public FuseTest { + +const static uint64_t FH = 0xdeadbeef1a7ebabe; + +public: +void expect_getattr(uint64_t ino) +{ + /* Until the attr cache is working, we may send an additional GETATTR */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(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; + })); + +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + EXPECT_LOOKUP(1, relpath).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.attr_valid = UINT64_MAX; + })); +} + +void expect_open(uint64_t ino, int times) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_OPEN && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke([](auto in, auto out) { + out->header.unique = in->header.unique; + out->header.len = sizeof(out->header); + SET_OUT_HEADER_LEN(out, open); + out->body.open.fh = Release::FH; + })); + +} + +void expect_release(uint64_t ino, int times, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_RELEASE && + in->header.nodeid == ino && + in->body.release.fh == Release::FH); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(r)); +} +}; + +// TODO: lock owner stuff + +/* If a file descriptor is duplicated, only the last close causes RELEASE */ +TEST_F(Release, dup) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + + expect_lookup(RELPATH, ino); + expect_open(ino, 1); + expect_getattr(ino); + expect_release(ino, 1, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDONLY); + EXPECT_LE(0, fd) << strerror(errno); + + fd2 = dup(fd); + + ASSERT_EQ(0, close(fd2)) << strerror(errno); + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +/* + * Some FUSE filesystem cache data internally and flush it on release. Such + * filesystems may generate errors during release. On Linux, these get + * returned by close(2). However, POSIX does not require close(2) to return + * this error. FreeBSD's fuse(4) should return EIO if it returns an error at + * all. + */ +/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */ +TEST_F(Release, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 1); + expect_getattr(ino); + expect_release(ino, 1, ReturnErrno(EIO)); + + fd = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno); +} + +/* + * fuse(4) will issue multiple FUSE_OPEN operations for the same file if it's + * opened with different modes. Each FUSE_OPEN should get its own + * FUSE_RELEASE. + */ +TEST_F(Release, multiple_opens) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + + expect_lookup(RELPATH, ino); + expect_open(ino, 2); + expect_getattr(ino); + expect_release(ino, 2, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDONLY); + EXPECT_LE(0, fd) << strerror(errno); + + fd2 = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd2) << strerror(errno); + + ASSERT_EQ(0, close(fd2)) << strerror(errno); + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +TEST_F(Release, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 1); + expect_getattr(ino); + expect_release(ino, 1, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDONLY); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, close(fd)) << strerror(errno); +} Added: projects/fuse2/tests/sys/fs/fuse/write.cc ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ projects/fuse2/tests/sys/fs/fuse/write.cc Fri Mar 8 19:01:31 2019 (r344930) @@ -0,0 +1,733 @@ +/*- + * 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 "mockfs.hh" +#include "utils.hh" + +/* + * TODO: remove FUSE_WRITE_CACHE definition when upgrading to protocol 7.9. + * This bit was actually part of kernel protocol version 7.2, but never + * documented until 7.9 + */ +#ifndef FUSE_WRITE_CACHE +#define FUSE_WRITE_CACHE 1 +#endif + +using namespace testing; + +class Write: public FuseTest { + +public: +int m_maxbcachebuf; + +virtual void SetUp() { + const char *node = "vfs.maxbcachebuf"; + int val = 0; + size_t size = sizeof(val); + + ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) + << strerror(errno); + m_maxbcachebuf = val; + + FuseTest::SetUp(); +} + +const static uint64_t FH = 0xdeadbeef1a7ebabe; +void expect_getattr(uint64_t ino, uint64_t size) +{ + /* Until the attr cache is working, we may send an additional GETATTR */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_GETATTR && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(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 = size; + out->body.attr.attr_valid = UINT64_MAX; + })); + +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + EXPECT_LOOKUP(1, relpath).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.attr_valid = UINT64_MAX; + })); +} + +void expect_open(uint64_t ino, uint32_t flags, int times) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_OPEN && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + out->header.len = sizeof(out->header); + SET_OUT_HEADER_LEN(out, open); + out->body.open.fh = Write::FH; + out->body.open.open_flags = flags; + })); +} + +void expect_read(uint64_t ino, uint64_t offset, uint64_t size, + const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_READ && + in->header.nodeid == ino && + in->body.read.fh == Write::FH && + in->body.read.offset == offset && + in->body.read.size == size); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + out->header.len = sizeof(struct fuse_out_header) + size; + memmove(out->body.bytes, contents, size); + })).RetiresOnSaturation(); + +} + +void expect_release(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in->header.opcode == FUSE_RELEASE && + in->header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(r)); +} + +void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, + uint32_t flags, const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in->body.bytes + + sizeof(struct fuse_write_in); + bool pid_ok; + + if (in->body.write.write_flags & FUSE_WRITE_CACHE) + pid_ok = true; + else + pid_ok = (pid_t)in->header.pid == getpid(); + + return (in->header.opcode == FUSE_WRITE && + in->header.nodeid == ino && + in->body.write.fh == Write::FH && + in->body.write.offset == offset && + in->body.write.size == isize && + pid_ok && + in->body.write.write_flags == flags && + 0 == bcmp(buf, contents, isize)); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in, auto out) { + out->header.unique = in->header.unique; + SET_OUT_HEADER_LEN(out, write); + out->body.write.size = osize; + })); +} + +}; + +class AioWrite: public Write { +virtual void SetUp() { + const char *node = "vfs.aio.enable_unsafe"; + int val = 0; + size_t size = sizeof(val); + + ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) + << strerror(errno); + // TODO: With GoogleTest 1.8.2, use SKIP instead + if (!val) + FAIL() << "vfs.aio.enable_unsafe must be set for this test"; + FuseTest::SetUp(); +} +}; + +/* Tests for the write-through cache mode */ +class WriteThrough: public Write { + +virtual void SetUp() { + const char *cache_mode_node = "vfs.fuse.data_cache_mode"; + const char *sync_resize_node = "vfs.fuse.sync_resize"; + int val = 0; + size_t size = sizeof(val); + + ASSERT_EQ(0, sysctlbyname(cache_mode_node, &val, &size, NULL, 0)) + << strerror(errno); + // TODO: With GoogleTest 1.8.2, use SKIP instead + if (val != 1) + FAIL() << "vfs.fuse.data_cache_mode must be set to 1 " + "(writethrough) for this test"; + + ASSERT_EQ(0, sysctlbyname(sync_resize_node, &val, &size, NULL, 0)) + << strerror(errno); + if (val != 0) + FAIL() << "vfs.fuse.sync_resize must be set to 0 for this test." + " That sysctl will probably be removed soon."; + + FuseTest::SetUp(); +} + +}; + +/* Tests for the writeback cache mode */ +class WriteBack: public Write { + +virtual void SetUp() { + const char *node = "vfs.fuse.data_cache_mode"; + int val = 0; + size_t size = sizeof(val); + + ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) + << strerror(errno); + // TODO: With GoogleTest 1.8.2, use SKIP instead + if (val != 2) + FAIL() << "vfs.fuse.data_cache_mode must be set to 2 " + "(writeback) for this test"; + FuseTest::SetUp(); +} + +}; + +/* AIO writes need to set the header's pid field correctly */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ +TEST_F(AioWrite, DISABLED_aio_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + uint64_t offset = 4096; + int fd; + ssize_t bufsize = strlen(CONTENTS); + struct aiocb iocb, *piocb; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_getattr(ino, 0); + expect_write(ino, offset, bufsize, bufsize, 0, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd) << strerror(errno); + + iocb.aio_nbytes = bufsize; + iocb.aio_fildes = fd; + iocb.aio_buf = (void *)CONTENTS; + iocb.aio_offset = offset; + iocb.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_write(&iocb)) << strerror(errno); + ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + +/* + * When a file is opened with O_APPEND, we should forward that flag to + * FUSE_OPEN (tested by Open.o_append) but still attempt to calculate the + * offset internally. That way we'll work both with filesystems that + * understand O_APPEND (and ignore the offset) and filesystems that don't (and + * simply use the offset). + * + * Note that verifying the O_APPEND flag in FUSE_OPEN is done in the + * Open.o_append test. + */ +TEST_F(Write, append) +{ + const ssize_t BUFSIZE = 9; + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS[BUFSIZE] = "abcdefgh"; + uint64_t ino = 42; + /* + * Set offset to a maxbcachebuf boundary so we don't need to RMW when + * using writeback caching + */ + uint64_t initial_offset = m_maxbcachebuf; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_getattr(ino, initial_offset); + expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, 0, CONTENTS); + + /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ + fd = open(FULLPATH, O_RDWR | O_APPEND); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + +TEST_F(Write, append_direct_io) +{ + const ssize_t BUFSIZE = 9; + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS[BUFSIZE] = "abcdefgh"; + uint64_t ino = 42; + uint64_t initial_offset = 4096; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_getattr(ino, initial_offset); + expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, 0, CONTENTS); + + fd = open(FULLPATH, O_WRONLY | O_APPEND); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + +/* A direct write should evict any overlapping cached data */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235774 */ +TEST_F(Write, DISABLED_direct_io_evicts_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS0[] = "abcdefgh"; + const char CONTENTS1[] = "ijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS0) + 1; + char readbuf[bufsize]; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_getattr(ino, bufsize); + expect_read(ino, 0, bufsize, CONTENTS0); + expect_write(ino, 0, bufsize, bufsize, 0, CONTENTS1); + + fd = open(FULLPATH, O_RDWR); + EXPECT_LE(0, fd) << strerror(errno); + + // Prime cache + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + + // Write directly, evicting cache + ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS1, bufsize)) << strerror(errno); + + // Read again. Cache should be bypassed + expect_read(ino, 0, bufsize, CONTENTS1); + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + ASSERT_STREQ(readbuf, CONTENTS1); + + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + +/* + * When the direct_io option is used, filesystems are allowed to write less + * data than requested + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236381 */ +TEST_F(Write, DISABLED_direct_io_short_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + ssize_t halfbufsize = bufsize / 2; + const char *halfcontents = CONTENTS + halfbufsize; + + expect_lookup(RELPATH, ino); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_getattr(ino, 0); + expect_write(ino, 0, bufsize, halfbufsize, 0, CONTENTS); + expect_write(ino, halfbufsize, halfbufsize, halfbufsize, 0, + halfcontents); + + fd = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* Deliberately leak fd. close(2) will be tested in release.cc */ +} + +/* + * An insidious edge case: the filesystem returns a short write, and the + * difference between what we requested and what it actually wrote crosses an + * iov element boundary + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236381 */ +TEST_F(Write, DISABLED_direct_io_short_write_iov) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS0 = "abcdefgh"; + const char *CONTENTS1 = "ijklmnop"; + const char *EXPECTED0 = "abcdefghijklmnop"; + const char *EXPECTED1 = "hijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t size0 = strlen(CONTENTS0) - 1; + ssize_t size1 = strlen(CONTENTS1) + 1; + ssize_t totalsize = size0 + size1; + struct iovec iov[2]; + + expect_lookup(RELPATH, ino); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_getattr(ino, 0); + expect_write(ino, 0, totalsize, size0, 0, EXPECTED0); + expect_write(ino, size0, size1, size1, 0, EXPECTED1); + + fd = open(FULLPATH, O_WRONLY); + EXPECT_LE(0, fd) << strerror(errno); + + iov[0].iov_base = (void*)CONTENTS0; *** DIFF OUTPUT TRUNCATED AT 1000 LINES ***