Skip to content

Commit

Permalink
Allow case-insensitive matching
Browse files Browse the repository at this point in the history
  • Loading branch information
therazix committed Jul 24, 2023
1 parent 1971d30 commit fb4cf07
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 20 deletions.
4 changes: 4 additions & 0 deletions docs/context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ Values are always converted to a string representation. Each
value is treated as if it was a component with version. Name of
the dimension doesn't matter, all are treated equally.

**Values are case-sensitive by default**,
which means that values like 'centos' and 'CentOS' are considered
different.

The characters ``:`` or ``.`` or ``-`` are used as version
separators and are handled in the same way. The following examples
demonstrate how the ``name`` and ``version`` parts are parsed::
Expand Down
8 changes: 7 additions & 1 deletion fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def update(self, data):
log.debug("Data for '{0}' updated.".format(self))
log.data(pretty(self.data))

def adjust(self, context, key='adjust', undecided='skip'):
def adjust(self, context, key='adjust', undecided='skip', case_sensitive=True):
"""
Adjust tree data based on provided context and rules
Expand All @@ -334,6 +334,10 @@ class describing the environment context. By default, the key
context dimension is not defined. By default, such rules are
skipped. In order to raise the fmf.context.CannotDecide
exception in such cases use undecided='raise'.
Optional 'case_sensitive' parameter can be used to specify
if the context dimension values should be case-sensitive when
matching the rules. By default, values are case-sensitive.
"""

# Check context sanity
Expand All @@ -356,6 +360,8 @@ class describing the environment context. By default, the key
except KeyError:
rules = []

context.case_sensitive = case_sensitive

# Check and apply each rule
for rule in rules:

Expand Down
82 changes: 63 additions & 19 deletions fmf/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __str__(self):
def __repr__(self):
return "{}({})".format(self.__class__.__name__, repr(self._to_compare))

def version_cmp(self, other, minor_mode=False, ordered=True):
def version_cmp(self, other, minor_mode=False, ordered=True, case_sensitive=True):
"""
Comparing two ContextValue objects
Expand Down Expand Up @@ -87,14 +87,19 @@ def version_cmp(self, other, minor_mode=False, ordered=True):
-1 when self < other
0 when self == other
1 when self > other
case_sensitive:
False ... ignore case when comparing
True ... case matters when comparing
"""
if not isinstance(other, self.__class__):
raise CannotDecide("Invalid types.")

if len(self._to_compare) == 0 or len(other._to_compare) == 0:
raise CannotDecide("Empty name part.")

if self._to_compare[0] != other._to_compare[0]:
if not self._compare_with_case(
self._to_compare[0], other._to_compare[0], case_sensitive=case_sensitive):
if ordered:
raise CannotDecide(
"Name parts differ, cannot compare for order.")
Expand All @@ -103,7 +108,8 @@ def version_cmp(self, other, minor_mode=False, ordered=True):
if minor_mode and len(other._to_compare) > 1:
# right side cares about 'major'
try:
if self._to_compare[1] != other._to_compare[1]:
if not self._compare_with_case(
self._to_compare[1], other._to_compare[1], case_sensitive=case_sensitive):
if ordered:
if len(other._to_compare) > 2:
# future Y comparison not allowed
Expand All @@ -119,7 +125,7 @@ def version_cmp(self, other, minor_mode=False, ordered=True):
# Now we can compare version parts as long as other needs to
compared = 0
for first, second in zip(self._to_compare[1:], other._to_compare[1:]):
compared = self.compare(first, second)
compared = self.compare(first, second, case_sensitive=case_sensitive)
if compared != 0: # not equal - return immediately
return compared
leftover_version_parts = len(other._to_compare) - len(self._to_compare)
Expand All @@ -138,7 +144,7 @@ def version_cmp(self, other, minor_mode=False, ordered=True):
return -1 # other is larger (more pars)

