EXPECT_THROW_EQ and ASSERT_THROW_EQ macros for unittests (#1232)

* EXPECT_THROW_EQ and ASSERT_THROW_EQ macros for unittests

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Address PR Feedback

Signed-off-by: Stephen Brawner <brawner@gmail.com>
This commit is contained in:
brawner 2020-07-17 12:03:18 -07:00
parent 60bcee36ab
commit 04bccb95cb
3 changed files with 404 additions and 0 deletions

View file

@ -518,6 +518,11 @@ if(TARGET test_subscription_options)
target_link_libraries(test_subscription_options ${PROJECT_NAME}) target_link_libraries(test_subscription_options ${PROJECT_NAME})
endif() 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 test resources
install( install(
DIRECTORY resources DIRECTORY resources

View file

@ -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 <gtest/gtest.h>
#include <cstring>
#include <type_traits>
#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<typename T,
typename = typename std::enable_if_t<
!std::is_convertible<T, std::exception>::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<typename T,
typename = typename std::enable_if_t<
!std::is_convertible<T, rclcpp::exceptions::RCLErrorBase>::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<typename T,
typename = typename std::enable_if_t<
std::is_convertible<T, rclcpp::exceptions::RCLErrorBase>::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<ExceptionT>( \
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_

View file

@ -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 <gtest/gtest.h>
#include <cstring>
#include <exception>
#include <string>
#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);
}
}