blob: 111b9955f9427b602d6d8f5af087066aa098c48e [file] [log] [blame]
// Copyright 2021 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 "src/storage/f2fs/bcache.h"
#include <gtest/gtest.h>
#include "src/storage/f2fs/common.h"
#include "src/storage/f2fs/f2fs_layout.h"
#include "src/storage/lib/block_client/cpp/fake_block_device.h"
#include "unit_lib.h"
namespace f2fs {
namespace {
using block_client::FakeBlockDevice;
constexpr uint32_t kMinVolumeSize = 104'857'600;
constexpr uint32_t kNumBlocks = kMinVolumeSize / kDefaultSectorSize;
TEST(BCacheTest, Trim) {
{
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = kNumBlocks, .block_size = kDefaultSectorSize, .supports_trim = false});
ASSERT_TRUE(device);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_TRUE(bc_or.is_ok());
fuchsia_hardware_block::wire::BlockInfo info;
bc_or->BlockGetInfo(&info);
block_t end_blk = static_cast<block_t>(bc_or->Maxblk() / (kBlockSize / info.block_size));
ASSERT_EQ(bc_or->Trim(0, end_blk), ZX_ERR_NOT_SUPPORTED);
}
{
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = kNumBlocks, .block_size = kDefaultSectorSize, .supports_trim = true});
ASSERT_TRUE(device);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_TRUE(bc_or.is_ok());
fuchsia_hardware_block::wire::BlockInfo info;
bc_or->BlockGetInfo(&info);
block_t end_blk = static_cast<block_t>(bc_or->Maxblk() / (kBlockSize / info.block_size));
ASSERT_EQ(bc_or->Trim(0, end_blk), ZX_OK);
}
}
TEST(BCacheTest, Exception) {
// Test zero block_size exception case
{
std::unique_ptr<f2fs::BcacheMapper> bc;
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = kNumBlocks, .block_size = 0, .supports_trim = false});
ASSERT_TRUE(device);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_TRUE(bc_or.is_error());
ASSERT_EQ(bc_or.status_value(), ZX_ERR_BAD_STATE);
}
// Test block_count overflow exception case
{
std::unique_ptr<f2fs::BcacheMapper> bc;
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = static_cast<uint64_t>(std::numeric_limits<uint32_t>::max()) * 8,
.block_size = kDefaultSectorSize,
.supports_trim = true});
ASSERT_TRUE(device);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_TRUE(bc_or.is_error());
ASSERT_EQ(bc_or.status_value(), ZX_ERR_OUT_OF_RANGE);
}
}
TEST(BCacheTest, VmoidReuse) {
std::unique_ptr<f2fs::BcacheMapper> bc;
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = kNumBlocks, .block_size = kDefaultSectorSize, .supports_trim = false});
ASSERT_TRUE(device);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_FALSE(bc_or.is_error());
zx::vmo vmo;
ASSERT_EQ(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo), ZX_OK);
storage::Vmoid vmoid;
// When the BcacheMapper is created, BlockAttachVmo() is called once internally. Therefore, the
// expected_vmoid starts at BLOCK_VMOID_INVALID + 2.
for (vmoid_t expected_vmoid = BLOCK_VMOID_INVALID + 2;
expected_vmoid < std::numeric_limits<vmoid_t>::max(); ++expected_vmoid) {
ASSERT_EQ(bc_or->BlockAttachVmo(vmo, &vmoid), ZX_OK);
ASSERT_EQ(vmoid.get(), expected_vmoid);
ASSERT_EQ(bc_or->BlockDetachVmo(std::move(vmoid)), ZX_OK);
}
ASSERT_EQ(bc_or->BlockAttachVmo(vmo, &vmoid), ZX_OK);
ASSERT_EQ(vmoid.get(), BLOCK_VMOID_INVALID + 2);
ASSERT_EQ(bc_or->BlockDetachVmo(std::move(vmoid)), ZX_OK);
}
TEST(BCacheTest, Flag) {
std::unique_ptr<f2fs::BcacheMapper> bc;
bool readonly_device = false;
auto device = std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = kNumBlocks, .block_size = kDefaultSectorSize, .supports_trim = false});
ASSERT_TRUE(device);
fuchsia_hardware_block::wire::Flag test_flag = fuchsia_hardware_block::wire::Flag::kReadonly |
fuchsia_hardware_block::wire::Flag::kRemovable |
fuchsia_hardware_block::wire::Flag::kTrimSupport |
fuchsia_hardware_block::wire::Flag::kFuaSupport;
device->SetInfoFlags(test_flag);
auto bc_or = CreateBcacheMapper(std::move(device), &readonly_device);
ASSERT_FALSE(bc_or.is_error());
fuchsia_hardware_block::wire::BlockInfo info;
bc_or->BlockGetInfo(&info);
ASSERT_EQ(test_flag, info.flags);
}
class BcacheMapperTest
: public testing::Test,
public testing::WithParamInterface<std::pair<std::vector<uint64_t>, std::vector<uint32_t>>> {
public:
BcacheMapperTest() : block_counts_(GetParam().first), block_sizes_(GetParam().second) {}
void SetUp() override {
ASSERT_EQ(block_counts_.size(), block_sizes_.size());
for (size_t i = 0; i < block_counts_.size(); ++i) {
fake_block_devices_.push_back(std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = block_counts_[i], .block_size = block_sizes_[i], .supports_trim = true}));
}
bool readonly_device = false;
auto bc_or = CreateBcacheMapper(std::move(fake_block_devices_), &readonly_device);
ASSERT_TRUE(bc_or.is_ok());
ASSERT_FALSE(readonly_device);
bcache_ = std::move(bc_or.value());
}
protected:
uint64_t GetMiddlePositionOfDevice(uint32_t num) {
ZX_ASSERT(num < block_counts_.size());
uint64_t start_off = 0;
uint64_t end_off = 0;
for (size_t i = 0; i < num; ++i) {
start_off += block_counts_[i] * block_sizes_[i] / bcache_->BlockSize();
}
end_off = start_off + block_counts_[num] * block_sizes_[num] / bcache_->BlockSize() - 1;
return (start_off + end_off) / 2;
}
uint64_t GetEndPositionOfDevice(uint32_t num) {
ZX_ASSERT(num < block_counts_.size());
uint64_t end_off = 0;
for (size_t i = 0; i <= num; ++i) {
end_off += block_counts_[i] * block_sizes_[i] / bcache_->BlockSize();
}
end_off--;
return end_off;
}
const std::vector<uint64_t> block_counts_;
const std::vector<uint32_t> block_sizes_;
std::vector<std::unique_ptr<block_client::BlockDevice>> fake_block_devices_;
std::unique_ptr<BcacheMapper> bcache_;
};
TEST_P(BcacheMapperTest, BlockGetInfo) {
fuchsia_hardware_block::wire::BlockInfo info;
bcache_->BlockGetInfo(&info);
// Check block size
ASSERT_EQ(info.block_size, *std::max_element(block_sizes_.begin(), block_sizes_.end()));
// Check block count
uint64_t byte_size = 0;
bcache_->ForEachBcache([&](Bcache *underlying_bc) {
byte_size += underlying_bc->Maxblk() * underlying_bc->BlockSize();
});
ASSERT_EQ(info.block_count * info.block_size, byte_size);
}
TEST_P(BcacheMapperTest, AttachDetachVmo) {
zx::vmo vmo;
ASSERT_EQ(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo), ZX_OK);
storage::Vmoid vmoid;
ASSERT_EQ(bcache_->BlockAttachVmo(vmo, &vmoid), ZX_OK);
ASSERT_EQ(bcache_->BlockDetachVmo(std::move(vmoid)), ZX_OK);
}
TEST_P(BcacheMapperTest, RunRequests) {
std::vector<std::pair<uint64_t, uint64_t>> dev_offset_and_length = {};
for (uint32_t start = 0; start < block_counts_.size(); ++start) {
for (uint32_t end = start; end < block_counts_.size(); ++end) {
dev_offset_and_length.emplace_back(
GetMiddlePositionOfDevice(start),
GetEndPositionOfDevice(end) - GetMiddlePositionOfDevice(start) + 1);
}
}
std::vector<char> pattern(bcache_->BlockSize(), 0b10101010);
for (const auto &[dev_offset, length] : dev_offset_and_length) {
storage::VmoBuffer buffer;
ASSERT_EQ(buffer.Initialize(bcache_.get(), bcache_->Maxblk(), bcache_->BlockSize(), "test"),
ZX_OK);
for (size_t i = 0; i < length; ++i) {
std::memcpy(buffer.Data(dev_offset + i), pattern.data(), bcache_->BlockSize());
}
ASSERT_EQ(bcache_->RunRequests(
{storage::BufferedOperation{.vmoid = buffer.vmoid(),
.op =
{
.type = storage::OperationType::kWrite,
.vmo_offset = dev_offset,
.dev_offset = dev_offset,
.length = length,
}}}),
ZX_OK);
buffer.Zero(dev_offset, length);
ASSERT_EQ(bcache_->RunRequests(
{storage::BufferedOperation{.vmoid = buffer.vmoid(),
.op =
{
.type = storage::OperationType::kRead,
.vmo_offset = dev_offset,
.dev_offset = dev_offset,
.length = length,
}}}),
ZX_OK);
for (size_t i = 0; i < length; ++i) {
auto ptr = buffer.Data(dev_offset + i);
ASSERT_EQ(std::memcmp(ptr, pattern.data(), bcache_->BlockSize()), 0);
}
}
}
std::set<uint64_t> GetRandomValuesWithoutDuplicates(uint64_t max, uint32_t count) {
std::set<uint64_t> random_numbers;
while (random_numbers.size() != count) {
uint64_t random_number =
static_cast<uint64_t>((static_cast<uint64_t>(rand()) << 32 | rand())) % max;
random_numbers.insert(random_number);
}
return random_numbers;
}
TEST_P(BcacheMapperTest, RunRequestsRandom) {
constexpr uint32_t kBatchSize = 3;
srand(testing::UnitTest::GetInstance()->random_seed());
auto random_offsets = GetRandomValuesWithoutDuplicates(bcache_->Maxblk(), kBatchSize * 2);
char random_pattern[kBatchSize];
{
std::vector<storage::BufferedOperation> operations;
storage::VmoBuffer buffer[kBatchSize];
uint32_t iteration = 0;
uint64_t prev = 0;
for (auto offset : random_offsets) {
if (iteration % 2 == 0) {
prev = offset;
} else {
uint64_t length = offset - prev;
ASSERT_EQ(
buffer[iteration / 2].Initialize(bcache_.get(), length, bcache_->BlockSize(), "test"),
ZX_OK);
random_pattern[iteration / 2] = static_cast<char>(rand() % 128);
for (size_t i = 0; i < length; ++i) {
std::memset(buffer[iteration / 2].Data(i), random_pattern[iteration / 2],
bcache_->BlockSize());
}
operations.push_back(storage::BufferedOperation{.vmoid = buffer[iteration / 2].vmoid(),
.op = storage::Operation{
.type = storage::OperationType::kWrite,
.vmo_offset = 0,
.dev_offset = prev,
.length = length,
}});
}
++iteration;
}
ASSERT_EQ(bcache_->RunRequests(operations), ZX_OK);
}
// Read Verify
{
std::vector<storage::BufferedOperation> operations;
storage::VmoBuffer buffer[kBatchSize];
uint32_t iteration = 0;
uint64_t prev = 0;
for (auto offset : random_offsets) {
if (iteration % 2 == 0) {
prev = offset;
} else {
uint64_t length = offset - prev;
ASSERT_EQ(
buffer[iteration / 2].Initialize(bcache_.get(), length, bcache_->BlockSize(), "test"),
ZX_OK);
operations.push_back(storage::BufferedOperation{.vmoid = buffer[iteration / 2].vmoid(),
.op = storage::Operation{
.type = storage::OperationType::kRead,
.vmo_offset = 0,
.dev_offset = prev,
.length = length,
}});
}
++iteration;
}
ASSERT_EQ(bcache_->RunRequests(operations), ZX_OK);
for (uint32_t i = 0; i < kBatchSize; ++i) {
for (uint32_t block = 0; block < buffer[i].capacity(); ++block) {
char *data = static_cast<char *>(buffer[i].Data(block));
for (uint32_t byte = 0; byte < buffer[i].BlockSize(); ++byte) {
ASSERT_EQ(data[byte], random_pattern[i]);
}
}
}
}
}
class F2fsMountedBcacheMapperTest : public BcacheMapperTest {
public:
F2fsMountedBcacheMapperTest() : BcacheMapperTest() {}
void MkfsOnMultiFakeDevWithOptions(std::unique_ptr<BcacheMapper> *bc, const MkfsOptions &options,
std::vector<uint64_t> block_counts,
std::vector<uint32_t> block_sizes, bool btrim) {
ASSERT_EQ(block_counts.size(), block_sizes.size());
std::vector<std::unique_ptr<block_client::BlockDevice>> devices;
for (size_t i = 0; i < block_counts.size(); ++i) {
devices.push_back(std::make_unique<FakeBlockDevice>(FakeBlockDevice::Config{
.block_count = block_counts[i], .block_size = block_sizes[i], .supports_trim = btrim}));
}
bool readonly_device = false;
auto bc_or = CreateBcacheMapper(std::move(devices), &readonly_device);
ASSERT_TRUE(bc_or.is_ok());
MkfsWorker mkfs(std::move(*bc_or), options);
auto ret = mkfs.DoMkfs();
ASSERT_EQ(ret.is_error(), false);
*bc = std::move(*ret);
}
void SetUp() override {
BcacheMapperTest::SetUp();
MkfsOnMultiFakeDevWithOptions(&bc_, MkfsOptions{}, block_counts_, block_sizes_, true);
FileTester::MountWithOptions(loop_.dispatcher(), MountOptions{}, &bc_, &fs_);
fbl::RefPtr<VnodeF2fs> root;
FileTester::CreateRoot(fs_.get(), &root);
root_dir_ = fbl::RefPtr<Dir>::Downcast(std::move(root));
}
void TearDown() override {
ASSERT_EQ(root_dir_->Close(), ZX_OK);
root_dir_ = nullptr;
FileTester::Unmount(std::move(fs_), &bc_);
FsckWorker fsck(std::move(bc_), FsckOptions{.repair = false});
ASSERT_EQ(fsck.Run(), ZX_OK);
}
protected:
std::unique_ptr<f2fs::BcacheMapper> bc_;
std::unique_ptr<F2fs> fs_;
fbl::RefPtr<Dir> root_dir_;
async::Loop loop_ = async::Loop(&kAsyncLoopConfigAttachToCurrentThread);
};
TEST_P(F2fsMountedBcacheMapperTest, ReadWrite) {
srand(testing::UnitTest::GetInstance()->random_seed());
zx::result test_file = root_dir_->Create("test", fs::CreationType::kFile);
ASSERT_TRUE(test_file.is_ok()) << test_file.status_string();
fbl::RefPtr<VnodeF2fs> test_file_vn = fbl::RefPtr<VnodeF2fs>::Downcast(*std::move(test_file));
File *test_file_ptr = static_cast<File *>(test_file_vn.get());
std::array<size_t, 5> num_pages = {1, 2, 4};
size_t total_pages = 0;
for (auto i : num_pages) {
total_pages += i;
}
const size_t data_size = kPageSize * total_pages;
auto w_buf = std::make_unique<char[]>(data_size);
for (size_t i = 0; i < data_size; ++i) {
w_buf[i] = static_cast<char>(rand() % 128);
}
// Write data for various sizes
char *w_buf_iter = w_buf.get();
for (auto i : num_pages) {
size_t cur_size = i * kPageSize;
FileTester::AppendToFile(test_file_ptr, w_buf_iter, cur_size);
w_buf_iter += cur_size;
}
ASSERT_EQ(test_file_ptr->GetSize(), data_size);
// Read verify again after clearing file cache
char r_buf[kPageSize];
{
WritebackOperation op = {.bSync = true};
test_file_ptr->Writeback(op);
test_file_ptr->ResetFileCache();
}
w_buf_iter = w_buf.get();
for (size_t i = 0; i < total_pages; ++i) {
FileTester::ReadFromFile(test_file_ptr, r_buf, kPageSize, i * kPageSize);
ASSERT_EQ(memcmp(r_buf, w_buf_iter, kPageSize), 0);
w_buf_iter += kPageSize;
}
ASSERT_EQ(test_file_vn->Close(), ZX_OK);
test_file_vn = nullptr;
}
const std::array<std::pair<std::vector<uint64_t>, std::vector<uint32_t>>, 3> kBlockCountsAndSizes =
{{{{65536, 65536, 65536, 65536},
{kDefaultSectorSize, kDefaultSectorSize, kDefaultSectorSize, kDefaultSectorSize}},
{{10, 10, 10, 200000},
{kDefaultSectorSize, kDefaultSectorSize, kDefaultSectorSize, kDefaultSectorSize}},
{{65535, 32768, 65535, 32768},
{kDefaultSectorSize, kDefaultSectorSize * 2, kDefaultSectorSize, kDefaultSectorSize * 2}}}};
INSTANTIATE_TEST_SUITE_P(BcacheMapperTest, BcacheMapperTest,
::testing::ValuesIn(kBlockCountsAndSizes));
INSTANTIATE_TEST_SUITE_P(F2fsMountedBcacheMapperTest, F2fsMountedBcacheMapperTest,
::testing::ValuesIn(kBlockCountsAndSizes));
} // namespace
} // namespace f2fs