diff --git a/rclcpp/test/CMakeLists.txt b/rclcpp/test/CMakeLists.txt index d150cad49b..f9dbf0c4b5 100644 --- a/rclcpp/test/CMakeLists.txt +++ b/rclcpp/test/CMakeLists.txt @@ -518,6 +518,11 @@ if(TARGET test_subscription_options) target_link_libraries(test_subscription_options ${PROJECT_NAME}) endif() +ament_add_gtest(test_rclcpp_gtest_macros utils/test_rclcpp_gtest_macros.cpp) +if(TARGET test_rclcpp_gtest_macros) + target_link_libraries(test_rclcpp_gtest_macros ${PROJECT_NAME}) +endif() + # Install test resources install( DIRECTORY resources diff --git a/rclcpp/test/utils/rclcpp_gtest_macros.hpp b/rclcpp/test/utils/rclcpp_gtest_macros.hpp new file mode 100644 index 0000000000..a7b1b97d77 --- /dev/null +++ b/rclcpp/test/utils/rclcpp_gtest_macros.hpp @@ -0,0 +1,195 @@ +// 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 UTILS__RCLCPP_GTEST_MACROS_HPP_ +#define UTILS__RCLCPP_GTEST_MACROS_HPP_ + +#include + +#include +#include + +#include "rclcpp/exceptions/exceptions.hpp" + +namespace rclcpp +{ +namespace testing +{ +namespace details +{ + +/** + * \brief Check if two thrown objects are equals. + * + * For generic thrown objects, probably is unlikely to be used. This type must + * overload the == and << operators. + */ +template::value>> +::testing::AssertionResult AreThrowableContentsEqual( + const T & expected, const T & actual, const char * expected_exception_expression, + const char * throwing_expression) +{ + if (expected == actual) { + return ::testing::AssertionSuccess() << + "'\nThe value of the non-standard throwable thrown by the expression\n'" << + throwing_expression << "'\n\nmatches the value of the expected thrown object\n'" << + expected_exception_expression << "'\n\t(" << expected << " == " << actual << ")\n"; + } + + return ::testing::AssertionFailure() << + "\nThe value of the non-standard throwable thrown by the expression\n'" << + throwing_expression << "'\n\ndoes not match the value of the expected thrown object\n'" << + expected_exception_expression << "'\n\t(" << expected << " != " << actual << ")\n"; +} + +/** + * \brief Check if two std::exceptions are equal according to their message. + * + * If the exception type also derives from rclcpp::Exception, then the next overload is called + * instead + */ +template::value>> +::testing::AssertionResult AreThrowableContentsEqual( + const std::exception & expected, const std::exception & actual, + const char * expected_exception_expression, + const char * throwing_expression) +{ + if (std::strcmp(expected.what(), actual.what()) == 0) { + return ::testing::AssertionSuccess() << + "'\nThe contents of the std::exception thrown by the expression\n'" << + throwing_expression << "':\n\te.what(): '" << actual.what() << + "'\n\nmatch the contents of the expected std::exception\n'" << + expected_exception_expression << "'\n\te.what(): '" << expected.what() << "'\n"; + } + + return ::testing::AssertionFailure() << + "\nThe contents of the std::exception thrown by the expression\n'" << + throwing_expression << "':\n\te.what(): '" << actual.what() << + "'\n\ndo not match the contents of the expected std::exception\n'" << + expected_exception_expression << "'\n\te.what(): '" << expected.what() << "'\n"; +} + +/** + * \brief Check if two exceptions that derive from rclcpp::RCLErrorBase are equal. + * + * This checks equality based on their return and message. It does not check the formatted + * message, which is what is reported by std::exception::what() for RCLErrors. + */ +template::value>> +::testing::AssertionResult AreThrowableContentsEqual( + const rclcpp::exceptions::RCLErrorBase & expected, + const rclcpp::exceptions::RCLErrorBase & actual, + const char * expected_exception_expression, + const char * throwing_expression) +{ + if ((expected.ret == actual.ret) && (expected.message == actual.message)) { + return ::testing::AssertionSuccess() << + "'\nThe contents of the RCLError thrown by the expression\n'" << throwing_expression << + "':\n\trcl_ret_t: " << actual.ret << "\n\tmessage: '" << actual.message << + "'\n\nmatch the contents of the expected RCLError\n'" << + expected_exception_expression << "'\n\trcl_ret_t: " << expected.ret << + "\n\tmessage: '" << expected.message << "'\n"; + } + + return ::testing::AssertionFailure() << + "'\nThe contents of the RCLError thrown by the expression\n'" << throwing_expression << + "':\n\trcl_ret_t: " << actual.ret << "\n\tmessage: '" << actual.message << + "'\n\ndo not match the contents of the expected RCLError\n'" << + expected_exception_expression << "'\n\trcl_ret_t: " << expected.ret << "\n\tmessage: '" << + expected.message << "'\n"; +} + +} // namespace details +} // namespace testing +} // namespace rclcpp + +/** + * \def CHECK_THROW_EQ_IMPL + * \brief Implemented check if statement throws expected exception. don't use directly, use + * RCLCPP_EXPECT_THROW_EQ or RCLCPP_ASSERT_THROW_EQ instead. + */ +#define CHECK_THROW_EQ_IMPL(throwing_statement, expected_exception, assertion_result) \ + do { \ + using ExceptionT = decltype(expected_exception); \ + try { \ + throwing_statement; \ + assertion_result = ::testing::AssertionFailure() << \ + "\nExpected the expression:\n\t'" #throwing_statement "'\nto throw: \n\t'" << \ + #expected_exception "'\nbut it did not throw.\n"; \ + } catch (const ExceptionT & e) { \ + assertion_result = \ + rclcpp::testing::details::AreThrowableContentsEqual( \ + expected_exception, e, #expected_exception, #throwing_statement); \ + } catch (const std::exception & e) { \ + assertion_result = ::testing::AssertionFailure() << \ + "\nExpected the expression:\n\t'" #throwing_statement "'\nto throw: \n\t'" << \ + #expected_exception "'\nbut it threw:\n\tType: " << typeid(e).name() << \ + "\n\te.what(): '" << e.what() << "'\n"; \ + } catch (...) { \ + assertion_result = ::testing::AssertionFailure() << \ + "\nExpected the expression:\n\t'" #throwing_statement "'\nto throw: \n\t'" << \ + #expected_exception "'\nbut it threw an unrecognized throwable type.\n"; \ + } \ + } while (0) + +/** + * \def RCLCPP_EXPECT_THROW_EQ + * \brief Check if a statement throws the expected exception type and that the exceptions matches + * the expected exception. + * + * Like other gtest EXPECT_ macros, this doesn't halt a test and return on failure. Instead it + * just adds a failure to the current test. + * + * See test_gtest_macros.cpp for examples + */ +#define RCLCPP_EXPECT_THROW_EQ(throwing_statement, expected_exception) \ + do { \ + ::testing::AssertionResult \ + is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable = \ + ::testing::AssertionSuccess(); \ + CHECK_THROW_EQ_IMPL( \ + throwing_statement, \ + expected_exception, \ + is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable); \ + EXPECT_TRUE(is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable); \ + } while (0) + +/** + * \def RCLCPP_ASSERT_THROW_EQ + * \brief Assert that a statement throws the expected exception type and that the exceptions + * matches the expected exception. + * + * See test_gtest_macros.cpp for examples + * + * Like other gtest ASSERT_ macros, this will halt the test on failure and return. + */ +#define RCLCPP_ASSERT_THROW_EQ(throwing_statement, expected_exception) \ + do { \ + ::testing::AssertionResult \ + is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable = \ + ::testing::AssertionSuccess(); \ + CHECK_THROW_EQ_IMPL( \ + throwing_statement, \ + expected_exception, \ + is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable); \ + ASSERT_TRUE(is_the_result_of_the_throwing_expression_equal_to_the_expected_throwable); \ + } while (0) + +#endif // UTILS__RCLCPP_GTEST_MACROS_HPP_ diff --git a/rclcpp/test/utils/test_rclcpp_gtest_macros.cpp b/rclcpp/test/utils/test_rclcpp_gtest_macros.cpp new file mode 100644 index 0000000000..2c3b3ff20b --- /dev/null +++ b/rclcpp/test/utils/test_rclcpp_gtest_macros.cpp @@ -0,0 +1,204 @@ +// 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. + +#include + +#include +#include + +#include + +#include "./rclcpp_gtest_macros.hpp" + +#include "rcl/rcl.h" +#include "rclcpp/rclcpp.hpp" + +struct NonStandardThrowable +{ + bool operator==(const NonStandardThrowable &) const + { + return true; + } +}; + +std::ostream & operator<<(std::ostream & os, const NonStandardThrowable &) +{ + os << "NonStandardThrowable"; + return os; +} + +TEST(TestGtestMacros, standard_exceptions) { + RCLCPP_EXPECT_THROW_EQ( + throw std::runtime_error("some runtime error"), + std::runtime_error("some runtime error")); + + RCLCPP_EXPECT_THROW_EQ( + throw std::invalid_argument("some invalid argument error"), + std::invalid_argument("some invalid argument error")); + + RCLCPP_ASSERT_THROW_EQ( + throw std::runtime_error("some runtime error"), + std::runtime_error("some runtime error")); + + RCLCPP_ASSERT_THROW_EQ( + throw std::invalid_argument("some invalid argument error"), + std::invalid_argument("some invalid argument error")); +} + +TEST(TestGtestMacros, standard_exceptions_not_equals) { + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + CHECK_THROW_EQ_IMPL( + throw std::runtime_error("some runtime error"), + std::range_error("some runtime error"), + result); + EXPECT_FALSE(result); + + CHECK_THROW_EQ_IMPL( + throw std::invalid_argument("some invalid argument error"), + std::invalid_argument("some different invalid argument error"), + result); + EXPECT_FALSE(result); +} + +TEST(TestGTestMacros, non_standard_types) { + RCLCPP_EXPECT_THROW_EQ(throw 0, 0); + + RCLCPP_EXPECT_THROW_EQ(throw 42, 42); + + RCLCPP_EXPECT_THROW_EQ(throw std::string("some string"), std::string("some string")); + + RCLCPP_EXPECT_THROW_EQ(throw NonStandardThrowable(), NonStandardThrowable()); + + RCLCPP_ASSERT_THROW_EQ(throw 0, 0); + + RCLCPP_ASSERT_THROW_EQ(throw 42, 42); + + RCLCPP_ASSERT_THROW_EQ(throw std::string("some string"), std::string("some string")); + + RCLCPP_ASSERT_THROW_EQ(throw NonStandardThrowable(), NonStandardThrowable()); +} + +TEST(TestGTestMacros, non_standard_types_not_equals) { + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + + CHECK_THROW_EQ_IMPL(throw 0, 1, result); + EXPECT_FALSE(result); + result = ::testing::AssertionSuccess(); + + CHECK_THROW_EQ_IMPL(throw -42, 42, result); + EXPECT_FALSE(result); + result = ::testing::AssertionSuccess(); + + CHECK_THROW_EQ_IMPL(throw std::string("some string"), std::string("some other string"), result); + EXPECT_FALSE(result); +} + +TEST(TestGTestMacros, rclcpp_exceptions) { + rcutils_error_state_t rcl_error_state = {"this is some error message", __FILE__, __LINE__}; + { + auto expected = + rclcpp::exceptions::RCLError(RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLError(RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + RCLCPP_EXPECT_THROW_EQ(throw actual, expected); + RCLCPP_ASSERT_THROW_EQ(throw actual, expected); + } + { + auto expected = + rclcpp::exceptions::RCLBadAlloc(RCL_RET_BAD_ALLOC, &rcl_error_state); + auto actual = + rclcpp::exceptions::RCLBadAlloc(RCL_RET_BAD_ALLOC, &rcl_error_state); + RCLCPP_EXPECT_THROW_EQ(throw actual, expected); + } + { + // Prefixes are not checked + auto expected = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &rcl_error_state, "different_prefix"); + RCLCPP_EXPECT_THROW_EQ(throw actual, expected); + RCLCPP_ASSERT_THROW_EQ(throw actual, expected); + } + { + // File names are not checked + rcutils_error_state_t different_error_state = rcl_error_state; + std::snprintf( + different_error_state.file, RCUTILS_ERROR_STATE_FILE_MAX_LENGTH, "different_file.cpp"); + auto expected = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &different_error_state, "exception_prefix"); + RCLCPP_EXPECT_THROW_EQ(throw actual, expected); + RCLCPP_ASSERT_THROW_EQ(throw actual, expected); + } + { + // Line numbers are not checked + rcutils_error_state_t different_error_state = rcl_error_state; + different_error_state.line_number += 42; + auto expected = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &different_error_state, "exception_prefix"); + RCLCPP_EXPECT_THROW_EQ(throw actual, expected); + RCLCPP_ASSERT_THROW_EQ(throw actual, expected); + } +} + +TEST(TestGTestMacros, rclcpp_exceptions_not_equal) { + rcutils_error_state_t rcl_error_state = {"this is some error message", __FILE__, __LINE__}; + { + // Check different return errors + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + auto expected = + rclcpp::exceptions::RCLError(RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + + auto actual = + rclcpp::exceptions::RCLError(RCL_RET_BAD_ALLOC, &rcl_error_state, "exception_prefix"); + CHECK_THROW_EQ_IMPL(throw actual, expected, result); + EXPECT_FALSE(result); + } + { + // Check different error messages + rcutils_error_state_t different_error_state = rcl_error_state; + std::snprintf( + different_error_state.message, + RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH, + "this is a different error message"); + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + auto expected = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLError( + RCL_RET_ERROR, &different_error_state, "exception_prefix"); + CHECK_THROW_EQ_IMPL(throw actual, expected, result); + EXPECT_FALSE(result); + } + { + // Check different exception types + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + auto expected = + rclcpp::exceptions::RCLError(RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + auto actual = + rclcpp::exceptions::RCLInvalidArgument(RCL_RET_ERROR, &rcl_error_state, "exception_prefix"); + CHECK_THROW_EQ_IMPL(throw actual, expected, result); + EXPECT_FALSE(result); + } +}