# Copyright (c) 2016-2023, MC ASIC Design Consulting
# All rights reserved.
#
# Author: Marek Cieplucha, https://github.com/mciepluc
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met
# (The BSD 2-Clause License):
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL POTENTIAL VENTURES LTD BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Functional Coverage features.
Classes:
* :class:`CoverageDB` - singleton containing coverage database.
* :class:`CoverItem` - base class for coverage, corresponds to a covergroup,
created automatically.
* :class:`CoverPoint` - a cover point with bins.
* :class:`CoverCross` - a cover cross of cover points.
* :class:`CoverCheck` - a cover point which checks only a pass/fail condition.
Functions:
* :func:`~.coverage_section` - allows for convenient definition of multiple
coverage items and combines them into a single decorator.
* :func:`~.merge_coverage` - merges coverage files in XML or YAML format.
"""
from functools import wraps
from collections import OrderedDict
import inspect
import operator
import itertools
import warnings
import copy
import threading
[docs]
class CoverageDB(dict):
""" Class (singleton) containing coverage database.
This is the coverage prefix tree (trie) containing all coverage objects
with name string as a key (using dot as a stage separator). Coverage
primitives may be accessed by string identificator. Example coverage trie
is shown below:
.. image:: coverstruct.png
:scale: 60 %
:align: center
Examples:
>>> CoverageDB()["top"] #access whole coverage under ``top``
>>> CoverageDB()["top.b"] #access whole covergroup
>>> CoverageDB()["top.b.cp1"] #access specific coverpoint
"""
_instance = None
_lock = threading.Lock()
def __new__(class_, *args, **kwargs):
if not isinstance(class_._instance, class_):
with class_._lock:
if not isinstance(class_._instance, class_):
class_._instance = dict.__new__(class_, *args, **kwargs)
return class_._instance
[docs]
def report_coverage(self, logger, bins=False, node=""):
"""Print sorted coverage with optional bins details.
Args:
logger (func): a logging function (e.g. logger.info or print).
bins (bool, optional): print bins details.
node (str, optional): starting node of the coverage trie.
"""
sorted_cov = sorted(self, key=str.lower)
for ii in filter(lambda _ : _.startswith(node), sorted_cov):
logger(" " * ii.count('.') + "%s : %s, coverage=%d, size=%d " %
(ii, self[ii], self[ii].coverage, self[ii].size)
)
if (type(self[ii]) is not CoverItem) & (bins):
for jj in self[ii].detailed_coverage:
logger(" " * ii.count('.') + " BIN %s : %s" %
(jj, self[ii].detailed_coverage[jj])
)
[docs]
def export_to_yaml(self, filename='coverage.yml'):
"""Export coverage_db to YAML document.
Args:
filename (str): output document name with .yml suffix
"""
import yaml
export_data = {}
for name_elem_full in sorted(self, key=str.lower):
attrib_dict = {}
attrib_dict['type'] = str(type(self[name_elem_full]))
attrib_dict['size'] = self[name_elem_full].size
attrib_dict['coverage'] = self[name_elem_full].coverage
attrib_dict['cover_percentage'] = round(self[name_elem_full].cover_percentage, 2)
if (type(self[name_elem_full]) is not CoverItem):
attrib_dict['weight'] = self[name_elem_full].weight
attrib_dict['at_least'] = self[name_elem_full].at_least
bins = []
hits = []
for key, value in self[name_elem_full].detailed_coverage.items():
if hasattr(key, '__iter__'): #convert iterables to string
key = str(key)
bins.append(key)
hits.append(value)
attrib_dict['bins:_hits'] = dict(zip(bins, hits))
export_data[name_elem_full] = attrib_dict
with open(filename, 'w') as outfile:
yaml.dump(export_data, outfile, default_flow_style=False)
[docs]
def export_to_xml(self, filename='coverage.xml'):
"""Export coverage_db to xml document.
Args:
filename (str): output document name with .xml suffix
"""
from xml.etree import ElementTree as et
xml_db_dict = {}
def create_top():
attrib_dict = {'abs_name': 'top'}
if 'top' in self:
attrib_dict['size'] = str(self['top'].size)
attrib_dict['coverage'] = str(self['top'].coverage)
attrib_dict['cover_percentage'] = str(round(
self['top'].cover_percentage, 2))
xml_db_dict['top'] = et.Element('top', attrib=attrib_dict)
def create_element(name_elem, parent, name_elem_full):
attrib_dict = {}
prefix = '' if 'top' in self else 'top.'
# Common attributes
attrib_dict['size'] = str(self[name_elem_full].size)
attrib_dict['coverage'] = str(self[name_elem_full].coverage)
attrib_dict['cover_percentage'] = str(round(
self[name_elem_full].cover_percentage, 2))
attrib_dict['abs_name'] = prefix+name_elem_full
if (type(self[name_elem_full]) is not CoverItem):
attrib_dict['weight'] = str(self[name_elem_full].weight)
attrib_dict['at_least'] = str(
self[name_elem_full].at_least)
# Create element: xml_db_dict[a.b.c] = et(a.b (parent), c(element))
xml_db_dict[name_elem_full] = et.SubElement(xml_db_dict[parent],
name_elem,
attrib=attrib_dict)
# Create bins for CoverCross and CoverPoint
if type(self[name_elem_full]) is not CoverItem:
bin_count = 0
# Database in format: key == bin, value == no_of_hits
for key, value in self[name_elem_full].detailed_coverage.items():
attrib_dict.clear()
attrib_dict['bin'] = str(key)
attrib_dict['hits'] = str(value)
attrib_dict['abs_name'] = (prefix+name_elem_full
+'.bin'+str(bin_count))
xml_db_dict[name_elem_full+'.bin'+str(bin_count)] = (
et.SubElement(xml_db_dict[name_elem_full],
'bin'+str(bin_count),
attrib=attrib_dict))
bin_count += 1
# ======================== Function body ==============================
create_top()
for name_elem_full in self:
if name_elem_full == 'top':
pass
else:
name_list = name_elem_full.split('.')
name_elem = name_list[-1]
name_parent = '.'.join(name_list[:-1])
if name_parent == '':
name_parent = 'top'
create_element(name_elem, name_parent, name_elem_full)
# update total coverage if there was no 'top' in coverage_db
if xml_db_dict['top'].attrib == {'abs_name': 'top'}:
top_size = 0
top_coverage = 0
for child in xml_db_dict['top']:
top_size += int(child.attrib['size'])
top_coverage += int(child.attrib['coverage'])
top_cover_percentage = round(top_coverage*100/top_size, 2)
xml_db_dict['top'].set('size', str(top_size))
xml_db_dict['top'].set('coverage', str(top_coverage))
xml_db_dict['top'].set(
'cover_percentage', str(top_cover_percentage))
root = et.ElementTree(xml_db_dict['top']).getroot()
_indent(root)
et.ElementTree(xml_db_dict['top']).write(filename)
# global variable collecting coverage in a prefix tree (trie)
coverage_db = CoverageDB()
""" Instance of the :class:`CoverageDB`."""
[docs]
class CoverItem(object):
"""Class used to describe coverage groups.
``CoverItem`` objects are created automatically. This is a base class for
all coverage primitives (:class:`CoverPoint`, :class:`CoverCross` or
:class:`CoverCheck`). It may be used as a base class for other,
user-defined coverage types.
"""
def __init__(self, name):
self._name = name
self._size = 0
self._coverage = 0
self._parent = None
self._children = []
self._new_hits = []
self._weight = 0
self._at_least = 0
self._threshold_callbacks = {}
self._bins_callbacks = {}
# check if parent exists
if "." in name:
parent_name = ".".join(name.split(".")[:-1])
if not parent_name in coverage_db:
CoverItem(name=parent_name)
self._parent = coverage_db[parent_name]
self._parent._children.append(self)
coverage_db[name] = self
def _update_coverage(self, coverage):
"""Update the parent coverage level as requested by derived classes.
"""
current_coverage = self._coverage
self._coverage += coverage
if self._parent is not None:
self._parent._update_coverage(coverage)
# notify callbacks
for ii in self._threshold_callbacks:
if (ii > 100 * current_coverage / self.size
and ii <= self.cover_percentage):
self._threshold_callbacks[ii]()
def _update_size(self, size):
"""Update the parent size as requested by derived classes.
"""
self._size += size
if self._parent is not None:
self._parent._update_size(size)
[docs]
def add_threshold_callback(self, callback, threshold):
"""Add a threshold callback to the :class:`CoverItem` or any its
derived class.
A callback is called (once) when the threshold is crossed, so that
coverage level of this particular cover group (or other object) exceeds
defined % value.
Args:
callback (func): a callback function.
threshold (int): a callback call threshold (% coverage).
Examples:
>>> def notify_threshold():
>>> print("reached 50% coverage!")
>>>
>>> # add callback to the cover group
>>> coverage_db["top.covergroup1"].add_threshold_callback(
>>> notify_threshold, 50
>>> )
>>> # add callback to the cover point
>>> coverage_db["top.covergroup1.coverpoint4"].add_threshold_callback(
>>> notify_threshold, 50
>>> )
"""
self._threshold_callbacks[threshold] = callback
[docs]
def add_bins_callback(self, callback, bins):
"""Add a bins callback to the derived class of the :class:`CoverItem`.
A callback is called (once) when a specific bin is covered.
Args:
callback (func): a callback function.
bins: a particular bin (type depends on bins type).
Examples:
>>> def notify_bins():
>>> print("covered bin 'special case'")
>>>
>>> coverage_db["top.covergroup1.coverpoint5"].add_bins_callback(
>>> notify_bins, 'special case'
>>> )
"""
self._bins_callbacks[bins] = callback
@property
def size(self):
"""Return size of the coverage primitive.
Size of the cover group (or other coverage primitive) is returned. This
is a total number of bins associated with assigned weights.
Returns:
int: size of the coverage primitive.
"""
return self._size
@property
def coverage(self):
"""Return size of the covered bins in the coverage primitive.
Number of the covered bins in cover group (or other coverage primitive)
is returned. This is a number of covered bins associated with assigned
weights.
Returns:
int: size of the covered bins.
"""
return self._coverage
@property
def cover_percentage(self):
"""Return coverage level of the coverage primitive.
Percent of the covered bins in cover group (or other coverage
primitive) is returned. This is basically a :meth:`coverage()` divided
by :meth:`size()` in %.
Returns:
float: percent of the coverage.
"""
return 100 * self.coverage / self.size
@property
def detailed_coverage(self):
"""Return detailed coverage - full list of bins associated with number
of hits. If labels are assigned to bins, labels are returned instead
of bins values.
A dictionary (bins) -> (number of hits) is returned.
Returns:
dict: dictionary associating number of hits with a particular bins.
"""
coverage = {}
for child in self._children:
coverage[child._name] = child.detailed_coverage
return coverage
@property
def new_hits(self):
"""Return bins hit at last sampling event. Works only for objects
deriving from :class:`CoverItem`.
Returns:
list: list of the new bins (which have not been already covered)
sampled at last sampling event.
"""
return self._new_hits
@property
def weight(self):
"""Return weight of the coverage primitive. Works only for objects
deriving from :class:`CoverItem`.
Returns:
int: weight of the coverage primitive.
"""
return self._weight
@property
def at_least(self):
"""Return ``at_least`` attribute of the coverage primitive. Works only
for objects deriving from :class:`CoverItem`.
Returns:
int: ``at_least`` attribute of the coverage primitive.
"""
return self._at_least
[docs]
class CoverPoint(CoverItem):
"""Class used to create coverage points as decorators.
This decorator samples members of the decorated function (its signature).
Sampling matches predefined bins according to the rule:
``rel(xf(args), bin) == True``
Args:
name (str): a ``CoverPoint`` path and name, defining its position in a
coverage trie.
vname (str, optional): a name of the variable to be covered (use this
only when covering a *single* variable in the decorated function
signature).
xf (func, optional): a transformation function which transforms
arguments of the decorated function. If ``vname`` and ``xf`` are
not defined, matched is a single input argument (if only one
exists) or a tuple (if multiple exist). Note that the ``self``
argument is *always* removed from the argument list.
bins (list): a list of bins objects to be matched. Note that for
non-trivial types, a ``rel`` must always be defined (or the
equality operator must be overloaded).
bins_labels (list, optional): a list of labels (str) associated with
defined bins. Both lists lengths must match.
rel (func, optional): a relation function which defines the bins
matching relation (by default, the equality operator ``==``).
weight (int, optional): a ``CoverPoint`` weight (by default ``1``).
at_least (int, optional): the number of hits per bins to be considered
as covered (by default ``1``).
inj (bool, optional): "injection" feature, defines that only one
bin can be matched at single sampling (default ``True``).
Example:
>>> @coverage.CoverPoint( # cover (arg/2) < 1 ... 4 (4 bins)
... name = "top.parent.coverpoint1",
... xf = lambda x : x/2,
... rel = lambda x, y : x < y,
... bins = list(range(5))
... )
>>> @coverage.CoverPoint( # cover (arg) == 1 ... 4 (4 bins)
... name = "top.parent.coverpoint2",
... vname = "arg",
... bins = list(range(5))
... )
>>> def decorated_func1(self, arg):
... ...
>>> @coverage.CoverPoint( # cover (arg1, arg2) == (1, 1) or (0, 0) (2 bins)
... name = "top.parent.coverpoint3",
... bins = [(1, 1), (0, 0)]
... )
>>> def decorated_func1(self, arg1, arg2):
... ...
"""
# conditional Object creation, only if name not already registered
def __new__(cls, name, vname=None, xf=None, rel=None, bins=[],
bins_labels=None, weight=1, at_least=1, inj=False):
if name in coverage_db:
return coverage_db[name]
else:
return super(CoverPoint, cls).__new__(cls)
def __init__(self, name, vname=None, xf=None, rel=None, bins=[],
bins_labels=None, weight=1, at_least=1, inj=True):
if not name in coverage_db:
CoverItem.__init__(self, name)
if self._parent is None:
raise Exception("CoverPoint must have a parent \
(parent.CoverPoint)")
if (bins_labels is not None) and (len(bins_labels) != len(bins)):
raise Exception("Length of bins and bins_labels must be \
equal")
self._bins_labels = bins_labels
self._transformation = xf
self._vname = vname
# equality operator is the default bins matching relation
self._relation = rel if rel is not None else operator.eq
self._weight = weight
self._at_least = at_least
self._injection = inj
if (len(bins) != 0):
self._size = self._weight * len(bins)
self._hits = OrderedDict.fromkeys(bins, 0)
else: # if no bins specified, add one bin equal True
self._size = self._weight
self._hits = OrderedDict.fromkeys([True], 0)
#make a map assigning label to the bin
if self._bins_labels is not None:
self._labels_bins = dict(zip(bins, bins_labels))
# determines whether decorated a bound method
self._decorates_method = None
# determines whether transformation function is a bound method
self._trans_is_method = None
self._parent._update_size(self._size)
self._new_hits = [] # list of bins hit per single function call
def __call__(self, f):
@wraps(f)
def _wrapped_function(*cb_args, **cb_kwargs):
if len(cb_kwargs) > 0:
raise Exception("Use of keyword args in sampling function call is not supported.")
# if transformation function not defined, simply return arguments
if self._transformation is None:
if self._vname is None:
def dummy_f(*cb_args): # return a tuple or single object
if len(cb_args) > 1:
return cb_args
else:
return cb_args[0]
# if vname defined, match it to the decroated function args
else:
arg_names = list(inspect.signature(f).parameters)
idx = arg_names.index(self._vname)
def dummy_f(*cb_args):
return cb_args[idx]
self._transformation = dummy_f
# for the first time only check if decorates method in the class
if self._decorates_method is None:
self._decorates_method = False
for x in inspect.getmembers(cb_args[0]):
if '__func__' in dir(x[1]):
# compare decorated function name with class functions
self._decorates_method = \
f.__name__ == x[1].__func__.__name__
if self._decorates_method:
break
# for the first time only check if a transformation function is a
# method
if self._trans_is_method is None:
self._trans_is_method = "self" in inspect.signature(
self._transformation).parameters
current_coverage = self.coverage
self._new_hits = []
# if function is bound then remove "self" from the arguments list
if self._decorates_method ^ self._trans_is_method:
result = self._transformation(*cb_args[1:])
else:
result = self._transformation(*cb_args)
# compare function result using relation function with matching
# bins
for bin in self._hits:
if self._relation(result, bin):
self._hits[bin] += 1
if self._bins_labels is not None:
self._new_hits.append(self._labels_bins[bin])
else:
self._new_hits.append(bin)
# check bins callbacks
if bin in self._bins_callbacks:
self._bins_callbacks[bin]()
# if injective function, continue through all bins
if self._injection:
break
# notify parent about new coverage level
self._parent._update_coverage(self.coverage - current_coverage)
# check threshold callbacks
for ii in self._threshold_callbacks:
if (ii > 100 * current_coverage / self.size
and ii <= 100 * self.coverage / self.size):
self._threshold_callbacks[ii]()
return f(*cb_args, **cb_kwargs)
return _wrapped_function
@property
def coverage(self):
coverage = self._size
for ii in self._hits:
if self._hits[ii] < self._at_least:
coverage -= self._weight
return coverage
@property
def detailed_coverage(self):
if self._bins_labels is not None:
return dict(zip(self._bins_labels, list(self._hits.values())))
return self._hits
[docs]
class CoverCross(CoverItem):
"""Class used to create coverage crosses as decorators.
This decorator samples members of the decorated function (its signature).
It matches tuples cross-bins which are Cartesian products of bins defined
in :class:`CoverPoints <CoverPoint>` (items).
Args:
name (str): a ``CoverCross`` path and name, defining its position in a
coverage trie.
items (list): a list of :class:`CoverPoints <CoverPoint>` by names,
to create a Cartesian product of cross-bins.
ign_bins (list, optional): a list of bins to be ignored.
weight (int, optional): a ``CoverCross`` weight (by default ``1``).
at_least (int, optional): the number of hits per bin to be considered
as covered (by default ``1``).
Example:
>>> @coverage.CoverPoint(
... name = "top.parent.coverpoint1",
... xf = lambda x, y: x,
... bins = list(range(5)) # 4 bins in total
... )
>>> @coverage.CoverPoint(
... name = "top.parent.coverpoint2",
... xf = lambda x, y: y,
... bins = list(range(5)) # 4 bins in total
... )
>>> @coverage.CoverCross(
... name = "top.parent.covercross",
... items = ["top.parent.coverpoint1", "top.parent.coverpoint2"],
... ign_bins = [(1, 1), (4, 4)], # 4x4 - 2 = 14 bins in total
... )
>>> def decorated_func(self, arg_a, arg_b):
>>> # bin from the bins list [(1, 2), (1, 3)...(4, 3)] will be matched
>>> # when a tuple (x=arg_a, y=arg_b) was sampled at this function call.
... ...
"""
# conditional Object creation, only if name not already registered
def __new__(cls, name, items=[], ign_bins=[], weight=1, at_least=1):
if name in coverage_db:
return coverage_db[name]
else:
return super(CoverCross, cls).__new__(cls)
def __init__(self, name, items=[], ign_bins=[], weight=1, at_least=1):
if not name in coverage_db:
CoverItem.__init__(self, name)
if self._parent is None:
raise Exception("CoverCross must have a parent \
(parent.CoverCross)")
self._weight = weight
self._at_least = at_least
# equality operator is the defult ignore bins matching relation
self._items = items
bins_lists = []
for cp_names in self._items:
bins_lists.append(
coverage_db[cp_names].detailed_coverage.keys())
# a map of cross-bins, key is a tuple of bins Cartesian product
self._hits = dict.fromkeys(itertools.product(*bins_lists), 0)
# remove ignore bins from _hits map if relation is true
for x_bin in list(self._hits.keys()):
for ignore_bins in ign_bins:
remove = True
for ii in range(0, len(x_bin)):
if ignore_bins[ii] is not None:
if (ignore_bins[ii] != x_bin[ii]):
remove = False
if remove and (x_bin in self._hits):
del self._hits[x_bin]
self._size = self._weight * len(self._hits)
self._parent._update_size(self._size)
def __call__(self, f):
@wraps(f)
def _wrapped_function(*cb_args, **cb_kwargs):
if len(cb_kwargs) > 0:
raise Exception("Use of keyword args in sampling function call is not supported.")
current_coverage = self.coverage
self._new_hits = []
hit_lists = []
for cp_name in self._items:
hit_lists.append(coverage_db[cp_name]._new_hits)
# a list of hit cross-bins, key is a tuple of bins Cartesian
# product
for x_bin_hit in list(itertools.product(*hit_lists)):
if x_bin_hit in self._hits:
self._hits[x_bin_hit] += 1
self._new_hits.append(x_bin_hit)
# check bins callbacks
if x_bin_hit in self._bins_callbacks:
self._bins_callbacks[x_bin_hit]()
# notify parent about new coverage level
self._parent._update_coverage(self.coverage - current_coverage)
# check threshold callbacks
for ii in self._threshold_callbacks:
if (ii > 100 * current_coverage / self.size
and ii <= 100 * self.coverage / self.size):
self._threshold_callbacks[ii]()
return f(*cb_args, **cb_kwargs)
return _wrapped_function
@property
def coverage(self):
coverage = self._size
for ii in self._hits:
if self._hits[ii] < self._at_least:
coverage -= self._weight
return coverage
@property
def detailed_coverage(self):
return self._hits
[docs]
class CoverCheck(CoverItem):
"""Class used to create coverage checks as decorators.
It is a simplified :class:`CoverPoint` with defined 2 bins:
*PASS* and *FAIL* and ``f_pass()`` and ``f_fail()`` functions.
Args:
name (str): a ``CoverCheck`` path and name, defining its position in a
coverage trie.
f_fail: a failure condition function - if it returns ``True``, the
coverage level is set to ``0`` permanently.
f_pass: a pass condition function - if it returns ``True``, the
coverage level is set to ``weight`` after ``at_least`` hits.
weight (int, optional): a ``CoverCheck`` weight (by default ``1``).
at_least (int, optional): the number of hits of the ``f_pass`` function
to consider a particular ``CoverCheck`` as covered.
Example:
>>> @coverage.CoverCheck(
... name = "top.parent.check",
... f_fail = lambda x : x == 0,
... f_pass = lambda x : x < 5)
>>> def decorated_fun(self, arg):
>>> # CoverCheck is 100% covered when (arg < 5) and never (arg == 0) was
>>> # sampled. CoverCheck is set to 0 unconditionally when at least once
>>> # (arg == 0) was sampled.
... ...
"""
# conditional Object creation, only if name not already registered
def __new__(cls, name, f_fail, f_pass=None, weight=1, at_least=1):
if name in coverage_db:
return coverage_db[name]
else:
return super(CoverCheck, cls).__new__(CoverCheck)
def __init__(self, name, f_fail, f_pass=None, weight=1, at_least=1):
if not name in coverage_db:
CoverItem.__init__(self, name)
if self._parent is None:
raise Exception("CoverCheck must have a parent \
(parent.CoverCheck)")
self._weight = weight
self._at_least = at_least
self._f_pass = f_pass
self._f_fail = f_fail
self._size = weight
self._hits = dict.fromkeys(["PASS", "FAIL"], 0)
# determines whether decorated a bound method
self._decorates_method = None
# determines whether pass function is a bound method
self._f_pass_is_method = None
# determines whether fail function is a bound method
self._f_fail_is_method = None
self._parent._update_size(self._size)
def __call__(self, f):
@wraps(f)
def _wrapped_function(*cb_args, **cb_kwargs):
if len(cb_kwargs) > 0:
raise Exception("Use of keyword args in sampling function call is not supported.")
# if pass function not defined always return True
if self._f_pass is None:
def dummy_f(*cb_args):
return True
self._f_pass = dummy_f
# for the first time only check if decorates method in the class
if self._decorates_method is None:
self._decorates_method = False
for x in inspect.getmembers(cb_args[0]):
if '__func__' in dir(x[1]):
# compare decorated function name with class functions
self._decorates_method = f.__name__ == x[
1].__func__.__name__
if self._decorates_method:
break
# for the first time only check if a pass/fail function is a method
if self._f_pass_is_method is None and self._f_pass:
self._f_pass_is_method = "self" in inspect.signature(
self._f_pass).parameters
if self._f_fail_is_method is None:
self._f_fail_is_method = "self" in inspect.signature(
self._f_fail).parameters
current_coverage = self.coverage
# may be False (failed), True (passed) or None (undetermined)
passed = None
# if function is bound then remove "self" from the arguments list
if self._decorates_method ^ self._f_pass_is_method:
passed = True if self._f_pass(*cb_args[1:]) else None
else:
passed = True if self._f_pass(*cb_args) else None
if self._decorates_method ^ self._f_fail_is_method:
passed = False if self._f_fail(*cb_args[1:]) else passed
else:
passed = False if self._f_fail(*cb_args) else passed
if passed:
self._hits["PASS"] += 1
elif passed is not None:
self._hits["FAIL"] += 1
if passed is not None:
# notify parent about new coverage level
self._parent._update_coverage(self.coverage - current_coverage)
# check threshold callbacks
for ii in self._threshold_callbacks:
if (ii > 100 * current_coverage / self.size
and ii <= 100 * self.coverage / self.size):
self._threshold_callbacks[ii]()
# check bins callbacks
if "PASS" in self._bins_callbacks and passed:
self._bins_callbacks["PASS"]()
elif "FAIL" in self._bins_callbacks and not passed:
self._bins_callbacks["FAIL"]()
return f(*cb_args, **cb_kwargs)
return _wrapped_function
@property
def coverage(self):
coverage = 0
if self._hits["FAIL"] == 0 and self._hits["PASS"] >= self._at_least:
coverage = self._weight
return coverage
@property
def detailed_coverage(self):
return self._hits
[docs]
def coverage_section(*coverItems):
"""Combine multiple coverage items into a single decorator.
Args:
*coverItems ((multiple) :class:`CoverItem`): coverage primitives to be
combined.
Example:
>>> my_coverage = coverage.coverage_section(
... coverage.CoverPoint("x", ...),
... coverage.CoverPoint("y", ...),
... coverage.CoverCross("z", ...),
... ...
... )
>>>
>>> @my_coverage
>>> def decorated_fun(self, arg):
... ...
"""
def _nested(*decorators):
def _decorator(f):
for dec in reversed(*decorators):
f = dec(f)
return f
return _decorator
return _nested(coverItems)
# XML pretty print format - ElementTree lib extension
def _indent(elem, level=0):
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_indent(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
[docs]
def merge_coverage(logger, merged_file_name, *files):
""" Function used for merging coverage metrics in XML and YAML format.
Args:
logger (func): a logger function
merged_file_name (str): output filename
*files ((multiple) str): comma separated filenames to merge coverage from
Example:
>>> merge_coverage('merged.xml', 'one.xml', 'other.xml') # merge one and other
"""
from xml.etree import ElementTree as et
import yaml
l = len(files)
if l == 0:
raise ValueError('Coverage merger got no files to merge')
def try_to_parse(parser_func, f, on_error):
try:
parser_func(f)
return True
except on_error:
return False
def is_xml(f):
return try_to_parse(et.parse, f, et.ParseError)
def is_yaml(f):
return try_to_parse(yaml.safe_load, f, yaml.YAMLError)
if is_xml(files[0]):
filetype = 'xml'
dbs = [et.parse(f).getroot() for f in files]
logger(f'XML fileformat detected')
elif is_yaml(files[0]):
filetype = 'yaml'
def load_yaml(filename, logger):
with open(filename, 'r') as stream:
try:
yaml_parsed = yaml.safe_load(stream)
except yaml.YAMLError as exc:
logger(exc)
return yaml_parsed
dbs = [load_yaml(f, logger) for f in files]
logger(f'YAML fileformat detected')
else:
raise ValueError('Coverage merger: unrecognized file format, provide yaml or xml')
merged_db = dbs[0]
def merge():
for db in dbs[1:]:
merge_element(db)
logger(f'Merged {l} {"file" if l==1 else "files"}')
if filetype == 'xml':
_indent(merged_db)
et.ElementTree(merged_db).write(merged_file_name)
else:
with open(merged_file_name, 'w') as outfile:
yaml.dump(merged_db, outfile, default_flow_style=False)
logger(f'Saving coverage database as {merged_file_name}')
def merge_element(db):
if filetype == 'xml':
pre_merge_db_dict = {elem.attrib['abs_name']: elem for elem in merged_db.iter()}
name_to_elem = {el.attrib['abs_name']: el for el in merged_db.iter()}
# Elements to be added, sort descending
new_elements = [elem for elem in db.iter()
if elem.attrib['abs_name'] not in name_to_elem.keys()]
new_elements.sort(key=lambda _: _.attrib['abs_name'].count('.'))
# Bins that will be updated
items_to_update = [elem for elem in db.iter() if 'bin' in elem.tag
and elem not in new_elements]
else:
pre_merge_db_keys = list(merged_db.keys())
new_elements = [elem_key for elem_key in db if elem_key not in merged_db]
# Elements with bins that will be updated
items_to_update = [elem_key for elem_key in db
if 'bins:_hits' in list(db[elem_key].keys())
and elem_key not in new_elements]
def get_parent_name(abs_name):
return '.'.join(abs_name.split('.')[:-1])
if filetype == 'xml':
def update_parent(name, bin_update=False, new_element_update=False,
coverage_upd=0, size_upd=0,):
parent_name = get_parent_name(name)
if parent_name == '':
return
else:
if new_element_update:
coverage_upd = int(
name_to_elem[name].attrib['coverage'])
size_upd = int(
name_to_elem[name].attrib['size'])
elif bin_update:
coverage_upd = int(
name_to_elem[parent_name].attrib['weight'])
size_upd = 0
# Update current parent
name_to_elem[parent_name].attrib['coverage'] = str(int(
name_to_elem[parent_name].attrib['coverage'])
+ coverage_upd)
name_to_elem[parent_name].attrib['size'] = str(int(
name_to_elem[parent_name].attrib['size'])+size_upd)
name_to_elem[parent_name].attrib['cover_percentage'] = str(
round((int(name_to_elem[parent_name].attrib['coverage'])
*100/int(name_to_elem[parent_name].attrib['size'])), 2))
# Recursively update parents
update_parent(parent_name, False, False, coverage_upd,
size_upd)
else:
def update_parent(name, coverage_upd, size_upd=0):
parent = get_parent_name(name)
if parent == '':
return
else:
coverage = merged_db[parent]['coverage']
size = merged_db[parent]['size']
merged_db[parent]['coverage'] = coverage + coverage_upd
if size_upd != 0:
merged_db[parent]['size'] = size + size_upd
merged_db[parent]['cover_percentage'] = round(merged_db[parent]['coverage']*100/merged_db[parent]['size'], 2)
# Update up to the root
update_parent(parent, coverage_upd, size_upd)
for elem in new_elements:
# Update parents only once per new cg/cp
if filetype == 'xml':
abs_name = elem.attrib['abs_name']
parent_name = get_parent_name(abs_name)
name_to_elem[abs_name] = et.SubElement(
name_to_elem[parent_name], elem.tag, attrib=elem.attrib)
if parent_name in pre_merge_db_dict:
update_parent(name=abs_name, bin_update=False,
new_element_update=True)
else:
parent_name = get_parent_name(elem)
if elem not in merged_db.keys():
merged_db[elem] = db[elem]
if parent_name in pre_merge_db_keys:
update_parent(elem, db[elem]['coverage'], db[elem]['size'])
# Update cps with bins / bins from the new db
for elem in items_to_update:
if filetype == 'xml':
hits = int(elem.attrib['hits'])
if hits > 0:
abs_name = elem.attrib['abs_name']
hits_orig = int(name_to_elem[abs_name].attrib['hits'])
# Update the bin value
name_to_elem[abs_name].attrib['hits'] = str(hits+hits_orig)
# Check if upstream needs updating
parent_name = get_parent_name(abs_name)
parent_hits_threshold = int(
name_to_elem[parent_name].attrib['at_least'])
if (hits_orig < parent_hits_threshold
and hits_orig+hits >= parent_hits_threshold):
update_parent(name=abs_name, bin_update=True,
new_element_update=False)
else:
new_hits_cnt = 0
weight = merged_db[elem]['weight']
at_least = merged_db[elem]['at_least']
for bin_name, hits in db[elem]['bins:_hits'].items():
hits_orig = merged_db[elem]['bins:_hits'][bin_name]
if (hits_orig < at_least and hits_orig+hits >= at_least):
new_hits_cnt += 1
merged_db[elem]['bins:_hits'][bin_name] += hits
if new_hits_cnt > 0:
coverage_upd = weight*new_hits_cnt
merged_db[elem]['coverage'] = merged_db[elem]['coverage']+coverage_upd
merged_db[elem]['cover_percentage'] = round(
merged_db[elem]['coverage']*100/merged_db[elem]['size'], 2)
update_parent(elem, coverage_upd) # update cover recursively
merge()
# deprecated
[docs]
def reportCoverage(logger, bins=False):
""".. deprecated:: 1.0"""
warnings.warn(
"Function reportCoverage() is deprecated, use "
+ "coverage_db.report_coverage() instead"
)
coverage_db.report_coverage(logger, bins)
[docs]
def coverageSection(*coverItems):
""".. deprecated:: 1.0"""
warnings.warn(
"Function coverageSection() is deprecated, use coverage_section() instead"
)
return coverage_section(*coverItems)