diff --git a/rclcpp_lifecycle/CMakeLists.txt b/rclcpp_lifecycle/CMakeLists.txt index 953cd0d..91b300f 100644 --- a/rclcpp_lifecycle/CMakeLists.txt +++ b/rclcpp_lifecycle/CMakeLists.txt @@ -56,16 +56,27 @@ if(BUILD_TESTING) ament_target_dependencies(test_lifecycle_node "rcl_lifecycle" "rclcpp" + "rcutils" ) - target_link_libraries(test_lifecycle_node ${PROJECT_NAME}) + target_link_libraries(test_lifecycle_node ${PROJECT_NAME} mimick) + endif() + ament_add_gtest(test_lifecycle_publisher test/test_lifecycle_publisher.cpp) + if(TARGET test_lifecycle_publisher) + ament_target_dependencies(test_lifecycle_publisher + "rcl_lifecycle" + "rclcpp" + "test_msgs" + ) + target_link_libraries(test_lifecycle_publisher ${PROJECT_NAME}) endif() ament_add_gtest(test_lifecycle_service_client test/test_lifecycle_service_client.cpp) if(TARGET test_lifecycle_service_client) ament_target_dependencies(test_lifecycle_service_client "rcl_lifecycle" "rclcpp" + "rcutils" ) - target_link_libraries(test_lifecycle_service_client ${PROJECT_NAME}) + target_link_libraries(test_lifecycle_service_client ${PROJECT_NAME} mimick) endif() ament_add_gtest(test_state_machine_info test/test_state_machine_info.cpp) if(TARGET test_state_machine_info) @@ -104,8 +115,12 @@ if(BUILD_TESTING) ament_target_dependencies(test_transition_wrapper "rcl_lifecycle" "rclcpp" + "rcutils" + ) + target_link_libraries(test_transition_wrapper ${PROJECT_NAME} mimick) + target_compile_definitions(test_transition_wrapper + PUBLIC RCUTILS_ENABLE_FAULT_INJECTION ) - target_link_libraries(test_transition_wrapper ${PROJECT_NAME}) endif() endif() diff --git a/rclcpp_lifecycle/package.xml b/rclcpp_lifecycle/package.xml index 0a315cb..2b3bc1f 100644 --- a/rclcpp_lifecycle/package.xml +++ b/rclcpp_lifecycle/package.xml @@ -24,6 +24,9 @@ ament_cmake_gtest ament_lint_auto ament_lint_common + mimick_vendor + rcutils + test_msgs ament_cmake diff --git a/rclcpp_lifecycle/test/mocking_utils/patch.hpp b/rclcpp_lifecycle/test/mocking_utils/patch.hpp new file mode 100644 index 0000000..7551beb --- /dev/null +++ b/rclcpp_lifecycle/test/mocking_utils/patch.hpp @@ -0,0 +1,527 @@ +// 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. + +// Original file taken from: +// https://github.com/ros2/rcutils/blob/master/test/mocking_utils/patch.hpp + +#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/error_handling.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 void(void) free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void); +}; + +/// 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 void(ArgT0) free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgT0 Argument type. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void, 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 void(ArgT0, ArgT1) free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void, 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 void(ArgT0, ArgT1, ArgT2) free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void, 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 void(ArgT0, ArgT1, ArgT2, ArgT3) free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void, 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 void(ArgT0, ArgT1, ArgT2, ArgT3, ArgT4) +/// free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define(mock_type, void, 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); +}; + +/// Traits specialization for void(ArgT0, ArgT1, ArgT2, ArgT3, ArgT4, ArgT5, ArgT6, ArgT7) +/// free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define( + mock_type, ReturnT, ArgT0, ArgT1, ArgT2, ArgT3, ArgT4, ArgT5, ArgT6, ArgT7, ArgT8, ArgT9); +}; + +/// Traits specialization for void(ArgT0, ArgT1, ArgT2, ArgT3, ArgT4, ArgT5) +/// free functions. +/** + * Necessary for Mimick macros to adjust accordingly when the return + * type is `void`. + * + * \tparam ID Numerical identifier of the patch. Ought to be unique. + * \tparam ArgTx Argument types. + */ +template +struct PatchTraits +{ + mmk_mock_define( + mock_type, void, 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) + : target_(target), proxy_(proxy) + { + } + + // 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) & + { + replace_with(replacement); + return *this; + } + + /// Inject a @p replacement for the patched function. + Patch && then_call(std::function replacement) && + { + replace_with(replacement); + return std::move(*this); + } + +private: + // Helper for template parameter pack expansion using `mmk_any` + // macro as pattern. + template + T any() {return mmk_any(T);} + + void replace_with(std::function replacement) + { + if (mock_) { + throw std::logic_error("Cannot configure patch more than once"); + } + auto type_erased_trampoline = + reinterpret_cast(prepare_trampoline(replacement)); + auto MMK_MANGLE(mock_type, create) = + PatchTraits::MMK_MANGLE(mock_type, create); + mock_ = mmk_mock(target_.c_str(), mock_type); + mmk_when(proxy_(any()...), .then_call = type_erased_trampoline); + } + + mock_type mock_{nullptr}; + std::string target_; + 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)) + +/// Prepare a mocking_utils::Patch for patching a `function` in a given `scope` +/// but defer applying any changes. +#define prepare_patch(scope, function) \ + make_patch<__COUNTER__, decltype(function)>( \ + MOCKING_UTILS_PATCH_TARGET(scope, function), MOCKING_UTILS_PATCH_PROXY(function) \ + ) + +/// Patch a `function` with a used-provided `replacement` in a given `scope`. +#define patch(scope, function, replacement) \ + prepare_patch(scope, function).then_call(replacement) + +/// Patch a `function` to always yield a given `return_code` in a given `scope`. +#define patch_and_return(scope, function, return_code) \ + patch(scope, function, [&](auto && ...) {return return_code;}) + +/// Patch a `function` to always yield a given `return_code` in a given `scope`. +#define patch_to_fail(scope, function, error_message, return_code) \ + patch( \ + scope, function, [&](auto && ...) { \ + RCUTILS_SET_ERROR_MSG(error_message); \ + return return_code; \ + }) + +/// Patch a `function` to execute normally but always yield a given `return_code` +/// in a given `scope`. +/** + * \warning On some Linux distributions (e.g. CentOS), pointers to function + * reference their PLT trampolines. In such cases, it is not possible to + * call `function` from within the mock. + */ +#define inject_on_return(scope, function, return_code) \ + patch( \ + scope, function, ([&, base = function](auto && ... __args) { \ + if (base != function) { \ + static_cast(base(std::forward(__args)...)); \ + } else { \ + RCUTILS_SAFE_FWRITE_TO_STDERR( \ + "[WARNING] mocking_utils::inject_on_return() cannot forward call to " \ + "original '" RCUTILS_STRINGIFY(function) "' function before injection\n" \ + " at " __FILE__ ":" RCUTILS_STRINGIFY(__LINE__) "\n"); \ + } \ + return return_code; \ + })) + +} // 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/rclcpp_lifecycle/test/test_lifecycle_node.cpp b/rclcpp_lifecycle/test/test_lifecycle_node.cpp index 93e4249..34674dd 100644 --- a/rclcpp_lifecycle/test/test_lifecycle_node.cpp +++ b/rclcpp_lifecycle/test/test_lifecycle_node.cpp @@ -24,9 +24,13 @@ #include "lifecycle_msgs/msg/state.hpp" #include "lifecycle_msgs/msg/transition.hpp" +#include "rcl_lifecycle/rcl_lifecycle.h" + #include "rclcpp/rclcpp.hpp" #include "rclcpp_lifecycle/lifecycle_node.hpp" +#include "./mocking_utils/patch.hpp" + using lifecycle_msgs::msg::State; using lifecycle_msgs::msg::Transition; @@ -37,13 +41,17 @@ protected: { rclcpp::init(0, nullptr); } + static void TearDownTestCase() + { + rclcpp::shutdown(); + } }; class EmptyLifecycleNode : public rclcpp_lifecycle::LifecycleNode { public: - explicit EmptyLifecycleNode(std::string node_name) - : rclcpp_lifecycle::LifecycleNode(std::move(node_name)) + explicit EmptyLifecycleNode(const std::string & node_name) + : rclcpp_lifecycle::LifecycleNode(node_name) {} }; @@ -142,6 +150,22 @@ TEST_F(TestDefaultStateMachine, empty_initializer) { EXPECT_EQ(State::PRIMARY_STATE_UNCONFIGURED, test_node->get_current_state().id()); } +TEST_F(TestDefaultStateMachine, empty_initializer_rcl_errors) { + { + auto patch = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_state_machine_init, RCL_RET_ERROR); + EXPECT_THROW( + std::make_shared("testnode").reset(), + std::runtime_error); + } + { + auto test_node = std::make_shared("testnode"); + auto patch = mocking_utils::inject_on_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_state_machine_fini, RCL_RET_ERROR); + EXPECT_NO_THROW(test_node.reset()); + } +} + TEST_F(TestDefaultStateMachine, trigger_transition) { auto test_node = std::make_shared("testnode"); @@ -163,6 +187,35 @@ TEST_F(TestDefaultStateMachine, trigger_transition) { rclcpp_lifecycle::Transition(Transition::TRANSITION_UNCONFIGURED_SHUTDOWN)).id()); } +TEST_F(TestDefaultStateMachine, trigger_transition_rcl_errors) { + auto test_node = std::make_shared("testnode"); + + { + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_state_machine_is_initialized, RCL_RET_ERROR); + EXPECT_EQ( + State::PRIMARY_STATE_UNCONFIGURED, + test_node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_CONFIGURE)).id()); + } + { + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_trigger_transition_by_id, RCL_RET_ERROR); + EXPECT_EQ( + State::PRIMARY_STATE_UNCONFIGURED, + test_node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_CONFIGURE)).id()); + } + { + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_trigger_transition_by_label, RCL_RET_ERROR); + EXPECT_EQ( + State::TRANSITION_STATE_CONFIGURING, + test_node->trigger_transition( + rclcpp_lifecycle::Transition(Transition::TRANSITION_CONFIGURE)).id()); + } +} + TEST_F(TestDefaultStateMachine, trigger_transition_with_error_code) { auto test_node = std::make_shared("testnode"); auto success = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; @@ -381,6 +434,8 @@ TEST_F(TestDefaultStateMachine, check_parameters) { auto descriptors = test_node->describe_parameters(parameter_names); EXPECT_EQ(descriptors.size(), parameter_names.size()); + // This actually throws inside NodeParameters::describe_parameters(), so it's not currently + // possible to cover this method 100%. EXPECT_THROW( test_node->describe_parameter("not_a_real_parameter"), rclcpp::exceptions::ParameterNotDeclaredException); @@ -437,6 +492,12 @@ TEST_F(TestDefaultStateMachine, check_parameters) { test_node->set_parameters({int_parameter}); EXPECT_EQ(parameters_set, 2u); + test_node->remove_on_set_parameters_callback(callback_handle.get()); + rclcpp::Parameter bool_parameter2(bool_name, rclcpp::ParameterValue(true)); + EXPECT_TRUE(test_node->set_parameter(bool_parameter2).successful); + EXPECT_EQ(parameters_set, 2u); + + // List parameters list_result = test_node->list_parameters({}, 0u); EXPECT_EQ(list_result.names.size(), 3u); @@ -489,7 +550,7 @@ TEST_F(TestDefaultStateMachine, test_getters) { EXPECT_NE(nullptr, const_cast(test_node.get())->get_clock()); } -TEST_F(TestDefaultStateMachine, test_graph) { +TEST_F(TestDefaultStateMachine, test_graph_topics) { auto test_node = std::make_shared("testnode"); auto names = test_node->get_node_names(); @@ -504,6 +565,19 @@ TEST_F(TestDefaultStateMachine, test_graph) { topic_names_and_types["/testnode/transition_event"][0].c_str(), "lifecycle_msgs/msg/TransitionEvent"); + EXPECT_EQ(1u, test_node->count_publishers("/testnode/transition_event")); + EXPECT_EQ(0u, test_node->count_subscribers("/testnode/transition_event")); + + auto publishers_info = test_node->get_publishers_info_by_topic("/testnode/transition_event"); + EXPECT_EQ(1u, publishers_info.size()); + auto subscriptions_info = + test_node->get_subscriptions_info_by_topic("/testnode/transition_event"); + EXPECT_EQ(0u, subscriptions_info.size()); +} + +TEST_F(TestDefaultStateMachine, test_graph_services) { + auto test_node = std::make_shared("testnode"); + auto service_names_and_types = test_node->get_service_names_and_types(); // These are specific to lifecycle nodes, other services are provided by rclcpp::Node ASSERT_NE( @@ -540,15 +614,48 @@ TEST_F(TestDefaultStateMachine, test_graph) { EXPECT_STREQ( service_names_and_types["/testnode/get_transition_graph"][0].c_str(), "lifecycle_msgs/srv/GetAvailableTransitions"); +} - EXPECT_EQ(1u, test_node->count_publishers("/testnode/transition_event")); - EXPECT_EQ(0u, test_node->count_subscribers("/testnode/transition_event")); +TEST_F(TestDefaultStateMachine, test_graph_services_by_node) { + auto test_node = std::make_shared("testnode"); - auto publishers_info = test_node->get_publishers_info_by_topic("/testnode/transition_event"); - EXPECT_EQ(publishers_info.size(), 1u); - auto subscriptions_info = - test_node->get_subscriptions_info_by_topic("/testnode/transition_event"); - EXPECT_EQ(subscriptions_info.size(), 0u); + auto service_names_and_types_by_node = + test_node->get_service_names_and_types_by_node("testnode", ""); + // These are specific to lifecycle nodes, other services are provided by rclcpp::Node + ASSERT_NE( + service_names_and_types_by_node.end(), + service_names_and_types_by_node.find(std::string("/testnode/change_state"))); + EXPECT_STREQ( + service_names_and_types_by_node["/testnode/change_state"][0].c_str(), + "lifecycle_msgs/srv/ChangeState"); + + ASSERT_NE( + service_names_and_types_by_node.end(), + service_names_and_types_by_node.find(std::string("/testnode/get_available_states"))); + EXPECT_STREQ( + service_names_and_types_by_node["/testnode/get_available_states"][0].c_str(), + "lifecycle_msgs/srv/GetAvailableStates"); + + ASSERT_NE( + service_names_and_types_by_node.end(), + service_names_and_types_by_node.find(std::string("/testnode/get_available_transitions"))); + EXPECT_STREQ( + service_names_and_types_by_node["/testnode/get_available_transitions"][0].c_str(), + "lifecycle_msgs/srv/GetAvailableTransitions"); + + ASSERT_NE( + service_names_and_types_by_node.end(), + service_names_and_types_by_node.find(std::string("/testnode/get_state"))); + EXPECT_STREQ( + service_names_and_types_by_node["/testnode/get_state"][0].c_str(), + "lifecycle_msgs/srv/GetState"); + + ASSERT_NE( + service_names_and_types_by_node.end(), + service_names_and_types_by_node.find(std::string("/testnode/get_transition_graph"))); + EXPECT_STREQ( + service_names_and_types_by_node["/testnode/get_transition_graph"][0].c_str(), + "lifecycle_msgs/srv/GetAvailableTransitions"); } TEST_F(TestDefaultStateMachine, test_callback_groups) { @@ -564,3 +671,16 @@ TEST_F(TestDefaultStateMachine, test_callback_groups) { EXPECT_EQ(groups.size(), 2u); EXPECT_EQ(groups[1].lock().get(), group.get()); } + +TEST_F(TestDefaultStateMachine, wait_for_graph_change) +{ + auto test_node = std::make_shared("testnode"); + EXPECT_THROW( + test_node->wait_for_graph_change(nullptr, std::chrono::milliseconds(1)), + rclcpp::exceptions::InvalidEventError); + + auto event = std::make_shared(); + EXPECT_THROW( + test_node->wait_for_graph_change(event, std::chrono::milliseconds(0)), + rclcpp::exceptions::EventNotRegisteredError); +} diff --git a/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp b/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp new file mode 100644 index 0000000..555354a --- /dev/null +++ b/rclcpp_lifecycle/test/test_lifecycle_publisher.cpp @@ -0,0 +1,104 @@ +// 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 "test_msgs/msg/empty.hpp" + +#include "rclcpp_lifecycle/lifecycle_node.hpp" +#include "rclcpp_lifecycle/lifecycle_publisher.hpp" + +class TestDefaultStateMachine : public ::testing::Test +{ +protected: + static void SetUpTestCase() + { + rclcpp::init(0, nullptr); + } + static void TearDownTestCase() + { + rclcpp::shutdown(); + } +}; + +class EmptyLifecycleNode : public rclcpp_lifecycle::LifecycleNode +{ +public: + explicit EmptyLifecycleNode(const std::string & node_name) + : rclcpp_lifecycle::LifecycleNode(node_name) + { + rclcpp::PublisherOptionsWithAllocator> options; + publisher_ = + std::make_shared>( + get_node_base_interface().get(), std::string("topic"), rclcpp::QoS(10), options); + add_publisher_handle(publisher_); + + // For coverage this is being added here + auto timer = create_wall_timer(std::chrono::seconds(1), []() {}); + add_timer_handle(timer); + } + + std::shared_ptr> publisher() + { + return publisher_; + } + +private: + std::shared_ptr> publisher_; +}; + +class TestLifecyclePublisher : public ::testing::Test +{ +public: + void SetUp() + { + rclcpp::init(0, nullptr); + node_ = std::make_shared("node"); + } + + void TearDown() + { + rclcpp::shutdown(); + } + +protected: + std::shared_ptr node_; +}; + +TEST_F(TestLifecyclePublisher, publish) { + node_->publisher()->on_deactivate(); + EXPECT_FALSE(node_->publisher()->is_activated()); + { + auto msg_ptr = std::make_unique(); + EXPECT_NO_THROW(node_->publisher()->publish(*msg_ptr)); + } + { + auto msg_ptr = std::make_unique(); + EXPECT_NO_THROW(node_->publisher()->publish(std::move(msg_ptr))); + } + node_->publisher()->on_activate(); + EXPECT_TRUE(node_->publisher()->is_activated()); + { + auto msg_ptr = std::make_unique(); + EXPECT_NO_THROW(node_->publisher()->publish(*msg_ptr)); + } + { + auto msg_ptr = std::make_unique(); + EXPECT_NO_THROW(node_->publisher()->publish(std::move(msg_ptr))); + } +} diff --git a/rclcpp_lifecycle/test/test_lifecycle_service_client.cpp b/rclcpp_lifecycle/test/test_lifecycle_service_client.cpp index 7ef4acc..ce19529 100644 --- a/rclcpp_lifecycle/test/test_lifecycle_service_client.cpp +++ b/rclcpp_lifecycle/test/test_lifecycle_service_client.cpp @@ -36,6 +36,8 @@ #include "rclcpp/rclcpp.hpp" #include "rclcpp_lifecycle/lifecycle_node.hpp" +#include "./mocking_utils/patch.hpp" + using namespace std::chrono_literals; constexpr char const * lifecycle_node_name = "lc_talker"; @@ -393,3 +395,56 @@ TEST_F(TestLifecycleServiceClient, wait_for_graph_change) node_graph->wait_for_graph_change(event, std::chrono::milliseconds(0)), rclcpp::exceptions::EventNotRegisteredError); } + +class TestLifecycleServiceClientRCLErrors : public ::testing::Test +{ +protected: + void SetUp() override + { + rclcpp::init(0, nullptr); + } + + void TearDown() override + { + rclcpp::shutdown(); + } +}; + +TEST_F(TestLifecycleServiceClientRCLErrors, call_services_rcl_errors) { + auto lifecycle_node = std::make_shared(); + auto lifecycle_client = std::make_shared("client_with_errors"); + + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_state_machine_is_initialized, RCL_RET_ERROR); + + // on_change_state + lifecycle_client->change_state( + lifecycle_msgs::msg::Transition::TRANSITION_CONFIGURE); + rclcpp::spin_some(lifecycle_client); + EXPECT_THROW( + rclcpp::spin_some(lifecycle_node->get_node_base_interface()), std::runtime_error); + + // on_get_state + lifecycle_client->get_state(); + rclcpp::spin_some(lifecycle_client); + EXPECT_THROW( + rclcpp::spin_some(lifecycle_node->get_node_base_interface()), std::runtime_error); + + // on_get_avilable_states + lifecycle_client->get_available_states(); + rclcpp::spin_some(lifecycle_client); + EXPECT_THROW( + rclcpp::spin_some(lifecycle_node->get_node_base_interface()), std::runtime_error); + + // on_get_available_transitions + lifecycle_client->get_available_transitions(); + rclcpp::spin_some(lifecycle_client); + EXPECT_THROW( + rclcpp::spin_some(lifecycle_node->get_node_base_interface()), std::runtime_error); + + // on_get_transition_graph + lifecycle_client->get_transition_graph(); + rclcpp::spin_some(lifecycle_client); + EXPECT_THROW( + rclcpp::spin_some(lifecycle_node->get_node_base_interface()), std::runtime_error); +} diff --git a/rclcpp_lifecycle/test/test_transition_wrapper.cpp b/rclcpp_lifecycle/test/test_transition_wrapper.cpp index 9895dc9..4f72d90 100644 --- a/rclcpp_lifecycle/test/test_transition_wrapper.cpp +++ b/rclcpp_lifecycle/test/test_transition_wrapper.cpp @@ -18,8 +18,12 @@ #include #include +#include "rcutils/testing/fault_injection.h" + #include "rclcpp_lifecycle/lifecycle_node.hpp" +#include "./mocking_utils/patch.hpp" + class TestTransitionWrapper : public ::testing::Test { protected: @@ -31,8 +35,11 @@ protected: class TransitionDerived : public rclcpp_lifecycle::Transition { public: + TransitionDerived(const uint8_t id, const std::string & label) + : Transition(id, label) {} + TransitionDerived( - uint8_t id, const std::string & label, + const uint8_t id, const std::string & label, rclcpp_lifecycle::State && start, rclcpp_lifecycle::State && goal) : Transition(id, label, std::move(start), std::move(goal)) {} void expose_reset() @@ -125,4 +132,80 @@ TEST_F(TestTransitionWrapper, exceptions) { EXPECT_THROW(a->goal_state(), std::runtime_error); EXPECT_THROW(a->id(), std::runtime_error); EXPECT_THROW(a->label(), std::runtime_error); + + { + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_transition_init, RCL_RET_ERROR); + + EXPECT_THROW( + std::make_shared(1, "one").reset(), + std::runtime_error); + + rclcpp_lifecycle::State state1(1, "start_state"); + rclcpp_lifecycle::State state2(2, "goal_state"); + EXPECT_THROW( + std::make_shared( + 2, "two", std::move(start_state), std::move(goal_state)).reset(), + std::runtime_error); + } + { + auto mock = mocking_utils::patch_and_return( + "lib:rclcpp_lifecycle", rcl_lifecycle_transition_fini, RCL_RET_ERROR); + auto transition1 = std::make_shared(1, "one"); + EXPECT_NO_THROW(transition1->expose_reset()); + + rclcpp_lifecycle::State state1(1, "start_state"); + rclcpp_lifecycle::State state2(2, "goal_state"); + auto transition2 = + std::make_shared(2, "two", std::move(start_state), std::move(goal_state)); + EXPECT_NO_THROW(transition2->expose_reset()); + } + + RCUTILS_FAULT_INJECTION_TEST( + { + std::shared_ptr transition = nullptr; + try { + transition = std::make_shared(1, "one"); + } catch (...) { + } + if (nullptr != transition) { + EXPECT_NO_THROW(transition->expose_reset()); + } + }); + + RCUTILS_FAULT_INJECTION_TEST( + { + std::shared_ptr transition = nullptr; + try { + { + // These will fail due to failed allocations + rclcpp_lifecycle::State state1(1, "start_state"); + rclcpp_lifecycle::State state2(2, "goal_state"); + + // Failed allocations and failed rcl init functions + transition = std::make_shared( + 2, "two", std::move(state1), std::move(state2)); + } + } catch (...) { + } + + if (nullptr != transition) { + EXPECT_NO_THROW(transition->expose_reset()); + } + }); + + RCUTILS_FAULT_INJECTION_TEST( + { + try { + // These will fail due to failed allocations + rclcpp_lifecycle::State state1(1, "start_state"); + rclcpp_lifecycle::State state2(2, "goal_state"); + + // Failed allocations and failed rcl init functions + auto a = std::make_shared(2, "two", std::move(state1), std::move(state2)); + auto b = std::make_shared(3, "three"); + *b = *a; + } catch (...) { + } + }); }