avocado-framework/avocado

View on GitHub
avocado/core/tags.py

Summary

Maintainability
C
1 day
Test Coverage
A
100%
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014-2019

"""
Test tags utilities module
"""

from avocado.core.resolver import ReferenceResolutionResult


def _parse_filter_by_tags(filter_by_tags):
    """
    Parses the various filter by tags in "command line" format

    The filtering of tests usually happens my means of "--filter-by-tags"
    command line options, and many can be given.  This parses the contents
    of those into a list of must/must_not pairs, which can be used directly
    for comparisons when filtering.

    :param filter_by_tags: params in the format given to "-t/--filter-by-tags"
    :type filter_by_tags: list of str
    :returns: list of tuples with (set, set)
    """
    result = []
    for raw_tags in filter_by_tags:
        required_tags = raw_tags.split(",")
        must = set()
        must_not = set()
        for tag in required_tags:
            if tag.startswith("-"):
                must_not.add(tag[1:])
            else:
                must.add(tag)
        result.append((must, must_not))
    return result


def _must_split_flat_key_val(must):
    """
    Splits the flat and key:val tags apart

    :returns: the flat set tags and the key:val tags
    :rtype: tuple(set, dict)
    """
    key_val = {}
    flat = set()
    for i in must:
        if ":" in i:
            k, v = i.split(":", 1)
            key_val[k] = v
        else:
            flat.add(i)
    return flat, key_val


def _must_key_val_matches(must_key_vals, test_tags, include_empty_key):
    """
    Checks if the required key:vals are fulfilled by the test_tags

    :rtype: bool
    """
    key_val_test_tags = {}
    for k, v in test_tags.items():
        if v is None:
            continue
        key_val_test_tags[k] = v

    for k, v in must_key_vals.items():
        if k not in test_tags:
            if include_empty_key:
                continue
            else:
                return False
        if v.startswith("-"):
            abs_v = v[1:]
            if abs_v in key_val_test_tags.get(k, set()):
                return False
        elif v not in key_val_test_tags.get(k, set()):
            return False
    return True


def filter_tags_on_runnables(
    resolutions, filter_by_tags, include_empty=False, include_empty_key=False
):
    """
    Filters out runnables that do not match the tags criteria given

    The filtering mechanism is agnostic to test type.  It means that
    if users request filtering by tag and the specific test type does
    not populate the test tags, it will be considered to have empty
    tags.

    :param resolutions: possible multiple resolutions for multiple
                        references
    :type resolutions: list of :class:`avocado.core.resolver.ReferenceResolution`
    :param filter_by_tags: the list of tag sets to use as filters
    :type filter_by_tags: list of comma separated tags (['foo,bar', 'fast'])
    :param include_empty: if true tests without tags will not be filtered out
    :type include_empty: bool
    :param include_empty_key: if true tests "keys" on key:val tags will be
                              included in the filtered results
    :type include_empty_key: bool
    :returns: the resolutions converted to runnables filtered by tags
    :rtype: list of :class:`avocado.core.nrunner.Runnable`
    """
    filtered = []
    must_must_nots = _parse_filter_by_tags(filter_by_tags)

    for resolution in resolutions:
        if resolution.result != ReferenceResolutionResult.SUCCESS:
            continue

        for runnable in resolution.resolutions:
            test_tags = runnable.tags or {}
            if not test_tags:
                if include_empty:
                    filtered.append(runnable)
                continue

            for must, must_not in must_must_nots:
                if must_not.intersection(test_tags):
                    continue

                must_flat, must_key_val = _must_split_flat_key_val(must)
                if must_key_val:
                    if not _must_key_val_matches(
                        must_key_val, test_tags, include_empty_key
                    ):
                        continue

                if must_flat:
                    if not must_flat.issubset(test_tags):
                        continue

                filtered.append(runnable)
                break

    return filtered