Source code for pyEDAA.Reports.CodeCoverage.UCIS.Blocks

# ==================================================================================================================== #
#              _____ ____    _        _      ____                       _                                              #
#  _ __  _   _| ____|  _ \  / \      / \    |  _ \ ___ _ __   ___  _ __| |_ ___                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |_) / _ \ '_ \ / _ \| '__| __/ __|                                       #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _|  _ <  __/ |_) | (_) | |  | |_\__ \                                       #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_| \_\___| .__/ \___/|_|   \__|___/                                       #
# |_|    |___/                                        |_|                                                              #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²)                                                  #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
The statement and block coverage of the UCIS XML interchange format: an instance's ``<blockCoverage>``, its statements,
blocks and processes.
"""
from __future__                                   import annotations

from typing                                       import TYPE_CHECKING, Generator, Generic, Iterable, \
                                                         Optional as Nullable, Self, TypeVar

from lxml.etree                                   import _Element
from pyTooling.Common                             import getFullyQualifiedName
from pyTooling.Decorators                         import export, readonly

from pyEDAA.Reports.CodeCoverage.UCIS.Bins        import Bin
from pyEDAA.Reports.CodeCoverage.UCIS.Elements    import Base, MetricCoverage, ObjectAttributesMixin, StatementID, \
                                                         UserAttribute

if TYPE_CHECKING:
	from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage


__all__ = ["BlockParentType"]

# A class with a property named like a class - ``Bin`` - can't name that class in the annotation of a field: the class
# body's namespace, where annotations are evaluated, binds the name to the property.
_Bin = Bin

BlockParentType = TypeVar("BlockParentType", bound="BlockCoverage | Process | Block")
"""A type variable for the parent of a :class:`Block`: a :class:`BlockCoverage`, a :class:`Process` or a
:class:`Block`."""


[docs] @export class BlockCoverage(MetricCoverage): """ A ``<blockCoverage>`` of an instance: its statements, its blocks or its processes with their blocks. The format states one of the three. """ _statements: list[Statement] #: The statements. _blocks: list[Block[BlockCoverage]] #: The blocks. _processes: list[Process] #: The processes.
[docs] def __init__( self, metricMode: Nullable[str] = None, weight: int = 1, userAttributes: Nullable[Iterable[UserAttribute]] = None, *, parent: Nullable[InstanceCoverage] = None ) -> None: """ Initialize the block coverage, and append it to the block coverages of its instance. Its statements, blocks and processes are added by creating them with this coverage as their parent. :param metricMode: Optional, the mode in which the coverage was measured. Default: ``None``. :param weight: Optional, weight of the mode in computing the coverage. Default: ``1``. :param userAttributes: Optional, the user-defined attributes. Default: ``None``. :param parent: Optional, the instance the coverage belongs to. Default: ``None``. :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`. :raises ValueError: If parameter ``weight`` is ``None``. :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. :raises ValueError: If parameter ``weight`` is negative. :raises TypeError: If parameter ``userAttributes`` isn't iterable. :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. :raises TypeError: If parameter ``parent`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage`. """ super().__init__(metricMode, weight, userAttributes, parent=parent) self._statements = [] self._blocks = [] self._processes = [] if parent is not None: parent._blockCoverages.append(self)
[docs] @classmethod def Parse(cls, element: _Element, *, parent: Nullable[InstanceCoverage] = None) -> Self: """ Parse a block coverage, its statements, blocks and processes from its ``<blockCoverage>`` element. :param element: The ``<blockCoverage>`` element. :param parent: Optional, the instance the coverage belongs to. Default: ``None``. :returns: The block coverage. :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states. """ blockCoverage = cls( element.attrib.get("metricMode"), int(element.attrib.get("weight", "1")), cls._ParseUserAttributes(element), parent=parent ) for statementElement in element.iterfind("{*}statement"): Statement.Parse(statementElement, parent=blockCoverage) for blockElement in element.iterfind("{*}block"): Block.Parse(blockElement, parent=blockCoverage) for processElement in element.iterfind("{*}process"): Process.Parse(processElement, parent=blockCoverage) return blockCoverage
[docs] def IterateBlocks(self) -> Generator[Block, None, None]: """ Iterate the blocks: those of the coverage and those of its processes, each followed by the blocks nested in it. :returns: A generator of the blocks. """ for block in self._blocks: yield block yield from block.IterateBlocks() for process in self._processes: for block in process._blocks: yield block yield from block.IterateBlocks()
@readonly def Statements(self) -> list[Statement]: """ Read-only property to access the statements (:attr:`_statements`). :returns: The statements, in the order the report lists them. """ return self._statements @readonly def Blocks(self) -> list[Block[BlockCoverage]]: """ Read-only property to access the blocks, which belong to no process (:attr:`_blocks`). :returns: The blocks, in the order the report lists them. """ return self._blocks @readonly def Processes(self) -> list[Process]: """ Read-only property to access the processes (:attr:`_processes`). :returns: The processes, in the order the report lists them. """ return self._processes
[docs] @export class Statement(Base, ObjectAttributesMixin): """ A ``<statement>`` of a block coverage: where the statement is, and its bin - how often it ran. """ _parent: Nullable[BlockCoverage] #: The block coverage the statement belongs to. _id: StatementID #: Where the statement is. _bin: _Bin #: How often the statement ran.
[docs] def __init__( self, statementID: StatementID, coverBin: Bin, alias: Nullable[str] = None, excluded: bool = False, excludedReason: Nullable[str] = None, weight: int = 1, userAttributes: Nullable[Iterable[UserAttribute]] = None, *, parent: Nullable[BlockCoverage] = None ) -> None: """ Initialize the statement, become its bin's parent, and append it to the statements of its block coverage. :param statementID: Where the statement is. :param coverBin: How often the statement ran. :param alias: Optional, an alias of the statement's name. Default: ``None``. :param excluded: Optional, whether the statement is excluded from the coverage. Default: ``False``. :param excludedReason: Optional, why the statement is excluded. Default: ``None``. :param weight: Optional, weight of the statement in computing the coverage. Default: ``1``. :param userAttributes: Optional, the user-defined attributes. Default: ``None``. :param parent: Optional, the block coverage the statement belongs to. Default: ``None``. :raises ValueError: If parameter ``statementID`` is ``None``. :raises TypeError: If parameter ``statementID`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`. :raises ValueError: If parameter ``coverBin`` is ``None``. :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`. :raises TypeError: If parameter ``alias`` isn't of type :class:`str`. :raises ValueError: If parameter ``excluded`` is ``None``. :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`. :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`. :raises ValueError: If parameter ``weight`` is ``None``. :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. :raises ValueError: If parameter ``weight`` is negative. :raises TypeError: If parameter ``userAttributes`` isn't iterable. :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. :raises TypeError: If parameter ``parent`` isn't of type :class:`BlockCoverage`. """ super().__init__(userAttributes) ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight) if statementID is None: raise ValueError("Parameter 'statementID' is None.") elif not isinstance(statementID, StatementID): ex = TypeError("Parameter 'statementID' is not of type 'StatementID'.") ex.add_note(f"Got type '{getFullyQualifiedName(statementID)}'.") raise ex if coverBin is None: raise ValueError("Parameter 'coverBin' is None.") elif not isinstance(coverBin, Bin): ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.") ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.") raise ex if parent is not None and not isinstance(parent, BlockCoverage): ex = TypeError("Parameter 'parent' is not of type 'BlockCoverage'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._parent = parent self._id = statementID self._bin = coverBin coverBin._parent = self if parent is not None: parent._statements.append(self)
[docs] @classmethod def Parse(cls, element: _Element, *, parent: Nullable[BlockCoverage] = None) -> Self: """ Parse a statement and its bin from its ``<statement>`` element. :param element: The ``<statement>`` element. :param parent: Optional, the block coverage the statement belongs to. Default: ``None``. :returns: The statement. :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states. """ return cls( StatementID.Parse(element.find("{*}id")), Bin.Parse(element.find("{*}bin")), element.attrib.get("alias"), cls._ParseBoolean(element.attrib.get("excluded", "false")), element.attrib.get("excludedReason"), int(element.attrib.get("weight", "1")), cls._ParseUserAttributes(element), parent=parent )
@readonly def Parent(self) -> Nullable[BlockCoverage]: """ Read-only property to access the block coverage the statement belongs to (:attr:`_parent`). :returns: The block coverage; ``None``, if the statement belongs to none. """ return self._parent @readonly def ID(self) -> StatementID: """ Read-only property to access where the statement is (:attr:`_id`). :returns: The source statement identifier. """ return self._id @readonly def Bin(self) -> Bin: """ Read-only property to access the statement's bin (:attr:`_bin`). :returns: The bin, stating how often the statement ran. """ return self._bin
[docs] @export class Process(Base, ObjectAttributesMixin): """ A ``<process>`` of a block coverage: its kind and its blocks. """ _parent: Nullable[BlockCoverage] #: The block coverage the process belongs to. _processType: str #: Kind of the process, e.g. ``always``. _blocks: list[Block[Process]] #: The blocks of the process.
[docs] def __init__( self, processType: str, alias: Nullable[str] = None, excluded: bool = False, excludedReason: Nullable[str] = None, weight: int = 1, userAttributes: Nullable[Iterable[UserAttribute]] = None, *, parent: Nullable[BlockCoverage] = None ) -> None: """ Initialize the process, and append it to the processes of its block coverage. Its blocks are added by creating them with this process as their parent. :param processType: Kind of the process, e.g. ``always`` or ``initial``. :param alias: Optional, an alias of the process' name. Default: ``None``. :param excluded: Optional, whether the process is excluded from the coverage. Default: ``False``. :param excludedReason: Optional, why the process is excluded. Default: ``None``. :param weight: Optional, weight of the process in computing the coverage. Default: ``1``. :param userAttributes: Optional, the user-defined attributes. Default: ``None``. :param parent: Optional, the block coverage the process belongs to. Default: ``None``. :raises ValueError: If parameter ``processType`` is ``None``. :raises TypeError: If parameter ``processType`` isn't of type :class:`str`. :raises TypeError: If parameter ``alias`` isn't of type :class:`str`. :raises ValueError: If parameter ``excluded`` is ``None``. :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`. :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`. :raises ValueError: If parameter ``weight`` is ``None``. :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. :raises ValueError: If parameter ``weight`` is negative. :raises TypeError: If parameter ``userAttributes`` isn't iterable. :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. :raises TypeError: If parameter ``parent`` isn't of type :class:`BlockCoverage`. """ super().__init__(userAttributes) ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight) if processType is None: raise ValueError("Parameter 'processType' is None.") elif not isinstance(processType, str): ex = TypeError("Parameter 'processType' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(processType)}'.") raise ex if parent is not None and not isinstance(parent, BlockCoverage): ex = TypeError("Parameter 'parent' is not of type 'BlockCoverage'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._parent = parent self._processType = processType self._blocks = [] if parent is not None: parent._processes.append(self)
[docs] @classmethod def Parse(cls, element: _Element, *, parent: Nullable[BlockCoverage] = None) -> Self: """ Parse a process and its blocks from its ``<process>`` element. :param element: The ``<process>`` element. :param parent: Optional, the block coverage the process belongs to. Default: ``None``. :returns: The process. :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states. """ process = cls( element.attrib["processType"], element.attrib.get("alias"), cls._ParseBoolean(element.attrib.get("excluded", "false")), element.attrib.get("excludedReason"), int(element.attrib.get("weight", "1")), cls._ParseUserAttributes(element), parent=parent ) for blockElement in element.iterfind("{*}block"): Block.Parse(blockElement, parent=process) return process
@readonly def Parent(self) -> Nullable[BlockCoverage]: """ Read-only property to access the block coverage the process belongs to (:attr:`_parent`). :returns: The block coverage; ``None``, if the process belongs to none. """ return self._parent @readonly def ProcessType(self) -> str: """ Read-only property to access the kind of the process (:attr:`_processType`). :returns: The kind, e.g. ``always``. """ return self._processType @readonly def Blocks(self) -> list[Block[Process]]: """ Read-only property to access the blocks of the process (:attr:`_blocks`). :returns: The blocks, in the order the report lists them. """ return self._blocks
[docs] @export class Block(Base, ObjectAttributesMixin, Generic[BlockParentType]): """ A ``<block>`` - or a ``<hierarchicalBlock>`` nested in a block -: where it is, the statements it contains, its bin - how often it ran - and the blocks nested in it. """ _parent: Nullable[BlockParentType] #: The block coverage, process or block the block belongs to. _id: StatementID #: Where the block is. _bin: _Bin #: How often the block ran. _statementIDs: list[StatementID] #: Where the statements of the block are. _parentProcess: Nullable[str] #: Kind of the process the block is in, e.g. ``always``. _blocks: list[Block[Block]] #: The blocks nested in this one.
[docs] def __init__( self, blockID: StatementID, coverBin: Bin, statementIDs: Nullable[Iterable[StatementID]] = None, parentProcess: Nullable[str] = None, alias: Nullable[str] = None, excluded: bool = False, excludedReason: Nullable[str] = None, weight: int = 1, userAttributes: Nullable[Iterable[UserAttribute]] = None, *, parent: Nullable[BlockParentType] = None ) -> None: """ Initialize the block, become its bin's parent, and append it to the blocks of its parent. The blocks nested in it are added by creating them with this block as their parent. :param blockID: Where the block is. :param coverBin: How often the block ran. :param statementIDs: Optional, where the statements of the block are. Default: ``None``. :param parentProcess: Optional, kind of the process the block is in, e.g. ``always``. Default: ``None``. :param alias: Optional, an alias of the block's name. Default: ``None``. :param excluded: Optional, whether the block is excluded from the coverage. Default: ``False``. :param excludedReason: Optional, why the block is excluded. Default: ``None``. :param weight: Optional, weight of the block in computing the coverage. Default: ``1``. :param userAttributes: Optional, the user-defined attributes. Default: ``None``. :param parent: Optional, the block coverage, process or block the block belongs to. Default: ``None``. :raises ValueError: If parameter ``blockID`` is ``None``. :raises TypeError: If parameter ``blockID`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`. :raises ValueError: If parameter ``coverBin`` is ``None``. :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`. :raises TypeError: If parameter ``statementIDs`` isn't iterable. :raises TypeError: If parameter ``statementIDs`` contains an element not of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`. :raises TypeError: If parameter ``parentProcess`` isn't of type :class:`str`. :raises TypeError: If parameter ``alias`` isn't of type :class:`str`. :raises ValueError: If parameter ``excluded`` is ``None``. :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`. :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`. :raises ValueError: If parameter ``weight`` is ``None``. :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. :raises ValueError: If parameter ``weight`` is negative. :raises TypeError: If parameter ``userAttributes`` isn't iterable. :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. :raises TypeError: If parameter ``parent`` isn't of type :class:`BlockCoverage`, :class:`Process` or :class:`Block`. """ super().__init__(userAttributes) ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight) if blockID is None: raise ValueError("Parameter 'blockID' is None.") elif not isinstance(blockID, StatementID): ex = TypeError("Parameter 'blockID' is not of type 'StatementID'.") ex.add_note(f"Got type '{getFullyQualifiedName(blockID)}'.") raise ex if coverBin is None: raise ValueError("Parameter 'coverBin' is None.") elif not isinstance(coverBin, Bin): ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.") ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.") raise ex if parentProcess is not None and not isinstance(parentProcess, str): ex = TypeError("Parameter 'parentProcess' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(parentProcess)}'.") raise ex if parent is not None and not isinstance(parent, (BlockCoverage, Process, Block)): ex = TypeError("Parameter 'parent' is not of type 'BlockCoverage', 'Process' or 'Block'.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._parent = parent self._id = blockID self._bin = coverBin self._statementIDs = statementIDList = [] self._parentProcess = parentProcess self._blocks = [] coverBin._parent = self if statementIDs is not None: if not isinstance(statementIDs, Iterable): ex = TypeError("Parameter 'statementIDs' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(statementIDs)}'.") raise ex for statementID in statementIDs: if not isinstance(statementID, StatementID): ex = TypeError("Parameter 'statementIDs' contains an element not of type 'StatementID'.") ex.add_note(f"Got type '{getFullyQualifiedName(statementID)}'.") raise ex statementIDList.append(statementID) if parent is not None: parent._blocks.append(self)
[docs] @classmethod def Parse(cls, element: _Element, *, parent: Nullable[BlockParentType] = None) -> Self: """ Parse a block, its bin and the blocks nested in it from its ``<block>`` or ``<hierarchicalBlock>`` element. :param element: The ``<block>`` or ``<hierarchicalBlock>`` element. :param parent: Optional, the block coverage, process or block the block belongs to. Default: ``None``. :returns: The block. :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states. """ block = cls( StatementID.Parse(element.find("{*}blockId")), Bin.Parse(element.find("{*}blockBin")), [StatementID.Parse(idElement) for idElement in element.iterfind("{*}statementId")], element.attrib.get("parentProcess"), element.attrib.get("alias"), cls._ParseBoolean(element.attrib.get("excluded", "false")), element.attrib.get("excludedReason"), int(element.attrib.get("weight", "1")), cls._ParseUserAttributes(element), parent=parent ) for blockElement in element.iterfind("{*}hierarchicalBlock"): Block.Parse(blockElement, parent=block) return block
[docs] def IterateBlocks(self) -> Generator[Block[Block], None, None]: """ Iterate the blocks nested in this one, each followed by the blocks nested in it. :returns: A generator of the blocks. """ for block in self._blocks: yield block yield from block.IterateBlocks()
@readonly def Parent(self) -> Nullable[BlockParentType]: """ Read-only property to access the block coverage, process or block the block belongs to (:attr:`_parent`). :returns: The :class:`BlockCoverage`, :class:`Process` or :class:`Block`; ``None``, if the block belongs to none. """ return self._parent @readonly def ID(self) -> StatementID: """ Read-only property to access where the block is (:attr:`_id`). :returns: The source statement identifier. """ return self._id @readonly def Bin(self) -> Bin: """ Read-only property to access the block's bin (:attr:`_bin`). :returns: The bin, stating how often the block ran. """ return self._bin @readonly def StatementIDs(self) -> list[StatementID]: """ Read-only property to access where the statements of the block are (:attr:`_statementIDs`). :returns: The source statement identifiers, in the order the report lists them. """ return self._statementIDs @readonly def ParentProcess(self) -> Nullable[str]: """ Read-only property to access the kind of the process the block is in (:attr:`_parentProcess`). :returns: The kind, e.g. ``always``; ``None``, if the report states none. """ return self._parentProcess @readonly def Blocks(self) -> list[Block[Block]]: """ Read-only property to access the blocks nested in this one (:attr:`_blocks`). :returns: The blocks, in the order the report lists them. """ return self._blocks