Coverage for pyEDAA/Reports/CodeCoverage/QuestaSim.py: 97%
949 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"""
32QuestaSim's coverage report XML: a model of the format, read from a report and converted to the common model.
34Questa SIM keeps coverage in a coverage database (UCDB), whose format is proprietary. The command
35``vcover report -xml -details`` - or ``coverage report -xml -details`` in a simulation - writes a report of it as XML,
36with the coverage items of each instance:
38.. code-block:: bash
40 vcover report -xml -details -output CoverageReport.xml Testsuite.ucdb
42The XML's root is ``<coverage_report>``, without namespace and without format version; it isn't the XML interchange
43format of the UCIS standard. Questa states its version - e.g. ``2026.2`` - and the command, which wrote the report. A
44report is validated against :file:`QuestaSim-Coverage.xsd` (:data:`SCHEMA`).
46The format's model keeps what the report states: a :class:`Document` holds the report's mode (:class:`ReportMode`) and
47its scopes - instances, design units or source files (:class:`Scope`) -, each with its coverage statistics
48(:class:`Statistics`), its source files and its coverage items: the statements, the ``if`` and ``case`` statements with
49their branches, and the states and transitions of finite state machines. Each element's constructor takes typed values,
50so the model can be built by hand: an element below the report names its parent with the keyword parameter ``parent``
51and is added to it. Its class method ``Parse`` reads the element's XML element.
53* A coverage statistics element states how many coverage items of a kind - e.g. statements or branches - a scope or a
54 summary record (:class:`Totals`) has, and how many of them were hit.
55* Which kind of scope a report lists, its mode decides: :class:`InstanceData`, :class:`DesignUnitData` or
56 :class:`FileData`. With details (``vcover report -details``), a scope names its source files in a source table and
57 lists its coverage items.
58* ``-code`` chooses the kinds of coverage items, e.g. ``-code bcesf`` for branches, conditions, expressions, statements
59 and finite state machines. A coverage item - :class:`Statement`, :class:`IfBranch`, :class:`CaseBranch` - names its
60 source file by the number the scope's source table gives it, its line, and its index among the items of its kind in
61 that line.
62* Unlike the other coverage items, a state (:class:`FSMState`) and a transition (:class:`FSMTransition`) of a finite
63 state machine name no file number: their file is the scope's, if the scope has a single file.
65:meth:`Document.ToCoverageSummary` converts the statements and branches to the common model of
66:mod:`pyEDAA.Reports.CodeCoverage`:
68* Coverage items are identified by their file, their line and their index in the line. The items of all scopes are
69 merged: an item of a design unit with several instances counts as often as in all instances together.
70* A line ran as often as its least often run statement or branching statement. An ``if`` or ``case`` statement adds
71 its branches to the line of its first branch - the ``if``, or the first case item -, each taken as often as Questa
72 counted.
73* A line, which ran, is covered, if all its branches were taken, otherwise partially covered.
75The coverage statistics, the states and transitions of finite state machines, conditions, expressions and toggles
76aren't converted.
78.. rubric:: Example
80.. code-block:: Python
82 from pathlib import Path
83 from pyEDAA.Reports.CodeCoverage.QuestaSim import Document
85 report = Document(Path("CoverageReport.xml"), analyzeAndConvert=True)
86 summary = report.ToCoverageSummary()
87 print(f"Lines: {summary.CoveredLines} of {summary.TotalLines}")
88 print(f"Branches: {summary.CoveredBranches} of {summary.TotalBranches}")
89"""
90from __future__ import annotations
92from io import BytesIO
93from pathlib import Path
94from typing import Generator, Mapping, Optional as Nullable, Self
96from lxml.etree import XMLParser, XMLSchema, XMLSchemaParseError, XMLSyntaxError
97from lxml.etree import _Element, _ElementTree, parse
98from pyTooling.Common import getFullyQualifiedName, getResourceFile, StringEnum
99from pyTooling.Decorators import export, readonly
100from pyTooling.Exceptions import ToolingException
101from pyTooling.MetaClasses import ExtendedType
102from pyTooling.Stopwatch import Stopwatch
104from pyEDAA.Reports import Resources
105from pyEDAA.Reports.CodeCoverage import CodeCoverageError, CoverageSummary, Document as cc_Document
106from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, Line as cc_Line, LineCoverageStatus
109__all__ = ["SCHEMA"]
111SCHEMA = "QuestaSim-Coverage.xsd" #: The XML schema a report is validated against.
113# A class with a property named like a class - ``Path``, ``Totals`` - can't name that class in the annotation of a
114# field: the class body's namespace, where annotations are evaluated, binds the name to the property.
115_Path = Path
118@export
119class ReportMode(StringEnum):
120 """
121 How a report groups its coverage data, as the attribute of ``<code_coverage_report>`` set to ``1`` states.
122 """
124 ByInstance = "byInstance" #: Per instance (``<instanceData>``), ``vcover report -setdefault byinstance``.
125 ByDesignUnit = "byDU" #: Per design unit (``<DuData>``), ``vcover report -setdefault bydu``.
126 ByFile = "byFile" #: Per source file (``<fileData>``), ``vcover report -setdefault byfile``.
129@export
130class CoverageKind(StringEnum):
131 """
132 Kind of coverage items a coverage statistics element counts, as its element name states.
133 """
135 Statements = "statements" #: Statements.
136 Branches = "branches" #: Branches of ``if`` and ``case`` statements.
137 Conditions = "conditions" #: Conditions.
138 Expressions = "expressions" #: Expressions.
139 States = "states" #: States of finite state machines.
140 Transitions = "transitions" #: Transitions of finite state machines.
141 Toggles = "toggleSummary" #: Signal toggles.
144@export
145class Statistics(metaclass=ExtendedType, slots=True):
146 """
147 A coverage statistics element of a scope or a summary record: how many coverage items of a kind it has, and how many
148 of them were hit.
150 The element is named after the kind of coverage items, e.g. ``<statements>`` or ``<branches>``.
151 """
153 _parent: Nullable[StatisticsMixin] #: The scope or summary record the coverage statistics belong to.
154 _kind: CoverageKind #: Kind of the coverage items.
155 _active: int #: Number of coverage items.
156 _hits: int #: Number of coverage items, which were hit.
157 _percent: float #: Percentage of coverage items, which were hit, as the report states it.
159 def __init__(
160 self,
161 kind: CoverageKind,
162 active: int,
163 hits: int,
164 percent: float,
165 *,
166 parent: Nullable[StatisticsMixin] = None
167 ) -> None:
168 """
169 Initialize the coverage statistics, and add them to the coverage statistics of their element.
171 :param kind: Kind of the coverage items.
172 :param active: Number of coverage items.
173 :param hits: Number of coverage items, which were hit.
174 :param percent: Percentage of coverage items, which were hit, as the report states it.
175 :param parent: Optional, the scope or summary record the coverage statistics belong to; they are added
176 to its coverage statistics by :attr:`Kind`. Default: ``None``.
177 :raises ValueError: If parameter ``kind`` is ``None``.
178 :raises TypeError: If parameter ``kind`` isn't of type :class:`CoverageKind`.
179 :raises ValueError: If parameter ``active`` is ``None``.
180 :raises TypeError: If parameter ``active`` isn't of type :class:`int`.
181 :raises ValueError: If parameter ``active`` is negative.
182 :raises ValueError: If parameter ``hits`` is ``None``.
183 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`.
184 :raises ValueError: If parameter ``hits`` is negative or greater than parameter ``active``.
185 :raises ValueError: If parameter ``percent`` is ``None``.
186 :raises TypeError: If parameter ``percent`` isn't of type :class:`float` or :class:`int`.
187 :raises ValueError: If parameter ``percent`` is out of range 0..100.
188 :raises TypeError: If parameter ``parent`` isn't of type :class:`StatisticsMixin`.
189 :raises CodeCoverageError: If the parent already has coverage statistics of this kind.
190 """
191 if kind is None:
192 raise ValueError("Parameter 'kind' is None.")
193 elif not isinstance(kind, CoverageKind):
194 ex = TypeError("Parameter 'kind' is not of type 'CoverageKind'.")
195 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
196 raise ex
198 if active is None:
199 raise ValueError("Parameter 'active' is None.")
200 elif not isinstance(active, int):
201 ex = TypeError("Parameter 'active' is not of type 'int'.")
202 ex.add_note(f"Got type '{getFullyQualifiedName(active)}'.")
203 raise ex
204 elif active < 0:
205 ex = ValueError("Parameter 'active' is negative.")
206 ex.add_note(f"Got value '{active}'.")
207 raise ex
209 if hits is None:
210 raise ValueError("Parameter 'hits' is None.")
211 elif not isinstance(hits, int):
212 ex = TypeError("Parameter 'hits' is not of type 'int'.")
213 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.")
214 raise ex
215 elif not (0 <= hits <= active):
216 ex = ValueError(f"Parameter 'hits' is out of range 0..{active}.")
217 ex.add_note(f"Got value '{hits}'.")
218 raise ex
220 if percent is None:
221 raise ValueError("Parameter 'percent' is None.")
222 elif not isinstance(percent, (float, int)):
223 ex = TypeError("Parameter 'percent' is not of type 'float' or 'int'.")
224 ex.add_note(f"Got type '{getFullyQualifiedName(percent)}'.")
225 raise ex
226 elif not (0.0 <= percent <= 100.0):
227 ex = ValueError("Parameter 'percent' is out of range 0..100.")
228 ex.add_note(f"Got value '{percent}'.")
229 raise ex
231 if parent is not None:
232 if not isinstance(parent, StatisticsMixin):
233 ex = TypeError("Parameter 'parent' is not of type 'StatisticsMixin'.")
234 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
235 raise ex
236 elif kind in parent._statistics:
237 raise CodeCoverageError(f"Coverage statistics '{kind}' are stated twice.")
239 self._parent = parent
240 self._kind = kind
241 self._active = active
242 self._hits = hits
243 self._percent = float(percent)
245 if parent is not None:
246 parent._statistics[kind] = self
248 @classmethod
249 def Parse(cls, element: _Element, *, parent: Nullable[StatisticsMixin] = None) -> Self:
250 """
251 Parse coverage statistics from their element.
253 :param element: The coverage statistics element.
254 :param parent: Optional, the scope or summary record the coverage statistics belong to. Default:
255 ``None``.
256 :returns: The coverage statistics.
257 :raises CodeCoverageError: If the element states more hit items than items.
258 :raises CodeCoverageError: If the parent already has coverage statistics of this kind.
259 """
260 active = int(element.attrib["active"])
261 hits = int(element.attrib["hits"])
262 if hits > active:
263 ex = CodeCoverageError(f"Coverage statistics '<{element.tag}>' state more hit items than items.")
264 ex.add_note(f"Got '{hits}' hit items of '{active}' items in line {element.sourceline}.")
265 raise ex
267 return cls(CoverageKind(element.tag), active, hits, float(element.attrib["percent"]), parent=parent)
269 @readonly
270 def Parent(self) -> Nullable[StatisticsMixin]:
271 """
272 Read-only property to access the scope or summary record the coverage statistics belong to (:attr:`_parent`).
274 :returns: The scope or summary record; ``None`` if the coverage statistics belong to none.
275 """
276 return self._parent
278 @readonly
279 def Kind(self) -> CoverageKind:
280 """
281 Read-only property to access the kind of the coverage items (:attr:`_kind`).
283 :returns: The kind of the coverage items.
284 """
285 return self._kind
287 @readonly
288 def Active(self) -> int:
289 """
290 Read-only property to access the number of coverage items (:attr:`_active`).
292 :returns: The number of coverage items.
293 """
294 return self._active
296 @readonly
297 def Hits(self) -> int:
298 """
299 Read-only property to access the number of coverage items, which were hit (:attr:`_hits`).
301 :returns: The number of hit coverage items.
302 """
303 return self._hits
305 @readonly
306 def Percent(self) -> float:
307 """
308 Read-only property to access the percentage of coverage items, which were hit, as the report states it
309 (:attr:`_percent`).
311 Questa truncates the percentage to two decimals: 66 of 74 items are ``89.18`` percent.
313 :returns: The percentage, from 0 to 100.
314 """
315 return self._percent
318@export
319class StatisticsMixin(metaclass=ExtendedType, mixin=True):
320 """
321 A mixin-class adding the coverage statistics an element states: per kind of coverage items, how many it has and how
322 many were hit.
323 """
325 _statistics: dict[CoverageKind, Statistics] #: The coverage statistics, by kind.
327 def __init__(self) -> None:
328 """
329 Initialize the coverage statistics to none.
331 Coverage statistics are added by creating them with the element as their parent.
332 """
333 self._statistics = {}
335 def _ParseStatistics(self, element: _Element) -> None:
336 """
337 Parse the coverage statistics elements below an element, with this element as their parent.
339 :param element: The element holding the coverage statistics elements.
340 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
341 :raises CodeCoverageError: If the element states a kind of coverage statistics twice.
342 """
343 kinds = [kind.value for kind in CoverageKind]
344 for child in element:
345 if child.tag in kinds:
346 Statistics.Parse(child, parent=self)
348 @readonly
349 def Statistics(self) -> dict[CoverageKind, Statistics]:
350 """
351 Read-only property to access the coverage statistics (:attr:`_statistics`).
353 :returns: The coverage statistics, by kind; a kind the element doesn't state has none.
354 """
355 return self._statistics
358@export
359class SourceItem(metaclass=ExtendedType, slots=True):
360 """
361 Base-class of the coverage items, which name where they are in the sources: a file, a line and an index in the line.
362 """
364 _file: Path #: Path of the source file, as the report states it.
365 _lineNumber: int #: Line number, counted from 1.
366 _index: int #: Index of the item among the items of its kind in its line, counted from 1.
368 def __init__(self, file: Path, lineNumber: int, index: int) -> None:
369 """
370 Initialize where the coverage item is in the sources.
372 :param file: Path of the source file, as the report states it.
373 :param lineNumber: Line number, counted from 1.
374 :param index: Index of the item among the items of its kind in its line, counted from 1.
375 :raises ValueError: If parameter ``file`` is ``None``.
376 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
377 :raises ValueError: If parameter ``lineNumber`` is ``None``.
378 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
379 :raises ValueError: If parameter ``lineNumber`` is less than 1.
380 :raises ValueError: If parameter ``index`` is ``None``.
381 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
382 :raises ValueError: If parameter ``index`` is less than 1.
383 """
384 if file is None:
385 raise ValueError("Parameter 'file' is None.")
386 elif not isinstance(file, Path):
387 ex = TypeError("Parameter 'file' is not of type 'Path'.")
388 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
389 raise ex
391 if lineNumber is None:
392 raise ValueError("Parameter 'lineNumber' is None.")
393 elif not isinstance(lineNumber, int):
394 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
395 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
396 raise ex
397 elif lineNumber < 1:
398 ex = ValueError("Parameter 'lineNumber' is less than 1.")
399 ex.add_note(f"Got value '{lineNumber}'.")
400 raise ex
402 if index is None:
403 raise ValueError("Parameter 'index' is None.")
404 elif not isinstance(index, int):
405 ex = TypeError("Parameter 'index' is not of type 'int'.")
406 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.")
407 raise ex
408 elif index < 1:
409 ex = ValueError("Parameter 'index' is less than 1.")
410 ex.add_note(f"Got value '{index}'.")
411 raise ex
413 self._file = file
414 self._lineNumber = lineNumber
415 self._index = index
417 @readonly
418 def File(self) -> Path:
419 """
420 Read-only property to access the path of the source file (:attr:`_file`).
422 :returns: The path, as the report states it.
423 """
424 return self._file
426 @readonly
427 def LineNumber(self) -> int:
428 """
429 Read-only property to access the line number (:attr:`_lineNumber`).
431 :returns: The line number, counted from 1.
432 """
433 return self._lineNumber
435 @readonly
436 def Index(self) -> int:
437 """
438 Read-only property to access the index of the item among the items of its kind in its line (:attr:`_index`).
440 :returns: The index, counted from 1.
441 """
442 return self._index
445@export
446class Statement(SourceItem):
447 """
448 A ``<stmt>`` of a scope: a statement, where it is in the sources, and how often it ran.
449 """
451 _parent: Nullable[Scope] #: The scope the statement belongs to.
452 _hits: int #: How often the statement ran.
454 def __init__(self, file: Path, lineNumber: int, index: int, hits: int, *, parent: Nullable[Scope] = None) -> None:
455 """
456 Initialize the statement, and append it to the statements of its scope.
458 :param file: Path of the source file, as the report states it.
459 :param lineNumber: Line number, counted from 1.
460 :param index: Index of the statement in its line, counted from 1.
461 :param hits: How often the statement ran.
462 :param parent: Optional, the scope the statement belongs to; the statement is appended to its statements.
463 Default: ``None``.
464 :raises ValueError: If parameter ``file`` is ``None``.
465 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
466 :raises ValueError: If parameter ``lineNumber`` is ``None``.
467 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
468 :raises ValueError: If parameter ``lineNumber`` is less than 1.
469 :raises ValueError: If parameter ``index`` is ``None``.
470 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
471 :raises ValueError: If parameter ``index`` is less than 1.
472 :raises ValueError: If parameter ``hits`` is ``None``.
473 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`.
474 :raises ValueError: If parameter ``hits`` is negative.
475 :raises TypeError: If parameter ``parent`` isn't of type
476 :class:`Scope`.
477 """
478 super().__init__(file, lineNumber, index)
480 if hits is None:
481 raise ValueError("Parameter 'hits' is None.")
482 elif not isinstance(hits, int):
483 ex = TypeError("Parameter 'hits' is not of type 'int'.")
484 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.")
485 raise ex
486 elif hits < 0:
487 ex = ValueError("Parameter 'hits' is negative.")
488 ex.add_note(f"Got value '{hits}'.")
489 raise ex
491 if parent is not None and not isinstance(parent, Scope):
492 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
493 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
494 raise ex
496 self._parent = parent
497 self._hits = hits
499 if parent is not None: 499 ↛ exitline 499 didn't return from function '__init__' because the condition on line 499 was always true
500 parent._statements.append(self)
502 @classmethod
503 def Parse(cls, element: _Element, *, parent: Scope) -> Self:
504 """
505 Parse a statement from its ``<stmt>`` element; its file is the one its file number names in the scope's source
506 table.
508 :param element: The ``<stmt>`` element.
509 :param parent: The scope the statement belongs to.
510 :returns: The statement.
511 :raises CodeCoverageError: If the statement's file number isn't in the scope's source table.
512 :raises CodeCoverageError: If the statement states no file number and its scope has no single file.
513 """
514 return cls(
515 parent._ResolveFile(element),
516 int(element.attrib["ln"]),
517 int(element.attrib["st"]),
518 int(element.attrib["hits"]),
519 parent=parent
520 )
522 @readonly
523 def Parent(self) -> Nullable[Scope]:
524 """
525 Read-only property to access the scope the statement belongs to (:attr:`_parent`).
527 :returns: The scope; ``None`` if the statement belongs to no scope.
528 """
529 return self._parent
531 @readonly
532 def Hits(self) -> int:
533 """
534 Read-only property to access how often the statement ran (:attr:`_hits`).
536 :returns: The count.
537 """
538 return self._hits
541@export
542class IfBranch(SourceItem):
543 """
544 An ``<ielem>`` of an ``if`` statement: a branch - ``if``, ``elsif``, ``else`` or the implicit *AllFalse* branch -, how
545 often it was taken, and how often its condition was false.
546 """
548 _parent: Nullable[IfStatement] #: The ``if`` statement the branch belongs to.
549 _trueCount: int #: How often the branch was taken.
550 _falseCount: int #: How often the branch's condition was false: the evaluation went on.
552 def __init__(
553 self,
554 file: Path,
555 lineNumber: int,
556 index: int,
557 trueCount: int,
558 falseCount: int,
559 *,
560 parent: Nullable[IfStatement] = None
561 ) -> None:
562 """
563 Initialize the branch, and append it to the branches of its ``if`` statement.
565 :param file: Path of the source file, as the report states it.
566 :param lineNumber: Line number of the branch's condition, counted from 1.
567 :param index: Index of the ``if`` statement in its line, counted from 1.
568 :param trueCount: How often the branch was taken.
569 :param falseCount: How often the branch's condition was false; ``0`` for an ``else`` or *AllFalse* branch.
570 :param parent: Optional, the ``if`` statement the branch belongs to. Default: ``None``.
571 :raises ValueError: If parameter ``file`` is ``None``.
572 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
573 :raises ValueError: If parameter ``lineNumber`` is ``None``.
574 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
575 :raises ValueError: If parameter ``lineNumber`` is less than 1.
576 :raises ValueError: If parameter ``index`` is ``None``.
577 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
578 :raises ValueError: If parameter ``index`` is less than 1.
579 :raises ValueError: If parameter ``trueCount`` is ``None``.
580 :raises TypeError: If parameter ``trueCount`` isn't of type :class:`int`.
581 :raises ValueError: If parameter ``trueCount`` is negative.
582 :raises ValueError: If parameter ``falseCount`` is ``None``.
583 :raises TypeError: If parameter ``falseCount`` isn't of type :class:`int`.
584 :raises ValueError: If parameter ``falseCount`` is negative.
585 :raises TypeError: If parameter ``parent`` isn't of type :class:`IfStatement`.
586 """
587 super().__init__(file, lineNumber, index)
589 if trueCount is None:
590 raise ValueError("Parameter 'trueCount' is None.")
591 elif not isinstance(trueCount, int):
592 ex = TypeError("Parameter 'trueCount' is not of type 'int'.")
593 ex.add_note(f"Got type '{getFullyQualifiedName(trueCount)}'.")
594 raise ex
595 elif trueCount < 0:
596 ex = ValueError("Parameter 'trueCount' is negative.")
597 ex.add_note(f"Got value '{trueCount}'.")
598 raise ex
600 if falseCount is None:
601 raise ValueError("Parameter 'falseCount' is None.")
602 elif not isinstance(falseCount, int):
603 ex = TypeError("Parameter 'falseCount' is not of type 'int'.")
604 ex.add_note(f"Got type '{getFullyQualifiedName(falseCount)}'.")
605 raise ex
606 elif falseCount < 0:
607 ex = ValueError("Parameter 'falseCount' is negative.")
608 ex.add_note(f"Got value '{falseCount}'.")
609 raise ex
611 if parent is not None and not isinstance(parent, IfStatement):
612 ex = TypeError("Parameter 'parent' is not of type 'IfStatement'.")
613 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
614 raise ex
616 self._parent = parent
617 self._trueCount = trueCount
618 self._falseCount = falseCount
620 if parent is not None: 620 ↛ exitline 620 didn't return from function '__init__' because the condition on line 620 was always true
621 parent._branches.append(self)
623 @classmethod
624 def Parse(cls, element: _Element, *, parent: IfStatement) -> Self:
625 """
626 Parse a branch from its ``<ielem>`` element.
628 :param element: The ``<ielem>`` element.
629 :param parent: The ``if`` statement the branch belongs to; it belongs to a scope.
630 :returns: The branch.
631 :raises CodeCoverageError: If the branch's file number isn't in the scope's source table, or it states none and
632 the scope has no single file.
633 """
634 return cls(
635 parent._parent._ResolveFile(element),
636 int(element.attrib["ln"]),
637 int(element.attrib["st"]),
638 int(element.attrib["true"]),
639 int(element.attrib["false"]),
640 parent=parent
641 )
643 @readonly
644 def Parent(self) -> Nullable[IfStatement]:
645 """
646 Read-only property to access the ``if`` statement the branch belongs to (:attr:`_parent`).
648 :returns: The ``if`` statement; ``None`` if the branch belongs to none.
649 """
650 return self._parent
652 @readonly
653 def TrueCount(self) -> int:
654 """
655 Read-only property to access how often the branch was taken (:attr:`_trueCount`).
657 :returns: The count.
658 """
659 return self._trueCount
661 @readonly
662 def FalseCount(self) -> int:
663 """
664 Read-only property to access how often the branch's condition was false (:attr:`_falseCount`).
666 Then the evaluation went on to the next branch: the count is the sum of the next branches' true counts.
668 :returns: The count; ``0`` for an ``else`` or *AllFalse* branch.
669 """
670 return self._falseCount
673@export
674class IfStatement(metaclass=ExtendedType, slots=True):
675 """
676 An ``<if>`` of a scope: an ``if`` statement - with its ``elsif`` and ``else`` branches - and how often each branch was
677 taken.
679 Each branch is an ``<ielem>`` (:class:`IfBranch`). An ``if`` statement without ``else`` has an implicit branch, the
680 *AllFalse* branch, taken whenever no condition was true; Questa lists it as the last ``<ielem>``, in the line of the
681 ``if``.
682 """
684 _parent: Nullable[Scope] #: The scope the ``if`` statement belongs to.
685 _hasElse: bool #: Whether the ``if`` statement has an ``else`` branch.
686 _branches: list[IfBranch] #: The branches, in the order of their conditions.
688 def __init__(self, hasElse: bool, *, parent: Nullable[Scope] = None) -> None:
689 """
690 Initialize the ``if`` statement, and append it to the ``if`` statements of its scope.
692 Its branches are added by creating them with this ``if`` statement as their parent.
694 :param hasElse: Whether the ``if`` statement has an ``else`` branch; otherwise its last branch is the implicit
695 *AllFalse* branch.
696 :param parent: Optional, the scope the ``if`` statement belongs to. Default: ``None``.
697 :raises ValueError: If parameter ``hasElse`` is ``None``.
698 :raises TypeError: If parameter ``hasElse`` isn't of type :class:`bool`.
699 :raises TypeError: If parameter ``parent`` isn't of type
700 :class:`Scope`.
701 """
702 if hasElse is None:
703 raise ValueError("Parameter 'hasElse' is None.")
704 elif not isinstance(hasElse, bool):
705 ex = TypeError("Parameter 'hasElse' is not of type 'bool'.")
706 ex.add_note(f"Got type '{getFullyQualifiedName(hasElse)}'.")
707 raise ex
709 if parent is not None and not isinstance(parent, Scope):
710 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
711 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
712 raise ex
714 self._parent = parent
715 self._hasElse = hasElse
716 self._branches = []
718 if parent is not None:
719 parent._ifStatements.append(self)
721 @classmethod
722 def Parse(cls, element: _Element, *, parent: Scope) -> Self:
723 """
724 Parse an ``if`` statement and its branches from its ``<if>`` element.
726 :param element: The ``<if>`` element.
727 :param parent: The scope the ``if`` statement belongs to.
728 :returns: The ``if`` statement.
729 :raises CodeCoverageError: If a branch's file number isn't in the scope's source table, or it states none and the
730 scope has no single file.
731 :raises CodeCoverageError: If the ``if`` statement states other numbers of branches and taken branches than its
732 branches have.
733 """
734 ifStatement = cls(element.attrib["hasElse"] == "1", parent=parent)
735 for branchElement in element.iterfind("ielem"):
736 IfBranch.Parse(branchElement, parent=ifStatement)
738 active = int(element.attrib["active"])
739 hits = int(element.attrib["hits"])
740 if active != len(ifStatement._branches) or hits != ifStatement.TakenBranches:
741 ex = CodeCoverageError(f"'<if>' in line {element.sourceline} states other figures than its branches have.")
742 ex.add_note(f"Got '{hits}' of '{active}' branches taken, its branches have {ifStatement.TakenBranches} of "
743 f"{len(ifStatement._branches)}.")
744 raise ex
746 return ifStatement
748 @readonly
749 def Parent(self) -> Nullable[Scope]:
750 """
751 Read-only property to access the scope the ``if`` statement belongs to (:attr:`_parent`).
753 :returns: The scope; ``None`` if the ``if`` statement belongs to no scope.
754 """
755 return self._parent
757 @readonly
758 def HasElse(self) -> bool:
759 """
760 Read-only property to access whether the ``if`` statement has an ``else`` branch (:attr:`_hasElse`).
762 :returns: ``True``, if it has an ``else`` branch; otherwise its last branch is the implicit *AllFalse* branch.
763 """
764 return self._hasElse
766 @readonly
767 def Branches(self) -> list[IfBranch]:
768 """
769 Read-only property to access the branches (:attr:`_branches`).
771 :returns: The branches, in the order of their conditions; the ``else`` or *AllFalse* branch last.
772 """
773 return self._branches
775 @readonly
776 def TakenBranches(self) -> int:
777 """
778 Read-only property to return the number of branches, which were taken.
780 :returns: The number of branches with a positive :attr:`IfBranch.TrueCount`.
781 """
782 return sum(1 for branch in self._branches if branch._trueCount > 0)
784 @readonly
785 def EvaluationCount(self) -> int:
786 """
787 Read-only property to return how often the ``if`` statement was evaluated.
789 Each evaluation takes exactly one branch, so it is the sum of the branches' :attr:`IfBranch.TrueCount`.
791 :returns: The number of evaluations.
792 """
793 return sum(branch._trueCount for branch in self._branches)
796@export
797class CaseBranch(SourceItem):
798 """
799 A ``<celem>`` of a ``case`` statement: a case item, and how often it was taken.
800 """
802 _parent: Nullable[CaseStatement] #: The ``case`` statement the branch belongs to.
803 _hits: int #: How often the branch was taken.
805 def __init__(
806 self,
807 file: Path,
808 lineNumber: int,
809 index: int,
810 hits: int,
811 *,
812 parent: Nullable[CaseStatement] = None
813 ) -> None:
814 """
815 Initialize the branch, and append it to the branches of its ``case`` statement.
817 :param file: Path of the source file, as the report states it.
818 :param lineNumber: Line number of the case item, counted from 1.
819 :param index: Index of the case item in its line, counted from 1.
820 :param hits: How often the branch was taken.
821 :param parent: Optional, the ``case`` statement the branch belongs to. Default: ``None``.
822 :raises ValueError: If parameter ``file`` is ``None``.
823 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
824 :raises ValueError: If parameter ``lineNumber`` is ``None``.
825 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
826 :raises ValueError: If parameter ``lineNumber`` is less than 1.
827 :raises ValueError: If parameter ``index`` is ``None``.
828 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
829 :raises ValueError: If parameter ``index`` is less than 1.
830 :raises ValueError: If parameter ``hits`` is ``None``.
831 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`.
832 :raises ValueError: If parameter ``hits`` is negative.
833 :raises TypeError: If parameter ``parent`` isn't of type :class:`CaseStatement`.
834 """
835 super().__init__(file, lineNumber, index)
837 if hits is None:
838 raise ValueError("Parameter 'hits' is None.")
839 elif not isinstance(hits, int):
840 ex = TypeError("Parameter 'hits' is not of type 'int'.")
841 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.")
842 raise ex
843 elif hits < 0:
844 ex = ValueError("Parameter 'hits' is negative.")
845 ex.add_note(f"Got value '{hits}'.")
846 raise ex
848 if parent is not None and not isinstance(parent, CaseStatement):
849 ex = TypeError("Parameter 'parent' is not of type 'CaseStatement'.")
850 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
851 raise ex
853 self._parent = parent
854 self._hits = hits
856 if parent is not None: 856 ↛ exitline 856 didn't return from function '__init__' because the condition on line 856 was always true
857 parent._branches.append(self)
859 @classmethod
860 def Parse(cls, element: _Element, *, parent: CaseStatement) -> Self:
861 """
862 Parse a branch from its ``<celem>`` element.
864 :param element: The ``<celem>`` element.
865 :param parent: The ``case`` statement the branch belongs to; it belongs to a scope.
866 :returns: The branch.
867 :raises CodeCoverageError: If the branch's file number isn't in the scope's source table, or it states none and
868 the scope has no single file.
869 """
870 return cls(
871 parent._parent._ResolveFile(element),
872 int(element.attrib["ln"]),
873 int(element.attrib["st"]),
874 int(element.attrib["hits"]),
875 parent=parent
876 )
878 @readonly
879 def Parent(self) -> Nullable[CaseStatement]:
880 """
881 Read-only property to access the ``case`` statement the branch belongs to (:attr:`_parent`).
883 :returns: The ``case`` statement; ``None`` if the branch belongs to none.
884 """
885 return self._parent
887 @readonly
888 def Hits(self) -> int:
889 """
890 Read-only property to access how often the branch was taken (:attr:`_hits`).
892 :returns: The count.
893 """
894 return self._hits
897@export
898class CaseStatement(metaclass=ExtendedType, slots=True):
899 """
900 A ``<case>`` of a scope: a ``case`` statement, and how often each of its branches was taken.
902 Each case item is a branch (``<celem>``, :class:`CaseBranch`); the ``case`` statement's line isn't stated.
903 """
905 _parent: Nullable[Scope] #: The scope the ``case`` statement belongs to.
906 _branches: list[CaseBranch] #: The branches, in the order of the case items.
908 def __init__(self, *, parent: Nullable[Scope] = None) -> None:
909 """
910 Initialize the ``case`` statement, and append it to the ``case`` statements of its scope.
912 Its branches are added by creating them with this ``case`` statement as their parent.
914 :param parent: Optional, the scope the ``case`` statement belongs to. Default: ``None``.
915 :raises TypeError: If parameter ``parent`` isn't of type
916 :class:`Scope`.
917 """
918 if parent is not None and not isinstance(parent, Scope):
919 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
920 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
921 raise ex
923 self._parent = parent
924 self._branches = []
926 if parent is not None:
927 parent._caseStatements.append(self)
929 @classmethod
930 def Parse(cls, element: _Element, *, parent: Scope) -> Self:
931 """
932 Parse a ``case`` statement and its branches from its ``<case>`` element.
934 :param element: The ``<case>`` element.
935 :param parent: The scope the ``case`` statement belongs to.
936 :returns: The ``case`` statement.
937 :raises CodeCoverageError: If a branch's file number isn't in the scope's source table, or it states none and the
938 scope has no single file.
939 :raises CodeCoverageError: If the ``case`` statement states other numbers of branches and taken branches than its
940 branches have.
941 """
942 caseStatement = cls(parent=parent)
943 for branchElement in element.iterfind("celem"):
944 CaseBranch.Parse(branchElement, parent=caseStatement)
946 active = int(element.attrib["active"])
947 hits = int(element.attrib["hits"])
948 if active != len(caseStatement._branches) or hits != caseStatement.TakenBranches:
949 ex = CodeCoverageError(f"'<case>' in line {element.sourceline} states other figures than its branches have.")
950 ex.add_note(f"Got '{hits}' of '{active}' branches taken, its branches have {caseStatement.TakenBranches} of "
951 f"{len(caseStatement._branches)}.")
952 raise ex
954 return caseStatement
956 @readonly
957 def Parent(self) -> Nullable[Scope]:
958 """
959 Read-only property to access the scope the ``case`` statement belongs to (:attr:`_parent`).
961 :returns: The scope; ``None`` if the ``case`` statement belongs to no scope.
962 """
963 return self._parent
965 @readonly
966 def Branches(self) -> list[CaseBranch]:
967 """
968 Read-only property to access the branches (:attr:`_branches`).
970 :returns: The branches, in the order of the case items.
971 """
972 return self._branches
974 @readonly
975 def TakenBranches(self) -> int:
976 """
977 Read-only property to return the number of branches, which were taken.
979 :returns: The number of branches with a positive :attr:`CaseBranch.Hits`.
980 """
981 return sum(1 for branch in self._branches if branch._hits > 0)
983 @readonly
984 def EvaluationCount(self) -> int:
985 """
986 Read-only property to return how often the ``case`` statement was evaluated.
988 Each evaluation takes exactly one branch, so it is the sum of the branches' :attr:`CaseBranch.Hits`.
990 :returns: The number of evaluations.
991 """
992 return sum(branch._hits for branch in self._branches)
995@export
996class FSMState(metaclass=ExtendedType, slots=True):
997 """
998 A ``<state>`` of a scope: a state of a finite state machine, and how often it was entered.
999 """
1001 _parent: Nullable[Scope] #: The scope the state belongs to.
1002 _name: str #: Name of the state.
1003 _file: Nullable[Path] #: Path of the source file, if known.
1004 _lineNumber: int #: Line number of the state, counted from 1.
1005 _hits: int #: How often the state was entered.
1007 def __init__(
1008 self,
1009 name: str,
1010 file: Nullable[Path],
1011 lineNumber: int,
1012 hits: int,
1013 *,
1014 parent: Nullable[Scope] = None
1015 ) -> None:
1016 """
1017 Initialize the state, and append it to the states of its scope.
1019 :param name: Name of the state.
1020 :param file: Path of the source file, as the report states it; ``None``, if unknown.
1021 :param lineNumber: Line number of the state, counted from 1.
1022 :param hits: How often the state was entered.
1023 :param parent: Optional, the scope the state belongs to. Default: ``None``.
1024 :raises ValueError: If parameter ``name`` is ``None``.
1025 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
1026 :raises ValueError: If parameter ``name`` is empty.
1027 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
1028 :raises ValueError: If parameter ``lineNumber`` is ``None``.
1029 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
1030 :raises ValueError: If parameter ``lineNumber`` is less than 1.
1031 :raises ValueError: If parameter ``hits`` is ``None``.
1032 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`.
1033 :raises ValueError: If parameter ``hits`` is negative.
1034 :raises TypeError: If parameter ``parent`` isn't of type
1035 :class:`Scope`.
1036 """
1037 if name is None:
1038 raise ValueError("Parameter 'name' is None.")
1039 elif not isinstance(name, str):
1040 ex = TypeError("Parameter 'name' is not of type 'str'.")
1041 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
1042 raise ex
1043 elif name == "":
1044 raise ValueError("Parameter 'name' is empty.")
1046 if file is not None and not isinstance(file, Path):
1047 ex = TypeError("Parameter 'file' is not of type 'Path'.")
1048 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
1049 raise ex
1051 if lineNumber is None:
1052 raise ValueError("Parameter 'lineNumber' is None.")
1053 elif not isinstance(lineNumber, int):
1054 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
1055 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
1056 raise ex
1057 elif lineNumber < 1:
1058 ex = ValueError("Parameter 'lineNumber' is less than 1.")
1059 ex.add_note(f"Got value '{lineNumber}'.")
1060 raise ex
1062 if hits is None:
1063 raise ValueError("Parameter 'hits' is None.")
1064 elif not isinstance(hits, int):
1065 ex = TypeError("Parameter 'hits' is not of type 'int'.")
1066 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.")
1067 raise ex
1068 elif hits < 0:
1069 ex = ValueError("Parameter 'hits' is negative.")
1070 ex.add_note(f"Got value '{hits}'.")
1071 raise ex
1073 if parent is not None and not isinstance(parent, Scope):
1074 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
1075 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1076 raise ex
1078 self._parent = parent
1079 self._name = name
1080 self._file = file
1081 self._lineNumber = lineNumber
1082 self._hits = hits
1084 if parent is not None: 1084 ↛ exitline 1084 didn't return from function '__init__' because the condition on line 1084 was always true
1085 parent._states.append(self)
1087 @classmethod
1088 def Parse(cls, element: _Element, *, parent: Scope) -> Self:
1089 """
1090 Parse a state from its ``<state>`` element.
1092 A ``<state>`` names no file number: its file is the scope's, if the scope has a single file.
1094 :param element: The ``<state>`` element.
1095 :param parent: The scope the state belongs to.
1096 :returns: The state.
1097 """
1098 file = next(iter(parent._files.values())) if len(parent._files) == 1 else None
1099 return cls(
1100 element.attrib["state"], file, int(element.attrib["ln"]), int(element.attrib["hits"]), parent=parent
1101 )
1103 @readonly
1104 def Parent(self) -> Nullable[Scope]:
1105 """
1106 Read-only property to access the scope the state belongs to (:attr:`_parent`).
1108 :returns: The scope; ``None`` if the state belongs to no scope.
1109 """
1110 return self._parent
1112 @readonly
1113 def Name(self) -> str:
1114 """
1115 Read-only property to access the name of the state (:attr:`_name`).
1117 :returns: The state's name, as the enumeration of the state machine names it.
1118 """
1119 return self._name
1121 @readonly
1122 def File(self) -> Nullable[Path]:
1123 """
1124 Read-only property to access the path of the source file (:attr:`_file`).
1126 :returns: The path, as the report states it; ``None``, if the scope has several files.
1127 """
1128 return self._file
1130 @readonly
1131 def LineNumber(self) -> int:
1132 """
1133 Read-only property to access the line number of the state (:attr:`_lineNumber`).
1135 :returns: The line number, counted from 1.
1136 """
1137 return self._lineNumber
1139 @readonly
1140 def Hits(self) -> int:
1141 """
1142 Read-only property to access how often the state was entered (:attr:`_hits`).
1144 :returns: The count.
1145 """
1146 return self._hits
1149@export
1150class FSMTransition(metaclass=ExtendedType, slots=True):
1151 """
1152 A ``<trans>`` of a scope: a transition of a finite state machine from one state to another, and how often it was
1153 taken.
1154 """
1156 _parent: Nullable[Scope] #: The scope the transition belongs to.
1157 _identifier: int #: Number of the transition in its state machine, counted from 0.
1158 _fromState: str #: Name of the state the transition leaves.
1159 _toState: str #: Name of the state the transition enters.
1160 _file: Nullable[Path] #: Path of the source file, if known.
1161 _lineNumber: int #: Line number of the transition, counted from 1.
1162 _hits: int #: How often the transition was taken.
1164 def __init__(
1165 self,
1166 identifier: int,
1167 fromState: str,
1168 toState: str,
1169 file: Nullable[Path],
1170 lineNumber: int,
1171 hits: int,
1172 *,
1173 parent: Nullable[Scope] = None
1174 ) -> None:
1175 """
1176 Initialize the transition, and append it to the transitions of its scope.
1178 :param identifier: Number of the transition in its state machine, counted from 0.
1179 :param fromState: Name of the state the transition leaves.
1180 :param toState: Name of the state the transition enters.
1181 :param file: Path of the source file, as the report states it; ``None``, if unknown.
1182 :param lineNumber: Line number of the transition, counted from 1.
1183 :param hits: How often the transition was taken.
1184 :param parent: Optional, the scope the transition belongs to. Default: ``None``.
1185 :raises ValueError: If parameter ``identifier`` is ``None``.
1186 :raises TypeError: If parameter ``identifier`` isn't of type :class:`int`.
1187 :raises ValueError: If parameter ``identifier`` is negative.
1188 :raises ValueError: If parameter ``fromState`` is ``None``.
1189 :raises TypeError: If parameter ``fromState`` isn't of type :class:`str`.
1190 :raises ValueError: If parameter ``fromState`` is empty.
1191 :raises ValueError: If parameter ``toState`` is ``None``.
1192 :raises TypeError: If parameter ``toState`` isn't of type :class:`str`.
1193 :raises ValueError: If parameter ``toState`` is empty.
1194 :raises TypeError: If parameter ``file`` isn't of type :class:`~pathlib.Path`.
1195 :raises ValueError: If parameter ``lineNumber`` is ``None``.
1196 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
1197 :raises ValueError: If parameter ``lineNumber`` is less than 1.
1198 :raises ValueError: If parameter ``hits`` is ``None``.
1199 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`.
1200 :raises ValueError: If parameter ``hits`` is negative.
1201 :raises TypeError: If parameter ``parent`` isn't of type
1202 :class:`Scope`.
1203 """
1204 if identifier is None:
1205 raise ValueError("Parameter 'identifier' is None.")
1206 elif not isinstance(identifier, int):
1207 ex = TypeError("Parameter 'identifier' is not of type 'int'.")
1208 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.")
1209 raise ex
1210 elif identifier < 0:
1211 ex = ValueError("Parameter 'identifier' is negative.")
1212 ex.add_note(f"Got value '{identifier}'.")
1213 raise ex
1215 if fromState is None:
1216 raise ValueError("Parameter 'fromState' is None.")
1217 elif not isinstance(fromState, str):
1218 ex = TypeError("Parameter 'fromState' is not of type 'str'.")
1219 ex.add_note(f"Got type '{getFullyQualifiedName(fromState)}'.")
1220 raise ex
1221 elif fromState == "":
1222 raise ValueError("Parameter 'fromState' is empty.")
1224 if toState is None:
1225 raise ValueError("Parameter 'toState' is None.")
1226 elif not isinstance(toState, str):
1227 ex = TypeError("Parameter 'toState' is not of type 'str'.")
1228 ex.add_note(f"Got type '{getFullyQualifiedName(toState)}'.")
1229 raise ex
1230 elif toState == "":
1231 raise ValueError("Parameter 'toState' is empty.")
1233 if file is not None and not isinstance(file, Path):
1234 ex = TypeError("Parameter 'file' is not of type 'Path'.")
1235 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
1236 raise ex
1238 if lineNumber is None: 1238 ↛ 1239line 1238 didn't jump to line 1239 because the condition on line 1238 was never true
1239 raise ValueError("Parameter 'lineNumber' is None.")
1240 elif not isinstance(lineNumber, int): 1240 ↛ 1241line 1240 didn't jump to line 1241 because the condition on line 1240 was never true
1241 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
1242 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
1243 raise ex
1244 elif lineNumber < 1:
1245 ex = ValueError("Parameter 'lineNumber' is less than 1.")
1246 ex.add_note(f"Got value '{lineNumber}'.")
1247 raise ex
1249 if hits is None: 1249 ↛ 1250line 1249 didn't jump to line 1250 because the condition on line 1249 was never true
1250 raise ValueError("Parameter 'hits' is None.")
1251 elif not isinstance(hits, int): 1251 ↛ 1252line 1251 didn't jump to line 1252 because the condition on line 1251 was never true
1252 ex = TypeError("Parameter 'hits' is not of type 'int'.")
1253 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.")
1254 raise ex
1255 elif hits < 0:
1256 ex = ValueError("Parameter 'hits' is negative.")
1257 ex.add_note(f"Got value '{hits}'.")
1258 raise ex
1260 if parent is not None and not isinstance(parent, Scope):
1261 ex = TypeError("Parameter 'parent' is not of type 'Scope'.")
1262 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1263 raise ex
1265 self._parent = parent
1266 self._identifier = identifier
1267 self._fromState = fromState
1268 self._toState = toState
1269 self._file = file
1270 self._lineNumber = lineNumber
1271 self._hits = hits
1273 if parent is not None: 1273 ↛ exitline 1273 didn't return from function '__init__' because the condition on line 1273 was always true
1274 parent._transitions.append(self)
1276 @classmethod
1277 def Parse(cls, element: _Element, *, parent: Scope) -> Self:
1278 """
1279 Parse a transition from its ``<trans>`` element, whose attribute ``state`` names both states.
1281 A ``<trans>`` names no file number: its file is the scope's, if the scope has a single file.
1283 :param element: The ``<trans>`` element.
1284 :param parent: The scope the transition belongs to.
1285 :returns: The transition.
1286 :raises CodeCoverageError: If attribute ``state`` doesn't name two states separated by ``->``. |br|
1287 Questa writes it as ``<state> -> <state>``.
1288 """
1289 fromState, separator, toState = element.attrib["state"].partition(" -> ")
1290 if separator == "" or fromState == "" or toState == "":
1291 ex = CodeCoverageError(f"'<trans>' in line {element.sourceline} doesn't name the states it connects.")
1292 ex.add_note(f"Got state='{element.attrib['state']}'.")
1293 ex.add_note("Questa writes it as '<state> -> <state>'.")
1294 raise ex
1296 file = next(iter(parent._files.values())) if len(parent._files) == 1 else None
1297 return cls(
1298 int(element.attrib["tid"]),
1299 fromState,
1300 toState,
1301 file,
1302 int(element.attrib["ln"]),
1303 int(element.attrib["hits"]),
1304 parent=parent
1305 )
1307 @readonly
1308 def Parent(self) -> Nullable[Scope]:
1309 """
1310 Read-only property to access the scope the transition belongs to (:attr:`_parent`).
1312 :returns: The scope; ``None`` if the transition belongs to no scope.
1313 """
1314 return self._parent
1316 @readonly
1317 def Identifier(self) -> int:
1318 """
1319 Read-only property to access the number of the transition in its state machine (:attr:`_identifier`).
1321 :returns: The number, counted from 0.
1322 """
1323 return self._identifier
1325 @readonly
1326 def FromState(self) -> str:
1327 """
1328 Read-only property to access the name of the state the transition leaves (:attr:`_fromState`).
1330 :returns: The state's name.
1331 """
1332 return self._fromState
1334 @readonly
1335 def ToState(self) -> str:
1336 """
1337 Read-only property to access the name of the state the transition enters (:attr:`_toState`).
1339 :returns: The state's name.
1340 """
1341 return self._toState
1343 @readonly
1344 def File(self) -> Nullable[Path]:
1345 """
1346 Read-only property to access the path of the source file (:attr:`_file`).
1348 :returns: The path, as the report states it; ``None``, if the scope has several files.
1349 """
1350 return self._file
1352 @readonly
1353 def LineNumber(self) -> int:
1354 """
1355 Read-only property to access the line number of the transition (:attr:`_lineNumber`).
1357 :returns: The line number, counted from 1.
1358 """
1359 return self._lineNumber
1361 @readonly
1362 def Hits(self) -> int:
1363 """
1364 Read-only property to access how often the transition was taken (:attr:`_hits`).
1366 :returns: The count.
1367 """
1368 return self._hits
1371@export
1372class Scope(StatisticsMixin, slots=True):
1373 """
1374 Base-class of the scopes: an instance, a design unit or a source file, with its coverage statistics, its source files
1375 and its statements.
1376 """
1378 _parent: Nullable[Report] #: The report the scope belongs to.
1379 _files: dict[int, Path] #: The source files, by the number the report gives them.
1380 _statements: list[Statement] #: The statements.
1381 _ifStatements: list[IfStatement] #: The ``if`` statements.
1382 _caseStatements: list[CaseStatement] #: The ``case`` statements.
1383 _states: list[FSMState] #: The states of finite state machines.
1384 _transitions: list[FSMTransition] #: The transitions of finite state machines.
1386 def __init__(self, files: Nullable[Mapping[int, Path]] = None, *, parent: Nullable[Report] = None) -> None:
1387 """
1388 Initialize the scope, and append it to the scopes of its report.
1390 Its coverage statistics and coverage items are added by creating them with this scope as their parent.
1392 :param files: Optional, the source files, by the number the report gives them. Default: ``None``.
1393 :param parent: Optional, the report the scope belongs to; the scope is appended to its scopes. Default:
1394 ``None``.
1395 :raises TypeError: If parameter ``files`` isn't a mapping.
1396 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`.
1397 :raises ValueError: If parameter ``files`` contains a negative key.
1398 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`.
1399 :raises TypeError: If parameter ``parent`` isn't of type
1400 :class:`Report`.
1401 """
1402 super().__init__()
1404 self._files = {}
1405 if files is not None:
1406 if not isinstance(files, Mapping):
1407 ex = TypeError("Parameter 'files' is not a mapping.")
1408 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.")
1409 raise ex
1411 for fileNumber, path in files.items():
1412 if not isinstance(fileNumber, int):
1413 ex = TypeError("Parameter 'files' contains a key not of type 'int'.")
1414 ex.add_note(f"Got type '{getFullyQualifiedName(fileNumber)}'.")
1415 raise ex
1416 elif fileNumber < 0:
1417 ex = ValueError("Parameter 'files' contains a negative key.")
1418 ex.add_note(f"Got value '{fileNumber}'.")
1419 raise ex
1420 elif not isinstance(path, Path):
1421 ex = TypeError("Parameter 'files' contains a value not of type 'Path'.")
1422 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1423 raise ex
1425 self._files[fileNumber] = path
1427 if parent is not None and not isinstance(parent, Report):
1428 ex = TypeError("Parameter 'parent' is not of type 'Report'.")
1429 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1430 raise ex
1432 self._parent = parent
1433 self._statements = []
1434 self._ifStatements = []
1435 self._caseStatements = []
1436 self._states = []
1437 self._transitions = []
1439 if parent is not None:
1440 parent._scopes.append(self)
1442 @staticmethod
1443 def _ParseSourceTable(element: _Element) -> dict[int, Path]:
1444 """
1445 Parse the source files of a scope's ``<sourceTable>``.
1447 :param element: The scope's element.
1448 :returns: The source files, by their number; none, if the scope has no source table.
1449 """
1450 return {
1451 int(fileMap.attrib["fn"]): Path(fileMap.attrib["path"]) for fileMap in element.iterfind("sourceTable/fileMap")
1452 }
1454 def _ParseItems(self, element: _Element) -> None:
1455 """
1456 Parse the coverage statistics and the coverage items of the scope from its element.
1458 :param element: The scope's element.
1459 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
1460 :raises CodeCoverageError: If the scope states a kind of coverage statistics twice.
1461 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in a scope of several files.
1462 :raises CodeCoverageError: If an ``if`` or ``case`` statement states other figures than its branches have.
1463 :raises CodeCoverageError: If a transition's name doesn't name its two states.
1464 """
1465 self._ParseStatistics(element)
1466 for child in element:
1467 if child.tag == "stmt":
1468 Statement.Parse(child, parent=self)
1469 elif child.tag == "if":
1470 IfStatement.Parse(child, parent=self)
1471 elif child.tag == "case":
1472 CaseStatement.Parse(child, parent=self)
1473 elif child.tag == "state":
1474 FSMState.Parse(child, parent=self)
1475 elif child.tag == "trans":
1476 FSMTransition.Parse(child, parent=self)
1478 def _ResolveFile(self, element: _Element) -> Path:
1479 """
1480 Return the source file a coverage item's element names by its file number.
1482 An element without file number is in the scope's file: the path of a ``<fileData>``, or the only file of the source
1483 table.
1485 :param element: The coverage item's element, e.g. a ``<stmt>``.
1486 :returns: The source file's path, as the report states it.
1487 :raises CodeCoverageError: If the file number isn't in the scope's source table.
1488 :raises CodeCoverageError: If the element names no file number, and the scope has no single file.
1489 """
1490 if (fileNumber := element.attrib.get("fn")) is not None:
1491 if (file := self._files.get(int(fileNumber))) is None:
1492 ex = CodeCoverageError(
1493 f"'<{element.tag}>' in line {element.sourceline} names file number '{fileNumber}', which is unknown."
1494 )
1495 ex.add_note(f"Known file numbers: {', '.join(str(number) for number in self._files) or 'none'}.")
1496 raise ex
1498 return file
1499 elif isinstance(self, FileData):
1500 return self._path
1501 elif len(self._files) == 1:
1502 return next(iter(self._files.values()))
1504 ex = CodeCoverageError(f"'<{element.tag}>' in line {element.sourceline} names no file number.")
1505 ex.add_note(f"Its scope names {len(self._files)} files.")
1506 raise ex
1508 @readonly
1509 def Parent(self) -> Nullable[Report]:
1510 """
1511 Read-only property to access the report the scope belongs to (:attr:`_parent`).
1513 :returns: The report; ``None`` if the scope belongs to none.
1514 """
1515 return self._parent
1517 @readonly
1518 def Files(self) -> dict[int, Path]:
1519 """
1520 Read-only property to access the source files (:attr:`_files`).
1522 :returns: The source files, by the number the report gives them; none, if the report has no details.
1523 """
1524 return self._files
1526 @readonly
1527 def Statements(self) -> list[Statement]:
1528 """
1529 Read-only property to access the statements (:attr:`_statements`).
1531 :returns: The statements, in the order the report lists them; none, if the report has no details.
1532 """
1533 return self._statements
1535 @readonly
1536 def IfStatements(self) -> list[IfStatement]:
1537 """
1538 Read-only property to access the ``if`` statements (:attr:`_ifStatements`).
1540 :returns: The ``if`` statements, in the order the report lists them; none, if the report has no details.
1541 """
1542 return self._ifStatements
1544 @readonly
1545 def CaseStatements(self) -> list[CaseStatement]:
1546 """
1547 Read-only property to access the ``case`` statements (:attr:`_caseStatements`).
1549 :returns: The ``case`` statements, in the order the report lists them; none, if the report has no details.
1550 """
1551 return self._caseStatements
1553 @readonly
1554 def States(self) -> list[FSMState]:
1555 """
1556 Read-only property to access the states of finite state machines (:attr:`_states`).
1558 :returns: The states, in the order the report lists them; none, if the report has no details.
1559 """
1560 return self._states
1562 @readonly
1563 def Transitions(self) -> list[FSMTransition]:
1564 """
1565 Read-only property to access the transitions of finite state machines (:attr:`_transitions`).
1567 :returns: The transitions, in the order the report lists them; none, if the report has no details.
1568 """
1569 return self._transitions
1572@export
1573class InstanceData(Scope):
1574 """
1575 An ``<instanceData>``: an instance of a design unit, or a package.
1576 """
1578 _path: str #: Hierarchical path of the instance.
1579 _designUnit: str #: Name of the design unit.
1580 _secondaryUnit: Nullable[str] #: Name of the secondary unit (the architecture), if the design unit has one.
1582 def __init__(
1583 self,
1584 path: str,
1585 designUnit: str,
1586 secondaryUnit: Nullable[str] = None,
1587 files: Nullable[Mapping[int, Path]] = None,
1588 *,
1589 parent: Nullable[Report] = None
1590 ) -> None:
1591 """
1592 Initialize the instance, and append it to the scopes of its report.
1594 :param path: Hierarchical path of the instance, separated by ``/``.
1595 :param designUnit: Name of the design unit.
1596 :param secondaryUnit: Optional, name of the secondary unit (the architecture). Default: ``None``.
1597 :param files: Optional, the source files, by the number the report gives them. Default: ``None``.
1598 :param parent: Optional, the report the instance belongs to. Default: ``None``.
1599 :raises TypeError: If parameter ``files`` isn't a mapping.
1600 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`.
1601 :raises ValueError: If parameter ``files`` contains a negative key.
1602 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`.
1603 :raises TypeError: If parameter ``parent`` isn't of type
1604 :class:`Report`.
1605 :raises ValueError: If parameter ``path`` is ``None``.
1606 :raises TypeError: If parameter ``path`` isn't of type :class:`str`.
1607 :raises ValueError: If parameter ``path`` is empty.
1608 :raises ValueError: If parameter ``designUnit`` is ``None``.
1609 :raises TypeError: If parameter ``designUnit`` isn't of type :class:`str`.
1610 :raises ValueError: If parameter ``designUnit`` is empty.
1611 :raises TypeError: If parameter ``secondaryUnit`` isn't of type :class:`str`.
1612 :raises ValueError: If parameter ``secondaryUnit`` is empty.
1613 """
1614 super().__init__(files, parent=parent)
1616 if path is None:
1617 raise ValueError("Parameter 'path' is None.")
1618 elif not isinstance(path, str):
1619 ex = TypeError("Parameter 'path' is not of type 'str'.")
1620 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1621 raise ex
1622 elif path == "":
1623 raise ValueError("Parameter 'path' is empty.")
1625 if designUnit is None:
1626 raise ValueError("Parameter 'designUnit' is None.")
1627 elif not isinstance(designUnit, str):
1628 ex = TypeError("Parameter 'designUnit' is not of type 'str'.")
1629 ex.add_note(f"Got type '{getFullyQualifiedName(designUnit)}'.")
1630 raise ex
1631 elif designUnit == "":
1632 raise ValueError("Parameter 'designUnit' is empty.")
1634 if secondaryUnit is not None:
1635 if not isinstance(secondaryUnit, str):
1636 ex = TypeError("Parameter 'secondaryUnit' is not of type 'str'.")
1637 ex.add_note(f"Got type '{getFullyQualifiedName(secondaryUnit)}'.")
1638 raise ex
1639 elif secondaryUnit == "":
1640 raise ValueError("Parameter 'secondaryUnit' is empty.")
1642 self._path = path
1643 self._designUnit = designUnit
1644 self._secondaryUnit = secondaryUnit
1646 @classmethod
1647 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self:
1648 """
1649 Parse an instance, its source files, coverage statistics and statements from its ``<instanceData>`` element.
1651 :param element: The ``<instanceData>`` element.
1652 :param parent: Optional, the report the instance belongs to. Default: ``None``.
1653 :returns: The instance.
1654 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
1655 :raises CodeCoverageError: If the instance states a kind of coverage statistics twice.
1656 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in an instance of several
1657 files.
1658 """
1659 instance = cls(
1660 element.attrib["path"],
1661 element.attrib["du"],
1662 element.attrib.get("sec"),
1663 cls._ParseSourceTable(element),
1664 parent=parent
1665 )
1666 instance._ParseItems(element)
1667 return instance
1669 @readonly
1670 def Path(self) -> str:
1671 """
1672 Read-only property to access the hierarchical path of the instance (:attr:`_path`).
1674 :returns: The path, separated by ``/``.
1675 """
1676 return self._path
1678 @readonly
1679 def DesignUnit(self) -> str:
1680 """
1681 Read-only property to access the name of the design unit (:attr:`_designUnit`).
1683 :returns: The design unit's name, as Questa writes it: in lower case for VHDL.
1684 """
1685 return self._designUnit
1687 @readonly
1688 def SecondaryUnit(self) -> Nullable[str]:
1689 """
1690 Read-only property to access the name of the secondary unit (:attr:`_secondaryUnit`).
1692 :returns: The architecture's name, or ``None`` - e.g. for a package.
1693 """
1694 return self._secondaryUnit
1697@export
1698class DesignUnitData(Scope):
1699 """
1700 A ``<DuData>``: a design unit, its instances' coverage combined.
1701 """
1703 _designUnit: str #: Name of the design unit.
1704 _secondaryUnit: Nullable[str] #: Name of the secondary unit (the architecture), if the design unit has one.
1706 def __init__(
1707 self,
1708 designUnit: str,
1709 secondaryUnit: Nullable[str] = None,
1710 files: Nullable[Mapping[int, Path]] = None,
1711 *,
1712 parent: Nullable[Report] = None
1713 ) -> None:
1714 """
1715 Initialize the design unit, and append it to the scopes of its report.
1717 :param designUnit: Name of the design unit.
1718 :param secondaryUnit: Optional, name of the secondary unit (the architecture). Default: ``None``.
1719 :param files: Optional, the source files, by the number the report gives them. Default: ``None``.
1720 :param parent: Optional, the report the design unit belongs to. Default: ``None``.
1721 :raises TypeError: If parameter ``files`` isn't a mapping.
1722 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`.
1723 :raises ValueError: If parameter ``files`` contains a negative key.
1724 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`.
1725 :raises TypeError: If parameter ``parent`` isn't of type
1726 :class:`Report`.
1727 :raises ValueError: If parameter ``designUnit`` is ``None``.
1728 :raises TypeError: If parameter ``designUnit`` isn't of type :class:`str`.
1729 :raises ValueError: If parameter ``designUnit`` is empty.
1730 :raises TypeError: If parameter ``secondaryUnit`` isn't of type :class:`str`.
1731 :raises ValueError: If parameter ``secondaryUnit`` is empty.
1732 """
1733 super().__init__(files, parent=parent)
1735 if designUnit is None:
1736 raise ValueError("Parameter 'designUnit' is None.")
1737 elif not isinstance(designUnit, str):
1738 ex = TypeError("Parameter 'designUnit' is not of type 'str'.")
1739 ex.add_note(f"Got type '{getFullyQualifiedName(designUnit)}'.")
1740 raise ex
1741 elif designUnit == "":
1742 raise ValueError("Parameter 'designUnit' is empty.")
1744 if secondaryUnit is not None:
1745 if not isinstance(secondaryUnit, str):
1746 ex = TypeError("Parameter 'secondaryUnit' is not of type 'str'.")
1747 ex.add_note(f"Got type '{getFullyQualifiedName(secondaryUnit)}'.")
1748 raise ex
1749 elif secondaryUnit == "": 1749 ↛ 1752line 1749 didn't jump to line 1752 because the condition on line 1749 was always true
1750 raise ValueError("Parameter 'secondaryUnit' is empty.")
1752 self._designUnit = designUnit
1753 self._secondaryUnit = secondaryUnit
1755 @classmethod
1756 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self:
1757 """
1758 Parse a design unit, its source files, coverage statistics and statements from its ``<DuData>`` element.
1760 :param element: The ``<DuData>`` element.
1761 :param parent: Optional, the report the design unit belongs to. Default: ``None``.
1762 :returns: The design unit.
1763 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
1764 :raises CodeCoverageError: If the design unit states a kind of coverage statistics twice.
1765 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in a design unit of several
1766 files.
1767 """
1768 designUnit = cls(element.attrib["du"], element.attrib.get("sec"), cls._ParseSourceTable(element), parent=parent)
1769 designUnit._ParseItems(element)
1770 return designUnit
1772 @readonly
1773 def DesignUnit(self) -> str:
1774 """
1775 Read-only property to access the name of the design unit (:attr:`_designUnit`).
1777 :returns: The design unit's name, as Questa writes it: in lower case for VHDL.
1778 """
1779 return self._designUnit
1781 @readonly
1782 def SecondaryUnit(self) -> Nullable[str]:
1783 """
1784 Read-only property to access the name of the secondary unit (:attr:`_secondaryUnit`).
1786 :returns: The architecture's name, or ``None`` - e.g. for a package.
1787 """
1788 return self._secondaryUnit
1791@export
1792class FileData(Scope):
1793 """
1794 A ``<fileData>``: a source file.
1795 """
1797 _path: _Path #: Path of the source file, as the report states it.
1799 def __init__(
1800 self,
1801 path: Path,
1802 files: Nullable[Mapping[int, Path]] = None,
1803 *,
1804 parent: Nullable[Report] = None
1805 ) -> None:
1806 """
1807 Initialize the source file, and append it to the scopes of its report.
1809 :param path: Path of the source file, as the report states it.
1810 :param files: Optional, the source files, by the number the report gives them. Default: ``None``.
1811 :param parent: Optional, the report the source file belongs to. Default: ``None``.
1812 :raises TypeError: If parameter ``files`` isn't a mapping.
1813 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`.
1814 :raises ValueError: If parameter ``files`` contains a negative key.
1815 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`.
1816 :raises TypeError: If parameter ``parent`` isn't of type
1817 :class:`Report`.
1818 :raises ValueError: If parameter ``path`` is ``None``.
1819 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1820 """
1821 super().__init__(files, parent=parent)
1823 if path is None:
1824 raise ValueError("Parameter 'path' is None.")
1825 elif not isinstance(path, Path):
1826 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1827 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1828 raise ex
1830 self._path = path
1832 @classmethod
1833 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self:
1834 """
1835 Parse a source file, its coverage statistics and statements from its ``<fileData>`` element.
1837 :param element: The ``<fileData>`` element.
1838 :param parent: Optional, the report the source file belongs to. Default: ``None``.
1839 :returns: The source file.
1840 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
1841 :raises CodeCoverageError: If the source file states a kind of coverage statistics twice.
1842 :raises CodeCoverageError: If a coverage item names an unknown file number.
1843 """
1844 fileData = cls(Path(element.attrib["path"]), cls._ParseSourceTable(element), parent=parent)
1845 fileData._ParseItems(element)
1846 return fileData
1848 @readonly
1849 def Path(self) -> _Path:
1850 """
1851 Read-only property to access the path of the source file (:attr:`_path`).
1853 :returns: The path, as the report states it.
1854 """
1855 return self._path
1858@export
1859class Totals(StatisticsMixin):
1860 """
1861 A summary record of a report - ``<summaryByFile>`` or ``<summaryByInstance>`` -: the coverage statistics of all its
1862 files or instances together.
1863 """
1865 _parent: Nullable[Report] #: The report the summary record belongs to.
1866 _mode: ReportMode #: What the summary record sums up: files or instances.
1867 _count: int #: Number of files or instances summed up.
1869 def __init__(self, mode: ReportMode, count: int, *, parent: Nullable[Report] = None) -> None:
1870 """
1871 Initialize the summary record, and add it to the summary records of its report.
1873 Its coverage statistics are added by creating them with this summary record as their parent.
1875 :param mode: What the summary record sums up: :attr:`ReportMode.ByFile` or
1876 :attr:`ReportMode.ByInstance`.
1877 :param count: Number of files or instances summed up.
1878 :param parent: Optional, the report the summary record belongs to; it is added to its summary records
1879 by :attr:`Mode`. Default: ``None``.
1880 :raises ValueError: If parameter ``mode`` is ``None``.
1881 :raises TypeError: If parameter ``mode`` isn't of type :class:`ReportMode`.
1882 :raises ValueError: If parameter ``mode`` is :attr:`ReportMode.ByDesignUnit`.
1883 :raises ValueError: If parameter ``count`` is ``None``.
1884 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
1885 :raises ValueError: If parameter ``count`` is negative.
1886 :raises TypeError: If parameter ``parent`` isn't of type
1887 :class:`Report`.
1888 :raises CodeCoverageError: If the report already has a summary record of this mode.
1889 """
1890 super().__init__()
1892 if mode is None:
1893 raise ValueError("Parameter 'mode' is None.")
1894 elif not isinstance(mode, ReportMode):
1895 ex = TypeError("Parameter 'mode' is not of type 'ReportMode'.")
1896 ex.add_note(f"Got type '{getFullyQualifiedName(mode)}'.")
1897 raise ex
1898 elif mode is ReportMode.ByDesignUnit:
1899 ex = ValueError("Parameter 'mode' is not 'ByFile' or 'ByInstance'.")
1900 ex.add_note(f"Got value '{mode.name}'.")
1901 raise ex
1903 if count is None:
1904 raise ValueError("Parameter 'count' is None.")
1905 elif not isinstance(count, int):
1906 ex = TypeError("Parameter 'count' is not of type 'int'.")
1907 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
1908 raise ex
1909 elif count < 0:
1910 ex = ValueError("Parameter 'count' is negative.")
1911 ex.add_note(f"Got value '{count}'.")
1912 raise ex
1914 if parent is not None:
1915 if not isinstance(parent, Report):
1916 ex = TypeError("Parameter 'parent' is not of type 'Report'.")
1917 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1918 raise ex
1919 elif mode in parent._totals:
1920 raise CodeCoverageError(f"Summary record '{mode}' is stated twice.")
1922 self._parent = parent
1923 self._mode = mode
1924 self._count = count
1926 if parent is not None:
1927 parent._totals[mode] = self
1929 @classmethod
1930 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self:
1931 """
1932 Parse a summary record and its coverage statistics from its ``<summaryByFile>`` or ``<summaryByInstance>``
1933 element.
1935 :param element: The summary record's element.
1936 :param parent: Optional, the report the summary record belongs to. Default: ``None``.
1937 :returns: The summary record.
1938 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
1939 """
1940 if element.tag == "summaryByFile": 1940 ↛ 1943line 1940 didn't jump to line 1943 because the condition on line 1940 was always true
1941 totals = cls(ReportMode.ByFile, int(element.attrib["files"]), parent=parent)
1942 else:
1943 totals = cls(ReportMode.ByInstance, int(element.attrib["instances"]), parent=parent)
1945 totals._ParseStatistics(element)
1946 return totals
1948 @readonly
1949 def Parent(self) -> Nullable[Report]:
1950 """
1951 Read-only property to access the report the summary record belongs to (:attr:`_parent`).
1953 :returns: The report; ``None`` if the summary record belongs to none.
1954 """
1955 return self._parent
1957 @readonly
1958 def Mode(self) -> ReportMode:
1959 """
1960 Read-only property to access what the summary record sums up (:attr:`_mode`).
1962 :returns: :attr:`ReportMode.ByFile` or :attr:`ReportMode.ByInstance`.
1963 """
1964 return self._mode
1966 @readonly
1967 def Count(self) -> int:
1968 """
1969 Read-only property to access the number of files or instances summed up (:attr:`_count`).
1971 :returns: The number of files or instances.
1972 """
1973 return self._count
1976_Totals = Totals
1979@export
1980class Report(metaclass=ExtendedType, mixin=True):
1981 """
1982 The root element ``<coverage_report>`` and its ``<code_coverage_report>``: the Questa version and the command, which
1983 wrote it, its mode, the summary records and the scopes.
1985 The root of the format's model: a summary record or a scope names it as its parent.
1986 """
1988 _toolVersion: Nullable[str] #: Version of Questa, which wrote the report.
1989 _command: Nullable[str] #: The command, which wrote the report.
1990 _mode: Nullable[ReportMode] #: How the report groups its coverage data.
1991 _hasLineDetails: bool #: Whether the report has details: source files and coverage items.
1992 _totals: dict[ReportMode, _Totals] #: The summary records, by what they sum up.
1993 _scopes: list[Scope] #: The scopes.
1995 def __init__(self) -> None:
1996 """
1997 Initialize an empty report.
1998 """
1999 self._toolVersion = None
2000 self._command = None
2001 self._mode = None
2002 self._hasLineDetails = False
2003 self._totals = {}
2004 self._scopes = []
2006 @readonly
2007 def ToolVersion(self) -> Nullable[str]:
2008 """
2009 Read-only property to access the version of Questa, which wrote the report (:attr:`_toolVersion`).
2011 Questa states its version since a release between 10.x and 2023.3, e.g. ``2026.2``.
2013 :returns: The version, as the report states it; ``None`` if the report doesn't state it, or before it was converted.
2014 """
2015 return self._toolVersion
2017 @readonly
2018 def Command(self) -> Nullable[str]:
2019 """
2020 Read-only property to access the command, which wrote the report (:attr:`_command`).
2022 :returns: The command, as the report states it; ``None`` if the report doesn't state it, or before it was converted.
2023 """
2024 return self._command
2026 @readonly
2027 def Mode(self) -> Nullable[ReportMode]:
2028 """
2029 Read-only property to access how the report groups its coverage data (:attr:`_mode`).
2031 :returns: The mode; ``None`` before the report was converted.
2032 """
2033 return self._mode
2035 @readonly
2036 def HasLineDetails(self) -> bool:
2037 """
2038 Read-only property to access whether the report has details (:attr:`_hasLineDetails`).
2040 A report written with ``-details`` names the source files of its scopes and lists their coverage items; it states
2041 ``lines="1"``.
2043 :returns: ``True``, if the report states details.
2044 """
2045 return self._hasLineDetails
2047 @readonly
2048 def Totals(self) -> dict[ReportMode, _Totals]:
2049 """
2050 Read-only property to access the summary records (:attr:`_totals`).
2052 :returns: The summary records, by what they sum up; none, if the report has no summary records.
2053 """
2054 return self._totals
2056 @readonly
2057 def Scopes(self) -> list[Scope]:
2058 """
2059 Read-only property to access the scopes (:attr:`_scopes`).
2061 :returns: The scopes, in the order the report lists them.
2062 """
2063 return self._scopes
2065 def IterateStatements(self) -> Generator[Statement, None, None]:
2066 """
2067 Iterate the statements of all scopes.
2069 :returns: A generator of the statements, scope by scope, in the order the report lists them.
2070 """
2071 for scope in self._scopes:
2072 yield from scope._statements
2075@export
2076class Document(cc_Document, Report):
2077 """
2078 A QuestaSim coverage report XML file: read into the format's model, and converted to the common model.
2079 """
2081 _xmlDocument: Nullable[_ElementTree] #: The parsed and validated XML document, after :meth:`Analyze`.
2083 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False) -> None:
2084 """
2085 Initialize the report, and optionally read it.
2087 :param xmlReportFile: Path to the coverage report XML file.
2088 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
2089 """
2090 super().__init__(xmlReportFile)
2091 Report.__init__(self)
2093 self._xmlDocument = None
2095 if analyzeAndConvert:
2096 self.Analyze()
2097 self.Convert()
2099 def Analyze(self) -> None:
2100 """
2101 Parse the XML file, check its root element, and validate the file against the XML schema (:data:`SCHEMA`).
2103 :raises CodeCoverageError: If the file doesn't exist.
2104 :raises CodeCoverageError: If the file can't be read.
2105 :raises CodeCoverageError: If the file isn't well-formed XML.
2106 :raises CodeCoverageError: If the root element isn't ``<coverage_report>``.
2107 :raises CodeCoverageError: If the report is a functional coverage report. |br|
2108 Write the report without '-cvg', '-directive' and '-assert'.
2109 :raises CodeCoverageError: If the XML schema can't be located or parsed.
2110 :raises CodeCoverageError: If the file isn't valid according to the XML schema.
2111 """
2112 if not self._path.exists():
2113 raise CodeCoverageError(f"QuestaSim coverage report '{self._path}' does not exist.") \
2114 from FileNotFoundError(f"File '{self._path}' not found.")
2116 with Stopwatch() as sw:
2117 try:
2118 content = self._path.read_bytes()
2119 except OSError as ex:
2120 raise CodeCoverageError(f"Couldn't read QuestaSim coverage report '{self._path}'.") from ex
2122 try:
2123 xmlDocument = parse(BytesIO(content), XMLParser(ns_clean=True), base_url=str(self._path))
2124 except XMLSyntaxError as ex:
2125 raise CodeCoverageError(f"XML syntax error in QuestaSim coverage report '{self._path}'.") from ex
2127 rootElement: _Element = xmlDocument.getroot()
2128 if rootElement.tag != "coverage_report":
2129 ex = CodeCoverageError(f"Root element of '{self._path}' is not '<coverage_report>'.")
2130 ex.add_note(f"Got root element '<{rootElement.tag}>'.")
2131 raise ex
2132 elif rootElement.find("functional_coverage_report") is not None:
2133 ex = CodeCoverageError(f"'{self._path}' is a functional coverage report, not a code coverage report.")
2134 ex.add_note("Write the report without '-cvg', '-directive' and '-assert'.")
2135 raise ex
2137 try:
2138 schemaResourceFile = getResourceFile(Resources, SCHEMA)
2139 except ToolingException as ex:
2140 raise CodeCoverageError(f"Couldn't locate XML Schema '{SCHEMA}' in package resources.") from ex
2142 try:
2143 xmlSchema = XMLSchema(parse(schemaResourceFile, XMLParser(ns_clean=True)))
2144 except (XMLSyntaxError, XMLSchemaParseError) as ex:
2145 raise CodeCoverageError(f"Error while parsing XML Schema '{SCHEMA}'.") from ex
2147 if not xmlSchema.validate(xmlDocument):
2148 ex = CodeCoverageError(f"Validation error for '{self._path}' using XSD schema '{SCHEMA}'.")
2149 for logEntry in xmlSchema.error_log:
2150 ex.add_note(str(logEntry))
2151 raise ex
2153 self._xmlDocument = xmlDocument
2155 self._analysisDuration = sw.Duration
2157 def Convert(self) -> None:
2158 """
2159 Convert the parsed and validated XML document to the format's model.
2161 :raises CodeCoverageError: If the XML file was not analyzed before. |br|
2162 Call 'Document.Analyze()' or create the document using
2163 'Document(path, analyzeAndConvert=True)'.
2164 :raises CodeCoverageError: If the report doesn't state exactly one mode.
2165 :raises CodeCoverageError: If a coverage statistics element states more hit items than items.
2166 :raises CodeCoverageError: If a scope or summary record states a kind of coverage statistics twice.
2167 :raises CodeCoverageError: If a statement names an unknown file number, or none in a scope of several files.
2168 """
2169 if self._xmlDocument is None:
2170 ex = CodeCoverageError(
2171 f"QuestaSim coverage report '{self._path}' needs to be read and analyzed by an XML parser."
2172 )
2173 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
2174 raise ex
2176 with Stopwatch() as sw:
2177 root: _Element = self._xmlDocument.getroot()
2178 codeCoverage: _Element = root.find("code_coverage_report")
2180 modes = [mode for mode in ReportMode if codeCoverage.attrib.get(mode.value) == "1"]
2181 if len(modes) != 1:
2182 ex = CodeCoverageError(f"QuestaSim coverage report '{self._path}' doesn't state exactly one mode.")
2183 ex.add_note(f"Got modes: {', '.join(mode.value for mode in modes) or 'none'}.")
2184 ex.add_note(f"One of the attributes {', '.join(mode.value for mode in ReportMode)} is '1'.")
2185 raise ex
2187 self._toolVersion = root.attrib.get("questa_version")
2188 self._command = root.attrib.get("command")
2189 self._mode = modes[0]
2190 self._hasLineDetails = codeCoverage.attrib.get("lines") == "1"
2192 for element in codeCoverage:
2193 if element.tag in ("summaryByFile", "summaryByInstance"): 2193 ↛ 2194line 2193 didn't jump to line 2194 because the condition on line 2193 was never true
2194 Totals.Parse(element, parent=self)
2195 elif element.tag == "instanceData": 2195 ↛ 2197line 2195 didn't jump to line 2197 because the condition on line 2195 was always true
2196 InstanceData.Parse(element, parent=self)
2197 elif element.tag == "DuData":
2198 DesignUnitData.Parse(element, parent=self)
2199 else:
2200 FileData.Parse(element, parent=self)
2202 self._conversionDuration = sw.Duration
2204 def ToCoverageSummary(self) -> CoverageSummary:
2205 """
2206 Convert the statements and branches of the format's model to the common model, and aggregate it.
2208 The statements of all scopes are merged by file, line and index, the ``if`` and ``case`` statements by the file,
2209 line and index of their first branch; merged items count as often as their items together. Each branch of an ``if``
2210 or ``case`` statement becomes a branch of the line of its first branch, taken as often as the branch's true count
2211 or hits. A line ran as often as its least often run statement or branching statement - the sum of its branches'
2212 counts. A line, which ran, is covered, if all its branches were taken, otherwise partially covered.
2214 :returns: The report's root of the common model, named after the report file.
2215 :raises CodeCoverageError: If the report has no statements: it was written without details. |br|
2216 Write the report with details: 'vcover report -xml -details ...'.
2217 :raises CodeCoverageError: If two scopes state the same ``if`` or ``case`` statement with different numbers of
2218 branches.
2219 :raises CodeCoverageError: If a file's path runs through another file.
2220 """
2221 hitsPerStatement: dict[tuple[Path, int, int], int] = {}
2222 branchesPerDecision: dict[tuple[Path, int, int, str], list[int]] = {}
2223 for scope in self._scopes:
2224 for statement in scope._statements:
2225 key = (statement._file, statement._lineNumber, statement._index)
2226 hitsPerStatement[key] = hitsPerStatement.get(key, 0) + statement._hits
2228 decisions = [
2229 ("if", [branch._trueCount for branch in ifStatement._branches], ifStatement._branches[0])
2230 for ifStatement in scope._ifStatements
2231 ] + [
2232 ("case", [branch._hits for branch in caseStatement._branches], caseStatement._branches[0])
2233 for caseStatement in scope._caseStatements
2234 ]
2235 for kind, counts, first in decisions:
2236 key = (first._file, first._lineNumber, first._index, kind)
2237 if (merged := branchesPerDecision.get(key)) is None:
2238 branchesPerDecision[key] = counts
2239 elif len(merged) != len(counts): 2239 ↛ 2240line 2239 didn't jump to line 2240 because the condition on line 2239 was never true
2240 ex = CodeCoverageError(
2241 f"The '{kind}' statement at '{first._file.as_posix()}:{first._lineNumber}' has {len(counts)} branches in "
2242 f"one scope, but {len(merged)} in another."
2243 )
2244 ex.add_note("The scopes merge their branches by position.")
2245 raise ex
2246 else:
2247 branchesPerDecision[key] = [total + count for total, count in zip(merged, counts)]
2249 if len(hitsPerStatement) == 0:
2250 ex = CodeCoverageError(f"QuestaSim coverage report '{self._path}' has no statements to convert.")
2251 ex.add_note("Write the report with details: 'vcover report -xml -details ...'.")
2252 raise ex
2254 countsPerLine: dict[Path, dict[int, list[int]]] = {}
2255 branchesPerLine: dict[Path, dict[int, list[int]]] = {}
2256 for (file, lineNumber, _), hits in hitsPerStatement.items():
2257 countsPerLine.setdefault(file, {}).setdefault(lineNumber, []).append(hits)
2259 for (file, lineNumber, _, _), counts in branchesPerDecision.items():
2260 countsPerLine.setdefault(file, {}).setdefault(lineNumber, []).append(sum(counts))
2261 branchesPerLine.setdefault(file, {}).setdefault(lineNumber, []).extend(counts)
2263 summary = CoverageSummary(self._path.stem)
2264 for path, counts in countsPerLine.items():
2265 file = summary.GetOrAddFile(path)
2266 branches = branchesPerLine.get(path, {})
2267 for lineNumber in sorted(counts):
2268 count = min(counts[lineNumber])
2269 branchCounts = branches.get(lineNumber, [])
2270 if count == 0:
2271 status = LineCoverageStatus.Uncovered
2272 elif any(branchCount == 0 for branchCount in branchCounts):
2273 status = LineCoverageStatus.PartiallyCovered
2274 else:
2275 status = LineCoverageStatus.Covered
2277 line = cc_Line(lineNumber, status, count, parent=file)
2278 for branchCount in branchCounts:
2279 branchStatus = LineCoverageStatus.Covered if branchCount > 0 else LineCoverageStatus.Uncovered
2280 cc_Branch(branchStatus, branchCount, parent=line)
2282 summary.Aggregate()
2283 return summary