summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fuse/BUILD1
-rw-r--r--test/fuse/README.md103
-rw-r--r--test/fuse/linux/BUILD21
-rw-r--r--test/fuse/linux/fuse_base.cc204
-rw-r--r--test/fuse/linux/fuse_base.h97
-rw-r--r--test/runner/defs.bzl2
6 files changed, 427 insertions, 1 deletions
diff --git a/test/fuse/BUILD b/test/fuse/BUILD
new file mode 100644
index 000000000..34b950644
--- /dev/null
+++ b/test/fuse/BUILD
@@ -0,0 +1 @@
+package(licenses = ["notice"])
diff --git a/test/fuse/README.md b/test/fuse/README.md
new file mode 100644
index 000000000..734c3a4e3
--- /dev/null
+++ b/test/fuse/README.md
@@ -0,0 +1,103 @@
+# gVisor FUSE Test Suite
+
+This is an integration test suite for fuse(4) filesystem. It runs under both
+gVisor and Linux, and ensures compatibility between the two. This test suite is
+based on system calls test.
+
+This document describes the framework of fuse integration test and the
+guidelines that should be followed when adding new fuse tests.
+
+## Integration Test Framework
+
+Please refer to the figure below. `>` is entering the function, `<` is leaving
+the function, and `=` indicates sequentially entering and leaving.
+
+```
+ | Client (Test Main Process) | Server (FUSE Daemon)
+ | |
+ | >TEST_F() |
+ | >SetUp() |
+ | =MountFuse() |
+ | >SetUpFuseServer() |
+ | [create communication pipes] |
+ | =fork() | =fork()
+ | >WaitCompleted() |
+ | [wait for MarkDone()] |
+ | | =ConsumeFuseInit()
+ | | =MarkDone()
+ | <WaitCompleted() |
+ | <SetUpFuseServer() |
+ | <SetUp() |
+ | >SetExpected() |
+ | [construct expected reaction] |
+ | | >FuseLoop()
+ | | >ReceiveExpected()
+ | | [wait data from pipe]
+ | [write data to pipe] |
+ | [wait for MarkDone()] |
+ | | [save data to memory]
+ | | =MarkDone()
+ | <SetExpected() |
+ | | <ReceiveExpected()
+ | | >read()
+ | | [wait for fs operation]
+ | >[Do fs operation] |
+ | [wait for fs response] |
+ | | <read()
+ | | =CompareRequest()
+ | | =write() [write fs response]
+ | <[Do fs operation] |
+ | =[Test fs operation result] |
+ | =[wait for MarkDone()] |
+ | | =MarkDone()
+ | >TearDown() |
+ | =UnmountFuse() |
+ | <TearDown() |
+ | <TEST_F() |
+```
+
+## Running the tests
+
+Based on syscall tests, fuse tests can run in different environments. To enable
+fuse testing environment, the test targets should be appended with `_fuse`.
+
+For example, to run fuse test in `stat_test.cc`:
+
+```bash
+$ bazel test //test/fuse:stat_test_runsc_ptrace_vfs2_fuse
+```
+
+Test all targets tagged with fuse:
+
+```bash
+$ bazel test --test_tag_filters=fuse //test/fuse/...
+```
+
+## Writing a new FUSE test
+
+1. Add test targets in `BUILD` and `linux/BUILD`.
+2. Inherit your test from `FuseTest` base class. It allows you to:
+ - Run a fake FUSE server in background during each test setup.
+ - Create pipes for communication and provide utility functions.
+ - Stop FUSE server after test completes.
+3. Customize your comparison function for request assessment in FUSE server.
+4. Add the mapping of the size of structs if you are working on new FUSE
+ opcode.
+ - Please update `FuseTest::GetPayloadSize()` for each new FUSE opcode.
+5. Build the expected request-response pair of your FUSE operation.
+6. Call `SetExpected()` function to inject the expected reaction.
+7. Check the response and/or errors.
+8. Finally call `WaitCompleted()` to ensure the FUSE server acts correctly.
+
+A few customized matchers used in syscalls test are encouraged to test the
+outcome of filesystem operations. Such as:
+
+```cc
+SyscallSucceeds()
+SyscallSucceedsWithValue(...)
+SyscallFails()
+SyscallFailsWithErrno(...)
+```
+
+Please refer to [test/syscalls/README.md](../syscalls/README.md) for further
+details.
diff --git a/test/fuse/linux/BUILD b/test/fuse/linux/BUILD
new file mode 100644
index 000000000..49dc96c20
--- /dev/null
+++ b/test/fuse/linux/BUILD
@@ -0,0 +1,21 @@
+load("//tools:defs.bzl", "cc_library", "gtest")
+
+package(
+ default_visibility = ["//:sandbox"],
+ licenses = ["notice"],
+)
+
+cc_library(
+ name = "fuse_base",
+ testonly = 1,
+ srcs = [
+ "fuse_base.cc",
+ "fuse_base.h",
+ ],
+ deps = [
+ gtest,
+ "//test/util:posix_error",
+ "//test/util:test_util",
+ "@com_google_absl//absl/strings:str_format",
+ ],
+)
diff --git a/test/fuse/linux/fuse_base.cc b/test/fuse/linux/fuse_base.cc
new file mode 100644
index 000000000..6c8432fd0
--- /dev/null
+++ b/test/fuse/linux/fuse_base.cc
@@ -0,0 +1,204 @@
+// Copyright 2020 The gVisor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "fuse_base.h"
+
+#include <fcntl.h>
+#include <linux/fuse.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <iostream>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/str_format.h"
+#include "test/util/posix_error.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+void FuseTest::SetUp() {
+ MountFuse();
+ SetUpFuseServer();
+}
+
+void FuseTest::TearDown() { UnmountFuse(); }
+
+// Since CompareRequest is running in background thread, gTest assertions and
+// expectations won't directly reflect the test result. However, the FUSE
+// background server still connects to the same standard I/O as testing main
+// thread. So EXPECT_XX can still be used to show different results. To
+// ensure failed testing result is observable, return false and the result
+// will be sent to test main thread via pipe.
+bool FuseTest::CompareRequest(void* expected_mem, size_t expected_len,
+ void* real_mem, size_t real_len) {
+ if (expected_len != real_len) return false;
+ return memcmp(expected_mem, real_mem, expected_len) == 0;
+}
+
+// SetExpected is called by the testing main thread to set expected request-
+// response pair of a single FUSE operation.
+void FuseTest::SetExpected(struct iovec* iov_in, int iov_in_cnt,
+ struct iovec* iov_out, int iov_out_cnt) {
+ EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_in, iov_in_cnt),
+ SyscallSucceedsWithValue(::testing::Gt(0)));
+ WaitCompleted();
+
+ EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_out, iov_out_cnt),
+ SyscallSucceedsWithValue(::testing::Gt(0)));
+ WaitCompleted();
+}
+
+// WaitCompleted waits for the FUSE server to finish its job and check if it
+// completes without errors.
+void FuseTest::WaitCompleted() {
+ char success;
+ EXPECT_THAT(RetryEINTR(read)(done_[0], &success, sizeof(success)),
+ SyscallSucceedsWithValue(1));
+}
+
+void FuseTest::MountFuse() {
+ EXPECT_THAT(dev_fd_ = open("/dev/fuse", O_RDWR), SyscallSucceeds());
+
+ std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, kMountOpts);
+ EXPECT_THAT(mount("fuse", kMountPoint, "fuse", MS_NODEV | MS_NOSUID,
+ mount_opts.c_str()),
+ SyscallSucceedsWithValue(0));
+}
+
+void FuseTest::UnmountFuse() {
+ EXPECT_THAT(umount(kMountPoint), SyscallSucceeds());
+ // TODO(gvisor.dev/issue/3330): ensure the process is terminated successfully.
+}
+
+// ConsumeFuseInit consumes the first FUSE request and returns the
+// corresponding PosixError.
+PosixError FuseTest::ConsumeFuseInit() {
+ RETURN_ERROR_IF_SYSCALL_FAIL(
+ RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size()));
+
+ struct iovec iov_out[2];
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_init_out),
+ .error = 0,
+ .unique = 2,
+ };
+ // Returns an empty init out payload since this is just a test.
+ struct fuse_init_out out_payload;
+ iov_out[0].iov_len = sizeof(out_header);
+ iov_out[0].iov_base = &out_header;
+ iov_out[1].iov_len = sizeof(out_payload);
+ iov_out[1].iov_base = &out_payload;
+
+ RETURN_ERROR_IF_SYSCALL_FAIL(RetryEINTR(writev)(dev_fd_, iov_out, 2));
+ return NoError();
+}
+
+// ReceiveExpected reads 1 pair of expected fuse request-response `iovec`s
+// from pipe and save them into member variables of this testing instance.
+void FuseTest::ReceiveExpected() {
+ // Set expected fuse_in request.
+ EXPECT_THAT(len_in_ = RetryEINTR(read)(set_expected_[0], mem_in_.data(),
+ mem_in_.size()),
+ SyscallSucceedsWithValue(::testing::Gt(0)));
+ MarkDone(len_in_ > 0);
+
+ // Set expected fuse_out response.
+ EXPECT_THAT(len_out_ = RetryEINTR(read)(set_expected_[0], mem_out_.data(),
+ mem_out_.size()),
+ SyscallSucceedsWithValue(::testing::Gt(0)));
+ MarkDone(len_out_ > 0);
+}
+
+// MarkDone writes 1 byte of success indicator through pipe.
+void FuseTest::MarkDone(bool success) {
+ char data = success ? 1 : 0;
+ EXPECT_THAT(RetryEINTR(write)(done_[1], &data, sizeof(data)),
+ SyscallSucceedsWithValue(1));
+}
+
+// FuseLoop is the implementation of the fake FUSE server. Read from /dev/fuse,
+// compare the request by CompareRequest (use derived function if specified),
+// and write the expected response to /dev/fuse.
+void FuseTest::FuseLoop() {
+ bool success = true;
+ ssize_t len = 0;
+ while (true) {
+ ReceiveExpected();
+
+ EXPECT_THAT(len = RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size()),
+ SyscallSucceedsWithValue(len_in_));
+ if (len != len_in_) success = false;
+
+ if (!CompareRequest(buf_.data(), len_in_, mem_in_.data(), len_in_)) {
+ std::cerr << "the FUSE request is not expected" << std::endl;
+ success = false;
+ }
+
+ EXPECT_THAT(len = RetryEINTR(write)(dev_fd_, mem_out_.data(), len_out_),
+ SyscallSucceedsWithValue(len_out_));
+ if (len != len_out_) success = false;
+ MarkDone(success);
+ }
+}
+
+// SetUpFuseServer creates 2 pipes. First is for testing client to send the
+// expected request-response pair, and the other acts as a checkpoint for the
+// FUSE server to notify the client that it can proceed.
+void FuseTest::SetUpFuseServer() {
+ ASSERT_THAT(pipe(set_expected_), SyscallSucceedsWithValue(0));
+ ASSERT_THAT(pipe(done_), SyscallSucceedsWithValue(0));
+
+ switch (fork()) {
+ case -1:
+ GTEST_FAIL();
+ return;
+ case 0:
+ break;
+ default:
+ ASSERT_THAT(close(set_expected_[0]), SyscallSucceedsWithValue(0));
+ ASSERT_THAT(close(done_[1]), SyscallSucceedsWithValue(0));
+ WaitCompleted();
+ return;
+ }
+
+ ASSERT_THAT(close(set_expected_[1]), SyscallSucceedsWithValue(0));
+ ASSERT_THAT(close(done_[0]), SyscallSucceedsWithValue(0));
+
+ MarkDone(ConsumeFuseInit().ok());
+
+ FuseLoop();
+ _exit(0);
+}
+
+// GetPayloadSize is a helper function to get the number of bytes of a
+// specific FUSE operation struct.
+size_t FuseTest::GetPayloadSize(uint32_t opcode, bool in) {
+ switch (opcode) {
+ case FUSE_INIT:
+ return in ? sizeof(struct fuse_init_in) : sizeof(struct fuse_init_out);
+ default:
+ break;
+ }
+ return 0;
+}
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/fuse_base.h b/test/fuse/linux/fuse_base.h
new file mode 100644
index 000000000..b008778de
--- /dev/null
+++ b/test/fuse/linux/fuse_base.h
@@ -0,0 +1,97 @@
+// Copyright 2020 The gVisor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GVISOR_TEST_FUSE_FUSE_BASE_H_
+#define GVISOR_TEST_FUSE_FUSE_BASE_H_
+
+#include <linux/fuse.h>
+#include <sys/uio.h>
+
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "test/util/posix_error.h"
+
+namespace gvisor {
+namespace testing {
+
+constexpr char kMountPoint[] = "/mnt";
+constexpr char kMountOpts[] = "rootmode=755,user_id=0,group_id=0";
+
+class FuseTest : public ::testing::Test {
+ public:
+ FuseTest() {
+ buf_.resize(FUSE_MIN_READ_BUFFER);
+ mem_in_.resize(FUSE_MIN_READ_BUFFER);
+ mem_out_.resize(FUSE_MIN_READ_BUFFER);
+ }
+ void SetUp() override;
+ void TearDown() override;
+
+ // CompareRequest is used by the FUSE server and should be implemented to
+ // compare different FUSE operations. It compares the actual FUSE input
+ // request with the expected one set by `SetExpected()`.
+ virtual bool CompareRequest(void* expected_mem, size_t expected_len,
+ void* real_mem, size_t real_len);
+
+ // SetExpected is called by the testing main thread. Writes a request-
+ // response pair into FUSE server's member variables via pipe.
+ void SetExpected(struct iovec* iov_in, int iov_in_cnt, struct iovec* iov_out,
+ int iov_out_cnt);
+
+ // WaitCompleted waits for FUSE server to complete its processing. It
+ // complains if the FUSE server responds failure during tests.
+ void WaitCompleted();
+
+ private:
+ void MountFuse();
+ void UnmountFuse();
+
+ // ConsumeFuseInit is only used during FUSE server setup.
+ PosixError ConsumeFuseInit();
+
+ // ReceiveExpected is the FUSE server side's corresponding code of
+ // `SetExpected()`. Save the request-response pair into its memory.
+ void ReceiveExpected();
+
+ // MarkDone is used by the FUSE server to tell testing main if it's OK to
+ // proceed next command.
+ void MarkDone(bool success);
+
+ // FuseLoop is where the FUSE server stay until it is terminated.
+ void FuseLoop();
+
+ // SetUpFuseServer creates 2 pipes for communication and forks FUSE server.
+ void SetUpFuseServer();
+
+ // GetPayloadSize is a helper function to get the number of bytes of a
+ // specific FUSE operation struct.
+ size_t GetPayloadSize(uint32_t opcode, bool in);
+
+ int dev_fd_;
+ int set_expected_[2];
+ int done_[2];
+
+ std::vector<char> buf_;
+ std::vector<char> mem_in_;
+ std::vector<char> mem_out_;
+ ssize_t len_in_;
+ ssize_t len_out_;
+};
+
+} // namespace testing
+} // namespace gvisor
+
+#endif // GVISOR_TEST_FUSE_FUSE_BASE_H_
diff --git a/test/runner/defs.bzl b/test/runner/defs.bzl
index c92392b35..ba4732ca6 100644
--- a/test/runner/defs.bzl
+++ b/test/runner/defs.bzl
@@ -201,7 +201,7 @@ def syscall_test(
platform = default_platform,
use_tmpfs = use_tmpfs,
add_uds_tree = add_uds_tree,
- tags = platforms[default_platform] + vfs2_tags,
+ tags = platforms[default_platform] + vfs2_tags + ["fuse"],
vfs2 = True,
fuse = True,
)