blob: 7e5ae44d335863c18e18094fa3bad6e3f4582132 [file] [log] [blame]
// Copyright 2019 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.
// Tests Transaction behavior.
#include <memory>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/storage/minfs/format.h"
#include "src/storage/minfs/minfs_private.h"
#include "src/storage/minfs/unowned_vmo_buffer.h"
#include "src/storage/minfs/writeback.h"
#include "zircon/errors.h"
namespace minfs {
namespace {
using ::testing::_;
constexpr size_t kTotalElements = 32768;
constexpr size_t kDefaultElements = kTotalElements / 64;
constexpr size_t kDefaultStartBlock = 0;
// Fake Storage class to be used in Transaction tests.
class FakeStorage : public AllocatorStorage {
public:
FakeStorage() = delete;
FakeStorage(const FakeStorage&) = delete;
FakeStorage& operator=(const FakeStorage&) = delete;
explicit FakeStorage(uint32_t units) : pool_used_(0), pool_total_(units) {}
~FakeStorage() override = default;
zx::result<> AttachVmo(const zx::vmo& vmo, storage::OwnedVmoid* vmoid) final { return zx::ok(); }
void Load(fs::BufferedOperationsBuilder* builder, storage::BlockBuffer* data) final {}
zx::result<> Extend(PendingWork* transaction, WriteData data, GrowMapCallback grow_map) final {
return zx::error(ZX_ERR_NO_SPACE);
}
uint32_t PoolAvailable() const final { return pool_total_ - pool_used_; }
uint32_t PoolTotal() const final { return pool_total_; }
// Write back the allocation of the following items to disk.
void PersistRange(PendingWork* transaction, WriteData data, size_t index, size_t count) final {}
void PersistAllocate(PendingWork* transaction, size_t count) final {
ZX_DEBUG_ASSERT(pool_used_ + count <= pool_total_);
pool_used_ += static_cast<uint32_t>(count);
}
void PersistRelease(PendingWork* transaction, size_t count) final {
ZX_DEBUG_ASSERT(pool_used_ >= count);
pool_used_ -= static_cast<uint32_t>(count);
}
private:
uint32_t pool_used_;
uint32_t pool_total_;
};
// Fake BlockDevice class to be used in Transaction tests.
class FakeBlockDevice : public block_client::BlockDevice {
public:
FakeBlockDevice() = default;
zx_status_t FifoTransaction(block_fifo_request_t* requests, size_t count) final { return ZX_OK; }
zx::result<std::string> GetTopologicalPath() const final { return zx::ok(std::string()); }
zx::result<> Rebind(std::string_view url_suffix) const override {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
zx_status_t BlockGetInfo(fuchsia_hardware_block::wire::BlockInfo* out_info) const final {
return ZX_OK;
}
zx_status_t BlockAttachVmo(const zx::vmo& vmo, storage::Vmoid* out_vmoid) final { return ZX_OK; }
zx_status_t VolumeGetInfo(
fuchsia_hardware_block_volume::wire::VolumeManagerInfo* out_manager,
fuchsia_hardware_block_volume::wire::VolumeInfo* out_volume) const final {
return ZX_OK;
}
zx_status_t VolumeQuerySlices(const uint64_t* slices, size_t slices_count,
fuchsia_hardware_block_volume::wire::VsliceRange* out_ranges,
size_t* out_ranges_count) const final {
return ZX_OK;
}
zx_status_t VolumeExtend(uint64_t offset, uint64_t length) final { return ZX_OK; }
zx_status_t VolumeShrink(uint64_t offset, uint64_t length) final { return ZX_OK; }
};
// Mock Minfs class to be used in Transaction tests.
class FakeMinfs : public TransactionalFs {
public:
FakeMinfs() {
info_.inode_count = kTotalElements;
info_.block_size = kMinfsBlockSize;
}
fbl::Mutex* GetLock() const override { return &txn_lock_; }
zx::result<std::unique_ptr<Transaction>> BeginTransaction(size_t reserve_inodes,
size_t reserve_blocks) override {
return zx::ok(nullptr);
}
void EnqueueCallback(SyncCallback callback) override {}
void CommitTransaction(std::unique_ptr<Transaction> transaction) override {}
Bcache* GetMutableBcache() override { return nullptr; }
Allocator& GetBlockAllocator() override {
if (!block_allocator_) {
std::unique_ptr<FakeStorage> storage(new FakeStorage(kTotalElements));
auto block_allocator_or = Allocator::Create(&builder_, std::move(storage));
ZX_ASSERT(block_allocator_or.is_ok());
block_allocator_ = std::move(block_allocator_or.value());
}
return *block_allocator_;
}
Allocator& GetInodeAllocator() override { return GetInodeManager().inode_allocator(); }
InodeManager& GetInodeManager() {
if (!inode_manager_) {
// Create superblock manager.
auto sb_manager_or = SuperblockManager::Create(&block_device_, info_, kDefaultStartBlock,
IntegrityCheck::kNone);
ZX_ASSERT(sb_manager_or.is_ok());
superblock_manager_ = std::move(sb_manager_or.value());
// Create inode manager.
AllocatorFvmMetadata fvm_metadata;
AllocatorMetadata metadata(kDefaultStartBlock, kDefaultStartBlock, false,
std::move(fvm_metadata), superblock_manager_.get(),
SuperblockAllocatorAccess::Inodes());
auto inode_manager_or =
InodeManager::Create(&block_device_, superblock_manager_.get(), &builder_,
std::move(metadata), kDefaultStartBlock, kTotalElements);
ZX_ASSERT(inode_manager_or.is_ok());
inode_manager_ = std::move(inode_manager_or.value());
}
return *inode_manager_;
}
zx::result<std::unique_ptr<Transaction>> CreateTransaction(size_t inodes, size_t blocks) {
return Transaction::Create(this, inodes, blocks, &GetInodeManager());
}
zx::result<std::unique_ptr<Transaction>> ContinueTransaction(
size_t blocks, std::unique_ptr<CachedBlockTransaction> cached_transaction) {
std::unique_ptr<Transaction> transaction =
Transaction::FromCachedBlockTransaction(this, std::move(cached_transaction));
if (auto status = transaction->ExtendBlockReservation(blocks); status.is_error()) {
return status.take_error();
}
return zx::ok(std::move(transaction));
}
private:
mutable fbl::Mutex txn_lock_;
FakeBlockDevice block_device_;
fs::BufferedOperationsBuilder builder_;
Superblock info_ = {};
std::unique_ptr<SuperblockManager> superblock_manager_;
std::unique_ptr<InodeManager> inode_manager_;
std::unique_ptr<Allocator> block_allocator_;
};
// Creates a Transaction using the public constructor, which by default contains no reservations.
TEST(TransactionTest, CreateTransactionNoReservationsAlt) {
FakeMinfs minfs;
Transaction transaction(&minfs);
}
// Creates a Transaction with no reservations.
TEST(TransactionTest, CreateTransactionNoReservations) {
FakeMinfs minfs;
ASSERT_TRUE(minfs.CreateTransaction(0, 0).is_ok());
}
// Creates a Transaction with inode and block reservations.
TEST(TransactionTest, CreateTransactionWithReservations) {
FakeMinfs minfs;
ASSERT_TRUE(minfs.CreateTransaction(kDefaultElements, kDefaultElements).is_ok());
}
// Creates a Transaction with inode and block reservations. Try to extend block reservation.
TEST(TransactionTest, ExtendBlockReservation) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kDefaultElements, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
ASSERT_TRUE(transaction_or->ExtendBlockReservation(kTotalElements - kDefaultElements).is_ok());
}
// Creates a Transaction with inode and block reservations. Try to extend block reservation more
// than available.
TEST(TransactionTest, ExtendBlockReservationFails) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kDefaultElements, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
ASSERT_TRUE(
transaction_or->ExtendBlockReservation(kTotalElements + 1 - kDefaultElements).is_error());
}
// Creates a Transaction with the maximum possible number of inodes and blocks reserved.
TEST(TransactionTest, CreateTransactionWithMaxBlockReservations) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kTotalElements, kTotalElements);
ASSERT_TRUE(transaction_or.is_ok());
}
TEST(TransactionTest, FromCachedBlockTransaction) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
auto cached_transaction = std::make_unique<CachedBlockTransaction>(
Transaction::TakeBlockReservations(std::move(transaction_or.value())));
transaction_or =
minfs.ContinueTransaction(kTotalElements - kDefaultElements, std::move(cached_transaction));
ASSERT_TRUE(transaction_or.is_ok());
ASSERT_EQ(transaction_or->block_reservation().GetReserved(), kTotalElements);
}
TEST(TransactionTest, FromCachedBlockTransactionFailsToExtend) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
auto cached_transaction = std::make_unique<CachedBlockTransaction>(
Transaction::TakeBlockReservations(std::move(transaction_or.value())));
transaction_or = minfs.ContinueTransaction(kTotalElements + 1 - kDefaultElements,
std::move(cached_transaction));
ASSERT_TRUE(transaction_or.is_error());
}
// Attempts to create a transaction with more than the maximum available inodes reserved.
TEST(TransactionTest, CreateTransactionTooManyInodesFails) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kTotalElements + 1, 0);
ASSERT_EQ(transaction_or.status_value(), ZX_ERR_NO_SPACE);
}
// Attempts to create a transaction with more than the maximum available blocks reserved.
TEST(TransactionTest, CreateTransactionTooManyBlocksFails) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, kTotalElements + 1);
ASSERT_EQ(transaction_or.status_value(), ZX_ERR_NO_SPACE);
}
// Attempts to reserve an blocks and inodes and then try to take only block reservation.
TEST(TransactionDeathTest, TakeBlockReservationsWithInodeReservationDies) {
FakeMinfs minfs;
ASSERT_DEATH(
{
auto transaction_or = minfs.CreateTransaction(kDefaultElements, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
[[maybe_unused]] auto result =
Transaction::TakeBlockReservations(std::move(transaction_or.value()));
},
_);
}
// Tests allocation of a single inode.
TEST(TransactionTest, InodeAllocationSucceeds) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kDefaultElements, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
transaction_or->AllocateInode();
}
// Tests allocation of a single block.
TEST(TransactionTest, BlockAllocationSucceeds) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(kDefaultElements, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
transaction_or->AllocateBlock();
}
// Attempts to allocate an inode when the transaction was not initialized properly.
TEST(TransactionDeathTest, AllocateInodeWithoutInitializationFails) {
FakeMinfs minfs;
Transaction transaction(&minfs);
ASSERT_DEATH(transaction.AllocateInode(), _);
}
// Attempts to allocate a block when the transaction was not initialized properly.
TEST(TransactionDeathTest, AllocateBlockWithoutInitializationFails) {
FakeMinfs minfs;
Transaction transaction(&minfs);
ASSERT_DEATH(transaction.AllocateBlock(), _);
}
#if ZX_DEBUG_ASSERT_IMPLEMENTED
// Attempts to allocate an inode when none have been reserved.
TEST(TransactionDeathTest, AllocateTooManyInodesFails) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(1, 0);
ASSERT_TRUE(transaction_or.is_ok());
// First allocation should succeed.
transaction_or->AllocateInode();
// Second allocation should fail.
ASSERT_DEATH(transaction_or->AllocateInode(), _);
}
#endif
#if ZX_DEBUG_ASSERT_IMPLEMENTED
// Attempts to allocate a block when none have been reserved.
TEST(TransactionDeathTest, AllocateTooManyBlocksFails) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, 1);
ASSERT_TRUE(transaction_or.is_ok());
// First allocation should succeed.
transaction_or->AllocateBlock();
// Second allocation should fail.
ASSERT_DEATH(transaction_or->AllocateBlock(), _);
}
#endif
// Checks that the Transaction's work is empty before any writes have been enqueued.
TEST(TransactionTest, VerifyNoWorkExistsBeforeEnqueue) {
FakeMinfs minfs;
Transaction transaction(&minfs);
// Metadata operations should be empty.
std::vector<storage::UnbufferedOperation> meta_operations =
transaction.RemoveMetadataOperations();
ASSERT_TRUE(meta_operations.empty());
// Data work should be empty.
std::vector<storage::UnbufferedOperation> data_operations = transaction.RemoveDataOperations();
ASSERT_TRUE(data_operations.empty());
}
// Checks that the Transaction's metadata work is populated after enqueueing metadata writes.
TEST(TransactionTest, EnqueueAndVerifyMetadataWork) {
FakeMinfs minfs;
Transaction transaction(&minfs);
storage::Operation op = {
.type = storage::OperationType::kWrite,
.vmo_offset = 2,
.dev_offset = 3,
.length = 4,
};
UnownedVmoBuffer buffer(zx::unowned_vmo(1));
transaction.EnqueueMetadata(op, &buffer);
std::vector<storage::UnbufferedOperation> meta_operations =
transaction.RemoveMetadataOperations();
ASSERT_EQ(meta_operations.size(), 1ul);
ASSERT_EQ(1, meta_operations[0].vmo);
ASSERT_EQ(meta_operations[0].op.vmo_offset, 2ul);
ASSERT_EQ(meta_operations[0].op.dev_offset, 3ul);
ASSERT_EQ(meta_operations[0].op.length, 4ul);
ASSERT_EQ(storage::OperationType::kWrite, meta_operations[0].op.type);
}
// Checks that the Transaction's data work is populated after enqueueing data writes.
TEST(TransactionTest, EnqueueAndVerifyDataWork) {
FakeMinfs minfs;
Transaction transaction(&minfs);
storage::Operation op = {
.type = storage::OperationType::kWrite,
.vmo_offset = 2,
.dev_offset = 3,
.length = 4,
};
UnownedVmoBuffer buffer(zx::unowned_vmo(1));
transaction.EnqueueData(op, &buffer);
std::vector<storage::UnbufferedOperation> data_operations = transaction.RemoveDataOperations();
ASSERT_EQ(data_operations.size(), 1ul);
ASSERT_EQ(1, data_operations[0].vmo);
ASSERT_EQ(data_operations[0].op.vmo_offset, 2ul);
ASSERT_EQ(data_operations[0].op.dev_offset, 3ul);
ASSERT_EQ(data_operations[0].op.length, 4ul);
ASSERT_EQ(storage::OperationType::kWrite, data_operations[0].op.type);
}
class MockVnodeMinfs : public VnodeMinfs, public fbl::Recyclable<MockVnodeMinfs> {
public:
explicit MockVnodeMinfs(bool* alive) : VnodeMinfs(nullptr), alive_(alive) { *alive_ = true; }
~MockVnodeMinfs() override { *alive_ = false; }
// Required for memory management, see the class comment above Vnode for more.
void fbl_recycle() { RecycleNode(); }
private:
bool IsDirectory() const final { return false; }
zx::result<> CanUnlink() const final { return zx::error(ZX_ERR_NOT_SUPPORTED); }
// minfs::Vnode interface.
blk_t GetBlockCount() const final { return 0; }
uint64_t GetSize() const final { return 0; }
void SetSize(uint32_t new_size) final {}
void AcquireWritableBlock(Transaction* transaction, blk_t local_bno, blk_t old_bno,
blk_t* out_bno) final {}
void DeleteBlock(PendingWork* transaction, blk_t local_bno, blk_t old_bno, bool indirect) final {}
void IssueWriteback(Transaction* transaction, blk_t vmo_offset, blk_t dev_offset,
blk_t count) final {}
bool HasPendingAllocation(blk_t vmo_offset) final { return false; }
void CancelPendingWriteback() final {}
bool DirtyCacheEnabled() const final { return false; }
bool IsDirty() const final { return false; }
zx::result<> FlushCachedWrites() final { return zx::ok(); }
void DropCachedWrites() final {}
// fs::Vnode interface.
fuchsia_io::NodeProtocolKinds GetProtocols() const final {
return fuchsia_io::NodeProtocolKinds::kFile;
}
zx_status_t Read(void* data, size_t len, size_t off, size_t* out_actual) final { return ZX_OK; }
zx_status_t Write(const void* data, size_t len, size_t offset, size_t* out_actual) final {
return ZX_OK;
}
zx_status_t Append(const void* data, size_t len, size_t* out_end, size_t* out_actual) final {
return ZX_OK;
}
zx_status_t Truncate(size_t len) final { return ZX_OK; }
bool* alive_;
};
// Checks that a pinned vnode is not attached to the transaction's data work.
TEST(TransactionTest, RemovePinnedVnodeContainsVnode) {
FakeMinfs minfs;
bool vnode_alive = false;
fbl::RefPtr<MockVnodeMinfs> vnode(fbl::AdoptRef(new MockVnodeMinfs(&vnode_alive)));
ASSERT_TRUE(vnode_alive);
Transaction transaction(&minfs);
transaction.PinVnode(std::move(vnode));
ASSERT_EQ(nullptr, vnode);
std::vector<fbl::RefPtr<VnodeMinfs>> pinned_vnodes = transaction.RemovePinnedVnodes();
ASSERT_EQ(pinned_vnodes.size(), 1ul);
pinned_vnodes.clear();
ASSERT_FALSE(vnode_alive);
}
TEST(TransactionTest, RemovePinnedVnodeContainsManyVnodes) {
FakeMinfs minfs;
size_t vnode_count = 4;
bool vnode_alive[vnode_count];
fbl::RefPtr<MockVnodeMinfs> vnodes[vnode_count];
Transaction transaction(&minfs);
for (size_t i = 0; i < vnode_count; i++) {
vnode_alive[i] = false;
vnodes[i] = fbl::AdoptRef(new MockVnodeMinfs(&vnode_alive[i]));
ASSERT_TRUE(vnode_alive[i]);
transaction.PinVnode(std::move(vnodes[i]));
ASSERT_EQ(nullptr, vnodes[i]);
}
std::vector<fbl::RefPtr<VnodeMinfs>> pinned_vnodes = transaction.RemovePinnedVnodes();
ASSERT_EQ(vnode_count, pinned_vnodes.size());
pinned_vnodes.clear();
for (size_t i = 0; i < vnode_count; i++) {
ASSERT_FALSE(vnode_alive[i]);
}
}
TEST(CachedBlockTransactionTest, FromZeroBlockReservation) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, 0);
ASSERT_TRUE(transaction_or.is_ok());
CachedBlockTransaction cached_transaction(
Transaction::TakeBlockReservations(std::move(transaction_or.value())));
ASSERT_EQ(cached_transaction.TakeBlockReservations()->GetReserved(), 0ul);
}
TEST(CachedBlockTransactionTest, FewBlocksReserved) {
FakeMinfs minfs;
auto transaction_or = minfs.CreateTransaction(0, kDefaultElements);
ASSERT_TRUE(transaction_or.is_ok());
CachedBlockTransaction cached_transaction(
Transaction::TakeBlockReservations(std::move(transaction_or.value())));
ASSERT_EQ(cached_transaction.TakeBlockReservations()->GetReserved(), kDefaultElements);
}
} // namespace
} // namespace minfs