diff --git a/CMakeLists.txt b/CMakeLists.txt index 35904906..abb89c5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,7 +181,7 @@ if(BUILD_TESTING) osrf_testing_tools_cpp::memory_tools LIBRARY_PRELOAD_ENVIRONMENT_IS_AVAILABLE) ament_add_gtest(test_logging test/test_logging.cpp) - target_link_libraries(test_logging ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools) + target_link_libraries(test_logging ${PROJECT_NAME} mimick osrf_testing_tools_cpp::memory_tools) add_executable(test_logging_long_messages test/test_logging_long_messages.cpp) target_link_libraries(test_logging_long_messages ${PROJECT_NAME}) @@ -234,7 +234,7 @@ if(BUILD_TESTING) test/test_char_array.cpp ) if(TARGET test_char_array) - target_link_libraries(test_char_array ${PROJECT_NAME}) + target_link_libraries(test_char_array ${PROJECT_NAME} mimick) endif() # Can't use C++ with stdatomic_helper.h @@ -275,7 +275,7 @@ if(BUILD_TESTING) test/test_split.cpp ) if(TARGET test_split) - target_link_libraries(test_split ${PROJECT_NAME}) + target_link_libraries(test_split ${PROJECT_NAME} mimick) endif() rcutils_custom_add_gtest(test_find @@ -322,7 +322,7 @@ if(BUILD_TESTING) ) if(TARGET test_filesystem) ament_target_dependencies(test_filesystem "osrf_testing_tools_cpp") - target_link_libraries(test_filesystem ${PROJECT_NAME}) + target_link_libraries(test_filesystem ${PROJECT_NAME} mimick) target_compile_definitions(test_filesystem PRIVATE BUILD_DIR="${CMAKE_CURRENT_BINARY_DIR}") endif() @@ -337,7 +337,7 @@ if(BUILD_TESTING) test/test_format_string.cpp ) if(TARGET test_format_string) - target_link_libraries(test_format_string ${PROJECT_NAME}) + target_link_libraries(test_format_string ${PROJECT_NAME} mimick) endif() rcutils_custom_add_gtest(test_string_map @@ -375,14 +375,14 @@ if(BUILD_TESTING) # which is appropriate when building the dll but not consuming it. target_compile_definitions(dummy_shared_library PRIVATE "DUMMY_SHARED_LIBRARY_BUILDING_DLL") endif() - target_link_libraries(test_shared_library ${PROJECT_NAME}) + target_link_libraries(test_shared_library ${PROJECT_NAME} mimick) endif() rcutils_custom_add_gtest(test_time test/test_time.cpp ENV ${memory_tools_test_env_vars}) if(TARGET test_time) - target_link_libraries(test_time ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools) + target_link_libraries(test_time ${PROJECT_NAME} mimick osrf_testing_tools_cpp::memory_tools) endif() rcutils_custom_add_gtest(test_snprintf @@ -436,7 +436,7 @@ if(BUILD_TESTING) RCUTILS_COLORIZED_OUTPUT=1 ) if(TARGET test_logging_custom_env) - target_link_libraries(test_logging_custom_env ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools) + target_link_libraries(test_logging_custom_env ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools mimick) endif() # RCUTILS_LOGGING_MAX_OUTPUT_FORMAT_LEN is defined as 2048, truncation should occur @@ -452,7 +452,7 @@ if(BUILD_TESTING) RCUTILS_COLORIZED_OUTPUT=0 ) if(TARGET test_logging_custom_env2) - target_link_libraries(test_logging_custom_env2 ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools) + target_link_libraries(test_logging_custom_env2 ${PROJECT_NAME} osrf_testing_tools_cpp::memory_tools mimick) endif() rcutils_custom_add_gtest(test_logging_bad_env test/test_logging_bad_env.cpp diff --git a/test/mocking_utils/filesystem.hpp b/test/mocking_utils/filesystem.hpp new file mode 100644 index 00000000..789a1ebc --- /dev/null +++ b/test/mocking_utils/filesystem.hpp @@ -0,0 +1,251 @@ +// Copyright 2020 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. + +#ifndef MOCKING_UTILS__FILESYSTEM_HPP_ +#define MOCKING_UTILS__FILESYSTEM_HPP_ + +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#else +#include +#endif + +#include +#include +#include + +#include "rcutils/macros.h" + +#include "patch.hpp" + +namespace mocking_utils +{ +namespace filesystem +{ + +/// Platform-independent set of file type constants. +namespace file_types +{ +constexpr auto REGULAR_FILE = S_IFREG; +constexpr auto DIRECTORY = S_IFDIR; +} // namespace file_types + +/// Platform-independent set of file permission constants. +namespace permissions +{ +#ifndef _WIN32 +constexpr auto USER_READABLE = S_IRUSR; +constexpr auto USER_WRITABLE = S_IWUSR; +#else +constexpr auto USER_READABLE = _S_IREAD; +constexpr auto USER_WRITABLE = _S_IWRITE; +#endif +} // namespace permissions + +// Deal with binary API quirks in 64 bit MacOS. +#if defined(__MACH__) && defined(_DARWIN_FEATURE_64_BIT_INODE) +#define MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, function) \ + (std::string(RCUTILS_STRINGIFY(function) "$INODE64") + "@" + (scope)) +#else +#define MOCKING_UTILS_FILESYSTEM_PATCH_TARGET MOCKING_UTILS_PATCH_TARGET +#endif + +#if !defined(_WIN32) + +/// Helper class for patching the filesystem API. +/** + * \tparam ID Numerical identifier for this patches. Ought to be unique. + */ +template +class FileSystem +{ +public: + /// Construct mocked filesystem. + /** + * \param[in] scope Scope target string, using Mimick syntax. + * \see mocking_utils::Patch documentation for further reference. + */ + explicit FileSystem(const std::string & scope) + : opendir_mock_( + MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, opendir), + MOCKING_UTILS_PATCH_PROXY(opendir)), +#ifndef _GNU_SOURCE + stat_mock_( + MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, stat), + MOCKING_UTILS_PATCH_PROXY(stat)) + { + stat_mock_.then_call( + std::bind( + &FileSystem::do_stat, this, + std::placeholders::_1, std::placeholders::_2)); +#else + // Deal with binary API quirks in GNU Linux. + __xstat_mock_( + MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, __xstat), + MOCKING_UTILS_PATCH_PROXY(__xstat)) + { + __xstat_mock_.then_call( + std::bind( + &FileSystem::do___xstat, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3)); +#endif + opendir_mock_.then_call(std::bind(&FileSystem::do_opendir, this, std::placeholders::_1)); + } + + /// Force APIs that return file descriptors or handles to fail as if these had been exhausted. + void exhaust_file_descriptors() + { + forced_errno_ = EMFILE; + } + + /// Get information from file in the mocked filesystem. + /** + * \param[in] path Path to the file whose information is to be retrieved. + * If file is not found, one will be added. + * \return mutable reference to file information. + */ + struct stat & file_info(const std::string & path) + { + return files_info_[path]; + } + +private: + DIR * do_opendir(const char *) + { + if (forced_errno_ != 0) { + errno = forced_errno_; + return NULL; + } + errno = ENOENT; + return NULL; + } + MOCKING_UTILS_PATCH_TYPE(ID, opendir) opendir_mock_; + +#ifndef _GNU_SOURCE + int do_stat(const char * path, struct stat * info) + { +#else + int do___xstat(int, const char * path, struct stat * info) + { +#endif + if (files_info_.count(path) == 0) { + errno = ENOENT; + return -1; + } + *info = files_info_[path]; + return 0; + } + +#ifndef _GNU_SOURCE + MOCKING_UTILS_PATCH_TYPE(ID, stat) stat_mock_; +#else + MOCKING_UTILS_PATCH_TYPE(ID, __xstat) __xstat_mock_; +#endif + + int forced_errno_{0}; + std::map files_info_; +}; + +#else // !defined(_WIN32) + +/// Helper class for patching the filesystem API. +/** + * \tparam ID Numerical identifier for this patches. Ought to be unique. + */ +template +class FileSystem +{ +public: + /// Construct mocked filesystem. + /** + * \param[in] scope Scope target string, using Mimick syntax. + * \see mocking_utils::Patch documentation for further reference. + */ + explicit FileSystem(const std::string & scope) + : find_first_file_mock_(MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, FindFirstFileA), + MOCKING_UTILS_PATCH_PROXY(FindFirstFileA)), + _stat_mock_(MOCKING_UTILS_FILESYSTEM_PATCH_TARGET(scope, _stat), + MOCKING_UTILS_PATCH_PROXY(_stat)) + { + find_first_file_mock_.then_call( + std::bind( + &FileSystem::do_FindFirstFileA, this, + std::placeholders::_1, std::placeholders::_2)); + _stat_mock_.then_call( + std::bind( + &FileSystem::do__stat, this, + std::placeholders::_1, std::placeholders::_2)); + } + + /// Force APIs that return file descriptors or handles to fail as if these had been exhausted. + void exhaust_file_descriptors() + { + forced_errno_ = ERROR_NO_MORE_SEARCH_HANDLES; + } + + /// Get information from file in the mocked filesystem. + /** + * \param[in] path Path to the file whose information is to be retrieved. + * If file is not found, one will be added. + * \return mutable reference to file information. + */ + struct _stat & file_info(const std::string & path) + { + return files_info_[path]; + } + +private: + HANDLE do_FindFirstFileA(LPCSTR, LPWIN32_FIND_DATAA) + { + if (forced_errno_ != 0) { + SetLastError(forced_errno_); + return INVALID_HANDLE_VALUE; + } + SetLastError(ERROR_FILE_NOT_FOUND); + return INVALID_HANDLE_VALUE; + } + + MOCKING_UTILS_PATCH_TYPE(ID, FindFirstFileA) find_first_file_mock_; + + int do__stat(const char * path, struct _stat * info) + { + if (files_info_.count(path) == 0) { + errno = ENOENT; + return -1; + } + *info = files_info_[path]; + return 0; + } + + MOCKING_UTILS_PATCH_TYPE(ID, _stat) _stat_mock_; + + int forced_errno_{0}; + std::map files_info_; +}; + +#endif // else !defined(_WIN32) + +} // namespace filesystem + +/// Patch filesystem API in a given `scope`. +#define patch_filesystem(scope) filesystem::FileSystem<__COUNTER__>(scope) + +} // namespace mocking_utils + +#endif // MOCKING_UTILS__FILESYSTEM_HPP_ diff --git a/test/mocking_utils/patch.hpp b/test/mocking_utils/patch.hpp new file mode 100644 index 00000000..37442d06 --- /dev/null +++ b/test/mocking_utils/patch.hpp @@ -0,0 +1,348 @@ +// Copyright 2020 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. + +#ifndef MOCKING_UTILS__PATCH_HPP_ +#define MOCKING_UTILS__PATCH_HPP_ + +#define MOCKING_UTILS_SUPPORT_VA_LIST +#if (defined(__aarch64__) || defined(__arm__) || defined(_M_ARM) || defined(__thumb__)) +// In ARM machines, va_list does not define comparison operators +// nor the compiler allows defining them via operator overloads. +// Thus, Mimick argument matching code will not compile. +#undef MOCKING_UTILS_SUPPORT_VA_LIST +#endif + +#ifdef MOCKING_UTILS_SUPPORT_VA_LIST +#include +#endif + +#include +#include +#include +#include + +#include "mimick/mimick.h" +#include "rcutils/macros.h" + +namespace mocking_utils +{ + +/// Mimick specific traits for each mocking_utils::Patch instance. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam SignatureT Type of the symbol to be patched. +*/ +template +struct PatchTraits; + +/// Traits specialization for ReturnT(void) free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT); +}; + +/// Traits specialization for ReturnT(ArgT0) free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgT0 Argument type. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT, ArgT0); +}; + +/// Traits specialization for ReturnT(ArgT0, ArgT1) free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT, ArgT0, ArgT1); +}; + +/// Traits specialization for ReturnT(ArgT0, ArgT1, ArgT2) free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT, ArgT0, ArgT1, ArgT2); +}; + +/// Traits specialization for ReturnT(ArgT0, ArgT1, ArgT2, ArgT3) free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT, ArgT0, ArgT1, ArgT2, ArgT3); +}; + +/// Traits specialization for ReturnT(ArgT0, ArgT1, ArgT2, ArgT3, ArgT4) +/// free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, ReturnT, ArgT0, ArgT1, ArgT2, ArgT3, ArgT4); +}; + +/// Traits specialization for ReturnT(ArgT0, ArgT1, ArgT2, ArgT3, ArgT4, ArgT5) +/// free functions. +/** + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define( + mock_type, ReturnT, ArgT0, ArgT1, ArgT2, ArgT3, ArgT4, ArgT5); +}; + +/// Generic trampoline to wrap generalized callables in plain functions. +/** + * \tparam ID Numerical identifier of this trampoline. Ought to be unique. + * \tparam SignatureT Type of the symbol this trampoline replaces. + */ +template +struct Trampoline; + +/// Trampoline specialization for free functions. +template +struct Trampoline +{ + static ReturnT base(ArgTs... args) + { + return target(std::forward(args)...); + } + + static std::function target; +}; + +template +std::function +Trampoline::target; + +/// Setup trampoline with the given @p target. +/** + * \param[in] target Callable that this trampoline will target. + * \return the plain base function of this trampoline. + * + * \tparam ID Numerical identifier of this trampoline. Ought to be unique. + * \tparam SignatureT Type of the symbol this trampoline replaces. + */ +template +auto prepare_trampoline(std::function target) +{ + Trampoline::target = target; + return Trampoline::base; +} + +/// Patch class for binary API mocking +/** + * Built on top of Mimick, to enable symbol mocking on a per dynamically + * linked binary object basis. + * + * \tparam ID Numerical identifier for this patch. Ought to be unique. + * \tparam SignatureT Type of the symbol to be patched. + */ +template +class Patch; + +/// Patch specialization for ReturnT(ArgTs...) free functions. +/** + * \tparam ID Numerical identifier for this patch. Ought to be unique. + * \tparam ReturnT Return value type. + * \tparam ArgTs Argument types. + */ +template +class Patch +{ +public: + using mock_type = typename PatchTraits::mock_type; + + /// Construct a patch. + /** + * \param[in] target Symbol target string, using Mimick syntax + * i.e. "symbol(@scope)?", where scope may be "self" to target the current + * binary, "lib:library_name" to target a given library, "file:path/to/library" + * to target a given file, or "sym:other_symbol" to target the first library + * that defines said symbol. + * \param[in] proxy An indirection to call the target function. + * This indirection must ensure this call goes through the function's + * trampoline, as setup by the dynamic linker. + * \return a mocking_utils::Patch instance. + */ + explicit Patch(const std::string & target, std::function proxy) + : proxy_(proxy) + { + auto MMK_MANGLE(mock_type, create) = + PatchTraits::MMK_MANGLE(mock_type, create); + mock_ = mmk_mock(target.c_str(), mock_type); + } + + // Copy construction and assignment are disabled. + Patch(const Patch &) = delete; + Patch & operator=(const Patch &) = delete; + + Patch(Patch && other) + { + mock_ = other.mock_; + other.mock_ = nullptr; + } + + Patch & operator=(Patch && other) + { + if (mock_) { + mmk_reset(mock_); + } + mock_ = other.mock_; + other.mock_ = nullptr; + } + + ~Patch() + { + if (mock_) { + mmk_reset(mock_); + } + } + + /// Inject a @p replacement for the patched function. + Patch & then_call(std::function replacement) & + { + auto type_erased_trampoline = + reinterpret_cast(prepare_trampoline(replacement)); + mmk_when(proxy_(any()...), .then_call = type_erased_trampoline); + return *this; + } + + /// Inject a @p replacement for the patched function. + Patch && then_call(std::function replacement) && + { + auto type_erased_trampoline = + reinterpret_cast(prepare_trampoline(replacement)); + mmk_when(proxy_(any()...), .then_call = type_erased_trampoline); + return std::move(*this); + } + +private: + // Helper for template parameter pack expansion using `mmk_any` + // macro as pattern. + template + T any() {return mmk_any(T);} + + mock_type mock_; + std::function proxy_; +}; + +/// Make a patch for a `target` function. +/** + * Useful for type deduction during \ref mocking_utils::Patch construction. + * + * \param[in] target Symbol target string, using Mimick syntax. + * \param[in] proxy An indirection to call the target function. + * \return a mocking_utils::Patch instance. + * + * \tparam ID Numerical identifier for this patch. Ought to be unique. + * \tparam SignatureT Type of the function to be patched. + * + * \sa mocking_utils::Patch for further reference. + */ +template +auto make_patch(const std::string & target, std::function proxy) +{ + return Patch(target, proxy); +} + +/// Define a dummy operator `op` for a given `type`. +/** + * Useful to enable patching functions that take arguments whose types + * do not define basic comparison operators, as required by Mimick. +*/ +#define MOCKING_UTILS_BOOL_OPERATOR_RETURNS_FALSE(type_, op) \ + template \ + typename std::enable_if::value, bool>::type \ + operator op(const T &, const T &) { \ + return false; \ + } + +/// Get the exact \ref mocking_utils::Patch type for a given `id` and `function`. +/** + * Useful to avoid ignored attribute warnings when using the \b decltype operator. + */ +#define MOCKING_UTILS_PATCH_TYPE(id, function) \ + decltype(mocking_utils::make_patch("", nullptr)) + +/// A transparent forwarding proxy to a given `function`. +/** + * Useful to ensure a call to `function` goes through its trampoline. + */ +#define MOCKING_UTILS_PATCH_PROXY(function) \ + [] (auto && ... args)->decltype(auto) { \ + return function(std::forward(args)...); \ + } + +/// Compute a Mimick symbol target string based on which `function` is to be patched +/// in which `scope`. +#define MOCKING_UTILS_PATCH_TARGET(scope, function) \ + (std::string(RCUTILS_STRINGIFY(function)) + "@" + (scope)) + +/// Patch a `function` with a used-provided `replacement` in a given `scope`. +#define patch(scope, function, replacement) \ + make_patch<__COUNTER__, decltype(function)>( \ + MOCKING_UTILS_PATCH_TARGET(scope, function), MOCKING_UTILS_PATCH_PROXY(function) \ + ).then_call(replacement) + +} // namespace mocking_utils + +#ifdef MOCKING_UTILS_SUPPORT_VA_LIST +// Define dummy comparison operators for C standard va_list type +MOCKING_UTILS_BOOL_OPERATOR_RETURNS_FALSE(va_list, ==) +MOCKING_UTILS_BOOL_OPERATOR_RETURNS_FALSE(va_list, !=) +MOCKING_UTILS_BOOL_OPERATOR_RETURNS_FALSE(va_list, <) +MOCKING_UTILS_BOOL_OPERATOR_RETURNS_FALSE(va_list, >) +#endif + +#endif // MOCKING_UTILS__PATCH_HPP_ diff --git a/test/mocking_utils/stdio.hpp b/test/mocking_utils/stdio.hpp new file mode 100644 index 00000000..aa1ee1f8 --- /dev/null +++ b/test/mocking_utils/stdio.hpp @@ -0,0 +1,59 @@ +// Copyright 2020 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. + +#ifndef MOCKING_UTILS__STDIO_HPP_ +#define MOCKING_UTILS__STDIO_HPP_ + +#include +#include + +#include "patch.hpp" + +namespace mocking_utils +{ + +#define MOCKING_UTILS_CAN_PATCH_VSNPRINTF + +#if !defined(MOCKING_UTILS_SUPPORT_VA_LIST) +#undef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +#endif + +#if defined(__MACH__) // as vsnprintf binary API differs between C and C++ in MacOS +#undef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +#endif + +#if defined(_WIN32) // as vsnprintf binary APIs are undocumented in Windows +#undef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +#endif + +#if defined(_WIN32) + +using _vsnprintf_s_type = + int (char *, size_t, size_t, const char *, va_list); // NOLINT(readability/casting) + +/// Patch _vsnprintf_s with the given `replacement` in the given `scope`. +// Signature must be explicitly provided to avoid ambiguity with template overloads. +#define patch__vsnprintf_s(scope, replacement) \ + make_patch<__COUNTER__, mocking_utils::_vsnprintf_s_type>( \ + MOCKING_UTILS_PATCH_TARGET(scope, _vsnprintf_s), MOCKING_UTILS_PATCH_PROXY(_vsnprintf_s) \ + ).then_call(replacement) + +/// Patch _vscprintf with the given `replacement` in the given `scope`. +#define patch__vscprintf(scope, replacement) patch(scope, _vscprintf, replacement) + +#endif // defined(_WIN32) + +} // namespace mocking_utils + +#endif // MOCKING_UTILS__STDIO_HPP_ diff --git a/test/test_char_array.cpp b/test/test_char_array.cpp index 7c948105..43bd712d 100644 --- a/test/test_char_array.cpp +++ b/test/test_char_array.cpp @@ -20,6 +20,9 @@ #include "rcutils/error_handling.h" #include "rcutils/types/char_array.h" +#include "./mocking_utils/patch.hpp" +#include "./mocking_utils/stdio.hpp" + class ArrayCharTest : public ::testing::Test { protected: @@ -125,8 +128,44 @@ TEST_F(ArrayCharTest, vsprintf_fail) { ret = example_logger(&char_array, "Long string for the case %d", 2); EXPECT_EQ(RCUTILS_RET_BAD_ALLOC, ret); rcutils_reset_error(); - char_array.allocator = allocator; + +#ifdef MOCKING_UTILS_CAN_PATCH_VSNPRINTF + size_t buffer_threshold = 0; +#ifdef _WIN32 + auto vscprintf_mock = mocking_utils::patch__vscprintf( + "lib:rcutils", [&](auto && ...) { + return static_cast(buffer_threshold); + }); + + auto vsnprintf_mock = mocking_utils::patch__vsnprintf_s( + "lib:rcutils", [&](auto && ...) { + errno = EINVAL; + return -1; + }); +#else + auto mock = mocking_utils::patch( + "lib:rcutils", vsnprintf, [&](char * buffer, size_t buffer_size, auto...) { + if (nullptr == buffer || buffer_size < buffer_threshold) { + return static_cast(buffer_threshold); + } + errno = EINVAL; + return -1; + }); +#endif + // Do not force char array resize. + buffer_threshold = 5; + ret = example_logger(&char_array, "Long string for the case %d", 2); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); + + // Force char array resize. + buffer_threshold = 20; + ret = example_logger(&char_array, "Long string for the case %d", 2); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); +#endif // MOCKING_UTILS_CAN_PATCH_VSNPRINTF + EXPECT_EQ(RCUTILS_RET_OK, rcutils_char_array_fini(&char_array)); } diff --git a/test/test_filesystem.cpp b/test/test_filesystem.cpp index df93be07..8e9f1c16 100644 --- a/test/test_filesystem.cpp +++ b/test/test_filesystem.cpp @@ -19,6 +19,8 @@ #include "osrf_testing_tools_cpp/scope_exit.hpp" +#include "./mocking_utils/filesystem.hpp" + static rcutils_allocator_t g_allocator = rcutils_get_default_allocator(); class TestFilesystemFixture : public ::testing::Test @@ -193,7 +195,7 @@ TEST_F(TestFilesystemFixture, is_readable) { EXPECT_TRUE(rcutils_is_readable(path)); } { - char * path = rcutils_join_path(this->test_path, "dummy_nonexisting_file.txt", g_allocator); + char * path = rcutils_join_path(this->test_path, "dummy_nonexistent_file.txt", g_allocator); OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( { g_allocator.deallocate(path, g_allocator.state); @@ -201,6 +203,21 @@ TEST_F(TestFilesystemFixture, is_readable) { ASSERT_FALSE(nullptr == path); EXPECT_FALSE(rcutils_is_readable(path)); } + { + char * path = rcutils_join_path(this->test_path, "dummy_nonexistent_file.txt", g_allocator); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + g_allocator.deallocate(path, g_allocator.state); + }); + ASSERT_FALSE(nullptr == path); + EXPECT_FALSE(rcutils_is_readable(path)); + } + { + auto fs = mocking_utils::patch_filesystem("lib:rcutils"); + const char * path = "fake_unreadable_file.txt"; + fs.file_info(path).st_mode &= ~mocking_utils::filesystem::permissions::USER_READABLE; + EXPECT_FALSE(rcutils_is_readable(path)); + } } TEST_F(TestFilesystemFixture, is_writable) { @@ -224,7 +241,7 @@ TEST_F(TestFilesystemFixture, is_writable) { EXPECT_TRUE(rcutils_is_writable(path)); } { - char * path = rcutils_join_path(this->test_path, "dummy_nonexisting_file.txt", g_allocator); + char * path = rcutils_join_path(this->test_path, "dummy_nonexistent_file.txt", g_allocator); OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( { g_allocator.deallocate(path, g_allocator.state); @@ -232,6 +249,12 @@ TEST_F(TestFilesystemFixture, is_writable) { ASSERT_FALSE(nullptr == path); EXPECT_FALSE(rcutils_is_writable(path)); } + { + auto fs = mocking_utils::patch_filesystem("lib:rcutils"); + const char * path = "fake_unwritable_file.txt"; + fs.file_info(path).st_mode &= ~mocking_utils::filesystem::permissions::USER_WRITABLE; + EXPECT_FALSE(rcutils_is_writable(path)); + } } TEST_F(TestFilesystemFixture, is_readable_and_writable) { @@ -254,6 +277,15 @@ TEST_F(TestFilesystemFixture, is_readable_and_writable) { ASSERT_FALSE(nullptr == path); EXPECT_TRUE(rcutils_is_readable_and_writable(path)); } + { + auto fs = mocking_utils::patch_filesystem("lib:rcutils"); + const char * path = "fake_writable_but_unreadable_file.txt"; + fs.file_info(path).st_mode |= mocking_utils::filesystem::permissions::USER_READABLE; + fs.file_info(path).st_mode &= ~mocking_utils::filesystem::permissions::USER_WRITABLE; + EXPECT_FALSE(rcutils_is_readable_and_writable(path)); + EXPECT_FALSE(rcutils_is_writable(path)); + EXPECT_TRUE(rcutils_is_readable(path)); + } { char * path = rcutils_join_path(this->test_path, "dummy_readable_writable_file.txt", g_allocator); @@ -330,22 +362,25 @@ TEST_F(TestFilesystemFixture, calculate_directory_size) { #ifdef WIN32 // Due to different line breaks on windows, we have one more byte in the file. // See https://github.com/ros2/rcutils/issues/198 - ASSERT_EQ(6u, size); + EXPECT_EQ(6u, size); #else - ASSERT_EQ(5u, size); + EXPECT_EQ(5u, size); #endif - OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( - { - g_allocator.deallocate(path, g_allocator.state); - }); + g_allocator.deallocate(path, g_allocator.state); char * non_existing_path = rcutils_join_path(this->test_path, "non_existing_folder", g_allocator); size = rcutils_calculate_directory_size(non_existing_path, g_allocator); - ASSERT_EQ(0u, size); - OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + EXPECT_EQ(0u, size); + g_allocator.deallocate(non_existing_path, g_allocator.state); + { - g_allocator.deallocate(non_existing_path, g_allocator.state); - }); + auto fs = mocking_utils::patch_filesystem("lib:rcutils"); + const char * path = "some_fake_directory/some_fake_folder"; + fs.file_info(path).st_mode |= mocking_utils::filesystem::file_types::DIRECTORY; + fs.exhaust_file_descriptors(); + size = rcutils_calculate_directory_size(path, g_allocator); + EXPECT_EQ(0u, size); + } } TEST_F(TestFilesystemFixture, calculate_file_size) { diff --git a/test/test_format_string.cpp b/test/test_format_string.cpp index cc48f16d..6d3045a8 100644 --- a/test/test_format_string.cpp +++ b/test/test_format_string.cpp @@ -17,6 +17,9 @@ #include #include "./allocator_testing_utils.h" +#include "./mocking_utils/patch.hpp" +#include "./mocking_utils/stdio.hpp" + #include "rcutils/allocator.h" #include "rcutils/format_string.h" @@ -59,3 +62,31 @@ TEST(test_format_string_limit, invalid_arguments) { formatted = rcutils_format_string_limit(failing_allocator, 10, "%s", "test"); EXPECT_STREQ(NULL, formatted); } + +#ifdef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +TEST(test_format_string_limit, on_internal_error) { +#ifdef _WIN32 + auto _vscprintf_mock = mocking_utils::patch__vscprintf( + "lib:rcutils", [](auto && ...) {return 1;}); + + auto _vsnprintf_s_mock = mocking_utils::patch__vsnprintf_s( + "lib:rcutils", [](auto && ...) { + errno = EINVAL; + return -1; + }); +#else + auto mock = mocking_utils::patch( + "lib:rcutils", vsnprintf, [](char * buffer, auto && ...) { + if (nullptr == buffer) { + return 1; // provide a dummy value if buffer required size is queried + } + errno = EINVAL; + return -1; + }); +#endif + + rcutils_allocator_t allocator = rcutils_get_default_allocator(); + char * formatted = rcutils_format_string_limit(allocator, 10, "%s", "test"); + EXPECT_STREQ(NULL, formatted); +} +#endif // MOCKING_UTILS_CAN_PATCH_VSNPRINTF diff --git a/test/test_logging_custom_env.cpp b/test/test_logging_custom_env.cpp index e76425ad..7fefa12a 100644 --- a/test/test_logging_custom_env.cpp +++ b/test/test_logging_custom_env.cpp @@ -18,6 +18,7 @@ #include #include "./allocator_testing_utils.h" +#include "./mocking_utils/patch.hpp" #include "osrf_testing_tools_cpp/scope_exit.hpp" #include "rcutils/logging.h" @@ -52,3 +53,10 @@ TEST(CLASSNAME(TestLoggingCustomEnv, RMW_IMPLEMENTATION), test_logging) { EXPECT_EQ(RCUTILS_RET_OK, rcutils_char_array_fini(&output_buf)); EXPECT_EQ(RCUTILS_RET_OK, rcutils_char_array_fini(&msg_buf)); } + +TEST(CLASSNAME(TestLoggingCustomEnv, RMW_IMPLEMENTATION), test_logging_with_buffering_issues) { + auto mock = mocking_utils::patch("lib:rcutils", setvbuf, [](auto && ...) {return -1;}); + EXPECT_FALSE(g_rcutils_logging_initialized); + EXPECT_EQ(RCUTILS_RET_ERROR, rcutils_logging_initialize()); + EXPECT_FALSE(g_rcutils_logging_initialized); +} diff --git a/test/test_shared_library.cpp b/test/test_shared_library.cpp index ca1b203a..e7b69089 100644 --- a/test/test_shared_library.cpp +++ b/test/test_shared_library.cpp @@ -17,6 +17,8 @@ #include #include "./allocator_testing_utils.h" +#include "./mocking_utils/patch.hpp" +#include "./mocking_utils/stdio.hpp" #include "rcutils/allocator.h" #include "rcutils/error_handling.h" @@ -46,6 +48,34 @@ TEST_F(TestSharedLibrary, basic_load) { EXPECT_TRUE(lib.lib_pointer == NULL); EXPECT_FALSE(rcutils_is_shared_library_loaded(&lib)); +#ifdef MOCKING_UTILS_CAN_PATCH_VSNPRINTF + { + // Check internal errors are handled correctly +#ifdef _WIN32 + auto _vscprintf_mock = mocking_utils::patch__vscprintf( + "lib:rcutils", [](auto && ...) {return 1;}); + + auto _vsnprintf_s_mock = mocking_utils::patch__vsnprintf_s( + "lib:rcutils", [](auto && ...) { + errno = EINVAL; + return -1; + }); +#else + auto mock = mocking_utils::patch( + "lib:rcutils", vsnprintf, [](char * buffer, auto && ...) { + if (nullptr == buffer) { + return 1; // provide a dummy value if buffer required size is queried + } + errno = EINVAL; + return -1; + }); +#endif + + ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, false); + ASSERT_EQ(RCUTILS_RET_ERROR, ret); + } +#endif // MOCKING_UTILS_CAN_PATCH_VSNPRINTF + // Check debug name works first because rcutils_load_shared_library should be called on // non-debug symbol name ret = rcutils_get_platform_library_name("dummy_shared_library", library_path, 1024, true); diff --git a/test/test_split.cpp b/test/test_split.cpp index 40bdfdeb..8a5724a0 100644 --- a/test/test_split.cpp +++ b/test/test_split.cpp @@ -77,7 +77,6 @@ TEST(test_split, split) { RCUTILS_RET_ERROR, rcutils_split("hello/world", '/', time_bomb_allocator, &tokens_fail)); - rcutils_string_array_t tokens0 = test_split("", '/', 0); ret = rcutils_string_array_fini(&tokens0); ASSERT_EQ(RCUTILS_RET_OK, ret); diff --git a/test/test_strerror.cpp b/test/test_strerror.cpp index a5759a85..6cee02a5 100644 --- a/test/test_strerror.cpp +++ b/test/test_strerror.cpp @@ -24,7 +24,7 @@ #include "rcutils/macros.h" #include "rcutils/strerror.h" -#include "./mimick.h" +#include "mimick/mimick.h" TEST(test_strerror, get_error) { // cleaning possible errors diff --git a/test/test_time.cpp b/test/test_time.cpp index 03796be5..b4f798e2 100644 --- a/test/test_time.cpp +++ b/test/test_time.cpp @@ -23,6 +23,9 @@ #include "rcutils/error_handling.h" #include "rcutils/time.h" +#include "./mocking_utils/patch.hpp" +#include "./mocking_utils/stdio.hpp" + using osrf_testing_tools_cpp::memory_tools::disable_monitoring_in_all_threads; using osrf_testing_tools_cpp::memory_tools::enable_monitoring_in_all_threads; using osrf_testing_tools_cpp::memory_tools::on_unexpected_calloc; @@ -188,6 +191,63 @@ TEST_F(TestTimeFixture, test_rcutils_steady_time_now) { llabs(steady_diff - sc_diff), RCUTILS_MS_TO_NS(k_tolerance_ms)) << "steady_clock differs"; } +#if !defined(_WIN32) + +// For mocking purposes +#if defined(__MACH__) +#include +#include +#define clock_gettime clock_get_time +#endif + +// Tests rcutils_system_time_now() and rcutils_steady_time_now() functions +// when system clocks misbehave. +TEST_F(TestTimeFixture, test_rcutils_with_bad_system_clocks) { +#if !defined (__MACH__) // as tv_sec is an unsigned integer there + { + auto mock = mocking_utils::patch( + "lib:rcutils", clock_gettime, + [](auto, auto * ts) { + ts->tv_sec = -1; + ts->tv_nsec = 0; + return 0; + }); + + rcutils_time_point_value_t now = 0; + rcutils_ret_t ret = rcutils_system_time_now(&now); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); + + ret = rcutils_steady_time_now(&now); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); + } +#endif + { + auto mock = mocking_utils::patch( + "lib:rcutils", clock_gettime, + [](auto, auto * ts) { + ts->tv_sec = 0; + ts->tv_nsec = -1; + return 0; + }); + + rcutils_time_point_value_t now = 0; + rcutils_ret_t ret = rcutils_system_time_now(&now); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); + + ret = rcutils_steady_time_now(&now); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); + } +} + +#if defined(__MACH__) +#undef clock_gettime +#endif +#endif // !defined(_WIN32) + // Tests the rcutils_time_point_value_as_nanoseconds_string() function. TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_nanoseconds_string) { rcutils_ret_t ret; @@ -239,6 +299,33 @@ TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_nanoseconds_string) { ret = rcutils_time_point_value_as_nanoseconds_string(&timepoint, buffer, sizeof(buffer)); EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str; EXPECT_STREQ("-0000000000000000100", buffer); + +#ifdef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +#ifdef _WIN32 + auto _vscprintf_mock = mocking_utils::patch__vscprintf( + "lib:rcutils", [](auto && ...) {return 1;}); + + auto _vsnprintf_s_mock = mocking_utils::patch__vsnprintf_s( + "lib:rcutils", [](auto && ...) { + errno = EINVAL; + return -1; + }); +#else + auto mock = mocking_utils::patch( + "lib:rcutils", vsnprintf, [](char * buffer, auto && ...) { + if (nullptr == buffer) { + return 1; // provide a dummy value if buffer required size is queried + } + errno = EINVAL; + return -1; + }); +#endif + + timepoint = 100; + ret = rcutils_time_point_value_as_nanoseconds_string(&timepoint, buffer, sizeof(buffer)); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); +#endif // MOCKING_UTILS_CAN_PATCH_VSNPRINTF } // Tests the rcutils_time_point_value_as_seconds_string() function. @@ -292,4 +379,31 @@ TEST_F(TestTimeFixture, test_rcutils_time_point_value_as_seconds_string) { ret = rcutils_time_point_value_as_seconds_string(&timepoint, buffer, sizeof(buffer)); EXPECT_EQ(RCUTILS_RET_OK, ret) << rcutils_get_error_string().str; EXPECT_STREQ("-0000000000.000000100", buffer); + +#ifdef MOCKING_UTILS_CAN_PATCH_VSNPRINTF +#ifdef _WIN32 + auto _vscprintf_mock = mocking_utils::patch__vscprintf( + "lib:rcutils", [](auto && ...) {return 1;}); + + auto _vsnprintf_s_mock = mocking_utils::patch__vsnprintf_s( + "lib:rcutils", [](auto && ...) { + errno = EINVAL; + return -1; + }); +#else + auto mock = mocking_utils::patch( + "lib:rcutils", vsnprintf, [](char * buffer, auto && ...) { + if (nullptr == buffer) { + return 1; // provide a dummy value if buffer required size is queried + } + errno = EINVAL; + return -1; + }); +#endif + + timepoint = 100; + ret = rcutils_time_point_value_as_seconds_string(&timepoint, buffer, sizeof(buffer)); + EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); +#endif // MOCKING_UTILS_CAN_PATCH_VSNPRINTF }