@staticmethod
def compare(first, second):
def compare(first, second, case_sensitive=True):
""" compare two version parts """
# Ideally use `from packaging import version` but we need older
# python support too so very rough
Expand All @@ -148,12 +154,35 @@ def compare(first, second):
second_version = int(second)
except ValueError:
# fallback to compare as strings
first_version = first
second_version = second
if case_sensitive:
first_version = first
second_version = second
else:
first_version = first.casefold()
second_version = second.casefold()
return (
(first_version > second_version) -
(first_version < second_version))

@staticmethod
def _compare_with_case(first, second, case_sensitive=True):
"""
Compare two values based on the case sensitivity setting.
:param first: first value
:param second: second value
:param case_sensitive: If True (default), the comparison is case-sensitive.
If False, the comparison is case-insensitive.
:return: True if the values match, False otherwise.
:rtype: bool
"""
if case_sensitive and first == second:
return True
if not case_sensitive and first.casefold() == second.casefold():
return True
return False

@staticmethod
def _split_to_version(text):
"""
Expand Down Expand Up @@ -197,15 +226,17 @@ def _op_eq(self, dimension_name, values):
""" '=' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=False) == 0
return dimension_value.version_cmp(
it_val, ordered=False, case_sensitive=self._case_sensitive) == 0

return self._op_core(dimension_name, values, comparator)

def _op_not_eq(self, dimension_name, values):
""" '!=' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=False) != 0
return dimension_value.version_cmp(
it_val, ordered=False, case_sensitive=self._case_sensitive) != 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -214,7 +245,7 @@ def _op_minor_eq(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=False) == 0
it_val, minor_mode=True, ordered=False, case_sensitive=self._case_sensitive) == 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -223,7 +254,7 @@ def _op_minor_not_eq(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=False) != 0
it_val, minor_mode=True, ordered=False, case_sensitive=self._case_sensitive) != 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -232,7 +263,7 @@ def _op_minor_less_or_eq(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=True) <= 0
it_val, minor_mode=True, ordered=True, case_sensitive=self._case_sensitive) <= 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -241,31 +272,34 @@ def _op_minor_less(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=True) < 0
it_val, minor_mode=True, ordered=True, case_sensitive=self._case_sensitive) < 0

return self._op_core(dimension_name, values, comparator)

def _op_less(self, dimension_name, values):
""" '<' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=True) < 0
return dimension_value.version_cmp(
it_val, ordered=True, case_sensitive=self._case_sensitive) < 0

return self._op_core(dimension_name, values, comparator)

def _op_less_or_equal(self, dimension_name, values):
""" '<=' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=True) <= 0
return dimension_value.version_cmp(
it_val, ordered=True, case_sensitive=self._case_sensitive) <= 0

return self._op_core(dimension_name, values, comparator)

def _op_greater_or_equal(self, dimension_name, values):
""" '>=' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=True) >= 0
return dimension_value.version_cmp(
it_val, ordered=True, case_sensitive=self._case_sensitive) >= 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -274,15 +308,16 @@ def _op_minor_greater_or_equal(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=True) >= 0
it_val, minor_mode=True, ordered=True, case_sensitive=self._case_sensitive) >= 0

return self._op_core(dimension_name, values, comparator)

def _op_greater(self, dimension_name, values):
""" '>' operator """

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(it_val, ordered=True) > 0
return dimension_value.version_cmp(
it_val, ordered=True, case_sensitive=self._case_sensitive) > 0

return self._op_core(dimension_name, values, comparator)

Expand All @@ -291,7 +326,7 @@ def _op_minor_greater(self, dimension_name, values):

def comparator(dimension_value, it_val):
return dimension_value.version_cmp(
it_val, minor_mode=True, ordered=True) > 0
it_val, minor_mode=True, ordered=True, case_sensitive=self._case_sensitive) > 0

return self._op_core(dimension_name, values, comparator)

