diff --git a/rcl_action/include/rcl_action/wait.h b/rcl_action/include/rcl_action/wait.h index 29bdeb341..653e0af72 100644 --- a/rcl_action/include/rcl_action/wait.h +++ b/rcl_action/include/rcl_action/wait.h @@ -73,6 +73,53 @@ rcl_action_wait_set_add_action_client( size_t * client_index, size_t * subscription_index); +/// Add a rcl_action_client_t to a wait set and get all entity indices. +/** + * This function is similar to rcl_action_wait_set_add_action_client() but + * returns all five entity indices (3 clients + 2 subscriptions) instead of just + * the starting indices. This is useful when using + * rcl_action_client_wait_set_get_entities_ready_with_indices() to avoid race + * conditions in multi-threaded scenarios. + * + *
+ * Attribute | Adherence + * ------------------ | ------------- + * Allocates Memory | Yes + * Thread-Safe | No + * Uses Atomics | No + * Lock-Free | Yes + * + * \param[inout] wait_set struct where action client service client and + * subscription are to be stored + * \param[in] action_client the action client to be added to the wait set + * \param[out] goal_client_index index of the goal client in the wait set. + * Optionally NULL. + * \param[out] cancel_client_index index of the cancel client in the wait set. + * Optionally NULL. + * \param[out] result_client_index index of the result client in the wait set. + * Optionally NULL. + * \param[out] feedback_subscription_index index of the feedback subscription. + * Optionally NULL. + * \param[out] status_subscription_index index of the status subscription. + * Optionally NULL. + * \return `RCL_RET_OK` if added successfully, or + * \return `RCL_RET_WAIT_SET_INVALID` if the wait set is zero initialized, or + * \return `RCL_RET_WAIT_SET_FULL` if the subscription set is full, or + * \return `RCL_RET_ACTION_CLIENT_INVALID` if the action client is invalid, or + * \return `RCL_RET_ERROR` if an unspecified error occurs. + */ +RCL_ACTION_PUBLIC +RCL_WARN_UNUSED +rcl_ret_t +rcl_action_wait_set_add_action_client_with_indices( + rcl_wait_set_t * wait_set, + const rcl_action_client_t * action_client, + size_t * goal_client_index, + size_t * cancel_client_index, + size_t * result_client_index, + size_t * feedback_subscription_index, + size_t * status_subscription_index); + /// Add a rcl_action_server_t to a wait set. /** * This function will add the underlying services to the wait set. @@ -238,6 +285,69 @@ rcl_action_client_wait_set_get_entities_ready( bool * is_cancel_response_ready, bool * is_result_response_ready); +/// Get the wait set entities that are ready for a rcl_action_client_t using +/// explicit indices. +/** + * This is a thread-safe variant of + * rcl_action_client_wait_set_get_entities_ready() that accepts explicit wait + * set indices rather than reading them from the action client's internal state. + * This is useful when multiple threads use different wait sets with the same + * action client, avoiding the race condition where indices from one wait set + * could be used with a different wait set. + * + * The caller should obtain the indices from + * rcl_action_wait_set_add_action_client(). + * + *
+ * Attribute | Adherence + * ------------------ | ------------- + * Allocates Memory | No + * Thread-Safe | Yes (when used with per-wait_set indices) + * Uses Atomics | No + * Lock-Free | Yes + * + * \param[in] wait_set struct where action server services are to be stored + * \param[in] action_client an action client to query + * \param[in] feedback_subscription_index index of the feedback subscription in + * the wait set + * \param[in] status_subscription_index index of the status subscription in the + * wait set + * \param[in] goal_client_index index of the goal client in the wait set + * \param[in] cancel_client_index index of the cancel client in the wait set + * \param[in] result_client_index index of the result client in the wait set + * \param[out] is_feedback_ready `true` if there is a feedback message ready to + * take, `false` otherwise + * \param[out] is_status_ready `true` if there is a status message ready to + * take, `false` otherwise + * \param[out] is_goal_response_ready `true` if there is a goal response message + * ready to take, `false` otherwise + * \param[out] is_cancel_response_ready `true` if there is a cancel response + * message ready to take, `false` otherwise + * \param[out] is_result_response_ready `true` if there is a result response + * message ready to take, `false` otherwise + * \return `RCL_RET_OK` if call is successful, or + * \return `RCL_RET_WAIT_SET_INVALID` if the wait set is invalid, or + * \return `RCL_RET_INVALID_ARGUMENT` if any arguments are invalid, or + * \return `RCL_RET_ACTION_CLIENT_INVALID` if the action client is invalid, or + * \return `RCL_RET_ERROR` if an unspecified error occurs. + */ +RCL_ACTION_PUBLIC +RCL_WARN_UNUSED +rcl_ret_t +rcl_action_client_wait_set_get_entities_ready_with_indices( + const rcl_wait_set_t * wait_set, + const rcl_action_client_t * action_client, + size_t feedback_subscription_index, + size_t status_subscription_index, + size_t goal_client_index, + size_t cancel_client_index, + size_t result_client_index, + bool * is_feedback_ready, + bool * is_status_ready, + bool * is_goal_response_ready, + bool * is_cancel_response_ready, + bool * is_result_response_ready); + /// Get the wait set entities that are ready for a rcl_action_server_t. /** * The caller can use this function to determine the relevant action server functions diff --git a/rcl_action/src/rcl_action/action_client.c b/rcl_action/src/rcl_action/action_client.c index 3a9bd07de..3d44dbc29 100644 --- a/rcl_action/src/rcl_action/action_client.c +++ b/rcl_action/src/rcl_action/action_client.c @@ -39,9 +39,7 @@ extern "C" #include "rmw/qos_profiles.h" #include "rmw/types.h" - -rcl_action_client_t -rcl_action_get_zero_initialized_client(void) +rcl_action_client_t rcl_action_get_zero_initialized_client(void) { static rcl_action_client_t null_action_client = {0}; return null_action_client; @@ -689,11 +687,132 @@ rcl_action_client_wait_set_get_entities_ready( return RCL_RET_OK; } -rcl_ret_t -rcl_action_client_set_goal_client_callback( - const rcl_action_client_t * action_client, - rcl_event_callback_t callback, - const void * user_data) +rcl_ret_t rcl_action_wait_set_add_action_client_with_indices( + rcl_wait_set_t *wait_set, const rcl_action_client_t *action_client, + size_t *goal_client_index, size_t *cancel_client_index, + size_t *result_client_index, size_t *feedback_subscription_index, + size_t *status_subscription_index) +{ + rcl_ret_t ret; + RCL_CHECK_ARGUMENT_FOR_NULL(wait_set, RCL_RET_WAIT_SET_INVALID); + if (!rcl_action_client_is_valid(action_client)) { + return RCL_RET_ACTION_CLIENT_INVALID; // error already set + } + + // Wait on action goal service response messages. + ret = rcl_wait_set_add_client(wait_set, &action_client->impl->goal_client, + &action_client->impl->wait_set_goal_client_index); + if (RCL_RET_OK != ret) { + return ret; + } + // Wait on action cancel service response messages. + ret = rcl_wait_set_add_client(wait_set, &action_client->impl->cancel_client, + &action_client->impl->wait_set_cancel_client_index); + if (RCL_RET_OK != ret) { + return ret; + } + // Wait on action result service response messages. + ret = rcl_wait_set_add_client(wait_set, &action_client->impl->result_client, + &action_client->impl->wait_set_result_client_index); + if (RCL_RET_OK != ret) { + return ret; + } + // Wait on action feedback messages. + ret = rcl_wait_set_add_subscription( + wait_set, &action_client->impl->feedback_subscription, + &action_client->impl->wait_set_feedback_subscription_index); + if (RCL_RET_OK != ret) { + return ret; + } + // Wait on action status messages. + ret = rcl_wait_set_add_subscription( + wait_set, &action_client->impl->status_subscription, &action_client->impl->wait_set_status_subscription_index); + if (RCL_RET_OK != ret) { + return ret; + } + + // Return all indices to caller + if (NULL != goal_client_index) { + *goal_client_index = action_client->impl->wait_set_goal_client_index; + } + if (NULL != cancel_client_index) { + *cancel_client_index = action_client->impl->wait_set_cancel_client_index; + } + if (NULL != result_client_index) { + *result_client_index = action_client->impl->wait_set_result_client_index; + } + if (NULL != feedback_subscription_index) { + *feedback_subscription_index = action_client->impl->wait_set_feedback_subscription_index; + } + if (NULL != status_subscription_index) { + *status_subscription_index = action_client->impl->wait_set_status_subscription_index; + } + return RCL_RET_OK; +} + +rcl_ret_t rcl_action_client_wait_set_get_entities_ready_with_indices( + const rcl_wait_set_t *wait_set, const rcl_action_client_t *action_client, + size_t feedback_subscription_index, size_t status_subscription_index, + size_t goal_client_index, size_t cancel_client_index, + size_t result_client_index, bool *is_feedback_ready, bool *is_status_ready, + bool *is_goal_response_ready, bool *is_cancel_response_ready, + bool *is_result_response_ready) +{ + RCL_CHECK_ARGUMENT_FOR_NULL(wait_set, RCL_RET_WAIT_SET_INVALID); + if (!rcl_action_client_is_valid(action_client)) { + return RCL_RET_ACTION_CLIENT_INVALID; // error already set + } + RCL_CHECK_ARGUMENT_FOR_NULL(is_feedback_ready, RCL_RET_INVALID_ARGUMENT); + RCL_CHECK_ARGUMENT_FOR_NULL(is_status_ready, RCL_RET_INVALID_ARGUMENT); + RCL_CHECK_ARGUMENT_FOR_NULL(is_goal_response_ready, RCL_RET_INVALID_ARGUMENT); + RCL_CHECK_ARGUMENT_FOR_NULL(is_cancel_response_ready, + RCL_RET_INVALID_ARGUMENT); + RCL_CHECK_ARGUMENT_FOR_NULL(is_result_response_ready, + RCL_RET_INVALID_ARGUMENT); + + // Bounds checking using provided indices + if (feedback_subscription_index >= wait_set->size_of_subscriptions) { + RCL_SET_ERROR_MSG( + "wait set index for feedback subscription is out of bounds"); + return RCL_RET_ERROR; + } + if (status_subscription_index >= wait_set->size_of_subscriptions) { + RCL_SET_ERROR_MSG( + "wait set index for status subscription is out of bounds"); + return RCL_RET_ERROR; + } + if (goal_client_index >= wait_set->size_of_clients) { + RCL_SET_ERROR_MSG("wait set index for goal client is out of bounds"); + return RCL_RET_ERROR; + } + if (cancel_client_index >= wait_set->size_of_clients) { + RCL_SET_ERROR_MSG("wait set index for cancel client is out of bounds"); + return RCL_RET_ERROR; + } + if (result_client_index >= wait_set->size_of_clients) { + RCL_SET_ERROR_MSG("wait set index for result client is out of bounds"); + return RCL_RET_ERROR; + } + + const rcl_action_client_impl_t *impl = action_client->impl; + const rcl_subscription_t *feedback_subscription = + wait_set->subscriptions[feedback_subscription_index]; + const rcl_subscription_t *status_subscription = + wait_set->subscriptions[status_subscription_index]; + const rcl_client_t *goal_client = wait_set->clients[goal_client_index]; + const rcl_client_t *cancel_client = wait_set->clients[cancel_client_index]; + const rcl_client_t *result_client = wait_set->clients[result_client_index]; + *is_feedback_ready = (&impl->feedback_subscription == feedback_subscription); + *is_status_ready = (&impl->status_subscription == status_subscription); + *is_goal_response_ready = (&impl->goal_client == goal_client); + *is_cancel_response_ready = (&impl->cancel_client == cancel_client); + *is_result_response_ready = (&impl->result_client == result_client); + return RCL_RET_OK; +} + +rcl_ret_t rcl_action_client_set_goal_client_callback( + const rcl_action_client_t *action_client, rcl_event_callback_t callback, + const void *user_data) { if (!rcl_action_client_is_valid(action_client)) { return RCL_RET_ACTION_CLIENT_INVALID; diff --git a/rcl_action/test/rcl_action/test_wait.cpp b/rcl_action/test/rcl_action/test_wait.cpp index 3529eef19..4f214c431 100644 --- a/rcl_action/test/rcl_action/test_wait.cpp +++ b/rcl_action/test/rcl_action/test_wait.cpp @@ -818,3 +818,290 @@ TEST_F(TestActionServerWait, test_server_wait_set_get_entities_ready) { EXPECT_TRUE(is_result_request_ready); EXPECT_TRUE(is_goal_expired); } + +// Tests for new thread-safe functions added to fix issue #1455 + +TEST_F(TestActionClientWait, test_wait_set_add_action_client_with_indices) { + // Test null wait_set + size_t goal_client_index = 42; + size_t cancel_client_index = 42; + size_t result_client_index = 42; + size_t feedback_subscription_index = 42; + size_t status_subscription_index = 42; + + rcl_ret_t ret = rcl_action_wait_set_add_action_client_with_indices( + nullptr, &action_client, &goal_client_index, &cancel_client_index, + &result_client_index, &feedback_subscription_index, + &status_subscription_index); + EXPECT_EQ(RCL_RET_WAIT_SET_INVALID, ret); + EXPECT_EQ(42u, goal_client_index); + EXPECT_EQ(42u, cancel_client_index); + EXPECT_EQ(42u, result_client_index); + EXPECT_EQ(42u, feedback_subscription_index); + EXPECT_EQ(42u, status_subscription_index); + rcl_reset_error(); + + rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set(); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT({ + EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set)) + << rcl_get_error_string().str; + }); + + // Test null action_client + ret = rcl_action_wait_set_add_action_client_with_indices( + &wait_set, nullptr, &goal_client_index, &cancel_client_index, + &result_client_index, &feedback_subscription_index, + &status_subscription_index); + EXPECT_EQ(RCL_RET_ACTION_CLIENT_INVALID, ret); + EXPECT_EQ(42u, goal_client_index); + rcl_reset_error(); + + // Test with wait_set that's too small (no capacity) + ret = rcl_wait_set_init(&wait_set, 0, 0, 0, 0, 0, 0, &this->context, + rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_OK, ret); + + ret = rcl_action_wait_set_add_action_client_with_indices( + &wait_set, &action_client, &goal_client_index, &cancel_client_index, + &result_client_index, &feedback_subscription_index, + &status_subscription_index); + EXPECT_EQ(RCL_RET_WAIT_SET_FULL, ret); + EXPECT_TRUE(rcl_error_is_set()); + rcl_reset_error(); + + // Typical case - wait_set with enough capacity (2 subscriptions, 3 clients) + EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set)) + << rcl_get_error_string().str; + ret = rcl_wait_set_init(&wait_set, 2, 0, 0, 3, 0, 0, &this->context, + rcl_get_default_allocator()); + ASSERT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + ret = rcl_action_wait_set_add_action_client_with_indices( + &wait_set, &action_client, &goal_client_index, &cancel_client_index, + &result_client_index, &feedback_subscription_index, + &status_subscription_index); + EXPECT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + // Check that all indices were set (they should be 0, 1, 2 for clients, 0, 1 + // for subscriptions) + EXPECT_EQ(0u, goal_client_index); + EXPECT_EQ(1u, cancel_client_index); + EXPECT_EQ(2u, result_client_index); + EXPECT_EQ(0u, feedback_subscription_index); + EXPECT_EQ(1u, status_subscription_index); + + // Test with null index pointers (should still work) + EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set)) + << rcl_get_error_string().str; + ret = rcl_wait_set_init(&wait_set, 2, 0, 0, 3, 0, 0, &this->context, + rcl_get_default_allocator()); + ASSERT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + ret = rcl_action_wait_set_add_action_client_with_indices( + &wait_set, &action_client, nullptr, nullptr, nullptr, nullptr, nullptr); + EXPECT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; +} + +TEST_F(TestActionClientWait, + test_client_wait_set_get_entities_ready_with_indices) { + const char *action_name = "test_action_client_name"; + const rosidl_action_type_support_t *action_typesupport = + ROSIDL_GET_ACTION_TYPE_SUPPORT(test_msgs, Fibonacci); + const rcl_action_client_options_t action_client_options = + rcl_action_client_get_default_options(); + + rcl_action_client_t action_client = rcl_action_get_zero_initialized_client(); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT({ + rcl_ret_t fini_ret = rcl_action_client_fini(&action_client, &this->node); + EXPECT_EQ(RCL_RET_OK, fini_ret) << rcl_get_error_string().str; + }); + + rcl_ret_t ret = + rcl_action_client_init(&action_client, &this->node, action_typesupport, + action_name, &action_client_options); + EXPECT_EQ(ret, RCL_RET_OK) << rcl_get_error_string().str; + rcl_reset_error(); + + rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set(); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT({ + EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set)) + << rcl_get_error_string().str; + }); + ret = rcl_wait_set_init(&wait_set, 2, 1, 1, 3, 1, 1, &this->context, + rcl_get_default_allocator()); + ASSERT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + bool is_feedback_ready = false; + bool is_status_ready = false; + bool is_goal_response_ready = false; + bool is_cancel_response_ready = false; + bool is_result_response_ready = false; + + // Test null wait_set + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + nullptr, &action_client, 0, 1, 0, 1, 2, &is_feedback_ready, + &is_status_ready, &is_goal_response_ready, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_WAIT_SET_INVALID); + rcl_reset_error(); + + // Test null action_client + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, nullptr, 0, 1, 0, 1, 2, &is_feedback_ready, &is_status_ready, + &is_goal_response_ready, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ACTION_CLIENT_INVALID); + rcl_reset_error(); + + // Test null output pointers + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 1, 0, 1, 2, nullptr, &is_status_ready, + &is_goal_response_ready, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_INVALID_ARGUMENT); + rcl_reset_error(); + + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 1, 0, 1, 2, &is_feedback_ready, nullptr, + &is_goal_response_ready, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_INVALID_ARGUMENT); + rcl_reset_error(); + + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 1, 0, 1, 2, &is_feedback_ready, + &is_status_ready, nullptr, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_INVALID_ARGUMENT); + rcl_reset_error(); + + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 1, 0, 1, 2, &is_feedback_ready, + &is_status_ready, &is_goal_response_ready, nullptr, + &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_INVALID_ARGUMENT); + rcl_reset_error(); + + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 1, 0, 1, 2, &is_feedback_ready, + &is_status_ready, &is_goal_response_ready, &is_cancel_response_ready, + nullptr); + EXPECT_EQ(ret, RCL_RET_INVALID_ARGUMENT); + rcl_reset_error(); + + // Test with out-of-bounds indices + wait_set.size_of_subscriptions = 1; + wait_set.size_of_clients = 1; + + // feedback_subscription_index out of bounds + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 10, 0, 0, 0, + 0, // feedback index 10 is out of bounds + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ERROR); + rcl_reset_error(); + + // status_subscription_index out of bounds + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 10, 0, 0, + 0, // status index 10 is out of bounds + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ERROR); + rcl_reset_error(); + + // goal_client_index out of bounds + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 0, 10, 0, + 0, // goal client index 10 is out of bounds + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ERROR); + rcl_reset_error(); + + // cancel_client_index out of bounds + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 0, 0, 10, + 0, // cancel client index 10 is out of bounds + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ERROR); + rcl_reset_error(); + + // result_client_index out of bounds + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 0, 0, 0, + 10, // result client index 10 is out of bounds + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(ret, RCL_RET_ERROR); + rcl_reset_error(); + + // Successful case with valid indices + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, 0, 0, 0, 0, + 0, // All indices are 0, which is valid for size 1 + &is_feedback_ready, &is_status_ready, &is_goal_response_ready, + &is_cancel_response_ready, &is_result_response_ready); + EXPECT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + // All entities should be not ready since the wait_set wasn't actually waited + // on + EXPECT_FALSE(is_feedback_ready); + EXPECT_FALSE(is_status_ready); + EXPECT_FALSE(is_goal_response_ready); + EXPECT_FALSE(is_cancel_response_ready); + EXPECT_FALSE(is_result_response_ready); +} + +TEST_F(TestActionClientWait, + test_add_and_get_entities_ready_with_indices_consistency) { + // This test verifies that indices returned by add_action_client_with_indices + // can be correctly used with get_entities_ready_with_indices + + rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set(); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT({ + EXPECT_EQ(RCL_RET_OK, rcl_wait_set_fini(&wait_set)) + << rcl_get_error_string().str; + }); + + // Initialize wait_set with enough capacity + rcl_ret_t ret = rcl_wait_set_init(&wait_set, 2, 0, 0, 3, 0, 0, &this->context, + rcl_get_default_allocator()); + ASSERT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + // Add action client and get indices + size_t goal_client_index = 0; + size_t cancel_client_index = 0; + size_t result_client_index = 0; + size_t feedback_subscription_index = 0; + size_t status_subscription_index = 0; + + ret = rcl_action_wait_set_add_action_client_with_indices( + &wait_set, &action_client, &goal_client_index, &cancel_client_index, + &result_client_index, &feedback_subscription_index, + &status_subscription_index); + ASSERT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + // Use those indices with get_entities_ready_with_indices + bool is_feedback_ready = false; + bool is_status_ready = false; + bool is_goal_response_ready = false; + bool is_cancel_response_ready = false; + bool is_result_response_ready = false; + + ret = rcl_action_client_wait_set_get_entities_ready_with_indices( + &wait_set, &action_client, feedback_subscription_index, + status_subscription_index, goal_client_index, cancel_client_index, + result_client_index, &is_feedback_ready, &is_status_ready, + &is_goal_response_ready, &is_cancel_response_ready, + &is_result_response_ready); + EXPECT_EQ(RCL_RET_OK, ret) << rcl_get_error_string().str; + + // All should be not ready (no data has been sent) + EXPECT_FALSE(is_feedback_ready); + EXPECT_FALSE(is_status_ready); + EXPECT_FALSE(is_goal_response_ready); + EXPECT_FALSE(is_cancel_response_ready); + EXPECT_FALSE(is_result_response_ready); +}