From 5bff2efed7fc7d3da66c460b2367d2b900082784 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Sun, 27 Feb 2022 03:20:59 +0900 Subject: [PATCH 01/10] Add boolean substitutions Signed-off-by: Kenji Miyake --- launch/launch/substitutions/__init__.py | 6 + .../substitutions/boolean_substitution.py | 171 ++++++++++++++++++ .../test_boolean_substitution_frontend.py | 110 +++++++++++ .../test_boolean_substitution.py | 114 ++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 launch/launch/substitutions/boolean_substitution.py create mode 100644 launch/test/launch/frontend/test_boolean_substitution_frontend.py create mode 100644 launch/test/launch/substitutions/test_boolean_substitution.py diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index d631bbbdd..8195ba242 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -15,6 +15,9 @@ """Package for substitutions.""" from .anon_name import AnonName +from .boolean_substitution import AndSubstitution +from .boolean_substitution import NotSubstitution +from .boolean_substitution import OrSubstitution from .command import Command from .environment_variable import EnvironmentVariable from .find_executable import FindExecutable @@ -28,12 +31,15 @@ from .this_launch_file_dir import ThisLaunchFileDir __all__ = [ + 'AndSubstitution', 'AnonName', 'Command', 'EnvironmentVariable', 'FindExecutable', 'LaunchConfiguration', 'LocalSubstitution', + 'NotSubstitution', + 'OrSubstitution', 'PathJoinSubstitution', 'PythonExpression', 'SubstitutionFailure', diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py new file mode 100644 index 000000000..1e08e8733 --- /dev/null +++ b/launch/launch/substitutions/boolean_substitution.py @@ -0,0 +1,171 @@ +# Copyright 2022 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 boolean substitutions.""" + +from typing import Iterable +from typing import Text + +from .substitution_failure import SubstitutionFailure +from ..conditions import evaluate_condition_expression +from ..conditions import InvalidConditionExpressionError +from ..frontend 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') +class NotSubstitution(Substitution): + """Substitution that returns 'not' of the input boolean value.""" + + def __init__(self, value: SomeSubstitutionsType) -> None: + """Create a NotSubstitution substitution.""" + super().__init__() + + self.__value = normalize_to_list_of_substitutions(value) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `NotSubstitution` substitution.""" + if len(data) != 1: + raise TypeError('not substitution expects 1 argument') + return cls, {'value': data[0]} + + @property + def value(self) -> Substitution: + """Getter for value.""" + return self.__value + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'NotSubstitution({self.value})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + condition_expression = normalize_to_list_of_substitutions( + perform_substitutions(context, self.value) + ) + condition = evaluate_condition_expression(context, condition_expression) + except InvalidConditionExpressionError: + raise SubstitutionFailure(f'invalid condition expression: {self.value}') + + return str(not condition) + + +@expose_substitution('and') +class AndSubstitution(Substitution): + """Substitution that returns 'and' of the input boolean values.""" + + def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: + """Create a AndSubstitution substitution.""" + super().__init__() + + self.__left = normalize_to_list_of_substitutions(left) + self.__right = normalize_to_list_of_substitutions(right) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AndSubstitution` substitution.""" + if len(data) != 2: + raise TypeError('and substitution expects 2 arguments') + return cls, {'left': data[0], 'right': data[1]} + + @property + def left(self) -> Substitution: + """Getter for left.""" + return self.__left + + @property + def right(self) -> Substitution: + """Getter for right.""" + return self.__right + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AndSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + condition_expression = normalize_to_list_of_substitutions( + perform_substitutions(context, self.left) + ) + left_condition = evaluate_condition_expression(context, condition_expression) + except InvalidConditionExpressionError: + raise SubstitutionFailure(f'invalid condition expression: {self.left}') + try: + condition_expression = normalize_to_list_of_substitutions( + perform_substitutions(context, self.right) + ) + right_condition = evaluate_condition_expression(context, condition_expression) + except InvalidConditionExpressionError: + raise SubstitutionFailure(f'invalid condition expression: {self.right}') + + return str(left_condition and right_condition) + + +@expose_substitution('or') +class OrSubstitution(Substitution): + """Substitution that returns 'or' of the input boolean values.""" + + def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: + """Create a AndSubstitution substitution.""" + super().__init__() + + self.__left = normalize_to_list_of_substitutions(left) + self.__right = normalize_to_list_of_substitutions(right) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AndSubstitution` substitution.""" + if len(data) != 2: + raise TypeError('and substitution expects 2 arguments') + return cls, {'left': data[0], 'right': data[1]} + + @property + def left(self) -> Substitution: + """Getter for left.""" + return self.__left + + @property + def right(self) -> Substitution: + """Getter for right.""" + return self.__right + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AndSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + try: + condition_expression = normalize_to_list_of_substitutions( + perform_substitutions(context, self.left) + ) + left_condition = evaluate_condition_expression(context, condition_expression) + except InvalidConditionExpressionError: + raise SubstitutionFailure(f'invalid condition expression: {self.left}') + try: + condition_expression = normalize_to_list_of_substitutions( + perform_substitutions(context, self.right) + ) + right_condition = evaluate_condition_expression(context, condition_expression) + except InvalidConditionExpressionError: + raise SubstitutionFailure(f'invalid condition expression: {self.right}') + + return str(left_condition or right_condition) diff --git a/launch/test/launch/frontend/test_boolean_substitution_frontend.py b/launch/test/launch/frontend/test_boolean_substitution_frontend.py new file mode 100644 index 000000000..ce8813de8 --- /dev/null +++ b/launch/test/launch/frontend/test_boolean_substitution_frontend.py @@ -0,0 +1,110 @@ +# Copyright 2022 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. + +import io +import textwrap + +from launch import LaunchService +from launch.frontend import Parser +from launch.utilities import perform_substitutions + + +def test_boolean_substitution_yaml(): + yaml_file = textwrap.dedent( + r""" + launch: + - let: { name: true_value, value: "true" } + - let: { name: false_value, value: "false" } + + - let: { name: not_true, value: "$(not $(var true_value))" } + - let: { name: not_false, value: "$(not $(var false_value))" } + + - let: { name: and_true_true, value: "$(and $(var true_value) $(var true_value))" } + - let: { name: and_true_false, value: "$(and $(var true_value) $(var false_value))" } + - let: { name: and_false_true, value: "$(and $(var false_value) $(var true_value))" } + - let: { name: and_false_false, value: "$(and $(var false_value) $(var false_value))" } + + - let: { name: or_true_true, value: "$(or $(var true_value) $(var true_value))" } + - let: { name: or_true_false, value: "$(or $(var true_value) $(var false_value))" } + - let: { name: or_false_true, value: "$(or $(var false_value) $(var true_value))" } + - let: { name: or_false_false, value: "$(or $(var false_value) $(var false_value))" } + """ + ) + with io.StringIO(yaml_file) as f: + check_boolean_substitution(f) + + +def test_boolean_substitution_xml(): + xml_file = textwrap.dedent( + r""" + + + + + + + + + + + + + + + + + + """ + ) + with io.StringIO(xml_file) as f: + check_boolean_substitution(f) + + +def check_boolean_substitution(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + assert 0 == ls.run() + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + sub_entries = ld.describe_sub_entities() + + not_true = sub_entries[2] + not_false = sub_entries[3] + + and_true_true = sub_entries[4] + and_true_false = sub_entries[5] + and_false_true = sub_entries[6] + and_false_false = sub_entries[7] + + or_true_true = sub_entries[8] + or_true_false = sub_entries[9] + or_false_true = sub_entries[10] + or_false_false = sub_entries[11] + + assert perform(not_true.value) == 'False' + assert perform(not_false.value) == 'True' + + assert perform(and_true_true.value) == 'True' + assert perform(and_true_false.value) == 'False' + assert perform(and_false_true.value) == 'False' + assert perform(and_false_false.value) == 'False' + + assert perform(or_true_true.value) == 'True' + assert perform(or_true_false.value) == 'True' + assert perform(or_false_true.value) == 'True' + assert perform(or_false_false.value) == 'False' diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py new file mode 100644 index 000000000..ac6737d83 --- /dev/null +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -0,0 +1,114 @@ +# Copyright 2022 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 AnonName substitution class.""" + +from launch import LaunchContext + +from launch.substitutions import AndSubstitution +from launch.substitutions import NotSubstitution +from launch.substitutions import OrSubstitution +from launch.substitutions.substitution_failure import SubstitutionFailure + +import pytest + + +@pytest.mark.parametrize('value', ['True', 'true', '1']) +def test_not_substitution_true(value): + lc = LaunchContext() + assert NotSubstitution(value).perform(lc) == 'False' + + +@pytest.mark.parametrize('value', ['False', 'false', '0']) +def test_not_substitution_false(value): + lc = LaunchContext() + assert NotSubstitution(value).perform(lc) == 'True' + + +def test_not_substitution_invalid(): + lc = LaunchContext() + with pytest.raises(SubstitutionFailure): + NotSubstitution('not-condition-expression').perform(lc) + + +@pytest.mark.parametrize('left', ['True', 'true', '1']) +@pytest.mark.parametrize('right', ['True', 'true', '1']) +def test_and_substitution_both_true(left, right): + lc = LaunchContext() + assert AndSubstitution(left, right).perform(lc) == 'True' + + +@pytest.mark.parametrize('left', ['True', 'true', '1']) +@pytest.mark.parametrize('right', ['False', 'false', '0']) +def test_and_substitution_left_true(left, right): + lc = LaunchContext() + assert AndSubstitution(left, right).perform(lc) == 'False' + + +@pytest.mark.parametrize('left', ['False', 'false', '0']) +@pytest.mark.parametrize('right', ['True', 'true', '1']) +def test_and_substitution_right_true(left, right): + lc = LaunchContext() + assert AndSubstitution(left, right).perform(lc) == 'False' + + +@pytest.mark.parametrize('left', ['False', 'false', '0']) +@pytest.mark.parametrize('right', ['False', 'false', '0']) +def test_and_substitution_both_false(left, right): + lc = LaunchContext() + assert AndSubstitution(left, right).perform(lc) == 'False' + + +def test_and_substitution_invalid(): + lc = LaunchContext() + with pytest.raises(SubstitutionFailure): + AndSubstitution('not-condition-expression', 'True').perform(lc) + with pytest.raises(SubstitutionFailure): + AndSubstitution('True', 'not-condition-expression').perform(lc) + + +@pytest.mark.parametrize('left', ['True', 'true', '1']) +@pytest.mark.parametrize('right', ['True', 'true', '1']) +def test_or_substitution_both_true(left, right): + lc = LaunchContext() + assert OrSubstitution(left, right).perform(lc) == 'True' + + +@pytest.mark.parametrize('left', ['True', 'true', '1']) +@pytest.mark.parametrize('right', ['False', 'false', '0']) +def test_or_substitution_left_true(left, right): + lc = LaunchContext() + assert OrSubstitution(left, right).perform(lc) == 'True' + + +@pytest.mark.parametrize('left', ['False', 'false', '0']) +@pytest.mark.parametrize('right', ['True', 'true', '1']) +def test_or_substitution_right_true(left, right): + lc = LaunchContext() + assert OrSubstitution(left, right).perform(lc) == 'True' + + +@pytest.mark.parametrize('left', ['False', 'false', '0']) +@pytest.mark.parametrize('right', ['False', 'false', '0']) +def test_or_substitution_both_false(left, right): + lc = LaunchContext() + assert OrSubstitution(left, right).perform(lc) == 'False' + + +def test_or_substitution_invalid(): + lc = LaunchContext() + with pytest.raises(SubstitutionFailure): + OrSubstitution('not-condition-expression', 'True').perform(lc) + with pytest.raises(SubstitutionFailure): + OrSubstitution('True', 'not-condition-expression').perform(lc) From a223ec33ad2d349d2dba8a0dbabf5ae4d157215d Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 4 Mar 2022 22:36:47 +0900 Subject: [PATCH 02/10] Add lower() to the output Signed-off-by: Kenji Miyake --- .../substitutions/boolean_substitution.py | 6 ++--- .../test_boolean_substitution_frontend.py | 24 +++++++++---------- .../test_boolean_substitution.py | 20 ++++++++-------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index 1e08e8733..9edceda13 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -64,7 +64,7 @@ def perform(self, context: LaunchContext) -> Text: except InvalidConditionExpressionError: raise SubstitutionFailure(f'invalid condition expression: {self.value}') - return str(not condition) + return str(not condition).lower() @expose_substitution('and') @@ -116,7 +116,7 @@ def perform(self, context: LaunchContext) -> Text: except InvalidConditionExpressionError: raise SubstitutionFailure(f'invalid condition expression: {self.right}') - return str(left_condition and right_condition) + return str(left_condition and right_condition).lower() @expose_substitution('or') @@ -168,4 +168,4 @@ def perform(self, context: LaunchContext) -> Text: except InvalidConditionExpressionError: raise SubstitutionFailure(f'invalid condition expression: {self.right}') - return str(left_condition or right_condition) + return str(left_condition or right_condition).lower() diff --git a/launch/test/launch/frontend/test_boolean_substitution_frontend.py b/launch/test/launch/frontend/test_boolean_substitution_frontend.py index ce8813de8..c525c5231 100644 --- a/launch/test/launch/frontend/test_boolean_substitution_frontend.py +++ b/launch/test/launch/frontend/test_boolean_substitution_frontend.py @@ -96,15 +96,15 @@ def perform(substitution): or_false_true = sub_entries[10] or_false_false = sub_entries[11] - assert perform(not_true.value) == 'False' - assert perform(not_false.value) == 'True' - - assert perform(and_true_true.value) == 'True' - assert perform(and_true_false.value) == 'False' - assert perform(and_false_true.value) == 'False' - assert perform(and_false_false.value) == 'False' - - assert perform(or_true_true.value) == 'True' - assert perform(or_true_false.value) == 'True' - assert perform(or_false_true.value) == 'True' - assert perform(or_false_false.value) == 'False' + assert perform(not_true.value) == 'false' + assert perform(not_false.value) == 'true' + + assert perform(and_true_true.value) == 'true' + assert perform(and_true_false.value) == 'false' + assert perform(and_false_true.value) == 'false' + assert perform(and_false_false.value) == 'false' + + assert perform(or_true_true.value) == 'true' + assert perform(or_true_false.value) == 'true' + assert perform(or_false_true.value) == 'true' + assert perform(or_false_false.value) == 'false' diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index ac6737d83..3982f7b20 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -27,13 +27,13 @@ @pytest.mark.parametrize('value', ['True', 'true', '1']) def test_not_substitution_true(value): lc = LaunchContext() - assert NotSubstitution(value).perform(lc) == 'False' + assert NotSubstitution(value).perform(lc) == 'false' @pytest.mark.parametrize('value', ['False', 'false', '0']) def test_not_substitution_false(value): lc = LaunchContext() - assert NotSubstitution(value).perform(lc) == 'True' + assert NotSubstitution(value).perform(lc) == 'true' def test_not_substitution_invalid(): @@ -46,28 +46,28 @@ def test_not_substitution_invalid(): @pytest.mark.parametrize('right', ['True', 'true', '1']) def test_and_substitution_both_true(left, right): lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'True' + assert AndSubstitution(left, right).perform(lc) == 'true' @pytest.mark.parametrize('left', ['True', 'true', '1']) @pytest.mark.parametrize('right', ['False', 'false', '0']) def test_and_substitution_left_true(left, right): lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'False' + assert AndSubstitution(left, right).perform(lc) == 'false' @pytest.mark.parametrize('left', ['False', 'false', '0']) @pytest.mark.parametrize('right', ['True', 'true', '1']) def test_and_substitution_right_true(left, right): lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'False' + assert AndSubstitution(left, right).perform(lc) == 'false' @pytest.mark.parametrize('left', ['False', 'false', '0']) @pytest.mark.parametrize('right', ['False', 'false', '0']) def test_and_substitution_both_false(left, right): lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'False' + assert AndSubstitution(left, right).perform(lc) == 'false' def test_and_substitution_invalid(): @@ -82,28 +82,28 @@ def test_and_substitution_invalid(): @pytest.mark.parametrize('right', ['True', 'true', '1']) def test_or_substitution_both_true(left, right): lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'True' + assert OrSubstitution(left, right).perform(lc) == 'true' @pytest.mark.parametrize('left', ['True', 'true', '1']) @pytest.mark.parametrize('right', ['False', 'false', '0']) def test_or_substitution_left_true(left, right): lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'True' + assert OrSubstitution(left, right).perform(lc) == 'true' @pytest.mark.parametrize('left', ['False', 'false', '0']) @pytest.mark.parametrize('right', ['True', 'true', '1']) def test_or_substitution_right_true(left, right): lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'True' + assert OrSubstitution(left, right).perform(lc) == 'true' @pytest.mark.parametrize('left', ['False', 'false', '0']) @pytest.mark.parametrize('right', ['False', 'false', '0']) def test_or_substitution_both_false(left, right): lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'False' + assert OrSubstitution(left, right).perform(lc) == 'false' def test_or_substitution_invalid(): From e160b939b3efbeb02ac0f6566086e8387207ec7f Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 4 Mar 2022 22:41:01 +0900 Subject: [PATCH 03/10] Move test file Signed-off-by: Kenji Miyake --- .../launch_xml/test_boolean_substitution.py | 25 ------------------- 1 file changed, 25 deletions(-) rename launch/test/launch/frontend/test_boolean_substitution_frontend.py => launch_xml/test/launch_xml/test_boolean_substitution.py (71%) diff --git a/launch/test/launch/frontend/test_boolean_substitution_frontend.py b/launch_xml/test/launch_xml/test_boolean_substitution.py similarity index 71% rename from launch/test/launch/frontend/test_boolean_substitution_frontend.py rename to launch_xml/test/launch_xml/test_boolean_substitution.py index c525c5231..59fecde14 100644 --- a/launch/test/launch/frontend/test_boolean_substitution_frontend.py +++ b/launch_xml/test/launch_xml/test_boolean_substitution.py @@ -20,31 +20,6 @@ from launch.utilities import perform_substitutions -def test_boolean_substitution_yaml(): - yaml_file = textwrap.dedent( - r""" - launch: - - let: { name: true_value, value: "true" } - - let: { name: false_value, value: "false" } - - - let: { name: not_true, value: "$(not $(var true_value))" } - - let: { name: not_false, value: "$(not $(var false_value))" } - - - let: { name: and_true_true, value: "$(and $(var true_value) $(var true_value))" } - - let: { name: and_true_false, value: "$(and $(var true_value) $(var false_value))" } - - let: { name: and_false_true, value: "$(and $(var false_value) $(var true_value))" } - - let: { name: and_false_false, value: "$(and $(var false_value) $(var false_value))" } - - - let: { name: or_true_true, value: "$(or $(var true_value) $(var true_value))" } - - let: { name: or_true_false, value: "$(or $(var true_value) $(var false_value))" } - - let: { name: or_false_true, value: "$(or $(var false_value) $(var true_value))" } - - let: { name: or_false_false, value: "$(or $(var false_value) $(var false_value))" } - """ - ) - with io.StringIO(yaml_file) as f: - check_boolean_substitution(f) - - def test_boolean_substitution_xml(): xml_file = textwrap.dedent( r""" From bb3b09af86c866155070feb2bc294a361a10a396 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 4 Mar 2022 23:22:38 +0900 Subject: [PATCH 04/10] Use perform_typed_substitution and simplify the code Signed-off-by: Kenji Miyake --- .../substitutions/boolean_substitution.py | 46 ++++------ .../test_boolean_substitution.py | 85 +++---------------- 2 files changed, 29 insertions(+), 102 deletions(-) diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index 9edceda13..b7810d976 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -26,6 +26,7 @@ from ..substitution import Substitution from ..utilities import normalize_to_list_of_substitutions from ..utilities import perform_substitutions +from ..utilities.type_utils import perform_typed_substitution @expose_substitution('not') @@ -57,12 +58,9 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - condition_expression = normalize_to_list_of_substitutions( - perform_substitutions(context, self.value) - ) - condition = evaluate_condition_expression(context, condition_expression) - except InvalidConditionExpressionError: - raise SubstitutionFailure(f'invalid condition expression: {self.value}') + condition = perform_typed_substitution(context, self.value, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) return str(not condition).lower() @@ -102,19 +100,13 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - condition_expression = normalize_to_list_of_substitutions( - perform_substitutions(context, self.left) - ) - left_condition = evaluate_condition_expression(context, condition_expression) - except InvalidConditionExpressionError: - raise SubstitutionFailure(f'invalid condition expression: {self.left}') + left_condition = perform_typed_substitution(context, self.left, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) try: - condition_expression = normalize_to_list_of_substitutions( - perform_substitutions(context, self.right) - ) - right_condition = evaluate_condition_expression(context, condition_expression) - except InvalidConditionExpressionError: - raise SubstitutionFailure(f'invalid condition expression: {self.right}') + right_condition = perform_typed_substitution(context, self.right, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) return str(left_condition and right_condition).lower() @@ -154,18 +146,12 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - condition_expression = normalize_to_list_of_substitutions( - perform_substitutions(context, self.left) - ) - left_condition = evaluate_condition_expression(context, condition_expression) - except InvalidConditionExpressionError: - raise SubstitutionFailure(f'invalid condition expression: {self.left}') + left_condition = perform_typed_substitution(context, self.left, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) try: - condition_expression = normalize_to_list_of_substitutions( - perform_substitutions(context, self.right) - ) - right_condition = evaluate_condition_expression(context, condition_expression) - except InvalidConditionExpressionError: - raise SubstitutionFailure(f'invalid condition expression: {self.right}') + right_condition = perform_typed_substitution(context, self.right, bool) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) return str(left_condition or right_condition).lower() diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index 3982f7b20..49d9cca7d 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -24,90 +24,31 @@ import pytest -@pytest.mark.parametrize('value', ['True', 'true', '1']) -def test_not_substitution_true(value): - lc = LaunchContext() - assert NotSubstitution(value).perform(lc) == 'false' - - -@pytest.mark.parametrize('value', ['False', 'false', '0']) -def test_not_substitution_false(value): - lc = LaunchContext() - assert NotSubstitution(value).perform(lc) == 'true' - - -def test_not_substitution_invalid(): +def test_not_substitution(): lc = LaunchContext() + assert NotSubstitution('true').perform(lc) == 'false' + assert NotSubstitution('false').perform(lc) == 'true' with pytest.raises(SubstitutionFailure): NotSubstitution('not-condition-expression').perform(lc) - -@pytest.mark.parametrize('left', ['True', 'true', '1']) -@pytest.mark.parametrize('right', ['True', 'true', '1']) -def test_and_substitution_both_true(left, right): - lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'true' - - -@pytest.mark.parametrize('left', ['True', 'true', '1']) -@pytest.mark.parametrize('right', ['False', 'false', '0']) -def test_and_substitution_left_true(left, right): - lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'false' - - -@pytest.mark.parametrize('left', ['False', 'false', '0']) -@pytest.mark.parametrize('right', ['True', 'true', '1']) -def test_and_substitution_right_true(left, right): - lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'false' - - -@pytest.mark.parametrize('left', ['False', 'false', '0']) -@pytest.mark.parametrize('right', ['False', 'false', '0']) -def test_and_substitution_both_false(left, right): - lc = LaunchContext() - assert AndSubstitution(left, right).perform(lc) == 'false' - - -def test_and_substitution_invalid(): +def test_and_substitution(): lc = LaunchContext() + assert AndSubstitution('true', 'true').perform(lc) == 'true' + assert AndSubstitution('true', 'false').perform(lc) == 'false' + assert AndSubstitution('false', 'true').perform(lc) == 'false' + assert AndSubstitution('false', 'false').perform(lc) == 'false' with pytest.raises(SubstitutionFailure): AndSubstitution('not-condition-expression', 'True').perform(lc) with pytest.raises(SubstitutionFailure): AndSubstitution('True', 'not-condition-expression').perform(lc) -@pytest.mark.parametrize('left', ['True', 'true', '1']) -@pytest.mark.parametrize('right', ['True', 'true', '1']) -def test_or_substitution_both_true(left, right): - lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'true' - - -@pytest.mark.parametrize('left', ['True', 'true', '1']) -@pytest.mark.parametrize('right', ['False', 'false', '0']) -def test_or_substitution_left_true(left, right): - lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'true' - - -@pytest.mark.parametrize('left', ['False', 'false', '0']) -@pytest.mark.parametrize('right', ['True', 'true', '1']) -def test_or_substitution_right_true(left, right): - lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'true' - - -@pytest.mark.parametrize('left', ['False', 'false', '0']) -@pytest.mark.parametrize('right', ['False', 'false', '0']) -def test_or_substitution_both_false(left, right): - lc = LaunchContext() - assert OrSubstitution(left, right).perform(lc) == 'false' - - -def test_or_substitution_invalid(): +def test_or_substitution(): lc = LaunchContext() + assert OrSubstitution('true', 'true').perform(lc) == 'true' + assert OrSubstitution('true', 'false').perform(lc) == 'true' + assert OrSubstitution('false', 'true').perform(lc) == 'true' + assert OrSubstitution('false', 'false').perform(lc) == 'false' with pytest.raises(SubstitutionFailure): OrSubstitution('not-condition-expression', 'True').perform(lc) with pytest.raises(SubstitutionFailure): From 375486a65fe4a463d386950898d12d735ae2dbd6 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Sat, 5 Mar 2022 17:47:28 +0900 Subject: [PATCH 05/10] fix for flake8 Signed-off-by: Kenji Miyake --- launch/launch/substitutions/boolean_substitution.py | 3 --- launch/test/launch/substitutions/test_boolean_substitution.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index b7810d976..016c844fd 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -18,14 +18,11 @@ from typing import Text from .substitution_failure import SubstitutionFailure -from ..conditions import evaluate_condition_expression -from ..conditions import InvalidConditionExpressionError from ..frontend 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 from ..utilities.type_utils import perform_typed_substitution diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index 49d9cca7d..406cc7f37 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -31,6 +31,7 @@ def test_not_substitution(): with pytest.raises(SubstitutionFailure): NotSubstitution('not-condition-expression').perform(lc) + def test_and_substitution(): lc = LaunchContext() assert AndSubstitution('true', 'true').perform(lc) == 'true' From 8bdf336fb2fac003a587ea27add46f6e38c2208a Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Thu, 10 Mar 2022 22:48:57 +0900 Subject: [PATCH 06/10] Add auto-expansion of LaunchConfiguration Signed-off-by: Kenji Miyake --- .../substitutions/boolean_substitution.py | 21 ++++++++++---- .../launch_xml/test_boolean_substitution.py | 28 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index 016c844fd..c90291e19 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -17,6 +17,7 @@ from typing import Iterable from typing import Text +from .launch_configuration import LaunchConfiguration from .substitution_failure import SubstitutionFailure from ..frontend import expose_substitution from ..launch_context import LaunchContext @@ -55,7 +56,9 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - condition = perform_typed_substitution(context, self.value, bool) + value = LaunchConfiguration(self.value, default=self.value).perform(context) + condition = perform_typed_substitution( + context, normalize_to_list_of_substitutions(value), bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) @@ -97,11 +100,15 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - left_condition = perform_typed_substitution(context, self.left, bool) + value = LaunchConfiguration(self.left, default=self.left).perform(context) + left_condition = perform_typed_substitution( + context, normalize_to_list_of_substitutions(value), bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) try: - right_condition = perform_typed_substitution(context, self.right, bool) + value = LaunchConfiguration(self.right, default=self.right).perform(context) + right_condition = perform_typed_substitution( + context, normalize_to_list_of_substitutions(value), bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) @@ -143,11 +150,15 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - left_condition = perform_typed_substitution(context, self.left, bool) + value = LaunchConfiguration(self.left, default=self.left).perform(context) + left_condition = perform_typed_substitution( + context, normalize_to_list_of_substitutions(value), bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) try: - right_condition = perform_typed_substitution(context, self.right, bool) + value = LaunchConfiguration(self.right, default=self.right).perform(context) + right_condition = perform_typed_substitution( + context, normalize_to_list_of_substitutions(value), bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) diff --git a/launch_xml/test/launch_xml/test_boolean_substitution.py b/launch_xml/test/launch_xml/test_boolean_substitution.py index 59fecde14..bd8f2d783 100644 --- a/launch_xml/test/launch_xml/test_boolean_substitution.py +++ b/launch_xml/test/launch_xml/test_boolean_substitution.py @@ -20,7 +20,7 @@ from launch.utilities import perform_substitutions -def test_boolean_substitution_xml(): +def test_boolean_substitution(): xml_file = textwrap.dedent( r""" @@ -46,6 +46,32 @@ def test_boolean_substitution_xml(): check_boolean_substitution(f) +def test_boolean_substitution_no_var(): + xml_file = textwrap.dedent( + r""" + + + + + + + + + + + + + + + + + + """ + ) + with io.StringIO(xml_file) as f: + check_boolean_substitution(f) + + def check_boolean_substitution(file): root_entity, parser = Parser.load(file) ld = parser.parse_description(root_entity) From 57296341a77859351a66e9e2e7b1bdc530ead113 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Fri, 11 Mar 2022 23:50:43 +0900 Subject: [PATCH 07/10] Revert "Add auto-expansion of LaunchConfiguration" This reverts commit 8bdf336fb2fac003a587ea27add46f6e38c2208a. Signed-off-by: Kenji Miyake --- .../substitutions/boolean_substitution.py | 21 ++++---------- .../launch_xml/test_boolean_substitution.py | 28 +------------------ 2 files changed, 6 insertions(+), 43 deletions(-) diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index c90291e19..016c844fd 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -17,7 +17,6 @@ from typing import Iterable from typing import Text -from .launch_configuration import LaunchConfiguration from .substitution_failure import SubstitutionFailure from ..frontend import expose_substitution from ..launch_context import LaunchContext @@ -56,9 +55,7 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - value = LaunchConfiguration(self.value, default=self.value).perform(context) - condition = perform_typed_substitution( - context, normalize_to_list_of_substitutions(value), bool) + condition = perform_typed_substitution(context, self.value, bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) @@ -100,15 +97,11 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - value = LaunchConfiguration(self.left, default=self.left).perform(context) - left_condition = perform_typed_substitution( - context, normalize_to_list_of_substitutions(value), bool) + left_condition = perform_typed_substitution(context, self.left, bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) try: - value = LaunchConfiguration(self.right, default=self.right).perform(context) - right_condition = perform_typed_substitution( - context, normalize_to_list_of_substitutions(value), bool) + right_condition = perform_typed_substitution(context, self.right, bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) @@ -150,15 +143,11 @@ def describe(self) -> Text: def perform(self, context: LaunchContext) -> Text: """Perform the substitution.""" try: - value = LaunchConfiguration(self.left, default=self.left).perform(context) - left_condition = perform_typed_substitution( - context, normalize_to_list_of_substitutions(value), bool) + left_condition = perform_typed_substitution(context, self.left, bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) try: - value = LaunchConfiguration(self.right, default=self.right).perform(context) - right_condition = perform_typed_substitution( - context, normalize_to_list_of_substitutions(value), bool) + right_condition = perform_typed_substitution(context, self.right, bool) except (TypeError, ValueError) as e: raise SubstitutionFailure(e) diff --git a/launch_xml/test/launch_xml/test_boolean_substitution.py b/launch_xml/test/launch_xml/test_boolean_substitution.py index bd8f2d783..59fecde14 100644 --- a/launch_xml/test/launch_xml/test_boolean_substitution.py +++ b/launch_xml/test/launch_xml/test_boolean_substitution.py @@ -20,7 +20,7 @@ from launch.utilities import perform_substitutions -def test_boolean_substitution(): +def test_boolean_substitution_xml(): xml_file = textwrap.dedent( r""" @@ -46,32 +46,6 @@ def test_boolean_substitution(): check_boolean_substitution(f) -def test_boolean_substitution_no_var(): - xml_file = textwrap.dedent( - r""" - - - - - - - - - - - - - - - - - - """ - ) - with io.StringIO(xml_file) as f: - check_boolean_substitution(f) - - def check_boolean_substitution(file): root_entity, parser = Parser.load(file) ld = parser.parse_description(root_entity) From fc180c390c01e865306a86158fc8035a32fae931 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Tue, 22 Mar 2022 21:06:38 +0900 Subject: [PATCH 08/10] Fix True to true Signed-off-by: Kenji Miyake --- .../launch/substitutions/test_boolean_substitution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index 406cc7f37..db8cbed37 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -39,9 +39,9 @@ def test_and_substitution(): assert AndSubstitution('false', 'true').perform(lc) == 'false' assert AndSubstitution('false', 'false').perform(lc) == 'false' with pytest.raises(SubstitutionFailure): - AndSubstitution('not-condition-expression', 'True').perform(lc) + AndSubstitution('not-condition-expression', 'true').perform(lc) with pytest.raises(SubstitutionFailure): - AndSubstitution('True', 'not-condition-expression').perform(lc) + AndSubstitution('true', 'not-condition-expression').perform(lc) def test_or_substitution(): @@ -51,6 +51,6 @@ def test_or_substitution(): assert OrSubstitution('false', 'true').perform(lc) == 'true' assert OrSubstitution('false', 'false').perform(lc) == 'false' with pytest.raises(SubstitutionFailure): - OrSubstitution('not-condition-expression', 'True').perform(lc) + OrSubstitution('not-condition-expression', 'true').perform(lc) with pytest.raises(SubstitutionFailure): - OrSubstitution('True', 'not-condition-expression').perform(lc) + OrSubstitution('true', 'not-condition-expression').perform(lc) From 8e27b4602a1f4f91a4b12f77b364aab77cad5c01 Mon Sep 17 00:00:00 2001 From: Kenji Miyake Date: Tue, 22 Mar 2022 21:09:44 +0900 Subject: [PATCH 09/10] Add tests to launch_yaml Signed-off-by: Kenji Miyake --- .../launch_yaml/test_boolean_substitution.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 launch_yaml/test/launch_yaml/test_boolean_substitution.py diff --git a/launch_yaml/test/launch_yaml/test_boolean_substitution.py b/launch_yaml/test/launch_yaml/test_boolean_substitution.py new file mode 100644 index 000000000..0eba61615 --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_boolean_substitution.py @@ -0,0 +1,84 @@ +# Copyright 2022 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. + +import io +import textwrap + +from launch import LaunchService +from launch.frontend import Parser +from launch.utilities import perform_substitutions + + +def test_boolean_substitution_yaml(): + yaml_file = textwrap.dedent( + r""" + launch: + - let: { name: true_value, value: "true" } + - let: { name: false_value, value: "false" } + + - let: { name: not_true, value: "$(not $(var true_value))" } + - let: { name: not_false, value: "$(not $(var false_value))" } + + - let: { name: and_true_true, value: "$(and $(var true_value) $(var true_value))" } + - let: { name: and_true_false, value: "$(and $(var true_value) $(var false_value))" } + - let: { name: and_false_true, value: "$(and $(var false_value) $(var true_value))" } + - let: { name: and_false_false, value: "$(and $(var false_value) $(var false_value))" } + + - let: { name: or_true_true, value: "$(or $(var true_value) $(var true_value))" } + - let: { name: or_true_false, value: "$(or $(var true_value) $(var false_value))" } + - let: { name: or_false_true, value: "$(or $(var false_value) $(var true_value))" } + - let: { name: or_false_false, value: "$(or $(var false_value) $(var false_value))" } + """ + ) + with io.StringIO(yaml_file) as f: + check_boolean_substitution(f) + + +def check_boolean_substitution(file): + root_entity, parser = Parser.load(file) + ld = parser.parse_description(root_entity) + ls = LaunchService() + ls.include_launch_description(ld) + assert 0 == ls.run() + + def perform(substitution): + return perform_substitutions(ls.context, substitution) + + sub_entries = ld.describe_sub_entities() + + not_true = sub_entries[2] + not_false = sub_entries[3] + + and_true_true = sub_entries[4] + and_true_false = sub_entries[5] + and_false_true = sub_entries[6] + and_false_false = sub_entries[7] + + or_true_true = sub_entries[8] + or_true_false = sub_entries[9] + or_false_true = sub_entries[10] + or_false_false = sub_entries[11] + + assert perform(not_true.value) == 'false' + assert perform(not_false.value) == 'true' + + assert perform(and_true_true.value) == 'true' + assert perform(and_true_false.value) == 'false' + assert perform(and_false_true.value) == 'false' + assert perform(and_false_false.value) == 'false' + + assert perform(or_true_true.value) == 'true' + assert perform(or_true_false.value) == 'true' + assert perform(or_false_true.value) == 'true' + assert perform(or_false_false.value) == 'false' From 7f4e06a513e5f021d27769db2344018cfc25a005 Mon Sep 17 00:00:00 2001 From: Kenji Miyake <31987104+kenji-miyake@users.noreply.github.com> Date: Wed, 23 Mar 2022 10:06:51 +0900 Subject: [PATCH 10/10] Update launch/test/launch/substitutions/test_boolean_substitution.py Co-authored-by: Jacob Perron Signed-off-by: Kenji Miyake --- launch/test/launch/substitutions/test_boolean_substitution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index db8cbed37..edd39cfb2 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the AnonName substitution class.""" +"""Tests for the boolean substitution classes.""" from launch import LaunchContext