Skip to content

Commit a993743

Browse files
committed
addressing #403
1 parent 6807d1d commit a993743

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ jobs:
139139
python-version: "3.12"
140140

141141
- name: Setup ROS 2
142+
# `ros-tooling/setup-ros@v0.7` still runs as a Node.js 20 action.
143+
# Force it onto Node 24 now so CI keeps working as GitHub deprecates
144+
# Node 20, and upgrade `setup-ros` to a Node 24-compatible release
145+
# when one becomes available.
146+
env:
147+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
142148
uses: ros-tooling/setup-ros@v0.7
143149
with:
144150
required-ros-distributions: jazzy

open-codegen/test/test_ros2.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,16 @@ def test_custom_ros2_configuration_is_rendered_into_generated_files(self):
125125
"""Custom ROS2 config values should appear in the generated package files."""
126126
ros2_dir = self.ros2_package_dir()
127127

128+
# The package metadata should reflect the user-provided ROS2 package name
129+
# and description, not the defaults from the templates.
128130
with open(os.path.join(ros2_dir, "package.xml"), encoding="utf-8") as f:
129131
package_xml = f.read()
130132
self.assertIn(f"<name>{self.PACKAGE_NAME}</name>", package_xml)
131133
self.assertIn(f"<description>{self.DESCRIPTION}</description>", package_xml)
132134

135+
# `open_optimizer.hpp` is where the generated node constants are wired in.
136+
# These assertions make sure the custom topic names, node name, rate, and
137+
# queue sizes are propagated into the generated C++ code.
133138
with open(os.path.join(ros2_dir, "include", "open_optimizer.hpp"), encoding="utf-8") as f:
134139
optimizer_header = f.read()
135140
self.assertIn(f'#define ROS2_NODE_{self.OPTIMIZER_NAME.upper()}_NODE_NAME "{self.NODE_NAME}"',
@@ -147,12 +152,16 @@ def test_custom_ros2_configuration_is_rendered_into_generated_files(self):
147152
f"#define ROS2_NODE_{self.OPTIMIZER_NAME.upper()}_PARAMS_TOPIC_QUEUE_SIZE {self.PARAMS_QUEUE_SIZE}",
148153
optimizer_header)
149154

155+
# The runtime YAML configuration should carry the custom topic names and
156+
# timer rate so the launched node uses the intended ROS2 parameters.
150157
with open(os.path.join(ros2_dir, "config", "open_params.yaml"), encoding="utf-8") as f:
151158
params_yaml = f.read()
152159
self.assertIn(f'result_topic: "{self.RESULT_TOPIC}"', params_yaml)
153160
self.assertIn(f'params_topic: "{self.PARAMS_TOPIC}"', params_yaml)
154161
self.assertIn(f"rate: {self.RATE}", params_yaml)
155162

163+
# The generated launch file should point to the correct package and
164+
# executable so `ros2 launch` can start the generated node.
156165
with open(os.path.join(ros2_dir, "launch", "open_optimizer.launch.py"), encoding="utf-8") as f:
157166
launch_file = f.read()
158167
self.assertIn(f'package="{self.PACKAGE_NAME}"', launch_file)
@@ -244,7 +253,11 @@ def ros2_test_env(cls):
244253
env = os.environ.copy()
245254
ros2_dir = cls.ros2_package_dir()
246255
os.makedirs(os.path.join(ros2_dir, ".ros_log"), exist_ok=True)
256+
# Keep ROS2 logs inside the generated package directory so the tests do
257+
# not depend on a global writable log location.
247258
env["ROS_LOG_DIR"] = os.path.join(ros2_dir, ".ros_log")
259+
# Fast DDS is the most reliable middleware choice in our CI/local test
260+
# setup when checking node discovery from separate processes.
248261
env.setdefault("RMW_IMPLEMENTATION", "rmw_fastrtps_cpp")
249262
env.pop("ROS_LOCALHOST_ONLY", None)
250263
return env
@@ -337,6 +350,9 @@ def _wait_for_node_and_topics(self, ros2_dir, env):
337350
node_result = None
338351
topic_result = None
339352
for _ in range(6):
353+
# `ros2 node list` confirms that the process joined the ROS graph,
354+
# while `ros2 topic list` confirms that the expected interfaces are
355+
# actually being advertised.
340356
node_result = self._run_shell(
341357
f"source {setup_script} && "
342358
"ros2 node list --no-daemon --spin-time 5",
@@ -364,13 +380,15 @@ def _wait_for_node_and_topics(self, ros2_dir, env):
364380

365381
def _assert_result_message(self, echo_stdout):
366382
"""Assert that the echoed result message indicates a successful solve."""
383+
# We do not compare the full numeric solution here; instead, we check
384+
# that the generated node returned a structurally valid result and that
385+
# the solver reported convergence.
367386
self.assertIn("solution", echo_stdout)
368-
# A bit of integration testing: check whether the solver was able to
369-
# solve the problem successfully.
370387
self.assertRegex(
371388
echo_stdout,
372389
r"solution:\s*\n(?:- .+\n)+",
373390
msg=f"Expected a non-empty solution vector in result output:\n{echo_stdout}")
391+
# `status: 0` matches `STATUS_CONVERGED` in the generated result message.
374392
self.assertIn("status: 0", echo_stdout)
375393
self.assertRegex(
376394
echo_stdout,
@@ -389,10 +407,12 @@ def _assert_result_message(self, echo_stdout):
389407
def _exercise_running_optimizer(self, ros2_dir, env):
390408
"""Publish one request and verify that one valid result message is returned."""
391409
_, setup_script = self.ros2_shell()
410+
# Start listening before publishing so the single response is not missed.
392411
echo_process = self._spawn_ros_process("ros2 topic echo /result --once", ros2_dir, env)
393412

394413
try:
395414
time.sleep(1)
415+
# Send one concrete request through the generated ROS2 interface.
396416
self._run_shell(
397417
f"source {setup_script} && "
398418
"ros2 topic pub --once /parameters "
@@ -411,6 +431,8 @@ def _exercise_running_optimizer(self, ros2_dir, env):
411431
def test_ros2_package_generation(self):
412432
"""Verify the ROS2 package files are generated."""
413433
ros2_dir = self.ros2_package_dir()
434+
# This is a lightweight smoke test for the generator itself before we
435+
# attempt the slower build/run integration tests below.
414436
self.assertTrue(os.path.isfile(os.path.join(ros2_dir, "package.xml")))
415437
self.assertTrue(os.path.isfile(os.path.join(ros2_dir, "CMakeLists.txt")))
416438
self.assertTrue(os.path.isfile(
@@ -420,6 +442,9 @@ def test_generated_ros2_package_works(self):
420442
"""Build, run, and call the generated ROS2 package."""
421443
ros2_dir = self.ros2_package_dir()
422444
env = self.ros2_test_env()
445+
446+
# First validate the plain `ros2 run` path, which exercises the
447+
# generated executable directly without going through the launch file.
423448
self._build_generated_package(ros2_dir, env)
424449

425450
node_process = self._spawn_ros_process(
@@ -438,6 +463,9 @@ def test_generated_ros2_launch_file_works(self):
438463
"""Build the package, launch the node, and verify the launch file works."""
439464
ros2_dir = self.ros2_package_dir()
440465
env = self.ros2_test_env()
466+
467+
# Then validate the generated launch description, which should bring up
468+
# the exact same node and parameters via `ros2 launch`.
441469
self._build_generated_package(ros2_dir, env)
442470

443471
launch_process = self._spawn_ros_process(

0 commit comments

Comments
 (0)