Merge pull request #3320 from craig08:fuse-integration-test

PiperOrigin-RevId: 324877577
This commit is contained in:
gVisor bot
2020-08-04 13:25:40 -07:00
7 changed files with 447 additions and 1 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# 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.
source $(dirname $0)/common.sh
# Run all vfs2_fuse system call tests.
test --test_tag_filters=fuse //test/fuse/...
+1
View File
@@ -0,0 +1 @@
package(licenses = ["notice"])
+103
View File
@@ -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.
+21
View File
@@ -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",
],
)
+204
View File
@@ -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
+97
View File
@@ -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_
+1 -1
View File
@@ -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,
)