blob: 3ca4099300c6288eb453ef04eb97d299adc5880d [file] [log] [blame]
// 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__