mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Merge pull request #3320 from craig08:fuse-integration-test
PiperOrigin-RevId: 324877577
This commit is contained in:
Executable
+20
@@ -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/...
|
||||
@@ -0,0 +1 @@
|
||||
package(licenses = ["notice"])
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user