@@ -243,12 +243,64 @@ def setUpRos2PackageGeneration(cls):
243243 solver_configuration = cls .solverConfig ()) \
244244 .build ()
245245
246+ @classmethod
247+ def _inject_deterministic_solver_error (cls ):
248+ """Patch the generated solver so negative `p[0]` triggers a known error."""
249+ solver_root = os .path .join (cls .TEST_DIR , cls .OPTIMIZER_NAME )
250+ target_lib = os .path .join (solver_root , "src" , "lib.rs" )
251+ with open (target_lib , "r" , encoding = "utf-8" ) as fh :
252+ solver_lib = fh .read ()
253+
254+ if "forced solver error for ROS2 test" in solver_lib :
255+ return
256+
257+ anchor = (
258+ ' assert_eq!(u.len(), ROSENBROCK_ROS2_NUM_DECISION_VARIABLES, '
259+ '"Wrong number of decision variables (u)");\n '
260+ )
261+ injected_guard = (
262+ anchor +
263+ '\n '
264+ ' if p[0] < 0.0 {\n '
265+ ' return Err(SolverError::Cost("forced solver error for ROS2 test"));\n '
266+ ' }\n '
267+ )
268+ if anchor not in solver_lib :
269+ raise RuntimeError ("Could not inject deterministic solver error into ROS2 solver" )
270+
271+ with open (target_lib , "w" , encoding = "utf-8" ) as fh :
272+ fh .write (solver_lib .replace (anchor , injected_guard , 1 ))
273+
274+ @classmethod
275+ def _rebuild_generated_solver_library (cls ):
276+ """Rebuild the generated Rust solver and refresh the ROS2 static library."""
277+ solver_root = os .path .join (cls .TEST_DIR , cls .OPTIMIZER_NAME )
278+ process = subprocess .Popen (
279+ ["cargo" , "build" ],
280+ cwd = solver_root ,
281+ stdout = subprocess .PIPE ,
282+ stderr = subprocess .PIPE ,
283+ )
284+ _stdout , stderr = process .communicate ()
285+ if process .returncode != 0 :
286+ raise RuntimeError (
287+ "Could not rebuild generated ROS2 solver:\n {}" .format (stderr .decode ())
288+ )
289+
290+ generated_static_lib = os .path .join (
291+ solver_root , "target" , "debug" , f"lib{ cls .OPTIMIZER_NAME } .a" )
292+ ros2_static_lib = os .path .join (
293+ cls .ros2_package_dir (), "extern_lib" , f"lib{ cls .OPTIMIZER_NAME } .a" )
294+ shutil .copyfile (generated_static_lib , ros2_static_lib )
295+
246296 @classmethod
247297 def setUpClass (cls ):
248298 """Generate the ROS2 package once before all tests run."""
249299 if shutil .which ("ros2" ) is None or shutil .which ("colcon" ) is None :
250300 raise unittest .SkipTest ("ROS2 CLI tools are not available in PATH" )
251301 cls .setUpRos2PackageGeneration ()
302+ cls ._inject_deterministic_solver_error ()
303+ cls ._rebuild_generated_solver_library ()
252304
253305 @classmethod
254306 def ros2_package_dir (cls ):
@@ -443,6 +495,12 @@ def _assert_invalid_request_message(self, echo_stdout, error_code, error_message
443495 self .assertIn (f"error_code: { error_code } " , echo_stdout )
444496 self .assertIn (error_message_fragment , echo_stdout )
445497
498+ def _assert_solver_error_message (self , echo_stdout , error_message_fragment ):
499+ """Assert that the echoed result message reports a solver-side failure."""
500+ self .assertIn ("status: 3" , echo_stdout )
501+ self .assertIn ("error_code: 2000" , echo_stdout )
502+ self .assertIn (error_message_fragment , echo_stdout )
503+
446504 def _publish_request_and_collect_result (self , ros2_dir , env , request_payload ):
447505 """Publish one request and return one echoed result message."""
448506 _ , setup_script = self .ros2_shell ()
@@ -484,6 +542,38 @@ def _exercise_invalid_request(self, ros2_dir, env):
484542 3003 ,
485543 "wrong number of parameters" )
486544
545+ def _exercise_invalid_initial_guess (self , ros2_dir , env ):
546+ """Verify that invalid warm-start dimensions are reported clearly."""
547+ echo_stdout = self ._publish_request_and_collect_result (
548+ ros2_dir ,
549+ env ,
550+ "{parameter: [1.0, 2.0], initial_guess: [0.0], initial_y: [], initial_penalty: 15.0}" )
551+ self ._assert_invalid_request_message (
552+ echo_stdout ,
553+ 1600 ,
554+ "initial guess has incompatible dimensions" )
555+
556+ def _exercise_invalid_initial_y (self , ros2_dir , env ):
557+ """Verify that invalid multiplier dimensions are reported clearly."""
558+ echo_stdout = self ._publish_request_and_collect_result (
559+ ros2_dir ,
560+ env ,
561+ "{parameter: [1.0, 2.0], initial_guess: [0.0, 0.0, 0.0, 0.0, 0.0], initial_y: [0.0], initial_penalty: 15.0}" )
562+ self ._assert_invalid_request_message (
563+ echo_stdout ,
564+ 1700 ,
565+ "wrong dimension of Lagrange multipliers" )
566+
567+ def _exercise_solver_error (self , ros2_dir , env ):
568+ """Verify that solver-side failures propagate to the ROS2 result message."""
569+ echo_stdout = self ._publish_request_and_collect_result (
570+ ros2_dir ,
571+ env ,
572+ "{parameter: [-1.0, 2.0], initial_guess: [0.0, 0.0, 0.0, 0.0, 0.0], initial_y: [], initial_penalty: 15.0}" )
573+ self ._assert_solver_error_message (
574+ echo_stdout ,
575+ "forced solver error for ROS2 test" )
576+
487577 def test_ros2_package_generation (self ):
488578 """Verify the ROS2 package files are generated."""
489579 ros2_dir = self .ros2_package_dir ()
@@ -512,6 +602,9 @@ def test_generated_ros2_package_works(self):
512602 self ._wait_for_node_and_topics (ros2_dir , env , node_process )
513603 self ._exercise_running_optimizer (ros2_dir , env )
514604 self ._exercise_invalid_request (ros2_dir , env )
605+ self ._exercise_invalid_initial_guess (ros2_dir , env )
606+ self ._exercise_invalid_initial_y (ros2_dir , env )
607+ self ._exercise_solver_error (ros2_dir , env )
515608 self ._exercise_running_optimizer (ros2_dir , env )
516609 finally :
517610 if node_process .poll () is None :
@@ -535,6 +628,9 @@ def test_generated_ros2_launch_file_works(self):
535628 self ._wait_for_node_and_topics (ros2_dir , env , launch_process )
536629 self ._exercise_running_optimizer (ros2_dir , env )
537630 self ._exercise_invalid_request (ros2_dir , env )
631+ self ._exercise_invalid_initial_guess (ros2_dir , env )
632+ self ._exercise_invalid_initial_y (ros2_dir , env )
633+ self ._exercise_solver_error (ros2_dir , env )
538634 self ._exercise_running_optimizer (ros2_dir , env )
539635 finally :
540636 if launch_process .poll () is None :
0 commit comments