ROS 2 Launch Framework

repository·rolling·Indexed 41 days ago

https://github.com/ros2/launch

A system for orchestrating the execution of multiple processes in ROS 2. It includes tools for managing complex systems and integration testing via launch_pytest and launch_testing, as well as support for defining launch descriptions using XML (launch_xml) and YAML (launch_yaml).

Tokens
5.6K
Snippets
13
Records
24
Agent score
100%

What's inside ROS 2 Launch

  1. Overview of the ROS 2 Launch Framework

    rolling
    The ROS 2 Launch Framework provides tools for launching multiple processes simultaneously and for writing tests that involve multiple processes. It is designed to manage the lifecycle and execution of complex systems by orchestrating various nodes and components.
  2. How active tests and post-shutdown tests work

    rolling

    The launch_testing framework distinguishes between two types of tests:

    1. Active Tests: Any class inheriting from unittest.TestCase that is not decorated with @launch_testing.post_shutdown_test. These run concurrently with the launched processes and are used to interact with running nodes (e.g., checking stdout in real-time).

    2. Post-Shutdown Tests: Any class inheriting from unittest.TestCase decorated with @launch_testing.post_shutdown_test. These run after the launched processes have been shut down. They are used to verify exit codes, final stdout/stderr, or side-effects created by the processes.

  3. Access process output and exit codes in tests

    rolling

    The framework automatically injects two objects into your test cases to allow access to process information:

    • self.proc_info: A ProcInfoHandler object used to access exit codes and manage process shutdown.
    • self.proc_output: An IoHandler object used to access stdout and stderr.

    Both objects provide dictionary-like access to process information and methods to wait for specific events.

  4. Extend the launch framework via extension points

    rolling

    The launch framework provides extension points that allow external Python packages to add new functionality to the core categories. To extend the framework, your custom classes must follow these inheritance requirements:

    • New Actions: Must inherit from launch.Action.
    • New Events: Must inherit from launch.Event.
    • New Substitutions: Must inherit from launch.Substitution.
    • New Launch Entities: Must inherit from launch.LaunchDescriptionEntity.
  5. Implement custom Event Handlers

    rolling

    Event handlers allow you to react to specific lifecycle events within a launch session. To create a custom event handler, you must inherit from the launch.EventHandler base class and implement two required methods:

    1. matches(event): Receives the event as input. It must return True if the handler should react to this specific event, or False otherwise.
    2. handle(event, launch_context): Receives the event and the current launch_context. This method can perform side effects and optionally returns a list of launch.LaunchDescriptionEntity objects. If returned, these entities are visited by the launch service depth-first.

    Note that event handlers are not LaunchDescriptionEntity objects themselves, but they are 'visited' by the launch service during event processing.

  6. How Launch Entities and Launch Descriptions work

    rolling

    The core of the launch framework is built around LaunchDescriptionEntity objects. These entities capture the user's intent for how a system should be launched and how the system should react to asynchronous events.

    Key components include:

    • launch.LaunchDescriptionEntity: The base class for all entities that are "launched". They implement a visit method (called during execution) and describe methods (called during introspection).
    • launch.LaunchDescription: The root entity that encapsulates a list of launch.Action objects. It serves as the starting point for the launch process.
    • launch.Action: A type of entity that performs side effects (like running a process) or yields other actions. Actions can be introspected without being executed.
    • launch.Substitution: Values embedded within descriptions that are only evaluated at runtime (e.g., fetching an environment variable or a launch configuration).
  7. How to use @pytest.mark.launch for different test types

    rolling

    The behavior of your test depends on whether it is a standard function, a coroutine, a generator, or an async generator. Use the shutdown argument to control when the test runs relative to the launch service lifecycle.

    Standard Functions and Coroutines

    • Normal behavior: Runs concurrently with the launch service.
    • shutdown=True: Runs only after the launch service has shut down.
    • Note on Coroutines: Coroutines run in the same event loop as the launch description, while normal functions run in a separate thread.

    Generators and Async Generators

    Generators allow you to split a test into two phases: a concurrent phase and a post-shutdown phase.

    • Phase 1: The code before the yield runs concurrently with the launch service.
    • Phase 2: The code after the yield runs after the launch service has shut down.
    • Constraint: You cannot pass the shutdown argument to the decorator when using generators.
    • Note on Async Generators: Async generators run in the same event loop as the launch description.
    # Standard function (concurrent)
    @pytest.mark.launch(fixture=my_ld_fixture)
    def normal_test_case():
        pass
    
    # Standard function (after shutdown)
    @pytest.mark.launch(fixture=my_ld_fixture, shutdown=True)
    def shutdown_test_case():
        pass
    
    # Generator (concurrent phase -> yield -> post-shutdown phase)
    @pytest.mark.launch(fixture=my_ld_fixture)
    def generator_test_case():
        assert True  # Runs while service is active
        yield
        assert True  # Runs after service shuts down
  8. Access nested Entities and children

    rolling

    Attributes in the YAML tree can themselves be entities or lists of entities.

    • Nested Entities: If an attribute is an entity, you can access its properties using get_attr with data_type=List[Entity] or by accessing the attribute name directly as a property.
    • Children: You can access all child elements of an entity using the .children property. This returns a list of launch_xml.Entity objects wrapping each child.
    # Accessing a list of entities (e.g., an 'env' list)
    env = e.get_attr('env', data_type=List[Entity])
    len(env) == 2
    env[0].get_attr('name') == 'a'
    
    # Accessing children directly
    # Returns a list of Entity objects wrapping the children
    child_entities = e.children
  9. Define launch_pytest fixtures

    rolling

    A @launch_pytest.fixture must return a launch.LaunchDescription object or a sequence where the first item is a launch.LaunchDescription.

    Controlling Test Start Timing

    You can include launch_pytest.actions.ReadyToTest() in your LaunchDescription to signal exactly when the launch service is ready and the active tests should begin. If you do not add this action, one will be automatically appended to the end of your description.

    Fixture Scopes

    launch_pytest fixtures support module, class, or function scopes (default is function).

    • function scope: A new launch service is started and shut down for every single test case.
    • module scope: The launch service is started once for the entire module, and all test cases run concurrently with that same service.

    Best Practices:

    • Do not mix module scope fixtures with class or function scope fixtures in the same file.
    • Avoid using scopes larger than module.
    • A single test should not depend on more than one launch_pytest fixture.
    @launch_pytest.fixture
    def launch_description(hello_world_proc):
        """Launch a simple process to print 'hello_world'."""
        return launch.LaunchDescription([
            hello_world_proc,
            # Tell launch when to start the test
            # If no ReadyToTest action is added, one will be appended automatically.
            launch_pytest.actions.ReadyToTest()
        ])
    
    @pytest.mark.launch(fixture=launch_description)
    def test_case():
        pass
  10. Configure launch tests in CMake

    rolling

    To integrate launch tests into a ROS 2 package using CMake:

    1. Add launch_testing_ament_cmake to your package.xml dependencies.
    2. In CMakeLists.txt, use find_package and add_launch_test.

    Basic usage:

    find_package(launch_testing_ament_cmake)
    add_launch_test(test/name_of_test.test.py)

    Passing arguments via CMake:

    add_launch_test(
      test/test_with_args.test.py
      ARGS "arg1:=foo"
    )
  11. How to use the Launch Service

    rolling

    The launch.LaunchService is a long-running activity that manages the event loop, dispatches events to handlers, and executes actions.

    To use a LaunchService in a typical application, follow these steps:

    1. Create a LaunchDescription (programmatically or via a markup file).
    2. Instantiate a LaunchService.
    3. Include the LaunchDescription in the service.
    4. (Recommended) Add a signal handler (e.g., for SIGINT) that calls shutdown() on the service.
    5. Run the service's event loop.

    Note: You can include launch descriptions asynchronously from other threads (e.g., from a REST or ROS service) while the launch service is already running.

  12. Quick start with launch_pytest

    rolling

    To use launch_pytest for integration testing, create a pytest file that defines a @launch_pytest.fixture returning a launch.LaunchDescription. Mark your test cases with @pytest.mark.launch(fixture=<fixture_name>) to associate them with that launch description.

    Run your tests using the standard pytest module command:

    python3 -m pytest <path_to_test_file>
    python3 -m pytest test/launch_pytest/examples/pytest_hello_world.py