Coverage for pyEDAA/Reports/CodeCoverage/UCIS/Elements.py: 84%
230 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 elements of the UCIS XML interchange format, which the coverage kinds share: the user-defined attributes, the
33attributes of an object, the source statement identifiers, and the base-class of an instance's coverage of a kind.
34"""
35from __future__ import annotations
37from typing import TYPE_CHECKING, Iterable, Optional as Nullable, Self
39from lxml.etree import _Element
40from pyTooling.Common import getFullyQualifiedName, StringEnum
41from pyTooling.Decorators import export, readonly
42from pyTooling.MetaClasses import ExtendedType, abstractclass
44from pyEDAA.Reports.CodeCoverage import CodeCoverageError
46if TYPE_CHECKING: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage
50@export
51class UserAttributeType(StringEnum):
52 """
53 Type of a user-defined attribute's value, as a ``<userAttr>``'s ``type`` states.
54 """
56 Integer = "int" #: A 32-bit integer.
57 Integer64 = "int64" #: A 64-bit integer.
58 Float = "float" #: A single precision floating point number.
59 Double = "double" #: A double precision floating point number.
60 String = "str" #: A string.
61 Bits = "bits" #: A bit string, e.g. ``01101100``, its length stated by ``len``.
64@export
65class UserAttribute(metaclass=ExtendedType, slots=True):
66 """
67 A ``<userAttr>`` of an element: a key, the type of its value, the value and - for a bit string - its length.
69 It is a value, as a path is: it names no parent.
70 """
72 _key: str #: Key of the attribute.
73 _type: UserAttributeType #: Type of the attribute's value.
74 _value: Nullable[int | float | str] #: The value, of the type :attr:`_type` names.
75 _length: Nullable[int] #: Length of a bit string.
77 def __init__(
78 self,
79 key: str,
80 attributeType: UserAttributeType,
81 value: Nullable[int | float | str] = None,
82 length: Nullable[int] = None
83 ) -> None:
84 """
85 Initialize the user-defined attribute.
87 :param key: Key of the attribute, e.g. ``optimization_level``.
88 :param attributeType: Type of the attribute's value.
89 :param value: Optional, the value: :class:`int` for ``int`` and ``int64``, :class:`float` for ``float`` and
90 ``double``, :class:`str` for ``str`` and ``bits``; ``None``, if the report states none.
91 Default: ``None``.
92 :param length: Optional, length of a bit string. Default: ``None``.
93 :raises ValueError: If parameter ``key`` is ``None``.
94 :raises TypeError: If parameter ``key`` isn't of type :class:`str`.
95 :raises ValueError: If parameter ``attributeType`` is ``None``.
96 :raises TypeError: If parameter ``attributeType`` isn't of type :class:`UserAttributeType`.
97 :raises TypeError: If parameter ``value`` isn't of the type parameter ``attributeType`` names.
98 :raises TypeError: If parameter ``length`` isn't of type :class:`int`.
99 """
100 if key is None: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 raise ValueError("Parameter 'key' is None.")
102 elif not isinstance(key, str): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 ex = TypeError("Parameter 'key' is not of type 'str'.")
104 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
105 raise ex
107 if attributeType is None: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 raise ValueError("Parameter 'attributeType' is None.")
109 elif not isinstance(attributeType, UserAttributeType):
110 ex = TypeError("Parameter 'attributeType' is not of type 'UserAttributeType'.")
111 ex.add_note(f"Got type '{getFullyQualifiedName(attributeType)}'.")
112 raise ex
114 if attributeType in (UserAttributeType.Integer, UserAttributeType.Integer64):
115 valueType = int
116 elif attributeType in (UserAttributeType.Float, UserAttributeType.Double):
117 valueType = float
118 else:
119 valueType = str
121 if value is not None and (not isinstance(value, valueType) or isinstance(value, bool)):
122 ex = TypeError(f"Parameter 'value' is not of type '{valueType.__name__}'.")
123 ex.add_note(f"Got type '{getFullyQualifiedName(value)}' for attribute type '{attributeType}'.")
124 raise ex
126 if length is not None and not isinstance(length, int): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 ex = TypeError("Parameter 'length' is not of type 'int'.")
128 ex.add_note(f"Got type '{getFullyQualifiedName(length)}'.")
129 raise ex
131 self._key = key
132 self._type = attributeType
133 self._value = value
134 self._length = length
136 @classmethod
137 def Parse(cls, element: _Element) -> Self:
138 """
139 Parse a user-defined attribute from its ``<userAttr>`` element.
141 The value is the element's text, surrounding whitespace removed; an empty text is no value.
143 :param element: The ``<userAttr>`` element.
144 :returns: The user-defined attribute.
145 :raises CodeCoverageError: If the value isn't of the type the attribute states.
146 """
147 key = element.attrib["key"]
148 attributeType = UserAttributeType(element.attrib["type"])
149 text = "" if element.text is None else element.text.strip()
150 length = element.attrib.get("len")
152 value: Nullable[int | float | str] = None
153 if text != "":
154 try:
155 if attributeType in (UserAttributeType.Integer, UserAttributeType.Integer64):
156 value = int(text)
157 elif attributeType in (UserAttributeType.Float, UserAttributeType.Double):
158 value = float(text)
159 else:
160 value = text
161 except ValueError as cause:
162 ex = CodeCoverageError(f"UCIS user attribute '{key}' states a value not of type '{attributeType}'.")
163 ex.add_note(f"Got value '{text}'.")
164 raise ex from cause
166 return cls(key, attributeType, value, None if length is None else int(length))
168 @readonly
169 def Key(self) -> str:
170 """
171 Read-only property to access the key of the attribute (:attr:`_key`).
173 :returns: The key.
174 """
175 return self._key
177 @readonly
178 def Type(self) -> UserAttributeType:
179 """
180 Read-only property to access the type of the attribute's value (:attr:`_type`).
182 :returns: The type.
183 """
184 return self._type
186 @readonly
187 def Value(self) -> Nullable[int | float | str]:
188 """
189 Read-only property to access the value (:attr:`_value`).
191 :returns: The value, of the type :attr:`Type` names; ``None``, if the report states none.
192 """
193 return self._value
195 @readonly
196 def Length(self) -> Nullable[int]:
197 """
198 Read-only property to access the length of a bit string (:attr:`_length`).
200 :returns: The length; ``None``, if the report states none.
201 """
202 return self._length
205@export
206@abstractclass
207class Base(metaclass=ExtendedType, slots=True):
208 """
209 Base-class of the elements stating user-defined attributes.
210 """
212 _userAttributes: list[UserAttribute] #: The user-defined attributes.
214 def __init__(self, userAttributes: Nullable[Iterable[UserAttribute]] = None) -> None:
215 """
216 Initialize the user-defined attributes.
218 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
219 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
220 :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`UserAttribute`.
221 """
222 self._userAttributes = []
224 if userAttributes is not None:
225 if not isinstance(userAttributes, Iterable): 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true
226 ex = TypeError("Parameter 'userAttributes' is not iterable.")
227 ex.add_note(f"Got type '{getFullyQualifiedName(userAttributes)}'.")
228 raise ex
230 for userAttribute in userAttributes:
231 if not isinstance(userAttribute, UserAttribute):
232 ex = TypeError("Parameter 'userAttributes' contains an element not of type 'UserAttribute'.")
233 ex.add_note(f"Got type '{getFullyQualifiedName(userAttribute)}'.")
234 raise ex
236 self._userAttributes.append(userAttribute)
238 @staticmethod
239 def _ParseUserAttributes(element: _Element) -> list[UserAttribute]:
240 """
241 Parse the user-defined attributes of an element, its ``<userAttr>`` elements.
243 :param element: The element.
244 :returns: The user-defined attributes, in the order the report lists them.
245 :raises CodeCoverageError: If a value isn't of the type its attribute states.
246 """
247 return [UserAttribute.Parse(attributeElement) for attributeElement in element.iterfind("{*}userAttr")]
249 @staticmethod
250 def _ParseBoolean(value: str) -> bool:
251 """
252 Parse an attribute's value of XML Schema type ``boolean``.
254 :param value: The value: ``true``, ``1``, ``false`` or ``0``.
255 :returns: The boolean.
256 """
257 return value in ("true", "1")
259 @readonly
260 def UserAttributes(self) -> list[UserAttribute]:
261 """
262 Read-only property to access the user-defined attributes (:attr:`_userAttributes`).
264 :returns: The user-defined attributes, in the order the report lists them.
265 """
266 return self._userAttributes
269@export
270class ObjectAttributesMixin(metaclass=ExtendedType, mixin=True):
271 """
272 A mixin-class adding the attributes of an object: its alias, whether it is excluded and why, and its weight.
273 """
275 _alias: Nullable[str] #: An alias of the object's name.
276 _isExcluded: bool #: Whether the object is excluded from the coverage.
277 _excludedReason: Nullable[str] #: Why the object is excluded.
278 _weight: int #: Weight of the object in computing the coverage.
280 def __init__(
281 self,
282 alias: Nullable[str] = None,
283 excluded: bool = False,
284 excludedReason: Nullable[str] = None,
285 weight: int = 1
286 ) -> None:
287 """
288 Initialize the attributes of the object.
290 :param alias: Optional, an alias of the object's name. Default: ``None``.
291 :param excluded: Optional, whether the object is excluded from the coverage. Default: ``False``.
292 :param excludedReason: Optional, why the object is excluded. Default: ``None``.
293 :param weight: Optional, weight of the object in computing the coverage. Default: ``1``.
294 :raises TypeError: If parameter ``alias`` isn't of type :class:`str`.
295 :raises ValueError: If parameter ``excluded`` is ``None``.
296 :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`.
297 :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`.
298 :raises ValueError: If parameter ``weight`` is ``None``.
299 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
300 :raises ValueError: If parameter ``weight`` is negative.
301 """
302 if alias is not None and not isinstance(alias, str): 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true
303 ex = TypeError("Parameter 'alias' is not of type 'str'.")
304 ex.add_note(f"Got type '{getFullyQualifiedName(alias)}'.")
305 raise ex
307 if excluded is None: 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true
308 raise ValueError("Parameter 'excluded' is None.")
309 elif not isinstance(excluded, bool):
310 ex = TypeError("Parameter 'excluded' is not of type 'bool'.")
311 ex.add_note(f"Got type '{getFullyQualifiedName(excluded)}'.")
312 raise ex
314 if excludedReason is not None and not isinstance(excludedReason, str): 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315 ex = TypeError("Parameter 'excludedReason' is not of type 'str'.")
316 ex.add_note(f"Got type '{getFullyQualifiedName(excludedReason)}'.")
317 raise ex
319 if weight is None: 319 ↛ 320line 319 didn't jump to line 320 because the condition on line 319 was never true
320 raise ValueError("Parameter 'weight' is None.")
321 elif not isinstance(weight, int) or isinstance(weight, bool): 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true
322 ex = TypeError("Parameter 'weight' is not of type 'int'.")
323 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.")
324 raise ex
325 elif weight < 0:
326 ex = ValueError("Parameter 'weight' is negative.")
327 ex.add_note(f"Got value '{weight}'.")
328 raise ex
330 self._alias = alias
331 self._isExcluded = excluded
332 self._excludedReason = excludedReason
333 self._weight = weight
335 @readonly
336 def Alias(self) -> Nullable[str]:
337 """
338 Read-only property to access the alias of the object's name (:attr:`_alias`).
340 :returns: The alias; ``None``, if the report states none.
341 """
342 return self._alias
344 @readonly
345 def IsExcluded(self) -> bool:
346 """
347 Read-only property to access whether the object is excluded from the coverage (:attr:`_isExcluded`).
349 :returns: ``True``, if excluded.
350 """
351 return self._isExcluded
353 @readonly
354 def ExcludedReason(self) -> Nullable[str]:
355 """
356 Read-only property to access why the object is excluded (:attr:`_excludedReason`).
358 :returns: The reason; ``None``, if the report states none.
359 """
360 return self._excludedReason
362 @readonly
363 def Weight(self) -> int:
364 """
365 Read-only property to access the weight of the object in computing the coverage (:attr:`_weight`).
367 :returns: The weight; ``1``, if the report states none.
368 """
369 return self._weight
372@export
373class StatementID(metaclass=ExtendedType, slots=True):
374 """
375 A source statement identifier: the source file, the line and the index of the statement in the line.
377 It is stated e.g. by an ``<id>`` element. It is a value, as a path is: it names no parent.
378 """
380 _fileID: int #: ID of the source file.
381 _lineNumber: int #: Line number, counted from 1.
382 _inlineCount: int #: Index of the statement in its line, counted from 1.
384 def __init__(self, fileID: int, lineNumber: int, inlineCount: int) -> None:
385 """
386 Initialize the source statement identifier.
388 :param fileID: ID of the source file.
389 :param lineNumber: Line number, counted from 1.
390 :param inlineCount: Index of the statement in its line, counted from 1.
391 :raises ValueError: If parameter ``fileID`` is ``None``.
392 :raises TypeError: If parameter ``fileID`` isn't of type :class:`int`.
393 :raises ValueError: If parameter ``fileID`` is less than 1.
394 :raises ValueError: If parameter ``lineNumber`` is ``None``.
395 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
396 :raises ValueError: If parameter ``lineNumber`` is less than 1.
397 :raises ValueError: If parameter ``inlineCount`` is ``None``.
398 :raises TypeError: If parameter ``inlineCount`` isn't of type :class:`int`.
399 :raises ValueError: If parameter ``inlineCount`` is less than 1.
400 """
401 for name, value in (
402 ("fileID", fileID),
403 ("lineNumber", lineNumber),
404 ("inlineCount", inlineCount)
405 ):
406 if value is None:
407 raise ValueError(f"Parameter '{name}' is None.")
408 elif not isinstance(value, int) or isinstance(value, bool):
409 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
410 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
411 raise ex
412 elif value < 1:
413 ex = ValueError(f"Parameter '{name}' is less than 1.")
414 ex.add_note(f"Got value '{value}'.")
415 raise ex
417 self._fileID = fileID
418 self._lineNumber = lineNumber
419 self._inlineCount = inlineCount
421 @classmethod
422 def Parse(cls, element: _Element) -> Self:
423 """
424 Parse a source statement identifier from its element.
426 The element is e.g. an ``<id>``.
428 :param element: The identifier's element.
429 :returns: The source statement identifier.
430 """
431 return cls(int(element.attrib["file"]), int(element.attrib["line"]), int(element.attrib["inlineCount"]))
433 @readonly
434 def FileID(self) -> int:
435 """
436 Read-only property to access the ID of the source file (:attr:`_fileID`).
438 :returns: The ID, as the report's source file states it.
439 """
440 return self._fileID
442 @readonly
443 def LineNumber(self) -> int:
444 """
445 Read-only property to access the line number (:attr:`_lineNumber`).
447 :returns: The line number, counted from 1.
448 """
449 return self._lineNumber
451 @readonly
452 def InlineCount(self) -> int:
453 """
454 Read-only property to access the index of the statement in its line (:attr:`_inlineCount`).
456 :returns: The index, counted from 1.
457 """
458 return self._inlineCount
460 def __repr__(self) -> str:
461 """
462 Return a representation of the identifier for debugging.
464 The first statement in line 12 of file 1 reads ``<StatementID 1:12:1>``.
466 :returns: The file ID, the line number and the index in the line.
467 """
468 return f"<StatementID {self._fileID}:{self._lineNumber}:{self._inlineCount}>"
471@export
472@abstractclass
473class MetricCoverage(Base):
474 """
475 Base-class of the coverage of one kind of an instance: its metric mode and weight.
477 A ``<blockCoverage>`` is such a coverage. An instance states a kind's coverage once per metric mode.
478 """
480 _parent: Nullable[InstanceCoverage] #: The instance the coverage belongs to.
481 _metricMode: Nullable[str] #: The mode in which the coverage was measured.
482 _weight: int #: Weight of the mode in computing the coverage.
484 def __init__(
485 self,
486 metricMode: Nullable[str] = None,
487 weight: int = 1,
488 userAttributes: Nullable[Iterable[UserAttribute]] = None,
489 *,
490 parent: Nullable[InstanceCoverage] = None
491 ) -> None:
492 """
493 Initialize the metric mode, the weight and the parent of the coverage.
495 A derived class adds the coverage to its instance.
497 :param metricMode: Optional, the mode in which the coverage was measured, e.g. ``UCIS:BITWISE_FLAT``.
498 Default: ``None``.
499 :param weight: Optional, weight of the mode in computing the coverage. Default: ``1``.
500 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
501 :param parent: Optional, the instance the coverage belongs to. Default: ``None``.
502 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
503 :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`UserAttribute`.
504 :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`.
505 :raises ValueError: If parameter ``weight`` is ``None``.
506 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
507 :raises ValueError: If parameter ``weight`` is negative.
508 :raises TypeError: If parameter ``parent`` isn't of type
509 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage`.
510 """
511 from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage
513 super().__init__(userAttributes)
515 if metricMode is not None and not isinstance(metricMode, str): 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true
516 ex = TypeError("Parameter 'metricMode' is not of type 'str'.")
517 ex.add_note(f"Got type '{getFullyQualifiedName(metricMode)}'.")
518 raise ex
520 if weight is None: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true
521 raise ValueError("Parameter 'weight' is None.")
522 elif not isinstance(weight, int) or isinstance(weight, bool): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true
523 ex = TypeError("Parameter 'weight' is not of type 'int'.")
524 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.")
525 raise ex
526 elif weight < 0: 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 ex = ValueError("Parameter 'weight' is negative.")
528 ex.add_note(f"Got value '{weight}'.")
529 raise ex
531 if parent is not None and not isinstance(parent, InstanceCoverage):
532 ex = TypeError("Parameter 'parent' is not of type 'InstanceCoverage'.")
533 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
534 raise ex
536 self._parent = parent
537 self._metricMode = metricMode
538 self._weight = weight
540 @readonly
541 def Parent(self) -> Nullable[InstanceCoverage]:
542 """
543 Read-only property to access the instance the coverage belongs to (:attr:`_parent`).
545 :returns: The instance; ``None``, if the coverage belongs to none.
546 """
547 return self._parent
549 @readonly
550 def MetricMode(self) -> Nullable[str]:
551 """
552 Read-only property to access the mode in which the coverage was measured (:attr:`_metricMode`).
554 :returns: The mode; ``None``, if the report states none.
555 """
556 return self._metricMode
558 @readonly
559 def Weight(self) -> int:
560 """
561 Read-only property to access the weight of the mode in computing the coverage (:attr:`_weight`).
563 :returns: The weight; ``1``, if the report states none.
564 """
565 return self._weight