avocado-framework/avocado

View on GitHub
avocado/core/parameters.py

Summary

Maintainability
A
1 hr
Test Coverage
A
98%
# 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-2017

"""
Module related to test parameters
"""

import logging
import re


class NoMatchError(KeyError):
    pass


class AvocadoParams:
    """
    Params object used to retrieve params from given path. It supports
    absolute and relative paths. For relative paths one can define multiple
    paths to search for the value.
    It contains compatibility wrapper to act as the original avocado Params,
    but by special usage you can utilize the new API. See ``get()``
    docstring for details.

    You can also iterate through all keys, but this can generate quite a lot
    of duplicate entries inherited from ancestor nodes.  It shouldn't produce
    false values, though.
    """

    def __init__(self, leaves, paths, logger_name=None):
        """
        :param leaves: List of TreeNode leaves defining current variant
        :param paths: list of entry points
        :param logger_name: the name of a logger to use to record attempts
                            to get parameters
        :type logger_name: str
        """
        self._rel_paths = []
        leaves = list(leaves)
        for i, path in enumerate(paths):
            path_leaves = self._get_matching_leaves(path, leaves)
            self._rel_paths.append(AvocadoParam(path_leaves, f"{int(i)}: {path}"))
        # Don't use non-mux-path params for relative paths
        path_leaves = self._get_matching_leaves("/*", leaves)
        self._abs_path = AvocadoParam(path_leaves, "*: *")
        self._cache = {}  # TODO: Implement something more efficient
        self._logger_name = logger_name

    def __eq__(self, other):
        if set(self.__dict__) != set(other.__dict__):
            return False
        for attr in self.__dict__:
            if getattr(self, attr) != getattr(other, attr):
                return False
        return True

    def __ne__(self, other):
        return not (self == other)

    def __repr__(self):
        return f"<AvocadoParams {self._str()}>"

    def __str__(self):
        return f"params {self._str()}"

    def _str(self):
        out = ",".join(_.str_leaves_variant for _ in self._rel_paths)
        if out:
            return self._abs_path.str_leaves_variant + "," + out
        else:
            return self._abs_path.str_leaves_variant

    def _get_matching_leaves(self, path, leaves):
        """
        Pops and returns list of matching nodes
        :param path: Path (str)
        :param leaves: list of TreeNode leaves
        """
        path_re = self._greedy_path_to_re(path)
        path_leaves = [leaf for leaf in leaves if path_re.search(leaf.path + "/")]
        for leaf in path_leaves:
            leaves.remove(leaf)
        return path_leaves

    @staticmethod
    def _greedy_path_to_re(path):
        """
        Converts user-friendly path with asterisk to a regex and compiles it

        :param path: a more natural, file-system-like/glob-like
                     expression for the paths
        :type path: builtin.str
        :returns: a compiled regex
        """
        if not path:
            return re.compile("^$")
        if path[-1] == "*":
            suffix = ""
            path = path[:-1]
        else:
            suffix = "$"
        return re.compile(path.replace("*", "[^/]*") + suffix)

    @staticmethod
    def _is_abspath(path):
        """Is this an absolute or relative path?"""
        if path.pattern and path.pattern[0] == "/":
            return True
        else:
            return False

    def get(self, key, path=None, default=None):
        """
        Retrieve value associated with key from params
        :param key: Key you're looking for
        :param path: namespace ['*']
        :param default: default value when not found
        :raise KeyError: In case of multiple different values (params clash)
        """
        if path is None:  # default path is any relative path
            path = "*"
        try:
            return self._cache[(key, path, default)]
        except (KeyError, TypeError):
            # KeyError - first query
            # TypeError - unable to hash
            value = self._get(key, path, default)
            if self._logger_name is not None:
                logger = logging.getLogger(self._logger_name)
                logger.debug(
                    "PARAMS (key=%s, path=%s, default=%s) => %r",
                    key,
                    path,
                    default,
                    value,
                )
            try:
                self._cache[(key, path, default)] = value
            except TypeError:
                pass
            return value

    def _get(self, key, path, default):
        """
        Actual params retrieval
        :param key: key you're looking for
        :param path: namespace
        :param default: default value when not found
        :raise KeyError: In case of multiple different values (params clash)
        """
        path_re = self._greedy_path_to_re(path)
        for param in self._rel_paths:
            try:
                return param.get_or_die(path_re, key)
            except NoMatchError:
                pass
        if self._is_abspath(path_re):
            try:
                return self._abs_path.get_or_die(path_re, key)
            except NoMatchError:
                pass
        return default

    def objects(self, key, path=None):
        """
        Return the names of objects defined using a given key.

        :param key: The name of the key whose value lists the objects
                (e.g. 'nics').
        """
        return self.get(path, key, "").split()

    def iteritems(self):
        """
        Iterate through all available params and yield origin, key and value
        of each unique value.
        """
        env = []
        for param in self._rel_paths:
            for path, key, value in param.iteritems():
                if (path, key) not in env:
                    env.append((path, key))
                    yield (path, key, value)
        for path, key, value in self._abs_path.iteritems():
            if (path, key) not in env:
                env.append((path, key))
                yield (path, key, value)


class AvocadoParam:
    """
    This is a single slice params. It can contain multiple leaves and tries to
    find matching results.
    """

    def __init__(self, leaves, name):
        """
        :param leaves: this slice's leaves
        :param name: this slice's name (identifier used in exceptions)
        """
        # Basic initialization
        self._leaves = leaves
        # names cache (leaf.path is quite expensive)
        self._leaf_names = [leaf.path + "/" for leaf in leaves]
        self.name = name

    def __eq__(self, other):
        if self.__dict__ == other.__dict__:
            return True
        else:
            return False

    def __ne__(self, other):
        return not (self == other)

    @property
    def str_leaves_variant(self):
        """String with identifier and all params"""
        return f"{self.name} ({self._leaf_names})"

    def _get_leaves(self, path):
        """
        Get all leaves matching the path
        """
        return [
            self._leaves[i]
            for i in range(len(self._leaf_names))
            if path.search(self._leaf_names[i])
        ]

    def get_or_die(self, path, key):
        """
        Get a value or raise exception if not present
        :raise NoMatchError: When no matches
        :raise KeyError: When value is not certain (multiple matches)
        """
        leaves = self._get_leaves(path)
        ret = [
            (leaf.environment[key], leaf.environment.origin[key])
            for leaf in leaves
            if key in leaf.environment
        ]
        if not ret:
            raise NoMatchError(
                f"No matches to {path.pattern} => "
                f" {key} in {self.str_leaves_variant}"
            )
        # make sure all params come from the same origin
        if len(set([_[1].path for _ in ret])) == 1:
            return ret[0][0]
        else:
            raise ValueError(
                "Multiple %s leaves contain the key '%s'; %s"  # pylint: disable=C0209
                % (
                    path.pattern,
                    key,
                    [
                        "%s=>%s" % (_[1].path, _[0])  # pylint: disable=C0209
                        for _ in ret
                    ],
                )
            )

    def iteritems(self):
        """
        Very basic implementation which iterates through __ALL__ params,
        which generates lots of duplicate entries due to inherited values.
        """
        for leaf in self._leaves:
            for key, value in leaf.environment.items():
                yield (leaf.environment.origin[key].path, key, value)