| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License |
| |
| #include <list> |
| #include <map> |
| #include <set> |
| #include <string> |
| |
| #include <stout/fs.hpp> |
| #include <stout/os.hpp> |
| #include <stout/path.hpp> |
| |
| #include <stout/os/getcwd.hpp> |
| #include <stout/os/ls.hpp> |
| #include <stout/os/mkdir.hpp> |
| #include <stout/os/stat.hpp> |
| #include <stout/os/touch.hpp> |
| |
| #include <stout/tests/utils.hpp> |
| |
| using std::list; |
| using std::set; |
| using std::string; |
| |
| |
| static hashset<string> listfiles(const string& directory) |
| { |
| hashset<string> fileset; |
| Try<list<string>> entries = os::ls(directory); |
| if (entries.isSome()) { |
| foreach (const string& entry, entries.get()) { |
| fileset.insert(entry); |
| } |
| } |
| return fileset; |
| } |
| |
| |
| class RmdirTest : public TemporaryDirectoryTest {}; |
| |
| |
| // TODO(hausdorff): This test is almost copy-pasted from |
| // `TrivialRemoveEmptyDirectoryRelativePath`; we should parameterize them to |
| // reduce redundancy. |
| TEST_F(RmdirTest, TrivialRemoveEmptyDirectoryAbsolutePath) |
| { |
| const string tmpdir = os::getcwd(); |
| |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| |
| // Successfully make directory using absolute path. |
| const string newDirectoryName = "newDirectory"; |
| const string newDirectoryAbsolutePath = path::join(tmpdir, newDirectoryName); |
| const hashset<string> expectedListing = { newDirectoryName }; |
| |
| EXPECT_SOME(os::mkdir(newDirectoryAbsolutePath)); |
| EXPECT_EQ(expectedListing, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| |
| // Successfully remove. |
| EXPECT_SOME(os::rmdir(newDirectoryAbsolutePath)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| } |
| |
| |
| TEST_F(RmdirTest, TrivialRemoveEmptyDirectoryRelativePath) |
| { |
| const string tmpdir = os::getcwd(); |
| |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| |
| // Successfully make directory using relative path. |
| const string newDirectoryName = "newDirectory"; |
| const hashset<string> expectedListing = { newDirectoryName }; |
| |
| EXPECT_SOME(os::mkdir(newDirectoryName)); |
| EXPECT_EQ(expectedListing, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryName)); |
| |
| // Successfully remove. |
| EXPECT_SOME(os::rmdir(newDirectoryName)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| } |
| |
| |
| // Tests behavior of `rmdir` when path points at a file instead of a directory. |
| TEST_F(RmdirTest, RemoveFile) |
| { |
| const string tmpdir = os::getcwd(); |
| |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| |
| // Successfully make directory using absolute path, and then `touch` a file |
| // in that folder. |
| const string newDirectoryName = "newDirectory"; |
| const string newDirectoryAbsolutePath = path::join(tmpdir, newDirectoryName); |
| const string newFileName = "newFile"; |
| const string newFileAbsolutePath = path::join( |
| newDirectoryAbsolutePath, |
| newFileName); |
| |
| const hashset<string> expectedRootListing = { newDirectoryName }; |
| const hashset<string> expectedSubListing = { newFileName }; |
| |
| EXPECT_SOME(os::mkdir(newDirectoryAbsolutePath)); |
| EXPECT_SOME(os::touch(newFileAbsolutePath)); |
| EXPECT_EQ(expectedRootListing, listfiles(tmpdir)); |
| EXPECT_EQ(expectedSubListing, listfiles(newDirectoryAbsolutePath)); |
| |
| // Successful recursive remove with `removeRoot` set to `true` (using the |
| // semantics of `rm -r`). |
| EXPECT_SOME(os::rmdir(newFileAbsolutePath)); |
| EXPECT_TRUE(os::exists(newDirectoryAbsolutePath)); |
| ASSERT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| |
| // Add file to directory again. |
| EXPECT_SOME(os::touch(newFileAbsolutePath)); |
| |
| // Successful recursive remove with `removeRoot` set to `false` (using the |
| // semantics of `rm -r`). |
| EXPECT_SOME(os::rmdir(newFileAbsolutePath, true, false)); |
| EXPECT_TRUE(os::exists(newDirectoryAbsolutePath)); |
| ASSERT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| |
| // Add file to directory again. |
| EXPECT_SOME(os::touch(newFileAbsolutePath)); |
| |
| // Error on non-recursive remove with `removeRoot` set to `true` (using the |
| // semantics of `rmdir`). |
| EXPECT_ERROR(os::rmdir(newFileAbsolutePath, false, true)); |
| EXPECT_TRUE(os::exists(newDirectoryAbsolutePath)); |
| EXPECT_TRUE(os::exists(newFileAbsolutePath)); |
| |
| // Error on non-recursive remove with `removeRoot` set to `false` (using the |
| // semantics of `rmdir`). |
| EXPECT_ERROR(os::rmdir(newFileAbsolutePath, false, false)); |
| EXPECT_TRUE(os::exists(newDirectoryAbsolutePath)); |
| EXPECT_TRUE(os::exists(newFileAbsolutePath)); |
| } |
| |
| |
| TEST_F(RmdirTest, RemoveRecursiveByDefault) |
| { |
| const string tmpdir = os::getcwd(); |
| |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| |
| // Successfully make directory using absolute path, and then `touch` a file |
| // in that folder. |
| const string newDirectoryName = "newDirectory"; |
| const string newDirectoryAbsolutePath = path::join(tmpdir, newDirectoryName); |
| const string newFileName = "newFile"; |
| const string newFileAbsolutePath = path::join( |
| newDirectoryAbsolutePath, |
| newFileName); |
| |
| const hashset<string> expectedRootListing = { newDirectoryName }; |
| const hashset<string> expectedSubListing = { newFileName }; |
| |
| EXPECT_SOME(os::mkdir(newDirectoryAbsolutePath)); |
| EXPECT_SOME(os::touch(newFileAbsolutePath)); |
| EXPECT_EQ(expectedRootListing, listfiles(tmpdir)); |
| EXPECT_EQ(expectedSubListing, listfiles(newDirectoryAbsolutePath)); |
| |
| // Successfully remove. |
| EXPECT_SOME(os::rmdir(newDirectoryAbsolutePath)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| } |
| |
| |
| TEST_F(RmdirTest, TrivialFailToRemoveInvalidPath) |
| { |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(os::getcwd())); |
| |
| // Removing fake relative paths should error out. |
| EXPECT_ERROR(os::rmdir("fakeRelativePath", false)); |
| EXPECT_ERROR(os::rmdir("fakeRelativePath", true)); |
| |
| // Directory still empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(os::getcwd())); |
| } |
| |
| |
| TEST_F(RmdirTest, FailToRemoveNestedInvalidPath) |
| { |
| const string tmpdir = os::getcwd(); |
| |
| // Directory is initially empty. |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(tmpdir)); |
| |
| // Successfully make directory using absolute path. |
| const string newDirectoryName = "newDirectory"; |
| const string newDirectoryAbsolutePath = path::join(tmpdir, newDirectoryName); |
| |
| const hashset<string> expectedRootListing = { newDirectoryName }; |
| |
| EXPECT_SOME(os::mkdir(newDirectoryAbsolutePath)); |
| EXPECT_EQ(expectedRootListing, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| |
| // Fail to remove a path to an invalid folder inside the |
| // `newDirectoryAbsolutePath`. |
| const string fakeAbsolutePath = path::join(newDirectoryAbsolutePath, "fake"); |
| EXPECT_ERROR(os::rmdir(fakeAbsolutePath, false)); |
| EXPECT_EQ(expectedRootListing, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| |
| // Test the same thing, but using the `recursive` flag. |
| EXPECT_ERROR(os::rmdir(fakeAbsolutePath, true)); |
| EXPECT_EQ(expectedRootListing, listfiles(tmpdir)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectoryAbsolutePath)); |
| } |
| |
| |
| #ifndef __WINDOWS__ |
| // This test verifies that `rmdir` can remove a directory with a |
| // device file. |
| // TODO(hausdorff): Port this test to Windows. It is not clear that `rdev` and |
| // `mknod` will implement the functionality expressed in this test, and as the |
| // need for these capabilities arise elsewhere in the codebase, we should |
| // rethink abstractions we need here, and subsequently, what this test should |
| // look like. This is `#ifdef`'d rather than `DISABLED_` because `rdev` doesn't |
| // exist on Windows. |
| TEST_F(RmdirTest, RemoveDirectoryWithDeviceFile) |
| { |
| #ifdef __FreeBSD__ |
| // If we're in a jail on FreeBSD, we can't use mknod. |
| if (isJailed()) { |
| return; |
| } |
| #endif |
| |
| // mknod requires root permission. |
| Result<string> user = os::user(); |
| ASSERT_SOME(user); |
| |
| if (user.get() != "root") { |
| return; |
| } |
| |
| // Create a 'char' device file with major number same as that of |
| // `/dev/null`. |
| const string deviceDirectory = path::join(os::getcwd(), |
| "deviceDirectory"); |
| ASSERT_SOME(os::mkdir(deviceDirectory)); |
| |
| const string device = "null"; |
| |
| const string existing = path::join("/dev", device); |
| ASSERT_TRUE(os::exists(existing)); |
| |
| Try<mode_t> mode = os::stat::mode(existing); |
| ASSERT_SOME(mode); |
| |
| Try<dev_t> rdev = os::stat::rdev(existing); |
| ASSERT_SOME(rdev); |
| |
| const string another = path::join(deviceDirectory, device); |
| ASSERT_FALSE(os::exists(another)); |
| |
| EXPECT_SOME(os::mknod(another, mode.get(), rdev.get())); |
| |
| EXPECT_SOME(os::rmdir(deviceDirectory)); |
| } |
| #endif // __WINDOWS__ |
| |
| |
| // This test verifies that `rmdir` can remove a directory with a |
| // symlink that has no target. |
| TEST_F(RmdirTest, SYMLINK_RmDirNoTargetSymbolicLink) |
| { |
| const string newDirectory = path::join(os::getcwd(), "newDirectory"); |
| ASSERT_SOME(os::mkdir(newDirectory)); |
| |
| const string link = path::join(newDirectory, "link"); |
| |
| // Create a symlink to non-existent file 'tmp'. |
| ASSERT_SOME(fs::symlink("tmp", link)); |
| |
| EXPECT_SOME(os::rmdir(newDirectory)); |
| } |
| |
| |
| // This test verifies that `rmdir` can remove a directory with a |
| // "hanging" symlink whose target has been deleted. |
| TEST_F(RmdirTest, SYMLINK_RemoveDirectoryHangingSymlink) |
| { |
| const string newDirectory = path::join(os::getcwd(), "newDirectory"); |
| ASSERT_SOME(os::mkdir(newDirectory)); |
| |
| const string link = path::join(newDirectory, "link"); |
| |
| // Create a hanging symlink to a directory. |
| ASSERT_SOME(os::mkdir("tmp")); |
| ASSERT_SOME(fs::symlink("tmp", link)); |
| ASSERT_SOME(os::rmdir("tmp")); |
| |
| // Remove the parent directory to exercise the recursive deletion path of |
| // `os::rmdir`. |
| EXPECT_SOME(os::rmdir(newDirectory)); |
| } |
| |
| |
| // This test verifies that `rmdir` will only remove the symbolic link and not |
| // the target directory. |
| TEST_F(RmdirTest, SYMLINK_RemoveDirectoryWithSymbolicLinkTargetDirectory) |
| { |
| const string newDirectory = path::join(os::getcwd(), "newDirectory"); |
| ASSERT_SOME(os::mkdir(newDirectory)); |
| |
| const string link = path::join(newDirectory, "link"); |
| |
| const string targetDirectory = path::join(os::getcwd(), "targetDirectory"); |
| |
| ASSERT_SOME(os::mkdir(targetDirectory)); |
| |
| // Create a symlink that targets a directory outside the 'newDirectory'. |
| ASSERT_SOME(fs::symlink(targetDirectory, link)); |
| |
| EXPECT_SOME(os::rmdir(newDirectory)); |
| |
| // Verify that the target directory is not removed. |
| ASSERT_TRUE(os::exists(targetDirectory)); |
| } |
| |
| |
| // This test verifies that `rmdir` will only remove the symbolic link and not |
| // the target file. |
| TEST_F(RmdirTest, SYMLINK_RemoveDirectoryWithSymbolicLinkTargetFile) |
| { |
| const string newDirectory = path::join(os::getcwd(), "newDirectory"); |
| ASSERT_SOME(os::mkdir(newDirectory)); |
| |
| const string link = path::join(newDirectory, "link"); |
| |
| const string targetFile = path::join(os::getcwd(), "targetFile"); |
| |
| ASSERT_SOME(os::touch(targetFile)); |
| |
| // Create a symlink that targets a file outside the 'newDirectory'. |
| ASSERT_SOME(fs::symlink(targetFile, link)); |
| |
| EXPECT_SOME(os::rmdir(newDirectory)); |
| |
| // Verify that the target file is not removed. |
| ASSERT_TRUE(os::exists(targetFile)); |
| } |
| |
| |
| // This tests that when appropriately instructed, `rmdir` can remove |
| // the files and subdirectories that appear in a directory but |
| // preserve the directory itself. |
| TEST_F(RmdirTest, RemoveDirectoryButPreserveRoot) |
| { |
| const string newDirectory = path::join(os::getcwd(), "newDirectory"); |
| ASSERT_SOME(os::mkdir(newDirectory)); |
| |
| const string subDirectory = path::join(newDirectory, "subDirectory"); |
| ASSERT_SOME(os::mkdir(subDirectory)); |
| |
| const string file1 = path::join(newDirectory, "file1"); |
| ASSERT_SOME(os::touch(file1)); |
| |
| const string file2 = path::join(subDirectory, "file2"); |
| ASSERT_SOME(os::touch(file2)); |
| |
| EXPECT_SOME(os::rmdir(newDirectory, true, false)); |
| EXPECT_TRUE(os::exists(newDirectory)); |
| EXPECT_EQ(hashset<string>::EMPTY, listfiles(newDirectory)); |
| } |
| |
| |
| #ifdef __linux__ |
| // This test fixture verifies that `rmdir` behaves correctly |
| // with option `continueOnError` and makes sure the undeletable |
| // files from tests are cleaned up during teardown. |
| class RmdirContinueOnErrorTest : public RmdirTest |
| { |
| public: |
| void TearDown() override |
| { |
| if (mountPoint.isSome()) { |
| if (os::system("umount -f -l " + mountPoint.get()) != 0) { |
| LOG(ERROR) << "Failed to unmount '" << mountPoint.get(); |
| } |
| } |
| |
| RmdirTest::TearDown(); |
| } |
| |
| protected: |
| Option<string> mountPoint; |
| }; |
| |
| |
| // This test creates a busy mount point which is not directly deletable |
| // by rmdir and verifies that rmdir deletes all files that |
| // it's able to delete if `continueOnError = true`. |
| TEST_F(RmdirContinueOnErrorTest, RemoveWithContinueOnError) |
| { |
| // Mounting a filesystem requires root permission. |
| Result<string> user = os::user(); |
| ASSERT_SOME(user); |
| |
| if (user.get() != "root") { |
| return; |
| } |
| |
| const string tmpdir = os::getcwd(); |
| |
| // Successfully make directory and then `touch` a file |
| // in that folder. |
| const string directory = "directory"; |
| |
| // The busy mount point goes before the regular file in `rmdir`'s |
| // directory traversal due to their names. This makes sure that |
| // an error occurs before all deletable files are deleted. |
| const string mountPoint_ = path::join( |
| directory, |
| "mount.point"); |
| |
| const string regularFile = path::join( |
| directory, |
| "regular.file"); |
| |
| ASSERT_SOME(os::mkdir(directory)); |
| ASSERT_SOME(os::mkdir(mountPoint_)); |
| ASSERT_SOME(os::touch(regularFile)); |
| |
| ASSERT_SOME_EQ(0, os::system( |
| "mount --bind " + mountPoint_ + " " + mountPoint_)); |
| |
| // Register the mount point for cleanup. |
| mountPoint = Option<string>(mountPoint_); |
| |
| EXPECT_TRUE(os::exists(directory)); |
| EXPECT_TRUE(os::exists(mountPoint_)); |
| EXPECT_TRUE(os::exists(regularFile)); |
| |
| // Run rmdir with `continueOnError = true`. |
| ASSERT_ERROR(os::rmdir(directory, true, true, true)); |
| |
| EXPECT_TRUE(os::exists(directory)); |
| EXPECT_TRUE(os::exists(mountPoint_)); |
| EXPECT_FALSE(os::exists(regularFile)); |
| } |
| #endif // __linux__ |