sapling/eden/fs/inodes/test/InodeMapTest.cpp
Xavier Deguillard 4707bc22ec inodes: thread an ObjectContext into unlink/rmdir
Summary:
All the callers of these methods have a real context in hand, let's use it
instead of creating a null one.

Reviewed By: genevievehelsel

Differential Revision: D26699271

fbshipit-source-id: 9fb268c9b3194d7e951e30ab5e90c4f2f0388e81
2021-03-04 17:51:50 -08:00

660 lines
21 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.
*/
#include "eden/fs/inodes/InodeMap.h"
#include <folly/Format.h>
#include <folly/String.h>
#include <folly/test/TestUtils.h>
#include <gtest/gtest.h>
#include "eden/fs/inodes/EdenMount.h"
#include "eden/fs/inodes/FileInode.h"
#include "eden/fs/inodes/Overlay.h"
#include "eden/fs/inodes/TreeInode.h"
#include "eden/fs/store/ObjectFetchContext.h"
#include "eden/fs/testharness/FakeTreeBuilder.h"
#include "eden/fs/testharness/TestMount.h"
#include "eden/fs/testharness/TestUtil.h"
#include "eden/fs/utils/Bug.h"
using namespace std::chrono_literals;
using namespace facebook::eden;
#ifdef _WIN32
// TODO(puneetk): Defining these flags here to fix the linker issue. These
// symbols should come from ThriftProtocol.lib. But, for some reason it's not
// getting imported from the lib on Windows, even though it's linking against
// the lib.
DEFINE_int32(
thrift_cpp2_protocol_reader_string_limit,
0,
"Limit on string size when deserializing thrift, 0 is no limit");
DEFINE_int32(
thrift_cpp2_protocol_reader_container_limit,
0,
"Limit on container size when deserializing thrift, 0 is no limit");
#endif
TEST(InodeMap, invalidInodeNumber) {
FakeTreeBuilder builder;
builder.setFile("Makefile", "all:\necho success\n");
builder.setFile("src/noop.c", "int main() { return 0; }\n");
TestMount testMount{builder};
EdenBugDisabler noCrash;
auto* inodeMap = testMount.getEdenMount()->getInodeMap();
auto future = inodeMap->lookupFileInode(0x12345678_ino);
EXPECT_THROW_RE(
std::move(future).get(), std::runtime_error, "unknown inode number");
}
TEST(InodeMap, simpleLookups) {
// Test simple lookups that succeed immediately from the LocalStore
FakeTreeBuilder builder;
builder.setFile("Makefile", "all:\necho success\n");
builder.setFile("src/noop.c", "int main() { return 0; }\n");
TestMount testMount{builder};
auto* inodeMap = testMount.getEdenMount()->getInodeMap();
// Look up the tree inode by name first
auto root = testMount.getEdenMount()->getRootInode();
auto srcTree =
root->getOrLoadChild("src"_pc, ObjectFetchContext::getNullContext())
.get();
// Next look up the tree by inode number
auto tree2 = inodeMap->lookupTreeInode(srcTree->getNodeId()).get();
EXPECT_EQ(srcTree, tree2);
EXPECT_EQ(RelativePath{"src"}, tree2->getPath());
// Next look up src/noop.c by name
auto noop =
tree2->getOrLoadChild("noop.c"_pc, ObjectFetchContext::getNullContext())
.get();
EXPECT_NE(srcTree->getNodeId(), noop->getNodeId());
// And look up src/noop.c by inode ID
auto noop2 = inodeMap->lookupFileInode(noop->getNodeId()).get();
EXPECT_EQ(noop, noop2);
EXPECT_EQ(RelativePath{"src/noop.c"}, noop2->getPath());
// lookupTreeInode() and lookupFileInode() should fail
// when called on the wrong file type.
EXPECT_THROW_ERRNO(
inodeMap->lookupFileInode(srcTree->getNodeId()).get(), EISDIR);
EXPECT_THROW_ERRNO(
inodeMap->lookupTreeInode(noop->getNodeId()).get(), ENOTDIR);
}
TEST(InodeMap, asyncLookup) {
auto builder = FakeTreeBuilder();
builder.setFile("README", "docs go here\n");
builder.setFile("src/runme.sh", "#!/bin/sh\necho hello world\n", true);
builder.setFile("src/test.txt", "this is a test file");
TestMount testMount{builder, false};
// Look up the "src" tree inode by name
// The future should only be fulfilled when after we make the tree ready
auto rootInode = testMount.getEdenMount()->getRootInode();
auto srcFuture =
rootInode->getOrLoadChild("src"_pc, ObjectFetchContext::getNullContext());
EXPECT_FALSE(srcFuture.isReady());
// Start a second lookup before the first is ready
auto srcFuture2 =
rootInode->getOrLoadChild("src"_pc, ObjectFetchContext::getNullContext());
EXPECT_FALSE(srcFuture2.isReady());
// Now make the tree ready
builder.setReady("src");
ASSERT_TRUE(srcFuture.isReady());
ASSERT_TRUE(srcFuture2.isReady());
auto srcTree = std::move(srcFuture).get(std::chrono::seconds(1));
auto srcTree2 = std::move(srcFuture2).get(std::chrono::seconds(1));
EXPECT_EQ(srcTree.get(), srcTree2.get());
}
TEST(InodeMap, asyncError) {
auto builder = FakeTreeBuilder();
builder.setFile("README", "docs go here\n");
builder.setFile("src/runme.sh", "#!/bin/sh\necho hello world\n", true);
builder.setFile("src/test.txt", "this is a test file");
TestMount testMount{builder, false};
// Look up the "src" tree inode by name
// The future should only be fulfilled when after we make the tree ready
auto rootInode = testMount.getEdenMount()->getRootInode();
auto srcFuture =
rootInode->getOrLoadChild("src"_pc, ObjectFetchContext::getNullContext());
EXPECT_FALSE(srcFuture.isReady());
// Start a second lookup before the first is ready
auto srcFuture2 =
rootInode->getOrLoadChild("src"_pc, ObjectFetchContext::getNullContext());
EXPECT_FALSE(srcFuture2.isReady());
// Now fail the tree lookup
builder.triggerError(
"src", std::domain_error("rejecting lookup for src tree"));
ASSERT_TRUE(srcFuture.isReady());
ASSERT_TRUE(srcFuture2.isReady());
EXPECT_THROW(std::move(srcFuture).get(), std::domain_error);
EXPECT_THROW(std::move(srcFuture2).get(), std::domain_error);
}
TEST(InodeMap, recursiveLookup) {
auto builder = FakeTreeBuilder();
builder.setFile("a/b/c/d/file.txt", "this is a test file");
TestMount testMount{builder, false};
const auto& edenMount = testMount.getEdenMount();
// Call EdenMount::getInode() on the root
auto rootFuture =
edenMount->getInode(""_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(rootFuture.isReady());
auto rootResult = std::move(rootFuture).get();
EXPECT_EQ(edenMount->getRootInode(), rootResult);
// Call EdenMount::getInode() to do a recursive lookup
auto fileFuture = edenMount->getInode(
"a/b/c/d/file.txt"_relpath, ObjectFetchContext::getNullContext());
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c/d/file.txt");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c/d");
ASSERT_TRUE(fileFuture.isReady());
auto fileInode = std::move(fileFuture).get();
EXPECT_EQ("a/b/c/d/file.txt"_relpath, fileInode->getPath().value());
}
TEST(InodeMap, recursiveLookupError) {
auto builder = FakeTreeBuilder();
builder.setFile("a/b/c/d/file.txt", "this is a test file");
TestMount testMount{builder, false};
const auto& edenMount = testMount.getEdenMount();
// Call EdenMount::getInode() on the root
auto rootFuture =
edenMount->getInode(""_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(rootFuture.isReady());
auto rootResult = std::move(rootFuture).get();
EXPECT_EQ(edenMount->getRootInode(), rootResult);
// Call EdenMount::getInode() to do a recursive lookup
auto fileFuture = edenMount->getInode(
"a/b/c/d/file.txt"_relpath, ObjectFetchContext::getNullContext());
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c/d/file.txt");
EXPECT_FALSE(fileFuture.isReady());
builder.triggerError(
"a/b/c/d", std::domain_error("error for testing purposes"));
ASSERT_TRUE(fileFuture.isReady());
EXPECT_THROW_RE(
std::move(fileFuture).get(),
std::domain_error,
"error for testing purposes");
}
TEST(InodeMap, renameDuringRecursiveLookup) {
auto builder = FakeTreeBuilder();
builder.setFile("a/b/c/d/file.txt", "this is a test file");
TestMount testMount{builder, false};
const auto& edenMount = testMount.getEdenMount();
// Call EdenMount::getInode() on the root
auto rootFuture =
edenMount->getInode(""_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(rootFuture.isReady());
auto rootResult = std::move(rootFuture).get();
EXPECT_EQ(edenMount->getRootInode(), rootResult);
// Call EdenMount::getInode() to do a recursive lookup
auto fileFuture = edenMount->getInode(
"a/b/c/d/file.txt"_relpath, ObjectFetchContext::getNullContext());
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b");
EXPECT_FALSE(fileFuture.isReady());
auto bFuture =
edenMount->getInode("a/b"_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(bFuture.isReady());
auto bInode = std::move(bFuture).get().asTreePtr();
// Rename c to x after the recursive resolution should have
// already looked it up
auto renameFuture =
bInode->rename("c"_pc, bInode, "x"_pc, InvalidationRequired::No);
ASSERT_TRUE(renameFuture.isReady());
EXPECT_FALSE(fileFuture.isReady());
// Now mark the rest of the tree ready
// Note that we don't actually have to mark the file itself ready.
// The Inode lookup itself doesn't need the blob data yet.
builder.setReady("a/b/c/d");
ASSERT_TRUE(fileFuture.isReady());
auto fileInode = std::move(fileFuture).get();
// We should have successfully looked up the inode, but it will report it
// self (correctly) at its new path now.
EXPECT_EQ("a/b/x/d/file.txt"_relpath, fileInode->getPath().value());
}
TEST(InodeMap, renameDuringRecursiveLookupAndLoad) {
auto builder = FakeTreeBuilder();
builder.setFile("a/b/c/d/file.txt", "this is a test file");
TestMount testMount{builder, false};
const auto& edenMount = testMount.getEdenMount();
// Call EdenMount::getInode() on the root
auto rootFuture =
edenMount->getInode(""_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(rootFuture.isReady());
auto rootResult = std::move(rootFuture).get();
EXPECT_EQ(edenMount->getRootInode(), rootResult);
// Call EdenMount::getInode() to do a recursive lookup
auto fileFuture = edenMount->getInode(
"a/b/c/d/file.txt"_relpath, ObjectFetchContext::getNullContext());
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a");
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b");
EXPECT_FALSE(fileFuture.isReady());
auto bFuture =
edenMount->getInode("a/b"_relpath, ObjectFetchContext::getNullContext());
ASSERT_TRUE(bFuture.isReady());
auto bInode = std::move(bFuture).get().asTreePtr();
// Rename c to x while the recursive resolution is still trying
// to look it up.
auto renameFuture =
bInode->rename("c"_pc, bInode, "x"_pc, InvalidationRequired::No);
// The rename will not complete until C becomes ready
EXPECT_FALSE(renameFuture.isReady());
EXPECT_FALSE(fileFuture.isReady());
builder.setReady("a/b/c");
ASSERT_TRUE(renameFuture.isReady());
EXPECT_FALSE(fileFuture.isReady());
// Now mark the rest of the tree ready
// Note that we don't actually have to mark the file itself ready.
// The Inode lookup itself doesn't need the blob data yet.
builder.setReady("a/b/c/d");
ASSERT_TRUE(fileFuture.isReady());
auto fileInode = std::move(fileFuture).get();
// We should have successfully looked up the inode, but it will report it
// self (correctly) at its new path now.
EXPECT_EQ("a/b/x/d/file.txt"_relpath, fileInode->getPath().value());
}
TEST(InodeMap, unloadedUnlinkedTreesAreRemovedFromOverlay) {
FakeTreeBuilder builder;
builder.setFile("dir1/file.txt", "contents");
builder.setFile("dir2/file.txt", "contents");
TestMount mount{builder};
auto edenMount = mount.getEdenMount();
auto root = edenMount->getRootInode();
auto dir1 =
edenMount->getInode("dir1"_relpath, ObjectFetchContext::getNullContext())
.get()
.asTreePtr();
auto dir2 =
edenMount->getInode("dir2"_relpath, ObjectFetchContext::getNullContext())
.get()
.asTreePtr();
auto dir1ino = dir1->getNodeId();
auto dir2ino = dir2->getNodeId();
dir1->unlink(
"file.txt"_pc,
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
dir2->unlink(
"file.txt"_pc,
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
// Test both having a positive and zero fuse reference counts.
dir2->incFsRefcount();
root->rmdir(
"dir1"_pc,
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
root->rmdir(
"dir2"_pc,
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
dir1.reset();
dir2.reset();
edenMount->getInodeMap()->decFsRefcount(dir2ino);
EXPECT_FALSE(mount.hasOverlayData(dir1ino));
EXPECT_FALSE(mount.hasOverlayData(dir2ino));
#ifndef _WIN32
EXPECT_FALSE(mount.hasMetadata(dir1ino));
EXPECT_FALSE(mount.hasMetadata(dir2ino));
#endif // !_WIN32
}
#ifndef _WIN32
TEST(InodeMap, unloadedFileMetadataIsForgotten) {
FakeTreeBuilder builder;
builder.setFile("dir1/file.txt", "contents");
builder.setFile("dir2/file.txt", "contents");
TestMount mount{builder};
auto edenMount = mount.getEdenMount();
auto root = edenMount->getRootInode();
auto dir1 =
edenMount
->getInode(
RelativePathPiece{"dir1"}, ObjectFetchContext::getNullContext())
.get()
.asTreePtr();
auto dir2 =
edenMount
->getInode(
RelativePathPiece{"dir2"}, ObjectFetchContext::getNullContext())
.get()
.asTreePtr();
auto file1 = edenMount
->getInode(
RelativePathPiece{"dir1/file.txt"},
ObjectFetchContext::getNullContext())
.get()
.asFilePtr();
auto file1ino = file1->getNodeId();
auto file2 = edenMount
->getInode(
RelativePathPiece{"dir2/file.txt"},
ObjectFetchContext::getNullContext())
.get()
.asFilePtr();
auto file2ino = file2->getNodeId();
EXPECT_TRUE(mount.hasMetadata(file1ino));
EXPECT_TRUE(mount.hasMetadata(file2ino));
// Try having both positive and zero FUSE reference counts.
file1->incFsRefcount();
file1.reset();
file2.reset();
dir1->unlink(
PathComponentPiece{"file.txt"},
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
dir2->unlink(
PathComponentPiece{"file.txt"},
InvalidationRequired::No,
ObjectFetchContext::getNullContext())
.get(0ms);
EXPECT_TRUE(mount.hasMetadata(file1ino));
EXPECT_FALSE(mount.hasMetadata(file2ino));
edenMount->getInodeMap()->decFsRefcount(file1ino);
EXPECT_FALSE(mount.hasMetadata(file1ino));
EXPECT_FALSE(mount.hasMetadata(file2ino));
}
#endif
struct InodePersistenceTreeTest : ::testing::Test {
InodePersistenceTreeTest() {
builder.setFile("dir/file1.txt", "contents1");
builder.setFile("dir/file2.txt", "contents2");
}
FakeTreeBuilder builder;
};
struct InodePersistenceTakeoverTest : InodePersistenceTreeTest {
InodePersistenceTakeoverTest()
: testMount{builder}, edenMount{testMount.getEdenMount()} {}
void SetUp() override {
InodePersistenceTreeTest::SetUp();
auto tree =
edenMount->getInode("dir"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file1 =
edenMount
->getInode(
"dir/file1.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file2 =
edenMount
->getInode(
"dir/file2.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
// Pretend FUSE is keeping references to these.
tree->incFsRefcount();
file1->incFsRefcount();
file2->incFsRefcount();
oldTreeId = tree->getNodeId();
oldFile1Id = file1->getNodeId();
oldFile2Id = file2->getNodeId();
edenMount.reset();
tree.reset();
file1.reset();
file2.reset();
#ifdef _WIN32
// Windows doesn't support graceful restart yet. Here these tests help
// test the consistency of the overlay. On Windows we are using Sqlite
// Overlay which maintains the same inode number for each inode, after
// remounts.
testMount.remount();
#else
testMount.remountGracefully();
#endif
edenMount = testMount.getEdenMount();
}
TestMount testMount;
std::shared_ptr<EdenMount> edenMount;
InodeNumber oldTreeId;
InodeNumber oldFile1Id;
InodeNumber oldFile2Id;
};
TEST_F(
InodePersistenceTakeoverTest,
preservesInodeNumbersForLoadedInodesDuringTakeover_lookupFirstByName) {
// Look up in a different order to avoid allocating the same numbers.
auto tree =
edenMount->getInode("dir"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file2 =
edenMount
->getInode(
"dir/file2.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file1 =
edenMount
->getInode(
"dir/file1.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
#ifndef _WIN32
EXPECT_EQ(1, tree->debugGetFsRefcount());
EXPECT_EQ(1, file1->debugGetFsRefcount());
EXPECT_EQ(1, file2->debugGetFsRefcount());
#endif // !1
EXPECT_EQ(oldTreeId, tree->getNodeId());
EXPECT_EQ(oldFile1Id, file1->getNodeId());
EXPECT_EQ(oldFile2Id, file2->getNodeId());
// Now try looking up by inode number.
EXPECT_EQ(
"dir",
edenMount->getInodeMap()->lookupInode(oldTreeId).get()->getLogPath());
EXPECT_EQ(
"dir/file1.txt",
edenMount->getInodeMap()->lookupInode(oldFile1Id).get()->getLogPath());
EXPECT_EQ(
"dir/file2.txt",
edenMount->getInodeMap()->lookupInode(oldFile2Id).get()->getLogPath());
}
// The following test will not work on Windows, because on Windows we remount
// instead of remountGracefully and remount doesn't pre-populate the InodeMap.
// The lookupFirstByName above will work because checking by name will populate
// the InodeMap for us.
#ifndef _WIN32
TEST_F(
InodePersistenceTakeoverTest,
preservesInodeNumbersForLoadedInodesDuringTakeover_lookupFirstByNumber) {
// Look up by number first.
EXPECT_EQ(
"dir",
edenMount->getInodeMap()->lookupInode(oldTreeId).get()->getLogPath());
EXPECT_EQ(
"dir/file1.txt",
edenMount->getInodeMap()->lookupInode(oldFile1Id).get()->getLogPath());
EXPECT_EQ(
"dir/file2.txt",
edenMount->getInodeMap()->lookupInode(oldFile2Id).get()->getLogPath());
// Verify the same inodes can be looked up by name too.
auto tree =
edenMount->getInode("dir"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file2 =
edenMount
->getInode(
"dir/file2.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file1 =
edenMount
->getInode(
"dir/file1.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
#ifndef _WIN32
EXPECT_EQ(1, tree->debugGetFsRefcount());
EXPECT_EQ(1, file1->debugGetFsRefcount());
EXPECT_EQ(1, file2->debugGetFsRefcount());
#endif // !_WIN32
EXPECT_EQ(oldTreeId, tree->getNodeId());
EXPECT_EQ(oldFile1Id, file1->getNodeId());
EXPECT_EQ(oldFile2Id, file2->getNodeId());
}
#endif
/**
* clang and gcc use the inode number of a header to determine whether it's the
* same file as one previously included and marked #pragma once.
*
* At least as long as the mount is up (including though graceful takeovers),
* Eden must provide consistent inode numbers.
*/
TEST_F(
InodePersistenceTreeTest,
preservesInodeNumbersForUnloadedInodesDuringTakeover) {
TestMount testMount{builder};
auto edenMount = testMount.getEdenMount();
auto tree =
edenMount->getInode("dir"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file1 =
edenMount
->getInode(
"dir/file1.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
auto file2 =
edenMount
->getInode(
"dir/file2.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
tree->incFsRefcount();
file1->incFsRefcount();
file2->incFsRefcount();
auto oldTreeId = tree->getNodeId();
auto oldFile1Id = file1->getNodeId();
auto oldFile2Id = file2->getNodeId();
tree->decFsRefcount();
file1->decFsRefcount();
file2->decFsRefcount();
edenMount.reset();
tree.reset();
file1.reset();
file2.reset();
#ifdef _WIN32
// Windows doesn't support graceful restart yet. Here these tests help
// test the consistency of the overlay. On Windows we are using Sqlite
// Overlay which maintains the same inode number for each inode, after
// remounts.
testMount.remount();
#else
testMount.remountGracefully();
#endif
edenMount = testMount.getEdenMount();
// Look up in a different order.
tree =
edenMount->getInode("dir"_relpath, ObjectFetchContext::getNullContext())
.get();
file2 = edenMount
->getInode(
"dir/file2.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
file1 = edenMount
->getInode(
"dir/file1.txt"_relpath, ObjectFetchContext::getNullContext())
.get();
EXPECT_EQ(oldTreeId, tree->getNodeId());
EXPECT_EQ(oldFile1Id, file1->getNodeId());
EXPECT_EQ(oldFile2Id, file2->getNodeId());
}