From bdedcf7aa4a78f44a3f7bfedeb566c89aa4762d8 Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Sun, 15 Dec 2024 00:19:15 +0100 Subject: [PATCH 1/6] Add tests to reproduce the issue of the failing activation --- controller_manager/CMakeLists.txt | 18 ++-- .../test/test_controller/test_controller.cpp | 31 +++++- ...st_controller_spawner_with_interfaces.yaml | 25 +++++ .../test/test_spawner_unspawner.cpp | 100 ++++++++++++++++++ 4 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 controller_manager/test/test_controller_spawner_with_interfaces.yaml diff --git a/controller_manager/CMakeLists.txt b/controller_manager/CMakeLists.txt index 1b0f308613..bc424fbcb7 100644 --- a/controller_manager/CMakeLists.txt +++ b/controller_manager/CMakeLists.txt @@ -213,17 +213,13 @@ if(BUILD_TESTING) ros2_control_test_assets::ros2_control_test_assets ) - install(FILES test/test_controller_spawner_with_fallback_controllers.yaml - DESTINATION test) - - install(FILES test/test_controller_spawner_with_type.yaml - DESTINATION test) - - install(FILES test/test_controller_overriding_parameters.yaml - DESTINATION test) - - install(FILES test/test_controller_spawner_wildcard_entries.yaml - DESTINATION test) + install(FILES + test/test_controller_spawner_with_type.yaml + test/test_controller_overriding_parameters.yaml + test/test_controller_spawner_with_fallback_controllers.yaml + test/test_controller_spawner_wildcard_entries.yaml + test/test_controller_spawner_with_interfaces.yaml + DESTINATION test) ament_add_gmock(test_hardware_management_srvs test/test_hardware_management_srvs.cpp diff --git a/controller_manager/test/test_controller/test_controller.cpp b/controller_manager/test/test_controller/test_controller.cpp index 04ae8c02c2..e2a1be0dae 100644 --- a/controller_manager/test/test_controller/test_controller.cpp +++ b/controller_manager/test/test_controller/test_controller.cpp @@ -87,7 +87,7 @@ controller_interface::return_type TestController::update( command_interfaces_[i].get_name().c_str()); return controller_interface::return_type::ERROR; } - RCLCPP_INFO( + RCLCPP_DEBUG( get_node()->get_logger(), "Setting value of command interface '%s' to %f", command_interfaces_[i].get_name().c_str(), external_commands_for_testing_[i]); command_interfaces_[i].set_value(external_commands_for_testing_[i]); @@ -101,6 +101,35 @@ CallbackReturn TestController::on_init() { return CallbackReturn::SUCCESS; } CallbackReturn TestController::on_configure(const rclcpp_lifecycle::State & /*previous_state*/) { + auto ctrl_node = get_node(); + if (!ctrl_node->has_parameter("command_interfaces")) + { + ctrl_node->declare_parameter("command_interfaces", std::vector({})); + } + if (!ctrl_node->has_parameter("state_interfaces")) + { + ctrl_node->declare_parameter("state_interfaces", std::vector({})); + } + const std::vector command_interfaces = + ctrl_node->get_parameter("command_interfaces").as_string_array(); + const std::vector state_interfaces = + ctrl_node->get_parameter("state_interfaces").as_string_array(); + if (!command_interfaces.empty() || !state_interfaces.empty()) + { + cmd_iface_cfg_.names.clear(); + state_iface_cfg_.names.clear(); + for (const auto & cmd_itf : command_interfaces) + { + cmd_iface_cfg_.names.push_back(cmd_itf); + } + cmd_iface_cfg_.type = controller_interface::interface_configuration_type::INDIVIDUAL; + external_commands_for_testing_.resize(command_interfaces.size(), 0.0); + for (const auto & state_itf : state_interfaces) + { + state_iface_cfg_.names.push_back(state_itf); + } + state_iface_cfg_.type = controller_interface::interface_configuration_type::INDIVIDUAL; + } return CallbackReturn::SUCCESS; } diff --git a/controller_manager/test/test_controller_spawner_with_interfaces.yaml b/controller_manager/test/test_controller_spawner_with_interfaces.yaml new file mode 100644 index 0000000000..89ea0c34aa --- /dev/null +++ b/controller_manager/test/test_controller_spawner_with_interfaces.yaml @@ -0,0 +1,25 @@ +ctrl_with_joint2_command_interface: + ros__parameters: + type: "controller_manager/test_controller" + command_interfaces: + - "joint2/velocity" + +ctrl_with_joint1_command_interface: + ros__parameters: + type: "controller_manager/test_controller" + command_interfaces: + - "joint1/position" + +ctrl_with_joint1_and_joint2_command_interfaces: + ros__parameters: + type: "controller_manager/test_controller" + command_interfaces: + - "joint1/position" + - "joint2/velocity" + +ctrl_with_state_interfaces: + ros__parameters: + type: "controller_manager/test_controller" + state_interfaces: + - "joint1/position" + - "joint2/position" \ No newline at end of file diff --git a/controller_manager/test/test_spawner_unspawner.cpp b/controller_manager/test/test_spawner_unspawner.cpp index 107c557dbb..02cb960628 100644 --- a/controller_manager/test/test_spawner_unspawner.cpp +++ b/controller_manager/test/test_spawner_unspawner.cpp @@ -437,6 +437,106 @@ TEST_F(TestLoadController, spawner_test_with_wildcard_entries_with_no_ctrl_name) verify_ctrl_parameter(wildcard_ctrl_1.c->get_node(), false); } +TEST_F(TestLoadController, spawner_test_failed_activation_of_controllers) +{ + const std::string test_file_path = ament_index_cpp::get_package_prefix("controller_manager") + + "/test/test_controller_spawner_with_interfaces.yaml"; + + ControllerManagerRunner cm_runner(this); + // Provide controller type via the parsed file + EXPECT_EQ( + call_spawner( + "ctrl_with_joint1_command_interface ctrl_with_joint2_command_interface -c " + "test_controller_manager " + "--controller-manager-timeout 1.0 " + "-p " + + test_file_path), + 0); + + ASSERT_EQ(cm_->get_loaded_controllers().size(), 2ul); + + auto ctrl_with_joint2_command_interface = cm_->get_loaded_controllers()[0]; + ASSERT_EQ(ctrl_with_joint2_command_interface.info.name, "ctrl_with_joint2_command_interface"); + ASSERT_EQ( + ctrl_with_joint2_command_interface.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME); + EXPECT_EQ( + ctrl_with_joint2_command_interface.c->get_lifecycle_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE); + ASSERT_EQ( + ctrl_with_joint2_command_interface.c->command_interface_configuration().names.size(), 1ul); + ASSERT_THAT( + ctrl_with_joint2_command_interface.c->command_interface_configuration().names, + std::vector({"joint2/velocity"})); + + auto ctrl_with_joint1_command_interface = cm_->get_loaded_controllers()[1]; + ASSERT_EQ(ctrl_with_joint1_command_interface.info.name, "ctrl_with_joint1_command_interface"); + ASSERT_EQ( + ctrl_with_joint1_command_interface.info.type, test_controller::TEST_CONTROLLER_CLASS_NAME); + EXPECT_EQ( + ctrl_with_joint1_command_interface.c->get_lifecycle_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE); + ASSERT_EQ( + ctrl_with_joint1_command_interface.c->command_interface_configuration().names.size(), 1ul); + ASSERT_THAT( + ctrl_with_joint1_command_interface.c->command_interface_configuration().names, + std::vector({"joint1/position"})); + + EXPECT_EQ( + call_spawner( + "ctrl_with_joint1_and_joint2_command_interfaces -c test_controller_manager " + "--controller-manager-timeout 1.0 " + "-p " + + test_file_path), + 256) + << "Should fail as the ctrl_with_joint1_command_interface and " + "ctrl_with_joint2_command_interface are active"; + + ASSERT_EQ(cm_->get_loaded_controllers().size(), 3ul); + + auto ctrl_with_joint1_and_joint2_command_interfaces = cm_->get_loaded_controllers()[0]; + ASSERT_EQ( + ctrl_with_joint1_and_joint2_command_interfaces.info.name, + "ctrl_with_joint1_and_joint2_command_interfaces"); + ASSERT_EQ( + ctrl_with_joint1_and_joint2_command_interfaces.info.type, + test_controller::TEST_CONTROLLER_CLASS_NAME); + ASSERT_EQ( + ctrl_with_joint1_and_joint2_command_interfaces.c->get_lifecycle_state().id(), + lifecycle_msgs::msg::State::PRIMARY_STATE_INACTIVE); + ASSERT_EQ( + ctrl_with_joint1_and_joint2_command_interfaces.c->command_interface_configuration() + .names.size(), + 2ul); + ASSERT_THAT( + ctrl_with_joint1_and_joint2_command_interfaces.c->command_interface_configuration().names, + std::vector({"joint1/position", "joint2/velocity"})); + + EXPECT_EQ(call_unspawner("ctrl_with_joint1_command_interface -c test_controller_manager"), 0); + + ASSERT_EQ(cm_->get_loaded_controllers().size(), 2ul); + EXPECT_EQ( + call_spawner( + "ctrl_with_joint1_and_joint2_command_interfaces -c test_controller_manager " + "--controller-manager-timeout 1.0 " + "-p " + + test_file_path), + 256) + << "Should fail as the ctrl_with_joint2_command_interface is still active"; + + EXPECT_EQ(call_unspawner("ctrl_with_joint2_command_interface -c test_controller_manager"), 0); + + ASSERT_EQ(cm_->get_loaded_controllers().size(), 1ul); + EXPECT_EQ( + call_spawner( + "ctrl_with_joint1_and_joint2_command_interfaces -c test_controller_manager " + "--controller-manager-timeout 1.0 " + "-p " + + test_file_path), + 0) + << "Should pass as the ctrl_with_joint1_command_interface and " + "ctrl_with_joint2_command_interface are inactive"; +} + TEST_F(TestLoadController, unload_on_kill) { // Launch spawner with unload on kill From 43ffcfc9dfe92ede4226214c3a944c4cb388c3a7 Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Sun, 15 Dec 2024 00:20:21 +0100 Subject: [PATCH 2/6] Add changes to fix the issue with the spawner --- .../controller_manager/spawner.py | 4 +-- controller_manager/src/controller_manager.cpp | 33 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/controller_manager/controller_manager/spawner.py b/controller_manager/controller_manager/spawner.py index c5a23defe4..0a4e5a4bdc 100644 --- a/controller_manager/controller_manager/spawner.py +++ b/controller_manager/controller_manager/spawner.py @@ -254,7 +254,7 @@ def main(args=None): ) if not ret.ok: node.get_logger().error( - bcolors.FAIL + "Failed to activate controller" + bcolors.ENDC + f"{bcolors.FAIL}Failed to activate controller : {controller_name}{bcolors.ENDC}" ) return 1 @@ -279,7 +279,7 @@ def main(args=None): ) if not ret.ok: node.get_logger().error( - bcolors.FAIL + "Failed to activate the parsed controllers list" + bcolors.ENDC + f"{bcolors.FAIL}Failed to activate the parsed controllers list : {controller_names}{bcolors.ENDC}" ) return 1 diff --git a/controller_manager/src/controller_manager.cpp b/controller_manager/src/controller_manager.cpp index 9f20e2b584..dafc650852 100644 --- a/controller_manager/src/controller_manager.cpp +++ b/controller_manager/src/controller_manager.cpp @@ -1468,6 +1468,7 @@ controller_interface::return_type ControllerManager::switch_controller( to = controllers; // update the claimed interface controller info + auto switch_result = controller_interface::return_type::OK; for (auto & controller : to) { if (is_controller_active(controller.c)) @@ -1488,6 +1489,28 @@ controller_interface::return_type ControllerManager::switch_controller( { controller.info.claimed_interfaces.clear(); } + if ( + std::find(activate_request_.begin(), activate_request_.end(), controller.info.name) != + activate_request_.end()) + { + if (!is_controller_active(controller.c)) + { + RCLCPP_ERROR( + get_logger(), "Could not activate controller : '%s'", controller.info.name.c_str()); + switch_result = controller_interface::return_type::ERROR; + } + } + if ( + std::find(deactivate_request_.begin(), deactivate_request_.end(), controller.info.name) != + deactivate_request_.end()) + { + if (is_controller_active(controller.c)) + { + RCLCPP_ERROR( + get_logger(), "Could not deactivate controller : '%s'", controller.info.name.c_str()); + switch_result = controller_interface::return_type::ERROR; + } + } } // switch lists @@ -1497,8 +1520,10 @@ controller_interface::return_type ControllerManager::switch_controller( clear_requests(); - RCLCPP_DEBUG(get_logger(), "Successfully switched controllers"); - return controller_interface::return_type::OK; + RCLCPP_DEBUG_EXPRESSION( + get_logger(), switch_result == controller_interface::return_type::OK, + "Successfully switched controllers"); + return switch_result; } controller_interface::ControllerInterfaceBaseSharedPtr ControllerManager::add_controller_impl( @@ -1730,6 +1755,7 @@ void ControllerManager::activate_controllers( get_logger(), "Resource conflict for controller '%s'. Command interface '%s' is already claimed.", controller_name.c_str(), command_interface.c_str()); + command_loans.clear(); assignment_successful = false; break; } @@ -1744,6 +1770,7 @@ void ControllerManager::activate_controllers( "Caught exception of type : %s while claiming the command interfaces. Can't activate " "controller '%s': %s", typeid(e).name(), controller_name.c_str(), e.what()); + command_loans.clear(); assignment_successful = false; break; } @@ -1813,6 +1840,7 @@ void ControllerManager::activate_controllers( RCLCPP_ERROR( get_logger(), "Caught exception of type : %s while activating the controller '%s': %s", typeid(e).name(), controller_name.c_str(), e.what()); + controller->release_interfaces(); continue; } catch (...) @@ -1820,6 +1848,7 @@ void ControllerManager::activate_controllers( RCLCPP_ERROR( get_logger(), "Caught unknown exception while activating the controller '%s'", controller_name.c_str()); + controller->release_interfaces(); continue; } From dbffae188f28ac5004acee99127c9f5a2de7d61e Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Sun, 15 Dec 2024 10:03:36 +0100 Subject: [PATCH 3/6] fix pre-commit --- .../test/test_controller_spawner_with_interfaces.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/controller_manager/test/test_controller_spawner_with_interfaces.yaml b/controller_manager/test/test_controller_spawner_with_interfaces.yaml index 89ea0c34aa..05b650c232 100644 --- a/controller_manager/test/test_controller_spawner_with_interfaces.yaml +++ b/controller_manager/test/test_controller_spawner_with_interfaces.yaml @@ -1,25 +1,25 @@ ctrl_with_joint2_command_interface: ros__parameters: type: "controller_manager/test_controller" - command_interfaces: + command_interfaces: - "joint2/velocity" ctrl_with_joint1_command_interface: ros__parameters: type: "controller_manager/test_controller" - command_interfaces: + command_interfaces: - "joint1/position" ctrl_with_joint1_and_joint2_command_interfaces: ros__parameters: type: "controller_manager/test_controller" - command_interfaces: + command_interfaces: - "joint1/position" - "joint2/velocity" ctrl_with_state_interfaces: ros__parameters: type: "controller_manager/test_controller" - state_interfaces: + state_interfaces: - "joint1/position" - - "joint2/position" \ No newline at end of file + - "joint2/position" From 44d2552238fa7a560908c0b066d895790283f57c Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Sun, 15 Dec 2024 11:28:48 +0100 Subject: [PATCH 4/6] Fix the other tests with the proper return code --- controller_manager/src/controller_manager.cpp | 9 +++++---- ...test_controllers_chaining_with_controller_manager.cpp | 5 ++--- controller_manager/test/test_release_interfaces.cpp | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/controller_manager/src/controller_manager.cpp b/controller_manager/src/controller_manager.cpp index dafc650852..eec1fe7bbc 100644 --- a/controller_manager/src/controller_manager.cpp +++ b/controller_manager/src/controller_manager.cpp @@ -1490,8 +1490,8 @@ controller_interface::return_type ControllerManager::switch_controller( controller.info.claimed_interfaces.clear(); } if ( - std::find(activate_request_.begin(), activate_request_.end(), controller.info.name) != - activate_request_.end()) + std::find(activate_controllers.begin(), activate_controllers.end(), controller.info.name) != + activate_controllers.end()) { if (!is_controller_active(controller.c)) { @@ -1501,8 +1501,9 @@ controller_interface::return_type ControllerManager::switch_controller( } } if ( - std::find(deactivate_request_.begin(), deactivate_request_.end(), controller.info.name) != - deactivate_request_.end()) + std::find( + deactivate_controllers.begin(), deactivate_controllers.end(), controller.info.name) != + deactivate_controllers.end()) { if (is_controller_active(controller.c)) { diff --git a/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp index c143ab4862..48f5509794 100644 --- a/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp +++ b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp @@ -1177,7 +1177,7 @@ TEST_P( switch_test_controllers( {DIFF_DRIVE_CONTROLLER, ODOM_PUBLISHER_CONTROLLER, SENSOR_FUSION_CONTROLLER}, {PID_RIGHT_WHEEL}, test_param.strictness, expected.at(test_param.strictness).future_status, - expected.at(test_param.strictness).return_type); + controller_interface::return_type::ERROR); // Preceding controller should stay deactivated and following controller // should be deactivated (if BEST_EFFORT) @@ -1508,8 +1508,7 @@ TEST_P( switch_test_controllers( {DIFF_DRIVE_CONTROLLER}, {PID_LEFT_WHEEL, PID_RIGHT_WHEEL}, test_param.strictness, - expected.at(test_param.strictness).future_status, - expected.at(test_param.strictness).return_type); + expected.at(test_param.strictness).future_status, controller_interface::return_type::ERROR); // Preceding controller should stay deactivated and following controller // should be deactivated (if BEST_EFFORT) diff --git a/controller_manager/test/test_release_interfaces.cpp b/controller_manager/test/test_release_interfaces.cpp index 9caef761ab..4dedb47241 100644 --- a/controller_manager/test/test_release_interfaces.cpp +++ b/controller_manager/test/test_release_interfaces.cpp @@ -84,7 +84,7 @@ TEST_F(TestReleaseInterfaces, switch_controllers_same_interface) ASSERT_EQ(std::future_status::timeout, switch_future.wait_for(std::chrono::milliseconds(100))) << "switch_controller should be blocking until next update cycle"; ControllerManagerRunner cm_runner(this); - EXPECT_EQ(controller_interface::return_type::OK, switch_future.get()); + EXPECT_EQ(controller_interface::return_type::ERROR, switch_future.get()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, abstract_test_controller1.c->get_lifecycle_state().id()); @@ -188,7 +188,7 @@ TEST_F(TestReleaseInterfaces, switch_controllers_same_interface) ASSERT_EQ(std::future_status::timeout, switch_future.wait_for(std::chrono::milliseconds(100))) << "switch_controller should be blocking until next update cycle"; ControllerManagerRunner cm_runner(this); - EXPECT_EQ(controller_interface::return_type::OK, switch_future.get()); + EXPECT_EQ(controller_interface::return_type::ERROR, switch_future.get()); ASSERT_EQ( lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE, abstract_test_controller1.c->get_lifecycle_state().id()); From 78056879a9afecb5221d7f0a7455671439745410 Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Sun, 15 Dec 2024 12:07:24 +0100 Subject: [PATCH 5/6] Update the fix logic to include the activation and deactivation list post filters --- controller_manager/src/controller_manager.cpp | 13 ++++++++----- ...controllers_chaining_with_controller_manager.cpp | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/controller_manager/src/controller_manager.cpp b/controller_manager/src/controller_manager.cpp index eec1fe7bbc..f357ba7aa9 100644 --- a/controller_manager/src/controller_manager.cpp +++ b/controller_manager/src/controller_manager.cpp @@ -1490,8 +1490,8 @@ controller_interface::return_type ControllerManager::switch_controller( controller.info.claimed_interfaces.clear(); } if ( - std::find(activate_controllers.begin(), activate_controllers.end(), controller.info.name) != - activate_controllers.end()) + std::find(activate_request_.begin(), activate_request_.end(), controller.info.name) != + activate_request_.end()) { if (!is_controller_active(controller.c)) { @@ -1500,10 +1500,13 @@ controller_interface::return_type ControllerManager::switch_controller( switch_result = controller_interface::return_type::ERROR; } } + /// @note The following is the case of the real controllers that are deactivated and doesn't + /// include the chained controllers that are deactivated and activated if ( - std::find( - deactivate_controllers.begin(), deactivate_controllers.end(), controller.info.name) != - deactivate_controllers.end()) + std::find(deactivate_request_.begin(), deactivate_request_.end(), controller.info.name) != + deactivate_request_.end() && + std::find(activate_request_.begin(), activate_request_.end(), controller.info.name) == + activate_request_.end()) { if (is_controller_active(controller.c)) { diff --git a/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp index 48f5509794..c143ab4862 100644 --- a/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp +++ b/controller_manager/test/test_controllers_chaining_with_controller_manager.cpp @@ -1177,7 +1177,7 @@ TEST_P( switch_test_controllers( {DIFF_DRIVE_CONTROLLER, ODOM_PUBLISHER_CONTROLLER, SENSOR_FUSION_CONTROLLER}, {PID_RIGHT_WHEEL}, test_param.strictness, expected.at(test_param.strictness).future_status, - controller_interface::return_type::ERROR); + expected.at(test_param.strictness).return_type); // Preceding controller should stay deactivated and following controller // should be deactivated (if BEST_EFFORT) @@ -1508,7 +1508,8 @@ TEST_P( switch_test_controllers( {DIFF_DRIVE_CONTROLLER}, {PID_LEFT_WHEEL, PID_RIGHT_WHEEL}, test_param.strictness, - expected.at(test_param.strictness).future_status, controller_interface::return_type::ERROR); + expected.at(test_param.strictness).future_status, + expected.at(test_param.strictness).return_type); // Preceding controller should stay deactivated and following controller // should be deactivated (if BEST_EFFORT) From 39c67da7d4880fd9e11068a8261f94480d05eab0 Mon Sep 17 00:00:00 2001 From: Sai Kishor Kothakota Date: Mon, 16 Dec 2024 18:00:34 +0100 Subject: [PATCH 6/6] Update controller_manager/test/test_spawner_unspawner.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christoph Fröhlich --- controller_manager/test/test_spawner_unspawner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller_manager/test/test_spawner_unspawner.cpp b/controller_manager/test/test_spawner_unspawner.cpp index 02cb960628..48ef5aaea5 100644 --- a/controller_manager/test/test_spawner_unspawner.cpp +++ b/controller_manager/test/test_spawner_unspawner.cpp @@ -489,7 +489,7 @@ TEST_F(TestLoadController, spawner_test_failed_activation_of_controllers) test_file_path), 256) << "Should fail as the ctrl_with_joint1_command_interface and " - "ctrl_with_joint2_command_interface are active"; + "ctrl_with_joint2_command_interface are active"; ASSERT_EQ(cm_->get_loaded_controllers().size(), 3ul);