// This test tests various ways of renaming files in nested directory structures. // It tests both that the rename operation succeeds, and that it doesn't deadlock // This is important because our CryNode implementation accesses multiple blobs // (source, source_parent, target_parent, target_grandparent) and if any of those // overlap, we need to make sure that we don't deadlock by trying to load them // at the same time. This is also why these tests nest quite deeply. #include #include "testutils/CryTestBase.h" #include #include #include #include #include using ::testing::Combine; using ::testing::ValuesIn; using ::testing::TestWithParam; using std::vector; namespace bf = boost::filesystem; using fspp::fuse::FuseErrnoException; namespace { vector SourceDirs() { return { "/", "/a1", "/a1/b1", "/a1/b1/c1", "/a1/b1/c1/d1", "/a1/b1/c1/d1/e1", "/a1/b1/c1/d1/e1/f1", }; } vector DestDirs() { auto result = SourceDirs(); result.push_back("/a2"); result.push_back("/a2/b"); result.push_back("/a2/b/c"); result.push_back("/a2/b/c/d"); result.push_back("/a2/b/c/d/e"); result.push_back("/a2/b/c/d/e/f"); result.push_back("/a1/b2"); result.push_back("/a1/b2/c"); result.push_back("/a1/b2/c/d"); result.push_back("/a1/b2/c/d/e"); result.push_back("/a1/b2/c/d/e/f"); result.push_back("/a1/b1/c2"); result.push_back("/a1/b1/c2/d"); result.push_back("/a1/b1/c2/d/e"); result.push_back("/a1/b1/c2/d/e/f"); result.push_back("/a1/b1/c1/d2"); result.push_back("/a1/b1/c1/d2/e"); result.push_back("/a1/b1/c1/d2/e/f"); result.push_back("/a1/b1/c1/d1/e2"); result.push_back("/a1/b1/c1/d1/e2/f"); result.push_back("/a1/b1/c1/d1/e1/f2"); return result; } } class CryNodeTest_RenameNested : public TestWithParam>, public CryTestBase { public: void CreateDirs() { for (const auto& dir : SourceDirs()) { if (dir != "/") { CreateDir(dir); } } } // NOLINTNEXTLINE(misc-no-recursion) void create_path_if_not_exists(const bf::path& path) { if (!Exists(path)) { if (path.has_parent_path()) { create_path_if_not_exists(path.parent_path()); } CreateDir(path); } } void expect_rename_succeeds(const bf::path& source_path, const bf::path& dest_path) { auto source = device().Load(source_path).value(); source->rename(dest_path); // Test that rename was successful if (source_path != dest_path && source_path != "/") { ASSERT_FALSE(Exists(source_path)); } ASSERT_TRUE(Exists(dest_path)); } void expect_rename_fails(const bf::path& source_path, const bf::path& dest_path, int expected_errno) { auto source = device().Load(source_path).value(); try { source->rename(dest_path); ASSERT(false, "Expected throw FuseErrnoException(" + std::to_string(expected_errno) + " but didn't throw"); } catch (const FuseErrnoException& e) { ASSERT_EQ(expected_errno, e.getErrno()); } // Test rename wasn't successful if (dest_path != "/") { ASSERT_TRUE(Exists(source_path)); } auto source_dirs = SourceDirs(); if (source_path != dest_path && source_path != "/" && std::find(source_dirs.begin(), source_dirs.end(), dest_path) == source_dirs.end()) { ASSERT_FALSE(Exists(dest_path)); } } }; INSTANTIATE_TEST_SUITE_P(All, CryNodeTest_RenameNested, Combine(ValuesIn(SourceDirs()), ValuesIn(DestDirs()))); TEST_P(CryNodeTest_RenameNested, Rename) { CreateDirs(); auto source_path = std::get<0>(GetParam()); auto dest_path = std::get<1>(GetParam()); if (dest_path.has_parent_path()) { create_path_if_not_exists(dest_path.parent_path()); } if (source_path == "/" || dest_path == "/") { expect_rename_fails(source_path, dest_path, EBUSY); } else if (source_path == dest_path) { // NOLINT(bugprone-branch-clone) expect_rename_succeeds(source_path, dest_path); } else if (boost::starts_with(source_path, dest_path)) { expect_rename_fails(source_path, dest_path, ENOTEMPTY); } else if (boost::starts_with(dest_path, source_path)) { expect_rename_fails(source_path, dest_path, EINVAL); } else { expect_rename_succeeds(source_path, dest_path); } }