Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/rule_engine/engine/_attribute_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,10 @@ def value_ends_with(self, value: Sequence[Any]) -> Callable[..., bool]:
return functools.partial(self._value_ends_with, value)

def _value_ends_with(self, value: Sequence[Any], suffix: Sequence[Any]) -> bool:
return value[-len(suffix):] == suffix
# index from the front (len(value) - len(suffix)) rather than the back
# (-len(suffix)) so an empty suffix yields an empty slice instead of the
# whole value; value[-0:] is value[0:], which made ends_with('') False
return value[len(value) - len(suffix):] == suffix

@attribute('is_empty', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.STRING, types.DataType.MAPPING, types.DataType.SET, result_type=types.DataType.BOOLEAN)
def value_is_empty(self, value: Sized) -> bool:
Expand Down
32 changes: 32 additions & 0 deletions tests/ast/expression/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,38 @@ def test_ast_expression_string_method_starts_with(self):
self.assertTrue(callable(method), "attribute starts_with failed (method not callable)")
self.assertEqual(method(prefix), result)

def test_ast_expression_value_methods_empty_affix(self):
# regression: an empty prefix/suffix must match any sequence, mirroring
# Python's str.startswith('') / str.endswith('') semantics. ends_with
# previously returned False for an empty suffix because
# value[-len(suffix):] is value[-0:] == value[0:] (the whole value)
# rather than an empty slice, diverging from its starts_with sibling.
cases = [
(ast.StringExpression, 'Rule Engine', '', 'Rule Engine'),
(ast.BytesExpression, b'Rule Engine', b'', b'Rule Engine'),
]
for expression_class, value, empty, whole in cases:
for method_name in ('ends_with', 'starts_with'):
symbol = expression_class(context, value)
expression = ast.GetAttributeExpression(context, symbol, method_name)
method = expression.evaluate(None)
self.assertTrue(callable(method), "attribute {} failed (method not callable)".format(method_name))
# an empty affix always matches
self.assertTrue(method(empty), "{}({!r}) should be True".format(method_name, empty))
# the whole value is both a prefix and a suffix of itself
self.assertTrue(method(whole), "{}({!r}) should be True".format(method_name, whole))

# arrays behave the same as strings and bytes
array_expression = ast.ArrayExpression(context, [
ast.StringExpression(context, 'Rule'),
ast.StringExpression(context, 'Engine')
])
for method_name in ('ends_with', 'starts_with'):
expression = ast.GetAttributeExpression(context, array_expression, method_name)
method = expression.evaluate(None)
self.assertTrue(method(()), "{}(()) should be True".format(method_name))
self.assertTrue(method(('Rule', 'Engine')), "{}(whole) should be True".format(method_name))

def test_ast_expression_string_attributes_flt(self):
combos = (
('3.14159', decimal.Decimal('3.14159')),
Expand Down
Loading