diff options
author | Craig Chi <craigchi@google.com> | 2020-08-04 12:27:55 -0700 |
---|---|---|
committer | Craig Chi <craigchi@google.com> | 2020-08-04 12:27:55 -0700 |
commit | 21d0334e7f4a98ec49e7f46cacf2d51258eaec33 (patch) | |
tree | 91445db153c48187ee0da351ac1804693f43d4d0 /test/fuse/linux | |
parent | 103a178026dc64986ff8329aaeafcd6c1fd34b91 (diff) |
Add FUSE integration test
This commit adds an integration test framework for FUSE support. Please
refer to the test example and test/fuse/README.md for further details.
Fixes #3098
Diffstat (limited to 'test/fuse/linux')
-rw-r--r-- | test/fuse/linux/BUILD | 21 | ||||
-rw-r--r-- | test/fuse/linux/fuse_base.cc | 204 | ||||
-rw-r--r-- | test/fuse/linux/fuse_base.h | 97 |
3 files changed, 322 insertions, 0 deletions
diff --git a/test/fuse/linux/BUILD b/test/fuse/linux/BUILD new file mode 100644 index 000000000..cc3383741 --- /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.h", + "fuse_base.cc", + ], + 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..87092b515 --- /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 "absl/strings/str_format.h" +#include "gmock/gmock.h" +#include "gtest/gtest.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_ |