blob: 60098c1a94bc9e2d2b226f99b47083cda7536c1d [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.
#include <lib/fit/defer.h>
#include <gtest/gtest.h>
#include "src/storage/lib/block_client/cpp/fake_block_device.h"
#include "src/storage/minfs/file.h"
#include "src/storage/minfs/format.h"
#include "src/storage/minfs/fsck.h"
#include "src/storage/minfs/runner.h"
#include "src/storage/minfs/test/unit/journal_integration_fixture.h"
namespace minfs {
namespace {
class JournalIntegrationTest : public JournalIntegrationFixture {
private:
// Creates an entry in the root of the filesystem and synchronizing writeback operations to
// storage.
void PerformOperation(Minfs& fs) override {
auto root = fs.VnodeGet(kMinfsRootIno);
ASSERT_TRUE(root.is_ok());
zx::result child = root->Create("foo", fs::CreationType::kFile);
ASSERT_TRUE(child.is_ok()) << child.status_string();
ASSERT_EQ(child->Close(), ZX_OK);
}
};
// WARNING: The numbers here may change if the filesystem issues different write patterns. Sadly,
// if write patterns do change, careful debugging needs to be done to find the new correct values.
//
// The important properties to preserve are:
// - Fsck (without journal replay) should fail.
// - Fsck (with journal replay) should succeed.
constexpr uint64_t kCreateEntryCutoff{UINT64_C(4) * JournalIntegrationTest::kDiskBlocksPerFsBlock};
TEST_F(JournalIntegrationTest, FsckWithRepairDoesReplayJournal) {
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kCreateEntryCutoff));
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
// We should be able to re-run fsck with the same results, with or without repairing.
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = false});
EXPECT_TRUE(bcache_or.is_ok());
}
TEST_F(JournalIntegrationTest, FsckWithReadOnlyDoesNotReplayJournal) {
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kCreateEntryCutoff));
EXPECT_TRUE(Fsck(std::move(bcache_or.value()), FsckOptions{.repair = false, .read_only = true})
.is_error());
}
TEST_F(JournalIntegrationTest, CreateWithRepairDoesReplayJournal) {
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kCreateEntryCutoff));
MountOptions options = {};
auto fs_or = Runner::Create(dispatcher(), std::move(bcache_or.value()), options);
EXPECT_TRUE(fs_or.is_ok());
bcache_or = zx::ok(Runner::Destroy(std::move(fs_or.value())));
EXPECT_TRUE(Fsck(std::move(bcache_or.value()), FsckOptions()).is_ok());
}
class JournalUnlinkTest : public JournalIntegrationFixture {
private:
// Creating but also removing an entry from the root of the filesystem, while a connection to the
// unlinked vnode remains alive.
void PerformOperation(Minfs& fs) override {
auto root = fs.VnodeGet(kMinfsRootIno);
ASSERT_TRUE(root.is_ok());
zx::result foo = root->Create("foo", fs::CreationType::kFile);
ASSERT_TRUE(foo.is_ok()) << foo.status_string();
zx::result bar = root->Create("bar", fs::CreationType::kFile);
ASSERT_TRUE(bar.is_ok()) << bar.status_string();
zx::result baz = root->Create("baz", fs::CreationType::kFile);
ASSERT_TRUE(baz.is_ok()) << baz.status_string();
ASSERT_EQ(root->Unlink("foo", false), ZX_OK);
ASSERT_EQ(root->Unlink("bar", false), ZX_OK);
ASSERT_EQ(root->Unlink("baz", false), ZX_OK);
RecordWriteCount(fs);
// This should succeed on the first pass when measuring, but will fail on the second pass when
// the fake device starts to fail writes.
foo->Close();
bar->Close();
baz->Close();
}
};
// Cuts the "unlink" operation off. Unlink typically needs to update the parent inode, the parent
// directory, and the inode allocation bitmap. By cutting the operation in two (without replay),
// the consistency checker should be able to identify inconsistent link counts between the multiple
// data structures.
//
// See note at beginning regarding tuning these numbers.
constexpr uint64_t kUnlinkCutoff{UINT64_C(3) * JournalUnlinkTest::kDiskBlocksPerFsBlock};
TEST_F(JournalUnlinkTest, FsckWithRepairDoesReplayJournal) {
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kUnlinkCutoff));
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
// We should be able to re-run fsck with the same results, with or without repairing.
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = false});
EXPECT_TRUE(bcache_or.is_ok());
}
TEST_F(JournalUnlinkTest, ReadOnlyFsckDoesNotReplayJournal) {
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kUnlinkCutoff));
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = false, .read_only = true});
EXPECT_TRUE(bcache_or.is_error());
}
class JournalGrowFvmTest : public JournalIntegrationFixture {
private:
void PerformOperation(Minfs& fs) override {
auto root = fs.VnodeGet(kMinfsRootIno);
ASSERT_TRUE(root.is_ok());
zx::result foo = root->Create("foo", fs::CreationType::kFile);
ASSERT_TRUE(foo.is_ok()) << foo.status_string();
// Write to a file until we cause an FVM extension.
std::vector<uint8_t> buf(TransactionLimits::kMaxWriteBytes);
size_t done = 0;
uint32_t slices = fs.Info().dat_slices;
while (fs.Info().dat_slices == slices) {
size_t written;
ASSERT_EQ(foo->Write(buf.data(), buf.size(), done, &written), ZX_OK);
ASSERT_EQ(written, buf.size());
done += written;
}
ASSERT_EQ(foo->Close(), ZX_OK);
// The infrastructure relies on the number of blocks written to block device
// to function properly. Sync here ensures that what was written in this
// function gets persisted to underlying block device.
sync_completion_t completion;
fs.Sync([&completion](zx_status_t status) { sync_completion_signal(&completion); });
ASSERT_EQ(sync_completion_wait(&completion, zx::duration::infinite().get()), ZX_OK);
}
};
// See note at beginning regarding tuning these numbers.
constexpr uint64_t kGrowFvmCutoff{UINT64_C(32) * JournalGrowFvmTest::kDiskBlocksPerFsBlock};
TEST_F(JournalGrowFvmTest, GrowingWithJournalReplaySucceeds) {
zx::result<std::unique_ptr<Bcache>> bcache_or = zx::ok(CutOffDevice(write_count()));
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
auto fs_or = Runner::Create(dispatcher(), std::move(bcache_or.value()), MountOptions());
ASSERT_TRUE(fs_or.is_ok());
EXPECT_EQ(fs_or->minfs().Info().dat_slices, 2u); // We expect the increased size.
}
TEST_F(JournalGrowFvmTest, GrowingWithNoReplaySucceeds) {
// In this test, 1 fewer block means the replay will fail.
zx::result<std::unique_ptr<Bcache>> bcache_or =
zx::ok(CutOffDevice(write_count() - kGrowFvmCutoff - kDiskBlocksPerFsBlock));
bcache_or = Fsck(std::move(bcache_or.value()), FsckOptions{.repair = true});
EXPECT_TRUE(bcache_or.is_ok());
auto fs_or = Runner::Create(dispatcher(), std::move(bcache_or.value()), MountOptions());
ASSERT_TRUE(fs_or.is_ok());
EXPECT_EQ(fs_or->minfs().Info().dat_slices, 1u); // We expect the old, smaller size.
}
// It is not safe for data writes to go to freed blocks until the metadata that frees them has been
// committed because data writes do not wait. This test verifies this by pausing writes and then
// freeing blocks and making sure that block doesn't get reused. This test currently relies on the
// allocator behaving a certain way, i.e. it allocates the first free block that it can find.
TEST_F(JournalIntegrationTest, BlocksAreReservedUntilMetadataIsCommitted) {
static constexpr int kBlockCount = 1 << 15;
auto device = std::make_unique<block_client::FakeBlockDevice>(kBlockCount, 512);
block_client::FakeBlockDevice* device_ptr = device.get();
auto bcache_or = Bcache::Create(std::move(device), kBlockCount);
ASSERT_TRUE(bcache_or.is_ok());
ASSERT_TRUE(Mkfs(bcache_or.value().get()).is_ok());
MountOptions options = {};
auto fs_or = Runner::Create(dispatcher(), std::move(bcache_or.value()), options);
ASSERT_TRUE(fs_or.is_ok());
// Create a file and make it allocate 1 block.
auto root = fs_or->minfs().VnodeGet(kMinfsRootIno);
ASSERT_TRUE(root.is_ok());
zx::result foo = root->Create("foo", fs::CreationType::kFile);
ASSERT_TRUE(foo.is_ok()) << foo.status_string();
auto close = fit::defer([foo]() { ASSERT_EQ(foo->Close(), ZX_OK); });
std::vector<uint8_t> buf(10, 0xaf);
size_t written;
ASSERT_EQ(foo->Write(buf.data(), buf.size(), 0, &written), ZX_OK);
sync_completion_t completion;
foo->Sync([&completion](zx_status_t status) { sync_completion_signal(&completion); });
ASSERT_EQ(sync_completion_wait(&completion, zx::duration::infinite().get()), ZX_OK);
ASSERT_EQ(written, buf.size());
// Make a note of which block was allocated.
auto foo_file = fbl::RefPtr<File>::Downcast(*foo);
blk_t block = foo_file->GetInode()->dnum[0];
EXPECT_NE(block, 0u);
// Pause writes now.
device_ptr->Pause();
// Truncate the file which should cause the block to be released.
ASSERT_EQ(foo->Truncate(0), ZX_OK);
// Write to the file again and make sure it gets written to a different block.
ASSERT_EQ(foo->Write(buf.data(), buf.size(), 0, &written), ZX_OK);
ASSERT_EQ(written, buf.size());
// The block that was allocated should be different.
EXPECT_NE(block, foo_file->GetInode()->dnum[0]);
// Resume so that fs can be destroyed.
device_ptr->Resume();
}
} // namespace
} // namespace minfs