Skip to content

Commit

Permalink
Allow filtering based on the node name
Browse files Browse the repository at this point in the history
Add support for filtering based on the node name by allowing plain
literals without the `key: name` format.
  • Loading branch information
psss committed Jun 3, 2024
1 parent 57a4820 commit 4f88531
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 11 deletions.
2 changes: 1 addition & 1 deletion fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def prune(self, whole=False, keys=None, names=None, filters=None,
continue
# Apply filters and conditions if given
try:
if not all([utils.filter(filter, node.data, regexp=True)
if not all([utils.filter(filter, node.data, regexp=True, name=node.name)
for filter in filters]):
continue
if not all([utils.evaluate(condition, node.data, node)
Expand Down
59 changes: 51 additions & 8 deletions fmf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ def evaluate(expression, data, _node=None):
raise FilterError("Internal key is not defined: {}".format(error))


def filter(filter, data, sensitive=True, regexp=False):
def filter(filter, data, sensitive=True, regexp=False, name=None):
"""
Return true if provided filter matches given dictionary of values
Apply advanced filter on the provided data dictionary
Filter supports disjunctive normal form with '|' used for OR, '&'
for AND and '-' for negation. Individual values are prefixed with
Expand All @@ -227,16 +227,35 @@ def filter(filter, data, sensitive=True, regexp=False):
tag: A, B, C ---> tag: A | tag: B | tag: C
If the ``key: value`` format is not detected, that is when ``:``
character is not used in the literal, the expression is considered
to be a search for the node name and will return ``True`` if
provided string matches the content of the optional ``name``
parameter::
/tests/core & tag: quick
Values should be provided as a dictionary of lists each describing
the values against which the filter is to be matched. For example::
data = {tag: ["Tier1", "TIPpass"], category: ["Sanity"]}
Other types of dictionary values are converted into a string.
A FilterError exception is raised when a dimension parsed from the
filter is not found in the data dictionary. Set option 'sensitive'
to False to enable case-insensitive matching. If 'regexp' option is
True, regular expressions can be used in the filter values as well.
:param sensitive: Set to False to enable case-insensitive matching.
:param regexp: If True, regular expressions can be used in the
filter values and name search as well.
:param name: Node name to be used when searching by name.
:raises FilterError: when a dimension parsed from the filter is not
found in the data dictionary or search for node name is detected
but node name is not provided.
:returns: True if the filter matches given dictionary of values and
the node name (if provided).
"""

def match_value(pattern, text):
Expand Down Expand Up @@ -280,17 +299,41 @@ def check_dimension(dimension, values):
# Every value must match at least one value for data
return all([check_value(dimension, value) for value in values])

def check_name(pattern: str) -> bool:
"""
Check whether the node name matches
Search for regular expression pattern if `regexp` is turned on,
simply compare strings otherwise.
"""
# Node name has to be provided if name search requested
if name is None:
raise FilterError(
f"Filter by name '{pattern}' requested "
f"but node name not provided to 'filter()'.")

if regexp:
return bool(re.search(pattern, name))
else:
return pattern == name

def check_clause(clause):
""" Split into literals and check whether all match """
# E.g. clause = 'tag: A, B & tag: C & tag: -D'
# Split into individual literals by dimension
literals = dict()
for literal in re.split(r"\s*&\s*", clause):
# E.g. literal = 'tag: A, B'
# Make sure the literal matches dimension:value format
# Check whether the literal matches dimension:value format
matched = re.match(r"^([^:]*)\s*:\s*(.*)$", literal)
if not matched:
raise FilterError("Invalid filter '{0}'".format(literal))
# Handle literal as a node name check. If name matches,
# no action needed, the decision is left on the
# remaining literals.
if check_name(literal):
continue
else:
return False
dimension, value = matched.groups()
values = [value]
# Append the literal value(s) to corresponding dimension list
Expand Down
26 changes: 24 additions & 2 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ def setup_method(self, method):

def test_invalid(self):
""" Invalid filter format """
with pytest.raises(utils.FilterError):
filter("x & y", self.data)
with pytest.raises(utils.FilterError):
filter("status:proposed", self.data)
with pytest.raises(utils.FilterError):
Expand All @@ -37,6 +35,30 @@ def test_empty_filter(self):
assert filter(None, self.data) is True
assert filter("", self.data) is True

def test_name_missing(self):
""" Node name has to be provided when searching for names """
with pytest.raises(utils.FilterError):
filter("/tests/core", self.data)
with pytest.raises(utils.FilterError):
filter("/tests/one | /tests/two", self.data)

def test_name_provided(self):
""" Searching by node names """
# Basic
assert filter("/tests/one", self.data, name="/tests/one") is True
assert filter("/tests/two", self.data, name="/tests/one") is False

# Combined
assert filter("/tests/one | /tests/two", self.data, name="/tests/one") is True
assert filter("/tests/one | tag: bad", self.data, name="/tests/one") is True
assert filter("/tests/one & tag: bad", self.data, name="/tests/one") is False
assert filter("/tests/wrong | tag: Tier1", self.data, name="/tests/one") is True
assert filter("/tests/wrong & tag: Tier1", self.data, name="/tests/one") is False

# Regular expressions
assert filter("/.*/one", self.data, name="/tests/one") is False
assert filter("/.*/one", self.data, name="/tests/one", regexp=True) is True

def test_basic(self):
""" Basic stuff and negation """
assert filter("tag: Tier1", self.data) is True
Expand Down

0 comments on commit 4f88531

Please sign in to comment.