From 58e01b60fd2ae15a7d8c45998ed4504f2cad0832 Mon Sep 17 00:00:00 2001 From: Martin Pecka Date: Mon, 24 Aug 2026 00:58:12 +0200 Subject: [PATCH] launch: Added is-empty and not-empty substitutions. Assisted-by: qwen3.5-122b via Jetbrains Junie agent Signed-off-by: Martin Pecka --- launch/doc/source/architecture.rst | 11 +++ launch/launch/substitutions/__init__.py | 4 + .../substitutions/is_empty_substitution.py | 91 ++++++++++++++++++ .../substitutions/not_empty_substitution.py | 91 ++++++++++++++++++ .../test_is_empty_substitution.py | 54 +++++++++++ .../test_not_empty_substitution.py | 54 +++++++++++ .../launch_xml/test_is_empty_substitution.py | 92 +++++++++++++++++++ .../launch_xml/test_not_empty_substitution.py | 92 +++++++++++++++++++ .../launch_yaml/test_is_empty_substitution.py | 92 +++++++++++++++++++ .../test_not_empty_substitution.py | 92 +++++++++++++++++++ 10 files changed, 673 insertions(+) create mode 100644 launch/launch/substitutions/is_empty_substitution.py create mode 100644 launch/launch/substitutions/not_empty_substitution.py create mode 100644 launch/test/launch/substitutions/test_is_empty_substitution.py create mode 100644 launch/test/launch/substitutions/test_not_empty_substitution.py create mode 100644 launch_xml/test/launch_xml/test_is_empty_substitution.py create mode 100644 launch_xml/test/launch_xml/test_not_empty_substitution.py create mode 100644 launch_yaml/test/launch_yaml/test_is_empty_substitution.py create mode 100644 launch_yaml/test/launch_yaml/test_not_empty_substitution.py diff --git a/launch/doc/source/architecture.rst b/launch/doc/source/architecture.rst index c76aef835..4431d1ace 100644 --- a/launch/doc/source/architecture.rst +++ b/launch/doc/source/architecture.rst @@ -149,6 +149,17 @@ There are many possible variations of a substitution, but here are some of the c - This substitution locates the full path to an executable on the PATH if it exists. +- :class:`launch.substitutions.IsEmptySubstitution` + + - This substitution checks whether a string is empty, returning 'true' or 'false'. + - For example, ``$(is-empty $(var arg))`` returns 'true' if the launch configuration is empty. + +- :class:`launch.substitutions.NotEmptySubstitution` + + - This substitution checks whether a string is not empty, returning 'true' or 'false'. + - For example, ``$(not-empty $(var arg))`` returns 'true' if the launch configuration has content. + - This is the inverse of :class:`launch.substitutions.IsEmptySubstitution`. + The base substitution class provides some common introspection interfaces (which the specific derived substitutions may influence). The Launch Service diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index 7fa7170e7..fbc84d5be 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -28,9 +28,11 @@ from .for_loop_var import ForEachVar from .for_loop_var import ForLoopIndex from .if_else_substitution import IfElseSubstitution +from .is_empty_substitution import IsEmptySubstitution from .launch_configuration import LaunchConfiguration from .launch_log_dir import LaunchLogDir from .local_substitution import LocalSubstitution +from .not_empty_substitution import NotEmptySubstitution from .not_equals_substitution import NotEqualsSubstitution from .path_join_substitution import PathJoinSubstitution from .path_join_substitution import PathSubstitution @@ -54,9 +56,11 @@ 'ForEachVar', 'ForLoopIndex', 'IfElseSubstitution', + 'IsEmptySubstitution', 'LaunchConfiguration', 'LaunchLogDir', 'LocalSubstitution', + 'NotEmptySubstitution', 'NotSubstitution', 'NotEqualsSubstitution', 'OrSubstitution', diff --git a/launch/launch/substitutions/is_empty_substitution.py b/launch/launch/substitutions/is_empty_substitution.py new file mode 100644 index 000000000..3b1176df7 --- /dev/null +++ b/launch/launch/substitutions/is_empty_substitution.py @@ -0,0 +1,91 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for the IsEmptySubstitution substitution.""" + +from typing import Any +from typing import Dict +from typing import List +from typing import Sequence +from typing import Text +from typing import Tuple +from typing import Type + +from ..frontend.expose import expose_substitution +from ..launch_context import LaunchContext +from ..some_substitutions_type import SomeSubstitutionsType +from ..substitution import Substitution +from ..utilities import normalize_to_list_of_substitutions +from ..utilities import perform_substitutions + + +@expose_substitution('is-empty') +class IsEmptySubstitution(Substitution): + """ + Substitution that checks whether a string is empty. + + Returns 'true' or 'false' strings depending on whether the input is empty. + + For example, checking if a launch configuration is empty: + + .. code-block:: python + + is_empty = IsEmptySubstitution(LaunchConfiguration('arg')) + + .. code-block:: xml + + + + .. code-block:: yaml + + - let: + name: empty_check + value: "$(is-empty $(var arg))" + + This can be useful for conditional logic based on whether a string value + has content or not. + """ + + def __init__(self, value: SomeSubstitutionsType) -> None: + """ + Create an IsEmptySubstitution. + + :param value: string or substitutions whose emptiness is checked + """ + super().__init__() + self.__value = normalize_to_list_of_substitutions(value) + + @classmethod + def parse( + cls, data: Sequence[SomeSubstitutionsType] + ) -> Tuple[Type['IsEmptySubstitution'], Dict[str, Any]]: + """Parse `IsEmptySubstitution` substitution.""" + if len(data) != 1: + raise TypeError('is-empty substitution expects 1 argument') + return cls, {'value': data[0]} + + @property + def value(self) -> List[Substitution]: + """Getter for the value to check.""" + return self.__value + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return 'IsEmpty({})'.format( + ' + '.join([sub.describe() for sub in self.value])) + + def perform(self, context: LaunchContext) -> Text: + """Perform substitutions and check if the result is empty.""" + result = perform_substitutions(context, self.value) + return str(result == '').lower() diff --git a/launch/launch/substitutions/not_empty_substitution.py b/launch/launch/substitutions/not_empty_substitution.py new file mode 100644 index 000000000..a5711f0e2 --- /dev/null +++ b/launch/launch/substitutions/not_empty_substitution.py @@ -0,0 +1,91 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for the NotEmptySubstitution substitution.""" + +from typing import Any +from typing import Dict +from typing import List +from typing import Sequence +from typing import Text +from typing import Tuple +from typing import Type + +from ..frontend.expose import expose_substitution +from ..launch_context import LaunchContext +from ..some_substitutions_type import SomeSubstitutionsType +from ..substitution import Substitution +from ..utilities import normalize_to_list_of_substitutions +from ..utilities import perform_substitutions + + +@expose_substitution('not-empty') +class NotEmptySubstitution(Substitution): + """ + Substitution that checks whether a string is not empty. + + Returns 'true' or 'false' strings depending on whether the input has content. + + For example, checking if launch configuration is not an empty string: + + .. code-block:: python + + not_empty = NotEmptySubstitution(LaunchConfiguration('arg')) + + .. code-block:: xml + + + + .. code-block:: yaml + + - let: + name: not_empty_check + value: "$(not-empty $(var arg))" + + This can be useful for conditional logic based on whether a string value + has content or not. It is the inverse of IsEmptySubstitution. + """ + + def __init__(self, value: SomeSubstitutionsType) -> None: + """ + Create a NotEmptySubstitution. + + :param value: string or substitutions whose non-emptiness is checked + """ + super().__init__() + self.__value = normalize_to_list_of_substitutions(value) + + @classmethod + def parse( + cls, data: Sequence[SomeSubstitutionsType] + ) -> Tuple[Type['NotEmptySubstitution'], Dict[str, Any]]: + """Parse `NotEmptySubstitution` substitution.""" + if len(data) != 1: + raise TypeError('not-empty substitution expects 1 argument') + return cls, {'value': data[0]} + + @property + def value(self) -> List[Substitution]: + """Getter for the value to check.""" + return self.__value + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return 'NotEmpty({})'.format( + ' + '.join([sub.describe() for sub in self.value])) + + def perform(self, context: LaunchContext) -> Text: + """Perform substitutions and check if the result is not empty.""" + result = perform_substitutions(context, self.value) + return str(result != '').lower() diff --git a/launch/test/launch/substitutions/test_is_empty_substitution.py b/launch/test/launch/substitutions/test_is_empty_substitution.py new file mode 100644 index 000000000..8bda7c91d --- /dev/null +++ b/launch/test/launch/substitutions/test_is_empty_substitution.py @@ -0,0 +1,54 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the IsEmptySubstitution substitution class.""" + +from launch import LaunchContext +from launch.substitutions import IsEmptySubstitution +from launch.substitutions import TextSubstitution + +import pytest + + +@pytest.mark.parametrize('value, expected', [ + ('', 'true'), + ('robot', 'false'), + (' ', 'false'), + ('\t\n\r', 'false'), +]) +def test_is_empty(value, expected): + """Test checking if a string is empty.""" + substitution = IsEmptySubstitution(value) + assert substitution.perform(LaunchContext()) == expected + + +def test_is_empty_nested_substitutions(): + """Test checking if a value assembled from multiple substitutions is empty.""" + substitution = IsEmptySubstitution([ + TextSubstitution(text='robot'), + TextSubstitution(text=' name'), + ]) + assert substitution.perform(LaunchContext()) == 'false' + + +def test_is_empty_parse(): + """Test the frontend parser contract.""" + substitution_type, kwargs = IsEmptySubstitution.parse(['some value']) + assert substitution_type is IsEmptySubstitution + assert kwargs == {'value': 'some value'} + + with pytest.raises(TypeError, match='expects 1 argument'): + IsEmptySubstitution.parse([]) + with pytest.raises(TypeError, match='expects 1 argument'): + IsEmptySubstitution.parse(['one', 'two']) diff --git a/launch/test/launch/substitutions/test_not_empty_substitution.py b/launch/test/launch/substitutions/test_not_empty_substitution.py new file mode 100644 index 000000000..54107430a --- /dev/null +++ b/launch/test/launch/substitutions/test_not_empty_substitution.py @@ -0,0 +1,54 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the NotEmptySubstitution substitution class.""" + +from launch import LaunchContext +from launch.substitutions import NotEmptySubstitution +from launch.substitutions import TextSubstitution + +import pytest + + +@pytest.mark.parametrize('value, expected', [ + ('', 'false'), + ('robot', 'true'), + (' ', 'true'), + ('\t\n\r', 'true'), +]) +def test_not_empty(value, expected): + """Test checking if a string is not empty.""" + substitution = NotEmptySubstitution(value) + assert substitution.perform(LaunchContext()) == expected + + +def test_not_empty_nested_substitutions(): + """Test checking if a value assembled from multiple substitutions is not empty.""" + substitution = NotEmptySubstitution([ + TextSubstitution(text='robot'), + TextSubstitution(text=' name'), + ]) + assert substitution.perform(LaunchContext()) == 'true' + + +def test_not_empty_parse(): + """Test the frontend parser contract.""" + substitution_type, kwargs = NotEmptySubstitution.parse(['some value']) + assert substitution_type is NotEmptySubstitution + assert kwargs == {'value': 'some value'} + + with pytest.raises(TypeError, match='expects 1 argument'): + NotEmptySubstitution.parse([]) + with pytest.raises(TypeError, match='expects 1 argument'): + NotEmptySubstitution.parse(['one', 'two']) diff --git a/launch_xml/test/launch_xml/test_is_empty_substitution.py b/launch_xml/test/launch_xml/test_is_empty_substitution.py new file mode 100644 index 000000000..8d782a5a1 --- /dev/null +++ b/launch_xml/test/launch_xml/test_is_empty_substitution.py @@ -0,0 +1,92 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test parsing an IsEmptySubstitution in an XML launch file.""" + +import io +import textwrap + +from launch.actions import DeclareLaunchArgument +from launch.actions import SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import IsEmptySubstitution + + +def test_nested(): + xml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'false' + + +def test_nested_empty(): + xml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'true' + + +def test_empty_string(): + xml_file = textwrap.dedent( + """ + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 1 + assert isinstance(launch_description.entities[0], SetLaunchConfiguration) + + context = LaunchContext() + let = launch_description.entities[0] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'true' diff --git a/launch_xml/test/launch_xml/test_not_empty_substitution.py b/launch_xml/test/launch_xml/test_not_empty_substitution.py new file mode 100644 index 000000000..c44769e44 --- /dev/null +++ b/launch_xml/test/launch_xml/test_not_empty_substitution.py @@ -0,0 +1,92 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test parsing a NotEmptySubstitution in an XML launch file.""" + +import io +import textwrap + +from launch.actions import DeclareLaunchArgument +from launch.actions import SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import NotEmptySubstitution + + +def test_nested(): + xml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'true' + + +def test_nested_empty(): + xml_file = textwrap.dedent( + """ + + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'false' + + +def test_empty_string(): + xml_file = textwrap.dedent( + """ + + + + """ + ) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 1 + assert isinstance(launch_description.entities[0], SetLaunchConfiguration) + + context = LaunchContext() + let = launch_description.entities[0] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'false' diff --git a/launch_yaml/test/launch_yaml/test_is_empty_substitution.py b/launch_yaml/test/launch_yaml/test_is_empty_substitution.py new file mode 100644 index 000000000..807317ab5 --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_is_empty_substitution.py @@ -0,0 +1,92 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test parsing an IsEmptySubstitution in a YAML launch file.""" + +import io + +from launch.actions import DeclareLaunchArgument +from launch.actions import SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import IsEmptySubstitution + + +def test_nested(): + yaml_file = """\ +launch: + - arg: + name: robot_name + default: rover + - let: + name: is_empty + value: $(is-empty $(var robot_name)) +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'false' + + +def test_nested_empty(): + yaml_file = """\ +launch: + - arg: + name: robot_name + default: '' + - let: + name: is_empty + value: $(is-empty $(var robot_name)) +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'true' + + +def test_empty_string(): + yaml_file = """\ +launch: + - let: + name: is_empty + value: $(is-empty '') +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 1 + assert isinstance(launch_description.entities[0], SetLaunchConfiguration) + + context = LaunchContext() + let = launch_description.entities[0] + assert isinstance(let.value[0], IsEmptySubstitution) + assert let.value[0].perform(context) == 'true' diff --git a/launch_yaml/test/launch_yaml/test_not_empty_substitution.py b/launch_yaml/test/launch_yaml/test_not_empty_substitution.py new file mode 100644 index 000000000..39eedc015 --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_not_empty_substitution.py @@ -0,0 +1,92 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test parsing a NotEmptySubstitution in a YAML launch file.""" + +import io + +from launch.actions import DeclareLaunchArgument +from launch.actions import SetLaunchConfiguration +from launch.frontend import Parser +from launch.launch_context import LaunchContext +from launch.substitutions import NotEmptySubstitution + + +def test_nested(): + yaml_file = """\ +launch: + - arg: + name: robot_name + default: rover + - let: + name: not_empty + value: $(not-empty $(var robot_name)) +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'true' + + +def test_nested_empty(): + yaml_file = """\ +launch: + - arg: + name: robot_name + default: '' + - let: + name: not_empty + value: $(not-empty $(var robot_name)) +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 2 + assert isinstance(launch_description.entities[0], DeclareLaunchArgument) + assert isinstance(launch_description.entities[1], SetLaunchConfiguration) + + context = LaunchContext() + launch_description.entities[0].visit(context) + + let = launch_description.entities[1] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'false' + + +def test_empty_string(): + yaml_file = """\ +launch: + - let: + name: not_empty + value: $(not-empty '') +""" + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + launch_description = parser.parse_description(root_entity) + + assert len(launch_description.entities) == 1 + assert isinstance(launch_description.entities[0], SetLaunchConfiguration) + + context = LaunchContext() + let = launch_description.entities[0] + assert isinstance(let.value[0], NotEmptySubstitution) + assert let.value[0].perform(context) == 'false'