diff --git a/CMakeLists.txt b/CMakeLists.txt index 28a9fca..8d003dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,12 @@ if(BUILD_TESTING) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(test_filesystem_helper ${PROJECT_NAME}) + ament_add_gtest(test_filesystem_helper_std test/test_filesystem_helper_std.cpp + ENV + EXPECTED_WORKING_DIRECTORY=$ + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(test_filesystem_helper_std ${PROJECT_NAME}) + ament_add_gtest(test_find_and_replace test/test_find_and_replace.cpp) target_link_libraries(test_find_and_replace ${PROJECT_NAME}) diff --git a/include/rcpputils/filesystem_helper.hpp b/include/rcpputils/filesystem_helper.hpp index 38f99fa..66f6019 100644 --- a/include/rcpputils/filesystem_helper.hpp +++ b/include/rcpputils/filesystem_helper.hpp @@ -66,14 +66,21 @@ static constexpr const char kPreferredSeparator = RCPPUTILS_IMPL_OS_DIRSEP; #undef RCPPUTILS_IMPL_OS_DIRSEP - +// TODO(ahcorde): Remove deprecated class on the next release. +#if !defined(_WIN32) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#else // !defined(_WIN32) +# pragma warning(push) +# pragma warning(disable: 4996) +#endif /** * \brief Drop-in replacement for [std::filesystem::path](https://en.cppreference.com/w/cpp/filesystem/path). * * It must conform to the same standard described and cannot include methods that are not * incorporated there. */ -class path +class [[deprecated("use std::filesystem instead of rcpputils::path")]] path { public: /** @@ -232,6 +239,7 @@ class path * \param[in] p The path to check * \return True if the path exists, false otherwise. */ +[[deprecated("Please use std::filesystem::is_regular_file(..) instead")]] RCPPUTILS_PUBLIC bool is_regular_file(const path & p) noexcept; /** @@ -240,6 +248,7 @@ RCPPUTILS_PUBLIC bool is_regular_file(const path & p) noexcept; * \param[in] p The path to check * \return True if the path is an existing directory, false otherwise. */ +[[deprecated("Please use std::filesystem::is_directory(..) instead")]] RCPPUTILS_PUBLIC bool is_directory(const path & p) noexcept; /** @@ -250,6 +259,7 @@ RCPPUTILS_PUBLIC bool is_directory(const path & p) noexcept; * * \throws std::sytem_error */ +[[deprecated("Please use std::filesystem::file_size(..) instead")]] RCPPUTILS_PUBLIC uint64_t file_size(const path & p); /** @@ -258,6 +268,7 @@ RCPPUTILS_PUBLIC uint64_t file_size(const path & p); * \param[in] path_to_check The path to check. * \return True if the path exists, false otherwise. */ +[[deprecated("Please use std::filesystem::exists(..) instead")]] RCPPUTILS_PUBLIC bool exists(const path & path_to_check); @@ -317,6 +328,7 @@ RCPPUTILS_PUBLIC std::filesystem::path create_temporary_directory( * * \throws std::system_error */ +[[deprecated("Please use std::filesystem::current_path(..) instead")]] RCPPUTILS_PUBLIC path current_path(); /** @@ -326,6 +338,7 @@ RCPPUTILS_PUBLIC path current_path(); * \param[in] p The path at which to create the directory. * \return Return true if the directory already exists or is created, false otherwise. */ +[[deprecated("Please use std::filesystem::create_directories(..) instead")]] RCPPUTILS_PUBLIC bool create_directories(const path & p); /** @@ -334,6 +347,7 @@ RCPPUTILS_PUBLIC bool create_directories(const path & p); * \param[in] p The path of the object to remove. * \return true if the file exists and it was successfully removed, false otherwise. */ +[[deprecated("Please use std::filesystem::remove(..) instead")]] RCPPUTILS_PUBLIC bool remove(const path & p); /** @@ -344,6 +358,7 @@ RCPPUTILS_PUBLIC bool remove(const path & p); * \param[in] p The path of the directory to remove. * \return true if the directory exists and it was successfully removed, false otherwise. */ +[[deprecated("Please use std::filesystem::remove_all(..) instead")]] RCPPUTILS_PUBLIC bool remove_all(const path & p); /** @@ -362,7 +377,9 @@ RCPPUTILS_PUBLIC path remove_extension(const path & file_path, int n_times = 1); * * \return True if both paths are equal as strings. */ +[[deprecated("This operator will be remove with the deprecated path class")]] RCPPUTILS_PUBLIC bool operator==(const path & a, const path & b); +[[deprecated("This operator will be remove with the deprecated path class")]] RCPPUTILS_PUBLIC bool operator!=(const path & a, const path & b); /** @@ -372,8 +389,15 @@ RCPPUTILS_PUBLIC bool operator!=(const path & a, const path & b); * \param[in] p The path to stringify * \return The ostream, for chaining */ +[[deprecated("This operator will be remove with the deprecated path class")]] RCPPUTILS_PUBLIC std::ostream & operator<<(std::ostream & os, const path & p); +// remove warning suppression +#if !defined(_WIN32) +# pragma GCC diagnostic pop +#else // !defined(_WIN32) +# pragma warning(pop) +#endif } // namespace fs } // namespace rcpputils diff --git a/src/filesystem_helper.cpp b/src/filesystem_helper.cpp index 9556fed..b91cca4 100644 --- a/src/filesystem_helper.cpp +++ b/src/filesystem_helper.cpp @@ -71,6 +71,14 @@ namespace rcpputils namespace fs { +#if !defined(_WIN32) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#else // !defined(_WIN32) +# pragma warning(push) +# pragma warning(disable: 4996) +#endif + /// \internal Returns true if the path is an absolute path with a drive letter on Windows. static bool is_absolute_with_drive_letter(const std::string & path); @@ -517,5 +525,11 @@ std::ostream & operator<<(std::ostream & os, const path & p) return os; } +// remove warning suppression +#if !defined(_WIN32) +# pragma GCC diagnostic pop +#else // !defined(_WIN32) +# pragma warning(pop) +#endif } // namespace fs } // namespace rcpputils diff --git a/src/find_library.cpp b/src/find_library.cpp index ce23987..dd4f29d 100644 --- a/src/find_library.cpp +++ b/src/find_library.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -71,8 +72,8 @@ std::string find_library_path(const std::string & library_name) std::string path_for_library(const std::string & directory, const std::string & library_name) { - auto path = rcpputils::fs::path(directory) / filename_for_library(library_name); - if (path.is_regular_file()) { + auto path = std::filesystem::path(directory) / filename_for_library(library_name); + if (std::filesystem::is_regular_file(path)) { return path.string(); } return ""; diff --git a/test/test_filesystem_helper.cpp b/test/test_filesystem_helper.cpp index 491ebc2..c277e39 100644 --- a/test/test_filesystem_helper.cpp +++ b/test/test_filesystem_helper.cpp @@ -25,6 +25,13 @@ static constexpr const bool is_win32 = true; #else static constexpr const bool is_win32 = false; #endif +#if !defined(_WIN32) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#else // !defined(_WIN32) +# pragma warning(push) +# pragma warning(disable: 4996) +#endif using path = rcpputils::fs::path; @@ -531,3 +538,9 @@ TEST(TestFilesystemHelper, equal_operators) path d = path("foo") / "bar"; EXPECT_EQ(c, d); } + +#if !defined(_WIN32) +# pragma GCC diagnostic pop +#else // !defined(_WIN32) +# pragma warning(pop) +#endif diff --git a/test/test_filesystem_helper_std.cpp b/test/test_filesystem_helper_std.cpp new file mode 100644 index 0000000..7f9beed --- /dev/null +++ b/test/test_filesystem_helper_std.cpp @@ -0,0 +1,499 @@ +// Copyright 2024 Open Source Robotics Foundation, Inc. +// +// 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 + +#include +#include + +#include "rcpputils/filesystem_helper.hpp" +#include "rcpputils/env.hpp" + +#ifdef _WIN32 +static constexpr const bool is_win32 = true; +#else +static constexpr const bool is_win32 = false; +#endif + +using path = std::filesystem::path; + +static const std::string build_extension_path() +{ + return is_win32 ? R"(C:\foo\bar\baz.yml)" : "/bar/foo/baz.yml"; +} + +static const std::string build_double_extension_path() +{ + return is_win32 ? R"(C:\bar\baz.bar.yml)" : "/foo/baz.bar.yml"; +} + +static const std::string build_no_extension_path() +{ + return is_win32 ? R"(.\test_folder)" : R"(./test_folder)"; +} + +static const std::string build_directory_path() +{ + return is_win32 ? R"(.\test_folder)" : R"(./test_folder)"; +} + +TEST(TestFilesystemHelper, join_path) +{ + { + auto p = path("foo") / path("bar"); + if (is_win32) { + EXPECT_EQ("foo\\bar", p.string()); + } else { + EXPECT_EQ("foo/bar", p.string()); + } + } + + if (is_win32) { + auto p = path("foo") / path("C:\\bar"); + EXPECT_EQ("C:\\bar", p.string()); + } else { + auto p = path("foo") / path("/bar"); + EXPECT_EQ("/bar", p.string()); + } + + { + // Just expect these join operations to be allowed with const paths + const auto p1 = path("foo"); + auto p1_baz = p1 / "baz"; + + const auto p2 = path("bar"); + auto p1_2 = p1 / p2; + } +} + +TEST(TestFilesystemHelper, parent_path) +{ + { + auto p = path("my") / path("path"); + EXPECT_EQ(p.parent_path().string(), path("my").string()); + } + { + auto p = path("foo"); + EXPECT_EQ(p.parent_path().string(), ""); + } + if (is_win32) { + { + auto p = path("C:\\foo"); + EXPECT_EQ(p.parent_path().string(), "C:\\"); + } + { + auto p = path("\\foo"); + EXPECT_EQ(p.parent_path().string(), "\\"); + } + { + auto p = path("C:\\"); + EXPECT_EQ(p.parent_path().string(), "C:\\"); + } + { + auto p = path("\\"); + EXPECT_EQ(p.parent_path().string(), "\\"); + } + } else { + { + auto p = path("/foo"); + EXPECT_EQ(p.parent_path().string(), "/"); + } + { + auto p = path("/"); + EXPECT_EQ(p.parent_path().string(), "/"); + } + } + { + auto p = path(""); + EXPECT_EQ(p.parent_path().string(), ""); + } +} + +TEST(TestFilesystemHelper, to_native_path) +{ + { + auto p = path("/foo/bar/baz"); + EXPECT_EQ("/foo/bar/baz", p.string()); + } + { + auto p = path("\\foo\\bar\\baz"); + if (is_win32) { + EXPECT_EQ("\\foo\\bar\\baz", p.string()); + } else { + EXPECT_EQ("\\foo\\bar\\baz", p.string()); + } + } + { + auto p = path("/foo//bar/baz"); + EXPECT_EQ("/foo//bar/baz", p.string()); + } + { + auto p = path("\\foo\\\\bar\\baz"); + if (is_win32) { + EXPECT_EQ("\\foo\\\\bar\\baz", p.string()); + } else { + EXPECT_EQ("\\foo\\\\bar\\baz", p.string()); + } + } +} + +TEST(TestFilesystemHelper, is_absolute) +{ + if (is_win32) { + { + auto p = path("C:\\foo\\bar\\baz"); + EXPECT_TRUE(p.is_absolute()); + } + { + auto p = path("D:\\foo\\bar\\baz"); + EXPECT_TRUE(p.is_absolute()); + } + { + auto p = path("C:/foo/bar/baz"); + EXPECT_TRUE(p.is_absolute()); + } + { + auto p = path("\\foo\\bar\\baz"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path("/foo/bar/baz"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path("foo/bar/baz"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path("C:"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path("C"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path(""); + EXPECT_FALSE(p.is_absolute()); + } + } else { + { + auto p = path("/foo/bar/baz"); + EXPECT_TRUE(p.is_absolute()); + } + { + auto p = path("foo/bar/baz"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path("f"); + EXPECT_FALSE(p.is_absolute()); + } + { + auto p = path(""); + EXPECT_FALSE(p.is_absolute()); + } + } +} + +TEST(TestFilesystemHelper, correct_extension) +{ + { + auto p = path(build_extension_path()); + auto ext = p.extension(); + EXPECT_EQ(".yml", ext.string()); + } + { + auto p = path(build_double_extension_path()); + auto ext = p.extension(); + EXPECT_EQ(".yml", ext.string()); + } + { + auto p = path(build_no_extension_path()); + auto ext = p.extension(); + EXPECT_EQ("", ext.string()); + } +} + +TEST(TestFilesystemHelper, is_empty) +{ + auto p_no_arg = path(); + EXPECT_TRUE(p_no_arg.empty()); + + auto p_empty_string = path(""); + EXPECT_TRUE(p_empty_string.empty()); +} + +TEST(TestFilesystemHelper, exists) +{ + { + auto p = path(""); + EXPECT_FALSE(std::filesystem::exists(p)); + } + { + auto p = path("."); + EXPECT_TRUE(std::filesystem::exists(p)); + } + { + auto p = path(".."); + EXPECT_TRUE(std::filesystem::exists(p)); + } + { + if (is_win32) { + auto p = path("\\"); + EXPECT_TRUE(std::filesystem::exists(p)); + } else { + auto p = path("/"); + EXPECT_TRUE(std::filesystem::exists(p)); + } + } +} + +/** + * Test filesystem manipulation API. + * + * NOTE: expects the current directory to be write-only, else test will fail. + * + */ +TEST(TestFilesystemHelper, filesystem_manipulation) +{ + auto dir = path(build_directory_path()); + std::filesystem::remove_all(dir); + EXPECT_FALSE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::create_directories(dir)); + EXPECT_TRUE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::is_directory(dir)); + EXPECT_FALSE(std::filesystem::is_regular_file(dir)); + + auto file = dir / "test_file.txt"; + uint64_t expected_file_size = 0; + { + std::ofstream output_buffer{file.string()}; + output_buffer << "test"; + expected_file_size = static_cast(output_buffer.tellp()); + } + + EXPECT_TRUE(std::filesystem::exists(file)); + EXPECT_TRUE(std::filesystem::is_regular_file(file)); + EXPECT_FALSE(std::filesystem::is_directory(file)); + std::error_code ec; + EXPECT_FALSE(std::filesystem::create_directories(file, ec)); + EXPECT_GE(std::filesystem::file_size(file), expected_file_size); + if (!is_win32) { + EXPECT_THROW((void)std::filesystem::file_size(dir), std::filesystem::filesystem_error) << + "file_size is only applicable for files!"; + } + EXPECT_FALSE(std::filesystem::remove(dir, ec)); + EXPECT_TRUE(std::filesystem::remove(file, ec)); + EXPECT_THROW((void)std::filesystem::file_size(file), std::filesystem::filesystem_error); + EXPECT_TRUE(std::filesystem::remove(dir)); + EXPECT_FALSE(std::filesystem::exists(file)); + EXPECT_FALSE(std::filesystem::exists(dir)); + auto temp_dir = std::filesystem::temp_directory_path(); + temp_dir = temp_dir / "rcpputils" / "test_folder"; + EXPECT_FALSE(std::filesystem::exists(temp_dir)); + EXPECT_TRUE(std::filesystem::create_directories(temp_dir)); + EXPECT_TRUE(std::filesystem::exists(temp_dir)); + EXPECT_TRUE(std::filesystem::remove(temp_dir)); + EXPECT_TRUE(std::filesystem::remove(temp_dir.parent_path())); + + // Remove empty directory + EXPECT_FALSE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::create_directories(dir)); + EXPECT_TRUE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::is_directory(dir)); + EXPECT_TRUE(std::filesystem::remove_all(dir)); + EXPECT_FALSE(std::filesystem::is_directory(dir)); + + // Remove non-existing directory + EXPECT_FALSE(std::filesystem::remove_all(std::filesystem::path("some") / "nonsense" / "dir")); + + EXPECT_FALSE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::create_directories(dir)); + + // Remove single file with remove_all + file = dir / "remove_all_single_file.txt"; + { + std::ofstream output_buffer{file.string()}; + output_buffer << "some content"; + } + ASSERT_TRUE(std::filesystem::exists(file)); + ASSERT_TRUE(std::filesystem::is_regular_file(file)); + EXPECT_TRUE(std::filesystem::remove_all(file)); + + // Remove directory and its content + EXPECT_TRUE(std::filesystem::exists(dir)); + EXPECT_TRUE(std::filesystem::is_directory(dir)); + + auto num_files = 10u; + for (auto i = 0u; i < num_files; ++i) { + std::string file_name = std::string("test_file") + std::to_string(i) + ".txt"; + auto file = dir / file_name; + { + std::ofstream output_buffer{file.string()}; + output_buffer << "test" << i; + } + } + + // remove shall fail given that directory is not empty + ASSERT_FALSE(std::filesystem::remove(dir, ec)); + + EXPECT_TRUE(std::filesystem::remove_all(dir, ec)); + + for (auto i = 0u; i < num_files; ++i) { + std::string file_name = std::string("test_file") + std::to_string(i) + ".txt"; + auto file = dir / file_name; + ASSERT_FALSE(std::filesystem::exists(file)); + } + ASSERT_FALSE(std::filesystem::exists(dir)); + + // Empty path/directory cannot be created + EXPECT_FALSE(std::filesystem::create_directories(std::filesystem::path(""), ec)); +} + +TEST(TestFilesystemHelper, remove_extension) +{ + auto p = path("foo.txt"); + p = p.replace_extension(); + EXPECT_EQ("foo", p.string()); +} + +TEST(TestFilesystemHelper, remove_extensions) +{ + auto p = path("foo.txt.compress"); + p = p.replace_extension().replace_extension(); + EXPECT_EQ("foo", p.string()); +} + +TEST(TestFilesystemHelper, remove_extensions_overcount) +{ + auto p = path("foo.txt.compress"); + p = p.replace_extension().replace_extension().replace_extension().replace_extension(); + EXPECT_EQ("foo", p.string()); +} + +TEST(TestFilesystemHelper, remove_extension_no_extension) +{ + auto p = path("foo"); + p = p.replace_extension(); + EXPECT_EQ("foo", p.string()); +} + +TEST(TestFilesystemHelper, get_cwd) +{ + std::string expected_dir = rcpputils::get_env_var("EXPECTED_WORKING_DIRECTORY"); + auto p = std::filesystem::current_path(); + EXPECT_EQ(expected_dir, p.string()); +} + +TEST(TestFilesystemHelper, parent_absolute_path) +{ + std::filesystem::path path("/home/foo/bar/baz"); + ASSERT_EQ(path.string(), "/home/foo/bar/baz"); + + std::filesystem::path parent = path.parent_path(); + ASSERT_EQ(parent.string(), "/home/foo/bar"); + + std::filesystem::path grandparent = parent.parent_path(); + ASSERT_EQ(grandparent.string(), "/home/foo"); + + if (is_win32) { + std::filesystem::path win_drive_letter("C:\\home\\foo\\bar"); + ASSERT_EQ(win_drive_letter.string(), "C:\\home\\foo\\bar"); + std::filesystem::path win_parent = win_drive_letter.parent_path(); + ASSERT_EQ(win_parent.string(), "C:\\home\\foo"); + std::filesystem::path win_grandparent = win_parent.parent_path(); + ASSERT_EQ(win_grandparent.string(), "C:\\home"); + } +} + +TEST(TestFilesystemHelper, create_temporary_directory) +{ + // basic usage + { + const std::string basename = "test_base_name"; + + const auto tmpdir1 = rcpputils::fs::create_temporary_directory(basename); + EXPECT_TRUE(std::filesystem::exists(tmpdir1)); + EXPECT_TRUE(std::filesystem::is_directory(tmpdir1)); + + auto tmp_file = tmpdir1 / "test_file.txt"; + { + std::ofstream output_buffer{tmp_file.string()}; + output_buffer << "test"; + } + EXPECT_TRUE(std::filesystem::exists(tmp_file)); + EXPECT_TRUE(std::filesystem::is_regular_file(tmp_file)); + + const auto tmpdir2 = rcpputils::fs::create_temporary_directory(basename); + EXPECT_TRUE(std::filesystem::exists(tmpdir2)); + EXPECT_TRUE(std::filesystem::is_directory(tmpdir2)); + + EXPECT_NE(tmpdir1.string(), tmpdir2.string()); + + EXPECT_TRUE(std::filesystem::remove_all(tmpdir1)); + EXPECT_TRUE(std::filesystem::remove_all(tmpdir2)); + } + + // bad names + { + if (is_win32) { + EXPECT_THROW(rcpputils::fs::create_temporary_directory("illegalchar?"), std::system_error); + } else { + EXPECT_THROW(rcpputils::fs::create_temporary_directory("base/name"), std::invalid_argument); + } + } + + // newly created paths + { + const auto new_relative = std::filesystem::current_path() / "child1" / "child2"; + const auto tmpdir = rcpputils::fs::create_temporary_directory("base_name", new_relative); + EXPECT_TRUE(std::filesystem::exists(tmpdir)); + EXPECT_TRUE(std::filesystem::is_directory(tmpdir)); + EXPECT_TRUE(std::filesystem::remove_all(tmpdir)); + } + + // edge case inputs + { + // Provided no base name we should still get a path with the 6 unique template chars + const auto tmpdir_emptybase = rcpputils::fs::create_temporary_directory(""); + EXPECT_EQ(tmpdir_emptybase.filename().string().size(), 6u); + + // Empty path doesn't exist and cannot be created + auto path1 = rcpputils::fs::create_temporary_directory("basename", path()); + EXPECT_TRUE(std::filesystem::exists(path1)); + + // With the template string XXXXXX already in the name, it will still be there, the unique + // portion is appended to the end. + const auto tmpdir_template_in_name = rcpputils::fs::create_temporary_directory("base_XXXXXX"); + EXPECT_TRUE(std::filesystem::exists(tmpdir_template_in_name)); + EXPECT_TRUE(std::filesystem::is_directory(tmpdir_template_in_name)); + // On Linux, it will not replace the base_name Xs, only the final 6 that the function appends. + // On OSX, it will replace _all_ trailing Xs. + // Either way, the result is unique, the exact value doesn't matter. + EXPECT_EQ(tmpdir_template_in_name.filename().string().rfind("base_", 0), 0u); + } +} + +TEST(TestFilesystemHelper, equal_operators) +{ + path a{"foo"}; + EXPECT_EQ(a, a); + path b{"bar"}; + EXPECT_NE(a, b); + + path c = a / b; + path d = path("foo") / "bar"; + EXPECT_EQ(c, d); +}