Expand Down Expand Up @@ -372,6 +407,7 @@ def __init__(self, *args, **kwargs):
:raises InvalidContext
"""
self._dimensions = {}
self._case_sensitive = True

# Initialized with rule
if args:
Expand All @@ -393,6 +429,14 @@ def __init__(self, *args, **kwargs):
[self.parse_value(val) for val in values]
)

@property
def case_sensitive(self) -> bool:
return self._case_sensitive

@case_sensitive.setter
def case_sensitive(self, value: bool):
self._case_sensitive = value

@staticmethod
def parse_rule(rule):
"""
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,19 @@ def test_comma(self):
assert distro.matches("distro < centos-stream-9, fedora-34")
assert not distro.matches("distro < fedora-34, centos-stream-8")

def test_case_insensitive(self):
""" Test for case-insensitive matching """
python = Context(component="python3-3.8.5-5.fc32")
python.case_sensitive = False

assert python.matches("component == python3")
assert not python.matches("component == invalid")
assert python.matches("component == PYTHON3,INVALID")
assert python.matches("component == Python3")
assert python.matches("component == PyTHon3-3.8.5-5.FC32")
assert python.matches("component > python3-3.7")
assert python.matches("component < PYTHON3-3.9")


class TestContextValue:
impossible_split = ["x86_64", "ppc64", "fips", "errata"]
Expand Down Expand Up @@ -304,6 +317,9 @@ def test_version_cmp(self):
assert first.version_cmp(ContextValue("name"), minor_mode=True) == 0
assert first.version_cmp(ContextValue("name"), ordered=True) == 0
assert first.version_cmp(ContextValue("name"), ordered=False) == 0
assert first.version_cmp(ContextValue("NAME"), ordered=True, case_sensitive=False) == 0
assert first.version_cmp(ContextValue("NAME"), ordered=False, case_sensitive=False) == 0
assert first.version_cmp(ContextValue("NAME"), ordered=False, case_sensitive=True) == 1

assert first.version_cmp(ContextValue("name-1"), ordered=False) == 1
with pytest.raises(CannotDecide):
Expand All @@ -329,6 +345,11 @@ def test_version_cmp(self):
ContextValue("name-1-2"),
minor_mode=True,
ordered=False)
with pytest.raises(CannotDecide):
first.version_cmp(
ContextValue("NAME"),
ordered=True,
case_sensitive=True)

second = ContextValue("name-1-2-3")
assert second.version_cmp(ContextValue("name"), ordered=False) == 0
Expand Down Expand Up @@ -403,6 +424,8 @@ def test_prints(self):
def test_compare(self):
assert ContextValue.compare("1", "1") == 0
assert ContextValue.compare("a", "a") == 0
assert ContextValue.compare("1", "1", case_sensitive=False) == 0
assert ContextValue.compare("A", "a", case_sensitive=False) == 0

assert ContextValue.compare("rawhide", "aaa") == 1
assert ContextValue.compare("rawhide", "9999") == 1
Expand All @@ -412,6 +435,17 @@ def test_compare(self):
def test_string_conversion(self):
assert Context.parse_value(1) == ContextValue("1")

def test_compare_with_case(self):
assert ContextValue._compare_with_case("1", "1", case_sensitive=True)
assert ContextValue._compare_with_case("name_1", "name_1", case_sensitive=True)
assert not ContextValue._compare_with_case("NAME", "name", case_sensitive=True)
assert not ContextValue._compare_with_case("name_1", "NAME_1", case_sensitive=True)

assert ContextValue._compare_with_case("1", "1", case_sensitive=False)
assert ContextValue._compare_with_case("name_1", "name_1", case_sensitive=False)
assert ContextValue._compare_with_case("NAME", "name", case_sensitive=False)
assert ContextValue._compare_with_case("name_1", "NAME_1", case_sensitive=False)


class TestParser:
# Missing expression
Expand Down

0 comments on commit fb4cf07

Please sign in to comment.