/* * 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/FileInode.h" #include #include #include #include #include #include "eden/fs/inodes/TreeInode.h" #include "eden/fs/store/IObjectStore.h" #include "eden/fs/testharness/FakeBackingStore.h" #include "eden/fs/testharness/FakeTreeBuilder.h" #include "eden/fs/testharness/TestChecks.h" #include "eden/fs/testharness/TestMount.h" #include "eden/fs/utils/StatTimes.h" using namespace facebook::eden; using folly::StringPiece; using folly::literals::string_piece_literals::operator""_sp; using namespace std::chrono_literals; std::ostream& operator<<(std::ostream& os, const timespec& ts) { os << folly::sformat("{}.{:09d}", ts.tv_sec, ts.tv_nsec); return os; } namespace std { namespace chrono { std::ostream& operator<<( std::ostream& os, const std::chrono::system_clock::time_point& tp) { auto duration = tp.time_since_epoch(); auto secs = duration_cast(duration); auto nsecs = duration_cast(duration - secs); os << folly::sformat("{}.{:09d}", secs.count(), nsecs.count()); return os; } } // namespace chrono } // namespace std /* * Helper functions for comparing timespec structs from file attributes * against C++11-style time_point objects. */ bool operator<(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) < tp; } bool operator<=(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) <= tp; } bool operator>(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) > tp; } bool operator>=(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) >= tp; } bool operator!=(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) != tp; } bool operator==(const timespec& ts, std::chrono::system_clock::time_point tp) { return folly::to(ts) == tp; } namespace { struct stat getFileAttr(const FileInodePtr& inode) { auto attrFuture = inode->stat(ObjectFetchContext::getNullContext()); // We unfortunately can't use an ASSERT_* check here, since it tries // to return from the function normally, rather than throwing. if (!attrFuture.isReady()) { // Use ADD_FAILURE() so that any SCOPED_TRACE() data will be reported, // then throw an exception. ADD_FAILURE() << "getattr() future is not ready"; throw std::runtime_error("getattr future is not ready"); } return std::move(attrFuture).get(); } struct stat setFileAttr( const FileInodePtr& inode, const DesiredMetadata& desired) { auto attrFuture = inode->setattr(desired); if (!attrFuture.isReady()) { ADD_FAILURE() << "setattr() future is not ready"; throw std::runtime_error("setattr future is not ready"); } return std::move(attrFuture).get(); } /** * Helper function used by BASIC_ATTR_XCHECKS() */ void basicAttrChecks(const FileInodePtr& inode, const struct stat& attr) { EXPECT_EQ(inode->getNodeId().getRawValue(), attr.st_ino); EXPECT_EQ(1, attr.st_nlink); EXPECT_EQ(inode->getMount()->getOwner().uid, attr.st_uid); EXPECT_EQ(inode->getMount()->getOwner().gid, attr.st_gid); EXPECT_EQ(0, attr.st_rdev); EXPECT_GT(attr.st_atime, 0); EXPECT_GT(attr.st_mtime, 0); EXPECT_GT(attr.st_ctime, 0); EXPECT_GT(attr.st_blksize, 0); // Note that st_blocks always refers to 512B blocks, and is not related to // the block size reported in st_blksize. // // Eden doesn't really store data in blocks internally, and instead simply // computes the value in st_blocks based on st_size. This is mainly so that // applications like "du" will report mostly sane results. if (attr.st_size == 0) { EXPECT_EQ(0, attr.st_blocks); } else { EXPECT_GE(512 * attr.st_blocks, attr.st_size); EXPECT_LT(512 * (attr.st_blocks - 1), attr.st_size); } } /** * Run some basic sanity checks on an inode's attributes. * * This can be invoked with either a two arguments (an inode and attributes), * or with just a single argument (just the inode). If only one argument is * supplied the attributes will be retrieved by calling getattr() on the inode. * * This checks several fixed invariants: * - The inode number reported in the attributes should match the input inode's * number. * - The UID and GID should match the EdenMount's user and group IDs. * - The link count should always be 1. * - The timestamps should be greater than 0. */ #define BASIC_ATTR_XCHECKS(inode, ...) \ ({ \ SCOPED_TRACE( \ folly::to("Originally from ", __FILE__, ":", __LINE__)); \ basicAttrChecks(inode, ##__VA_ARGS__); \ }) } // namespace class FileInodeTest : public ::testing::Test { protected: void SetUp() override { // Default to a nonzero time. mount_.getClock().advance(9876min); // 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"}, {"dir/sub/b.txt", "This is b.txt.\n"}}); mount_.initialize(builder); } TestMount mount_; }; TEST_F(FileInodeTest, getType) { auto dir = mount_.getTreeInode("dir/sub"); auto regularFile = mount_.getFileInode("dir/a.txt"); EXPECT_EQ(dtype_t::Dir, dir->getType()); EXPECT_EQ(dtype_t::Regular, regularFile->getType()); } TEST_F(FileInodeTest, getattrFromBlob) { auto inode = mount_.getFileInode("dir/a.txt"); auto attr = getFileAttr(inode); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0644), attr.st_mode); EXPECT_EQ(15, attr.st_size); EXPECT_EQ(1, attr.st_blocks); } TEST_F(FileInodeTest, getattrFromOverlay) { auto start = mount_.getClock().getTimePoint(); mount_.addFile("dir/new_file.c", "hello\nworld\n"); auto inode = mount_.getFileInode("dir/new_file.c"); auto attr = getFileAttr(inode); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0644), attr.st_mode); EXPECT_EQ(12, attr.st_size); EXPECT_EQ(1, attr.st_blocks); EXPECT_EQ(stAtimepoint(attr), start); EXPECT_EQ(stMtimepoint(attr), start); EXPECT_EQ(stCtimepoint(attr), start); } void testSetattrTruncateAll(TestMount& mount) { auto inode = mount.getFileInode("dir/a.txt"); DesiredMetadata desired; desired.size = 0; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0644), attr.st_mode); EXPECT_EQ(0, attr.st_size); EXPECT_EQ(0, attr.st_blocks); EXPECT_FILE_INODE(inode, "", 0644); } TEST_F(FileInodeTest, setattrTruncateAll) { testSetattrTruncateAll(mount_); } TEST_F(FileInodeTest, setattrTruncateAllMaterialized) { // Modify the inode before running the test, so that // it will be materialized in the overlay. auto inode = mount_.getFileInode("dir/a.txt"); inode->write("THIS IS A.TXT.\n", 0); inode.reset(); testSetattrTruncateAll(mount_); } TEST_F(FileInodeTest, setattrTruncatePartial) { auto inode = mount_.getFileInode("dir/a.txt"); DesiredMetadata desired; desired.size = 4; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0644), attr.st_mode); EXPECT_EQ(4, attr.st_size); EXPECT_FILE_INODE(inode, "This", 0644); } TEST_F(FileInodeTest, setattrBiggerSize) { auto inode = mount_.getFileInode("dir/a.txt"); DesiredMetadata desired; desired.size = 30; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0644), attr.st_mode); EXPECT_EQ(30, attr.st_size); StringPiece expectedContents( "This is a.txt.\n" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 30); EXPECT_FILE_INODE(inode, expectedContents, 0644); } TEST_F(FileInodeTest, setattrPermissions) { auto inode = mount_.getFileInode("dir/a.txt"); DesiredMetadata desired; for (int n = 0; n <= 0777; ++n) { desired.mode = n; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | n), attr.st_mode); EXPECT_EQ(15, attr.st_size); EXPECT_FILE_INODE(inode, "This is a.txt.\n", n); } } TEST_F(FileInodeTest, setattrFileType) { auto inode = mount_.getFileInode("dir/a.txt"); DesiredMetadata desired; // File type bits in the mode should be ignored. desired.mode = S_IFLNK | 0755; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ((S_IFREG | 0755), attr.st_mode) << "File type bits in the mode should be ignored by setattr()"; EXPECT_EQ(15, attr.st_size); EXPECT_FILE_INODE(inode, "This is a.txt.\n", 0755); } TEST_F(FileInodeTest, setattrAtime) { auto inode = mount_.getFileInode("dir/a.txt"); DesiredMetadata desired; // Set the atime to a specific value timespec atime; atime.tv_sec = 1234; atime.tv_nsec = 5678; desired.atime = atime; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ(1234, attr.st_atime); EXPECT_EQ(1234, stAtime(attr).tv_sec); EXPECT_EQ(5678, stAtime(attr).tv_nsec); mount_.getClock().advance(10min); // Ask to set the atime to the current time desired.atime = mount_.getClock().getRealtime(); attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ( mount_.getClock().getTimePoint(), folly::to(stAtime(attr))); } void testSetattrMtime(TestMount& mount) { auto inode = mount.getFileInode("dir/a.txt"); DesiredMetadata desired; // Set the mtime to a specific value timespec mtime; mtime.tv_sec = 1234; mtime.tv_nsec = 5678; desired.mtime = mtime; auto attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ(1234, attr.st_mtime); EXPECT_EQ(1234, stMtime(attr).tv_sec); EXPECT_EQ(5678, stMtime(attr).tv_nsec); // Ask to set the mtime to the current time mount.getClock().advance(1234min); auto start = mount.getClock().getTimePoint(); desired.mtime = mount.getClock().getRealtime(); attr = setFileAttr(inode, desired); BASIC_ATTR_XCHECKS(inode, attr); EXPECT_EQ(start, folly::to(stMtime(attr))); } TEST_F(FileInodeTest, setattrMtime) { testSetattrMtime(mount_); } TEST_F(FileInodeTest, setattrMtimeMaterialized) { // Modify the inode before running the test, so that // it will be materialized in the overlay. auto inode = mount_.getFileInode("dir/a.txt"); inode->write("THIS IS A.TXT.\n", 0); inode.reset(); testSetattrMtime(mount_); } namespace { bool isInodeMaterialized(const TreeInodePtr& inode) { return inode->getContents().wlock()->isMaterialized(); } } // namespace TEST_F(FileInodeTest, writingMaterializesParent) { auto inode = mount_.getFileInode("dir/sub/b.txt"); auto parent = mount_.getTreeInode("dir/sub"); auto grandparent = mount_.getTreeInode("dir"); EXPECT_EQ(false, isInodeMaterialized(grandparent)); EXPECT_EQ(false, isInodeMaterialized(parent)); auto written = inode->write("abcd", 0).get(); EXPECT_EQ(4, written); EXPECT_EQ(true, isInodeMaterialized(grandparent)); EXPECT_EQ(true, isInodeMaterialized(parent)); } TEST_F(FileInodeTest, truncatingMaterializesParent) { auto inode = mount_.getFileInode("dir/sub/b.txt"); auto parent = mount_.getTreeInode("dir/sub"); auto grandparent = mount_.getTreeInode("dir"); EXPECT_EQ(false, isInodeMaterialized(grandparent)); EXPECT_EQ(false, isInodeMaterialized(parent)); DesiredMetadata desired; desired.size = 0; (void)inode->setattr(desired).get(0ms); EXPECT_EQ(true, isInodeMaterialized(grandparent)); EXPECT_EQ(true, isInodeMaterialized(parent)); } TEST(FileInode, truncatingDuringLoad) { FakeTreeBuilder builder; builder.setFiles({{"notready.txt", "Contents not ready.\n"}}); TestMount mount_; mount_.initialize(builder, false); auto inode = mount_.getFileInode("notready.txt"); auto backingStore = mount_.getBackingStore(); auto storedBlob = backingStore->getStoredBlob(*inode->getBlobHash()); auto readAllFuture = inode->readAll(ObjectFetchContext::getNullContext()); EXPECT_EQ(false, readAllFuture.isReady()); { // Synchronously truncate the file while the load is in progress. DesiredMetadata desired; desired.size = 0; (void)inode->setattr(desired).get(0ms); // Deallocate the handle here, closing the open file. } // Verify, from the caller's perspective, the load is complete (but empty). EXPECT_EQ(true, readAllFuture.isReady()); EXPECT_EQ("", readAllFuture.value()); // Now finish the ObjectStore load request to make sure the FileInode // handles the state correctly. storedBlob->setReady(); } TEST(FileInode, readDuringLoad) { // Build a tree to test against, but do not mark the state ready yet FakeTreeBuilder builder; auto contents = "Contents not ready.\n"_sp; builder.setFiles({{"notready.txt", contents}}); TestMount mount_; mount_.initialize(builder, false); // Load the inode and start reading the contents auto inode = mount_.getFileInode("notready.txt"); auto dataFuture = inode->read(4096, 0, ObjectFetchContext::getNullContext()); EXPECT_FALSE(dataFuture.isReady()); // Make the backing store data ready now. builder.setAllReady(); // The read() operation should have completed now. ASSERT_TRUE(dataFuture.isReady()); EXPECT_EQ(contents, std::move(dataFuture).get()->moveToFbString()); } TEST(FileInode, writeDuringLoad) { // Build a tree to test against, but do not mark the state ready yet FakeTreeBuilder builder; builder.setFiles({{"notready.txt", "Contents not ready.\n"}}); TestMount mount_; mount_.initialize(builder, false); // Load the inode and start reading the contents auto inode = mount_.getFileInode("notready.txt"); auto newContents = "TENTS"_sp; auto writeFuture = inode->write(newContents, 3); EXPECT_FALSE(writeFuture.isReady()); // Make the backing store data ready now. builder.setAllReady(); // The write() operation should have completed now. ASSERT_TRUE(writeFuture.isReady()); EXPECT_EQ(newContents.size(), std::move(writeFuture).get()); // We should be able to read back our modified data now. EXPECT_FILE_INODE(inode, "ConTENTS not ready.\n", 0644); } TEST(FileInode, truncateDuringLoad) { // Build a tree to test against, but do not mark the state ready yet FakeTreeBuilder builder; builder.setFiles({{"notready.txt", "Contents not ready.\n"}}); TestMount mount_; mount_.initialize(builder, false); auto inode = mount_.getFileInode("notready.txt"); // Start reading the contents auto dataFuture = inode->read(4096, 0, ObjectFetchContext::getNullContext()); EXPECT_FALSE(dataFuture.isReady()); // Truncate the file while the initial read is in progress. This should // immediately truncate the file even without needing to wait for the data // from the object store. DesiredMetadata desired; desired.size = 0; (void)inode->setattr(desired).get(0ms); // The read should complete now too. ASSERT_TRUE(dataFuture.isReady()); EXPECT_EQ("", std::move(dataFuture).get()->moveToFbString()); // For good measure, test reading and writing some more. inode->write("foobar\n"_sp, 5).get(0ms); dataFuture = inode->read(4096, 0, ObjectFetchContext::getNullContext()); ASSERT_TRUE(dataFuture.isReady()); EXPECT_EQ( "\0\0\0\0\0foobar\n"_sp, std::move(dataFuture).get()->moveToFbString()); EXPECT_FILE_INODE(inode, "\0\0\0\0\0foobar\n"_sp, 0644); } TEST(FileInode, dropsCacheWhenFullyRead) { FakeTreeBuilder builder; builder.setFiles({{"bigfile.txt", "1234567890ab"}}); TestMount mount{builder}; auto blobCache = mount.getBlobCache(); auto inode = mount.getFileInode("bigfile.txt"); auto hash = inode->getBlobHash().value(); EXPECT_FALSE(blobCache->get(hash).object); inode->read(4, 0, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); inode->read(4, 4, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); inode->read(4, 8, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_FALSE(blobCache->contains(hash)); } TEST(FileInode, keepsCacheIfPartiallyReread) { FakeTreeBuilder builder; builder.setFiles({{"bigfile.txt", "1234567890ab"}}); TestMount mount{builder}; auto blobCache = mount.getBlobCache(); auto inode = mount.getFileInode("bigfile.txt"); auto hash = inode->getBlobHash().value(); EXPECT_FALSE(blobCache->contains(hash)); inode->read(6, 0, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); inode->read(6, 6, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_FALSE(blobCache->contains(hash)); inode->read(6, 0, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); // Evicts again on the second full read! inode->read(6, 6, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_FALSE(blobCache->contains(hash)); } TEST(FileInode, dropsCacheWhenMaterialized) { FakeTreeBuilder builder; builder.setFiles({{"bigfile.txt", "1234567890ab"}}); TestMount mount{builder}; auto blobCache = mount.getBlobCache(); auto inode = mount.getFileInode("bigfile.txt"); auto hash = inode->getBlobHash().value(); EXPECT_FALSE(blobCache->get(hash).object); inode->read(4, 0, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); inode->write("data"_sp, 0).get(0ms); EXPECT_FALSE(blobCache->contains(hash)); } TEST(FileInode, dropsCacheWhenUnloaded) { FakeTreeBuilder builder; builder.setFiles({{"bigfile.txt", "1234567890ab"}}); TestMount mount{builder}; auto blobCache = mount.getBlobCache(); auto inode = mount.getFileInode("bigfile.txt"); auto hash = inode->getBlobHash().value(); inode->read(4, 0, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)); inode.reset(); mount.getEdenMount()->getRootInode()->unloadChildrenNow(); EXPECT_FALSE(blobCache->contains(hash)); } TEST(FileInode, reloadsBlobIfCacheIsEvicted) { FakeTreeBuilder builder; builder.setFiles({{"bigfile.txt", "1234567890ab"}}); TestMount mount{builder}; auto blobCache = mount.getBlobCache(); auto inode = mount.getFileInode("bigfile.txt"); auto hash = inode->getBlobHash().value(); inode->read(4, 0, ObjectFetchContext::getNullContext()).get(0ms); blobCache->clear(); EXPECT_FALSE(blobCache->contains(hash)); inode->read(4, 4, ObjectFetchContext::getNullContext()).get(0ms); EXPECT_TRUE(blobCache->contains(hash)) << "reading should insert hash " << hash << " into cache"; } // TODO: test multiple flags together // TODO: ensure ctime is updated after every call to setattr() // TODO: ensure mtime is updated after opening a file, writing to it, then // closing it. #endif