Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions launch/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
Changelog for package launch
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

3.10.0 (2026-05-07)
-------------------
* Robust pytest_ignore_collect for multi-version Pytest compatibility (`#974 <https://github.com/ros2/launch/issues/974>`_)
* Fix Pytest 8/9 compatibility and coroutine leaks in launch_pytest (`#972 <https://github.com/ros2/launch/issues/972>`_)
* Contributors: Michael Carroll

3.9.7 (2026-04-09)
------------------
* Correct typos (`#961 <https://github.com/ros2/launch/issues/961>`_)
* hotfix (`#950 <https://github.com/ros2/launch/issues/950>`_)
* Declare Boolean Launch Argument (`#944 <https://github.com/ros2/launch/issues/944>`_)
* Support frontends for PathJoinSubstitution (`#943 <https://github.com/ros2/launch/issues/943>`_)
* Contributors: Auguste Lalande, Christophe Bedard, David V. Lu!!, Michael Carlstrom

3.9.6 (2026-01-12)
------------------
* test python substitution with submodules (`#688 <https://github.com/ros2/launch/issues/688>`_)
Expand Down
37 changes: 28 additions & 9 deletions launch/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,33 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from pathlib import PurePath
import pathlib

import pytest

def pytest_ignore_collect(path):
# pytest doctest messes up when trying to import .launch.py packages, ignore them.
# It also messes up when trying to import launch.logging.handlers due to conflicts with
# logging.handlers, ignore that as well.
return str(path).endswith((
'.launch.py',
str(PurePath('logging') / 'handlers.py'),
))

def _pytest_version_ge(major, minor=0, patch=0):
"""Return True if pytest version is >= the given version."""
pytest_version = tuple(int(v) for v in pytest.__version__.split('.'))
return pytest_version >= (major, minor, patch)


def _should_ignore(p):
if p is None:
return False
path = pathlib.Path(p)
# Ignore .launch.py files — not valid Python module names.
if path.name.endswith('.launch.py'):
return True
# Ignore launch.logging.handlers — collides with stdlib logging.handlers.
if path.name == 'handlers.py' and path.parent.name == 'logging':
return True
return False


