Source code for cocotb_coverage.coverage

# 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)