Testing

curobo_ros ships 7 integration suites (config/config_test/*.yaml → generated test/test_*.py) plus the standard ament_copyright / ament_flake8 / ament_pep257 linters. This page covers how to run them and how the generator that produces them works.

Running the tests

The normal way — colcon test

colcon test --packages-select curobo_ros
colcon test-result --all --verbose

This runs all 10 tests (7 launch suites + 3 linters) and reports a JUnit summary. No special flags are needed.

A full run takes several minutes: each suite launches the real unified_planner node, waits for it to build its cuRobo solvers, then exercises services and topics against it.

One suite at a time — launch_test

Useful while iterating on a single YAML, since you see live output instead of waiting for the whole package:

launch_test test/test_test_kinematics.py

All suites, outside colcon — run_all_tests.sh

test/run_all_tests.sh

Generated alongside the suites; loops launch_test over all 7 with per-suite timeouts. Same tests as colcon test, without the linters and without a JUnit report.

The generator — ros2_test_compose

Nothing under test/ is hand-written except test_flake8.py / test_copyright.py / test_pep257.py. Everything else — the 7 test_test_*.py suites and run_all_tests.sh — is generated from config/config_test/*.yaml and carries a DO NOT EDIT banner. Never edit a generated file by hand: the fix belongs in the YAML, or in the generator itself if the YAML can’t express it. A hand-patched generated file silently regresses the next time anyone reruns the generator.

Regenerate after touching any config/config_test/*.yaml:

ros2 run ros2_test_compose test_generator --ros-args -p package_name:=curobo_ros

To check the generator itself hasn’t drifted (e.g. after changing ros2_test_compose), regenerate into a scratch copy and diff instead of overwriting in place — see the “Checking the generator hasn’t drifted” section of ros2_test_compose/README.md.

Key YAML settings (environment: block)

  • ready_service / ready_wait — how the suite knows the node is up. setUpClass polls get_service_names_and_types() until the named service is advertised (it is never called — observing the graph can’t hang even if a service callback is broken). unified_planner builds its solvers synchronously in __init__ (25–35 s), so all 7 curobo_ros YAMLs set:

    ready_service: "/unified_planner/generate_trajectory"
    ready_wait: 180.0
    startup_delay: 0.0
    

    There is no readiness service to call: the node exposes its state as the node_is_available parameter, and ready_service deliberately watches the graph instead.

  • allowable_exit_codes (optional, suite-wide, not per-test) — exit codes accepted besides 0. None of the curobo_ros YAMLs set it: all launched processes (planner + robot_state_publisher + static_transform_publisher + joint_state_publisher) exit 0 cleanly. Only add it once a suite is legitimately known to exit some other way (a GUI killed on shutdown, a node that doesn’t catch SIGINT).

  • wait_for_timeout (default 10 s) — per-test timeout for topic/service checks; a per-test timeout: key overrides it. Actions use a separate, larger action_timeout (default 60 s) since they run a whole trajectory.

  • Budget a solver rebuild generously. set_collision_cache, update_motion_gen_config, the first add_object and attach_object all rebuild and re-warm the solvers synchronously. Measured on an idle Jetson Orin that is ~25 s — but after ~30 minutes of sustained GPU load the SoC throttles and the same rebuild takes ~37 s. Every such call therefore uses timeout: 90.0. A budget sitting just above the idle cost does not detect a hang, it just turns the whole suite red on a warm or shared machine.

Full reference for every key: ros2_test_compose/README.md.

Known flakes

Two suites fail intermittently for reasons unrelated to what they assert. Both reproduce on an unchanged tree, so a single red run is not a regression signal — rerun the suite before investigating.

test_test_object.py::test_05_attach_object occasionally times out (Service call to '/unified_planner/attach_object' timed out). Root cause: MorphIt’s stochastic sphere-fit retries (morphit_sphere_fit: attempt N returned too few spheres), which can take longer than the test’s timeout on a slow attempt.

test_test_trajectory.py occasionally fails on test_exit_codes alone: every service assertion passes, then the planner process aborts (SIGABRT, exit -6) a few seconds after the shutdown SIGINT, during CUDA/solver teardown. Recognise it by the log — all numbered tests green, and process has died [... exit code -6] after sending signal 'SIGINT'. Nothing is wrong with the trajectories it just planned.

Next steps