diff --git a/docs/context.rst b/docs/context.rst index 6d4ea7f..5250653 100644 --- a/docs/context.rst +++ b/docs/context.rst @@ -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:]]+ @@ -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 diff --git a/docs/releases.rst b/docs/releases.rst index 944383d..154cabf 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fmf/context.py b/fmf/context.py index 61404f0..7ba7170 100644 --- a/fmf/context.py +++ b/fmf/context.py @@ -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__): @@ -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 """ @@ -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 @@ -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 diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index 5d17891..915d57c 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -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"]