Coverage for pyEDAA/Reports/CodeCoverage/UCIS/Branches.py: 79%
148 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32The branch coverage of the UCIS XML interchange format: an instance's ``<branchCoverage>``, its branching statements
33and their branches.
34"""
35from __future__ import annotations
37from typing import TYPE_CHECKING, Generator, Generic, Iterable
38from typing import Optional as Nullable, Self, TypeVar
40from lxml.etree import _Element
41from pyTooling.Common import getFullyQualifiedName
42from pyTooling.Decorators import export, readonly
44from pyEDAA.Reports.CodeCoverage.UCIS.Bins import Bin
45from pyEDAA.Reports.CodeCoverage.UCIS.Elements import Base, MetricCoverage, ObjectAttributesMixin, StatementID
46from pyEDAA.Reports.CodeCoverage.UCIS.Elements import UserAttribute
48if TYPE_CHECKING: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage
52__all__ = ["BranchStatementParentType"]
54# A class with a property named like a class - ``Bin`` - can't name that class in the annotation of a field: the class
55# body's namespace, where annotations are evaluated, binds the name to the property.
56_Bin = Bin
58BranchStatementParentType = TypeVar("BranchStatementParentType", bound="BranchCoverage | Branch")
59"""A type variable for the parent of a :class:`BranchStatement`: a :class:`BranchCoverage` or a :class:`Branch`."""
62@export
63class BranchCoverage(MetricCoverage):
64 """
65 A ``<branchCoverage>`` of an instance: its branching statements.
66 """
68 _statements: list[BranchStatement[BranchCoverage]] #: The branching statements.
70 def __init__(
71 self,
72 metricMode: Nullable[str] = None,
73 weight: int = 1,
74 userAttributes: Nullable[Iterable[UserAttribute]] = None,
75 *,
76 parent: Nullable[InstanceCoverage] = None
77 ) -> None:
78 """
79 Initialize the branch coverage, and append it to the branch coverages of its instance.
81 Its branching statements are added by creating them with this coverage as their parent.
83 :param metricMode: Optional, the mode in which the coverage was measured. Default: ``None``.
84 :param weight: Optional, weight of the mode in computing the coverage. Default: ``1``.
85 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
86 :param parent: Optional, the instance the coverage belongs to. Default: ``None``.
87 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
88 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
89 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
90 :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`.
91 :raises ValueError: If parameter ``weight`` is ``None``.
92 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
93 :raises ValueError: If parameter ``weight`` is negative.
94 :raises TypeError: If parameter ``parent`` isn't of type
95 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage`.
96 """
97 super().__init__(metricMode, weight, userAttributes, parent=parent)
99 self._statements = []
101 if parent is not None: 101 ↛ exitline 101 didn't return from function '__init__' because the condition on line 101 was always true
102 parent._branchCoverages.append(self)
104 @classmethod
105 def Parse(cls, element: _Element, *, parent: Nullable[InstanceCoverage] = None) -> Self:
106 """
107 Parse a branch coverage and its branching statements from its ``<branchCoverage>`` element.
109 :param element: The ``<branchCoverage>`` element.
110 :param parent: Optional, the instance the coverage belongs to. Default: ``None``.
111 :returns: The branch coverage.
112 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
113 """
114 branchCoverage = cls(
115 element.attrib.get("metricMode"),
116 int(element.attrib.get("weight", "1")),
117 cls._ParseUserAttributes(element),
118 parent=parent
119 )
121 for statementElement in element.iterfind("{*}statement"):
122 BranchStatement.Parse(statementElement, parent=branchCoverage)
124 return branchCoverage
126 def IterateStatements(self) -> Generator[BranchStatement, None, None]:
127 """
128 Iterate the branching statements, each followed by the branching statements nested in its branches.
130 :returns: A generator of the branching statements.
131 """
132 for statement in self._statements:
133 yield statement
134 yield from statement.IterateStatements()
136 @readonly
137 def Statements(self) -> list[BranchStatement[BranchCoverage]]:
138 """
139 Read-only property to access the branching statements (:attr:`_statements`).
141 :returns: The branching statements, which aren't nested in a branch, in the order the report lists them.
142 """
143 return self._statements
146@export
147class BranchStatement(Base, ObjectAttributesMixin, Generic[BranchStatementParentType]):
148 """
149 A branching statement - a ``<statement>`` of a branch coverage, or a ``<nestedBranch>`` of a branch -: where it is,
150 its kind, its expression and its branches.
151 """
153 _parent: Nullable[BranchStatementParentType] #: The branch coverage or branch the statement belongs to.
154 _id: StatementID #: Where the branching statement is.
155 _statementType: str #: Kind of the branching statement, e.g. ``if``.
156 _branchExpression: Nullable[str] #: The expression the statement branches on.
157 _branches: list[Branch] #: The branches.
159 def __init__(
160 self,
161 statementID: StatementID,
162 statementType: str,
163 branchExpression: Nullable[str] = None,
164 alias: Nullable[str] = None,
165 excluded: bool = False,
166 excludedReason: Nullable[str] = None,
167 weight: int = 1,
168 userAttributes: Nullable[Iterable[UserAttribute]] = None,
169 *,
170 parent: Nullable[BranchStatementParentType] = None
171 ) -> None:
172 """
173 Initialize the branching statement, and append it to the branching statements of its parent.
175 Its branches are added by creating them with this statement as their parent.
177 :param statementID: Where the branching statement is.
178 :param statementType: Kind of the branching statement, e.g. ``if`` or ``case``.
179 :param branchExpression: Optional, the expression the statement branches on, e.g. ``(x)``. Default: ``None``.
180 :param alias: Optional, an alias of the statement's name. Default: ``None``.
181 :param excluded: Optional, whether the statement is excluded from the coverage. Default: ``False``.
182 :param excludedReason: Optional, why the statement is excluded. Default: ``None``.
183 :param weight: Optional, weight of the statement in computing the coverage. Default: ``1``.
184 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
185 :param parent: Optional, the branch coverage or branch the statement belongs to. Default: ``None``.
186 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
187 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
188 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
189 :raises TypeError: If parameter ``alias`` isn't of type :class:`str`.
190 :raises ValueError: If parameter ``excluded`` is ``None``.
191 :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`.
192 :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`.
193 :raises ValueError: If parameter ``weight`` is ``None``.
194 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
195 :raises ValueError: If parameter ``weight`` is negative.
196 :raises ValueError: If parameter ``statementID`` is ``None``.
197 :raises TypeError: If parameter ``statementID`` isn't of type
198 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`.
199 :raises ValueError: If parameter ``statementType`` is ``None``.
200 :raises TypeError: If parameter ``statementType`` isn't of type :class:`str`.
201 :raises TypeError: If parameter ``branchExpression`` isn't of type :class:`str`.
202 :raises TypeError: If parameter ``parent`` isn't of type :class:`BranchCoverage` or :class:`Branch`.
203 """
204 super().__init__(userAttributes)
205 ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight)
207 if statementID is None: 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true
208 raise ValueError("Parameter 'statementID' is None.")
209 elif not isinstance(statementID, StatementID): 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true
210 ex = TypeError("Parameter 'statementID' is not of type 'StatementID'.")
211 ex.add_note(f"Got type '{getFullyQualifiedName(statementID)}'.")
212 raise ex
214 if statementType is None: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 raise ValueError("Parameter 'statementType' is None.")
216 elif not isinstance(statementType, str): 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true
217 ex = TypeError("Parameter 'statementType' is not of type 'str'.")
218 ex.add_note(f"Got type '{getFullyQualifiedName(statementType)}'.")
219 raise ex
221 if branchExpression is not None and not isinstance(branchExpression, str): 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 ex = TypeError("Parameter 'branchExpression' is not of type 'str'.")
223 ex.add_note(f"Got type '{getFullyQualifiedName(branchExpression)}'.")
224 raise ex
226 if parent is not None and not isinstance(parent, (BranchCoverage, Branch)):
227 ex = TypeError("Parameter 'parent' is not of type 'BranchCoverage' or 'Branch'.")
228 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
229 raise ex
231 self._parent = parent
232 self._id = statementID
233 self._statementType = statementType
234 self._branchExpression = branchExpression
235 self._branches = []
237 if parent is not None: 237 ↛ exitline 237 didn't return from function '__init__' because the condition on line 237 was always true
238 parent._statements.append(self)
240 @classmethod
241 def Parse(cls, element: _Element, *, parent: Nullable[BranchStatementParentType] = None) -> Self:
242 """
243 Parse a branching statement and its branches from its ``<statement>`` or ``<nestedBranch>`` element.
245 :param element: The ``<statement>`` or ``<nestedBranch>`` element.
246 :param parent: Optional, the branch coverage or branch the statement belongs to. Default: ``None``.
247 :returns: The branching statement.
248 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
249 """
250 statement = cls(
251 StatementID.Parse(element.find("{*}id")),
252 element.attrib["statementType"],
253 element.attrib.get("branchExpr"),
254 element.attrib.get("alias"),
255 cls._ParseBoolean(element.attrib.get("excluded", "false")),
256 element.attrib.get("excludedReason"),
257 int(element.attrib.get("weight", "1")),
258 cls._ParseUserAttributes(element),
259 parent=parent
260 )
262 for branchElement in element.iterfind("{*}branch"):
263 Branch.Parse(branchElement, parent=statement)
265 return statement
267 def IterateStatements(self) -> Generator[BranchStatement[Branch], None, None]:
268 """
269 Iterate the branching statements nested in this statement's branches, each followed by those nested in it.
271 :returns: A generator of the branching statements.
272 """
273 for branch in self._branches:
274 for statement in branch._statements:
275 yield statement
276 yield from statement.IterateStatements()
278 @readonly
279 def Parent(self) -> Nullable[BranchStatementParentType]:
280 """
281 Read-only property to access the branch coverage or branch the statement belongs to (:attr:`_parent`).
283 :returns: The :class:`BranchCoverage` or :class:`Branch`; ``None``, if the statement belongs to none.
284 """
285 return self._parent
287 @readonly
288 def ID(self) -> StatementID:
289 """
290 Read-only property to access where the branching statement is (:attr:`_id`).
292 :returns: The source statement identifier.
293 """
294 return self._id
296 @readonly
297 def StatementType(self) -> str:
298 """
299 Read-only property to access the kind of the branching statement (:attr:`_statementType`).
301 :returns: The kind, e.g. ``if`` or ``case``.
302 """
303 return self._statementType
305 @readonly
306 def BranchExpression(self) -> Nullable[str]:
307 """
308 Read-only property to access the expression the statement branches on (:attr:`_branchExpression`).
310 :returns: The expression; ``None``, if the report states none.
311 """
312 return self._branchExpression
314 @readonly
315 def Branches(self) -> list[Branch]:
316 """
317 Read-only property to access the branches (:attr:`_branches`).
319 :returns: The branches, in the order the report lists them.
320 """
321 return self._branches
324@export
325class Branch(Base):
326 """
327 A ``<branch>`` of a branching statement: where it is, its bin - how often it was taken - and the branching
328 statements nested in it.
329 """
331 _parent: Nullable[BranchStatement] #: The branching statement the branch belongs to.
332 _id: StatementID #: Where the branch is.
333 _bin: _Bin #: How often the branch was taken.
334 _statements: list[BranchStatement[Branch]] #: The branching statements nested in the branch.
336 def __init__(
337 self,
338 branchID: StatementID,
339 coverBin: Bin,
340 userAttributes: Nullable[Iterable[UserAttribute]] = None,
341 *,
342 parent: Nullable[BranchStatement] = None
343 ) -> None:
344 """
345 Initialize the branch, become its bin's parent, and append it to the branches of its branching statement.
347 The branching statements nested in it are added by creating them with this branch as their parent.
349 :param branchID: Where the branch is.
350 :param coverBin: How often the branch was taken.
351 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
352 :param parent: Optional, the branching statement the branch belongs to. Default: ``None``.
353 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
354 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
355 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
356 :raises ValueError: If parameter ``branchID`` is ``None``.
357 :raises TypeError: If parameter ``branchID`` isn't of type
358 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`.
359 :raises ValueError: If parameter ``coverBin`` is ``None``.
360 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`.
361 :raises TypeError: If parameter ``parent`` isn't of type :class:`BranchStatement`.
362 """
363 super().__init__(userAttributes)
365 if branchID is None: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true
366 raise ValueError("Parameter 'branchID' is None.")
367 elif not isinstance(branchID, StatementID): 367 ↛ 368line 367 didn't jump to line 368 because the condition on line 367 was never true
368 ex = TypeError("Parameter 'branchID' is not of type 'StatementID'.")
369 ex.add_note(f"Got type '{getFullyQualifiedName(branchID)}'.")
370 raise ex
372 if coverBin is None: 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true
373 raise ValueError("Parameter 'coverBin' is None.")
374 elif not isinstance(coverBin, Bin): 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true
375 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.")
376 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
377 raise ex
379 if parent is not None and not isinstance(parent, BranchStatement): 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 ex = TypeError("Parameter 'parent' is not of type 'BranchStatement'.")
381 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
382 raise ex
384 self._parent = parent
385 self._id = branchID
386 self._bin = coverBin
387 self._statements = []
388 coverBin._parent = self
390 if parent is not None: 390 ↛ exitline 390 didn't return from function '__init__' because the condition on line 390 was always true
391 parent._branches.append(self)
393 @classmethod
394 def Parse(cls, element: _Element, *, parent: Nullable[BranchStatement] = None) -> Self:
395 """
396 Parse a branch, its bin and the branching statements nested in it from its ``<branch>`` element.
398 :param element: The ``<branch>`` element.
399 :param parent: Optional, the branching statement the branch belongs to. Default: ``None``.
400 :returns: The branch.
401 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
402 """
403 branch = cls(
404 StatementID.Parse(element.find("{*}id")),
405 Bin.Parse(element.find("{*}branchBin")),
406 cls._ParseUserAttributes(element),
407 parent=parent
408 )
410 for statementElement in element.iterfind("{*}nestedBranch"): 410 ↛ 411line 410 didn't jump to line 411 because the loop on line 410 never started
411 BranchStatement.Parse(statementElement, parent=branch)
413 return branch
415 @readonly
416 def Parent(self) -> Nullable[BranchStatement]:
417 """
418 Read-only property to access the branching statement the branch belongs to (:attr:`_parent`).
420 :returns: The branching statement; ``None``, if the branch belongs to none.
421 """
422 return self._parent
424 @readonly
425 def ID(self) -> StatementID:
426 """
427 Read-only property to access where the branch is (:attr:`_id`).
429 :returns: The source statement identifier.
430 """
431 return self._id
433 @readonly
434 def Bin(self) -> Bin:
435 """
436 Read-only property to access the branch's bin (:attr:`_bin`).
438 :returns: The bin, stating how often the branch was taken.
439 """
440 return self._bin
442 @readonly
443 def Statements(self) -> list[BranchStatement[Branch]]:
444 """
445 Read-only property to access the branching statements nested in the branch (:attr:`_statements`).
447 :returns: The branching statements, in the order the report lists them.
448 """
449 return self._statements