Coverage for pyEDAA/Reports/CodeCoverage/AldecUCDB.py: 89%
748 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# Artur Porebski (Aldec Inc.) #
12# Michal Pacula (Aldec Inc.) #
13# #
14# License: #
15# ==================================================================================================================== #
16# Copyright 2021-2026 Electronic Design Automation Abstraction (EDA²) #
17# #
18# Licensed under the Apache License, Version 2.0 (the "License"); #
19# you may not use this file except in compliance with the License. #
20# You may obtain a copy of the License at #
21# #
22# http://www.apache.org/licenses/LICENSE-2.0 #
23# #
24# Unless required by applicable law or agreed to in writing, software #
25# distributed under the License is distributed on an "AS IS" BASIS, #
26# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
27# See the License for the specific language governing permissions and #
28# limitations under the License. #
29# #
30# SPDX-License-Identifier: Apache-2.0 #
31# ==================================================================================================================== #
32#
33"""
34Aldec's UCDB XML code coverage format: a model of the format, read from a report and converted to the common model.
36Aldec's simulators Riviera-PRO and Active-HDL keep code coverage in a coverage database (ACDB), which the tool
37``acdb2xml`` exports as XML: ``acdb2xml -i aggregate.acdb -o ucdb.xml``. The XML names its namespace ``www.aldec.com``
38and its root ``<ux:ucdb>``; it is a dump of the database's UCIS data model - scopes, coverage items (bins) and history
39nodes -, not the XML interchange format of the UCIS standard. A report states no format version, only the tool's name
40and version, e.g. ``Riviera-PRO 2022.04``; it is validated against :file:`Aldec-UCDB.xsd` (:data:`SCHEMA`),
41reverse-engineered from reports of Riviera-PRO.
43The format's model keeps what the report states: a :class:`Document` holds its attributes, :class:`HistoryNode`
44elements - nested, a merge holds the tests it merged -, :class:`Scope` elements - nested, e.g. design units, their
45instances, processes and branching statements - with their :class:`Bin` elements, and :class:`Command` elements. Each
46element's constructor takes typed values, so the model can be built by hand: an element below the report names its
47parent with the keyword parameter ``parent`` and is added to it. Its class method ``Parse`` reads the element's XML
48element.
50* The scopes are the design units and their instances; the bins are the coverage items - e.g. statements and
51 branches - with their counts, and where they are in the sources (:class:`SourceLocation`).
52* Every element may state attributes (:class:`AttributesMixin`); the named elements derive from :class:`Base`.
53* The kinds of scopes (:class:`ScopeType`), bins (:class:`CoverType`) and source languages (:class:`Language`) are
54 named as the UCIS standard names them in its C API, without the prefix ``UCIS_``, e.g. ``DU_MODULE`` for
55 ``UCIS_DU_MODULE``.
57:meth:`Document.ToCoverageSummary` converts the statement coverage to the common model of
58:mod:`pyEDAA.Reports.CodeCoverage`:
60* The statements are the statement bins (``STMTBIN``) of the instances: the bins of the scopes below each top-level
61 scope, which isn't a design unit. A statement is identified by its file, its line and its index in the line
62 (attribute ``#SINDEX#``); with ``mergeInstances``, the statements of all instances of a design unit are merged into
63 one, which ran as often as in all instances together (:meth:`Report.IterateStatements`).
64* A file's path is the one a statement bin's source location states, relative to the directory the tool ran in; these
65 directories are the summary's source directories.
66* A line ran as often as its least often run statement: it is covered, if all its statements ran. A line, whose
67 statements are all excluded, is excluded; an excluded statement doesn't count for its line.
68* The branches, the blocks and the other kinds of coverage aren't converted, and the format names no units.
70.. rubric:: Example
72.. code-block:: Python
74 from pathlib import Path
75 from pyEDAA.Reports.CodeCoverage.AldecUCDB import Document
77 report = Document(Path("ucdb.xml"), analyzeAndConvert=True)
78 summary = report.ToCoverageSummary(mergeInstances=True)
79 for file in summary.IterateFiles():
80 print(f"{file.Path}: {file.LineCoverage:.1%}")
81"""
82from __future__ import annotations
84from enum import IntEnum, IntFlag
85from io import BytesIO
86from pathlib import Path
87from typing import Generator, Generic, Iterable, Mapping, Optional as Nullable, Self, TypeVar
89from lxml.etree import XMLParser, XMLSchema, XMLSchemaParseError, XMLSyntaxError
90from lxml.etree import _Element, _ElementTree, parse
91from pyTooling.Common import getFullyQualifiedName, getResourceFile, StringEnum
92from pyTooling.Decorators import export, readonly
93from pyTooling.Exceptions import ToolingException
94from pyTooling.MetaClasses import ExtendedType, abstractclass
95from pyTooling.Stopwatch import Stopwatch
96from pyTooling.Versioning import CalendarVersion
98from pyEDAA.Reports import Resources
99from pyEDAA.Reports.CodeCoverage import CodeCoverageError, CoverageSummary, Document as cc_Document
100from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus
103__all__ = ["SCHEMA", "NAMESPACE", "NAMESPACES", "HistoryNodeParentType", "ScopeParentType"]
105SCHEMA = "Aldec-UCDB.xsd" #: The XML schema a report is validated against.
107NAMESPACE = "www.aldec.com" #: The XML namespace of the format's elements.
108NAMESPACES = {"ux": NAMESPACE} #: The XML namespaces of the format by their prefix, for finding elements.
110HistoryNodeParentType = TypeVar("HistoryNodeParentType", bound="Report | HistoryNode")
111"""A type variable for the parent of a :class:`HistoryNode`: a :class:`Report`
112or a :class:`HistoryNode`."""
114ScopeParentType = TypeVar("ScopeParentType", bound="Report | Scope")
115"""A type variable for the parent of a :class:`Scope`: a :class:`Report` or a
116:class:`Scope`."""
119@export
120class ScopeType(StringEnum):
121 """
122 Kind of a ``<ux:scope>``, as its ``type`` states: a design unit, an instance of one, or a scope of a coverage kind.
123 """
125 Toggle = "TOGGLE" #: Toggle coverage of a signal.
126 Branch = "BRANCH" #: Branch coverage of a branching statement.
127 Expression = "EXPR" #: Expression coverage.
128 Condition = "COND" #: Condition coverage.
129 Instance = "INSTANCE" #: An instance of a design unit, e.g. of a Verilog module.
130 Process = "PROCESS" #: A process, e.g. a Verilog ``always`` block.
131 Block = "BLOCK" #: A block, e.g. a VHDL ``block`` statement.
132 Function = "FUNCTION" #: A function.
133 ForkJoin = "FORKJOIN" #: A Verilog ``fork ... join`` block.
134 Generate = "GENERATE" #: A generate block.
135 Generic = "GENERIC" #: A scope of a generic kind.
136 Class = "CLASS" #: A SystemVerilog class.
137 CoverGroup = "COVERGROUP" #: A SystemVerilog covergroup type.
138 CoverInstance = "COVERINSTANCE" #: An instance of a SystemVerilog covergroup.
139 CoverPoint = "COVERPOINT" #: A SystemVerilog coverpoint.
140 Cross = "CROSS" #: A SystemVerilog cross coverage.
141 Cover = "COVER" #: A cover directive.
142 Assert = "ASSERT" #: An assertion.
143 Program = "PROGRAM" #: An instance of a SystemVerilog program.
144 Package = "PACKAGE" #: An instance of a package.
145 Task = "TASK" #: A Verilog task.
146 Interface = "INTERFACE" #: An instance of a SystemVerilog interface.
147 FSM = "FSM" #: A finite state machine.
148 DesignUnitModule = "DU_MODULE" #: Design unit: a Verilog module.
149 DesignUnitArchitecture = "DU_ARCH" #: Design unit: a VHDL architecture.
150 DesignUnitPackage = "DU_PACKAGE" #: Design unit: a package.
151 DesignUnitProgram = "DU_PROGRAM" #: Design unit: a SystemVerilog program.
152 DesignUnitInterface = "DU_INTERFACE" #: Design unit: a SystemVerilog interface.
153 FSMStates = "FSM_STATES" #: The states of a finite state machine.
154 FSMTransitions = "FSM_TRANS" #: The transitions of a finite state machine.
155 CoverBlock = "COVBLOCK" #: A block of coverage items.
156 CoverGroupBinScope = "CVGBINSCOPE" #: A scope of covergroup bins.
157 IllegalBinScope = "ILLEGALBINSCOPE" #: A scope of illegal covergroup bins.
158 IgnoreBinScope = "IGNOREBINSCOPE" #: A scope of ignored covergroup bins.
161@export
162class CoverType(StringEnum):
163 """
164 Kind of a ``<ux:bin>``, as its ``type`` states.
166 A kind Aldec's export has no name for is stated by its value in hexadecimal, as the UCIS C API defines it.
167 """
169 CoverGroupBin = "CVGBIN" #: A bin of a SystemVerilog covergroup.
170 CoverBin = "COVERBIN" #: A cover directive's pass.
171 AssertBin = "ASSERTBIN" #: An assertion's failure.
172 StatementBin = "STMTBIN" #: A statement.
173 BranchBin = "BRANCHBIN" #: A branch of a branching statement.
174 ExpressionBin = "EXPRBIN" #: A row of an expression's truth table.
175 ConditionBin = "CONDBIN" #: A row of a condition's truth table.
176 ToggleBin = "TOGGLEBIN" #: A transition of a signal's toggle coverage.
177 PassBin = "PASSBIN" #: An assertion's pass.
178 FSMBin = "FSMBIN" #: A state or transition of a finite state machine.
179 UserBin = "USERBIN" #: A bin defined by the user.
180 Count = "COUNT" #: A counter.
181 FailBin = "FAILBIN" #: A cover directive's failure.
182 VacuousBin = "VACUOUSBIN" #: An assertion's vacuous pass.
183 DisabledBin = "DISABLEDBIN" #: An assertion's or cover directive's disabled attempt.
184 AttemptBin = "ATTEMPTBIN" #: An assertion's or cover directive's attempt.
185 ActiveBin = "ACTIVEBIN" #: An assertion's or cover directive's active thread.
186 IgnoreBin = "IGNOREBIN" #: An ignored bin of a SystemVerilog covergroup.
187 IllegalBin = "ILLEGALBIN" #: An illegal bin of a SystemVerilog covergroup.
188 DefaultBin = "DEFAULTBIN" #: The default bin of a SystemVerilog covergroup.
189 PeakActiveBin = "PEAKACTIVEBIN" #: The peak number of an assertion's or cover directive's active threads.
190 BlockBin = "1000000" #: A block of statements, stated by its value ``UCIS_BLOCKBIN``: ``0x1000000``.
193@export
194class Language(StringEnum):
195 """
196 Language of the source of a ``<ux:scope>``, as its ``lang`` states.
197 """
199 VHDL = "VHDL" #: VHDL.
200 Verilog = "VLOG" #: Verilog.
201 SystemVerilog = "SV" #: SystemVerilog.
202 SystemC = "SYSTEMC" #: SystemC.
203 PSLInVHDL = "PSL_VHDL" #: PSL embedded in VHDL.
204 PSLInVerilog = "PSL_VLOG" #: PSL embedded in Verilog.
205 PSLInSystemVerilog = "PSL_SV" #: PSL embedded in SystemVerilog.
206 PSLInSystemC = "PSL_SYSTEMC" #: PSL embedded in SystemC.
207 E = "E" #: The verification language *e*.
208 Vera = "VERA" #: The verification language OpenVera.
209 NoLanguage = "NONE" #: No source language.
210 Other = "OTHER" #: Another source language.
213# A class with a property named like a class - ``Language`` - can't name that class in the annotation of a field: the
214# class body's namespace, where annotations are evaluated, binds the name to the property.
215_Language = Language
218@export
219class HistoryNodeKind(IntEnum):
220 """
221 Kind of a ``<ux:hnode>``, as its ``type`` states.
222 """
224 Test = 1 #: A test: a simulation run, which wrote a coverage database.
225 Merge = 2 #: A merge of coverage databases.
228@export
229class BinFlags(IntFlag):
230 """
231 Flags of a ``<ux:bin>``, as its ``flags`` state in hexadecimal; a flag not named here is kept.
232 """
234 Is32Bit = 0x00000001 #: The count is a 32-bit number.
235 Is64Bit = 0x00000002 #: The count is a 64-bit number.
236 IsVector = 0x00000004 #: The bin is a vector of counts.
237 HasGoal = 0x00000008 #: The bin states a goal.
238 HasWeight = 0x00000010 #: The bin states a weight.
239 ExcludePragma = 0x00000020 #: Excluded by a pragma in the source.
240 ExcludeFile = 0x00000040 #: Excluded by an exclusion file.
241 ExcludeInstance = 0x00000080 #: Excluded in this instance.
242 ExcludeAuto = 0x00000100 #: Excluded automatically by the tool.
243 HasLimit = 0x00000400 #: The count saturates at a limit.
244 HasCount = 0x00000800 #: The bin states a count.
245 IsCovered = 0x00001000 #: The bin reached its goal.
247 Excluded = 0x000001E0 #: Any of the exclusion flags.
250@export
251class AttributesMixin(metaclass=ExtendedType, mixin=True):
252 """
253 A mixin-class adding the attributes an element states: its ``<ux:attr>`` elements, each a key and a typed value.
255 An attribute's type is its value's type: ``int`` is :class:`int`, ``double`` is :class:`float`, ``str`` is
256 :class:`str`.
257 """
259 _attributes: dict[str, int | float | str] #: The attributes, by key.
261 def __init__(self, attributes: Nullable[Mapping[str, int | float | str]] = None) -> None:
262 """
263 Initialize the attributes.
265 :param attributes: Optional, the attributes by key, e.g. ``{"#SINDEX#": 1}``. Default: ``None``.
266 :raises TypeError: If parameter ``attributes`` isn't a mapping.
267 :raises TypeError: If parameter ``attributes`` contains a key not of type :class:`str`.
268 :raises ValueError: If parameter ``attributes`` contains an empty key.
269 :raises TypeError: If parameter ``attributes`` contains a value not of type :class:`int`, :class:`float` or
270 :class:`str`.
271 """
272 self._attributes = {}
274 if attributes is not None:
275 if not isinstance(attributes, Mapping):
276 ex = TypeError("Parameter 'attributes' is not a mapping.")
277 ex.add_note(f"Got type '{getFullyQualifiedName(attributes)}'.")
278 raise ex
280 for key, value in attributes.items():
281 if not isinstance(key, str):
282 ex = TypeError("Parameter 'attributes' contains a key not of type 'str'.")
283 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.")
284 raise ex
285 elif key == "":
286 raise ValueError("Parameter 'attributes' contains an empty key.")
288 if not isinstance(value, (int, float, str)):
289 ex = TypeError("Parameter 'attributes' contains a value not of type 'int', 'float' or 'str'.")
290 ex.add_note(f"Got type '{getFullyQualifiedName(value)}' for key '{key}'.")
291 raise ex
293 self._attributes[key] = value
295 @staticmethod
296 def _ParseAttributes(element: _Element) -> dict[str, int | float | str]:
297 """
298 Parse the attributes an element states, from its ``<ux:attr>`` elements.
300 :param element: The element stating the attributes.
301 :returns: The attributes by key, each value of the type the attribute states.
302 :raises CodeCoverageError: If an attribute's value isn't of the type the attribute states.
303 """
304 attributes: dict[str, int | float | str] = {}
305 for attributeElement in element.iterfind("ux:attr", NAMESPACES):
306 key = attributeElement.attrib["key"]
307 attributeType = attributeElement.attrib["type"]
308 text = attributeElement.text if attributeElement.text is not None else ""
309 try:
310 if attributeType == "int":
311 attributes[key] = int(text)
312 elif attributeType == "double":
313 attributes[key] = float(text)
314 else:
315 attributes[key] = text
316 except ValueError as cause:
317 ex = CodeCoverageError(f"UCDB attribute '{key}' states a value not of type '{attributeType}'.")
318 ex.add_note(f"Got value '{text}'.")
319 raise ex from cause
321 return attributes
323 @readonly
324 def Attributes(self) -> dict[str, int | float | str]:
325 """
326 Read-only property to access the attributes (:attr:`_attributes`).
328 :returns: The attributes by key, e.g. ``#SINDEX#`` - a statement's index in its line - or ``UCIS:tool``.
329 """
330 return self._attributes
333@export
334@abstractclass
335class Base(metaclass=ExtendedType, slots=True):
336 """
337 Base-class of the named elements below the report.
339 Scopes and bins are named elements.
340 """
342 _name: str #: Name of the element.
344 def __init__(self, name: str) -> None:
345 """
346 Initialize the name of the element.
348 :param name: Name of the element.
349 :raises ValueError: If parameter ``name`` is ``None``.
350 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
351 """
352 if name is None:
353 raise ValueError("Parameter 'name' is None.")
354 elif not isinstance(name, str):
355 ex = TypeError("Parameter 'name' is not of type 'str'.")
356 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
357 raise ex
359 self._name = name
361 @readonly
362 def Name(self) -> str:
363 """
364 Read-only property to access the name of the element (:attr:`_name`).
366 :returns: The name.
367 """
368 return self._name
371@export
372class SourceLocation(metaclass=ExtendedType, slots=True):
373 """
374 A ``<ux:src>`` of a scope or bin: the source file, the directory its name is relative to, the line and the token.
376 It is a value, as a path is: it names no parent.
377 """
379 _file: Path #: Path of the source file, relative to :attr:`_workDirectory`, if not absolute.
380 _workDirectory: Path #: The directory the tool ran in.
381 _lineNumber: int #: Line number, counted from 1; ``0`` for a scope without a line.
382 _token: int #: Number of the token in the line.
384 def __init__(self, file: Path, workDirectory: Path, lineNumber: int, token: int) -> None:
385 """
386 Initialize the source location.
388 :param file: Path of the source file, e.g. ``src/counter.sv``.
389 :param workDirectory: The directory the tool ran in.
390 :param lineNumber: Line number, counted from 1; ``0`` for a scope without a line.
391 :param token: Number of the token in the line.
392 :raises ValueError: If parameter ``file`` is ``None``.
393 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
394 :raises ValueError: If parameter ``workDirectory`` is ``None``.
395 :raises TypeError: If parameter ``workDirectory`` isn't of type :class:`~pathlib.Path`.
396 :raises ValueError: If parameter ``lineNumber`` is ``None``.
397 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
398 :raises ValueError: If parameter ``lineNumber`` is negative.
399 :raises ValueError: If parameter ``token`` is ``None``.
400 :raises TypeError: If parameter ``token`` isn't of type :class:`int`.
401 :raises ValueError: If parameter ``token`` is negative.
402 """
403 if file is None:
404 raise ValueError("Parameter 'file' is None.")
405 elif not isinstance(file, Path):
406 ex = TypeError("Parameter 'file' is not of type 'Path'.")
407 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
408 raise ex
410 if workDirectory is None: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 raise ValueError("Parameter 'workDirectory' is None.")
412 elif not isinstance(workDirectory, Path):
413 ex = TypeError("Parameter 'workDirectory' is not of type 'Path'.")
414 ex.add_note(f"Got type '{getFullyQualifiedName(workDirectory)}'.")
415 raise ex
417 if lineNumber is None: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 raise ValueError("Parameter 'lineNumber' is None.")
419 elif not isinstance(lineNumber, int): 419 ↛ 420line 419 didn't jump to line 420 because the condition on line 419 was never true
420 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
421 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
422 raise ex
423 elif lineNumber < 0:
424 ex = ValueError("Parameter 'lineNumber' is negative.")
425 ex.add_note(f"Got value '{lineNumber}'.")
426 raise ex
428 if token is None:
429 raise ValueError("Parameter 'token' is None.")
430 elif not isinstance(token, int): 430 ↛ 431line 430 didn't jump to line 431 because the condition on line 430 was never true
431 ex = TypeError("Parameter 'token' is not of type 'int'.")
432 ex.add_note(f"Got type '{getFullyQualifiedName(token)}'.")
433 raise ex
434 elif token < 0: 434 ↛ 435line 434 didn't jump to line 435 because the condition on line 434 was never true
435 ex = ValueError("Parameter 'token' is negative.")
436 ex.add_note(f"Got value '{token}'.")
437 raise ex
439 self._file = file
440 self._workDirectory = workDirectory
441 self._lineNumber = lineNumber
442 self._token = token
444 @classmethod
445 def Parse(cls, element: _Element) -> Self:
446 """
447 Parse a source location from its ``<ux:src>`` element.
449 :param element: The ``<ux:src>`` element.
450 :returns: The source location.
451 """
452 return cls(
453 Path(element.attrib["file"]),
454 Path(element.attrib["workdir"]),
455 int(element.attrib["line"]),
456 int(element.attrib["token"])
457 )
459 @readonly
460 def File(self) -> Path:
461 """
462 Read-only property to access the path of the source file (:attr:`_file`).
464 :returns: The path, relative to :attr:`WorkDirectory`, if not absolute.
465 """
466 return self._file
468 @readonly
469 def WorkDirectory(self) -> Path:
470 """
471 Read-only property to access the directory the tool ran in (:attr:`_workDirectory`).
473 It is a path on the machine the coverage was measured on, so it may not exist where the report is read.
475 :returns: The directory.
476 """
477 return self._workDirectory
479 @readonly
480 def LineNumber(self) -> int:
481 """
482 Read-only property to access the line number (:attr:`_lineNumber`).
484 :returns: The line number, counted from 1; ``0`` for a scope without a line.
485 """
486 return self._lineNumber
488 @readonly
489 def Token(self) -> int:
490 """
491 Read-only property to access the number of the token in the line (:attr:`_token`).
493 :returns: The token's number.
494 """
495 return self._token
498@export
499class Bin(Base, AttributesMixin):
500 """
501 A ``<ux:bin>`` of a scope: a coverage item, how often it was hit, its flags, and where it is in the sources.
503 Statements and branches are coverage items.
504 """
506 _parent: Nullable[Scope] #: The scope the bin belongs to.
507 _type: CoverType #: Kind of the bin.
508 _flags: BinFlags #: Flags of the bin, e.g. whether it is excluded.
509 _count: int #: How often the coverage item was hit.
510 _source: SourceLocation #: Where the coverage item is in the sources.
512 def __init__(
513 self,
514 name: str,
515 coverType: CoverType,
516 flags: BinFlags,
517 count: int,
518 source: SourceLocation,
519 attributes: Nullable[Mapping[str, int | float | str]] = None,
520 *,
521 parent: Nullable[Scope] = None
522 ) -> None:
523 """
524 Initialize the bin, and append it to the bins of its scope.
526 :param name: Name of the bin, e.g. ``true_branch``; empty for a statement.
527 :param coverType: Kind of the bin.
528 :param flags: Flags of the bin.
529 :param count: How often the coverage item was hit.
530 :param source: Where the coverage item is in the sources.
531 :param attributes: Optional, the attributes by key, e.g. ``{"#SINDEX#": 1}``. Default: ``None``.
532 :param parent: Optional, the scope the bin belongs to; the bin is appended to its bins. Default: ``None``.
533 :raises ValueError: If parameter ``name`` is ``None``.
534 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
535 :raises TypeError: If parameter ``attributes`` isn't a mapping.
536 :raises TypeError: If parameter ``attributes`` contains a key not of type :class:`str`.
537 :raises ValueError: If parameter ``attributes`` contains an empty key.
538 :raises TypeError: If parameter ``attributes`` contains a value not of type :class:`int`, :class:`float` or
539 :class:`str`.
540 :raises ValueError: If parameter ``coverType`` is ``None``.
541 :raises TypeError: If parameter ``coverType`` isn't of type
542 :class:`CoverType`.
543 :raises ValueError: If parameter ``flags`` is ``None``.
544 :raises TypeError: If parameter ``flags`` isn't of type :class:`BinFlags`.
545 :raises ValueError: If parameter ``count`` is ``None``.
546 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
547 :raises ValueError: If parameter ``count`` is negative.
548 :raises ValueError: If parameter ``source`` is ``None``.
549 :raises TypeError: If parameter ``source`` isn't of type :class:`SourceLocation`.
550 :raises TypeError: If parameter ``parent`` isn't of type :class:`Scope`.
551 """
552 super().__init__(name)
553 AttributesMixin.__init__(self, attributes)
555 if coverType is None: 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true
556 raise ValueError("Parameter 'coverType' is None.")
557 elif not isinstance(coverType, CoverType):
558 ex = TypeError("Parameter 'coverType' is not of type 'CoverType'.")
559 ex.add_note(f"Got type '{getFullyQualifiedName(coverType)}'.")
560 raise ex
562 if flags is None: 562 ↛ 563line 562 didn't jump to line 563 because the condition on line 562 was never true
563 raise ValueError("Parameter 'flags' is None.")
564 elif not isinstance(flags, BinFlags):
565 ex = TypeError("Parameter 'flags' is not of type 'BinFlags'.")
566 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.")
567 raise ex
569 if count is None: 569 ↛ 570line 569 didn't jump to line 570 because the condition on line 569 was never true
570 raise ValueError("Parameter 'count' is None.")
571 elif not isinstance(count, int): 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true
572 ex = TypeError("Parameter 'count' is not of type 'int'.")
573 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
574 raise ex
575 elif count < 0:
576 ex = ValueError("Parameter 'count' is negative.")
577 ex.add_note(f"Got value '{count}'.")
578 raise ex
580 if source is None: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true
581 raise ValueError("Parameter 'source' is None.")
582 elif not isinstance(source, SourceLocation):
583 ex = TypeError("Parameter 'source' is not of type 'SourceLocation'.")
584 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
585 raise ex
587 if parent is not None and not isinstance(parent, Scope):
588 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
589 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
590 raise ex
592 self._parent = parent
593 self._type = coverType
594 self._flags = flags
595 self._count = count
596 self._source = source
598 if parent is not None:
599 parent._bins.append(self)
601 @classmethod
602 def Parse(cls, element: _Element, *, parent: Nullable[Scope] = None) -> Self:
603 """
604 Parse a bin, its count, source location and attributes from its ``<ux:bin>`` element.
606 :param element: The ``<ux:bin>`` element.
607 :param parent: Optional, the scope the bin belongs to. Default: ``None``.
608 :returns: The bin.
609 :raises CodeCoverageError: If an attribute's value isn't of the type the attribute states.
610 """
611 return cls(
612 element.attrib["name"],
613 CoverType(element.attrib["type"]),
614 BinFlags(int(element.attrib["flags"], 16)),
615 int(element.find("ux:count", NAMESPACES).text),
616 SourceLocation.Parse(element.find("ux:src", NAMESPACES)),
617 cls._ParseAttributes(element),
618 parent=parent
619 )
621 @readonly
622 def Parent(self) -> Nullable[Scope]:
623 """
624 Read-only property to access the scope the bin belongs to (:attr:`_parent`).
626 :returns: The scope; ``None`` if the bin belongs to no scope.
627 """
628 return self._parent
630 @readonly
631 def Type(self) -> CoverType:
632 """
633 Read-only property to access the kind of the bin (:attr:`_type`).
635 :returns: The kind, e.g. a statement.
636 """
637 return self._type
639 @readonly
640 def Flags(self) -> BinFlags:
641 """
642 Read-only property to access the flags of the bin (:attr:`_flags`).
644 :returns: The flags.
645 """
646 return self._flags
648 @readonly
649 def IsExcluded(self) -> bool:
650 """
651 Read-only property to return whether the bin is excluded from the measurement.
653 :returns: ``True``, if :attr:`Flags` has one of the exclusion flags (:attr:`BinFlags.Excluded`).
654 """
655 return (self._flags & BinFlags.Excluded) != 0
657 @readonly
658 def Count(self) -> int:
659 """
660 Read-only property to access how often the coverage item was hit (:attr:`_count`).
662 :returns: The count, e.g. how often a statement ran.
663 """
664 return self._count
666 @readonly
667 def Source(self) -> SourceLocation:
668 """
669 Read-only property to access where the coverage item is in the sources (:attr:`_source`).
671 :returns: The source location.
672 """
673 return self._source
676@export
677class Scope(Base, AttributesMixin, Generic[ScopeParentType]):
678 """
679 A ``<ux:scope>`` of the report or of a scope: its bins and the scopes below it.
681 Design units, their instances, processes and branching statements are scopes.
682 """
684 _parent: Nullable[ScopeParentType] #: The report or scope the scope belongs to.
685 _type: ScopeType #: Kind of the scope.
686 _language: _Language #: Language of the scope's source.
687 _weight: int #: Weight of the scope.
688 _flags: int #: Flags of the scope.
689 _source: SourceLocation #: Where the scope is in the sources.
690 _designUnit: Nullable[str] #: Name of the design unit an instance instantiates.
691 _scopes: list[Scope[Scope]] #: The scopes below this one.
692 _bins: list[Bin] #: The bins of this scope.
694 def __init__(
695 self,
696 name: str,
697 scopeType: ScopeType,
698 language: _Language,
699 weight: int,
700 flags: int,
701 source: SourceLocation,
702 designUnit: Nullable[str] = None,
703 attributes: Nullable[Mapping[str, int | float | str]] = None,
704 *,
705 parent: Nullable[ScopeParentType] = None
706 ) -> None:
707 """
708 Initialize the scope, and append it to the scopes of its report or scope.
710 Its scopes and bins are added by creating them with this scope as their parent.
712 :param name: Name of the scope, e.g. ``work.counter`` for a design unit or ``uut`` for an instance.
713 :param scopeType: Kind of the scope.
714 :param language: Language of the scope's source.
715 :param weight: Weight of the scope.
716 :param flags: Flags of the scope.
717 :param source: Where the scope is in the sources.
718 :param designUnit: Optional, name of the design unit an instance instantiates, e.g. ``work.counter``.
719 Default: ``None``.
720 :param attributes: Optional, the attributes by key, e.g. ``{"#GOAL#": 100}``. Default: ``None``.
721 :param parent: Optional, the report or scope the scope belongs to; the scope is appended to its scopes.
722 Default: ``None``.
723 :raises ValueError: If parameter ``name`` is ``None``.
724 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
725 :raises TypeError: If parameter ``attributes`` isn't a mapping.
726 :raises TypeError: If parameter ``attributes`` contains a key not of type :class:`str`.
727 :raises ValueError: If parameter ``attributes`` contains an empty key.
728 :raises TypeError: If parameter ``attributes`` contains a value not of type :class:`int`, :class:`float` or
729 :class:`str`.
730 :raises ValueError: If parameter ``name`` is empty.
731 :raises ValueError: If parameter ``scopeType`` is ``None``.
732 :raises TypeError: If parameter ``scopeType`` isn't of type
733 :class:`ScopeType`.
734 :raises ValueError: If parameter ``language`` is ``None``.
735 :raises TypeError: If parameter ``language`` isn't of type
736 :class:`Language`.
737 :raises ValueError: If parameter ``weight`` is ``None``.
738 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
739 :raises ValueError: If parameter ``weight`` is negative.
740 :raises ValueError: If parameter ``flags`` is ``None``.
741 :raises TypeError: If parameter ``flags`` isn't of type :class:`int`.
742 :raises ValueError: If parameter ``flags`` is negative.
743 :raises ValueError: If parameter ``source`` is ``None``.
744 :raises TypeError: If parameter ``source`` isn't of type :class:`SourceLocation`.
745 :raises TypeError: If parameter ``designUnit`` isn't of type :class:`str`.
746 :raises ValueError: If parameter ``designUnit`` is empty.
747 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`
748 or :class:`Scope`.
749 """
750 super().__init__(name)
751 AttributesMixin.__init__(self, attributes)
753 if name == "":
754 raise ValueError("Parameter 'name' is empty.")
756 if scopeType is None: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true
757 raise ValueError("Parameter 'scopeType' is None.")
758 elif not isinstance(scopeType, ScopeType):
759 ex = TypeError("Parameter 'scopeType' is not of type 'ScopeType'.")
760 ex.add_note(f"Got type '{getFullyQualifiedName(scopeType)}'.")
761 raise ex
763 if language is None:
764 raise ValueError("Parameter 'language' is None.")
765 elif not isinstance(language, Language): 765 ↛ 766line 765 didn't jump to line 766 because the condition on line 765 was never true
766 ex = TypeError("Parameter 'language' is not of type 'Language'.")
767 ex.add_note(f"Got type '{getFullyQualifiedName(language)}'.")
768 raise ex
770 if weight is None: 770 ↛ 771line 770 didn't jump to line 771 because the condition on line 770 was never true
771 raise ValueError("Parameter 'weight' is None.")
772 elif not isinstance(weight, int): 772 ↛ 773line 772 didn't jump to line 773 because the condition on line 772 was never true
773 ex = TypeError("Parameter 'weight' is not of type 'int'.")
774 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.")
775 raise ex
776 elif weight < 0:
777 ex = ValueError("Parameter 'weight' is negative.")
778 ex.add_note(f"Got value '{weight}'.")
779 raise ex
781 if flags is None: 781 ↛ 782line 781 didn't jump to line 782 because the condition on line 781 was never true
782 raise ValueError("Parameter 'flags' is None.")
783 elif not isinstance(flags, int):
784 ex = TypeError("Parameter 'flags' is not of type 'int'.")
785 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.")
786 raise ex
787 elif flags < 0: 787 ↛ 788line 787 didn't jump to line 788 because the condition on line 787 was never true
788 ex = ValueError("Parameter 'flags' is negative.")
789 ex.add_note(f"Got value '{flags}'.")
790 raise ex
792 if source is None:
793 raise ValueError("Parameter 'source' is None.")
794 elif not isinstance(source, SourceLocation): 794 ↛ 795line 794 didn't jump to line 795 because the condition on line 794 was never true
795 ex = TypeError("Parameter 'source' is not of type 'SourceLocation'.")
796 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
797 raise ex
799 if designUnit is not None:
800 if not isinstance(designUnit, str): 800 ↛ 801line 800 didn't jump to line 801 because the condition on line 800 was never true
801 ex = TypeError("Parameter 'designUnit' is not of type 'str'.")
802 ex.add_note(f"Got type '{getFullyQualifiedName(designUnit)}'.")
803 raise ex
804 elif designUnit == "":
805 raise ValueError("Parameter 'designUnit' is empty.")
807 if parent is not None and not isinstance(parent, (Report, Scope)):
808 ex = TypeError("Parameter 'parent' is not of type 'Report' or 'Scope'.")
809 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
810 raise ex
812 self._parent = parent
813 self._type = scopeType
814 self._language = language
815 self._weight = weight
816 self._flags = flags
817 self._source = source
818 self._designUnit = designUnit
819 self._scopes = []
820 self._bins = []
822 if parent is not None:
823 parent._scopes.append(self)
825 @classmethod
826 def Parse(cls, element: _Element, *, parent: Nullable[ScopeParentType] = None) -> Self:
827 """
828 Parse a scope, its attributes, bins and the scopes below it from its ``<ux:scope>`` element.
830 :param element: The ``<ux:scope>`` element.
831 :param parent: Optional, the report or scope the scope belongs to. Default: ``None``.
832 :returns: The scope.
833 :raises CodeCoverageError: If an attribute's value isn't of the type the attribute states.
834 """
835 scope = cls(
836 element.attrib["name"],
837 ScopeType(element.attrib["type"]),
838 Language(element.attrib["lang"]),
839 int(element.attrib["weight"]),
840 int(element.attrib["flags"], 16),
841 SourceLocation.Parse(element.find("ux:src", NAMESPACES)),
842 element.attrib.get("du"),
843 cls._ParseAttributes(element),
844 parent=parent
845 )
847 for binElement in element.iterfind("ux:bin", NAMESPACES):
848 Bin.Parse(binElement, parent=scope)
850 for scopeElement in element.iterfind("ux:scope", NAMESPACES):
851 Scope.Parse(scopeElement, parent=scope)
853 return scope
855 def IterateBins(self) -> Generator[Bin, None, None]:
856 """
857 Iterate the bins of this scope, then those of the scopes below it, depth-first.
859 :returns: A generator of the bins.
860 """
861 yield from self._bins
862 for scope in self._scopes:
863 yield from scope.IterateBins()
865 @readonly
866 def Parent(self) -> Nullable[ScopeParentType]:
867 """
868 Read-only property to access the report or scope the scope belongs to (:attr:`_parent`).
870 :returns: The :class:`Report` or :class:`Scope`; ``None`` if the scope
871 belongs to none.
872 """
873 return self._parent
875 @readonly
876 def Type(self) -> ScopeType:
877 """
878 Read-only property to access the kind of the scope (:attr:`_type`).
880 :returns: The kind, e.g. a design unit or an instance.
881 """
882 return self._type
884 @readonly
885 def Language(self) -> _Language:
886 """
887 Read-only property to access the language of the scope's source (:attr:`_language`).
889 :returns: The language.
890 """
891 return self._language
893 @readonly
894 def Weight(self) -> int:
895 """
896 Read-only property to access the weight of the scope (:attr:`_weight`).
898 :returns: The weight.
899 """
900 return self._weight
902 @readonly
903 def Flags(self) -> int:
904 """
905 Read-only property to access the flags of the scope (:attr:`_flags`).
907 :returns: The flags, as stated.
908 """
909 return self._flags
911 @readonly
912 def Source(self) -> SourceLocation:
913 """
914 Read-only property to access where the scope is in the sources (:attr:`_source`).
916 :returns: The source location.
917 """
918 return self._source
920 @readonly
921 def DesignUnit(self) -> Nullable[str]:
922 """
923 Read-only property to access the name of the design unit an instance instantiates (:attr:`_designUnit`).
925 :returns: The design unit's name, e.g. ``work.counter``; ``None`` for a scope, which isn't an instance.
926 """
927 return self._designUnit
929 @readonly
930 def Scopes(self) -> list[Scope[Scope]]:
931 """
932 Read-only property to access the scopes below this one (:attr:`_scopes`).
934 :returns: The scopes, in the order the report lists them.
935 """
936 return self._scopes
938 @readonly
939 def Bins(self) -> list[Bin]:
940 """
941 Read-only property to access the bins of this scope (:attr:`_bins`).
943 :returns: The bins, in the order the report lists them.
944 """
945 return self._bins
948@export
949class HistoryNode(Base, AttributesMixin, Generic[HistoryNodeParentType]):
950 """
951 A ``<ux:hnode>`` of the report or of a merge: a test or a merge of databases, its attributes and merged nodes.
953 A test wrote a coverage database, a merge merged databases. The attributes state e.g. the tool, the date and the
954 command line.
955 """
957 _parent: Nullable[HistoryNodeParentType] #: The report or merge the history node belongs to.
958 _physicalName: Path #: Path of the coverage database.
959 _kind: HistoryNodeKind #: Kind of the history node.
960 _historyNodes: list[HistoryNode[HistoryNode]] #: The history nodes merged by this one.
962 def __init__(
963 self,
964 name: str,
965 physicalName: Path,
966 kind: HistoryNodeKind,
967 attributes: Nullable[Mapping[str, int | float | str]] = None,
968 *,
969 parent: Nullable[HistoryNodeParentType] = None
970 ) -> None:
971 """
972 Initialize the history node, and append it to the history nodes of its report or merge.
974 The history nodes it merged are added by creating them with this history node as their parent.
976 :param name: Logical name of the history node, e.g. of the test.
977 :param physicalName: Path of the coverage database, e.g. ``cov/aggregate/aggregate.acdb``.
978 :param kind: Kind of the history node.
979 :param attributes: Optional, the attributes by key, e.g. ``{"UCIS:tool": "Riviera-PRO"}``. Default: ``None``.
980 :param parent: Optional, the report or merge the history node belongs to; the history node is appended
981 to its history nodes. Default: ``None``.
982 :raises ValueError: If parameter ``name`` is ``None``.
983 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
984 :raises TypeError: If parameter ``attributes`` isn't a mapping.
985 :raises TypeError: If parameter ``attributes`` contains a key not of type :class:`str`.
986 :raises ValueError: If parameter ``attributes`` contains an empty key.
987 :raises TypeError: If parameter ``attributes`` contains a value not of type :class:`int`, :class:`float` or
988 :class:`str`.
989 :raises ValueError: If parameter ``name`` is empty.
990 :raises ValueError: If parameter ``physicalName`` is ``None``.
991 :raises TypeError: If parameter ``physicalName`` isn't of type :class:`~pathlib.Path`.
992 :raises ValueError: If parameter ``kind`` is ``None``.
993 :raises TypeError: If parameter ``kind`` isn't of type :class:`HistoryNodeKind`.
994 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`
995 or :class:`HistoryNode`.
996 """
997 super().__init__(name)
998 AttributesMixin.__init__(self, attributes)
1000 if name == "":
1001 raise ValueError("Parameter 'name' is empty.")
1003 if physicalName is None: 1003 ↛ 1004line 1003 didn't jump to line 1004 because the condition on line 1003 was never true
1004 raise ValueError("Parameter 'physicalName' is None.")
1005 elif not isinstance(physicalName, Path):
1006 ex = TypeError("Parameter 'physicalName' is not of type 'Path'.")
1007 ex.add_note(f"Got type '{getFullyQualifiedName(physicalName)}'.")
1008 raise ex
1010 if kind is None: 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true
1011 raise ValueError("Parameter 'kind' is None.")
1012 elif not isinstance(kind, HistoryNodeKind):
1013 ex = TypeError("Parameter 'kind' is not of type 'HistoryNodeKind'.")
1014 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
1015 raise ex
1017 if parent is not None and not isinstance(parent, (Report, HistoryNode)):
1018 ex = TypeError("Parameter 'parent' is not of type 'Report' or 'HistoryNode'.")
1019 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1020 raise ex
1022 self._parent = parent
1023 self._physicalName = physicalName
1024 self._kind = kind
1025 self._historyNodes = []
1027 if parent is not None:
1028 parent._historyNodes.append(self)
1030 @classmethod
1031 def Parse(cls, element: _Element, *, parent: Nullable[HistoryNodeParentType] = None) -> Self:
1032 """
1033 Parse a history node, its attributes and the history nodes it merged from its ``<ux:hnode>`` element.
1035 :param element: The ``<ux:hnode>`` element.
1036 :param parent: Optional, the report or merge the history node belongs to. Default: ``None``.
1037 :returns: The history node.
1038 :raises CodeCoverageError: If an attribute's value isn't of the type the attribute states.
1039 """
1040 historyNode = cls(
1041 element.attrib["logical_name"],
1042 Path(element.attrib["physical_name"]),
1043 HistoryNodeKind(int(element.attrib["type"])),
1044 cls._ParseAttributes(element),
1045 parent=parent
1046 )
1048 for historyNodeElement in element.iterfind("ux:hnode", NAMESPACES):
1049 HistoryNode.Parse(historyNodeElement, parent=historyNode)
1051 return historyNode
1053 @readonly
1054 def Parent(self) -> Nullable[HistoryNodeParentType]:
1055 """
1056 Read-only property to access the report or merge the history node belongs to (:attr:`_parent`).
1058 :returns: The :class:`Report` or :class:`HistoryNode`; ``None`` if the
1059 history node belongs to none.
1060 """
1061 return self._parent
1063 @readonly
1064 def PhysicalName(self) -> Path:
1065 """
1066 Read-only property to access the path of the coverage database (:attr:`_physicalName`).
1068 :returns: The path, as the tool stated it, e.g. relative to the directory it ran in.
1069 """
1070 return self._physicalName
1072 @readonly
1073 def Kind(self) -> HistoryNodeKind:
1074 """
1075 Read-only property to access the kind of the history node (:attr:`_kind`).
1077 :returns: The kind: a test or a merge.
1078 """
1079 return self._kind
1081 @readonly
1082 def HistoryNodes(self) -> list[HistoryNode[HistoryNode]]:
1083 """
1084 Read-only property to access the history nodes merged by this one (:attr:`_historyNodes`).
1086 :returns: The history nodes, in the order the report lists them; empty for a test.
1087 """
1088 return self._historyNodes
1091@export
1092class Command(metaclass=ExtendedType, slots=True):
1093 """
1094 A ``<ux:command>`` of the report: a record of a command applied to the coverage database.
1096 An exclusion is such a command. Its fields are kept as stated, not interpreted.
1097 """
1099 _parent: Nullable[Report] #: The report the command belongs to.
1100 _type: int #: Kind of the command.
1101 _flags: int #: Flags of the command.
1102 _data: str #: Data of the command, e.g. a list of values separated by ``;``.
1103 _index: int #: Index of the command.
1105 def __init__(self, commandType: int, flags: int, data: str, index: int, *, parent: Nullable[Report] = None) -> None:
1106 """
1107 Initialize the command, and append it to the commands of its report.
1109 :param commandType: Kind of the command.
1110 :param flags: Flags of the command.
1111 :param data: Data of the command.
1112 :param index: Index of the command.
1113 :param parent: Optional, the report the command belongs to; the command is appended to its commands.
1114 Default: ``None``.
1115 :raises ValueError: If parameter ``commandType`` is ``None``.
1116 :raises TypeError: If parameter ``commandType`` isn't of type :class:`int`.
1117 :raises ValueError: If parameter ``flags`` is ``None``.
1118 :raises TypeError: If parameter ``flags`` isn't of type :class:`int`.
1119 :raises ValueError: If parameter ``flags`` is negative.
1120 :raises ValueError: If parameter ``data`` is ``None``.
1121 :raises TypeError: If parameter ``data`` isn't of type :class:`str`.
1122 :raises ValueError: If parameter ``index`` is ``None``.
1123 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
1124 :raises ValueError: If parameter ``index`` is negative.
1125 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`.
1126 """
1127 if commandType is None: 1127 ↛ 1128line 1127 didn't jump to line 1128 because the condition on line 1127 was never true
1128 raise ValueError("Parameter 'commandType' is None.")
1129 elif not isinstance(commandType, int):
1130 ex = TypeError("Parameter 'commandType' is not of type 'int'.")
1131 ex.add_note(f"Got type '{getFullyQualifiedName(commandType)}'.")
1132 raise ex
1134 if flags is None: 1134 ↛ 1135line 1134 didn't jump to line 1135 because the condition on line 1134 was never true
1135 raise ValueError("Parameter 'flags' is None.")
1136 elif not isinstance(flags, int): 1136 ↛ 1137line 1136 didn't jump to line 1137 because the condition on line 1136 was never true
1137 ex = TypeError("Parameter 'flags' is not of type 'int'.")
1138 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.")
1139 raise ex
1140 elif flags < 0:
1141 ex = ValueError("Parameter 'flags' is negative.")
1142 ex.add_note(f"Got value '{flags}'.")
1143 raise ex
1145 if data is None:
1146 raise ValueError("Parameter 'data' is None.")
1147 elif not isinstance(data, str): 1147 ↛ 1148line 1147 didn't jump to line 1148 because the condition on line 1147 was never true
1148 ex = TypeError("Parameter 'data' is not of type 'str'.")
1149 ex.add_note(f"Got type '{getFullyQualifiedName(data)}'.")
1150 raise ex
1152 if index is None: 1152 ↛ 1153line 1152 didn't jump to line 1153 because the condition on line 1152 was never true
1153 raise ValueError("Parameter 'index' is None.")
1154 elif not isinstance(index, int): 1154 ↛ 1155line 1154 didn't jump to line 1155 because the condition on line 1154 was never true
1155 ex = TypeError("Parameter 'index' is not of type 'int'.")
1156 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.")
1157 raise ex
1158 elif index < 0:
1159 ex = ValueError("Parameter 'index' is negative.")
1160 ex.add_note(f"Got value '{index}'.")
1161 raise ex
1163 if parent is not None and not isinstance(parent, Report):
1164 ex = TypeError("Parameter 'parent' is not of type 'Report'.")
1165 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1166 raise ex
1168 self._parent = parent
1169 self._type = commandType
1170 self._flags = flags
1171 self._data = data
1172 self._index = index
1174 if parent is not None: 1174 ↛ exitline 1174 didn't return from function '__init__' because the condition on line 1174 was always true
1175 parent._commands.append(self)
1177 @classmethod
1178 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self:
1179 """
1180 Parse a command from its ``<ux:command>`` element.
1182 :param element: The ``<ux:command>`` element.
1183 :param parent: Optional, the report the command belongs to. Default: ``None``.
1184 :returns: The command.
1185 """
1186 return cls(
1187 int(element.attrib["type"]),
1188 int(element.attrib["flags"]),
1189 element.attrib["data"],
1190 int(element.attrib["index"]),
1191 parent=parent
1192 )
1194 @readonly
1195 def Parent(self) -> Nullable[Report]:
1196 """
1197 Read-only property to access the report the command belongs to (:attr:`_parent`).
1199 :returns: The report; ``None`` if the command belongs to no report.
1200 """
1201 return self._parent
1203 @readonly
1204 def Type(self) -> int:
1205 """
1206 Read-only property to access the kind of the command (:attr:`_type`).
1208 :returns: The kind, as stated.
1209 """
1210 return self._type
1212 @readonly
1213 def Flags(self) -> int:
1214 """
1215 Read-only property to access the flags of the command (:attr:`_flags`).
1217 :returns: The flags, as stated.
1218 """
1219 return self._flags
1221 @readonly
1222 def Data(self) -> str:
1223 """
1224 Read-only property to access the data of the command (:attr:`_data`).
1226 :returns: The data, as stated, e.g. ``3;1;10;dut.sv;63;/project;0;0;``.
1227 """
1228 return self._data
1230 @readonly
1231 def Index(self) -> int:
1232 """
1233 Read-only property to access the index of the command (:attr:`_index`).
1235 :returns: The index, as stated.
1236 """
1237 return self._index
1240@export
1241class Statement(metaclass=ExtendedType, slots=True):
1242 """
1243 A statement of a source line: its statement bins - of one instance, or merged, of all instances -, and how often it
1244 ran.
1246 A statement isn't an element of the format: :meth:`Report.IterateStatements` computes it from the bins.
1247 """
1249 _file: Path #: Path of the source file.
1250 _lineNumber: int #: Line number, counted from 1.
1251 _index: int #: Index of the statement in its line.
1252 _bins: list[Bin] #: The statement bins of the statement.
1253 _count: int #: How often the statement ran: the sum of its bins' counts, without the excluded ones.
1254 _isExcluded: bool #: Whether all the statement's bins are excluded.
1256 def __init__(self, file: Path, lineNumber: int, index: int, bins: Iterable[Bin]) -> None:
1257 """
1258 Initialize the statement, its count and whether it is excluded.
1260 :param file: Path of the source file, as the bins' source locations state it.
1261 :param lineNumber: Line number, counted from 1.
1262 :param index: Index of the statement in its line, counted from 1.
1263 :param bins: The statement bins of the statement: one, or of all instances, if merged.
1264 :raises ValueError: If parameter ``file`` is ``None``.
1265 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
1266 :raises ValueError: If parameter ``lineNumber`` is ``None``.
1267 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
1268 :raises ValueError: If parameter ``lineNumber`` is less than 1.
1269 :raises ValueError: If parameter ``index`` is ``None``.
1270 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
1271 :raises ValueError: If parameter ``index`` is negative.
1272 :raises ValueError: If parameter ``bins`` is ``None``.
1273 :raises TypeError: If parameter ``bins`` isn't iterable.
1274 :raises TypeError: If parameter ``bins`` contains an element not of type
1275 :class:`Bin`.
1276 :raises ValueError: If parameter ``bins`` is empty.
1277 """
1278 if file is None: 1278 ↛ 1279line 1278 didn't jump to line 1279 because the condition on line 1278 was never true
1279 raise ValueError("Parameter 'file' is None.")
1280 elif not isinstance(file, Path): 1280 ↛ 1281line 1280 didn't jump to line 1281 because the condition on line 1280 was never true
1281 ex = TypeError("Parameter 'file' is not of type 'Path'.")
1282 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
1283 raise ex
1285 if lineNumber is None: 1285 ↛ 1286line 1285 didn't jump to line 1286 because the condition on line 1285 was never true
1286 raise ValueError("Parameter 'lineNumber' is None.")
1287 elif not isinstance(lineNumber, int): 1287 ↛ 1288line 1287 didn't jump to line 1288 because the condition on line 1287 was never true
1288 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
1289 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
1290 raise ex
1291 elif lineNumber < 1:
1292 ex = ValueError("Parameter 'lineNumber' is less than 1.")
1293 ex.add_note(f"Got value '{lineNumber}'.")
1294 raise ex
1296 if index is None: 1296 ↛ 1297line 1296 didn't jump to line 1297 because the condition on line 1296 was never true
1297 raise ValueError("Parameter 'index' is None.")
1298 elif not isinstance(index, int): 1298 ↛ 1299line 1298 didn't jump to line 1299 because the condition on line 1298 was never true
1299 ex = TypeError("Parameter 'index' is not of type 'int'.")
1300 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.")
1301 raise ex
1302 elif index < 0: 1302 ↛ 1303line 1302 didn't jump to line 1303 because the condition on line 1302 was never true
1303 ex = ValueError("Parameter 'index' is negative.")
1304 ex.add_note(f"Got value '{index}'.")
1305 raise ex
1307 if bins is None:
1308 raise ValueError("Parameter 'bins' is None.")
1309 elif not isinstance(bins, Iterable):
1310 ex = TypeError("Parameter 'bins' is not iterable.")
1311 ex.add_note(f"Got type '{getFullyQualifiedName(bins)}'.")
1312 raise ex
1314 self._file = file
1315 self._lineNumber = lineNumber
1316 self._index = index
1317 self._bins = []
1318 self._count = 0
1319 self._isExcluded = True
1321 for coverBin in bins:
1322 if not isinstance(coverBin, Bin):
1323 ex = TypeError("Parameter 'bins' contains an element not of type 'Bin'.")
1324 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
1325 raise ex
1327 self._bins.append(coverBin)
1328 if not coverBin.IsExcluded:
1329 self._count += coverBin._count
1330 self._isExcluded = False
1332 if len(self._bins) == 0:
1333 raise ValueError("Parameter 'bins' is empty.")
1335 @readonly
1336 def File(self) -> Path:
1337 """
1338 Read-only property to access the path of the source file (:attr:`_file`).
1340 :returns: The path, as the bins' source locations state it.
1341 """
1342 return self._file
1344 @readonly
1345 def LineNumber(self) -> int:
1346 """
1347 Read-only property to access the line number (:attr:`_lineNumber`).
1349 :returns: The line number, counted from 1.
1350 """
1351 return self._lineNumber
1353 @readonly
1354 def Index(self) -> int:
1355 """
1356 Read-only property to access the index of the statement in its line (:attr:`_index`).
1358 :returns: The index, counted from 1.
1359 """
1360 return self._index
1362 @readonly
1363 def Bins(self) -> list[Bin]:
1364 """
1365 Read-only property to access the statement bins of the statement (:attr:`_bins`).
1367 :returns: The bins: one, or of all instances, if merged.
1368 """
1369 return self._bins
1371 @readonly
1372 def Count(self) -> int:
1373 """
1374 Read-only property to access how often the statement ran (:attr:`_count`).
1376 :returns: The sum of the counts of the bins, which aren't excluded.
1377 """
1378 return self._count
1380 @readonly
1381 def IsExcluded(self) -> bool:
1382 """
1383 Read-only property to access whether the statement is excluded from the measurement (:attr:`_isExcluded`).
1385 :returns: ``True``, if all its bins are excluded.
1386 """
1387 return self._isExcluded
1390@export
1391class Report(AttributesMixin, mixin=True):
1392 """
1393 The root element ``<ux:ucdb>``: the tool, which wrote it, its attributes, the history nodes, the scopes and the
1394 commands.
1396 The root of the format's model: a history node, a scope or a command names it as its parent.
1397 """
1399 _tool: Nullable[str] #: Name of the tool, which wrote the report.
1400 _toolVersion: Nullable[CalendarVersion] #: Version of the tool, which wrote the report.
1401 _historyNodes: list[HistoryNode[Report]] #: The history nodes.
1402 _scopes: list[Scope[Report]] #: The top-level scopes.
1403 _commands: list[Command] #: The commands.
1405 def __init__(self) -> None:
1406 """
1407 Initialize an empty report.
1408 """
1409 AttributesMixin.__init__(self)
1411 self._tool = None
1412 self._toolVersion = None
1413 self._historyNodes = []
1414 self._scopes = []
1415 self._commands = []
1417 @readonly
1418 def Tool(self) -> Nullable[str]:
1419 """
1420 Read-only property to access the name of the tool, which wrote the report (:attr:`_tool`).
1422 :returns: The tool's name, e.g. ``Riviera-PRO``; ``None`` before the report was converted.
1423 """
1424 return self._tool
1426 @readonly
1427 def ToolVersion(self) -> Nullable[CalendarVersion]:
1428 """
1429 Read-only property to access the version of the tool, which wrote the report (:attr:`_toolVersion`).
1431 :returns: The tool's version, e.g. ``2022.04``; ``None`` before the report was converted.
1432 """
1433 return self._toolVersion
1435 @readonly
1436 def HistoryNodes(self) -> list[HistoryNode[Report]]:
1437 """
1438 Read-only property to access the history nodes (:attr:`_historyNodes`).
1440 :returns: The history nodes, in the order the report lists them: the tests, or the merge of the tests.
1441 """
1442 return self._historyNodes
1444 @readonly
1445 def Scopes(self) -> list[Scope[Report]]:
1446 """
1447 Read-only property to access the top-level scopes (:attr:`_scopes`).
1449 :returns: The scopes, in the order the report lists them: e.g. the design units and the top-level instances.
1450 """
1451 return self._scopes
1453 @readonly
1454 def Commands(self) -> list[Command]:
1455 """
1456 Read-only property to access the commands (:attr:`_commands`).
1458 :returns: The commands, in the order the report lists them.
1459 """
1460 return self._commands
1462 def IterateStatements(self, mergeInstances: bool = False) -> Generator[Statement, None, None]:
1463 """
1464 Iterate the statements: the statement bins of the instances.
1466 The statement bins are those of the scopes below each top-level scope, which isn't a design unit. A statement is
1467 identified by its file, its line and its index in the line - attribute ``#SINDEX#`` of its bin.
1469 :param mergeInstances: Optional, if true, merge the bins of all instances, which identify the same statement,
1470 into one statement; otherwise each bin is a statement. Default: ``False``.
1471 :returns: A generator of the statements, in the order of their first bin.
1472 :raises ValueError: If parameter ``mergeInstances`` is ``None``.
1473 :raises TypeError: If parameter ``mergeInstances`` isn't of type :class:`bool`.
1474 :raises CodeCoverageError: If a statement bin states no statement index.
1475 """
1476 if mergeInstances is None:
1477 raise ValueError("Parameter 'mergeInstances' is None.")
1478 elif not isinstance(mergeInstances, bool):
1479 ex = TypeError("Parameter 'mergeInstances' is not of type 'bool'.")
1480 ex.add_note(f"Got type '{getFullyQualifiedName(mergeInstances)}'.")
1481 raise ex
1483 designUnits = (
1484 ScopeType.DesignUnitModule, ScopeType.DesignUnitArchitecture, ScopeType.DesignUnitPackage,
1485 ScopeType.DesignUnitProgram, ScopeType.DesignUnitInterface
1486 )
1487 mergedBins: dict[tuple[Path, int, int], list[Bin]] = {}
1488 for scope in self._scopes:
1489 if scope._type in designUnits:
1490 continue
1492 for coverBin in scope.IterateBins():
1493 if coverBin._type is not CoverType.StatementBin:
1494 continue
1496 source = coverBin._source
1497 if not isinstance(index := coverBin._attributes.get("#SINDEX#"), int):
1498 ex = CodeCoverageError(
1499 f"UCDB statement bin at '{source._file.as_posix()}:{source._lineNumber}' states no statement index."
1500 )
1501 ex.add_note("A statement bin states its index in the line by attribute '#SINDEX#' of type 'int'.")
1502 raise ex
1504 if mergeInstances:
1505 mergedBins.setdefault((source._file, source._lineNumber, index), []).append(coverBin)
1506 else:
1507 yield Statement(source._file, source._lineNumber, index, (coverBin, ))
1509 for (file, lineNumber, index), bins in mergedBins.items():
1510 yield Statement(file, lineNumber, index, bins)
1513@export
1514class Document(cc_Document, Report):
1515 """
1516 An Aldec UCDB XML code coverage report: read into the format's model, and converted to the common model.
1517 """
1519 _xmlDocument: Nullable[_ElementTree] #: The parsed and validated XML document, after :meth:`Analyze`.
1521 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False) -> None:
1522 """
1523 Initialize the report, and optionally read it.
1525 :param xmlReportFile: Path to the UCDB XML file.
1526 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
1527 """
1528 super().__init__(xmlReportFile)
1529 Report.__init__(self)
1531 self._xmlDocument = None
1533 if analyzeAndConvert:
1534 self.Analyze()
1535 self.Convert()
1537 def Analyze(self) -> None:
1538 """
1539 Parse the XML file, check its root element, and validate the file against the XML schema (:data:`SCHEMA`).
1541 :raises CodeCoverageError: If the file doesn't exist.
1542 :raises CodeCoverageError: If the file can't be read.
1543 :raises CodeCoverageError: If the file isn't well-formed XML.
1544 :raises CodeCoverageError: If the root element isn't ``<ux:ucdb>`` of namespace ``www.aldec.com``.
1545 :raises CodeCoverageError: If the XML schema can't be located or parsed.
1546 :raises CodeCoverageError: If the file isn't valid according to the XML schema.
1547 """
1548 if not self._path.exists():
1549 raise CodeCoverageError(f"Aldec UCDB file '{self._path}' does not exist.") \
1550 from FileNotFoundError(f"File '{self._path}' not found.")
1552 with Stopwatch() as sw:
1553 try:
1554 content = self._path.read_bytes()
1555 except OSError as ex:
1556 raise CodeCoverageError(f"Couldn't read Aldec UCDB file '{self._path}'.") from ex
1558 try:
1559 xmlDocument = parse(BytesIO(content), XMLParser(ns_clean=True), base_url=str(self._path))
1560 except XMLSyntaxError as ex:
1561 raise CodeCoverageError(f"XML syntax error in Aldec UCDB file '{self._path}'.") from ex
1563 rootElement: _Element = xmlDocument.getroot()
1564 if rootElement.tag != f"{{{NAMESPACE}}}ucdb":
1565 ex = CodeCoverageError(f"Root element of '{self._path}' is not '<ux:ucdb>' of namespace '{NAMESPACE}'.")
1566 ex.add_note(f"Got root element '<{rootElement.tag}>'.")
1567 raise ex
1569 try:
1570 schemaResourceFile = getResourceFile(Resources, SCHEMA)
1571 except ToolingException as ex:
1572 raise CodeCoverageError(f"Couldn't locate XML Schema '{SCHEMA}' in package resources.") from ex
1574 try:
1575 xmlSchema = XMLSchema(parse(schemaResourceFile, XMLParser(ns_clean=True)))
1576 except (XMLSyntaxError, XMLSchemaParseError) as ex:
1577 raise CodeCoverageError(f"Error while parsing XML Schema '{SCHEMA}'.") from ex
1579 if not xmlSchema.validate(xmlDocument):
1580 ex = CodeCoverageError(f"Validation error for '{self._path}' using XSD schema '{SCHEMA}'.")
1581 for logEntry in xmlSchema.error_log:
1582 ex.add_note(str(logEntry))
1583 raise ex
1585 self._xmlDocument = xmlDocument
1587 self._analysisDuration = sw.Duration
1589 def Convert(self) -> None:
1590 """
1591 Convert the parsed and validated XML document to the format's model.
1593 :raises CodeCoverageError: If the XML file was not analyzed before. |br|
1594 Call 'Document.Analyze()' or create the document using
1595 'Document(path, analyzeAndConvert=True)'.
1596 :raises CodeCoverageError: If an attribute's value isn't of the type the attribute states.
1597 """
1598 if self._xmlDocument is None:
1599 ex = CodeCoverageError(f"Aldec UCDB file '{self._path}' needs to be read and analyzed by an XML parser.")
1600 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
1601 raise ex
1603 with Stopwatch() as sw:
1604 root: _Element = self._xmlDocument.getroot()
1605 tool, _, version = root.attrib["version"].rpartition(" ")
1606 self._tool = tool
1607 self._toolVersion = CalendarVersion.Parse(version)
1608 self._attributes = self._ParseAttributes(root)
1610 for historyNodeElement in root.iterfind("ux:hnode", NAMESPACES):
1611 HistoryNode.Parse(historyNodeElement, parent=self)
1613 for scopeElement in root.iterfind("ux:scope", NAMESPACES):
1614 Scope.Parse(scopeElement, parent=self)
1616 for commandElement in root.iterfind("ux:command", NAMESPACES):
1617 Command.Parse(commandElement, parent=self)
1619 self._conversionDuration = sw.Duration
1621 def ToCoverageSummary(self, mergeInstances: bool = False) -> CoverageSummary:
1622 """
1623 Convert the statement coverage of the format's model to the common model, and aggregate it.
1625 A line ran as often as its least often run statement (:meth:`~Report.IterateStatements`): it is covered, if all its
1626 statements ran, otherwise uncovered. A line, whose statements are all excluded, is excluded.
1628 :param mergeInstances: Optional, if true, merge the statements of all instances of a design unit. Default:
1629 ``False``.
1630 :returns: The report's root of the common model, named after the report file. Its source
1631 directories are the directories the tool ran in.
1632 :raises ValueError: If parameter ``mergeInstances`` is ``None``.
1633 :raises TypeError: If parameter ``mergeInstances`` isn't of type :class:`bool`.
1634 :raises CodeCoverageError: If a statement bin states no statement index.
1635 :raises CodeCoverageError: If a file's path runs through another file.
1636 """
1637 statementsPerFile: dict[Path, dict[int, list[Statement]]] = {}
1638 sourceDirectories: dict[Path, None] = {}
1639 for statement in self.IterateStatements(mergeInstances):
1640 statementsPerFile.setdefault(statement._file, {}).setdefault(statement._lineNumber, []).append(statement)
1641 for coverBin in statement._bins:
1642 sourceDirectories[coverBin._source._workDirectory] = None
1644 summary = CoverageSummary(self._path.stem, sourceDirectories=sourceDirectories)
1645 for path, statementsPerLine in statementsPerFile.items():
1646 file = summary.GetOrAddFile(path)
1647 for lineNumber in sorted(statementsPerLine):
1648 counts = [statement._count for statement in statementsPerLine[lineNumber] if not statement._isExcluded]
1649 if len(counts) == 0:
1650 cc_Line(lineNumber, LineCoverageStatus.Excluded, parent=file)
1651 else:
1652 count = min(counts)
1653 status = LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered
1654 cc_Line(lineNumber, status, count, parent=file)
1656 summary.Aggregate()
1657 return summary