Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix tmp::entry::move method #129

Merged
merged 32 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
55e888d
Test GitHub Actions configuration
bugdea1er Oct 8, 2024
baa5c5a
Remove debug printing
bugdea1er Oct 8, 2024
09161d1
Remove more debug prints
bugdea1er Oct 8, 2024
67adc6a
Do not explicitly initialize paths where not needed
bugdea1er Oct 8, 2024
0b0127b
Remove managed path only when moving between devices
bugdea1er Oct 8, 2024
5605b16
Add tests for `entry::move` to itself
bugdea1er Oct 8, 2024
2883088
Add test for move overwriting
bugdea1er Oct 8, 2024
abbe800
Add a test for moving a file to a non-existing path
bugdea1er Oct 8, 2024
330eeba
Add a test for moving a file to an existing directory
bugdea1er Oct 8, 2024
af277ea
Tidy up includes
bugdea1er Oct 8, 2024
f7bf048
Add a test for moving a file to a non-existing directory
bugdea1er Oct 8, 2024
a23f1cf
Fix clang-tidy warnings
bugdea1er Oct 8, 2024
5692798
Add tests for moving to existing paths
bugdea1er Oct 8, 2024
d806b32
Test moving a directory to a non-existing path
bugdea1er Oct 8, 2024
e59edf4
Fix a bug when moving a directory to an existing file between devices
bugdea1er Oct 8, 2024
b4a2ca7
Expand the documentation for `entry::move`
bugdea1er Oct 8, 2024
d21978d
Close file streams after reading contents
bugdea1er Oct 8, 2024
c03415e
Check file types before renaming in `entry::move`
bugdea1er Oct 8, 2024
6050b45
Fix test specially for Windows
bugdea1er Oct 8, 2024
7a32f91
Actually Windows behaves the same here
bugdea1er Oct 8, 2024
3f5317a
Fix from `is_regular_file` to `!is_directory`
bugdea1er Oct 8, 2024
c5190e2
Fix Windows specific rename behaviour
bugdea1er Oct 8, 2024
8bb39f5
Maybe clashing names is the problem?
bugdea1er Oct 8, 2024
23fa6c2
Fix test files cleanup
bugdea1er Oct 8, 2024
e565443
Fix tests on Windows
bugdea1er Oct 13, 2024
999bacc
Define one error value for a platform
bugdea1er Oct 13, 2024
c9c5352
Fix Windows platform macro usage
bugdea1er Oct 13, 2024
4aef5c1
Simplify the move implementation
bugdea1er Oct 13, 2024
031ed66
Explain the code
bugdea1er Oct 13, 2024
0268044
Fix tests for Linux systems
bugdea1er Oct 13, 2024
3c67b0b
Remove this only after checking for errors
bugdea1er Oct 13, 2024
294b531
Fix clean up logic
bugdea1er Oct 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion include/tmp/entry
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public:
native_handle_type native_handle() const noexcept;

