From 96df813adb979faf1406bc016db0a0e14b336a1c Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Mon, 11 Apr 2022 07:32:26 -0500 Subject: [PATCH] Add common::testing module (#314) This introduces ignition::common::testing, a component to hold all of our testing support functionality. Signed-off-by: Michael Carroll --- CMakeLists.txt | 2 +- av/src/AudioDecoder.cc | 5 +- events/src/Event_TEST.cc | 1 + include/ignition/common/TempDirectory.hh | 16 ++ src/TempDirectory.cc | 22 +- src/TempDirectory_TEST.cc | 56 +++++ test/integration/video_encoder.cc | 1 + .../include/ignition/common/CMakeLists.txt | 1 + .../ignition/common/testing/AutoLogFixture.hh | 59 +++++ .../ignition/common/testing/BazelTestPaths.hh | 58 +++++ .../ignition/common/testing/CMakeTestPaths.hh | 48 +++++ .../ignition/common/testing/TestPaths.hh | 201 ++++++++++++++++++ .../include/ignition/common/testing/Utils.hh | 40 ++++ .../common/testing/detail/AutoLogFixture.hh | 120 +++++++++++ testing/src/AutoLogFixture_TEST.cc | 34 +++ testing/src/BazelTestPaths.cc | 55 +++++ testing/src/BazelTestPaths_TEST.cc | 69 ++++++ testing/src/CMakeLists.txt | 17 ++ testing/src/CMakeTestPaths.cc | 46 ++++ testing/src/CMakeTestPaths_TEST.cc | 98 +++++++++ testing/src/TestPaths.cc | 101 +++++++++ testing/src/Utils.cc | 53 +++++ testing/src/Utils_TEST.cc | 38 ++++ testing/test_files/example.txt | 0 24 files changed, 1135 insertions(+), 6 deletions(-) create mode 100644 testing/include/ignition/common/CMakeLists.txt create mode 100644 testing/include/ignition/common/testing/AutoLogFixture.hh create mode 100644 testing/include/ignition/common/testing/BazelTestPaths.hh create mode 100644 testing/include/ignition/common/testing/CMakeTestPaths.hh create mode 100644 testing/include/ignition/common/testing/TestPaths.hh create mode 100644 testing/include/ignition/common/testing/Utils.hh create mode 100644 testing/include/ignition/common/testing/detail/AutoLogFixture.hh create mode 100644 testing/src/AutoLogFixture_TEST.cc create mode 100644 testing/src/BazelTestPaths.cc create mode 100644 testing/src/BazelTestPaths_TEST.cc create mode 100644 testing/src/CMakeLists.txt create mode 100644 testing/src/CMakeTestPaths.cc create mode 100644 testing/src/CMakeTestPaths_TEST.cc create mode 100644 testing/src/TestPaths.cc create mode 100644 testing/src/Utils.cc create mode 100644 testing/src/Utils_TEST.cc create mode 100644 testing/test_files/example.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index a9d507c8e..4bbaba937 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,7 +132,7 @@ configure_file("${PROJECT_SOURCE_DIR}/cppcheck.suppress.in" ${PROJECT_BINARY_DIR}/cppcheck.suppress) ign_configure_build(QUIT_IF_BUILD_ERRORS - COMPONENTS av events geospatial graphics profiler) + COMPONENTS av events geospatial graphics profiler testing) #============================================================================ # Create package information diff --git a/av/src/AudioDecoder.cc b/av/src/AudioDecoder.cc index 561ce4544..4ff2b237f 100644 --- a/av/src/AudioDecoder.cc +++ b/av/src/AudioDecoder.cc @@ -325,7 +325,8 @@ bool AudioDecoder::SetFile(const std::string &_filename) avcodec_parameters_to_context(this->dataPtr->codecCtx, this->dataPtr->formatCtx->streams[this->dataPtr->audioStream]->codecpar); #else - this->dataPtr->codec = avcodec_find_decoder(this->dataPtr->codecCtx->codec_id); + this->dataPtr->codec = avcodec_find_decoder( + this->dataPtr->codecCtx->codec_id); #endif if (this->dataPtr->codec == nullptr) @@ -346,7 +347,7 @@ bool AudioDecoder::SetFile(const std::string &_filename) #endif // Open codec - if (avcodec_open2(this->dataPtr->codecCtx, + if (avcodec_open2(this->dataPtr->codecCtx, this->dataPtr->codec, nullptr) < 0) { ignerr << "Couldn't open audio codec.\n"; diff --git a/events/src/Event_TEST.cc b/events/src/Event_TEST.cc index 4a94d5450..34cb76cbe 100644 --- a/events/src/Event_TEST.cc +++ b/events/src/Event_TEST.cc @@ -21,6 +21,7 @@ #include #include +#include using namespace ignition; diff --git a/include/ignition/common/TempDirectory.hh b/include/ignition/common/TempDirectory.hh index a23c3eab3..259acdf1f 100644 --- a/include/ignition/common/TempDirectory.hh +++ b/include/ignition/common/TempDirectory.hh @@ -74,6 +74,22 @@ namespace ignition const std::string &_subDir = "ignition", bool _cleanup = true); + /// \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 _root/_subdir/_prefixXXXXX/ + /// + /// \param[in[ _root Explicitly set the root directory + /// \param[in] _prefix String to be expanded for the template + /// \param[in] _subDir Subdirectory in OS _root, if desired + /// \param[in] _cleanup True to indicate that the filesystem should + /// be cleaned as part of the destructor + public: explicit TempDirectory(const std::string &_root, + 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(); diff --git a/src/TempDirectory.cc b/src/TempDirectory.cc index cf65b3a64..7f3809b30 100644 --- a/src/TempDirectory.cc +++ b/src/TempDirectory.cc @@ -161,16 +161,31 @@ class ignition::common::TempDirectory::Implementation TempDirectory::TempDirectory(const std::string &_prefix, const std::string &_subDir, bool _cleanup): - dataPtr(ignition::utils::MakeUniqueImpl()) + TempDirectory(common::tempDirectoryPath(), _prefix, _subDir, _cleanup) { +} +///////////////////////////////////////////////// +TempDirectory::TempDirectory(const std::string &_root, + const std::string &_prefix, + const std::string &_subDir, + bool _cleanup): + dataPtr(ignition::utils::MakeUniqueImpl()) +{ this->dataPtr->oldPath = common::cwd(); this->dataPtr->doCleanup = _cleanup; - auto tempPath = common::tempDirectoryPath(); + if (_root.empty()) + { + this->dataPtr->isValid = false; + ignwarn << "Failed to create temp directory: _root was empty\n"; + return; + } + + auto tempPath = _root; if (!_subDir.empty()) { - tempPath = common::joinPaths(tempPath, _subDir); + tempPath = common::joinPaths(_root, _subDir); } this->dataPtr->path = common::createTempDirectory(_prefix, tempPath); if (!this->dataPtr->path.empty()) @@ -181,6 +196,7 @@ TempDirectory::TempDirectory(const std::string &_prefix, this->dataPtr->path = common::cwd(); } + ///////////////////////////////////////////////// TempDirectory::~TempDirectory() { diff --git a/src/TempDirectory_TEST.cc b/src/TempDirectory_TEST.cc index 14af3aff5..4fde626d6 100644 --- a/src/TempDirectory_TEST.cc +++ b/src/TempDirectory_TEST.cc @@ -66,14 +66,60 @@ TEST(TempDirectory, createTempDirectoryEmptyBase) TEST(TempDirectory, TempDirectory) { std::string path; + auto curDir = ignition::common::cwd(); { ignition::common::TempDirectory tmp("temp_dir", "ignition", true); EXPECT_TRUE(tmp.Valid()); EXPECT_TRUE(tmp.DoCleanup()); + // Current directory changed to tempdir + EXPECT_NE(curDir, ignition::common::cwd()); path = tmp.Path(); EXPECT_FALSE(path.empty()); EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_EQ(path, ignition::common::cwd()); } + // Current directory changed back to previous + EXPECT_EQ(curDir, ignition::common::cwd()); + EXPECT_FALSE(ignition::common::exists(path)); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, TempDirectoryRoot) +{ + std::string path; + auto curDir = ignition::common::cwd(); + { + auto p = ignition::common::tempDirectoryPath(); + ignition::common::TempDirectory tmp(p, "temp_dir", "ignition", true); + EXPECT_TRUE(tmp.Valid()); + EXPECT_TRUE(tmp.DoCleanup()); + EXPECT_NE(curDir, ignition::common::cwd()); + path = tmp.Path(); + EXPECT_FALSE(path.empty()); + EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_EQ(path, ignition::common::cwd()); + } + // Current directory changed back to previous + EXPECT_EQ(curDir, ignition::common::cwd()); + EXPECT_FALSE(ignition::common::exists(path)); +} + +///////////////////////////////////////////////// +TEST(TempDirectory, TempDirectoryEmptyRoot) +{ + std::string path; + auto curDir = ignition::common::cwd(); + { + ignition::common::TempDirectory tmp("", "temp_dir", "ignition", true); + EXPECT_FALSE(tmp.Valid()); + EXPECT_TRUE(tmp.DoCleanup()); + // Since not successfully created, no update + EXPECT_EQ(curDir, ignition::common::cwd()); + path = tmp.Path(); + EXPECT_TRUE(path.empty()); + EXPECT_FALSE(ignition::common::exists(path)); + } + EXPECT_EQ(curDir, ignition::common::cwd()); EXPECT_FALSE(ignition::common::exists(path)); } @@ -81,14 +127,19 @@ TEST(TempDirectory, TempDirectory) TEST(TempDirectory, TempDirectoryNoClean) { std::string path; + auto curDir = ignition::common::cwd(); { ignition::common::TempDirectory tmp("temp_dir", "ignition", false); EXPECT_TRUE(tmp.Valid()); EXPECT_FALSE(tmp.DoCleanup()); + EXPECT_NE(curDir, ignition::common::cwd()); path = tmp.Path(); EXPECT_FALSE(path.empty()); EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_EQ(path, ignition::common::cwd()); } + // Current directory always changes back, regardless of doClean + EXPECT_EQ(curDir, ignition::common::cwd()); EXPECT_TRUE(ignition::common::exists(path)); EXPECT_TRUE(ignition::common::removeDirectory(path)); } @@ -97,16 +148,21 @@ TEST(TempDirectory, TempDirectoryNoClean) TEST(TempDirectory, TempDirectoryNoCleanLater) { std::string path; + auto curDir = ignition::common::cwd(); { ignition::common::TempDirectory tmp("temp_dir", "ignition", true); EXPECT_TRUE(tmp.Valid()); EXPECT_TRUE(tmp.DoCleanup()); + EXPECT_NE(curDir, ignition::common::cwd()); path = tmp.Path(); EXPECT_FALSE(path.empty()); EXPECT_TRUE(ignition::common::exists(path)); + EXPECT_EQ(path, ignition::common::cwd()); tmp.DoCleanup(false); EXPECT_FALSE(tmp.DoCleanup()); } + // Current directory always changes back, regardless of doClean + EXPECT_EQ(curDir, ignition::common::cwd()); EXPECT_TRUE(ignition::common::exists(path)); EXPECT_TRUE(ignition::common::removeDirectory(path)); } diff --git a/test/integration/video_encoder.cc b/test/integration/video_encoder.cc index 922ea1c58..4ebe8dd8d 100644 --- a/test/integration/video_encoder.cc +++ b/test/integration/video_encoder.cc @@ -18,6 +18,7 @@ // needed on MacOS #include +#include "ignition/common/Console.hh" #include "ignition/common/VideoEncoder.hh" #include "ignition/common/Video.hh" #include "ignition/common/ffmpeg_inc.hh" diff --git a/testing/include/ignition/common/CMakeLists.txt b/testing/include/ignition/common/CMakeLists.txt new file mode 100644 index 000000000..027bd91ff --- /dev/null +++ b/testing/include/ignition/common/CMakeLists.txt @@ -0,0 +1 @@ +ign_install_all_headers(COMPONENT testing) diff --git a/testing/include/ignition/common/testing/AutoLogFixture.hh b/testing/include/ignition/common/testing/AutoLogFixture.hh new file mode 100644 index 000000000..c685723f9 --- /dev/null +++ b/testing/include/ignition/common/testing/AutoLogFixture.hh @@ -0,0 +1,59 @@ +/* +* Copyright (C) 2022 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_TESTING_AUTOLOGFIXTURE_HH_ +#define IGNITION_COMMON_TESTING_AUTOLOGFIXTURE_HH_ + +#include + +#include +#include + +#include + +namespace ignition::common::testing +{ +/// \brief A utility class that stores test logs in ~/.ignition/test_logs. +/// This functionality is needed to keep all the log information reported +/// by ignition during continuous integration. Without this, debugging +/// failing tests is significantly more difficult. +class AutoLogFixture : public ::testing::Test +{ + /// \brief Constructor + public: AutoLogFixture(); + + /// \brief Destructor + public: virtual ~AutoLogFixture(); + + /// \brief Setup the test fixture. This gets called by gtest. + protected: virtual void SetUp() override; + + /// \brief Get a string with the full log file path. + /// \return The full log file path as a string. + protected: std::string FullLogPath() const; + + /// \brief Get a string with all the log content loaded from the disk. + /// \return A string with all the log content. + protected: std::string LogContent() const; + + /// \brief Pointer to private data. + IGN_UTILS_UNIQUE_IMPL_PTR(dataPtr) +}; +} // namespace ignition::common::testing + +#include + +#endif // IGNITION_COMMON_TESTING_AUTOLOGFIXTURE_HH_ diff --git a/testing/include/ignition/common/testing/BazelTestPaths.hh b/testing/include/ignition/common/testing/BazelTestPaths.hh new file mode 100644 index 000000000..b8927a86f --- /dev/null +++ b/testing/include/ignition/common/testing/BazelTestPaths.hh @@ -0,0 +1,58 @@ +/* +* Copyright (C) 2022 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_TESTING_BAZELTESTPATHS_HH_ +#define IGNITION_COMMON_TESTING_BAZELTESTPATHS_HH_ + +#include + +#include "ignition/common/testing/TestPaths.hh" +#include "ignition/common/testing/Export.hh" + +namespace ignition::common::testing +{ + +/// \brief Implementation of TestPaths interface for Bazel +/// +/// It is not intended that users will directly construct this, but rather +/// utilize the TestPathFactory. +/// +/// The main mechanism for detecting a bazel build is via the presence of the +/// TEST_SRCDIR and TEST_UNDECLARED_OUTPUTS_DIR environment variables. +/// Additionally, tests built via ign-bazel should set IGN_BAZEL_DIR. +/// +/// For source files to be available for bazel builds, they need to be set in +/// the "data" section of the relevant cc_library or cc_test call. +class BazelTestPaths: public TestPaths +{ + /// \brief Constructor from TestPaths + public: using TestPaths::TestPaths; + + /// \brief Destructor + public: IGNITION_COMMON_TESTING_VISIBLE ~BazelTestPaths() override; + + /// Documentation inherited + public: bool IGNITION_COMMON_TESTING_VISIBLE + ProjectSourcePath(std::string &_sourceDir) override; + + /// Documentation inherited + public: bool IGNITION_COMMON_TESTING_VISIBLE + TestTmpPath(std::string &_tmpDir) override; +}; + +} // namespace ignition::common::testing + +#endif // IGNITION_COMMON_TESTING_AUTOLOGFIXTURE_HH_ diff --git a/testing/include/ignition/common/testing/CMakeTestPaths.hh b/testing/include/ignition/common/testing/CMakeTestPaths.hh new file mode 100644 index 000000000..9b18dd10c --- /dev/null +++ b/testing/include/ignition/common/testing/CMakeTestPaths.hh @@ -0,0 +1,48 @@ +/* +* Copyright (C) 2022 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_TESTING_CMAKETESTPATHS_HH_ +#define IGNITION_COMMON_TESTING_CMAKETESTPATHS_HH_ + +#include + +#include "ignition/common/testing/TestPaths.hh" +#include "ignition/common/testing/Export.hh" + +namespace ignition::common::testing +{ +/// \brief Implementation of TestPaths interface for CMake +/// +/// It is not intended that users will directly construct this, but rather +/// utilize the TestPathFactory. +class CMakeTestPaths: public TestPaths +{ + /// \brief Constructor from TestPaths + public: using TestPaths::TestPaths; + + /// \brief Destructor + public: IGNITION_COMMON_TESTING_VISIBLE ~CMakeTestPaths() override; + + /// Documentation inherited + public: bool IGNITION_COMMON_TESTING_VISIBLE + ProjectSourcePath(std::string &_sourceDir) override; + + /// Documentation inherited + public: bool IGNITION_COMMON_TESTING_VISIBLE + TestTmpPath(std::string &_tmpDir) override; +}; +} // namespace ignition::common::testing +#endif // IGNITION_COMMON_TESTING_CMAKETESTPATHS_HH_ diff --git a/testing/include/ignition/common/testing/TestPaths.hh b/testing/include/ignition/common/testing/TestPaths.hh new file mode 100644 index 000000000..da20fb214 --- /dev/null +++ b/testing/include/ignition/common/testing/TestPaths.hh @@ -0,0 +1,201 @@ +/* +* Copyright (C) 2022 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_TESTING_TESTPATHS_HH_ +#define IGNITION_COMMON_TESTING_TESTPATHS_HH_ + +#include +#include + +#include "ignition/common/Filesystem.hh" +#include "ignition/common/TempDirectory.hh" +#include "ignition/common/Util.hh" + +#include "ignition/common/testing/Export.hh" + +#ifndef TESTING_PROJECT_SOURCE_DIR +#define TESTING_PROJECT_SOURCE_DIR "" +#endif + +namespace ignition::common::testing +{ + +////////////////////////////////////////////////// +/// \brief Constant referring to the project source dir of the current +/// project. +/// +/// For CMake builds, it is expected that this is injected via a +/// buildsystem define of TESTING_PROJECT_SOURCE_DIR. +/// This is done automatically for tests using ign-cmake's ign_build_tests +/// +/// For Bazel builds, it is expected to be empty +constexpr char kTestingProjectSourceDir[] = TESTING_PROJECT_SOURCE_DIR; + +////////////////////////////////////////////////// +/// \brief List of buildsystem types +enum class IGNITION_COMMON_TESTING_VISIBLE BuildType +{ + kUnknown, + kCMake, + kBazel +}; + +////////////////////////////////////////////////// +/// \brief Helper interface to generate path information to support +/// test access to source/data files +/// +/// It is intended that there is an implementation of this interface for +/// each relevant buildsystem. +class TestPaths +{ + /// \brief Constructor + /// \param[in] _projectSourcePath Path to the root of the project source + public: IGNITION_COMMON_TESTING_VISIBLE + explicit TestPaths(const std::string &_projectSourcePath = + kTestingProjectSourceDir); + + /// \brief Destructor + public: IGNITION_COMMON_TESTING_VISIBLE + virtual ~TestPaths() = 0; + + /// brief Populate the path to the root project source directory + /// \param[out] _sourceDir path to the root project source directory + /// \return True if path successfully found and set, false otherwise + public: virtual bool IGNITION_COMMON_TESTING_VISIBLE + ProjectSourcePath(std::string &_sourceDir) = 0; + + /// \brief Populate the path to a temporary directory + /// \param[out] _tmpDir path to the root temporary directory + /// \return True if path successfully found and set, false otherwise + public: virtual bool IGNITION_COMMON_TESTING_VISIBLE + TestTmpPath(std::string &_tmpDir) = 0; + + /// \brief Path to the root of the project source + protected: std::string projectSourcePath; +}; + +////////////////////////////////////////////////// +/// \brief Implementation of MakeTestTempDirectory +/// +/// The TempDirectory will have the form $TMPDIR/_subdir/_prefixXXXXX/ +/// +/// \param[in] _projectSourcePath Root of project source or empty +/// \param[in] _prefix Prefix of the temporary directory +/// \param[in] _subDir Additional subdirectory for temporary directory +/// \param[in] _cleanup True to indicate that the filesystem should +/// be cleaned as part of the destructor +/// \return Shared pointer to TempDirectory +std::shared_ptr +IGNITION_COMMON_TESTING_VISIBLE +MakeTestTempDirectoryImpl(const std::string &_projectSourcePath, + const std::string &_prefix = "test", + const std::string &_subDir = "ignition", + bool _cleanup = true); + + +////////////////////////////////////////////////// +/// \brief Create a temporary directory for test output in an OS and build +/// appropriate location +/// +/// The TempDirectory will have the form $TMPDIR/_subdir/_prefixXXXXX/ +/// +/// \param[in] _prefix Prefix of the temporary directory +/// \param[in] _subDir Additional subdirectory for temporary directory +/// \param[in] _cleanup True to indicate that the filesystem should +/// be cleaned as part of the destructor +/// \return Shared pointer to TempDirectory +inline std::shared_ptr +MakeTestTempDirectory(const std::string &_prefix = "test", + const std::string &_subDir = "ignition", + bool _cleanup = true) +{ + return MakeTestTempDirectoryImpl(kTestingProjectSourceDir, + _prefix, + _subDir, + _cleanup); +} + +////////////////////////////////////////////////// +/// \brief Return the current build type +/// +/// \param[in] _projectSourcePath Root of project source or empty +/// \return The current build type +BuildType +IGNITION_COMMON_TESTING_VISIBLE +TestBuildType( + const std::string &_projectSourcePath = kTestingProjectSourceDir); + +////////////////////////////////////////////////// +/// \brief Get a TestPaths object for the current build type +/// +/// \param[in] _projectSourcePath Root of project source or empty +/// \return TestPaths implementation for the current build type +std::unique_ptr +IGNITION_COMMON_TESTING_VISIBLE +TestPathFactory( + const std::string &_projectSourcePath = kTestingProjectSourceDir); + +////////////////////////////////////////////////// +/// \brief Get the path to a file in the project source tree +/// +/// Example: to get ign-common/test/data/box.dae +/// SourceFile("test", "data", "box.dae"); +/// +/// \param[in] args Relative path to the source file +/// \return Full path to the source file +template +std::string SourceFile(Args const &... args) +{ + auto testPaths = TestPathFactory(kTestingProjectSourceDir); + assert(nullptr != testPaths); + + std::string dataDir; + testPaths->ProjectSourcePath(dataDir); + return common::joinPaths(dataDir, args...); +} + +////////////////////////////////////////////////// +/// \brief Get the path to a file in the project test directory tree +/// +/// Example: to get ign-common/test/data/box.dae +/// TestFile("data", "box.dae"); +/// +/// \param[in] args Path to the file, relative to the test directory +/// \return Full path to the source file +template +std::string TestFile(Args const &... args) +{ + return SourceFile("test", args...); +} + +////////////////////////////////////////////////// +/// \brief Get the path to a file in a temporary directory +/// +/// Example: to get ${TMP}/.ignition/foo.log +/// TempPath(".ignition", "foo.log"); +/// +/// \param[in] args Path to the file, relative to the temporary directory +/// \return Full path to the temporary directory +template +std::string TempPath(Args const &... args) +{ + auto testPaths = TestPathFactory(kTestingProjectSourceDir); + std::string dataDir; + testPaths->TestTmpPath(dataDir); + return common::joinPaths(dataDir, args...); +} +} // namespace ignition::common::testing +#endif // IGNITION_COMMON_TESTING_TESTPATHS_HH_ diff --git a/testing/include/ignition/common/testing/Utils.hh b/testing/include/ignition/common/testing/Utils.hh new file mode 100644 index 000000000..a4ae24c1a --- /dev/null +++ b/testing/include/ignition/common/testing/Utils.hh @@ -0,0 +1,40 @@ +/* +* Copyright (C) 2022 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_TESTING_UTILS_HH_ +#define IGNITION_COMMON_TESTING_UTILS_HH_ + +#include +#include +#include + +#include "ignition/common/testing/Export.hh" + +namespace ignition::common::testing +{ + +///////////////////////////////////////////////// +/// \brief Create an empty file with no content +/// +/// \param[in] _filename Filename of the file to be created +/// \return true if file successfully created, false otherwise +bool +IGNITION_COMMON_TESTING_VISIBLE +createNewEmptyFile(const std::string &_filename); + +} // namespace ignition::common::testing + +#endif // IGNITION_COMMON_TESTING_TESTPATHS_HH_ diff --git a/testing/include/ignition/common/testing/detail/AutoLogFixture.hh b/testing/include/ignition/common/testing/detail/AutoLogFixture.hh new file mode 100644 index 000000000..e44914590 --- /dev/null +++ b/testing/include/ignition/common/testing/detail/AutoLogFixture.hh @@ -0,0 +1,120 @@ +/* +* Copyright (C) 2022 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_TESTING_DETAIL_AUTOLOGFIXTURE_HH_ +#define IGNITION_COMMON_TESTING_DETAIL_AUTOLOGFIXTURE_HH_ + +#include "ignition/common/testing/AutoLogFixture.hh" + +#include +#include + +#include +#include +#include +#include + +namespace ignition::common::testing +{ + +////////////////////////////////////////////////// +class AutoLogFixture::Implementation +{ + /// \brief String with the full path of the logfile + public: std::string logFilename; + + /// \brief String with the full path to log directory + public: std::string logDirectory; + + /// \brief String with the base path to log directory + public: std::string logBasePath; + + /// \brief Temporary directory to run test in + public: std::unique_ptr temp; +}; + + +////////////////////////////////////////////////// +AutoLogFixture::AutoLogFixture(): + dataPtr(ignition::utils::MakeUniqueImpl()) +{ +} + +////////////////////////////////////////////////// +AutoLogFixture::~AutoLogFixture() +{ + ignLogClose(); + EXPECT_TRUE(ignition::common::unsetenv(IGN_HOMEDIR)); +} + +////////////////////////////////////////////////// +void AutoLogFixture::SetUp() +{ + const ::testing::TestInfo *const testInfo = + ::testing::UnitTest::GetInstance()->current_test_info(); + + std::string testName = testInfo->name(); + std::string testCaseName = testInfo->test_case_name(); + this->dataPtr->logFilename = testCaseName + "_" + testName + ".log"; + + this->dataPtr->temp = std::make_unique( + "test", "ign_common", false); + ASSERT_TRUE(this->dataPtr->temp->Valid()); + common::setenv(IGN_HOMEDIR, this->dataPtr->temp->Path()); + + // Initialize Console + auto logPath = common::joinPaths(this->dataPtr->temp->Path(), "test_logs"); + ignLogInit(logPath, this->dataPtr->logFilename); + + ASSERT_FALSE(logPath.empty()); + ASSERT_TRUE(common::exists( + common::joinPaths(logPath, this->dataPtr->logFilename))); + + ignition::common::Console::SetVerbosity(4); + + // Read the full path to the log directory. + this->dataPtr->logDirectory = ignLogDirectory(); + ASSERT_FALSE(this->dataPtr->logDirectory.empty()); + ASSERT_TRUE(ignition::common::exists(this->dataPtr->logDirectory)); +} + +////////////////////////////////////////////////// +std::string AutoLogFixture::FullLogPath() const +{ + return ignition::common::joinPaths( + this->dataPtr->logDirectory, this->dataPtr->logFilename); +} + +////////////////////////////////////////////////// +std::string AutoLogFixture::LogContent() const +{ + std::string loggedString; + // Open the log file, and read back the string + std::ifstream ifs(this->FullLogPath().c_str(), std::ios::in); + + while (!ifs.eof()) + { + std::string line; + std::getline(ifs, line); + loggedString += line; + } + return loggedString; +} + +} // namespace ignition::common::testing + +#endif // IGNITION_COMMON_TESTING_DETAIL_AUTOLOGFIXTURE_HH_ + diff --git a/testing/src/AutoLogFixture_TEST.cc b/testing/src/AutoLogFixture_TEST.cc new file mode 100644 index 000000000..c2118c695 --- /dev/null +++ b/testing/src/AutoLogFixture_TEST.cc @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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 + +using namespace ignition::common; +using namespace ignition::common::testing; + +///////////////////////////////////////////////// +TEST_F(AutoLogFixture, AutoLogFixture) +{ + EXPECT_FALSE(this->FullLogPath().empty()); + EXPECT_TRUE(this->LogContent().empty()); + + Console::SetVerbosity(0); + igndbg << "This is a debug" << std::endl; + EXPECT_FALSE(this->LogContent().empty()); +} diff --git a/testing/src/BazelTestPaths.cc b/testing/src/BazelTestPaths.cc new file mode 100644 index 000000000..c355df0f0 --- /dev/null +++ b/testing/src/BazelTestPaths.cc @@ -0,0 +1,55 @@ +/* +* Copyright (C) 2022 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 "ignition/common/testing/BazelTestPaths.hh" + +#include +#include + +namespace ignition::common::testing +{ + +////////////////////////////////////////////////// +BazelTestPaths::~BazelTestPaths() = default; + +////////////////////////////////////////////////// +bool BazelTestPaths::ProjectSourcePath(std::string &_sourceDir) +{ + std::string test_srcdir, bazel_path; + + if (common::env("TEST_SRCDIR", test_srcdir) && + common::env("IGN_BAZEL_PATH", bazel_path)) + { + _sourceDir = ignition::common::joinPaths(test_srcdir, + "ignition", + bazel_path); + return true; + } + else + { + return false; + } +} + +////////////////////////////////////////////////// +bool BazelTestPaths::TestTmpPath(std::string &_tmpDir) +{ + return common::env("TEST_UNDECLARED_OUTPUTS_DIR", _tmpDir); +} + +} // namespace ignition::common::testing + diff --git a/testing/src/BazelTestPaths_TEST.cc b/testing/src/BazelTestPaths_TEST.cc new file mode 100644 index 000000000..f0ea20a6a --- /dev/null +++ b/testing/src/BazelTestPaths_TEST.cc @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 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 "ignition/common/Filesystem.hh" +#include "ignition/common/testing/TestPaths.hh" +#include "ignition/common/testing/BazelTestPaths.hh" + +using namespace ignition::common; + +///////////////////////////////////////////////// +TEST(BazelTestPaths, ProjectSourcePath) +{ + ignition::common::testing::BazelTestPaths testPaths; + + std::string sourceDir; + ASSERT_TRUE(testPaths.ProjectSourcePath(sourceDir)); + ASSERT_FALSE(sourceDir.empty()); + ASSERT_TRUE(exists(sourceDir)) << sourceDir; + ASSERT_TRUE(isDirectory(sourceDir)); + + auto installedDir = joinPaths(sourceDir, "testing", "test_files"); + EXPECT_TRUE(exists(installedDir)) << installedDir; + EXPECT_TRUE(isDirectory(installedDir)); + + auto installedFile = joinPaths(installedDir, "example.txt"); + EXPECT_TRUE(exists(installedFile)); + EXPECT_TRUE(isFile(installedFile)); +} + +///////////////////////////////////////////////// +TEST(BazelTestPaths, TestTmpPath) +{ + ignition::common::testing::BazelTestPaths testPaths; + + std::string tmpDir; + ASSERT_TRUE(testPaths.TestTmpPath(tmpDir)); + ASSERT_FALSE(tmpDir.empty()); + ASSERT_TRUE(exists(tmpDir)) << tmpDir; + ASSERT_TRUE(isDirectory(tmpDir)); +} + +///////////////////////////////////////////////// +TEST(BazelTestPaths, TestBuildType) +{ + using BuildType = ignition::common::testing::BuildType; + ASSERT_EQ(BuildType::kBazel, ignition::common::testing::TestBuildType()); +} + +///////////////////////////////////////////////// +TEST(BazelTestPaths, TestPathFactory) +{ + auto testPaths = ignition::common::testing::TestPathFactory(); + ASSERT_NE(nullptr, testPaths); +} diff --git a/testing/src/CMakeLists.txt b/testing/src/CMakeLists.txt new file mode 100644 index 000000000..970bd7a81 --- /dev/null +++ b/testing/src/CMakeLists.txt @@ -0,0 +1,17 @@ +set(sources + BazelTestPaths.cc + CMakeTestPaths.cc + TestPaths.cc + Utils.cc +) + +set(test_sources + AutoLogFixture_TEST.cc + CMakeTestPaths_TEST.cc + Utils_TEST.cc +) + +ign_add_component(testing SOURCES ${sources} GET_TARGET_NAME testing_target) + +ign_build_tests(TYPE UNIT SOURCES ${test_sources} + LIB_DEPS ${testing_target}) diff --git a/testing/src/CMakeTestPaths.cc b/testing/src/CMakeTestPaths.cc new file mode 100644 index 000000000..cf9f69c1f --- /dev/null +++ b/testing/src/CMakeTestPaths.cc @@ -0,0 +1,46 @@ +/* +* Copyright (C) 2022 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 "ignition/common/testing/CMakeTestPaths.hh" + +namespace ignition::common::testing +{ + +////////////////////////////////////////////////// +CMakeTestPaths::~CMakeTestPaths() = default; + +////////////////////////////////////////////////// +bool CMakeTestPaths::ProjectSourcePath(std::string &_sourceDir) +{ + + if (!this->projectSourcePath.empty()) + { + _sourceDir = this->projectSourcePath; + return true; + } + + return false; +} + +////////////////////////////////////////////////// +bool CMakeTestPaths::TestTmpPath(std::string &_tmpDir) +{ + _tmpDir = ignition::common::tempDirectoryPath(); + return true; +} + +} // namespace ignition::common::testing diff --git a/testing/src/CMakeTestPaths_TEST.cc b/testing/src/CMakeTestPaths_TEST.cc new file mode 100644 index 000000000..f18aa0a8e --- /dev/null +++ b/testing/src/CMakeTestPaths_TEST.cc @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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 "ignition/common/Filesystem.hh" +#include "ignition/common/testing/TestPaths.hh" +#include "ignition/common/testing/CMakeTestPaths.hh" + +using namespace ignition::common; +using namespace ignition::common::testing; + +constexpr char kFakeTestPaths[] = ""; + +///////////////////////////////////////////////// +TEST(TestPaths, ProjectSourcePathUnset) +{ + EXPECT_EQ(BuildType::kUnknown, TestBuildType(kFakeTestPaths)); + + auto testPaths = TestPathFactory(kFakeTestPaths); + EXPECT_EQ(nullptr, testPaths); + + auto tempDir = MakeTestTempDirectoryImpl(kFakeTestPaths); + EXPECT_EQ(nullptr, tempDir); +} + +///////////////////////////////////////////////// +TEST(CMakeTestPaths, TestingProjectSourceDir) +{ + ASSERT_NE(0u, strlen(ignition::common::testing::kTestingProjectSourceDir)); +} + +///////////////////////////////////////////////// +TEST(CMakeTestPaths, ProjectSourcePathUnset) +{ + ignition::common::testing::CMakeTestPaths testPaths(kFakeTestPaths); + std::string sourceDir; + EXPECT_FALSE(testPaths.ProjectSourcePath(sourceDir)); + EXPECT_TRUE(sourceDir.empty()); +} + +///////////////////////////////////////////////// +TEST(CMakeTestPaths, TestBuildType) +{ + ASSERT_EQ(BuildType::kCMake, ignition::common::testing::TestBuildType()); +} + +///////////////////////////////////////////////// +TEST(CMakeTestPaths, ProjectSourcePath) +{ + ignition::common::testing::CMakeTestPaths testPaths; + + std::string sourceDir; + ASSERT_TRUE(testPaths.ProjectSourcePath(sourceDir)); + ASSERT_FALSE(sourceDir.empty()); + ASSERT_TRUE(exists(sourceDir)) << sourceDir; + ASSERT_TRUE(isDirectory(sourceDir)); + + auto installedDir = joinPaths(sourceDir, "testing", "test_files"); + EXPECT_TRUE(exists(installedDir)) << installedDir; + EXPECT_TRUE(isDirectory(installedDir)); + + auto installedFile = joinPaths(installedDir, "example.txt"); + EXPECT_TRUE(exists(installedFile)); + EXPECT_TRUE(isFile(installedFile)); +} + +///////////////////////////////////////////////// +TEST(CMakeTestPaths, TestTmpPath) +{ + ignition::common::testing::CMakeTestPaths testPaths; + + std::string tmpDir; + ASSERT_TRUE(testPaths.TestTmpPath(tmpDir)); + ASSERT_FALSE(tmpDir.empty()); + ASSERT_TRUE(exists(tmpDir)) << tmpDir; + ASSERT_TRUE(isDirectory(tmpDir)); +} + +///////////////////////////////////////////////// +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/testing/src/TestPaths.cc b/testing/src/TestPaths.cc new file mode 100644 index 000000000..54854ddac --- /dev/null +++ b/testing/src/TestPaths.cc @@ -0,0 +1,101 @@ +/* +* Copyright (C) 2022 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 "ignition/common/testing/TestPaths.hh" +#include "ignition/common/testing/BazelTestPaths.hh" +#include "ignition/common/testing/CMakeTestPaths.hh" + +#include +#include + +namespace ignition::common::testing +{ +////////////////////////////////////////////////// +TestPaths::TestPaths(const std::string &_projectSourcePath) + : projectSourcePath(_projectSourcePath) +{ +} + +////////////////////////////////////////////////// +TestPaths::~TestPaths() = default; + +////////////////////////////////////////////////// +BuildType TestBuildType(const std::string &_projectSourcePath) +{ + std::string ign_bazel; + bool ign_bazel_set = common::env("IGN_BAZEL", ign_bazel); + bool ign_cmake_set = !_projectSourcePath.empty(); + + if (ign_bazel_set && ign_cmake_set) + { + ignwarn << "Detected settings from Bazel and CMake, preferring CMake\n"; + } + + if (ign_cmake_set) + return BuildType::kCMake; + else if (ign_bazel_set) + return BuildType::kBazel; + else + return BuildType::kUnknown; +} + +////////////////////////////////////////////////// +std::unique_ptr +TestPathFactory(const std::string &_projectSourcePath) +{ + std::unique_ptr ret {nullptr}; + + switch(TestBuildType(_projectSourcePath)) + { + case BuildType::kBazel: + ret = std::make_unique(_projectSourcePath); + break; + case BuildType::kCMake: + ret = std::make_unique(_projectSourcePath); + break; + case BuildType::kUnknown: + ret = nullptr; + break; + default: + ret = nullptr; + break; + } + return ret; +} + +////////////////////////////////////////////////// +std::shared_ptr +MakeTestTempDirectoryImpl(const std::string &_projectSourcePath, + const std::string &_prefix, + const std::string &_subDir, + bool _cleanup) +{ + auto testPaths = TestPathFactory(_projectSourcePath); + + if (!testPaths) + return nullptr; + + std::string dataDir; + testPaths->TestTmpPath(dataDir); + + if (dataDir.empty()) + return nullptr; + + return std::make_shared( + dataDir, _prefix, _subDir, _cleanup); +} + +} // namespace ignition::common::testing diff --git a/testing/src/Utils.cc b/testing/src/Utils.cc new file mode 100644 index 000000000..311853484 --- /dev/null +++ b/testing/src/Utils.cc @@ -0,0 +1,53 @@ +/* +* Copyright (C) 2022 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 "ignition/common/testing/Utils.hh" + +#include +#include + +namespace ignition::common::testing +{ + +///////////////////////////////////////////////// +std::string getRandomNumber(int32_t _min, int32_t _max) +{ + // Initialize random number generator. + uint32_t seed = std::random_device {}(); + std::mt19937 randGenerator(seed); + + // Create a random number based on an integer converted to string. + std::uniform_int_distribution d(_min, _max); + + return std::to_string(d(randGenerator)); +} + +///////////////////////////////////////////////// +bool createNewEmptyFile(const std::string &_filename) +{ + try + { + std::fstream fs(_filename, std::ios::out); + } + catch(...) + { + return false; + } + return true; +} + +} // namespace ignition::common::testing diff --git a/testing/src/Utils_TEST.cc b/testing/src/Utils_TEST.cc new file mode 100644 index 000000000..5a484a52f --- /dev/null +++ b/testing/src/Utils_TEST.cc @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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 + +using namespace ignition::common; +using namespace ignition::common::testing; + +///////////////////////////////////////////////// +TEST(Utils, CreateNewEmptyFile) +{ + auto tmpDir = MakeTestTempDirectory(); + auto path = joinPaths(tmpDir->Path(), "foobar.txt"); + + ASSERT_TRUE(tmpDir->Valid()); + ASSERT_FALSE(exists(path)); + + EXPECT_TRUE(createNewEmptyFile(path)); + EXPECT_TRUE(exists(path)) << path; + EXPECT_TRUE(removeFile(path)); +} + diff --git a/testing/test_files/example.txt b/testing/test_files/example.txt new file mode 100644 index 000000000..e69de29bb