@@ -246,6 +246,110 @@ def ros_launch(
246246 ** kwargs
247247 )
248248
249+ _LAUNCH_TEST_MAIN_TEMPLATE = """\
250+ import os
251+ import sys
252+
253+ from python.runfiles import runfiles as runfiles_api
254+
255+ assert __name__ == "__main__"
256+ runfiles = runfiles_api.Create()
257+ launch_test_file = runfiles.Rlocation({launch_test_respath})
258+
259+ # This shim only ever runs inside a Bazel test sandbox. The sandbox sets
260+ # HOME=/does_not_exist (read-only) and the project .bazelrc sets ROS_HOME to
261+ # a similar sentinel. RCL log-dir priority: ROS_LOG_DIR > $ROS_HOME/log >
262+ # $HOME/.ros/log. Redirect all three unconditionally to the per-test tmpdir.
263+ _tmpdir = os.environ.get("TEST_TMPDIR", "/tmp")
264+ os.environ["ROS_LOG_DIR"] = _tmpdir
265+ os.environ["ROS_HOME"] = _tmpdir
266+ os.environ["HOME"] = _tmpdir
267+
268+ sys.argv = ["launch_test", launch_test_file] + sys.argv[1:]
269+
270+ from launch_testing.launch_test import main
271+ sys.exit(main())
272+ """
273+
274+ def ros_launch_test (
275+ name ,
276+ launch_test_file ,
277+ workspace_name = None ,
278+ executables = [],
279+ share = [],
280+ data = [],
281+ deps = [],
282+ rmw_implementation = None ,
283+ isolate = True ,
284+ ** kwargs ):
285+ """
286+ Builds a launch_testing test and wraps it with a shim that will inject the
287+ minimal runtime environment necessary for execution when depending on
288+ targets from this ROS 2 local repository.
289+
290+ The test file must export generate_test_description() and any number of
291+ unittest.TestCase subclasses following the launch_testing convention.
292+
293+ Args:
294+ name: test target name
295+ launch_test_file: label of the launch_testing test file
296+ workspace_name: optional ament package name override (defaults to
297+ the Bzlmod module name)
298+ executables: executable targets to register in the ament index,
299+ enabling Node(package=<pkg>, executable=...) lookups
300+ share: data file targets to register under share/<pkg>/, enabling
301+ FindPackageShare(<pkg>) and get_package_share_directory(<pkg>)
302+ data: additional runtime data deps
303+ deps: additional Python deps beyond launch_testing and launch
304+ rmw_implementation: optional RMW implementation to run against
305+ isolate: whether to use network namespace isolation (default True)
306+
307+ Additional keyword arguments are forwarded to ros_py_test.
308+ """
309+ main = "{}_launch_test_main.py" .format (name )
310+ launch_test_respath = _make_respath (launch_test_file , workspace_name )
311+
312+ generate_file (
313+ name = main ,
314+ content = _LAUNCH_TEST_MAIN_TEMPLATE .format (
315+ launch_test_respath = repr (launch_test_respath ),
316+ ),
317+ visibility = ["//visibility:private" ],
318+ testonly = True ,
319+ )
320+
321+ if executables or share :
322+ package_name = workspace_name if workspace_name != None else native .module_name ()
323+ index_target = "_{}_ament_index" .format (name )
324+ ament_index_share_files (
325+ name = index_target ,
326+ package_name = package_name ,
327+ executables = executables ,
328+ srcs = share ,
329+ visibility = ["//visibility:private" ],
330+ )
331+ data = data + executables + share + [":" + index_target ]
332+
333+ tags = list (kwargs .pop ("tags" , []))
334+ if "nolint" not in tags :
335+ tags .append ("nolint" )
336+
337+ ros_py_test (
338+ name = name ,
339+ srcs = [main ],
340+ main = main ,
341+ data = data + [launch_test_file ],
342+ deps = deps + [
343+ "@bazel_ros2_rules//deps/python/runfiles" ,
344+ "@ros2//:launch_testing_py" ,
345+ "@ros2//:launch_py" ,
346+ ],
347+ rmw_implementation = rmw_implementation ,
348+ isolate = isolate ,
349+ tags = tags ,
350+ ** kwargs
351+ )
352+
249353def ros_py_test (
250354 name ,
251355 rmw_implementation = None ,
0 commit comments