diff --git a/launch/launch/conditions/launch_configuration_equals.py b/launch/launch/conditions/launch_configuration_equals.py index 11a35a798..3a41fee03 100644 --- a/launch/launch/conditions/launch_configuration_equals.py +++ b/launch/launch/conditions/launch_configuration_equals.py @@ -16,6 +16,7 @@ from typing import Optional from typing import Text +import warnings from ..condition import Condition from ..launch_context import LaunchContext @@ -36,6 +37,10 @@ class LaunchConfigurationEquals(Condition): If ``None`` is provided instead of a string expression, then the condition evaluates to ``True`` if the launch configuration is not set. + + .. deprecated:: 1.1.0 + Replaced by the more universally usable substitutions: + 'EqualsSubstitution' and 'NotEqualsSubstitution' """ def __init__( @@ -43,6 +48,16 @@ def __init__( launch_configuration_name: Text, expected_value: Optional[SomeSubstitutionsType] ) -> None: + warnings.warn( + "The 'LaunchConfigurationEquals' and 'LaunchConfigurationNotEquals' Conditions are " + " deprecated. Use the 'EqualsSubstitution' and 'NotEqualsSubstitution' substitutions " + 'instead! E.g.:\n' + ' IfCondition(\n ' + "\tEqualsSubstitution(LaunchConfiguration('some_launch_arg'), \"some_equality_check\")" + '\n )', + UserWarning + ) + self.__launch_configuration_name = launch_configuration_name if expected_value is not None: self.__expected_value = normalize_to_list_of_substitutions(expected_value) diff --git a/launch/launch/conditions/launch_configuration_not_equals.py b/launch/launch/conditions/launch_configuration_not_equals.py index b30d989e0..879dd9017 100644 --- a/launch/launch/conditions/launch_configuration_not_equals.py +++ b/launch/launch/conditions/launch_configuration_not_equals.py @@ -34,6 +34,10 @@ class LaunchConfigurationNotEquals(LaunchConfigurationEquals): If ``None`` is provided instead of a string expression, then the condition evaluates to ``True`` if the launch configuration is set. + + .. deprecated:: 1.1.0 + Replaced by the more universally usable substitutions: + 'EqualsSubstitution' and 'NotEqualsSubstitution' """ def __init__( @@ -41,6 +45,7 @@ def __init__( launch_configuration_name: Text, expected_value: Optional[SomeSubstitutionsType] ) -> None: + # This is deprecated! Use `NotEqualsSubstitution` instead! super().__init__(launch_configuration_name, expected_value) def _predicate_func(self, context: LaunchContext) -> bool: diff --git a/launch/launch/substitutions/__init__.py b/launch/launch/substitutions/__init__.py index 8195ba242..a77d33636 100644 --- a/launch/launch/substitutions/__init__.py +++ b/launch/launch/substitutions/__init__.py @@ -15,14 +15,18 @@ """Package for substitutions.""" from .anon_name import AnonName +from .boolean_substitution import AllSubstitution from .boolean_substitution import AndSubstitution +from .boolean_substitution import AnySubstitution from .boolean_substitution import NotSubstitution from .boolean_substitution import OrSubstitution from .command import Command from .environment_variable import EnvironmentVariable +from .equals_substitution import EqualsSubstitution from .find_executable import FindExecutable from .launch_configuration import LaunchConfiguration from .local_substitution import LocalSubstitution +from .not_equals_substitution import NotEqualsSubstitution from .path_join_substitution import PathJoinSubstitution from .python_expression import PythonExpression from .substitution_failure import SubstitutionFailure @@ -31,14 +35,18 @@ from .this_launch_file_dir import ThisLaunchFileDir __all__ = [ + 'AllSubstitution', 'AndSubstitution', + 'AnySubstitution', 'AnonName', 'Command', + 'EqualsSubstitution', 'EnvironmentVariable', 'FindExecutable', 'LaunchConfiguration', 'LocalSubstitution', 'NotSubstitution', + 'NotEqualsSubstitution', 'OrSubstitution', 'PathJoinSubstitution', 'PythonExpression', diff --git a/launch/launch/substitutions/boolean_substitution.py b/launch/launch/substitutions/boolean_substitution.py index 016c844fd..45593e76b 100644 --- a/launch/launch/substitutions/boolean_substitution.py +++ b/launch/launch/substitutions/boolean_substitution.py @@ -67,7 +67,7 @@ class AndSubstitution(Substitution): """Substitution that returns 'and' of the input boolean values.""" def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: - """Create a AndSubstitution substitution.""" + """Create an AndSubstitution substitution.""" super().__init__() self.__left = normalize_to_list_of_substitutions(left) @@ -113,7 +113,7 @@ class OrSubstitution(Substitution): """Substitution that returns 'or' of the input boolean values.""" def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> None: - """Create a AndSubstitution substitution.""" + """Create an OrSubstitution substitution.""" super().__init__() self.__left = normalize_to_list_of_substitutions(left) @@ -121,7 +121,7 @@ def __init__(self, left: SomeSubstitutionsType, right: SomeSubstitutionsType) -> @classmethod def parse(cls, data: Iterable[SomeSubstitutionsType]): - """Parse `AndSubstitution` substitution.""" + """Parse `OrSubstitution` substitution.""" if len(data) != 2: raise TypeError('and substitution expects 2 arguments') return cls, {'left': data[0], 'right': data[1]} @@ -152,3 +152,94 @@ def perform(self, context: LaunchContext) -> Text: raise SubstitutionFailure(e) return str(left_condition or right_condition).lower() + + +@expose_substitution('any') +class AnySubstitution(Substitution): + """ + Substitutes to the string 'true' if at least one of the input arguments evaluates to true. + + If none of the arguments evaluate to true, then this substitution returns the string 'false'. + """ + + def __init__(self, *args: SomeSubstitutionsType) -> None: + """ + Create an AnySubstitution substitution. + + The following string arguments evaluate to true: '1', 'true', 'True', 'on' + """ + super().__init__() + + self.__args = [normalize_to_list_of_substitutions(arg) for arg in args] + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AnySubstitution` substitution.""" + return cls, {'args': data} + + @property + def args(self) -> Substitution: + """Getter for args.""" + return self.__args + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AnySubstitution({" ".join(self.args)})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + substituted_conditions = [] + for arg in self.args: + try: + arg_condition = perform_typed_substitution(context, arg, bool) + substituted_conditions.append(arg_condition) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + + return str(any(substituted_conditions)).lower() + + +@expose_substitution('all') +class AllSubstitution(Substitution): + """ + Substitutes to the string 'true' if all of the input arguments evaluate to true. + + If any of the arguments evaluates to false, then this substitution returns the string 'false'. + """ + + def __init__(self, *args: SomeSubstitutionsType) -> None: + """ + Create an AllSubstitution substitution. + + The following string arguments evaluate to true: '1', 'true', 'True', 'on' + The following string arguments evaluate to false: '0', 'false', 'False', 'off' + """ + super().__init__() + + self.__args = [normalize_to_list_of_substitutions(arg) for arg in args] + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `AllSubstitution` substitution.""" + return cls, {'args': data} + + @property + def args(self) -> Substitution: + """Getter for args.""" + return self.__args + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'AllSubstitution({" ".join(self.args)})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + substituted_conditions = [] + for arg in self.args: + try: + arg_condition = perform_typed_substitution(context, arg, bool) + substituted_conditions.append(arg_condition) + except (TypeError, ValueError) as e: + raise SubstitutionFailure(e) + + return str(all(substituted_conditions)).lower() diff --git a/launch/launch/substitutions/equals_substitution.py b/launch/launch/substitutions/equals_substitution.py new file mode 100644 index 000000000..0fb0b9f2f --- /dev/null +++ b/launch/launch/substitutions/equals_substitution.py @@ -0,0 +1,119 @@ +# 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 the EqualsSubstitution substitution.""" + +import math + +from typing import Any +from typing import Iterable +from typing import Optional +from typing import Text +from typing import Union + +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 is_substitution, perform_substitutions + + +def _str_is_bool(input_str: Text) -> bool: + """Check if string input is convertible to a boolean.""" + if not isinstance(input_str, Text): + return False + else: + return input_str.lower() in ('true', 'false', '1', '0') + + +def _str_is_float(input_str: Text) -> bool: + """Check if string input is convertible to a float.""" + try: + float(input_str) + return True + except ValueError: + return False + + +@expose_substitution('equals') +class EqualsSubstitution(Substitution): + """ + Substitution that checks if two inputs are equal. + + Returns 'true' or 'false' strings depending on the result. + """ + + def __init__( + self, + left: Optional[Union[Any, Iterable[Any]]], + right: Optional[Union[Any, Iterable[Any]]] + ) -> None: + """Create an EqualsSubstitution substitution.""" + super().__init__() + + if not is_substitution(left): + if left is None: + left = '' + elif isinstance(left, bool): + left = str(left).lower() + else: + left = str(left) + + if not is_substitution(right): + if right is None: + right = '' + elif isinstance(right, bool): + right = str(right).lower() + else: + right = str(right) + + self.__left = normalize_to_list_of_substitutions(left) + self.__right = normalize_to_list_of_substitutions(right) + + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): + """Parse `EqualsSubstitution` 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'EqualsSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + left = perform_substitutions(context, self.left) + right = perform_substitutions(context, self.right) + + # Special case for booleans + if _str_is_bool(left) and _str_is_bool(right): + return str((left.lower() in ('true', '1')) == (right.lower() in ('true', '1'))).lower() + + # Special case for floats (epsilon closeness) + if _str_is_float(left) and _str_is_float(right): + return str(math.isclose(float(left), float(right))).lower() + + return str(left == right).lower() diff --git a/launch/launch/substitutions/not_equals_substitution.py b/launch/launch/substitutions/not_equals_substitution.py new file mode 100644 index 000000000..372650425 --- /dev/null +++ b/launch/launch/substitutions/not_equals_substitution.py @@ -0,0 +1,50 @@ +# 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 the NotEqualsSubstitution substitution.""" + +from typing import Any +from typing import Iterable +from typing import Optional +from typing import Text +from typing import Union + +from .equals_substitution import EqualsSubstitution +from ..frontend import expose_substitution +from ..launch_context import LaunchContext + + +@expose_substitution('not-equals') +class NotEqualsSubstitution(EqualsSubstitution): + """ + Substitution that checks if two inputs are not equal. + + Returns 'true' or 'false' strings depending on the result. + """ + + def __init__( + self, + left: Optional[Union[Any, Iterable[Any]]], + right: Optional[Union[Any, Iterable[Any]]] + ) -> None: + """Create a NotEqualsSubstitution substitution.""" + super().__init__(left, right) + + def describe(self) -> Text: + """Return a description of this substitution as a string.""" + return f'NotEqualsSubstitution({self.left} {self.right})' + + def perform(self, context: LaunchContext) -> Text: + """Perform the substitution.""" + return str(not (super().perform(context) == 'true')).lower() diff --git a/launch/test/launch/substitutions/test_boolean_substitution.py b/launch/test/launch/substitutions/test_boolean_substitution.py index 86725ffc1..7a2679b71 100644 --- a/launch/test/launch/substitutions/test_boolean_substitution.py +++ b/launch/test/launch/substitutions/test_boolean_substitution.py @@ -16,7 +16,9 @@ from launch import LaunchContext +from launch.substitutions import AllSubstitution from launch.substitutions import AndSubstitution +from launch.substitutions import AnySubstitution from launch.substitutions import NotSubstitution from launch.substitutions import OrSubstitution from launch.substitutions.substitution_failure import SubstitutionFailure @@ -88,3 +90,63 @@ def test_or_substitution(): OrSubstitution('not-condition-expression', 'true').perform(lc) with pytest.raises(SubstitutionFailure): OrSubstitution('true', 'not-condition-expression').perform(lc) + + +def test_any_substitution(): + lc = LaunchContext() + assert AnySubstitution().perform(lc) == 'false' + assert AnySubstitution('true').perform(lc) == 'true' + assert AnySubstitution('false').perform(lc) == 'false' + assert AnySubstitution('true', 'true').perform(lc) == 'true' + assert AnySubstitution('true', 'false').perform(lc) == 'true' + assert AnySubstitution('false', 'true').perform(lc) == 'true' + assert AnySubstitution('false', 'false').perform(lc) == 'false' + assert AnySubstitution('true', 'true', 'true').perform(lc) == 'true' + assert AnySubstitution('true', 'true', 'false').perform(lc) == 'true' + assert AnySubstitution('false', 'false', 'false').perform(lc) == 'false' + + assert AnySubstitution('1').perform(lc) == 'true' + assert AnySubstitution('0').perform(lc) == 'false' + assert AnySubstitution('1', 'true').perform(lc) == 'true' + assert AnySubstitution('1', 'false').perform(lc) == 'true' + assert AnySubstitution('0', 'true').perform(lc) == 'true' + assert AnySubstitution('0', 'false').perform(lc) == 'false' + assert AnySubstitution('1', 'true', 'true').perform(lc) == 'true' + assert AnySubstitution('1', 'true', 'false').perform(lc) == 'true' + assert AnySubstitution('true', 'true', '0').perform(lc) == 'true' + assert AnySubstitution('0', 'false', 'false').perform(lc) == 'false' + + with pytest.raises(SubstitutionFailure): + AnySubstitution('not-condition-expression', 'true').perform(lc) + with pytest.raises(SubstitutionFailure): + AnySubstitution('true', 'not-condition-expression').perform(lc) + + +def test_all_substitution(): + lc = LaunchContext() + assert AllSubstitution().perform(lc) == 'true' + assert AllSubstitution('true').perform(lc) == 'true' + assert AllSubstitution('false').perform(lc) == 'false' + assert AllSubstitution('true', 'true').perform(lc) == 'true' + assert AllSubstitution('true', 'false').perform(lc) == 'false' + assert AllSubstitution('false', 'true').perform(lc) == 'false' + assert AllSubstitution('false', 'false').perform(lc) == 'false' + assert AllSubstitution('true', 'true', 'true').perform(lc) == 'true' + assert AllSubstitution('true', 'true', 'false').perform(lc) == 'false' + assert AllSubstitution('false', 'false', 'false').perform(lc) == 'false' + + assert AllSubstitution('1').perform(lc) == 'true' + assert AllSubstitution('0').perform(lc) == 'false' + assert AllSubstitution('1', 'true').perform(lc) == 'true' + assert AllSubstitution('1', 'false').perform(lc) == 'false' + assert AllSubstitution('0', 'true').perform(lc) == 'false' + assert AllSubstitution('0', 'false').perform(lc) == 'false' + assert AllSubstitution('1', 'true', 'true').perform(lc) == 'true' + assert AllSubstitution('1', 'true', 'false').perform(lc) == 'false' + assert AllSubstitution('true', 'true', '0').perform(lc) == 'false' + assert AllSubstitution('0', 'false', 'false').perform(lc) == 'false' + + with pytest.raises(SubstitutionFailure): + AllSubstitution('not-condition-expression', 'true').perform(lc) + with pytest.raises(SubstitutionFailure): + AllSubstitution('true', 'not-condition-expression').perform(lc) diff --git a/launch/test/launch/substitutions/test_equals_substitution.py b/launch/test/launch/substitutions/test_equals_substitution.py new file mode 100644 index 000000000..b9211ecc0 --- /dev/null +++ b/launch/test/launch/substitutions/test_equals_substitution.py @@ -0,0 +1,107 @@ +# 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 EqualsSubstitution class.""" + +import os + +from launch import LaunchContext + +from launch.substitutions import EqualsSubstitution +from launch.substitutions import PathJoinSubstitution + + +def test_equals_substitution(): + def _permute_assertion(left, right, context, output): + assert EqualsSubstitution(left, right).perform(context) == output + assert EqualsSubstitution(right, left).perform(context) == output + + lc = LaunchContext() + + # NoneType + _permute_assertion(None, None, lc, 'true') + _permute_assertion(None, '', lc, 'true') + _permute_assertion(None, 'something', lc, 'false') + + # Booleans + _permute_assertion(True, True, lc, 'true') + _permute_assertion(False, False, lc, 'true') + _permute_assertion(True, False, lc, 'false') + + _permute_assertion(True, 'true', lc, 'true') + _permute_assertion(True, 'True', lc, 'true') + _permute_assertion(False, 'false', lc, 'true') + _permute_assertion(False, 'False', lc, 'true') + _permute_assertion(True, 'False', lc, 'false') + + _permute_assertion(True, 1, lc, 'true') + _permute_assertion(True, '1', lc, 'true') + _permute_assertion(True, 0, lc, 'false') + _permute_assertion(True, '0', lc, 'false') + _permute_assertion(True, '10', lc, 'false') + _permute_assertion(True, '-1', lc, 'false') + + _permute_assertion(False, 1, lc, 'false') + _permute_assertion(False, '1', lc, 'false') + _permute_assertion(False, 0, lc, 'true') + _permute_assertion(False, '0', lc, 'true') + _permute_assertion(False, '10', lc, 'false') + _permute_assertion(False, '-1', lc, 'false') + + _permute_assertion('true', 1, lc, 'true') + _permute_assertion('true', '1', lc, 'true') + _permute_assertion('true', '0', lc, 'false') + _permute_assertion('true', 'true', lc, 'true') + _permute_assertion('false', 1, lc, 'false') + _permute_assertion('false', '1', lc, 'false') + _permute_assertion('false', '0', lc, 'true') + _permute_assertion('false', 'false', lc, 'true') + _permute_assertion('true', 'false', lc, 'false') + + # Numerics + _permute_assertion(1, 1, lc, 'true') + _permute_assertion(1, 0, lc, 'false') + _permute_assertion(1, -1, lc, 'false') + _permute_assertion(10, 10, lc, 'true') + _permute_assertion(10, -10, lc, 'false') + + _permute_assertion(10, 10.0, lc, 'true') + _permute_assertion(10, 10 + 1e-10, lc, 'true') + _permute_assertion(10, 10.1, lc, 'false') + _permute_assertion(10.0, -10.0, lc, 'false') + + _permute_assertion(float('nan'), float('nan'), lc, 'false') + _permute_assertion(float('nan'), 'nan', lc, 'false') + _permute_assertion('nan', 'nan', lc, 'false') # Special case + + _permute_assertion(float('inf'), float('inf'), lc, 'true') + _permute_assertion(float('inf'), 'inf', lc, 'true') + _permute_assertion('inf', 'inf', lc, 'true') + + _permute_assertion(float('inf'), float('-inf'), lc, 'false') + _permute_assertion(float('inf'), '-inf', lc, 'false') + _permute_assertion('inf', '-inf', lc, 'false') + _permute_assertion('-inf', '-inf', lc, 'true') + + # Strings + _permute_assertion('wow', 'wow', lc, 'true') + _permute_assertion('wow', True, lc, 'false') + _permute_assertion('wow', 1, lc, 'false') + _permute_assertion('wow', 0, lc, 'false') + _permute_assertion('wow', 10, lc, 'false') + + # Substitutions + path = ['asd', 'bsd', 'cds'] + sub = PathJoinSubstitution(path) + assert EqualsSubstitution(sub, os.path.join(*path)).perform(lc) == 'true' diff --git a/launch/test/launch/substitutions/test_not_equals_substitution.py b/launch/test/launch/substitutions/test_not_equals_substitution.py new file mode 100644 index 000000000..0a8b7eb28 --- /dev/null +++ b/launch/test/launch/substitutions/test_not_equals_substitution.py @@ -0,0 +1,107 @@ +# 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 NotEqualsSubstitution class.""" + +import os + +from launch import LaunchContext + +from launch.substitutions import NotEqualsSubstitution +from launch.substitutions import PathJoinSubstitution + + +def test_not_equals_substitution(): + def _permute_assertion(left, right, context, output): + assert NotEqualsSubstitution(left, right).perform(context) == output + assert NotEqualsSubstitution(right, left).perform(context) == output + + lc = LaunchContext() + + # NoneType + _permute_assertion(None, None, lc, 'false') + _permute_assertion(None, '', lc, 'false') + _permute_assertion(None, 'something', lc, 'true') + + # Booleans + _permute_assertion(True, True, lc, 'false') + _permute_assertion(False, False, lc, 'false') + _permute_assertion(True, False, lc, 'true') + + _permute_assertion(True, 'true', lc, 'false') + _permute_assertion(True, 'True', lc, 'false') + _permute_assertion(False, 'false', lc, 'false') + _permute_assertion(False, 'False', lc, 'false') + _permute_assertion(True, 'False', lc, 'true') + + _permute_assertion(True, 1, lc, 'false') + _permute_assertion(True, '1', lc, 'false') + _permute_assertion(True, 0, lc, 'true') + _permute_assertion(True, '0', lc, 'true') + _permute_assertion(True, '10', lc, 'true') + _permute_assertion(True, '-1', lc, 'true') + + _permute_assertion(False, 1, lc, 'true') + _permute_assertion(False, '1', lc, 'true') + _permute_assertion(False, 0, lc, 'false') + _permute_assertion(False, '0', lc, 'false') + _permute_assertion(False, '10', lc, 'true') + _permute_assertion(False, '-1', lc, 'true') + + _permute_assertion('true', 1, lc, 'false') + _permute_assertion('true', '1', lc, 'false') + _permute_assertion('true', '0', lc, 'true') + _permute_assertion('true', 'true', lc, 'false') + _permute_assertion('false', 1, lc, 'true') + _permute_assertion('false', '1', lc, 'true') + _permute_assertion('false', '0', lc, 'false') + _permute_assertion('false', 'false', lc, 'false') + _permute_assertion('true', 'false', lc, 'true') + + # Numerics + _permute_assertion(1, 1, lc, 'false') + _permute_assertion(1, 0, lc, 'true') + _permute_assertion(1, -1, lc, 'true') + _permute_assertion(10, 10, lc, 'false') + _permute_assertion(10, -10, lc, 'true') + + _permute_assertion(10, 10.0, lc, 'false') + _permute_assertion(10, 10 + 1e-10, lc, 'false') + _permute_assertion(10, 10.1, lc, 'true') + _permute_assertion(10.0, -10.0, lc, 'true') + + _permute_assertion(float('nan'), float('nan'), lc, 'true') + _permute_assertion(float('nan'), 'nan', lc, 'true') + _permute_assertion('nan', 'nan', lc, 'true') # Special case + + _permute_assertion(float('inf'), float('inf'), lc, 'false') + _permute_assertion(float('inf'), 'inf', lc, 'false') + _permute_assertion('inf', 'inf', lc, 'false') + + _permute_assertion(float('inf'), float('-inf'), lc, 'true') + _permute_assertion(float('inf'), '-inf', lc, 'true') + _permute_assertion('inf', '-inf', lc, 'true') + _permute_assertion('-inf', '-inf', lc, 'false') + + # Strings + _permute_assertion('wow', 'wow', lc, 'false') + _permute_assertion('wow', True, lc, 'true') + _permute_assertion('wow', 1, lc, 'true') + _permute_assertion('wow', 0, lc, 'true') + _permute_assertion('wow', 10, lc, 'true') + + # Substitutions + path = ['asd', 'bsd', 'cds'] + sub = PathJoinSubstitution(path) + assert NotEqualsSubstitution(sub, os.path.join(*path)).perform(lc) == 'false'