blob: 77524c9e29b3f624cd607d7819c8cc247eac17f6 [file] [log] [blame]
// Copyright 2020 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 <fcntl.h>
#include <lib/fdio/cpp/caller.h>
#include <unistd.h>
#include <zircon/errors.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test.h"
#include "src/storage/fs_test/fs_test_fixture.h"
#include "src/storage/minfs/format.h"
#include "src/storage/minfs/minfs_private.h"
namespace minfs {
namespace {
const std::string kDirtyBytesPropertyName = "dirty_bytes";
class DirtyCacheTest : public fs_test::FilesystemTest {
public:
uint64_t GetDirtyBytes() {
std::optional<diagnostics::reader::InspectData> snapshot;
fs().TakeSnapshot(&snapshot);
ZX_ASSERT_MSG(snapshot.has_value(), "Failed to take snapshot");
ZX_ASSERT_MSG(snapshot->payload().has_value(), "Failed to capture payload in snapshot");
const inspect::Hierarchy* detail_node = snapshot->payload().value()->GetByPath({"fs.detail"});
ZX_ASSERT_MSG(detail_node != nullptr, "Failed to find expected node in Inspect hierarchy!");
const auto* dirty_bytes_property =
detail_node->node().get_property<inspect::IntPropertyValue>(kDirtyBytesPropertyName);
ZX_ASSERT_MSG(dirty_bytes_property != nullptr,
"Failed to find dirty bytes property in specified node!");
return static_cast<uint64_t>(dirty_bytes_property->value());
}
};
class BufferedFile {
public:
BufferedFile(DirtyCacheTest* fs, const std::string& path, size_t max_size, size_t bytes_to_write,
size_t bytes_per_write, bool remount_verify = true) {
remount_verify_ = remount_verify;
ZX_ASSERT(fs != nullptr);
ZX_ASSERT(!path.empty());
ZX_ASSERT(bytes_to_write <= max_size);
if (bytes_per_write > 0 && bytes_to_write > 0) {
ZX_ASSERT(bytes_to_write >= bytes_per_write);
}
fs_ = fs;
bytes_per_write_ = bytes_per_write;
file_path_ = path;
fd_.reset(open(file_path_.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
ZX_ASSERT(fd_.is_valid());
buffer_.resize(max_size, 0);
// Fill buffer with "random" data.
for (size_t i = 0; i < max_size; i++) {
buffer_[i] = static_cast<uint8_t>(rand());
}
for (size_t i = 0; i < bytes_to_write; i += bytes_per_write) {
if ((i + bytes_per_write) > bytes_to_write) {
bytes_per_write = bytes_to_write % bytes_per_write_;
}
Write(bytes_per_write, i);
}
}
~BufferedFile() {
// Verify open fd.
Verify(expected_file_size_);
// Verify close/reopened file.
Reopen();
Verify(expected_file_size_);
// Verify file after unmount and mount.
if (remount_verify_) {
RemountAndReopen();
Verify(expected_file_size_);
}
}
void Reopen() {
fd_.reset(open(file_path_.c_str(), O_RDWR));
ASSERT_TRUE(fd_);
}
void Write(size_t bytes, std::optional<off_t> offset = std::nullopt) {
if (!offset) {
offset = lseek(fd_.get(), 0, SEEK_CUR);
}
ASSERT_TRUE(offset.value() < static_cast<off_t>(buffer_.size()));
ASSERT_TRUE((offset.value() + bytes) <= buffer_.size());
ASSERT_EQ(lseek(fd_.get(), offset.value(), SEEK_SET), offset.value());
ASSERT_EQ(write(fd_.get(), buffer_.data() + offset.value(), bytes), static_cast<off_t>(bytes));
if (expected_file_size_ < (offset.value() + bytes)) {
expected_file_size_ = offset.value() + bytes;
}
}
void RemountAndReopen() {
ASSERT_TRUE(fs_->fs().Unmount().is_ok());
ASSERT_TRUE(fs_->fs().Mount().is_ok());
Reopen();
}
private:
// Verifies file size and contents.
void Verify(size_t expected_size) {
struct stat stats;
ASSERT_EQ(fstat(fd_.get(), &stats), 0);
ASSERT_EQ(stats.st_size, static_cast<off_t>(expected_size));
std::unique_ptr<uint8_t[]> read_buffer(new uint8_t[expected_size]);
ASSERT_EQ(pread(fd_.get(), read_buffer.get(), expected_size, 0),
static_cast<ssize_t>(expected_size));
ASSERT_EQ(std::memcmp(buffer_.data(), read_buffer.get(), expected_size), 0);
}
// Pointer to the test to help unmount and remount.
DirtyCacheTest* fs_ = nullptr;
// Absolute path of the file.
std::string file_path_;
// File's open handle.
fbl::unique_fd fd_;
// File's content. Only |expected_file_size_| bytes of this are valid.
std::vector<uint8_t> buffer_;
// The max write size at a time.
size_t bytes_per_write_;
// The current expected size of the file.
size_t expected_file_size_ = 0;
// If true, verifies file after unmount and mount.
bool remount_verify_ = true;
};
BufferedFile CreateTestFile(DirtyCacheTest* fs, size_t file_max_size, int bytes_to_write,
int bytes_per_write, const std::string& file_name = "foo",
bool remount_verify = true) {
return BufferedFile(fs, fs->fs().mount_path() + file_name, file_max_size, bytes_to_write,
bytes_per_write, remount_verify);
}
constexpr size_t kBytesPerWrite = kMinfsBlockSize;
constexpr size_t kBytesToWrite = 2 * kBytesPerWrite;
constexpr size_t kFileMaxSize = kBytesToWrite;
TEST_P(DirtyCacheTest, CleanlyMountedFs) {
{ auto file = CreateTestFile(this, kFileMaxSize, 0, 0); }
ASSERT_EQ(GetDirtyBytes(), 0u);
}
TEST_P(DirtyCacheTest, DirtyBytesAfterWrite) {
auto file = CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), kFileMaxSize);
}
TEST_P(DirtyCacheTest, NoDirtyByteAfterClose) {
{
auto file = CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), kFileMaxSize);
}
ASSERT_EQ(GetDirtyBytes(), 0u);
}
TEST_P(DirtyCacheTest, UnmountFlushedPendingWrites) {
auto file = CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), kFileMaxSize);
file.RemountAndReopen();
}
TEST_P(DirtyCacheTest, MultipleByteWriteToSameBlockKeepsDirtyBytesTheSame) {
constexpr size_t kFileMaxSize = kMinfsBlockSize;
constexpr size_t kBytesPerWrite = 10;
{
auto file = CreateTestFile(this, kFileMaxSize, kFileMaxSize, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), minfs::kMinfsBlockSize);
}
ASSERT_EQ(GetDirtyBytes(), 0u);
}
TEST_P(DirtyCacheTest, MultipleBlocWriteToSameOffsetKeepsDirtyBytesTheSame) {
{
constexpr size_t kBytesPerWrite = kMinfsBlockSize;
constexpr size_t kBytesToWrite = kBytesPerWrite;
constexpr size_t kFileMaxSize = kBytesToWrite;
auto file = CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), kBytesPerWrite);
for (int i = 0; i < 10; i++) {
file.Write(kBytesPerWrite, 0);
ASSERT_EQ(GetDirtyBytes(), kBytesPerWrite);
}
ASSERT_EQ(GetDirtyBytes(), kBytesPerWrite);
}
ASSERT_EQ(GetDirtyBytes(), 0u);
}
TEST_P(DirtyCacheTest, MultipleBlocWritesMakesMultipleBlocksDirty) {
constexpr size_t kBytesPerWrite = kMinfsBlockSize;
constexpr size_t kBytesToWrite = 2 * kBytesPerWrite;
constexpr size_t kFileMaxSize = kBytesToWrite;
{
auto file = CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite);
ASSERT_EQ(GetDirtyBytes(), kBytesToWrite);
}
ASSERT_EQ(GetDirtyBytes(), 0u);
}
// Test creates a few files and writes to few of them. He we test that the fs handles such a case
// well.
TEST_P(DirtyCacheTest, FewCleanFewDirtyFiles) {
constexpr size_t kBytesPerWrite = kMinfsBlockSize;
constexpr size_t kBytesToWrite = 2 * kBytesPerWrite;
constexpr size_t kFileMaxSize = kBytesToWrite;
{
auto dirty1 =
CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite, "dirty1", false);
auto clean1 = CreateTestFile(this, 0, 0, 0, "clean1", false);
auto dirty2 =
CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite, "dirty2", false);
auto clean2 = CreateTestFile(this, 0, 0, 0, "clean2", false);
auto dirty3 =
CreateTestFile(this, kFileMaxSize, kBytesToWrite, kBytesPerWrite, "dirty3", false);
ASSERT_EQ(GetDirtyBytes(), kBytesToWrite * 3);
}
}
} // namespace
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, DirtyCacheTest,
testing::ValuesIn(fs_test::AllTestFilesystems()),
testing::PrintToStringParamName());
} // namespace minfs