Skip to content

Commit

Permalink
Implement regular expression search for context (#272)
Browse files Browse the repository at this point in the history
Support new operator `~` for matching patterns and `!~` for
negative matching patterns in the context rules.

Fix #226.
  • Loading branch information
psss authored Jan 7, 2025
1 parent 95c9745 commit db640a0
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 6 deletions.
8 changes: 7 additions & 1 deletion docs/context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ supported operators consult the following grammar outline::
expression ::= 'true' | 'false'
dimension ::= [[:alnum:]]+
binary_operator ::= '==' | '!=' | '<' | '<=' | '>' | '>=' |
'~=' | '~!=' | '~<' | '~<=' | '~>' | '~>='
'~=' | '~!=' | '~<' | '~<=' | '~>' | '~>=' | '~' | '!~'
unary_operator ::= 'is defined' | 'is not defined'
values ::= value (',' value)*
value ::= [[:alnum:]]+
Expand All @@ -65,6 +65,12 @@ Let's demonstrate the syntax on a couple of real-life examples::
# check whether a dimension is defined
collection is not defined

# search dimension value for a regular expression
initiator ~ .*-ci

# make sure that the value does not match given regular expression
arch !~ ppc64.*

# disable adjust rule (e.g. during debugging / experimenting)
false and <original rule>

Expand Down
8 changes: 8 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
======================


fmf-1.6.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to search :ref:`context` dimension values using regular
expressions, it is now possible to use operator ``~`` for matching
patterns and operator ``!~`` for non matching patterns.


fmf-1.5.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
31 changes: 26 additions & 5 deletions fmf/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ class InvalidContext(Exception):
class ContextValue:
""" Value for dimension """

def __init__(self, origin):
def __init__(self, raw):
"""
ContextValue("foo-1.2.3")
ContextValue(["foo", "1", "2", "3"])
"""
if isinstance(origin, (tuple, list)):
self._to_compare = tuple(origin)
if isinstance(raw, (tuple, list)):
self._to_compare = tuple(raw)
else:
self._to_compare = self._split_to_version(origin)
self._to_compare = self._split_to_version(raw)

# Store the original string for regexp processing
self.raw = raw

def __eq__(self, other):
if isinstance(other, self.__class__):
Expand Down Expand Up @@ -238,6 +241,22 @@ def comparator(dimension_value, it_val):

return self._op_core(dimension_name, values, comparator)

def _op_match(self, dimension_name, values):
""" '~' operator, regular expression matches """

def comparator(dimension_value, it_val):
return re.search(it_val.raw, dimension_value.raw) is not None

return self._op_core(dimension_name, values, comparator)

def _op_not_match(self, dimension_name, values):
""" '~' operator, regular expression does not match """

def comparator(dimension_value, it_val):
return re.search(it_val.raw, dimension_value.raw) is None

return self._op_core(dimension_name, values, comparator)

def _op_minor_eq(self, dimension_name, values):
""" '~=' operator """

Expand Down Expand Up @@ -371,6 +390,8 @@ def _op_core(self, dimension_name, values, comparator):
"~>=": _op_minor_greater_or_equal,
">": _op_greater,
"~>": _op_minor_greater,
"~": _op_match,
"!~": _op_not_match,
}

# Triple expression: dimension operator values
Expand All @@ -379,7 +400,7 @@ def _op_core(self, dimension_name, values, comparator):
r"(\w+)"
+ r"\s*("
+ r"|".join(
set(operator_map.keys()) - {"is defined", "is not defined"})
[key for key in operator_map if key not in ["is defined", "is not defined"]])
+ r")\s*"
+ r"([^=].*)")
# Double expression: dimension operator
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,31 @@ def test_case_insensitive(self):
assert python.matches("component > python3-3.7")
assert python.matches("component < PYTHON3-3.9")

def test_regular_expression_matching(self):
""" Matching regular expressions """

assert Context(distro="fedora-42").matches("distro ~ ^fedora-42$")
assert Context(distro="fedora-42").matches("distro ~ fedora")
assert Context(distro="fedora-42").matches("distro ~ fedora|rhel")
assert Context(distro="fedora-42").matches("distro ~ fedora-4.*")
assert not Context(distro="fedora-42").matches("distro ~ fedora-3.*")
assert not Context(distro="fedora-42").matches("distro ~ ubuntu")

assert Context(arch="ppc64").matches("arch ~ ppc64.*")
assert Context(arch="ppc64le").matches("arch ~ ppc64.*")
assert not Context(arch="ppc64le").matches("arch ~ ppc64$")

assert not Context(distro="fedora-42").matches("distro !~ ^fedora-42$")
assert not Context(distro="fedora-42").matches("distro !~ fedora")
assert not Context(distro="fedora-42").matches("distro !~ fedora|rhel")
assert not Context(distro="fedora-42").matches("distro !~ fedora-4.*")
assert Context(distro="fedora-42").matches("distro !~ fedora-3.*")
assert Context(distro="fedora-42").matches("distro !~ ubuntu")

assert not Context(arch="ppc64").matches("arch !~ ppc64.*")
assert not Context(arch="ppc64le").matches("arch !~ ppc64.*")
assert Context(arch="ppc64le").matches("arch !~ ppc64$")


class TestContextValue:
impossible_split = ["x86_64", "ppc64", "fips", "errata"]
Expand Down

0 comments on commit db640a0

Please sign in to comment.