/// Moves the managed path recursively to a given target, releasing
/// ownership of the managed path
/// ownership of the managed path; behaves like `std::filesystem::rename`
/// even when moving between filesystems
/// @note The target path parent is created when this function is called
/// @param to A path to the target file or directory
/// @throws std::filesystem::filesystem_error if cannot move the owned path
Expand Down
41 changes: 34 additions & 7 deletions src/entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,27 +117,54 @@ entry::native_handle_type entry::native_handle() const noexcept {

void entry::move(const fs::path& to) {
std::error_code ec;
if (fs::exists(to)) {
if (!fs::is_directory(*this) && fs::is_directory(to)) {
ec = std::make_error_code(std::errc::is_a_directory);
throw_move_error(to, ec);
}

if (fs::is_directory(*this) && !fs::is_directory(to)) {
ec = std::make_error_code(std::errc::not_a_directory);
throw_move_error(to, ec);
}
}

create_parent(to, ec);
if (ec) {
throw_move_error(to, ec);
}

fs::rename(*this, to, ec);
if (ec == std::errc::cross_device_link) {
if (fs::is_regular_file(*this) && fs::is_directory(to)) {
ec = std::make_error_code(std::errc::is_a_directory);
throw_move_error(to, ec);
}
bool copying = false;

#ifdef _WIN32
// On Windows, the underlying `MoveFileExW` fails when moving a directory
// between drives; in that case we copy the directory manually
copying = fs::is_directory(*this) && path().root_name() != to.root_name();
if (copying) {
fs::copy(*this, to, copy_options, ec);
} else {
fs::rename(*this, to, ec);
}
#else
// On POSIX-compliant systems, the underlying `rename` function may return
// `EXDEV` if the implementation does not support links between file systems;
// so we try to rename the file, and if we fail with `EXDEV`, move it manually
fs::rename(*this, to, ec);
copying = ec == std::errc::cross_device_link;
if (copying) {
fs::remove_all(to);
fs::copy(*this, to, copy_options, ec);
}
#endif

if (ec) {
throw_move_error(to, ec);
}

remove(*this);
if (copying) {
remove(*this);
}

pathobject.clear();
}

Expand Down
5 changes: 3 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ project(${PROJECT_NAME}.test)

find_package(GTest)

add_executable(${PROJECT_NAME} directory.cpp file.cpp)
add_executable(${PROJECT_NAME} entry.cpp directory.cpp file.cpp)
target_link_libraries(${PROJECT_NAME} tmp::tmp GTest::gtest_main)
target_compile_definitions(${PROJECT_NAME}
PRIVATE LABEL="com.github.bugdea1er.tmp")
PRIVATE LABEL="com.github.bugdea1er.tmp"
BUILD_DIR="${CMAKE_CURRENT_BINARY_DIR}")

# On some platforms (e.g. Windows) CMake doesn't write load paths properly
# This solution to put outputs in the same directory is good enough
Expand Down
23 changes: 1 addition & 22 deletions tests/directory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ TEST(directory, list) {

/// Tests that destructor removes a directory
TEST(directory, destructor) {
fs::path path = fs::path();
fs::path path;
entry::native_handle_type handle;
{
directory tmpdir = directory();
Expand Down Expand Up @@ -172,27 +172,6 @@ TEST(directory, move_assignment) {
EXPECT_TRUE(native_handle_is_valid(fst.native_handle()));
}

/// Tests directory moving
TEST(directory, move) {
fs::path path = fs::path();
entry::native_handle_type handle;

fs::path to = fs::temp_directory_path() / "non-existing" / "parent";
{
directory tmpdir = directory();
path = tmpdir;
handle = tmpdir.native_handle();

tmpdir.move(to);
}

EXPECT_FALSE(fs::exists(path));
EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(native_handle_is_valid(handle));

fs::remove_all(fs::temp_directory_path() / "non-existing");
}

/// Tests directory swapping
TEST(directory, swap) {
directory fst = directory();
Expand Down
233 changes: 233 additions & 0 deletions tests/entry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#include <tmp/directory>
#include <tmp/entry>
#include <tmp/file>

#include "utils.hpp"

#include <gtest/gtest.h>

#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>

namespace tmp {
namespace {

namespace fs = std::filesystem;

/// Returns a temporary file containing `Hello world!`
file test_file() {
file tmpfile = file();
tmpfile.write("Hello, world!");

return tmpfile;
}

/// Returns a temporary directory with a file containing `Hello world!`
directory test_directory() {
directory tmpdir = directory();
std::ofstream(tmpdir / "file") << "Hello, world!";

return tmpdir;
}
} // namespace

/// Tests that moving a temporary file to itself does nothing
TEST(entry, move_file_to_self) {
fs::path path;
entry::native_handle_type handle;

{
file tmpfile = test_file();
path = tmpfile;
handle = tmpfile.native_handle();

tmpfile.move(tmpfile);
}

EXPECT_TRUE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(path);
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

fs::remove_all(path);
}

/// Tests moving a temporary file to existing non-directory file
TEST(entry, move_file_to_existing_file) {
fs::path path;
entry::native_handle_type handle;

fs::path to = fs::path(BUILD_DIR) / "move_file_to_existing_test";
std::ofstream(to / "file") << "Goodbye, world!";

{
file tmpfile = test_file();
path = tmpfile;
handle = tmpfile.native_handle();

tmpfile.move(to);
}

EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(to);
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

fs::remove_all(to);
}

/// Tests moving a temporary file to an existing directory
TEST(entry, move_file_to_existing_directory) {
fs::path directory = fs::path(BUILD_DIR) / "existing_directory";
fs::create_directories(directory);

EXPECT_THROW(test_file().move(directory), fs::filesystem_error);

fs::remove_all(directory);
}

/// Tests moving a temporary file to a non-existing file
TEST(entry, move_file_to_non_existing_file) {
fs::path path;
entry::native_handle_type handle;

fs::path parent = fs::path(BUILD_DIR) / "non-existing1";
fs::path to = parent / "path";

{
file tmpfile = test_file();
path = tmpfile;
handle = tmpfile.native_handle();

tmpfile.move(to);
}

EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(to);
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

fs::remove_all(parent);
}

/// Tests moving a temporary file to a non-existing directory
TEST(entry, move_file_to_non_existing_directory) {
fs::path parent = fs::path(BUILD_DIR) / "non-existing2";
fs::path to = parent / "path/";

EXPECT_THROW(test_file().move(to), fs::filesystem_error);

fs::remove_all(parent);
}

/// Tests that moving a temporary directory to itself does nothing
TEST(entry, move_directory_to_self) {
fs::path path;
entry::native_handle_type handle;

{
directory tmpdir = test_directory();
path = tmpdir;
handle = tmpdir.native_handle();

tmpdir.move(tmpdir);
}

EXPECT_TRUE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(path / "file");
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

fs::remove_all(path);
}

/// Tests moving a temporary directory to existing directory
TEST(entry, move_directory_to_existing_directory) {
fs::path path;
entry::native_handle_type handle;

fs::path to = fs::path(BUILD_DIR) / "move_directory_to_existing_test";
std::ofstream(to / "file2") << "Goodbye, world!";

{
directory tmpdir = test_directory();
path = tmpdir;
handle = tmpdir.native_handle();

tmpdir.move(to);
}

EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(to / "file");
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

EXPECT_FALSE(fs::exists(to / "file2"));

fs::remove_all(to);
}

/// Tests moving a temporary directory to an existing file
TEST(entry, move_directory_to_existing_file) {
fs::path to = fs::path(BUILD_DIR) / "existing_file";
std::ofstream(to) << "Goodbye, world!";

EXPECT_THROW(test_directory().move(to), fs::filesystem_error);

fs::remove_all(to);
}

/// Tests moving a temporary directory to a non-existing path
TEST(entry, move_directory_to_non_existing_path) {
fs::path path;
entry::native_handle_type handle;

fs::path parent = fs::path(BUILD_DIR) / "non-existing3";
fs::path to = parent / "path";

{
directory tmpdir = test_directory();
path = tmpdir;
handle = tmpdir.native_handle();

tmpdir.move(to);
}

EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(fs::exists(path));
EXPECT_FALSE(native_handle_is_valid(handle));

{
auto stream = std::ifstream(to / "file");
auto content = std::string(std::istreambuf_iterator<char>(stream), {});
EXPECT_EQ(content, "Hello, world!");
}

fs::remove_all(parent);
}
} // namespace tmp
23 changes: 1 addition & 22 deletions tests/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ TEST(file, output_stream_append_text) {

/// Tests that destructor removes a file
TEST(file, destructor) {
fs::path path = fs::path();
fs::path path;
entry::native_handle_type handle;
{
file tmpfile = file();
Expand Down Expand Up @@ -392,27 +392,6 @@ TEST(file, move_assignment) {
EXPECT_TRUE(native_handle_is_valid(fst.native_handle()));
}

/// Tests file moving
TEST(file, move) {
fs::path path = fs::path();
entry::native_handle_type handle;

fs::path to = fs::temp_directory_path() / "non-existing" / "parent";
{
file tmpfile = file();
path = tmpfile;
handle = tmpfile.native_handle();

tmpfile.move(to);
}

EXPECT_FALSE(fs::exists(path));
EXPECT_TRUE(fs::exists(to));
EXPECT_FALSE(native_handle_is_valid(handle));

fs::remove_all(fs::temp_directory_path() / "non-existing");
}

/// Tests file swapping
TEST(file, swap) {
file fst = file();
Expand Down