Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions demos/ota_nav2_sensor_fix/ota_update_plugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,25 @@ 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
test/test_plugin_smoke.cpp
)
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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
bburda marked this conversation as resolved.
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
Expand Down
68 changes: 61 additions & 7 deletions tests/smoke_test_demo_narrative.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -------------

Expand Down Expand Up @@ -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 <pattern>` fails inside the gateway container
# (process gone), up to $2 seconds.
poll_process_gone() {
Expand Down Expand Up @@ -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.
Expand All @@ -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

# ---------------------------------------------------------------------
Expand Down
Loading