diff --git a/include/ignition/common/TempDirectory.hh b/include/ignition/common/TempDirectory.hh new file mode 100644 index 000000000..b66c691e5 --- /dev/null +++ b/include/ignition/common/TempDirectory.hh @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Open Source Robotics Foundation + * + * 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. + * + */ + +#ifndef IGNITION_COMMON_TEMPDIRECTORY_HH_ +#define IGNITION_COMMON_TEMPDIRECTORY_HH_ + +#include +#include + +#include +#include +#include + +namespace ignition +{ + namespace common + { + class TempDirectoryPrivate; + + /// \brief Return the path to a directory suitable for temporary files. + /// + /// Calls std::filesystem::temp_directory_path, refer to the standard + /// documentation for your platform for behaviors. + /// \return A directory suitable for temporary files. + std::string IGNITION_COMMON_VISIBLE tempDirectoryPath(); + + /// \brief Create a directory in the tempDirectoryPath by expanding + /// a name template + /// + /// On execution, will create the directory: + /// "_parentPath"/"_baseName" + "XXXXXX", where XXXXXX will be filled + /// out by an OS-appropriate method (eg mkdtmp/_mktemp_s) + /// + /// \param[in] _baseName String to be prepended to the expanded template + /// \param[in] _parentPath Location to create the directory + /// \param[in] _warningOp Allow or suppress filesystem warnings + /// \return Path to newly-created temporary directory + std::string IGNITION_COMMON_VISIBLE createTempDirectory( + const std::string &_baseName, + const std::string &_parentPath, + const FilesystemWarningOp _warningOp = FSWO_LOG_WARNINGS); + + /// \class TempDirectory TempDirectory.hh ignitin/common/TempDirectory.hh + /// \brief Create a temporary directory in the OS temp location. + /// Upon construction, the current working directory will be set to this + /// new temporary directory. + /// Upon destruction, the current working directory will be restored to the + /// location when the TempDirectory object was constructed. + class IGNITION_COMMON_VISIBLE TempDirectory + { + /// \brief Create a directory in the tempDirectoryPath by expanding + /// a name template. This directory can also be automatically cleaned + /// up when the object goes out of scope. + /// + /// The TempDirectory will have the form $TMPDIR/_subdir/_prefixXXXXX/ + /// + /// \param[in] _prefix String to be expanded for the template + /// \param[in] _subDir Subdirectory in OS $TMPDIR, if desired + /// \param[in] _cleanup True to indicate that the filesystem should + /// be cleaned as part of the destructor + public: TempDirectory(const std::string &_prefix = "temp_dir", + const std::string &_subDir = "ignition", + bool _cleanup = true); + + /// \brief Destroy the temporary directory, removing from filesystem + /// if cleanup is true. + public: ~TempDirectory(); + + /// \brief Indicate if the TempDirectory object is in a valid state + /// and that the folder exists on the filesystem + /// \return true if the TempDirectory is valid + public: bool Valid() const; + + /// \brief Set if the folder on disk should be cleaned. + /// + /// This is useful if you wish to clean by default during a test, but + /// retain the contents of the TempDirectory if the test fails. + /// \param[in] _doCleanup True to indicate that the filesystem should + /// be cleaned as part of the destructor + public: void DoCleanup(bool _doCleanup); + + /// \brief Retrieve the current cleanup flag state + /// \return true if filesystem cleanup will occur + public: bool DoCleanup() const; + + /// \brief Retrieve the fully-expanded temporary directory path + /// \return the temporary directory path + public: std::string Path() const; + + /// \brief Private data pointer. + IGN_UTILS_IMPL_PTR(dataPtr) + }; + } // namespace common +} // namespace ignition +#endif // IGNITION_COMMON_TEMPDIRECTORY_HH_ + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 90e333f1e..7b2974194 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,9 @@ target_link_libraries(${PROJECT_LIBRARY_TARGET_NAME} target_include_directories(${PROJECT_LIBRARY_TARGET_NAME} PRIVATE ${ignition-math${IGN_MATH_VER}_INCLUDE_DIRS}) +target_include_directories(${PROJECT_LIBRARY_TARGET_NAME} PUBLIC + ${ignition-utils${IGN_UTILS_VER}_INCLUDE_DIRS}) + # Handle non-Windows configuration settings if(NOT WIN32) # Link the libraries that we don't expect to find on Windows diff --git a/src/Console.cc b/src/Console.cc index 0e8b1fbbb..ff013fe23 100644 --- a/src/Console.cc +++ b/src/Console.cc @@ -252,7 +252,7 @@ void FileLogger::Init(const std::string &_directory, if (isDirectory(logPath)) this->logDirectory = logPath; else - this->logDirectory = logPath.substr(0, logPath.rfind(separator(""))); + this->logDirectory = common::parentPath(logPath); this->initialized = true; diff --git a/src/Console_TEST.cc b/src/Console_TEST.cc index 2cc3805fd..28b05668c 100644 --- a/src/Console_TEST.cc +++ b/src/Console_TEST.cc @@ -33,23 +33,21 @@ const int g_messageRepeat = 4; class Console_TEST : public ::testing::Test { protected: virtual void SetUp() { - // Set IGN_HOMEDIR and store it - common::testing::TestSetHomePath(this->logBasePath); + this->temp = std::make_unique( + "test", "ign_common", true); + ASSERT_TRUE(this->temp->Valid()); + common::setenv(IGN_HOMEDIR, this->temp->Path()); } /// \brief Clear out all the directories we produced during this test. - public: virtual ~Console_TEST() + public: virtual void TearDown() { + ignLogClose(); EXPECT_TRUE(ignition::common::unsetenv(IGN_HOMEDIR)); - - if (ignition::common::isDirectory(this->logBasePath)) - { - ignLogClose(); - EXPECT_TRUE(ignition::common::removeAll(this->logBasePath)); - } } - private: std::string logBasePath; + /// \brief Temporary directory to run test in + private: std::unique_ptr temp; }; std::string GetLogContent(const std::string &_filename) @@ -58,6 +56,7 @@ std::string GetLogContent(const std::string &_filename) std::string path; EXPECT_TRUE(ignition::common::env(IGN_HOMEDIR, path)); path = ignition::common::joinPaths(path, _filename); + EXPECT_TRUE(ignition::common::exists(path)); // Open the log file, and read back the string std::ifstream ifs(path.c_str(), std::ios::in); diff --git a/src/SystemPaths_TEST.cc b/src/SystemPaths_TEST.cc index f20878fb0..a17950baa 100644 --- a/src/SystemPaths_TEST.cc +++ b/src/SystemPaths_TEST.cc @@ -26,6 +26,7 @@ #include "ignition/common/Util.hh" #include "ignition/common/StringUtils.hh" #include "ignition/common/SystemPaths.hh" +#include "ignition/common/TempDirectory.hh" #ifdef _WIN32 #define snprintf _snprintf @@ -36,11 +37,22 @@ using namespace ignition; const char kPluginPath[] = "IGN_PLUGIN_PATH"; const char kFilePath[] = "IGN_FILE_PATH"; +class TestTempDirectory : public ignition::common::TempDirectory +{ + public: TestTempDirectory(): + ignition::common::TempDirectory("systempaths", "ign_common", true) + { + } +}; + class SystemPathsFixture : public ::testing::Test { // Documentation inherited public: virtual void SetUp() { + this->temp = std::make_unique(); + ASSERT_TRUE(this->temp->Valid()); + common::env(kPluginPath, this->backupPluginPath); common::unsetenv(kPluginPath); @@ -59,6 +71,7 @@ class SystemPathsFixture : public ::testing::Test { common::setenv(kPluginPath, this->backupPluginPath); common::setenv(kFilePath, this->backupFilePath); + this->temp.reset(); } /// \brief Backup of plugin paths to be restored after the test @@ -69,6 +82,9 @@ class SystemPathsFixture : public ::testing::Test /// \brief Root of filesystem according to each platform public: std::string filesystemRoot; + + /// \brief Temporary directory to execute test in + public: std::unique_ptr temp; }; std::string SystemPathsJoin(const std::vector &_paths) diff --git a/src/TempDirectory.cc b/src/TempDirectory.cc new file mode 100644 index 000000000..8e44b38fc --- /dev/null +++ b/src/TempDirectory.cc @@ -0,0 +1,229 @@ +/* + * Copyright 2021 Open Source Robotics Foundation + * + * 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 + +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +using namespace ignition; +using namespace common; + +///////////////////////////////////////////////// +// Return true if success, false if error +inline bool fs_warn(const std::string &_fcn, + const std::error_code &_ec, + const FilesystemWarningOp &_warningOp = FSWO_LOG_WARNINGS) +{ + if (_ec) + { + if (FSWO_LOG_WARNINGS == _warningOp) + { + ignwarn << "Failed ignition::common::" << _fcn + << " (ec: " << _ec << " " << _ec.message() << ")\n"; + } + return false; + } + return true; +} + +///////////////////////////////////////////////// +// Helper implementation of std::filesystem::temp_directory_path +// https://en.cppreference.com/w/cpp/filesystem/temp_directory_path +// \TODO(anyone) remove when using `std::filesystem` in C++17 and greater. +std::string temp_directory_path(std::error_code& _err) +{ + _err = std::error_code(); +#ifdef _WIN32 + TCHAR temp_path[MAX_PATH]; + DWORD size = GetTempPathA(MAX_PATH, temp_path); + if (size > MAX_PATH || size == 0) { + _err = std::error_code( + static_cast(GetLastError()), std::system_category()); + } + temp_path[size] = '\0'; +#else + std::string temp_path; + if(!ignition::common::env("TMPDIR", temp_path)) + { + temp_path = "/tmp"; + } +#endif + return std::string(temp_path); +} + +///////////////////////////////////////////////// +std::string ignition::common::tempDirectoryPath() +{ + std::error_code ec; + auto ret = temp_directory_path(ec); + + if (!fs_warn("tempDirectoryPath", ec)) + { + ret = ""; + } + + return ret; +} + +///////////////////////////////////////////////// +/// \brief Internal method for createTempDirectory +/// +/// This is primarily to scope the "throw" behavior from when this +/// was copied from rclcpp. +std::string createTempDirectory( + const std::string &_baseName, + const std::string &_parentPath) +{ + std::string parentPath(_parentPath); + std::string templatePath = _baseName + "XXXXXX"; + + std::string fullTemplateStr = joinPaths(parentPath, templatePath); + if (!createDirectories(parentPath)) + { + std::error_code ec{errno, std::system_category()}; + errno = 0; + throw std::system_error(ec, "could not create the parent directory"); + } + +#ifdef _WIN32 + errno_t errcode = _mktemp_s(&fullTemplateStr[0], fullTemplateStr.size() + 1); + if (errcode) { + std::error_code ec(static_cast(errcode), std::system_category()); + throw std::system_error(ec, + "could not format the temp directory name template"); + } + const std::string finalPath{fullTemplateStr}; + if (!createDirectories(finalPath)) { + std::error_code ec(static_cast(GetLastError()), + std::system_category()); + throw std::system_error(ec, "could not create the temp directory"); + } +#else + const char * dirName = mkdtemp(&fullTemplateStr[0]); + if (dirName == nullptr) { + std::error_code ec{errno, std::system_category()}; + errno = 0; + throw std::system_error(ec, + "could not format or create the temp directory"); + } + const std::string finalPath{dirName}; +#endif + + return finalPath; +} + +///////////////////////////////////////////////// +std::string ignition::common::createTempDirectory( + const std::string &_baseName, + const std::string &_parentPath, + const FilesystemWarningOp _warningOp) +{ + std::string ret; + try { + ret = ::createTempDirectory(_baseName, _parentPath); + } + catch (const std::system_error &ex) + { + ret = ""; + if(FSWO_LOG_WARNINGS == _warningOp) + { + ignwarn << "Failed to create temp directory: " << ex.what() << "\n"; + } + } + return ret; +} + + +class ignition::common::TempDirectory::Implementation +{ + /// \brief Current working directory before creation of temporary dir. + public: std::string oldPath {""}; + + /// \brief Path of the temporary directory + public: std::string path {""}; + + /// \brief True if the temporary directory exists + public: bool isValid {false}; + + /// \brief True if the temporary directory should be cleaned up from + /// disk when the object goes out of scope. + public: bool doCleanup {true}; +}; + +///////////////////////////////////////////////// +TempDirectory::TempDirectory(const std::string &_prefix, + const std::string &_subDir, + bool _cleanup): + dataPtr(ignition::utils::MakeImpl()) +{ + + this->dataPtr->oldPath = common::cwd(); + this->dataPtr->doCleanup = _cleanup; + + auto tempPath = common::tempDirectoryPath(); + if (!_subDir.empty()) + { + tempPath = common::joinPaths(tempPath, _subDir); + } + this->dataPtr->path = common::createTempDirectory(_prefix, tempPath); + if (!this->dataPtr->path.empty()) + { + this->dataPtr->isValid = true; + common::chdir(this->dataPtr->path); + } +} + +///////////////////////////////////////////////// +TempDirectory::~TempDirectory() +{ + common::chdir(this->dataPtr->oldPath); + if (this->dataPtr->isValid && this->dataPtr->doCleanup) + { + common::removeAll(this->dataPtr->path); + } +} + +///////////////////////////////////////////////// +bool TempDirectory::Valid() const +{ + return this->dataPtr->isValid; +} + +///////////////////////////////////////////////// +void TempDirectory::DoCleanup(bool _doCleanup) +{ + this->dataPtr->doCleanup = _doCleanup; +} + +///////////////////////////////////////////////// +bool TempDirectory::DoCleanup() const +{ + return this->dataPtr->doCleanup; +} + +///////////////////////////////////////////////// +std::string TempDirectory::Path() const +{ + return this->dataPtr->path; +} diff --git a/src/TempDirectory_TEST.cc b/src/TempDirectory_TEST.cc new file mode 100644 index 000000000..a0c3ad7bb --- /dev/null +++ b/src/TempDirectory_TEST.cc @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * 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 + +///////////////////////////////////////////////// +TEST(TempDirectory, tempDirectoryPath) +{ + // OS TMPDIR should never be empty + ASSERT_FALSE(ignition::common::tempDirectoryPath().empty()); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, createTempDirectory) +{ + // OS TMPDIR should never be empty + auto tmpDir = ignition::common::tempDirectoryPath(); + ASSERT_FALSE(tmpDir.empty()); + + // Nominal case + // eg /tmp/fooXXXXXX + auto tmp = ignition::common::createTempDirectory("foo", tmpDir); + ASSERT_FALSE(tmp.empty()); + EXPECT_NE(std::string::npos, tmp.find("foo")); + EXPECT_NE(std::string::npos, tmp.find(tmpDir)); + + // Should also work for subdirectories + // eg /tmp/bar/fooXXXXXX + tmpDir = ignition::common::joinPaths(tmpDir, "bar"); + auto tmp2 = ignition::common::createTempDirectory("bar", tmpDir); + ASSERT_FALSE(tmp2.empty()); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, createTempDirectoryEmptyBase) +{ + // OS TMPDIR should never be empty + auto tmpDir = ignition::common::tempDirectoryPath(); + ASSERT_FALSE(tmpDir.empty()); + + // Create with an empty basename (eg /tmp/XXXXXX) + auto tmp = ignition::common::createTempDirectory("", tmpDir); + ASSERT_FALSE(tmp.empty()); + EXPECT_EQ(std::string::npos, tmp.find("foo")); + EXPECT_NE(std::string::npos, tmp.find(tmpDir)); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, TempDirectory) +{ + std::string path; + { + ignition::common::TempDirectory tmp("temp_dir", "ignition", true); + EXPECT_TRUE(tmp.Valid()); + EXPECT_TRUE(tmp.DoCleanup()); + path = tmp.Path(); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(ignition::common::exists(path)); + } + EXPECT_FALSE(ignition::common::exists(path)); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, TempDirectoryNoClean) +{ + std::string path; + { + ignition::common::TempDirectory tmp("temp_dir", "ignition", false); + EXPECT_TRUE(tmp.Valid()); + EXPECT_FALSE(tmp.DoCleanup()); + path = tmp.Path(); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(ignition::common::exists(path)); + } + EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_TRUE(ignition::common::removeDirectory(path)); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, TempDirectoryNoCleanLater) +{ + std::string path; + { + ignition::common::TempDirectory tmp("temp_dir", "ignition", true); + EXPECT_TRUE(tmp.Valid()); + EXPECT_TRUE(tmp.DoCleanup()); + path = tmp.Path(); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(ignition::common::exists(path)); + tmp.DoCleanup(false); + EXPECT_FALSE(tmp.DoCleanup()); + } + EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_TRUE(ignition::common::removeDirectory(path)); +} + diff --git a/test/test_config.h.in b/test/test_config.h.in index 0a4616552..aea2bf931 100644 --- a/test/test_config.h.in +++ b/test/test_config.h.in @@ -13,6 +13,7 @@ #include #include "ignition/common/Console.hh" #include "ignition/common/Filesystem.hh" +#include "ignition/common/TempDirectory.hh" #include "ignition/common/Util.hh" #define PROJECT_BINARY_PATH "${PROJECT_BINARY_DIR}" @@ -59,37 +60,9 @@ namespace ignition } else { - _tmpDir = common::joinPaths("${PROJECT_BINARY_DIR}", "tmp"); - return true; - } - } - - /// \brief Method to retrieve temporary home directory for tests - /// - /// This will update the contents of the home directory path variable - /// (HOME on Linux/MacOS, HOMEPATH on Windows) to this newly-set - /// directory - /// This additionally sets the HOME and HOMEPATH environment variables - /// - /// \param[inout] _homeDir Full path to the home directory - /// \return True if directory is set correctly, false otherwise - bool TestSetHomePath(std::string &_homeDir) - { - if (common::env("TEST_UNDECLARED_OUTPUTS_DIR", _homeDir)) - { - return ignition::common::setenv(IGN_HOMEDIR, _homeDir); - } - else - { - if (TestTmpPath(_homeDir)) - { - // Set both for linux and windows - return ignition::common::setenv(IGN_HOMEDIR, _homeDir); - } - else - { - return false; - } + _tmpDir = common::createTempDirectory("ignition", + common::tempDirectoryPath()); + return !_tmpDir.empty(); } } @@ -143,11 +116,14 @@ namespace ignition std::string testCaseName = testInfo->test_case_name(); this->logFilename = testCaseName + "_" + testName + ".log"; - common::testing::TestSetHomePath(this->logBasePath); + this->temp = std::make_unique( + "test", "ign_common", true); + ASSERT_TRUE(this->temp->Valid()); + common::setenv(IGN_HOMEDIR, this->temp->Path()); // Initialize Console - ignLogInit(common::joinPaths(this->logBasePath, "test_logs"), - this->logFilename); + ignLogInit(common::joinPaths(this->temp->Path(), "test_logs"), + this->logFilename); ignition::common::Console::SetVerbosity(4); @@ -184,7 +160,7 @@ namespace ignition public: virtual ~AutoLogFixture() { ignLogClose(); - ignition::common::removeAll(this->logBasePath); + EXPECT_TRUE(ignition::common::unsetenv(IGN_HOMEDIR)); } /// \brief String with the full path of the logfile @@ -195,6 +171,9 @@ namespace ignition /// \brief String with the base path to log directory private: std::string logBasePath; + + /// \brief Temporary directory to run test in + private: std::unique_ptr temp; }; } // namespace testing } // namespace common