diff --git a/src/stepfunctions/steps/choice_rule.py b/src/stepfunctions/steps/choice_rule.py index eff4694..30bcfdd 100644 --- a/src/stepfunctions/steps/choice_rule.py +++ b/src/stepfunctions/steps/choice_rule.py @@ -6,9 +6,9 @@ # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the "license" file accompanying this file. This file 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 +# or in the "license" file accompanying this file. This file 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. from __future__ import absolute_import @@ -19,6 +19,7 @@ 'Numeric': (int, float), 'Boolean': (bool,), 'Timestamp': (str,), + 'Is': (bool,) } @@ -30,10 +31,10 @@ class BaseRule(object): def to_dict(self): return {} - + def __repr__(self): return '{}()'.format(self.__class__.__name__) - + def __str__(self): return '{}'.format(self.to_dict()) @@ -48,9 +49,9 @@ def __init__(self, variable, operator, value): """ Args: variable (str): Path to the variable to compare. - operator (str): Comparison operator to be applied. - value (type depends on *operator*): Constant value to compare `variable` against. - + operator (str): Comparison operator to be applied. + value (type depends on *operator*): Constant value or Path to compare `variable` against. + Raises: ValueError: If `variable` doesn't start with '$' ValueError: If `value` is not the appropriate datatype for the `operator` specified. @@ -58,16 +59,21 @@ def __init__(self, variable, operator, value): # Validate the variable name if not isinstance(variable, StepInput) and not variable.startswith('$'): raise ValueError("Expected variable must be a placeholder or must start with '$', but got '{variable}'".format(variable=variable)) - + # Validate the variable value - for k, v in VALIDATORS.items(): - if operator.startswith(k) and not isinstance(value, v): - raise ValueError('Expected value to be a {type}, but got {value}'.format(type=k, value=value)) + # If operator ends with Path, value must be a Path + if operator.endswith("Path"): + if not isinstance(value, StepInput) and not value.startswith('$'): + raise ValueError("Expected value must be a placeholder or must start with '$', but got '{value}'".format(value=value)) + else: + for k, v in VALIDATORS.items(): + if operator.startswith(k) and not isinstance(value, v): + raise ValueError('Expected value to be a {type}, but got {value}'.format(type=k, value=value)) self.variable = variable self.operator = operator self.value = value - + def to_dict(self): if isinstance(self.variable, StepInput): result = { 'Variable': self.variable.to_jsonpath() } @@ -75,7 +81,7 @@ def to_dict(self): result = { 'Variable': self.variable } result[self.operator] = self.value return result - + def __repr__(self): return '{}(variable={!r}, operator={!r}, value={!r})'.format( self.__class__.__name__, @@ -94,20 +100,20 @@ def __init__(self, operator, rules): Args: operator (str): Compounding operator to be applied. rules (list(BaseRule)): List of rules to compound together. - + Raises: ValueError: If any item in the `rules` list is not a BaseRule object. """ for rule in rules: if not isinstance(rule, BaseRule): raise ValueError("Rule '{rule}' is invalid".format(rule=rule)) - + self.operator = operator self.rules = rules - + def to_dict(self): return { self.operator: [ rule.to_dict() for rule in self.rules ] } - + def __repr__(self): return '{}(operator={!r}, rules={!r})'.format( self.__class__.__name__, @@ -125,20 +131,20 @@ def __init__(self, rule): """ Args: rules (BaseRule): Rule to negate. - + Raises: ValueError: If `rule` is not a BaseRule object. """ if not isinstance(rule, BaseRule): raise ValueError("Rule '{rule}' is invalid".format(rule=rule)) - + self.rule = rule - + def to_dict(self): return { 'Not': self.rule.to_dict() } - + def __repr__(self): return '{}(rule={!r})'.format( self.__class__.__name__, @@ -151,7 +157,7 @@ class ChoiceRule(object): """ Factory class for creating a choice rule. """ - + @classmethod def StringEquals(cls, variable, value): """ @@ -165,7 +171,21 @@ def StringEquals(cls, variable, value): Rule: Rule with `StringEquals` operator. """ return Rule(variable, 'StringEquals', value) - + + @classmethod + def StringEqualsPath(cls, variable, value): + """ + Creates a rule with the `StringEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `StringEqualsPath` operator. + """ + return Rule(variable, 'StringEqualsPath', value) + @classmethod def StringLessThan(cls, variable, value): """ @@ -180,6 +200,20 @@ def StringLessThan(cls, variable, value): """ return Rule(variable, 'StringLessThan', value) + @classmethod + def StringLessThanPath(cls, variable, value): + """ + Creates a rule with the `StringLessThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `StringLessThanPath` operator. + """ + return Rule(variable, 'StringLessThanPath', value) + @classmethod def StringGreaterThan(cls, variable, value): """ @@ -194,6 +228,20 @@ def StringGreaterThan(cls, variable, value): """ return Rule(variable, 'StringGreaterThan', value) + @classmethod + def StringGreaterThanPath(cls, variable, value): + """ + Creates a rule with the `StringGreaterThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `StringGreaterThanPath` operator. + """ + return Rule(variable, 'StringGreaterThanPath', value) + @classmethod def StringLessThanEquals(cls, variable, value): """ @@ -208,6 +256,20 @@ def StringLessThanEquals(cls, variable, value): """ return Rule(variable, 'StringLessThanEquals', value) + @classmethod + def StringLessThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `StringLessThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `StringLessThanEqualsPath` operator. + """ + return Rule(variable, 'StringLessThanEqualsPath', value) + @classmethod def StringGreaterThanEquals(cls, variable, value): """ @@ -222,6 +284,20 @@ def StringGreaterThanEquals(cls, variable, value): """ return Rule(variable, 'StringGreaterThanEquals', value) + @classmethod + def StringGreaterThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `StringGreaterThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `StringGreaterThanEqualsPath` operator. + """ + return Rule(variable, 'StringGreaterThanEqualsPath', value) + @classmethod def NumericEquals(cls, variable, value): """ @@ -236,6 +312,20 @@ def NumericEquals(cls, variable, value): """ return Rule(variable, 'NumericEquals', value) + @classmethod + def NumericEqualsPath(cls, variable, value): + """ + Creates a rule with the `NumericEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `NumericEqualsPath` operator. + """ + return Rule(variable, 'NumericEqualsPath', value) + @classmethod def NumericLessThan(cls, variable, value): """ @@ -250,6 +340,20 @@ def NumericLessThan(cls, variable, value): """ return Rule(variable, 'NumericLessThan', value) + @classmethod + def NumericLessThanPath(cls, variable, value): + """ + Creates a rule with the `NumericLessThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `NumericLessThanPath` operator. + """ + return Rule(variable, 'NumericLessThanPath', value) + @classmethod def NumericGreaterThan(cls, variable, value): """ @@ -264,6 +368,20 @@ def NumericGreaterThan(cls, variable, value): """ return Rule(variable, 'NumericGreaterThan', value) + @classmethod + def NumericGreaterThanPath(cls, variable, value): + """ + Creates a rule with the `NumericGreaterThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `NumericGreaterThanPath` operator. + """ + return Rule(variable, 'NumericGreaterThanPath', value) + @classmethod def NumericLessThanEquals(cls, variable, value): """ @@ -278,6 +396,20 @@ def NumericLessThanEquals(cls, variable, value): """ return Rule(variable, 'NumericLessThanEquals', value) + @classmethod + def NumericLessThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `NumericLessThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `NumericLessThanEqualsPath` operator. + """ + return Rule(variable, 'NumericLessThanEqualsPath', value) + @classmethod def NumericGreaterThanEquals(cls, variable, value): """ @@ -292,6 +424,20 @@ def NumericGreaterThanEquals(cls, variable, value): """ return Rule(variable, 'NumericGreaterThanEquals', value) + @classmethod + def NumericGreaterThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `NumericGreaterThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `NumericGreaterThanEqualsPath` operator. + """ + return Rule(variable, 'NumericGreaterThanEqualsPath', value) + @classmethod def BooleanEquals(cls, variable, value): """ @@ -306,6 +452,20 @@ def BooleanEquals(cls, variable, value): """ return Rule(variable, 'BooleanEquals', value) + @classmethod + def BooleanEqualsPath(cls, variable, value): + """ + Creates a rule with the `BooleanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `BooleanEqualsPath` operator. + """ + return Rule(variable, 'BooleanEqualsPath', value) + @classmethod def TimestampEquals(cls, variable, value): """ @@ -320,6 +480,20 @@ def TimestampEquals(cls, variable, value): """ return Rule(variable, 'TimestampEquals', value) + @classmethod + def TimestampEqualsPath(cls, variable, value): + """ + Creates a rule with the `TimestampEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `TimestampEqualsPath` operator. + """ + return Rule(variable, 'TimestampEqualsPath', value) + @classmethod def TimestampLessThan(cls, variable, value): """ @@ -334,6 +508,20 @@ def TimestampLessThan(cls, variable, value): """ return Rule(variable, 'TimestampLessThan', value) + @classmethod + def TimestampLessThanPath(cls, variable, value): + """ + Creates a rule with the `TimestampLessThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `TimestampLessThanPath` operator. + """ + return Rule(variable, 'TimestampLessThanPath', value) + @classmethod def TimestampGreaterThan(cls, variable, value): """ @@ -348,6 +536,20 @@ def TimestampGreaterThan(cls, variable, value): """ return Rule(variable, 'TimestampGreaterThan', value) + @classmethod + def TimestampGreaterThanPath(cls, variable, value): + """ + Creates a rule with the `TimestampGreaterThanPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `TimestampGreaterThanPath` operator. + """ + return Rule(variable, 'TimestampGreaterThanPath', value) + @classmethod def TimestampLessThanEquals(cls, variable, value): """ @@ -362,6 +564,20 @@ def TimestampLessThanEquals(cls, variable, value): """ return Rule(variable, 'TimestampLessThanEquals', value) + @classmethod + def TimestampLessThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `TimestampLessThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `TimestampLessThanEqualsPath` operator. + """ + return Rule(variable, 'TimestampLessThanEqualsPath', value) + @classmethod def TimestampGreaterThanEquals(cls, variable, value): """ @@ -376,6 +592,122 @@ def TimestampGreaterThanEquals(cls, variable, value): """ return Rule(variable, 'TimestampGreaterThanEquals', value) + @classmethod + def TimestampGreaterThanEqualsPath(cls, variable, value): + """ + Creates a rule with the `TimestampGreaterThanEqualsPath` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): Path to the value to compare `variable` against. + + Returns: + Rule: Rule with `TimestampGreaterThanEqualsPath` operator. + """ + return Rule(variable, 'TimestampGreaterThanEqualsPath', value) + + @classmethod + def IsNull(cls, variable, value): + """ + Creates a rule with the `IsNull` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether the value at `variable` is equal to the JSON literal null or not. + + Returns: + Rule: Rule with `IsNull` operator. + """ + return Rule(variable, 'IsNull', value) + + @classmethod + def IsPresent(cls, variable, value): + """ + Creates a rule with the `IsPresent` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether a field at `variable` exists in the input or not. + + Returns: + Rule: Rule with `IsPresent` operator. + """ + return Rule(variable, 'IsPresent', value) + + @classmethod + def IsString(cls, variable, value): + """ + Creates a rule with the `IsString` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether the value at `variable` is a string or not. + + Returns: + Rule: Rule with `IsString` operator. + """ + return Rule(variable, 'IsString', value) + + @classmethod + def IsNumeric(cls, variable, value): + """ + Creates a rule with the `IsNumeric` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether the value at `variable` is a number or not. + + Returns: + Rule: Rule with `IsNumeric` operator. + """ + return Rule(variable, 'IsNumeric', value) + + @classmethod + def IsTimestamp(cls, variable, value): + """ + Creates a rule with the `IsTimestamp` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether the value at `variable` is a timestamp or not. + + Returns: + Rule: Rule with `IsTimestamp` operator. + """ + return Rule(variable, 'IsTimestamp', value) + + @classmethod + def IsBoolean(cls, variable, value): + """ + Creates a rule with the `IsBoolean` operator. + + Args: + variable (str): Path to the variable to compare. + value (bool): Whether the value at `variable` is a boolean or not. + + Returns: + Rule: Rule with `IsBoolean` operator. + """ + return Rule(variable, 'IsBoolean', value) + + @classmethod + def StringMatches(cls, variable, value): + """ + Creates a rule with the `StringMatches` operator. + + Args: + variable (str): Path to the variable to compare. + value (str): A string pattern that may contain one or more `*` characters to compare the value at `variable` to. + The `*` character can be escaped using two backslashes. + The comparison yields true if the variable matches the pattern, where `*` is a wildcard that matches zero or more characters. + + Returns: + Rule: Rule with `StringMatches` operator. + """ + return Rule(variable, 'StringMatches', value) + + + @classmethod def And(cls, rules): """ @@ -388,7 +720,7 @@ def And(cls, rules): CompoundRule: Compound rule with `And` operator. """ return CompoundRule('And', rules) - + @classmethod def Or(cls, rules): """ @@ -401,7 +733,7 @@ def Or(cls, rules): CompoundRule: Compound rule with `Or` operator. """ return CompoundRule('Or', rules) - + @classmethod def Not(cls, rule): """ diff --git a/tests/unit/test_choice_rule.py b/tests/unit/test_choice_rule.py index 015880a..435dfd0 100644 --- a/tests/unit/test_choice_rule.py +++ b/tests/unit/test_choice_rule.py @@ -6,9 +6,9 @@ # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the "license" file accompanying this file. This file 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 +# or in the "license" file accompanying this file. This file 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. from __future__ import absolute_import @@ -45,7 +45,7 @@ def test_variable_value_must_be_consistent(): func = getattr(ChoiceRule, numeric_function) with pytest.raises(ValueError): func('$.Variable', 'ABC') - + with pytest.raises(ValueError): ChoiceRule.BooleanEquals('$.Variable', 42) @@ -61,6 +61,131 @@ def test_variable_value_must_be_consistent(): with pytest.raises(ValueError): func('$.Variable', True) +def test_path_comparator_raises_error_when_value_is_not_a_path(): + path_comparators = { + 'StringEqualsPath', + 'NumericEqualsPath', + 'TimestampEqualsPath', + 'BooleanEqualsPath' + } + for path_comparator in path_comparators: + func = getattr(ChoiceRule, path_comparator) + with pytest.raises(ValueError): + func('$.Variable', 'string') + +def test_is_comparator_raises_error_when_value_is_not_a_bool(): + type_comparators = { + 'IsPresent', + 'IsNull', + 'IsString', + 'IsNumeric', + 'IsBoolean', + 'IsTimestamp' + } + + for type_comparator in type_comparators: + func = getattr(ChoiceRule, type_comparator) + with pytest.raises(ValueError): + func('$.Variable', 'string') + with pytest.raises(ValueError): + func('$.Variable', 101) + +def test_static_comparator_serialization(): + string_timestamp_static_comparators = { + 'StringEquals', + 'StringLessThan', + 'StringLessThanEquals', + 'StringGreaterThan', + 'StringGreaterThanEquals', + 'TimestampEquals', + 'TimestampLessThan', + 'TimestampGreaterThan', + 'TimestampLessThanEquals' + } + + for string_timestamp_static_comparator in string_timestamp_static_comparators: + type_rule = getattr(ChoiceRule, string_timestamp_static_comparator)('$.input', 'hello') + expected_dict = {} + expected_dict['Variable'] = '$.input' + expected_dict[string_timestamp_static_comparator] = 'hello' + assert type_rule.to_dict() == expected_dict + + number_static_comparators = { + 'NumericEquals', + 'NumericLessThan', + 'NumericGreaterThan', + 'NumericLessThanEquals', + 'NumericGreaterThanEquals' + } + + for number_static_comparator in number_static_comparators: + type_rule = getattr(ChoiceRule, number_static_comparator)('$.input', 123) + expected_dict = {} + expected_dict['Variable'] = '$.input' + expected_dict[number_static_comparator] = 123 + assert type_rule.to_dict() == expected_dict + + boolean_static_comparators = { + 'BooleanEquals' + } + + for boolean_static_comparator in boolean_static_comparators: + type_rule = getattr(ChoiceRule, boolean_static_comparator)('$.input', False) + expected_dict = {} + expected_dict['Variable'] = '$.input' + expected_dict[boolean_static_comparator] = False + assert type_rule.to_dict() == expected_dict + +def test_dynamic_comparator_serialization(): + dynamic_comparators = { + 'StringEqualsPath', + 'StringLessThanPath', + 'StringLessThanEqualsPath', + 'StringGreaterThanPath', + 'StringGreaterThanEqualsPath', + 'TimestampEqualsPath', + 'TimestampLessThanPath', + 'TimestampGreaterThanPath', + 'TimestampLessThanEqualsPath', + 'NumericEqualsPath', + 'NumericLessThanPath', + 'NumericGreaterThanPath', + 'NumericLessThanEqualsPath', + 'NumericGreaterThanEqualsPath', + 'BooleanEqualsPath' + } + + for dynamic_comparator in dynamic_comparators: + type_rule = getattr(ChoiceRule, dynamic_comparator)('$.input', '$.input2') + expected_dict = {} + expected_dict['Variable'] = '$.input' + expected_dict[dynamic_comparator] = '$.input2' + assert type_rule.to_dict() == expected_dict + +def test_type_check_comparators_serialization(): + type_comparators = { + 'IsPresent', + 'IsNull', + 'IsString', + 'IsNumeric', + 'IsBoolean', + 'IsTimestamp' + } + + for type_comparator in type_comparators: + type_rule = getattr(ChoiceRule, type_comparator)('$.input', True) + expected_dict = {} + expected_dict['Variable'] = '$.input' + expected_dict[type_comparator] = True + assert type_rule.to_dict() == expected_dict + +def test_string_matches_serialization(): + string_matches_rule = ChoiceRule.StringMatches('$.input', 'hello*world\\*') + assert string_matches_rule.to_dict() == { + 'Variable': '$.input', + 'StringMatches': 'hello*world\\*' + } + def test_rule_serialization(): bool_rule = ChoiceRule.BooleanEquals('$.BooleanVariable', True) assert bool_rule.to_dict() == { diff --git a/tests/unit/test_steps.py b/tests/unit/test_steps.py index 1002238..87b5f17 100644 --- a/tests/unit/test_steps.py +++ b/tests/unit/test_steps.py @@ -6,9 +6,9 @@ # # http://www.apache.org/licenses/LICENSE-2.0 # -# or in the "license" file accompanying this file. This file 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 +# or in the "license" file accompanying this file. This file 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. from __future__ import absolute_import @@ -32,7 +32,7 @@ def test_state_creation(): parameters = {'Key': 'Value'}, result_path = '$.Result' ) - + assert state.to_dict() == { 'Type': 'Void', 'Comment': 'This is a comment', @@ -56,7 +56,7 @@ def test_pass_state_creation(): 'Result': 'Pass', 'End': True } - + def test_verify_pass_state_fields(): pass_state = Pass( state_id='Pass', @@ -149,16 +149,22 @@ def test_verify_wait_state_fields(): def test_choice_state_creation(): choice_state = Choice('Choice', input_path='$.Input') + choice_state.add_choice(ChoiceRule.IsPresent("$.StringVariable1", True), Pass("End State 1")) choice_state.add_choice(ChoiceRule.StringEquals("$.StringVariable1", "ABC"), Pass("End State 1")) - choice_state.add_choice(ChoiceRule.StringLessThanEquals("$.StringVariable2", "ABC"), Pass("End State 2")) + choice_state.add_choice(ChoiceRule.StringLessThanEqualsPath("$.StringVariable2", "$.value"), Pass("End State 2")) choice_state.default_choice(Pass('End State 3')) assert choice_state.state_id == 'Choice' - assert len(choice_state.choices) == 2 + assert len(choice_state.choices) == 3 assert choice_state.default.state_id == 'End State 3' assert choice_state.to_dict() == { 'Type': 'Choice', 'InputPath': '$.Input', 'Choices': [ + { + 'Variable': '$.StringVariable1', + 'IsPresent': True, + 'Next': 'End State 1' + }, { 'Variable': '$.StringVariable1', 'StringEquals': 'ABC', @@ -166,7 +172,7 @@ def test_choice_state_creation(): }, { 'Variable': '$.StringVariable2', - 'StringLessThanEquals': 'ABC', + 'StringLessThanEqualsPath': '$.value', 'Next': 'End State 2' } ], @@ -283,13 +289,13 @@ def test_append_states_after_terminal_state_will_fail(): chain.append(Pass('Pass')) chain.append(Fail('Fail')) chain.append(Pass('Pass2')) - + with pytest.raises(ValueError): chain = Chain() chain.append(Pass('Pass')) chain.append(Succeed('Succeed')) chain.append(Pass('Pass2')) - + with pytest.raises(ValueError): chain = Chain() chain.append(Pass('Pass')) @@ -317,7 +323,7 @@ def test_chaining_steps(): with pytest.raises(DuplicateStatesInChain): chain3 = Chain([chain1, chain2]) - + s1.next(s2) chain3 = Chain([s3, s1]) assert chain3.steps == [s3, s1] @@ -344,7 +350,7 @@ def test_catch_fail_for_unsupported_state(): def test_retry_fail_for_unsupported_state(): c1 = Choice('My Choice') - + with pytest.raises(ValueError): c1.add_catch(Catch(error_equals=["States.NoChoiceMatched"], next_step=Fail("ChoiceFailed")))