-
Notifications
You must be signed in to change notification settings - Fork 139
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add boolean substitutions Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Add lower() to the output Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Move test file Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Use perform_typed_substitution and simplify the code Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * fix for flake8 Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Add auto-expansion of LaunchConfiguration Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Revert "Add auto-expansion of LaunchConfiguration" This reverts commit 8bdf336. Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Fix True to true Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Add tests to launch_yaml Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> * Update launch/test/launch/substitutions/test_boolean_substitution.py Co-authored-by: Jacob Perron <jacob@openrobotics.org> Signed-off-by: Kenji Miyake <kenji.miyake@tier4.jp> Co-authored-by: Jacob Perron <jacob@openrobotics.org>
- Loading branch information
1 parent
d2eb2ba
commit fbb5f51
Showing
5 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# 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 ..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.type_utils import perform_typed_substitution | ||
|
||
|
||
@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 = perform_typed_substitution(context, self.value, bool) | ||
except (TypeError, ValueError) as e: | ||
raise SubstitutionFailure(e) | ||
|
||
return str(not condition).lower() | ||
|
||
|
||
@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: | ||
left_condition = perform_typed_substitution(context, self.left, bool) | ||
except (TypeError, ValueError) as e: | ||
raise SubstitutionFailure(e) | ||
try: | ||
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() | ||
|
||
|
||
@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: | ||
left_condition = perform_typed_substitution(context, self.left, bool) | ||
except (TypeError, ValueError) as e: | ||
raise SubstitutionFailure(e) | ||
try: | ||
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() |
56 changes: 56 additions & 0 deletions
56
launch/test/launch/substitutions/test_boolean_substitution.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# 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 boolean substitution classes.""" | ||
|
||
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 | ||
|
||
|
||
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) | ||
|
||
|
||
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) | ||
|
||
|
||
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): | ||
OrSubstitution('true', 'not-condition-expression').perform(lc) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# 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_xml(): | ||
xml_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))" /> | ||
</launch> | ||
""" | ||
) | ||
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' |
Oops, something went wrong.