diff --git a/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt b/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt index 050ca09..ba7618a 100644 --- a/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt +++ b/demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt @@ -74,6 +74,7 @@ install(DIRECTORY include/ DESTINATION include) if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) + include(ROS2MedkitTestDomain) ament_add_gtest(test_ota_update_plugin test/test_operation_dispatcher.cpp test/test_catalog_client.cpp @@ -81,6 +82,17 @@ if(BUILD_TESTING) ) target_link_libraries(test_ota_update_plugin ota_update_plugin_core) target_include_directories(test_ota_update_plugin PRIVATE src) + # The suite creates no ROS entity: the catalog client, the operation + # dispatcher and the plugin are exercised against doubles, and the one test + # that uses the real ProcessRunner spawns a path that does not exist. Nothing + # here touches DDS, so the test needs no domain of its own. + # + # Guarded, because ament_add_gtest registers nothing when the executable was + # not created, and declaring a property on a test that does not exist is a + # hard configure error rather than the skip ament intended. + if(TARGET test_ota_update_plugin) + medkit_test_needs_no_domain(test_ota_update_plugin) + endif() endif() ament_package() diff --git a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml index ad5e816..959487f 100644 --- a/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml +++ b/demos/ota_nav2_sensor_fix/ros2_packages/ota_nav2_sensor_fix_demo/config/nav2_params.yaml @@ -247,7 +247,30 @@ global_costmap: raytrace_max_range: 3.0 raytrace_min_range: 0.0 obstacle_max_range: 2.5 - obstacle_min_range: 0.0 + # Global planning ignores anything the scan reports closer than this, + # and that decides WHICH Nav2 node fails when the lidar regresses. + # The phantom sector broken_lidar overlays is fixed to the robot and + # reports a constant range, so without a floor the global costmap + # paints it around the robot's own position and navfn aborts with + # "failed to create plan" before the controller runs out of + # trajectories - and the log bridge watches controller_server, not + # planner_server, so the supporting fault would go missing. + # + # This is a range in the LASER frame, not a distance from the robot. + # The laser sits ~0.268 m ahead of base_footprint, so 0.35 here is + # ~0.62 m ahead of centre, against robot_radius 0.45 on this costmap: + # roughly 0.17 m in which a real obstacle is already outside the + # footprint and still invisible to global planning. That band is the + # local costmap's, which reads the same scan with no floor at all. + # Global plans are routing, so trading that band for the phantom is + # the right way round. + # + # The floor must stay above the phantom range, which lives as the + # phantom_range_m default (0.22) in broken_lidar_node.cpp and is set + # nowhere else - a launch override raising it past 0.35 puts the + # planner back in the failure path, and the narrative smoke test is + # what catches that. + obstacle_min_range: 0.35 static_layer: plugin: "nav2_costmap_2d::StaticLayer" map_subscribe_transient_local: True diff --git a/tests/smoke_test_demo_narrative.sh b/tests/smoke_test_demo_narrative.sh index e42700a..08d5491 100755 --- a/tests/smoke_test_demo_narrative.sh +++ b/tests/smoke_test_demo_narrative.sh @@ -28,8 +28,11 @@ # 1. Boot: broken_lidar_3_0_0 is applied (entrypoint auto-apply) and # scan_sensor_node is running broken_lidar_node; fixed_lidar_3_0_1 is # NOT yet registered (boot catalog holds only the bad update). -# 2. send-goal.sh -> ACTION_NAVIGATE_TO_POSE_ABORTED reaches CONFIRMED on -# bt-navigator, and controller-server picks up a supporting LOG_* fault. +# 2. Once action_status_bridge is watching /navigate_to_pose (the goal must +# not precede it - see wait_for_action_bridge), send-goal.sh -> +# ACTION_NAVIGATE_TO_POSE_ABORTED reaches CONFIRMED on bt-navigator, and +# controller-server picks up a supporting LOG_* fault whose message is +# the controller's own stall, not just any error. # 3. Fault detail (bt-navigator) has environment_data.snapshots >= 1, and # the rosbag bulk-data download returns a non-empty MCAP body. # 4. publish-fix.sh -> fixed_lidar_3_0_1 appears in /updates (SOVD @@ -68,6 +71,21 @@ CONTROLLER_ENTITY="apps/controller-server" # controller-server's LOG_CONTROLLER_SERVER_* code is content-hashed (derived # from the log message), so it is never matched by exact code - only by # "does this entity have any fault at all" (see fault_present with code=""). +# +# The message is matched instead, because "any fault on controller-server" is +# too weak for the one assertion that says WHICH Nav2 node failed. The log +# bridge promotes every controller_server ERROR at or above its severity floor, +# so a TF error or a lifecycle error would satisfy a bare count check. These +# messages are the controller saying it cannot move: the progress checker +# (FailedToMakeProgress), the controller patience (PatienceExceeded) and the +# controller having no legal trajectory left (NoValidControl, which carries +# DWB's own wording). The third only reaches ERROR when failure_tolerance is +# zero - above zero the controller retries and logs it at WARN until patience +# runs out - so it is here for the config, not for the run we expect. +# If the global costmap ever starts marking the phantom again, planner_server +# aborts the goal first, controller_server logs none of these, and this is +# the assertion that goes red. +CONTROLLER_STALL_MSG="Failed to make progress|Controller patience exceeded|Could not find a legal trajectory" # --- Helpers built on top of smoke_lib.sh's api_get/poll_until ------------- @@ -154,6 +172,31 @@ poll_process_running() { return 1 } +# Wait until action_status_bridge says it is watching navigate_to_pose. +# +# The goal must not be sent before this. The bridge fixes a fault's source on +# the FIRST status message it sees for an action, and the action status topic +# is transient-local: a bridge that subscribes while a goal is already in +# flight gets a latched sample immediately, before the ROS graph has resolved +# the publisher's node name. The source then stays the action name +# (/navigate_to_pose) instead of the server's node (/bt_navigator), and the +# fault never lands on the bt-navigator entity this test asserts on. +# +# demo.launch.py starts the bridges on a 15 s timer while send-goal.sh retries +# until nav2 accepts, so on a fast boot the goal wins that race. +wait_for_action_bridge() { + local timeout="${1:-60}" + local elapsed=0 + while [ $elapsed -lt "$timeout" ]; do + if docker logs "$GATEWAY_CONTAINER" 2>&1 | grep -q "Watching action '/navigate_to_pose'"; then + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + return 1 +} + # Poll until `pgrep -af ` fails inside the gateway container # (process gone), up to $2 seconds. poll_process_gone() { @@ -216,6 +259,15 @@ fi # --------------------------------------------------------------------- section "Reactive fault: send-goal.sh triggers ACTION_NAVIGATE_TO_POSE_ABORTED" +echo " Waiting for action_status_bridge to watch /navigate_to_pose (max 60s)..." +if wait_for_action_bridge 60; then + pass "action_status_bridge is watching /navigate_to_pose before the goal is sent" +else + fail "action_status_bridge is watching /navigate_to_pose before the goal is sent" \ + "bridge never reported the action within 60s - a goal sent now would be attributed to the action name, not to bt-navigator" + exit 1 +fi + # x=1.8, y=2.3 (frame map) drives straight into the phantom sector so nav2 # reliably stalls - the send-goal.sh script defaults elsewhere are for # ad-hoc operator use, not this repeatable regression check. @@ -231,12 +283,14 @@ else "fault never reached CONFIRMED within 60s - either nav2 didn't accept the goal or the action-status bridge is broken" fi -echo " Waiting for a supporting LOG_* fault on ${CONTROLLER_ENTITY} (max 60s)..." -if poll_until "/${CONTROLLER_ENTITY}/faults" '.items | length > 0' 60; then - pass "supporting LOG_* fault present on ${CONTROLLER_ENTITY}" +echo " Waiting for a supporting LOG_* stall fault on ${CONTROLLER_ENTITY} (max 60s)..." +if poll_until "/${CONTROLLER_ENTITY}/faults" \ + ".items[] | select(.description | test(\"${CONTROLLER_STALL_MSG}\"))" \ + 60; then + pass "supporting LOG_* fault present on ${CONTROLLER_ENTITY} and reports the controller stall" else - fail "supporting LOG_* fault present on ${CONTROLLER_ENTITY}" \ - "no fault appeared within 60s - either nav2 didn't stall or the log bridge is broken" + fail "supporting LOG_* fault present on ${CONTROLLER_ENTITY} and reports the controller stall" \ + "no fault matching '${CONTROLLER_STALL_MSG}' within 60s - either nav2 aborted somewhere other than the controller, or the log bridge is broken" fi # ---------------------------------------------------------------------