if _pytest_version_ge(8):
def pytest_ignore_collect(collection_path, config):
return _should_ignore(collection_path)
else:
def pytest_ignore_collect(path, config):
return _should_ignore(path)
2 changes: 1 addition & 1 deletion launch/launch/actions/execute_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ def execute(self, context: LaunchContext) -> None:
self.__shutdown_future = context.asyncio_loop.create_future()
self.__logger = launch.logging.get_logger(name)
if name is None:
raise RuntimeError('Cannot get Ouput Loggers with None name')
raise RuntimeError('Cannot get Output Loggers with None name')
if not isinstance(self.__output, dict):
self.__stdout_logger, self.__stderr_logger = \
launch.logging.get_output_loggers(
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/actions/execute_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,8 @@ def parse(

if 'additional_env' not in ignore:
# Conditions won't be allowed in the `env` tag.
# If that feature is needed, `set_enviroment_variable` and
# `unset_enviroment_variable` actions should be used.
# If that feature is needed, `set_environment_variable` and
# `unset_environment_variable` actions should be used.
env = entity.get_attr('env', data_type=List[Entity], optional=True)
if env is not None:
kwargs['additional_env'] = {
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/actions/for_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def generate_launch_description():
- log_info:
message: "'$(for-var name)' id=$(for-var id)"

The above examples would ouput the following log messages by default:
The above examples would output the following log messages by default:

.. code-block:: text

Expand Down Expand Up @@ -306,7 +306,7 @@ def generate_launch_description():
- log_info:
message: i=$(index i)

The above examples would ouput the following log messages by default:
The above examples would output the following log messages by default:

.. code-block:: text

Expand Down
1 change: 0 additions & 1 deletion launch/launch/event_handlers/on_process_exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from ..launch_context import LaunchContext
from ..some_entities_type import SomeEntitiesType


if TYPE_CHECKING:
from ..action import Action # noqa: F401
from ..actions import ExecuteLocal # noqa: F401
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/launch_introspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
from .utilities import normalize_to_list_of_substitutions


def indent(lines: List[Text], indention: Text = ' ') -> List[Text]:
def indent(lines: List[Text], indentation: Text = ' ') -> List[Text]:
"""Indent a list of strings and return them."""
return ['{}{}'.format(indention, line) for line in lines]
return ['{}{}'.format(indentation, line) for line in lines]


def tree_like_indent(lines: List[Text]) -> List[Text]:
Expand Down
2 changes: 1 addition & 1 deletion launch/launch/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2019 Open Source Robotics Foundation, Inc.
# Copyright 2019 Open Source Robotics Foundation, Inc. # noqa: A005
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion launch/launch/substitutions/environment_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def __init__(
@classmethod
def parse(cls, data: Sequence[SomeSubstitutionsType]
) -> Tuple[Type['EnvironmentVariable'], Dict[str, Any]]:
"""Parse `EnviromentVariable` substitution."""
"""Parse `EnvironmentVariable` substitution."""
if len(data) < 1 or len(data) > 2:
raise TypeError('env substitution expects 1 or 2 arguments')
kwargs = {'name': data[0]}
Expand Down
4 changes: 2 additions & 2 deletions launch/launch/substitutions/string_join_substitution.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class StringJoinSubstitution(Substitution):

This takes in a list of string components as substitutions.
The substitutions for each string component are performed and concatenated,
and then all string components are joined with a specified delimiter as seperation.
and then all string components are joined with a specified delimiter as separation.

For example:

Expand Down Expand Up @@ -76,7 +76,7 @@ def __init__(
Create a StringJoinSubstitution.

:param substitutions: the list of string component substitutions to join
:param delimiter: the text inbetween two consecutive components (default no text)
:param delimiter: the text in between two consecutive components (default no text)
"""
from ..utilities import normalize_to_list_of_substitutions

Expand Down
2 changes: 1 addition & 1 deletion launch/launch/utilities/type_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def get_typed_value(


# Unfortunately, mypy is unable to correctly infer that `is_substitution` can
# only return True when the passed tpe is either a substitution or a mixed
# only return True when the passed type is either a substitution or a mixed
# list of strings and substitutions. Indeed, there is no way that I could find
# using overloads to describe "anything else than the above two types".
# Can be redone properly with TypeIs once mypy version 1.10+ is available on all platforms
Expand Down
2 changes: 1 addition & 1 deletion launch/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>launch</name>
<version>3.9.6</version>
<version>3.10.0</version>
<description>The ROS launch tool.</description>

<maintainer email="aditya.pande@openrobotics.org">Aditya Pande</maintainer>
Expand Down
4 changes: 3 additions & 1 deletion launch/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[pytest]
junit_family=xunit2
addopts = --doctest-modules
# Disable doctest modules temporarily to avoid ImportPathMismatchError with logging.handlers
# addopts = --doctest-modules
timeout=900
timeout_method=thread
pythonpath = test
2 changes: 1 addition & 1 deletion launch/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name=package_name,
version='3.9.6',
version='3.10.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/' + package_name, ['package.xml']),
Expand Down
11 changes: 11 additions & 0 deletions launch_pytest/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
Changelog for package launch_pytest
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

3.10.0 (2026-05-07)
-------------------
* Fix Pytest 8/9 compatibility and coroutine leaks in launch_pytest (`#972 <https://github.com/ros2/launch/issues/972>`_)
* Contributors: Michael Carroll

3.9.7 (2026-04-09)
------------------
* fix regressions (`#959 <https://github.com/ros2/launch/issues/959>`_)
* fix: add get_launch_test_fixture_scope for pytest compatibility (`#949 <https://github.com/ros2/launch/issues/949>`_)
* Contributors: Daisuke Nishimatsu, Michael Carlstrom

3.9.6 (2026-01-12)
------------------

Expand Down
9 changes: 6 additions & 3 deletions launch_pytest/launch_pytest/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ def scope_gt(scope1, scope2):
from _pytest.fixtures import scopemismatch as scope_gt


def finalize_launch_service(launch_service, eprefix='', auto_shutdown=True):
def finalize_launch_service(launch_service, eprefix='', auto_shutdown=True, task=None):
if auto_shutdown:
launch_service.shutdown(force_sync=True)
loop = launch_service.event_loop
if loop is not None and not loop.is_closed():
rc = loop.run_until_complete(launch_service.task)
assert rc == 0, f"{eprefix} launch service failed when finishing, return code '{rc}'"
if task is None:
task = launch_service.task
if task is not None:
rc = loop.run_until_complete(task)
assert rc == 0, f"{eprefix} launch service failed when finishing, return code '{rc}'"


def get_launch_service_fixture(*, scope='function', overridable=True):
Expand Down
17 changes: 10 additions & 7 deletions launch_pytest/launch_pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,17 @@ def pytest_fixture_setup(fixturedef, request):
run_async_task = event_loop.create_task(ls.run_async(
shutdown_when_idle=options['shutdown_when_idle']
))
fixturedef.addfinalizer(functools.partial(
finalize_launch_service,
ls,
eprefix=eprefix,
auto_shutdown=options['auto_shutdown'],
task=run_async_task
))
ready = get_ready_to_test_action(ld)
asyncio.set_event_loop(event_loop)
event = asyncio.Event()
ready._add_callback(lambda: event.set())
fixturedef.addfinalizer(functools.partial(
finalize_launch_service, ls, eprefix=eprefix, auto_shutdown=options['auto_shutdown']))
run_until_complete(event_loop, event.wait())
# this is guaranteed by the current run_async() implementation, let's check it just in case
# it changes in the future
Expand Down Expand Up @@ -361,7 +366,7 @@ def pytest_pyfunc_call(pyfuncitem):
wrap_generator(func, event_loop, on_shutdown)
)
shutdown_item._fixtureinfo = shutdown_item.session._fixturemanager.getfixtureinfo(
shutdown_item, shutdown_item.obj, shutdown_item.cls, funcargs=True)
shutdown_item, shutdown_item.obj, shutdown_item.cls)
else:
pyfuncitem.obj = wrap_generator_fscope(func, event_loop, on_shutdown)
elif inspect.isasyncgenfunction(func):
Expand All @@ -371,7 +376,7 @@ def pytest_pyfunc_call(pyfuncitem):
wrap_asyncgen(func, event_loop, on_shutdown)
)
shutdown_item._fixtureinfo = shutdown_item.session._fixturemanager.getfixtureinfo(
shutdown_item, shutdown_item.obj, shutdown_item.cls, funcargs=True)
shutdown_item, shutdown_item.obj, shutdown_item.cls)
else:
pyfuncitem.obj = wrap_asyncgen_fscope(func, event_loop, on_shutdown)
elif not getattr(pyfuncitem.obj, '_launch_pytest_wrapped', False):
Expand Down Expand Up @@ -417,8 +422,7 @@ def wrap_generator(func, event_loop, on_shutdown):
"""Return wrappers for the normal test and the teardown test for a generator function."""
gen = None

def shutdown():
nonlocal gen
def shutdown(**kwargs):
if gen is None:
skip('shutdown test skipped because the test failed before')
on_shutdown()
Expand Down Expand Up @@ -469,7 +473,6 @@ def wrap_asyncgen(func, event_loop, on_shutdown):
agen = None

def shutdown(**kwargs):
nonlocal agen
if agen is None:
skip('shutdown test skipped because the test failed before')
on_shutdown()
Expand Down
2 changes: 1 addition & 1 deletion launch_pytest/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>launch_pytest</name>
<version>3.9.6</version>
<version>3.10.0</version>
<description>A package to create tests which involve launch files and multiple processes.</description>

<maintainer email="aditya.pande@openrobotics.org">Aditya Pande</maintainer>
Expand Down
2 changes: 1 addition & 1 deletion launch_pytest/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name=package_name,
version='3.9.6',
version='3.10.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages', [f'resource/{package_name}']),
Expand Down
13 changes: 13 additions & 0 deletions launch_testing/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
Changelog for package launch_testing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

3.10.0 (2026-05-07)
-------------------
* Robust pytest_ignore_collect for multi-version Pytest compatibility (`#974 <https://github.com/ros2/launch/issues/974>`_)
* Fix Pytest 8/9 compatibility and coroutine leaks in launch_pytest (`#972 <https://github.com/ros2/launch/issues/972>`_)
* Contributors: Michael Carroll

3.9.7 (2026-04-09)
------------------
* Correct typos (`#961 <https://github.com/ros2/launch/issues/961>`_)
* Fix test_io_tests for Ubuntu26 (`#960 <https://github.com/ros2/launch/issues/960>`_)
* Fix flake8 (`#952 <https://github.com/ros2/launch/issues/952>`_)
* Contributors: Auguste Lalande, Michael Carlstrom

3.9.6 (2026-01-12)
------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def assertInStdout(self, msg):

def get_nearby_lines(self):

# This works by concatinating a few of the process_io outputs that we received, then
# This works by concatenating a few of the process_io outputs that we received, then
# searching forward and backward for two return-lines in each direction, then returning
# just that portion to give context about where a failure happened

Expand Down
2 changes: 1 addition & 1 deletion launch_testing/launch_testing/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def retry_on_failure(*, times, delay=None):
"""
Mark a test case to be retried up to `times` on AssertionError.

:param times: The number of times to rety the test.
:param times: The number of times to retry the test.
:param delay: The time to wait between retries, in seconds.
A value of None will result in zero delay.
"""
Expand Down
Loading