blob: 5bab4d72a31afbf9185aff6025c509fd15a0091a [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 <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <threads.h>
#include <time.h>
#include <unistd.h>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test_fixture.h"
namespace fs_test {
namespace {
constexpr int kFail = -1;
constexpr int kDone = 0;
constexpr int kBlockSize = 8192;
constexpr int kBufferSize = 65536;
class RandomOpTest;
unsigned GenerateSeed() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return ts.tv_nsec;
}
struct Worker {
Worker(RandomOpTest& env, std::string_view name, ssize_t size)
: env(env), size(size), name(name) {}
RandomOpTest& env;
fbl::unique_fd fd;
ssize_t size;
std::string name;
unsigned seed = GenerateSeed();
unsigned opcnt = 0;
};
constexpr int KiB(int n) { return n * 1024; }
constexpr int MiB(int n) { return n * 1024; }
constexpr struct {
std::string_view name;
uint32_t size;
} kWork[] = {
// one thread per work entry
{"thd0000", KiB(5)}, {"thd0001", MiB(10)}, {"thd0002", KiB(512)}, {"thd0003", KiB(512)},
{"thd0004", KiB(512)}, {"thd0005", MiB(20)}, {"thd0006", KiB(512)}, {"thd0007", KiB(512)},
};
class RandomOpTest : public FilesystemTest {
public:
struct RandomOp {
const char* name;
int (*fn)(Worker*);
unsigned weight;
};
const std::vector<RandomOp>& operations() const { return operations_; }
bool debug() const { return debug_; }
protected:
RandomOpTest() {
AddRandomOperations();
for (size_t n = 0; n < std::size(kWork); n++) {
NewWorker(kWork[n].name, kWork[n].size);
}
}
void NewWorker(std::string_view name, uint32_t size) {
all_workers_.push_back(std::make_unique<Worker>(*this, name, size));
}
static void TaskDebugOp(Worker* w, const char* fn) {
RandomOpTest& env = w->env;
w->opcnt++;
if (env.debug()) {
std::cout << w->name << "[" << w->opcnt << "] " << fn << std::endl;
}
}
static void TaskError(Worker* w, const char* fn, const char* msg) {
int errnum = errno;
char buf[128];
strerror_r(errnum, buf, sizeof(buf));
ADD_FAILURE() << w->name << " ERROR " << fn << "(" << msg << "): " << buf << "(" << errnum
<< ")";
}
static int TaskCreateA(Worker* w) {
// put a page of data into /a
TaskDebugOp(w, "t: create_a");
int fd = open(w->env.GetPath("a").c_str(), O_RDWR + O_CREAT, 0666);
if (fd < 0) {
// errno may be one of EEXIST
if (errno != EEXIST) {
TaskError(w, "t: create_a", "open");
return kFail;
}
} else {
char buf[kBlockSize];
memset(buf, 0xab, sizeof(buf));
ssize_t len = write(fd, buf, sizeof(buf));
if (len < 0) {
TaskError(w, "t: create_a", "write");
return kFail;
}
assert(len == sizeof(buf));
EXPECT_EQ(close(fd), 0);
}
return kDone;
}
static int TaskCreateB(Worker* w) {
// put a page of data into /b
TaskDebugOp(w, "t: create_b");
int fd = open(w->env.GetPath("b").c_str(), O_RDWR + O_CREAT, 0666);
if (fd < 0) {
// errno may be one of EEXIST
if (errno != EEXIST) {
TaskError(w, "t: create_b", "open");
return kFail;
}
} else {
char buf[kBlockSize];
memset(buf, 0xba, sizeof(buf));
ssize_t len = write(fd, buf, sizeof(buf));
if (len < 0) {
TaskError(w, "t: create_a", "write");
return kFail;
}
assert(len == sizeof(buf));
EXPECT_EQ(close(fd), 0);
}
return kDone;
}
static int TaskRenameAB(Worker* w) {
// rename /a -> /b
TaskDebugOp(w, "t: rename_ab");
int rc = rename(w->env.GetPath("a").c_str(), w->env.GetPath("b").c_str());
if (rc < 0) {
// errno may be one of ENOENT
if (errno != ENOENT) {
TaskError(w, "t: rename_ab", "rename");
return kFail;
}
}
return kDone;
}
static int TaskRenameBA(Worker* w) {
// rename ::/b -> ::/a
TaskDebugOp(w, "t: rename_ba");
int rc = rename(w->env.GetPath("b").c_str(), w->env.GetPath("a").c_str());
if (rc < 0) {
// errno may be one of ENOENT
if (errno != ENOENT) {
TaskError(w, "t: rename_ba", "rename");
return kFail;
}
}
return kDone;
}
static int TaskMakePrivateDir(Worker* w) {
// mkdir ::/threadname
TaskDebugOp(w, "t: make_private_dir");
int rc = mkdir(w->env.GetPath(w->name).c_str(), 0755);
if (rc < 0) {
// errno may be one of EEXIST, ENOENT
if (errno != ENOENT && errno != EEXIST) {
TaskError(w, "t: make_private_dir", "mkdir");
return kFail;
}
}
return kDone;
}
static int TaskMoveAToPrivate(Worker* w) {
// mv ::/a -> ::/threadname/a
TaskDebugOp(w, "t: mv_a_to__private");
int rc = rename(w->env.GetPath("a").c_str(), w->env.GetPath(w->name + "/a").c_str());
if (rc < 0) {
// errno may be one of EEXIST, ENOENT, ENOTDIR
if (errno != EEXIST && errno != ENOENT && errno != ENOTDIR) {
TaskError(w, "t: mv_a_to__private", "rename");
return kFail;
}
}
return kDone;
}
static int TaskWritePrivateB(Worker* w) {
// put a page of data into /threadname/b
TaskDebugOp(w, "t: write_private_b");
int fd = open(w->env.GetPath(w->name + "/b").c_str(), O_RDWR + O_EXCL + O_CREAT, 0666);
if (fd < 0) {
// errno may be one of ENOENT, EISDIR, ENOTDIR, EEXIST
if (errno != ENOENT && errno != EISDIR && errno != ENOTDIR && errno != EEXIST) {
TaskError(w, "t: write_private_b", "open");
return kFail;
}
} else {
char buf[kBlockSize];
memset(buf, 0xba, sizeof(buf));
ssize_t len = write(fd, buf, sizeof(buf));
if (len < 0) {
TaskError(w, "t: write_private_b", "write");
return kFail;
}
assert(len == sizeof(buf));
EXPECT_EQ(close(fd), 0);
}
return kDone;
}
static int TaskRenamePrivateBA(Worker* w) {
// move /threadname/b -> /a
TaskDebugOp(w, "t: rename_private_ba");
int rc = rename((w->env.GetPath(w->name) + "/b").c_str(), w->env.GetPath("a").c_str());
if (rc < 0) {
// errno may be one of EEXIST, ENOENT
if (errno != EEXIST && errno != ENOENT) {
TaskError(w, "t: rename_private_ba", "rename");
return kFail;
}
}
return kDone;
}
static int TaskRenamePrivateAB(Worker* w) {
// move /threadname/a -> /b
TaskDebugOp(w, "t: rename_private_ab");
int rc = rename((w->env.GetPath(w->name) + "/a").c_str(), w->env.GetPath("b").c_str());
if (rc < 0) {
// errno may be one of EEXIST, ENOENT
if (errno != EEXIST && errno != ENOENT) {
TaskError(w, "t: rename_private_ab", "rename");
return kFail;
}
}
return kDone;
}
static int TaskOpenPrivateA(Worker* w) {
// close(fd); fd <- open("/threadhame/a")
TaskDebugOp(w, "t: open_private_a");
const std::string file_name = w->env.GetPath(w->name) + "/a";
w->fd = fbl::unique_fd(open(file_name.c_str(), O_RDWR + O_CREAT + O_EXCL, 0666));
if (!w->fd) {
if (errno == EEXIST) {
w->fd = fbl::unique_fd(open(file_name.c_str(), O_RDWR));
if (!w->fd) {
TaskError(w, "t: open_private_a", "open-existing");
return kFail;
}
} else {
// errno may be one of EEXIST, ENOENT
if (errno != ENOENT) {
TaskError(w, "t: open_private_a", "open");
return kFail;
}
}
}
return kDone;
}
static int TaskCloseFd(Worker* w) {
w->fd.reset();
return kDone;
}
static int TaskWriteFdBig(Worker* w) {
// write(fd, big buffer, ...)
TaskDebugOp(w, "t: write_fd_big");
if (w->fd) {
char buf[kBufferSize];
memset(buf, 0xab, sizeof(buf));
ssize_t len = write(w->fd.get(), buf, sizeof(buf));
if (len < 0) {
// errno may be one of ??
TaskError(w, "t: write_fd_small", "write");
return kFail;
} else {
assert(len == sizeof(buf));
off_t off = lseek(w->fd.get(), 0, SEEK_CUR);
assert(off >= 0);
if (off >= w->size) {
off = lseek(w->fd.get(), 0, SEEK_SET);
assert(off == 0);
}
}
}
return kDone;
}
static int TaskWriteFdSmall(Worker* w) {
// write(fd, small buffer, ...)
TaskDebugOp(w, "t: write_fd_small");
if (w->fd) {
char buf[kBlockSize];
memset(buf, 0xab, sizeof(buf));
ssize_t len = write(w->fd.get(), buf, sizeof(buf));
if (len < 0) {
// errno may be one of ??
TaskError(w, "t: write_fd_small", "write");
return kFail;
} else {
assert(len == sizeof(buf));
off_t off = lseek(w->fd.get(), 0, SEEK_CUR);
assert(off >= 0);
if (off >= w->size) {
off = lseek(w->fd.get(), 0, SEEK_SET);
assert(off == 0);
}
}
}
return kDone;
}
static int TaskTruncateFd(Worker* w) {
// ftruncate(fd)
TaskDebugOp(w, "t: truncate_fd");
if (w->fd) {
int rc = ftruncate(w->fd.get(), 0);
if (rc < 0) {
// errno may be one of ??
TaskError(w, "t: truncate_fd", "truncate");
return kFail;
}
}
return kDone;
}
static int TaskUtimeFd(Worker* w) {
// utime(fd)
TaskDebugOp(w, "t: utime_fd");
if (w->fd) {
struct timespec ts[2] = {
{.tv_nsec = UTIME_OMIT}, // no atime
{.tv_nsec = UTIME_NOW}, // mtime == now
};
int rc = futimens(w->fd.get(), ts);
if (rc < 0) {
TaskError(w, "t: utime_fd", "futimens");
return kFail;
}
}
return kDone;
}
static int TaskSeekFdEnd(Worker* w) {
TaskDebugOp(w, "t: seek_fd_end");
if (w->fd) {
off_t rc = lseek(w->fd.get(), 0, SEEK_END);
if (rc < 0) {
// errno may be one of ??
TaskError(w, "t: seek_fd_end", "lseek");
return kFail;
}
}
return kDone;
}
static int TaskSeekFdStart(Worker* w) {
// fseek(fd, SEEK_SET, 0)
TaskDebugOp(w, "t: seek_fd_start");
if (w->fd) {
off_t rc = lseek(w->fd.get(), 0, SEEK_SET);
if (rc < 0) {
// errno may be one of ??
TaskError(w, "t: seek_fd_start", "lseek");
return kFail;
}
}
return kDone;
}
static int TaskTruncateA(Worker* w) {
// truncate("/a")
int rc = truncate(w->env.GetPath("a").c_str(), 0);
if (rc < 0) {
// errno may be one of ENOENT
if (errno != ENOENT) {
TaskError(w, "t: truncate_a", "truncate");
return kFail;
}
}
return kDone;
}
// create a weighted list of operations for each thread
void AddRandomOperations() {
for (size_t i = 0; i < std::size(kOperations); ++i) {
operations_.insert(operations_.end(), kOperations[i].weight, kOperations[i]);
}
}
static int DoRandomOperations(void* arg) {
constexpr int kNumSerialOperations = 4; // yield every 1/n ops
constexpr int kMaxOperations = 1000;
Worker* w = static_cast<Worker*>(arg);
RandomOpTest& env = w->env;
// for some big number of operations
// do an operation and yield, repeat
for (int i = 0; i < kMaxOperations; i++) {
int idx = rand_r(&w->seed) % env.operations().size();
const RandomOp& op = env.operations()[idx];
if (op.fn(w) != kDone) {
ADD_FAILURE() << w->name << ": op " << op.name << " failed";
}
if (idx % kNumSerialOperations != 0)
thrd_yield();
}
// Close the worker's personal fd (if it is open) and
// unlink the worker directory.
std::cout << "work thread(" << w->name << ") done" << std::endl;
TaskCloseFd(w);
unlink(w->env.GetPath(w->name).c_str());
// currently, threads either return kDone or exit the test
return kDone;
}
static constexpr RandomOp kOperations[] = {
{"TaskCreateA", TaskCreateA, 1},
{"TaskCreateB", TaskCreateB, 1},
{"TaskRenameAB", TaskRenameAB, 4},
{"TaskRenameBA", TaskRenameBA, 4},
{"TaskMakePrivateDir", TaskMakePrivateDir, 4},
{"TaskMoveAToPrivate", TaskMoveAToPrivate, 1},
{"TaskWritePrivateB", TaskWritePrivateB, 1},
{"TaskRenamePrivateBA", TaskRenamePrivateBA, 1},
{"TaskRenamePrivateAB", TaskRenamePrivateAB, 1},
{"TaskOpenPrivateA", TaskOpenPrivateA, 5},
{"TaskCloseFd", TaskCloseFd, 2},
{"TaskWriteFdBig", TaskWriteFdBig, 20},
{"TaskWriteFdSmall", TaskWriteFdSmall, 20},
{"TaskTruncateFd", TaskTruncateFd, 2},
{"TaskUtimeFd", TaskUtimeFd, 2},
{"TaskSeekFd", TaskSeekFdStart, 2},
{"TaskSeekFdEnd", TaskSeekFdEnd, 2},
{"TaskTruncateA", TaskTruncateA, 1},
};
std::vector<std::unique_ptr<Worker>> all_workers_;
std::vector<RandomOp> operations_;
std::vector<thrd_t> threads_;
bool debug_ = false;
};
TEST_P(RandomOpTest, MultiThreaded) {
for (auto& w : all_workers_) {
// start the workers on separate threads
thrd_t t;
ASSERT_EQ(thrd_create(&t, DoRandomOperations, w.get()), thrd_success);
threads_.push_back(t);
}
for (thrd_t t : threads_) {
int rc;
ASSERT_EQ(thrd_join(t, &rc), thrd_success);
ASSERT_EQ(rc, kDone) << "Background thread joined, but failed";
}
}
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, RandomOpTest, testing::ValuesIn(AllTestFilesystems()),
testing::PrintToStringParamName());
} // namespace
} // namespace fs_test