Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 8 Mar 2019 19:01:32 +0000 (UTC)
From:      Alan Somers <asomers@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-projects@freebsd.org
Subject:   svn commit: r344930 - projects/fuse2/tests/sys/fs/fuse
Message-ID:  <201903081901.x28J1W7l061651@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
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 <sys/mount.h>
 #include <sys/stat.h>
 #include <sys/uio.h>
+#include <sys/user.h>
 
 #include <fcntl.h>
+#include <libutil.h>
 #include <pthread.h>
 #include <signal.h>
 #include <stdlib.h>
@@ -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<void (const struct mockfs_buf_in *in,
+			    struct mockfs_buf_out *out)>
+ProcessMockerT;
+
 /*
  * Helper function used for setting an error expectation for any fuse operation.
  * The operation will return the supplied error
  */
-std::function<void (const struct mockfs_buf_in *in, struct mockfs_buf_out *out)>
-ReturnErrno(int error);
+ProcessMockerT ReturnErrno(int error);
 
 /* Helper function used for returning negative cache entries for LOOKUP */
 std::function<void (const struct mockfs_buf_in *in, struct mockfs_buf_out *out)>
@@ -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 <fcntl.h>
+#include <unistd.h>
+}
+
+#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 <sys/types.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/sysctl.h>
+#include <sys/uio.h>
+
+#include <aio.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#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 ***



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201903081901.x28J1W7l061651>