sapling/eden/fs/inodes/test/OverlayTest.cpp
Andres Suarez 21c95391ca Apply clang-format update fixes
Reviewed By: igorsugak

Differential Revision: D25861960

fbshipit-source-id: e3c39c080429058a58cdc66d45350e5d1420f98c
2021-01-10 10:06:29 -08:00

983 lines
30 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
#ifndef _WIN32
#include "eden/fs/inodes/Overlay.h"
#include "eden/fs/inodes/overlay/FsOverlay.h"
#include <folly/Exception.h>
#include <folly/Expected.h>
#include <folly/FileUtil.h>
#include <folly/Range.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/TestUtil.h>
#include <folly/logging/test/TestLogHandler.h>
#include <folly/synchronization/test/Barrier.h>
#include <folly/test/TestUtils.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include "eden/fs/inodes/EdenMount.h"
#include "eden/fs/inodes/FileInode.h"
#include "eden/fs/inodes/OverlayFile.h"
#include "eden/fs/inodes/TreeInode.h"
#include "eden/fs/service/PrettyPrinters.h"
#include "eden/fs/telemetry/NullStructuredLogger.h"
#include "eden/fs/testharness/FakeBackingStore.h"
#include "eden/fs/testharness/FakeTreeBuilder.h"
#include "eden/fs/testharness/TempFile.h"
#include "eden/fs/testharness/TestChecks.h"
#include "eden/fs/testharness/TestMount.h"
#include "eden/fs/testharness/TestUtil.h"
#include "eden/fs/utils/PathFuncs.h"
#include "eden/fs/utils/SpawnedProcess.h"
using namespace folly::string_piece_literals;
namespace facebook {
namespace eden {
namespace {
std::string debugDumpOverlayInodes(Overlay&, InodeNumber rootInode);
} // namespace
TEST(OverlayGoldMasterTest, can_load_overlay_v2) {
// eden/test-data/overlay-v2.tgz contains a saved copy of an overlay
// directory generated by edenfs. Unpack it into a temporary directory,
// then try loading it.
//
// This test helps ensure that new edenfs versions can still successfully load
// this overlay format even if we change how the overlay is saved in the
// future.
auto tmpdir = makeTempDir("eden_test");
SpawnedProcess tarProcess(
{"/usr/bin/tar",
"-xzf",
"eden/test-data/overlay-v2.tgz",
"-C",
tmpdir.path().string()});
EXPECT_EQ(tarProcess.wait().str(), "exited with status 0");
auto overlay = Overlay::create(
realpath(tmpdir.path().string()) + "overlay-v2"_pc,
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
Hash hash1{folly::ByteRange{"abcdabcdabcdabcdabcd"_sp}};
Hash hash2{folly::ByteRange{"01234012340123401234"_sp}};
Hash hash3{folly::ByteRange{"e0e0e0e0e0e0e0e0e0e0"_sp}};
Hash hash4{folly::ByteRange{"44444444444444444444"_sp}};
auto rootTree = overlay->loadOverlayDir(kRootNodeId);
auto file = overlay->openFile(2_ino, FsOverlay::kHeaderIdentifierFile);
auto subdir = overlay->loadOverlayDir(3_ino);
auto emptyDir = overlay->loadOverlayDir(4_ino);
auto hello = overlay->openFile(5_ino, FsOverlay::kHeaderIdentifierFile);
ASSERT_TRUE(rootTree);
EXPECT_EQ(2, rootTree->size());
const auto& fileEntry = rootTree->at("file"_pc);
EXPECT_EQ(2_ino, fileEntry.getInodeNumber());
EXPECT_EQ(hash1, fileEntry.getHash());
EXPECT_EQ(S_IFREG | 0644, fileEntry.getInitialMode());
const auto& subdirEntry = rootTree->at("subdir"_pc);
EXPECT_EQ(3_ino, subdirEntry.getInodeNumber());
EXPECT_EQ(hash2, subdirEntry.getHash());
EXPECT_EQ(S_IFDIR | 0755, subdirEntry.getInitialMode());
EXPECT_TRUE(file.lseek(FsOverlay::kHeaderLength, SEEK_SET).hasValue());
auto result = file.readFile();
EXPECT_FALSE(result.hasError());
EXPECT_EQ("contents", result.value());
ASSERT_TRUE(subdir);
EXPECT_EQ(2, subdir->size());
const auto& emptyEntry = subdir->at("empty"_pc);
EXPECT_EQ(4_ino, emptyEntry.getInodeNumber());
EXPECT_EQ(hash3, emptyEntry.getHash());
EXPECT_EQ(S_IFDIR | 0755, emptyEntry.getInitialMode());
const auto& helloEntry = subdir->at("hello"_pc);
EXPECT_EQ(5_ino, helloEntry.getInodeNumber());
EXPECT_EQ(hash4, helloEntry.getHash());
EXPECT_EQ(S_IFREG | 0644, helloEntry.getInitialMode());
ASSERT_TRUE(emptyDir);
EXPECT_EQ(0, emptyDir->size());
EXPECT_TRUE(hello.lseek(FsOverlay::kHeaderLength, SEEK_SET).hasValue());
result = file.readFile();
EXPECT_FALSE(result.hasError());
EXPECT_EQ("", result.value());
}
class OverlayTest : public ::testing::Test {
protected:
void SetUp() override {
// Set up a directory structure that we will use for most
// of the tests below
FakeTreeBuilder builder;
builder.setFiles({
{"dir/a.txt", "This is a.txt.\n"},
});
mount_.initialize(builder);
}
// Helper method to check if two timestamps are same or not.
static void expectTimeSpecsEqual(
const EdenTimestamp& at,
const EdenTimestamp& bt) {
auto a = at.toTimespec();
auto b = bt.toTimespec();
EXPECT_EQ(a.tv_sec, b.tv_sec);
EXPECT_EQ(a.tv_nsec, b.tv_nsec);
}
static void expectTimeStampsEqual(
const InodeTimestamps& a,
const InodeTimestamps& b) {
expectTimeSpecsEqual(a.atime, b.atime);
expectTimeSpecsEqual(a.mtime, b.mtime);
expectTimeSpecsEqual(a.ctime, b.ctime);
}
TestMount mount_;
};
TEST_F(OverlayTest, testRemount) {
mount_.addFile("dir/new.txt", "test\n");
mount_.remount();
// Confirm that the tree has been updated correctly.
auto newInode = mount_.getFileInode("dir/new.txt");
EXPECT_FILE_INODE(newInode, "test\n", 0644);
}
TEST_F(OverlayTest, testModifyRemount) {
// inode object has to be destroyed
// before remount is called to release the reference
{
auto inode = mount_.getFileInode("dir/a.txt");
EXPECT_FILE_INODE(inode, "This is a.txt.\n", 0644);
}
// materialize a directory
mount_.overwriteFile("dir/a.txt", "contents changed\n");
mount_.remount();
auto newInode = mount_.getFileInode("dir/a.txt");
EXPECT_FILE_INODE(newInode, "contents changed\n", 0644);
}
// In memory timestamps should be same before and after a remount.
// (inmemory timestamps should be written to overlay on
// on unmount and should be read back from the overlay on remount)
TEST_F(OverlayTest, testTimeStampsInOverlayOnMountAndUnmount) {
// Materialize file and directory
// test timestamp behavior in overlay on remount.
InodeTimestamps beforeRemountFile;
InodeTimestamps beforeRemountDir;
mount_.overwriteFile("dir/a.txt", "contents changed\n");
{
// We do not want to keep references to inode in order to remount.
auto inodeFile = mount_.getFileInode("dir/a.txt");
EXPECT_FILE_INODE(inodeFile, "contents changed\n", 0644);
beforeRemountFile = inodeFile->getMetadata().timestamps;
}
{
// Check for materialized files.
mount_.remount();
auto inodeRemount = mount_.getFileInode("dir/a.txt");
auto afterRemount = inodeRemount->getMetadata().timestamps;
expectTimeStampsEqual(beforeRemountFile, afterRemount);
}
{
auto inodeDir = mount_.getTreeInode("dir");
beforeRemountDir = inodeDir->getMetadata().timestamps;
}
{
// Check for materialized directory
mount_.remount();
auto inodeRemount = mount_.getTreeInode("dir");
auto afterRemount = inodeRemount->getMetadata().timestamps;
expectTimeStampsEqual(beforeRemountDir, afterRemount);
}
}
TEST_F(OverlayTest, roundTripThroughSaveAndLoad) {
auto hash = Hash{"0123456789012345678901234567890123456789"};
auto overlay = mount_.getEdenMount()->getOverlay();
auto ino1 = overlay->allocateInodeNumber();
auto ino2 = overlay->allocateInodeNumber();
auto ino3 = overlay->allocateInodeNumber();
DirContents dir(kPathMapDefaultCaseSensitive);
dir.emplace("one"_pc, S_IFREG | 0644, ino2, hash);
dir.emplace("two"_pc, S_IFDIR | 0755, ino3);
overlay->saveOverlayDir(ino1, dir);
auto result = overlay->loadOverlayDir(ino1);
ASSERT_TRUE(result);
const auto* newDir = &*result;
EXPECT_EQ(2, newDir->size());
const auto& one = newDir->find("one"_pc)->second;
const auto& two = newDir->find("two"_pc)->second;
EXPECT_EQ(ino2, one.getInodeNumber());
EXPECT_FALSE(one.isMaterialized());
EXPECT_EQ(ino3, two.getInodeNumber());
EXPECT_TRUE(two.isMaterialized());
}
TEST_F(OverlayTest, getFilePath) {
InodePath path;
path = FsOverlay::getFilePath(1_ino);
EXPECT_EQ("01/1"_relpath, path);
path = FsOverlay::getFilePath(1234_ino);
EXPECT_EQ("d2/1234"_relpath, path);
// It's slightly unfortunate that we use hexadecimal for the subdirectory
// name and decimal for the final inode path. That doesn't seem worth fixing
// for now.
path = FsOverlay::getFilePath(15_ino);
EXPECT_EQ("0f/15"_relpath, path);
path = FsOverlay::getFilePath(16_ino);
EXPECT_EQ("10/16"_relpath, path);
}
TEST(PlainOverlayTest, new_overlay_is_clean) {
folly::test::TemporaryDirectory testDir;
auto overlay = Overlay::create(
AbsolutePath{testDir.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
EXPECT_TRUE(overlay->hadCleanStartup());
}
TEST(PlainOverlayTest, reopened_overlay_is_clean) {
folly::test::TemporaryDirectory testDir;
{
auto overlay = Overlay::create(
AbsolutePath{testDir.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
}
auto overlay = Overlay::create(
AbsolutePath{testDir.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
EXPECT_TRUE(overlay->hadCleanStartup());
}
TEST(PlainOverlayTest, unclean_overlay_is_dirty) {
folly::test::TemporaryDirectory testDir;
auto localDir = AbsolutePath{testDir.path().string()};
{
auto overlay = Overlay::create(
AbsolutePath{testDir.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
}
if (unlink((localDir + "next-inode-number"_pc).c_str())) {
folly::throwSystemError("removing saved inode numebr");
}
auto overlay = Overlay::create(
AbsolutePath{testDir.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
EXPECT_FALSE(overlay->hadCleanStartup());
}
enum class OverlayRestartMode {
CLEAN,
UNCLEAN,
};
class RawOverlayTest : public ::testing::TestWithParam<OverlayRestartMode> {
public:
RawOverlayTest() : testDir_{makeTempDir("eden_raw_overlay_test_")} {
loadOverlay();
}
void recreate(std::optional<OverlayRestartMode> restartMode = std::nullopt) {
unloadOverlay(restartMode);
loadOverlay();
}
void unloadOverlay(
std::optional<OverlayRestartMode> restartMode = std::nullopt) {
overlay->close();
overlay = nullptr;
switch (restartMode.value_or(GetParam())) {
case OverlayRestartMode::CLEAN:
break;
case OverlayRestartMode::UNCLEAN:
if (unlink((getLocalDir() + "next-inode-number"_pc).c_str())) {
folly::throwSystemError("removing saved inode numebr");
}
break;
}
}
void loadOverlay() {
overlay = Overlay::create(
getLocalDir(),
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>());
overlay->initialize().get();
}
void corruptOverlayFile(InodeNumber inodeNumber) {
corruptOverlayFileByTruncating(inodeNumber);
}
void corruptOverlayFileByTruncating(InodeNumber inodeNumber) {
EXPECT_FALSE(overlay) << "Overlay should not be open when corrupting";
folly::checkUnixError(
folly::truncateNoInt(getOverlayFilePath(inodeNumber).c_str(), 0));
}
void corruptOverlayFileByDeleting(InodeNumber inodeNumber) {
EXPECT_FALSE(overlay) << "Overlay should not be open when corrupting";
folly::checkUnixError(unlink(getOverlayFilePath(inodeNumber).c_str()));
}
AbsolutePath getOverlayFilePath(InodeNumber inodeNumber) {
return getLocalDir() +
RelativePathPiece{FsOverlay::getFilePath(inodeNumber)};
}
AbsolutePath getLocalDir() {
return AbsolutePath{testDir_.path().string()};
}
folly::test::TemporaryDirectory testDir_;
std::shared_ptr<Overlay> overlay;
};
TEST_P(RawOverlayTest, closed_overlay_stress_test) {
constexpr unsigned kThreadCount = 10;
auto executor = folly::CPUThreadPoolExecutor(kThreadCount + 1);
std::vector<folly::Future<folly::Unit>> futures;
futures.reserve(kThreadCount);
folly::test::Barrier gate{kThreadCount + 1};
for (unsigned i = 0; i < kThreadCount; ++i) {
futures.emplace_back(folly::via(&executor, [&] {
auto ino = overlay->allocateInodeNumber();
OverlayFile result;
try {
result =
overlay->createOverlayFile(ino, folly::ByteRange{"contents"_sp});
} catch (std::system_error& e) {
if ("cannot access overlay after it is closed: Input/output error"_sp !=
e.what()) {
printf("createOverlayFile failed: %s\n", e.what());
throw e;
}
// The Overlay is already closed, so just return successfully.
gate.wait();
return;
}
// Block until after overlay has closed
gate.wait();
ASSERT_TRUE(overlay->isClosed());
try {
char data[] = "new contents";
struct iovec iov;
iov.iov_base = data;
iov.iov_len = sizeof(data);
result.pwritev(&iov, 1, FsOverlay::kHeaderLength).value();
throw std::system_error(
EIO,
std::generic_category(),
"should not be able to successfully write to overlay");
} catch (std::system_error& e) {
if (strcmp(
e.what(),
"cannot access overlay after it is closed: Input/output error")) {
printf("pwritev failed: %s\n", e.what());
throw e;
}
}
}));
}
overlay->close();
// Wake the waiting threads
gate.wait();
auto finished = folly::collectAllUnsafe(futures).get();
for (auto& f : finished) {
EXPECT_FALSE(f.hasException()) << f.exception().what();
}
}
TEST_P(RawOverlayTest, cannot_create_overlay_file_in_corrupt_overlay) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
// Remove the overlay directory in order to make file creation fail.
auto path = testDir_.path();
boost::filesystem::remove_all(path);
EXPECT_THROW(
overlay->createOverlayFile(ino2, folly::ByteRange{"contents"_sp}),
std::system_error);
// Restore the overlay directory and make sure we can create an overlay file
// and close the overlay.
boost::filesystem::create_directory(path);
loadOverlay();
ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
EXPECT_NO_THROW(
overlay->createOverlayFile(ino2, folly::ByteRange{"contents"_sp}));
overlay->close();
}
TEST_P(RawOverlayTest, cannot_save_overlay_dir_when_closed) {
overlay->close();
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
DirContents dir(kPathMapDefaultCaseSensitive);
EXPECT_THROW_RE(
overlay->saveOverlayDir(ino2, dir),
std::system_error,
"cannot access overlay after it is closed");
}
TEST_P(RawOverlayTest, cannot_create_overlay_file_when_closed) {
overlay->close();
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
EXPECT_THROW_RE(
overlay->createOverlayFile(ino2, folly::ByteRange{"contents"_sp}),
std::system_error,
"cannot access overlay after it is closed");
}
TEST_P(RawOverlayTest, cannot_remove_overlay_file_when_closed) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
EXPECT_NO_THROW(
overlay->createOverlayFile(ino2, folly::ByteRange{"contents"_sp}));
overlay->close();
EXPECT_THROW_RE(
overlay->removeOverlayData(ino2),
std::system_error,
"cannot access overlay after it is closed");
}
TEST_P(RawOverlayTest, max_inode_number_is_1_if_overlay_is_empty) {
EXPECT_EQ(kRootNodeId, overlay->getMaxInodeNumber());
EXPECT_EQ(2_ino, overlay->allocateInodeNumber());
recreate(OverlayRestartMode::CLEAN);
EXPECT_EQ(2_ino, overlay->getMaxInodeNumber());
EXPECT_EQ(3_ino, overlay->allocateInodeNumber());
recreate(OverlayRestartMode::UNCLEAN);
EXPECT_EQ(kRootNodeId, overlay->getMaxInodeNumber());
EXPECT_EQ(2_ino, overlay->allocateInodeNumber());
}
TEST_P(RawOverlayTest, remembers_max_inode_number_of_tree_inodes) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
DirContents dir(kPathMapDefaultCaseSensitive);
overlay->saveOverlayDir(ino2, dir);
recreate();
EXPECT_EQ(2_ino, overlay->getMaxInodeNumber());
}
TEST_P(RawOverlayTest, remembers_max_inode_number_of_tree_entries) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
auto ino3 = overlay->allocateInodeNumber();
auto ino4 = overlay->allocateInodeNumber();
DirContents dir(kPathMapDefaultCaseSensitive);
dir.emplace(PathComponentPiece{"f"}, S_IFREG | 0644, ino3);
dir.emplace(PathComponentPiece{"d"}, S_IFDIR | 0755, ino4);
overlay->saveOverlayDir(kRootNodeId, dir);
recreate();
SCOPED_TRACE("Inodes:\n" + debugDumpOverlayInodes(*overlay, kRootNodeId));
EXPECT_EQ(4_ino, overlay->getMaxInodeNumber());
}
TEST_P(RawOverlayTest, remembers_max_inode_number_of_file) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
auto ino3 = overlay->allocateInodeNumber();
// When materializing, overlay data is written leaf-to-root.
// The File is written first.
overlay->createOverlayFile(ino3, folly::ByteRange{"contents"_sp});
recreate();
EXPECT_EQ(3_ino, overlay->getMaxInodeNumber());
}
TEST_P(
RawOverlayTest,
inode_number_scan_includes_linked_directory_despite_its_corruption) {
auto subdirectoryIno = overlay->allocateInodeNumber();
auto rootIno = kRootNodeId;
ASSERT_GT(subdirectoryIno, rootIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace("subdirectory"_pc, S_IFDIR | 0755, subdirectoryIno);
overlay->saveOverlayDir(rootIno, root);
overlay->saveOverlayDir(
subdirectoryIno, DirContents(kPathMapDefaultCaseSensitive));
unloadOverlay();
corruptOverlayFile(subdirectoryIno);
loadOverlay();
EXPECT_EQ(subdirectoryIno, overlay->getMaxInodeNumber());
}
TEST_P(
RawOverlayTest,
inode_number_scan_continues_scanning_despite_corrupted_directory) {
// Check that the next inode number is recomputed correctly even in the
// presence of corrupted directory data in the overlay.
//
// The old scan algorithm we used to used would traverse down the directory
// tree, so we needed to ensure that it still found orphan parts of the tree.
// The newer OverlayChecker code uses a completely different algorithm which
// isn't susceptible to this same problem, but it still seems worth testing
// this behavior.
//
// We test with the following overlay structure:
//
// / (rootIno)
// corrupted_by_truncation/ (corruptedByTruncationIno)
// temp/ (tempDirIno)
// temp/corrupted_by_deletion (corruptedByDeletionIno)
//
struct PathNames {
PathComponentPiece corruptedByTruncationName;
PathComponentPiece tempName;
};
auto rootIno = kRootNodeId;
auto corruptedByTruncationIno = InodeNumber{};
auto tempDirIno = InodeNumber{};
auto corruptedByDeletionIno = InodeNumber{};
auto setUpOverlay = [&](const PathNames& pathNames) {
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace(
pathNames.corruptedByTruncationName,
S_IFDIR | 0755,
corruptedByTruncationIno);
root.emplace(pathNames.tempName, S_IFDIR | 0755, tempDirIno);
overlay->saveOverlayDir(rootIno, root);
overlay->saveOverlayDir(
corruptedByTruncationIno, DirContents(kPathMapDefaultCaseSensitive));
DirContents tempDir(kPathMapDefaultCaseSensitive);
tempDir.emplace(
"corrupted_by_deletion"_pc, S_IFDIR | 0755, corruptedByDeletionIno);
overlay->saveOverlayDir(tempDirIno, tempDir);
overlay->saveOverlayDir(
corruptedByDeletionIno, DirContents(kPathMapDefaultCaseSensitive));
};
const PathNames pathNamesToTest[] = {
// Test a few different path name variations, to ensure traversal order
// doesn't matter.
PathNames{
.corruptedByTruncationName = "A_corrupted_by_truncation"_pc,
.tempName = "B_temp"_pc},
PathNames{
.corruptedByTruncationName = "B_corrupted_by_truncation"_pc,
.tempName = "A_temp"_pc},
};
for (auto pathNames : pathNamesToTest) {
corruptedByTruncationIno = overlay->allocateInodeNumber();
tempDirIno = overlay->allocateInodeNumber();
corruptedByDeletionIno = overlay->allocateInodeNumber();
auto maxIno = std::max(
{tempDirIno, corruptedByTruncationIno, corruptedByDeletionIno});
ASSERT_EQ(corruptedByDeletionIno, maxIno);
setUpOverlay(pathNames);
SCOPED_TRACE(
"Inodes before corruption:\n" +
debugDumpOverlayInodes(*overlay, rootIno));
unloadOverlay();
corruptOverlayFileByTruncating(corruptedByTruncationIno);
corruptOverlayFileByDeleting(corruptedByDeletionIno);
loadOverlay();
EXPECT_EQ(maxIno, overlay->getMaxInodeNumber());
}
}
TEST_P(RawOverlayTest, inode_numbers_not_reused_after_unclean_shutdown) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
overlay->allocateInodeNumber();
auto ino4 = overlay->allocateInodeNumber();
auto ino5 = overlay->allocateInodeNumber();
// When materializing, overlay data is written leaf-to-root.
// The File is written first.
overlay->createOverlayFile(ino5, folly::ByteRange{"contents"_sp});
// The subdir is written next.
DirContents subdir(kPathMapDefaultCaseSensitive);
subdir.emplace(PathComponentPiece{"f"}, S_IFREG | 0644, ino5);
overlay->saveOverlayDir(ino4, subdir);
// Crashed before root was written.
recreate();
SCOPED_TRACE(
"Inodes from subdir:\n" + debugDumpOverlayInodes(*overlay, ino4));
EXPECT_EQ(5_ino, overlay->getMaxInodeNumber());
}
TEST_P(RawOverlayTest, inode_numbers_after_takeover) {
auto ino2 = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, ino2);
auto ino3 = overlay->allocateInodeNumber();
auto ino4 = overlay->allocateInodeNumber();
auto ino5 = overlay->allocateInodeNumber();
// Write a subdir.
DirContents subdir(kPathMapDefaultCaseSensitive);
subdir.emplace(PathComponentPiece{"f"}, S_IFREG | 0644, ino5);
overlay->saveOverlayDir(ino4, subdir);
// Write the root.
DirContents dir(kPathMapDefaultCaseSensitive);
dir.emplace(PathComponentPiece{"f"}, S_IFREG | 0644, ino3);
dir.emplace(PathComponentPiece{"d"}, S_IFDIR | 0755, ino4);
overlay->saveOverlayDir(kRootNodeId, dir);
recreate();
// Rewrite the root (say, after a takeover) without the file.
DirContents newroot(kPathMapDefaultCaseSensitive);
newroot.emplace(PathComponentPiece{"d"}, S_IFDIR | 0755, 4_ino);
overlay->saveOverlayDir(kRootNodeId, newroot);
recreate(OverlayRestartMode::CLEAN);
SCOPED_TRACE("Inodes:\n" + debugDumpOverlayInodes(*overlay, kRootNodeId));
// Ensure an inode in the overlay but not referenced by the previous session
// counts.
EXPECT_EQ(5_ino, overlay->getMaxInodeNumber());
}
INSTANTIATE_TEST_CASE_P(
Clean,
RawOverlayTest,
::testing::Values(OverlayRestartMode::CLEAN));
INSTANTIATE_TEST_CASE_P(
Unclean,
RawOverlayTest,
::testing::Values(OverlayRestartMode::UNCLEAN));
TEST(OverlayInodePath, defaultInodePathIsEmpty) {
InodePath path;
EXPECT_STREQ(path.c_str(), "");
}
class DebugDumpOverlayInodesTest : public ::testing::Test {
public:
DebugDumpOverlayInodesTest()
: testDir_{makeTempDir("eden_DebugDumpOverlayInodesTest")},
overlay{Overlay::create(
AbsolutePathPiece{testDir_.path().string()},
kPathMapDefaultCaseSensitive,
std::make_shared<NullStructuredLogger>())} {
overlay->initialize().get();
}
folly::test::TemporaryDirectory testDir_;
std::shared_ptr<Overlay> overlay;
};
TEST_F(DebugDumpOverlayInodesTest, dump_empty_directory) {
auto ino = kRootNodeId;
EXPECT_EQ(1_ino, ino);
overlay->saveOverlayDir(ino, DirContents(kPathMapDefaultCaseSensitive));
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (0 total):\n",
debugDumpOverlayInodes(*overlay, ino));
}
TEST_F(DebugDumpOverlayInodesTest, dump_directory_with_3_regular_files) {
auto rootIno = kRootNodeId;
EXPECT_EQ(1_ino, rootIno);
auto fileAIno = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, fileAIno);
auto fileBIno = overlay->allocateInodeNumber();
EXPECT_EQ(3_ino, fileBIno);
auto fileCIno = overlay->allocateInodeNumber();
EXPECT_EQ(4_ino, fileCIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace("file_a"_pc, S_IFREG | 0644, fileAIno);
root.emplace("file_b"_pc, S_IFREG | 0644, fileBIno);
root.emplace("file_c"_pc, S_IFREG | 0644, fileCIno);
overlay->saveOverlayDir(rootIno, root);
overlay->createOverlayFile(fileAIno, folly::ByteRange{""_sp});
overlay->createOverlayFile(fileBIno, folly::ByteRange{""_sp});
overlay->createOverlayFile(fileCIno, folly::ByteRange{""_sp});
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (3 total):\n"
" 2 f 644 file_a\n"
" 3 f 644 file_b\n"
" 4 f 644 file_c\n",
debugDumpOverlayInodes(*overlay, rootIno));
}
TEST_F(DebugDumpOverlayInodesTest, dump_directory_with_an_empty_subdirectory) {
auto rootIno = kRootNodeId;
EXPECT_EQ(1_ino, rootIno);
auto subdirIno = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, subdirIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace("subdir"_pc, S_IFDIR | 0755, subdirIno);
overlay->saveOverlayDir(rootIno, root);
overlay->saveOverlayDir(subdirIno, DirContents(kPathMapDefaultCaseSensitive));
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (1 total):\n"
" 2 d 755 subdir\n"
"/subdir\n"
" Inode number: 2\n"
" Entries (0 total):\n",
debugDumpOverlayInodes(*overlay, rootIno));
}
TEST_F(DebugDumpOverlayInodesTest, dump_directory_with_unsaved_subdirectory) {
auto rootIno = kRootNodeId;
EXPECT_EQ(1_ino, rootIno);
auto directoryDoesNotExistIno = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, directoryDoesNotExistIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace(
"directory_does_not_exist"_pc, S_IFDIR | 0755, directoryDoesNotExistIno);
overlay->saveOverlayDir(rootIno, root);
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (1 total):\n"
" 2 d 755 directory_does_not_exist\n"
"/directory_does_not_exist\n"
" Inode number: 2\n",
debugDumpOverlayInodes(*overlay, rootIno));
}
TEST_F(DebugDumpOverlayInodesTest, dump_directory_with_unsaved_regular_file) {
auto rootIno = kRootNodeId;
EXPECT_EQ(1_ino, rootIno);
auto regularFileDoesNotExistIno = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, regularFileDoesNotExistIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace(
"regular_file_does_not_exist"_pc,
S_IFREG | 0644,
regularFileDoesNotExistIno);
overlay->saveOverlayDir(rootIno, root);
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (1 total):\n"
" 2 f 644 regular_file_does_not_exist\n",
debugDumpOverlayInodes(*overlay, rootIno));
}
TEST_F(DebugDumpOverlayInodesTest, directories_are_dumped_depth_first) {
auto rootIno = kRootNodeId;
EXPECT_EQ(1_ino, rootIno);
auto subdirAIno = overlay->allocateInodeNumber();
EXPECT_EQ(2_ino, subdirAIno);
auto subdirAXIno = overlay->allocateInodeNumber();
EXPECT_EQ(3_ino, subdirAXIno);
auto subdirAYIno = overlay->allocateInodeNumber();
EXPECT_EQ(4_ino, subdirAYIno);
auto subdirBIno = overlay->allocateInodeNumber();
EXPECT_EQ(5_ino, subdirBIno);
auto subdirBXIno = overlay->allocateInodeNumber();
EXPECT_EQ(6_ino, subdirBXIno);
DirContents root(kPathMapDefaultCaseSensitive);
root.emplace("subdir_a"_pc, S_IFDIR | 0755, subdirAIno);
root.emplace("subdir_b"_pc, S_IFDIR | 0755, subdirBIno);
overlay->saveOverlayDir(rootIno, root);
DirContents subdirA(kPathMapDefaultCaseSensitive);
subdirA.emplace("x"_pc, S_IFDIR | 0755, subdirAXIno);
subdirA.emplace("y"_pc, S_IFDIR | 0755, subdirAYIno);
overlay->saveOverlayDir(subdirAIno, subdirA);
DirContents subdirB(kPathMapDefaultCaseSensitive);
subdirB.emplace("x"_pc, S_IFDIR | 0755, subdirBXIno);
overlay->saveOverlayDir(subdirBIno, subdirB);
overlay->saveOverlayDir(
subdirAXIno, DirContents(kPathMapDefaultCaseSensitive));
overlay->saveOverlayDir(
subdirAYIno, DirContents(kPathMapDefaultCaseSensitive));
overlay->saveOverlayDir(
subdirBXIno, DirContents(kPathMapDefaultCaseSensitive));
EXPECT_EQ(
"/\n"
" Inode number: 1\n"
" Entries (2 total):\n"
" 2 d 755 subdir_a\n"
" 5 d 755 subdir_b\n"
"/subdir_a\n"
" Inode number: 2\n"
" Entries (2 total):\n"
" 3 d 755 x\n"
" 4 d 755 y\n"
"/subdir_a/x\n"
" Inode number: 3\n"
" Entries (0 total):\n"
"/subdir_a/y\n"
" Inode number: 4\n"
" Entries (0 total):\n"
"/subdir_b\n"
" Inode number: 5\n"
" Entries (1 total):\n"
" 6 d 755 x\n"
"/subdir_b/x\n"
" Inode number: 6\n"
" Entries (0 total):\n",
debugDumpOverlayInodes(*overlay, rootIno));
}
namespace {
void debugDumpOverlayInodes(
Overlay& overlay,
InodeNumber rootInode,
AbsolutePathPiece path,
std::ostringstream& out) {
out << path << "\n";
out << " Inode number: " << rootInode << "\n";
auto dir = overlay.loadOverlayDir(rootInode);
if (dir) {
auto& dirContents = *dir;
out << " Entries (" << dirContents.size() << " total):\n";
auto dtypeToString = [](dtype_t dtype) noexcept -> const char* {
switch (dtype) {
case dtype_t::Dir:
return "d";
case dtype_t::Regular:
return "f";
default:
return "?";
}
};
for (const auto& [entryPath, entry] : dirContents) {
auto permissions = entry.getInitialMode() & ~S_IFMT;
out << " " << std::dec << std::setw(11) << entry.getInodeNumber() << " "
<< dtypeToString(entry.getDtype()) << " " << std::oct << std::setw(4)
<< permissions << " " << entryPath << "\n";
}
for (const auto& [entryPath, entry] : dirContents) {
if (entry.getDtype() == dtype_t::Dir) {
debugDumpOverlayInodes(
overlay, entry.getInodeNumber(), path + entryPath, out);
}
}
}
}
std::string debugDumpOverlayInodes(Overlay& overlay, InodeNumber rootInode) {
std::ostringstream out;
debugDumpOverlayInodes(overlay, rootInode, AbsolutePathPiece{}, out);
return out.str();
}
} // namespace
} // namespace eden
} // namespace facebook
#endif