blob: 3c3b31914b2dcd8cc1bc0943c22a3b5bf73b30f4 [file] [log] [blame] [edit]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <threads.h>
#include <unistd.h>
#include <atomic>
#include <functional>
#include <tuple>
#include <vector>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test_fixture.h"
namespace fs_test {
namespace {
using ParamType = std::tuple<TestFilesystemOptions, /*reuse_subdirectory=*/bool>;
class InodeReuseTest : public BaseFilesystemTest, public testing::WithParamInterface<ParamType> {
public:
InodeReuseTest() : BaseFilesystemTest(std::get<0>(GetParam())) {}
bool reuse_subdirectory() const { return std::get<1>(GetParam()); }
};
// Try repeatedly creating and removing a file within a directory, as fast as possible, in an
// attempt to trigger filesystem-internal threading races between creation and deletion of a file.
TEST_P(InodeReuseTest, InodeReuse) {
const std::string reuse = GetPath("reuse");
ASSERT_EQ(mkdir(reuse.c_str(), 0755), 0);
DIR* d = opendir(reuse.c_str());
ASSERT_NE(d, nullptr);
for (size_t i = 0; i < 1000; i++) {
ASSERT_EQ(mkdirat(dirfd(d), "foo", 0666), 0);
if (reuse_subdirectory()) {
ASSERT_EQ(mkdirat(dirfd(d), "foo/bar", 0666), 0);
ASSERT_EQ(unlinkat(dirfd(d), "foo/bar", 0), 0);
}
ASSERT_EQ(unlinkat(dirfd(d), "foo", 0), 0);
}
ASSERT_EQ(closedir(d), 0);
ASSERT_EQ(rmdir(reuse.c_str()), 0);
}
// Return codes from helper threads
constexpr int kSuccess = 1;
constexpr int kFailure = -1;
constexpr int kUnexpectedFailure = -2;
using thrd_cb_t = int(void*);
class ThreadingTest : public FilesystemTest {
protected:
// Launch some threads, and have them all execute callback 'cb'.
//
// It is expected that:
// - kSuccessCount threads will return "kSuccess"
// - ALL OTHER threads will return "kFailure"
//
// In any other condition, this helper fails. For example, returning "kUnexpectedFailure" from cb
// is an easy way to fail the entire test from a background thread.
template <size_t kNumThreads, size_t kSuccessCount>
void ThreadActionTest(std::function<int()> cb) {
static_assert(kNumThreads >= kSuccessCount, "Need more threads or less successes");
thrd_t threads[kNumThreads];
auto callback_wrapper = +[](void* arg) {
auto cb = reinterpret_cast<std::function<int()>*>(arg);
return (*cb)();
};
for (size_t i = 0; i < kNumThreads; i++) {
ASSERT_EQ(thrd_create(&threads[i], callback_wrapper, &cb), thrd_success);
}
// Join all threads first before checking whether they were successful. This way all threads
// will be cleaned up even if we encounter a failure.
int success[kNumThreads];
int result[kNumThreads];
for (size_t i = 0; i < kNumThreads; i++) {
success[i] = thrd_join(threads[i], &result[i]);
};
size_t success_count = 0;
for (size_t i = 0; i < kNumThreads; i++) {
ASSERT_EQ(success[i], thrd_success);
if (result[i] == kSuccess) {
success_count++;
ASSERT_LE(success_count, kSuccessCount) << "Too many succeeding threads";
} else {
ASSERT_EQ(result[i], kFailure) << "Unexpected return code from worker thread";
}
}
ASSERT_EQ(success_count, kSuccessCount) << "Not enough succeeding threads";
}
};
constexpr size_t kIterCount = 10;
TEST_P(ThreadingTest, CreateUnlinkExclusive) {
for (size_t i = 0; i < kIterCount; i++) {
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
fbl::unique_fd fd(open(GetPath("exclusive").c_str(), O_RDWR | O_CREAT | O_EXCL));
if (fd) {
return close(fd.release()) == 0 ? kSuccess : kUnexpectedFailure;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
if (unlink(GetPath("exclusive").c_str()) == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
}
}
TEST_P(ThreadingTest, MkdirRmdirExclusive) {
for (size_t i = 0; i < kIterCount; i++) {
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
if (mkdir(GetPath("exclusive").c_str(), 0666) == 0) {
return kSuccess;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
if (rmdir(GetPath("exclusive").c_str()) == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
}
}
TEST_P(ThreadingTest, RenameExclusive) {
for (size_t i = 0; i < kIterCount; i++) {
// Test case of renaming from a single source.
ASSERT_EQ(mkdir(GetPath("rename_start").c_str(), 0666), 0);
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
if (rename(GetPath("rename_start").c_str(), GetPath("rename_end").c_str()) == 0) {
return kSuccess;
} else if (errno == ENOENT) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_EQ(rmdir(GetPath("rename_end").c_str()), 0);
// Test case of renaming from multiple sources at once, to a single destination
std::atomic<uint32_t> ctr{0};
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this, &ctr]() {
char start[128];
snprintf(start, sizeof(start) - 1, GetPath("rename_start_%u").c_str(), ctr.fetch_add(1));
if (mkdir(start, 0666)) {
return kUnexpectedFailure;
}
// Give the target a child, so it cannot be overwritten as a target
char child[256];
snprintf(child, sizeof(child) - 1, "%s/child", start);
if (mkdir(child, 0666)) {
return kUnexpectedFailure;
}
if (rename(start, GetPath("rename_end").c_str()) == 0) {
return kSuccess;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
return rmdir(child) == 0 && rmdir(start) == 0 ? kFailure : kUnexpectedFailure;
}
return kUnexpectedFailure;
})));
DIR* dir = opendir(GetPath("rename_end").c_str());
ASSERT_NE(dir, nullptr);
struct dirent* de;
while ((de = readdir(dir)) && de != nullptr) {
unlinkat(dirfd(dir), de->d_name, AT_REMOVEDIR);
}
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("rename_end").c_str()), 0);
}
}
TEST_P(ThreadingTest, RenameOverwrite) {
for (size_t i = 0; i < kIterCount; i++) {
// Test case of renaming from multiple sources at once, to a single destination
std::atomic<uint32_t> ctr{0};
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 10>([this, &ctr]() {
char start[128];
snprintf(start, sizeof(start) - 1, GetPath("rename_start_%u").c_str(), ctr.fetch_add(1));
if (mkdir(start, 0666)) {
return kUnexpectedFailure;
}
if (rename(start, GetPath("rename_end").c_str()) == 0) {
return kSuccess;
}
return kUnexpectedFailure;
})));
ASSERT_EQ(rmdir(GetPath("rename_end").c_str()), 0);
}
}
using ThreadingLinkTest = ThreadingTest;
TEST_P(ThreadingLinkTest, LinkExclusive) {
for (size_t i = 0; i < kIterCount; i++) {
fbl::unique_fd fd(open(GetPath("link_start").c_str(), O_RDWR | O_CREAT | O_EXCL));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_NO_FATAL_FAILURE((ThreadActionTest<10, 1>([this]() {
if (link(GetPath("link_start").c_str(), GetPath("link_end").c_str()) == 0) {
return kSuccess;
} else if (errno == EEXIST) {
return kFailure;
}
return kUnexpectedFailure;
})));
ASSERT_EQ(unlink(GetPath("link_start").c_str()), 0);
ASSERT_EQ(unlink(GetPath("link_end").c_str()), 0);
if (fs().GetTraits().can_unmount) {
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Fsck().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
}
}
}
std::vector<TestFilesystemOptions> GetThreadingLinkTestCombinations() {
std::vector<TestFilesystemOptions> test_combinations;
for (TestFilesystemOptions options : AllTestFilesystems()) {
if (options.filesystem->GetTraits().supports_hard_links) {
test_combinations.push_back(options);
}
}
return test_combinations;
}
std::string GetParamDescription(const testing::TestParamInfo<ParamType>& param) {
std::stringstream s;
s << std::get<0>(param.param) << (std::get<1>(param.param) ? "ReusingSubdir" : "");
return s.str();
}
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, InodeReuseTest,
testing::Combine(testing::ValuesIn(AllTestFilesystems()), testing::Bool()),
GetParamDescription);
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, ThreadingTest, testing::ValuesIn(AllTestFilesystems()),
testing::PrintToStringParamName());
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, ThreadingLinkTest,
testing::ValuesIn(GetThreadingLinkTestCombinations()),
testing::PrintToStringParamName());
} // namespace
} // namespace fs_test