Source code for pyEDAA.Reports.CodeCoverage.LLVM

# ==================================================================================================================== #
#              _____ ____    _        _      ____                       _                                              #
#  _ __  _   _| ____|  _ \  / \      / \    |  _ \ ___ _ __   ___  _ __| |_ ___                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |_) / _ \ '_ \ / _ \| '__| __/ __|                                       #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _|  _ <  __/ |_) | (_) | |  | |_\__ \                                       #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_| \_\___| .__/ \___/|_|   \__|___/                                       #
# |_|    |___/                                        |_|                                                              #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²)                                                  #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
#
"""
LLVM's JSON code coverage export: a model of the format, read from a report and converted to the common model.

``llvm-cov export -format=text`` writes the format - type ``llvm.coverage.json.export`` - from the source-based code
coverage of Clang, and of other compilers based on LLVM, e.g. rustc or Swift. A report is validated against the JSON
Schema :file:`LLVM-Coverage-JSON.schema.json`, reverse-engineered from llvm-cov, which accepts the format versions
2.0.0 to 3.1.0. The format's model keeps what the report states: a :class:`Document` holds :class:`File` records - each
with its segments, branch regions, MC/DC records, expansions and summary -, and :class:`Function` records - each with
its regions, branch regions and MC/DC records.

* A report states a segment (:class:`Segment`) as a JSON array - a tuple of numbers and flags -, a file, a function or
  an expansion (:class:`Expansion`) as a JSON object.
* A report states a source region as a JSON array, a tuple of numbers: a :class:`Region`, a :class:`BranchRegion` or a
  :class:`MCDCRecord` with its :class:`TestVector` objects. A region names its source range by line and column, and
  the file by an index into the file paths of its function or expansion; :attr:`Base.FilePath` and :attr:`Base.File`
  look the index up.
* A summary (:class:`Summary`) holds the counters (:class:`Counters`) llvm-cov computed for a file or the whole report.

Each class of the format's model takes typed values in its constructor, so a model can be built by hand, too. A
classmethod ``Parse`` reads the class' JSON element and calls the constructor; :meth:`Document.Convert` parses a
report's files, functions and totals with it. Every element (:class:`Element`) knows the element containing it and the
report's root.

:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`:

* A file's path is relative to the directory common to all files, which becomes the report's source directory.
* A line's count is derived from the file's segments, as ``llvm-cov show`` derives it: the line is executable, if a
  region with a count starts on it, or a region with a count continues from a previous line - unless a skipped region
  starts the line. Its count is the largest count of the regions starting on it - gap regions excluded - and of the
  region continuing from a previous line.
* A branch region becomes two branches - its true and its false outcome - of the line it starts at, or of the line its
  macro is expanded at. The branch regions of a function's instantiations, e.g. of a template, are summed. A line with
  an outcome never taken is partially covered.
* A file becomes a source file unit, and each function a function of its file's unit, named as the report names it -
  without the file name of its translation unit, which prefixes a function local to it, e.g. ``Statistics.c:Square``.
  The format doesn't demangle C++ names.

.. rubric:: Example

.. code-block:: Python

   from pathlib import Path
   from pyEDAA.Reports.CodeCoverage.LLVM import Document

   report = Document(Path("coverage.json"), analyzeAndConvert=True)
   summary = report.ToCoverageSummary()
   print(f"{summary.FileCount} files: {summary.LineCoverage:.1%} of lines, {summary.BranchCoverage:.1%} of branches")
"""
from __future__                  import annotations

from enum                        import Enum
from json                        import JSONDecodeError, loads
from pathlib                     import Path
from re                          import match
from typing                      import Any, ClassVar, Generic, Iterable, Optional as Nullable, Self, TypeVar

from pyTooling.Common            import getFullyQualifiedName, readResourceFile
from pyTooling.Decorators        import export, readonly
from pyTooling.Exceptions        import ToolingException
from pyTooling.MetaClasses       import ExtendedType
from pyTooling.Stopwatch         import Stopwatch
from pyTooling.Versioning        import SemanticVersion

from pyEDAA.Reports              import Resources
from pyEDAA.Reports.CodeCoverage import AlreadyInCoverageReportError, Branch as cc_Branch, CodeCoverageError, \
                                        CoverageSummary, DuplicateFileError, Document as cc_Document, File as cc_File, \
                                        Function as cc_Function, Line as cc_Line, LineCoverageStatus, \
                                        SourceFile as cc_SourceFile


__all__ = ["SCHEMA"]

SCHEMA = "LLVM-Coverage-JSON.schema.json"  #: The JSON Schema a report is validated against.

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

ParentType = TypeVar("ParentType")


[docs] @export class RegionKind(Enum): """The kind of a source region, as LLVM's coverage mapping numbers it.""" Code = 0 #: Code associated with a counter. Expansion = 1 #: The source range of a macro expansion, which maps to a file of its own. Skipped = 2 #: Code skipped by the preprocessor - e.g. by ``#if 0`` -, empty lines and comments. Gap = 3 #: Code between two regions, whose count is a line's count only, if no other region starts on it. Branch = 4 #: A condition with the counts of its true and false outcome. MCDCDecision = 5 #: A decision of conditions, measured for MC/DC. MCDCBranch = 6 #: A condition of a decision measured for MC/DC, with the counts of its true and false outcome.
[docs] @export class Element(Generic[ParentType], metaclass=ExtendedType, slots=True): """ Base-class of every element of the format's model: a reference to the element containing it, and to the report's root. A container connects the elements given to its constructor; an element created with a ``parent`` is added to the parent's collection. When a file or function joins a report, the report becomes the root of every element below it. """ _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; assigned below the classes. _parent: Nullable[ParentType] #: The element containing this one, or ``None``. _root: Nullable[Report] #: The report's root, or ``None`` while the element isn't part of a report.
[docs] def __init__(self, *, parent: Nullable[ParentType] = None) -> None: """ Initialize an element with its parent and the parent's root. The parent doesn't list the element yet: the class adds it to the parent's collection, after setting its fields. :param parent: Optional, the element containing this one. Default: ``None``. :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`. """ if parent is None: self._parent = None self._root = None elif not isinstance(parent, self._PARENT_TYPE): typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE) ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex else: self._parent = parent self._root = parent._root
@readonly def Parent(self) -> Nullable[ParentType]: """ Read-only property to access the element containing this one (:attr:`_parent`). :returns: The parent, or ``None`` if the element belongs to none. """ return self._parent @readonly def Root(self) -> Nullable[Report]: """ Read-only property to access the report's root (:attr:`_root`). :returns: The report, or ``None`` while the element isn't part of a report. """ return self._root
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the element. A class containing elements overrides it, to set the root of those too. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root
[docs] @export class Base(Element[ParentType]): """ Base-class of the source regions: a source range, the file it is in, the file it expands to, and its kind. """ _lineStart: int #: Line the region starts at. _columnStart: int #: Column the region starts at. _lineEnd: int #: Line the region ends at. _columnEnd: int #: Column the region ends at. _fileID: Nullable[int] #: Index of the region's file in the file paths of its function or expansion. _expandedFileID: int #: Index of the file an expansion region expands to. _kind: RegionKind #: The kind of the region.
[docs] def __init__( self, lineStart: int, columnStart: int, lineEnd: int, columnEnd: int, fileID: Nullable[int], expandedFileID: int, kind: RegionKind, *, parent: Nullable[ParentType] = None ) -> None: """ Initialize the source range, the region's files and its kind. :param lineStart: Line the region starts at. :param columnStart: Column the region starts at. :param lineEnd: Line the region ends at. :param columnEnd: Column the region ends at. :param fileID: Index of the region's file in the file paths of its function or expansion; ``None``, if not stated. :param expandedFileID: Index of the file an expansion region expands to. :param kind: The kind of the region. :param parent: Optional, the element containing the region. Default: ``None``. :raises ValueError: If parameter ``lineStart`` is ``None``. :raises TypeError: If parameter ``lineStart`` isn't of type :class:`int`. :raises ValueError: If parameter ``lineStart`` is less than 1. :raises ValueError: If parameter ``columnStart`` is ``None``. :raises TypeError: If parameter ``columnStart`` isn't of type :class:`int`. :raises ValueError: If parameter ``columnStart`` is less than 1. :raises ValueError: If parameter ``lineEnd`` is ``None``. :raises TypeError: If parameter ``lineEnd`` isn't of type :class:`int`. :raises ValueError: If parameter ``lineEnd`` is less than 1. :raises ValueError: If parameter ``columnEnd`` is ``None``. :raises TypeError: If parameter ``columnEnd`` isn't of type :class:`int`. :raises ValueError: If parameter ``columnEnd`` is less than 1. :raises TypeError: If parameter ``fileID`` isn't of type :class:`int`. :raises ValueError: If parameter ``fileID`` is negative. :raises ValueError: If parameter ``fileID`` is no index into the file paths of parameter ``parent``. :raises ValueError: If parameter ``expandedFileID`` is ``None``. :raises TypeError: If parameter ``expandedFileID`` isn't of type :class:`int`. :raises ValueError: If parameter ``expandedFileID`` is negative. :raises ValueError: If parameter ``expandedFileID`` of an expansion region is no index into the file paths of parameter ``parent``. :raises ValueError: If parameter ``kind`` is ``None``. :raises TypeError: If parameter ``kind`` isn't of type :class:`RegionKind`. :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`. """ super().__init__(parent=parent) for name, position in ( ("lineStart", lineStart), ("columnStart", columnStart), ("lineEnd", lineEnd), ("columnEnd", columnEnd) ): if position is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(position, int): ex = TypeError(f"Parameter '{name}' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.") raise ex elif position < 1: ex = ValueError(f"Parameter '{name}' is less than 1.") ex.add_note(f"Got value '{position}'.") raise ex if fileID is not None: if not isinstance(fileID, int): ex = TypeError("Parameter 'fileID' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(fileID)}'.") raise ex elif fileID < 0: ex = ValueError("Parameter 'fileID' is negative.") ex.add_note(f"Got value '{fileID}'.") raise ex if expandedFileID is None: raise ValueError("Parameter 'expandedFileID' is None.") elif not isinstance(expandedFileID, int): ex = TypeError("Parameter 'expandedFileID' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(expandedFileID)}'.") raise ex elif expandedFileID < 0: ex = ValueError("Parameter 'expandedFileID' is negative.") ex.add_note(f"Got value '{expandedFileID}'.") raise ex if kind is None: raise ValueError("Parameter 'kind' is None.") elif not isinstance(kind, RegionKind): ex = TypeError("Parameter 'kind' is not of type 'RegionKind'.") ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.") raise ex if isinstance(parent, (Function, Expansion)): for name, value in ( ("fileID", fileID), ("expandedFileID", expandedFileID if kind is RegionKind.Expansion else None) ): if value is not None and value >= len(parent._filePaths): ex = ValueError(f"Parameter '{name}' is no index into the file paths of parameter 'parent'.") ex.add_note(f"Got value '{value}' for {len(parent._filePaths)} file paths.") raise ex self._lineStart = lineStart self._columnStart = columnStart self._lineEnd = lineEnd self._columnEnd = columnEnd self._fileID = fileID self._expandedFileID = expandedFileID self._kind = kind
@readonly def LineStart(self) -> int: """ Read-only property to access the line the region starts at (:attr:`_lineStart`). :returns: The line number, counted from 1. """ return self._lineStart @readonly def ColumnStart(self) -> int: """ Read-only property to access the column the region starts at (:attr:`_columnStart`). :returns: The column number, counted from 1. """ return self._columnStart @readonly def LineEnd(self) -> int: """ Read-only property to access the line the region ends at (:attr:`_lineEnd`). :returns: The line number, counted from 1. """ return self._lineEnd @readonly def ColumnEnd(self) -> int: """ Read-only property to access the column the region ends at (:attr:`_columnEnd`). :returns: The column number, counted from 1. """ return self._columnEnd @readonly def FileID(self) -> Nullable[int]: """ Read-only property to access the index of the region's file (:attr:`_fileID`). For a region listed by a file, the index refers to the file paths of a function the report doesn't name; :attr:`FilePath` is the file's path then. :returns: The index into the file paths of the region's function or expansion; ``None`` for an MC/DC record before format version 3.0.1. """ return self._fileID @readonly def ExpandedFileID(self) -> int: """ Read-only property to access the index of the file an expansion region expands to (:attr:`_expandedFileID`). :returns: The index into the file paths of the region's function or expansion; ``0`` for other kinds of regions. """ return self._expandedFileID @readonly def Kind(self) -> RegionKind: """ Read-only property to access the kind of the region (:attr:`_kind`). :returns: The kind. """ return self._kind @readonly def FilePath(self) -> Nullable[Path]: """ Read-only property to return the path of the region's file. A region of a function or expansion names its file by :attr:`FileID`, an index into the parent's file paths. A region listed by a file lies in that file. :returns: The path, as the compiler named it; ``None`` if the region belongs to no element, or for an MC/DC record of a function before format version 3.0.1, which states no file. """ if self._parent is None: return None elif isinstance(self._parent, File): return self._parent._path elif self._fileID is None: return None return self._parent._filePaths[self._fileID] @readonly def File(self) -> Nullable[File]: """ Read-only property to return the region's file, as the report lists it. :returns: The file of :attr:`FilePath` in the report's files; ``None`` if the region isn't part of a report, or the report doesn't list the file, e.g. one excluded by ``-ignore-filename-regex``. """ if self._root is None or (path := self.FilePath) is None: return None return self._root._files.get(path, None) @readonly def ExpandedFilePath(self) -> Nullable[Path]: """ Read-only property to return the path of the file an expansion region expands to (:attr:`ExpandedFileID`). :returns: The path, as the compiler named it; ``None`` for another kind of region, or a region belonging to no function or expansion. """ if self._kind is not RegionKind.Expansion or not isinstance(self._parent, (Function, Expansion)): return None return self._parent._filePaths[self._expandedFileID] @readonly def ExpandedFile(self) -> Nullable[File]: """ Read-only property to return the file an expansion region expands to, as the report lists it. :returns: The file of :attr:`ExpandedFilePath` in the report's files; ``None`` if there is none, the region isn't part of a report, or the report doesn't list the file. """ if self._root is None or (path := self.ExpandedFilePath) is None: return None return self._root._files.get(path, None)
[docs] @export class Region(Base["Function | Expansion"]): """ A region of a function or an expansion: a source range with a count. A region is ``[lineStart, columnStart, lineEnd, columnEnd, count, fileID, expandedFileID, kind]``. """ _count: int #: How often the region's code ran.
[docs] def __init__( self, lineStart: int, columnStart: int, lineEnd: int, columnEnd: int, count: int, fileID: int, expandedFileID: int, kind: RegionKind, *, parent: Nullable[Function | Expansion] = None ) -> None: """ Initialize a region, and add it to the regions of its function or to the target regions of its expansion. :param lineStart: Line the region starts at. :param columnStart: Column the region starts at. :param lineEnd: Line the region ends at. :param columnEnd: Column the region ends at. :param count: How often the region's code ran. :param fileID: Index of the region's file in the file paths of its function or expansion. :param expandedFileID: Index of the file an expansion region expands to. :param kind: The kind of the region. :param parent: Optional, the function or expansion the region belongs to. Default: ``None``. :raises ValueError: If parameter ``count`` is ``None``. :raises TypeError: If parameter ``count`` isn't of type :class:`int`. :raises ValueError: If parameter ``count`` is negative. :raises ValueError: If parameter ``fileID`` is ``None``. :raises TypeError: If parameter ``parent`` isn't of type :class:`Function` or :class:`Expansion`. """ super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) if fileID is None: raise ValueError("Parameter 'fileID' is None.") if count is None: raise ValueError("Parameter 'count' is None.") elif not isinstance(count, int): ex = TypeError("Parameter 'count' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") raise ex elif count < 0: ex = ValueError("Parameter 'count' is negative.") ex.add_note(f"Got value '{count}'.") raise ex self._count = count if parent is not None: (parent._regions if isinstance(parent, Function) else parent._targetRegions).append(self)
[docs] @classmethod def Parse(cls, region: list[Any], *, parent: Nullable[Function | Expansion] = None) -> Self: """ Parse a region from its JSON array. :param region: The JSON array of the region. :param parent: Optional, the function or expansion the region belongs to. Default: ``None``. :returns: The region. """ return cls(*region[:7], RegionKind(region[7]), parent=parent)
@readonly def Count(self) -> int: """ Read-only property to access how often the region's code ran (:attr:`_count`). :returns: The count. """ return self._count
[docs] @export class BranchRegion(Base["File | Function | Expansion"]): """ A branch region: a condition with the counts of its true and false outcome. A branch region is ``[lineStart, columnStart, lineEnd, columnEnd, trueCount, falseCount, fileID, expandedFileID, kind]``. """ _trueCount: int #: How often the condition was true. _falseCount: int #: How often the condition was false.
[docs] def __init__( self, lineStart: int, columnStart: int, lineEnd: int, columnEnd: int, trueCount: int, falseCount: int, fileID: int, expandedFileID: int, kind: RegionKind, *, parent: Nullable[File | Function | Expansion] = None ) -> None: """ Initialize a branch region, and add it to the branch regions of its file, function or expansion. :param lineStart: Line the branch region starts at. :param columnStart: Column the branch region starts at. :param lineEnd: Line the branch region ends at. :param columnEnd: Column the branch region ends at. :param trueCount: How often the condition was true. :param falseCount: How often the condition was false. :param fileID: Index of the branch region's file in the file paths of its function or expansion. :param expandedFileID: Index of the file an expansion region expands to; ``0`` for a branch region. :param kind: The kind of the region: a branch, or a condition measured for MC/DC. :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``. :raises ValueError: If parameter ``trueCount`` is ``None``. :raises TypeError: If parameter ``trueCount`` isn't of type :class:`int`. :raises ValueError: If parameter ``trueCount`` is negative. :raises ValueError: If parameter ``falseCount`` is ``None``. :raises TypeError: If parameter ``falseCount`` isn't of type :class:`int`. :raises ValueError: If parameter ``falseCount`` is negative. :raises ValueError: If parameter ``fileID`` is ``None``. :raises TypeError: If parameter ``parent`` isn't of type :class:`File`, :class:`Function` or :class:`Expansion`. """ super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) if fileID is None: raise ValueError("Parameter 'fileID' is None.") for name, value in (("trueCount", trueCount), ("falseCount", falseCount)): if value is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(value, int): ex = TypeError(f"Parameter '{name}' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex elif value < 0: ex = ValueError(f"Parameter '{name}' is negative.") ex.add_note(f"Got value '{value}'.") raise ex self._trueCount = trueCount self._falseCount = falseCount if parent is not None: parent._branches.append(self)
[docs] @classmethod def Parse(cls, branch: list[Any], *, parent: Nullable[File | Function | Expansion] = None) -> Self: """ Parse a branch region from its JSON array. :param branch: The JSON array of the branch region. :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``. :returns: The branch region. """ return cls(*branch[:8], RegionKind(branch[8]), parent=parent)
@readonly def TrueCount(self) -> int: """ Read-only property to access how often the condition was true (:attr:`_trueCount`). :returns: The count. """ return self._trueCount @readonly def FalseCount(self) -> int: """ Read-only property to access how often the condition was false (:attr:`_falseCount`). :returns: The count. """ return self._falseCount
[docs] @export class TestVector(Element["MCDCRecord"]): """ A test vector of an MC/DC decision - in format version 3.1.0 -: the values of its conditions, and the outcome. """ _conditions: list[Nullable[bool]] #: The values of the conditions; ``None`` for a condition not evaluated. _executed: bool #: Whether the test vector was executed. _result: Nullable[bool] #: The outcome of the decision.
[docs] def __init__( self, conditions: Iterable[Nullable[bool]], executed: bool, result: Nullable[bool], *, parent: Nullable[MCDCRecord] = None ) -> None: """ Initialize a test vector, and add it to the test vectors of its MC/DC record. :param conditions: The values of the conditions; ``None`` for a condition not evaluated. :param executed: Whether the test vector was executed. :param result: The outcome of the decision. :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``. :raises ValueError: If parameter ``conditions`` is ``None``. :raises TypeError: If parameter ``conditions`` isn't iterable. :raises TypeError: If parameter ``conditions`` contains an element not of type :class:`bool`, other than ``None``. :raises ValueError: If parameter ``executed`` is ``None``. :raises TypeError: If parameter ``executed`` isn't of type :class:`bool`. :raises TypeError: If parameter ``result`` isn't of type :class:`bool`. :raises TypeError: If parameter ``parent`` isn't of type :class:`MCDCRecord`. """ super().__init__(parent=parent) if conditions is None: raise ValueError("Parameter 'conditions' is None.") elif not isinstance(conditions, Iterable): ex = TypeError("Parameter 'conditions' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.") raise ex if executed is None: raise ValueError("Parameter 'executed' is None.") elif not isinstance(executed, bool): ex = TypeError("Parameter 'executed' is not of type 'bool'.") ex.add_note(f"Got type '{getFullyQualifiedName(executed)}'.") raise ex if result is not None and not isinstance(result, bool): ex = TypeError("Parameter 'result' is not of type 'bool'.") ex.add_note(f"Got type '{getFullyQualifiedName(result)}'.") raise ex self._conditions = conditionList = [] self._executed = executed self._result = result for condition in conditions: if condition is not None and not isinstance(condition, bool): ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.") ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.") raise ex conditionList.append(condition) if parent is not None: parent._testVectors.append(self)
[docs] @classmethod def Parse(cls, testVector: dict[str, Any], *, parent: Nullable[MCDCRecord] = None) -> Self: """ Parse a test vector from its JSON object. :param testVector: The JSON object of the test vector. :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``. :returns: The test vector. """ return cls(testVector["conditions"], testVector["executed"], testVector["result"], parent=parent)
@readonly def Conditions(self) -> list[Nullable[bool]]: """ Read-only property to access the values of the conditions (:attr:`_conditions`). :returns: The values, by condition; ``None`` for a condition not evaluated. """ return self._conditions @readonly def Executed(self) -> bool: """ Read-only property to access whether the test vector was executed (:attr:`_executed`). :returns: ``True`` for an executed test vector; ``False`` for a missing one, listed by ``-show-mcdc-non-executed-vectors``. """ return self._executed @readonly def Result(self) -> Nullable[bool]: """ Read-only property to access the outcome of the decision (:attr:`_result`). :returns: The outcome. """ return self._result
[docs] @export class MCDCRecord(Base["File | Function"]): """ An MC/DC record: a decision of conditions, and for each condition, whether a pair of test vectors showed its independent effect on the outcome. The record's array grew with the format version: ``[lineStart, columnStart, lineEnd, columnEnd, expandedFileID, kind, conditions]`` in 2.0.1; version 3.0.0 added the numbers of true and false decisions after the source range, version 3.0.1 the ``fileID`` before ``expandedFileID``, and version 3.1.0 the test vectors at the end. """ _trueDecisions: Nullable[int] #: Number of executed test vectors with the outcome true, if stated. _falseDecisions: Nullable[int] #: Number of executed test vectors with the outcome false, if stated. _conditions: list[bool] #: Whether a pair of test vectors showed the independent effect, by condition. _testVectors: list[TestVector] #: The test vectors; empty before format version 3.1.0.
[docs] def __init__( self, lineStart: int, columnStart: int, lineEnd: int, columnEnd: int, fileID: Nullable[int], expandedFileID: int, kind: RegionKind, conditions: Iterable[bool], trueDecisions: Nullable[int] = None, falseDecisions: Nullable[int] = None, testVectors: Nullable[Iterable[TestVector]] = None, *, parent: Nullable[File | Function] = None ) -> None: """ Initialize an MC/DC record, and add it to the MC/DC records of its file or function. :param lineStart: Line the decision starts at. :param columnStart: Column the decision starts at. :param lineEnd: Line the decision ends at. :param columnEnd: Column the decision ends at. :param fileID: Index of the decision's file in the file paths of its function or expansion; ``None``, if not stated. :param expandedFileID: Index of the file an expansion region expands to; ``0`` for an MC/DC record. :param kind: The kind of the region: an MC/DC decision. :param conditions: Whether a pair of test vectors showed the independent effect, by condition. :param trueDecisions: Optional, number of executed test vectors with the outcome true. Default: ``None``. :param falseDecisions: Optional, number of executed test vectors with the outcome false. Default: ``None``. :param testVectors: Optional, the test vectors. Default: none. :param parent: Optional, the file or function the MC/DC record belongs to. Default: ``None``. :raises ValueError: If parameter ``conditions`` is ``None``. :raises TypeError: If parameter ``conditions`` isn't iterable. :raises TypeError: If parameter ``conditions`` contains an element not of type :class:`bool`. :raises TypeError: If parameter ``trueDecisions`` isn't of type :class:`int`. :raises ValueError: If parameter ``trueDecisions`` is negative. :raises TypeError: If parameter ``falseDecisions`` isn't of type :class:`int`. :raises ValueError: If parameter ``falseDecisions`` is negative. :raises TypeError: If parameter ``testVectors`` isn't iterable. :raises TypeError: If parameter ``testVectors`` contains an element not of type :class:`TestVector`. :raises AlreadyInCoverageReportError: If parameter ``testVectors`` contains a test vector of another MC/DC record. :raises TypeError: If parameter ``parent`` isn't of type :class:`File` or :class:`Function`. """ super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) for name, value in (("trueDecisions", trueDecisions), ("falseDecisions", falseDecisions)): if value is None: continue elif not isinstance(value, int): ex = TypeError(f"Parameter '{name}' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex elif value < 0: ex = ValueError(f"Parameter '{name}' is negative.") ex.add_note(f"Got value '{value}'.") raise ex if conditions is None: raise ValueError("Parameter 'conditions' is None.") elif not isinstance(conditions, Iterable): ex = TypeError("Parameter 'conditions' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.") raise ex self._trueDecisions = trueDecisions self._falseDecisions = falseDecisions self._conditions = conditionList = [] self._testVectors = testVectorList = [] for condition in conditions: if not isinstance(condition, bool): ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.") ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.") raise ex conditionList.append(condition) if testVectors is not None: if not isinstance(testVectors, Iterable): ex = TypeError("Parameter 'testVectors' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(testVectors)}'.") raise ex for testVector in testVectors: if not isinstance(testVector, TestVector): ex = TypeError("Parameter 'testVectors' contains an element not of type 'TestVector'.") ex.add_note(f"Got type '{getFullyQualifiedName(testVector)}'.") raise ex elif testVector._parent is not None: raise AlreadyInCoverageReportError("Parameter 'testVectors' contains a test vector of another MC/DC record.") testVector._parent = self testVector._root = self._root testVectorList.append(testVector) if parent is not None: parent._mcdcRecords.append(self)
[docs] @classmethod def Parse(cls, record: list[Any], *, parent: Nullable[File | Function] = None) -> Self: """ Parse an MC/DC record from its JSON array, in the shape of its format version. :param record: The JSON array of the MC/DC record. :param parent: Optional, the file or function the MC/DC record belongs to. Default: ``None``. :returns: The MC/DC record. """ if len(record) == 7: return cls(*record[:4], None, record[4], RegionKind(record[5]), record[6], parent=parent) elif len(record) == 9: return cls(*record[:4], None, record[6], RegionKind(record[7]), record[8], record[4], record[5], parent=parent) return cls( *record[:4], record[6], record[7], RegionKind(record[8]), record[9], record[4], record[5], [TestVector.Parse(testVector) for testVector in record[10]] if len(record) > 10 else None, parent=parent )
@readonly def TrueDecisions(self) -> Nullable[int]: """ Read-only property to access the number of executed test vectors with the outcome true (:attr:`_trueDecisions`). :returns: The number; ``None`` before format version 3.0.0. """ return self._trueDecisions @readonly def FalseDecisions(self) -> Nullable[int]: """ Read-only property to access the number of executed test vectors with the outcome false (:attr:`_falseDecisions`). :returns: The number; ``None`` before format version 3.0.0. """ return self._falseDecisions @readonly def Conditions(self) -> list[bool]: """ Read-only property to access, by condition, whether a pair of test vectors showed its independent effect (:attr:`_conditions`). :returns: ``True`` for each condition shown to affect the outcome on its own. """ return self._conditions @readonly def TestVectors(self) -> list[TestVector]: """ Read-only property to access the test vectors (:attr:`_testVectors`). :returns: The test vectors; empty before format version 3.1.0. """ return self._testVectors
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the MC/DC record, and of its test vectors. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root for testVector in self._testVectors: testVector._SetRoot(root)
[docs] @export class Counters(Element["Summary"]): """ The counters of one kind of a summary: how many there are, and how many are covered. A summary counts lines, functions, regions, branches and MC/DC conditions. The summary given the counters becomes their parent. """ _count: int #: Number of lines, functions, regions, branches or MC/DC conditions. _covered: int #: Number of the covered ones. _notCovered: Nullable[int] #: Number of the uncovered ones, if stated. _percent: float #: The coverage in percent.
[docs] def __init__(self, count: int, covered: int, percent: float, notCovered: Nullable[int] = None) -> None: """ Initialize counters. :param count: Number of lines, functions, regions, branches or MC/DC conditions. :param covered: Number of the covered ones. :param percent: The coverage in percent. :param notCovered: Optional, number of the uncovered ones. Default: ``None``. :raises ValueError: If parameter ``count`` is ``None``. :raises TypeError: If parameter ``count`` isn't of type :class:`int`. :raises ValueError: If parameter ``count`` is negative. :raises ValueError: If parameter ``covered`` is ``None``. :raises TypeError: If parameter ``covered`` isn't of type :class:`int`. :raises ValueError: If parameter ``covered`` is negative. :raises ValueError: If parameter ``percent`` is ``None``. :raises TypeError: If parameter ``percent`` isn't of type :class:`float` or :class:`int`. :raises ValueError: If parameter ``percent`` is out of range 0..100. :raises TypeError: If parameter ``notCovered`` isn't of type :class:`int`. :raises ValueError: If parameter ``notCovered`` is negative. """ super().__init__() for name, value in (("count", count), ("covered", covered)): if value is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(value, int): ex = TypeError(f"Parameter '{name}' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") raise ex elif value < 0: ex = ValueError(f"Parameter '{name}' is negative.") ex.add_note(f"Got value '{value}'.") raise ex if notCovered is not None: if not isinstance(notCovered, int): ex = TypeError("Parameter 'notCovered' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(notCovered)}'.") raise ex elif notCovered < 0: ex = ValueError("Parameter 'notCovered' is negative.") ex.add_note(f"Got value '{notCovered}'.") raise ex if percent is None: raise ValueError("Parameter 'percent' is None.") elif not isinstance(percent, (float, int)): ex = TypeError("Parameter 'percent' is not of type 'float'.") ex.add_note(f"Got type '{getFullyQualifiedName(percent)}'.") raise ex elif not 0 <= percent <= 100: ex = ValueError("Parameter 'percent' is out of range 0..100.") ex.add_note(f"Got value '{percent}'.") raise ex self._count = count self._covered = covered self._notCovered = notCovered self._percent = percent
[docs] @classmethod def Parse(cls, counters: dict[str, Any]) -> Self: """ Parse counters from their JSON object. :param counters: The JSON object of the counters. :returns: The counters. """ return cls(counters["count"], counters["covered"], counters["percent"], counters.get("notcovered"))
@readonly def Count(self) -> int: """ Read-only property to access the number of lines, functions, regions, branches or MC/DC conditions (:attr:`_count`). :returns: The number. """ return self._count @readonly def Covered(self) -> int: """ Read-only property to access the number of covered ones (:attr:`_covered`). :returns: The number. """ return self._covered @readonly def NotCovered(self) -> Nullable[int]: """ Read-only property to access the number of uncovered ones (:attr:`_notCovered`). :returns: The number, or ``None`` for lines, functions and instantiations, which don't state it. """ return self._notCovered @readonly def Percent(self) -> float: """ Read-only property to access the coverage (:attr:`_percent`). :returns: The coverage in percent. """ return self._percent
[docs] @export class Summary(Element["File | Report"]): """ A ``summary``: the counters llvm-cov computed for a file or the whole report. llvm-cov counts a file's lines, regions and branches per function, and merges a function's instantiations by taking the best one. The file or report given the summary becomes its parent. """ _lines: Counters #: The counters of lines. _functions: Counters #: The counters of functions, a function's instantiations counted once. _instantiations: Counters #: The counters of function instantiations. _regions: Counters #: The counters of code regions. _branches: Nullable[Counters] #: The counters of branch outcomes, if stated. _mcdc: Nullable[Counters] #: The counters of MC/DC conditions, if stated.
[docs] def __init__( self, lines: Counters, functions: Counters, instantiations: Counters, regions: Counters, branches: Nullable[Counters] = None, mcdc: Nullable[Counters] = None ) -> None: """ Initialize a summary. :param lines: The counters of lines. :param functions: The counters of functions, a function's instantiations counted once. :param instantiations: The counters of function instantiations. :param regions: The counters of code regions. :param branches: Optional, the counters of branch outcomes. Default: ``None``. :param mcdc: Optional, the counters of MC/DC conditions. Default: ``None``. :raises ValueError: If parameter ``lines`` is ``None``. :raises TypeError: If parameter ``lines`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``lines`` are the counters of another summary. :raises ValueError: If parameter ``functions`` is ``None``. :raises TypeError: If parameter ``functions`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``functions`` are the counters of another summary. :raises ValueError: If parameter ``instantiations`` is ``None``. :raises TypeError: If parameter ``instantiations`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``instantiations`` are the counters of another summary. :raises ValueError: If parameter ``regions`` is ``None``. :raises TypeError: If parameter ``regions`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``regions`` are the counters of another summary. :raises TypeError: If parameter ``branches`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``branches`` are the counters of another summary. :raises TypeError: If parameter ``mcdc`` isn't of type :class:`Counters`. :raises AlreadyInCoverageReportError: If parameter ``mcdc`` are the counters of another summary. """ super().__init__() for name, counters in ( ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions) ): if counters is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(counters, Counters): ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.") ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.") raise ex for name, counters in (("branches", branches), ("mcdc", mcdc)): if counters is not None and not isinstance(counters, Counters): ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.") ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.") raise ex for name, counters in ( ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions), ("branches", branches), ("mcdc", mcdc) ): if counters is None: continue elif counters._parent is not None: raise AlreadyInCoverageReportError(f"Parameter '{name}' are the counters of another summary.") counters._parent = self self._lines = lines self._functions = functions self._instantiations = instantiations self._regions = regions self._branches = branches self._mcdc = mcdc
[docs] @classmethod def Parse(cls, summary: dict[str, Any]) -> Self: """ Parse a summary from its JSON object. :param summary: The JSON object ``summary`` or ``totals``. :returns: The summary. """ return cls( Counters.Parse(summary["lines"]), Counters.Parse(summary["functions"]), Counters.Parse(summary["instantiations"]), Counters.Parse(summary["regions"]), Counters.Parse(summary["branches"]) if "branches" in summary else None, Counters.Parse(summary["mcdc"]) if "mcdc" in summary else None )
@readonly def Lines(self) -> Counters: """ Read-only property to access the counters of lines (:attr:`_lines`). :returns: The counters. """ return self._lines @readonly def Functions(self) -> Counters: """ Read-only property to access the counters of functions, a function's instantiations counted once (:attr:`_functions`). :returns: The counters. """ return self._functions @readonly def Instantiations(self) -> Counters: """ Read-only property to access the counters of function instantiations (:attr:`_instantiations`). :returns: The counters. """ return self._instantiations @readonly def Regions(self) -> Counters: """ Read-only property to access the counters of code regions (:attr:`_regions`). :returns: The counters. """ return self._regions @readonly def Branches(self) -> Nullable[Counters]: """ Read-only property to access the counters of branch outcomes - two per branch region - (:attr:`_branches`). :returns: The counters, or ``None`` before LLVM 12. """ return self._branches @readonly def MCDC(self) -> Nullable[Counters]: """ Read-only property to access the counters of MC/DC conditions (:attr:`_mcdc`). :returns: The counters, or ``None`` before LLVM 18. """ return self._mcdc
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the summary, and of its counters. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root for counters in (self._lines, self._functions, self._instantiations, self._regions, self._branches, self._mcdc): if counters is not None: counters._SetRoot(root)
_Summary = Summary
[docs] @export class Segment(Element["File"]): """ A segment of a file: from a position on, the code has a count - until the next segment. A segment is ``[line, column, count, hasCount, isRegionEntry, isGapRegion]``. Format version 2.0.0 has no ``isGapRegion``. """ _line: int #: Line the segment starts at. _column: int #: Column the segment starts at. _count: int #: The count of the code from here on. _hasCount: bool #: Whether the code from here on has a count; false e.g. after a function's end. _isRegionEntry: bool #: Whether a region starts here; false, where a region continues after a nested one. _isGapRegion: bool #: Whether a gap region starts here.
[docs] def __init__( self, line: int, column: int, count: int, hasCount: bool, isRegionEntry: bool, isGapRegion: bool = False, *, parent: Nullable[File] = None ) -> None: """ Initialize a segment, and add it to the segments of its file. :param line: Line the segment starts at. :param column: Column the segment starts at. :param count: The count of the code from here on. :param hasCount: Whether the code from here on has a count. :param isRegionEntry: Whether a region starts here. :param isGapRegion: Optional, whether a gap region starts here. Default: ``False``. :param parent: Optional, the file the segment belongs to. Default: ``None``. :raises ValueError: If parameter ``line`` is ``None``. :raises TypeError: If parameter ``line`` isn't of type :class:`int`. :raises ValueError: If parameter ``line`` is less than 1. :raises ValueError: If parameter ``column`` is ``None``. :raises TypeError: If parameter ``column`` isn't of type :class:`int`. :raises ValueError: If parameter ``column`` is less than 1. :raises ValueError: If parameter ``count`` is ``None``. :raises TypeError: If parameter ``count`` isn't of type :class:`int`. :raises ValueError: If parameter ``count`` is negative. :raises ValueError: If parameter ``hasCount`` is ``None``. :raises TypeError: If parameter ``hasCount`` isn't of type :class:`bool`. :raises ValueError: If parameter ``isRegionEntry`` is ``None``. :raises TypeError: If parameter ``isRegionEntry`` isn't of type :class:`bool`. :raises ValueError: If parameter ``isGapRegion`` is ``None``. :raises TypeError: If parameter ``isGapRegion`` isn't of type :class:`bool`. :raises TypeError: If parameter ``parent`` isn't of type :class:`File`. """ super().__init__(parent=parent) for name, position in (("line", line), ("column", column)): if position is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(position, int): ex = TypeError(f"Parameter '{name}' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.") raise ex elif position < 1: ex = ValueError(f"Parameter '{name}' is less than 1.") ex.add_note(f"Got value '{position}'.") raise ex if count is None: raise ValueError("Parameter 'count' is None.") elif not isinstance(count, int): ex = TypeError("Parameter 'count' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") raise ex elif count < 0: ex = ValueError("Parameter 'count' is negative.") ex.add_note(f"Got value '{count}'.") raise ex for name, flag in (("hasCount", hasCount), ("isRegionEntry", isRegionEntry), ("isGapRegion", isGapRegion)): if flag is None: raise ValueError(f"Parameter '{name}' is None.") elif not isinstance(flag, bool): ex = TypeError(f"Parameter '{name}' is not of type 'bool'.") ex.add_note(f"Got type '{getFullyQualifiedName(flag)}'.") raise ex self._line = line self._column = column self._count = count self._hasCount = hasCount self._isRegionEntry = isRegionEntry self._isGapRegion = isGapRegion if parent is not None: parent._segments.append(self)
[docs] @classmethod def Parse(cls, segment: list[Any], *, parent: Nullable[File] = None) -> Self: """ Parse a segment from its JSON array. :param segment: The JSON array of the segment. :param parent: Optional, the file the segment belongs to. Default: ``None``. :returns: The segment. """ return cls(*segment[:5], segment[5] if len(segment) > 5 else False, parent=parent)
@readonly def Line(self) -> int: """ Read-only property to access the line the segment starts at (:attr:`_line`). :returns: The line number, counted from 1. """ return self._line @readonly def Column(self) -> int: """ Read-only property to access the column the segment starts at (:attr:`_column`). :returns: The column number, counted from 1. """ return self._column @readonly def Count(self) -> int: """ Read-only property to access the count of the code from the segment's start on (:attr:`_count`). :returns: How often the code ran; ``0`` without a count. """ return self._count @readonly def HasCount(self) -> bool: """ Read-only property to access whether the code from the segment's start on has a count (:attr:`_hasCount`). :returns: ``False`` e.g. after the end of a function, or in a skipped region. """ return self._hasCount @readonly def IsRegionEntry(self) -> bool: """ Read-only property to access whether a region starts at the segment's start (:attr:`_isRegionEntry`). :returns: ``False``, where a region continues after a region nested in it. """ return self._isRegionEntry @readonly def IsGapRegion(self) -> bool: """ Read-only property to access whether a gap region starts at the segment's start (:attr:`_isGapRegion`). :returns: ``True`` for a gap region; ``False`` in format version 2.0.0. """ return self._isGapRegion
[docs] @export class Expansion(Element["File"]): """ A macro expansion in a file: the region it is expanded at, the regions of the function it is in, and its branches. """ _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index. _sourceRegion: Region #: The region the macro is expanded at. _targetRegions: list[Region] #: The regions of the function the expansion is in. _branches: list[BranchRegion] #: The branch regions in the expansion, and in the expansions nested in it.
[docs] def __init__( self, filePaths: Iterable[Path], sourceRegion: Region, targetRegions: Iterable[Region], branches: Nullable[Iterable[BranchRegion]] = None, *, parent: Nullable[File] = None ) -> None: """ Initialize an expansion, and add it to the expansions of its file. :param filePaths: The paths of the files, which the file IDs of the regions index. :param sourceRegion: The region the macro is expanded at. :param targetRegions: The regions of the function the expansion is in. :param branches: Optional, the branch regions in the expansion, and in the expansions nested in it. Default: none. :param parent: Optional, the file the expansion belongs to. Default: ``None``. :raises ValueError: If parameter ``filePaths`` is ``None``. :raises TypeError: If parameter ``filePaths`` isn't iterable. :raises TypeError: If parameter ``filePaths`` contains an element not of type :class:`~pathlib.Path`. :raises ValueError: If parameter ``sourceRegion`` is ``None``. :raises TypeError: If parameter ``sourceRegion`` isn't of type :class:`Region`. :raises AlreadyInCoverageReportError: If parameter ``sourceRegion`` is a region of another element. :raises ValueError: If parameter ``sourceRegion`` is a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises ValueError: If parameter ``targetRegions`` is ``None``. :raises TypeError: If parameter ``targetRegions`` isn't iterable. :raises TypeError: If parameter ``targetRegions`` contains an element not of type :class:`Region`. :raises AlreadyInCoverageReportError: If parameter ``targetRegions`` contains a region of another element. :raises ValueError: If parameter ``targetRegions`` contains a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises TypeError: If parameter ``branches`` isn't iterable. :raises TypeError: If parameter ``branches`` contains an element not of type :class:`BranchRegion`. :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element. :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises TypeError: If parameter ``parent`` isn't of type :class:`File`. """ super().__init__(parent=parent) if filePaths is None: raise ValueError("Parameter 'filePaths' is None.") elif not isinstance(filePaths, Iterable): ex = TypeError("Parameter 'filePaths' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.") raise ex if sourceRegion is None: raise ValueError("Parameter 'sourceRegion' is None.") elif not isinstance(sourceRegion, Region): ex = TypeError("Parameter 'sourceRegion' is not of type 'Region'.") ex.add_note(f"Got type '{getFullyQualifiedName(sourceRegion)}'.") raise ex elif sourceRegion._parent is not None: raise AlreadyInCoverageReportError("Parameter 'sourceRegion' is a region of another element.") if targetRegions is None: raise ValueError("Parameter 'targetRegions' is None.") self._filePaths = filePathList = [] self._sourceRegion = sourceRegion self._targetRegions = [] self._branches = [] for filePath in filePaths: if not isinstance(filePath, Path): ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.") raise ex filePathList.append(filePath) for name, elements, elementType, target in ( ("targetRegions", targetRegions, Region, self._targetRegions), ("branches", branches, BranchRegion, self._branches) ): if elements is None: continue elif not isinstance(elements, Iterable): ex = TypeError(f"Parameter '{name}' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") raise ex for element in elements: if not isinstance(element, elementType): ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.") ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") raise ex elif element._parent is not None: raise AlreadyInCoverageReportError(f"Parameter '{name}' contains a region of another element.") elif element._fileID is not None and element._fileID >= len(self._filePaths): ex = ValueError(f"Parameter '{name}' contains a region of a file ID out of range.") ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.") raise ex elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): ex = ValueError(f"Parameter '{name}' contains an expansion region of an expanded file ID out of range.") ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.") raise ex element._parent = self element._root = self._root target.append(element) if sourceRegion._fileID >= len(self._filePaths): ex = ValueError("Parameter 'sourceRegion' is a region of a file ID out of range.") ex.add_note(f"Got value '{sourceRegion._fileID}' for {len(self._filePaths)} file paths.") raise ex elif sourceRegion._kind is RegionKind.Expansion and sourceRegion._expandedFileID >= len(self._filePaths): ex = ValueError("Parameter 'sourceRegion' is an expansion region of an expanded file ID out of range.") ex.add_note(f"Got value '{sourceRegion._expandedFileID}' for {len(self._filePaths)} file paths.") raise ex sourceRegion._parent = self sourceRegion._root = self._root if parent is not None: parent._expansions.append(self)
[docs] @classmethod def Parse(cls, expansion: dict[str, Any], *, parent: Nullable[File] = None) -> Self: """ Parse an expansion from its JSON object. :param expansion: The JSON object of the expansion. :param parent: Optional, the file the expansion belongs to. Default: ``None``. :returns: The expansion. """ return cls( [Path(filename.replace("\\", "/")) for filename in expansion["filenames"]], Region.Parse(expansion["source_region"]), [Region.Parse(region) for region in expansion["target_regions"]], [BranchRegion.Parse(branch) for branch in expansion.get("branches", [])], parent=parent )
@readonly def FilePaths(self) -> list[Path]: """ Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`). :returns: The paths, as the compiler named them. """ return self._filePaths @readonly def SourceRegion(self) -> Region: """ Read-only property to access the region the macro is expanded at (:attr:`_sourceRegion`). :returns: The region. """ return self._sourceRegion @readonly def TargetRegions(self) -> list[Region]: """ Read-only property to access the regions of the function the expansion is in (:attr:`_targetRegions`). :returns: The regions. """ return self._targetRegions @readonly def Branches(self) -> list[BranchRegion]: """ Read-only property to access the branch regions in the expansion, and in the expansions nested in it (:attr:`_branches`). :returns: The branch regions; empty before LLVM 12. """ return self._branches
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the expansion, and of its source region, target regions and branch regions. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root for region in (self._sourceRegion, *self._targetRegions, *self._branches): region._SetRoot(root)
[docs] @export class File(Element["Report"]): """ A file: its segments, branch regions, MC/DC records and expansions, and its summary. A report written with ``-summary-only`` has only the summary, one written with ``-skip-expansions`` no expansions. """ _path: _Path #: The file's path, as the compiler named it. _segments: list[Segment] #: The segments, by position. _branches: list[BranchRegion] #: The branch regions of the file's functions. _mcdcRecords: list[MCDCRecord] #: The MC/DC records of the file's functions. _expansions: list[Expansion] #: The macro expansions in the file. _summary: _Summary #: The counters llvm-cov computed.
[docs] def __init__( self, path: Path, summary: Summary, segments: Nullable[Iterable[Segment]] = None, branches: Nullable[Iterable[BranchRegion]] = None, mcdcRecords: Nullable[Iterable[MCDCRecord]] = None, expansions: Nullable[Iterable[Expansion]] = None, *, parent: Nullable[Report] = None ) -> None: """ Initialize a file, and add it to the files of its report. :param path: The file's path, as the compiler named it. :param summary: The counters llvm-cov computed. :param segments: Optional, the segments, by position. Default: none. :param branches: Optional, the branch regions of the file's functions. Default: none. :param mcdcRecords: Optional, the MC/DC records of the file's functions. Default: none. :param expansions: Optional, the macro expansions in the file. Default: none. :param parent: Optional, the report the file belongs to. Default: ``None``. :raises ValueError: If parameter ``path`` is ``None``. :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`. :raises ValueError: If parameter ``summary`` is ``None``. :raises TypeError: If parameter ``summary`` isn't of type :class:`Summary`. :raises AlreadyInCoverageReportError: If parameter ``summary`` is the summary of another element. :raises TypeError: If parameter ``segments`` isn't iterable. :raises TypeError: If parameter ``segments`` contains an element not of type :class:`Segment`. :raises AlreadyInCoverageReportError: If parameter ``segments`` contains an element of another element. :raises TypeError: If parameter ``branches`` isn't iterable. :raises TypeError: If parameter ``branches`` contains an element not of type :class:`BranchRegion`. :raises AlreadyInCoverageReportError: If parameter ``branches`` contains an element of another element. :raises TypeError: If parameter ``mcdcRecords`` isn't iterable. :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type :class:`MCDCRecord`. :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains an element of another element. :raises TypeError: If parameter ``expansions`` isn't iterable. :raises TypeError: If parameter ``expansions`` contains an element not of type :class:`Expansion`. :raises AlreadyInCoverageReportError: If parameter ``expansions`` contains an element of another element. :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`. :raises DuplicateFileError: If parameter ``parent`` has a file of the same path. """ super().__init__(parent=parent) if path is None: raise ValueError("Parameter 'path' is None.") elif not isinstance(path, Path): ex = TypeError("Parameter 'path' is not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") raise ex elif parent is not None and path in parent._files: ex = DuplicateFileError("Parameter 'parent' has a file of the same path.") ex.add_note(f"Got value '{path.as_posix()}'.") raise ex if summary is None: raise ValueError("Parameter 'summary' is None.") elif not isinstance(summary, Summary): ex = TypeError("Parameter 'summary' is not of type 'Summary'.") ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.") raise ex elif summary._parent is not None: raise AlreadyInCoverageReportError("Parameter 'summary' is the summary of another element.") self._path = path self._segments = [] self._branches = [] self._mcdcRecords = [] self._expansions = [] self._summary = summary for name, elements, elementType, target in ( ("segments", segments, Segment, self._segments), ("branches", branches, BranchRegion, self._branches), ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords), ("expansions", expansions, Expansion, self._expansions) ): if elements is None: continue elif not isinstance(elements, Iterable): ex = TypeError(f"Parameter '{name}' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") raise ex for element in elements: if not isinstance(element, elementType): ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.") ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") raise ex elif element._parent is not None: raise AlreadyInCoverageReportError(f"Parameter '{name}' contains an element of another element.") element._parent = self element._SetRoot(self._root) target.append(element) summary._parent = self summary._SetRoot(self._root) if parent is not None: parent._files[path] = self
[docs] @classmethod def Parse(cls, file: dict[str, Any], *, parent: Nullable[Report] = None) -> Self: """ Parse a file from its JSON object. :param file: The JSON object of the file. :param parent: Optional, the report the file belongs to. Default: ``None``. :returns: The file. """ return cls( Path(file["filename"].replace("\\", "/")), Summary.Parse(file["summary"]), [Segment.Parse(segment) for segment in file.get("segments", [])], [BranchRegion.Parse(branch) for branch in file.get("branches", [])], [MCDCRecord.Parse(record) for record in file.get("mcdc_records", [])], [Expansion.Parse(expansion) for expansion in file.get("expansions", [])], parent=parent )
@readonly def Path(self) -> Path: """ Read-only property to access the file's path (:attr:`_path`). :returns: The path, as the compiler named it - usually absolute. """ return self._path @readonly def Segments(self) -> list[Segment]: """ Read-only property to access the segments (:attr:`_segments`). :returns: The segments, by position; empty in a summary-only report. """ return self._segments @readonly def Branches(self) -> list[BranchRegion]: """ Read-only property to access the branch regions of the file's functions (:attr:`_branches`). Which branch regions of a macro expansion are listed here, depends on the LLVM version. :returns: The branch regions. """ return self._branches @readonly def MCDCRecords(self) -> list[MCDCRecord]: """ Read-only property to access the MC/DC records of the file's functions (:attr:`_mcdcRecords`). :returns: The MC/DC records. """ return self._mcdcRecords @readonly def Expansions(self) -> list[Expansion]: """ Read-only property to access the macro expansions in the file (:attr:`_expansions`). :returns: The expansions. """ return self._expansions @readonly def Summary(self) -> Summary: """ Read-only property to access the counters llvm-cov computed (:attr:`_summary`). :returns: The summary. """ return self._summary
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the file, and of its segments, branch regions, MC/DC records, expansions and summary. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root for element in (*self._segments, *self._branches, *self._mcdcRecords, *self._expansions, self._summary): element._SetRoot(root)
[docs] @export class Function(Element["Report"]): """ A function - an instantiation of a template is a function of its own -: how often it was called, its regions, branch regions and MC/DC records, and the files they are in. """ _name: str #: The function's name, as the profile names it - e.g. mangled. _count: int #: How often the function was called. _regions: list[Region] #: The regions. _branches: list[BranchRegion] #: The branch regions. _mcdcRecords: list[MCDCRecord] #: The MC/DC records. _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index.
[docs] def __init__( self, name: str, count: int, regions: Iterable[Region], filePaths: Iterable[Path], branches: Nullable[Iterable[BranchRegion]] = None, mcdcRecords: Nullable[Iterable[MCDCRecord]] = None, *, parent: Nullable[Report] = None ) -> None: """ Initialize a function, and add it to the functions of its report. :param name: The function's name, as the profile names it - e.g. mangled. :param count: How often the function was called. :param regions: The regions. :param filePaths: The paths of the files, which the file IDs of the regions index. :param branches: Optional, the branch regions. Default: none. :param mcdcRecords: Optional, the MC/DC records. Default: none. :param parent: Optional, the report the function belongs to. Default: ``None``. :raises ValueError: If parameter ``name`` is ``None``. :raises TypeError: If parameter ``name`` isn't of type :class:`str`. :raises ValueError: If parameter ``name`` is empty. :raises ValueError: If parameter ``count`` is ``None``. :raises TypeError: If parameter ``count`` isn't of type :class:`int`. :raises ValueError: If parameter ``count`` is negative. :raises ValueError: If parameter ``regions`` is ``None``. :raises TypeError: If parameter ``regions`` isn't iterable. :raises TypeError: If parameter ``regions`` contains an element not of type :class:`Region`. :raises AlreadyInCoverageReportError: If parameter ``regions`` contains a region of another element. :raises ValueError: If parameter ``regions`` contains a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises ValueError: If parameter ``filePaths`` is ``None``. :raises TypeError: If parameter ``filePaths`` isn't iterable. :raises TypeError: If parameter ``filePaths`` contains an element not of type :class:`~pathlib.Path`. :raises TypeError: If parameter ``branches`` isn't iterable. :raises TypeError: If parameter ``branches`` contains an element not of type :class:`BranchRegion`. :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element. :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises TypeError: If parameter ``mcdcRecords`` isn't iterable. :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type :class:`MCDCRecord`. :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains a region of another element. :raises ValueError: If parameter ``mcdcRecords`` contains a region of a file ID or an expanded file ID out of range of parameter ``filePaths``. :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`. """ super().__init__(parent=parent) if name is None: raise ValueError("Parameter 'name' is None.") elif not isinstance(name, str): ex = TypeError("Parameter 'name' is not of type 'str'.") ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex elif name == "": raise ValueError("Parameter 'name' is empty.") if count is None: raise ValueError("Parameter 'count' is None.") elif not isinstance(count, int): ex = TypeError("Parameter 'count' is not of type 'int'.") ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") raise ex elif count < 0: ex = ValueError("Parameter 'count' is negative.") ex.add_note(f"Got value '{count}'.") raise ex if regions is None: raise ValueError("Parameter 'regions' is None.") if filePaths is None: raise ValueError("Parameter 'filePaths' is None.") elif not isinstance(filePaths, Iterable): ex = TypeError("Parameter 'filePaths' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.") raise ex self._name = name self._count = count self._regions = [] self._branches = [] self._mcdcRecords = [] self._filePaths = filePathList = [] for filePath in filePaths: if not isinstance(filePath, Path): ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.") ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.") raise ex filePathList.append(filePath) for parameterName, elements, elementType, target in ( ("regions", regions, Region, self._regions), ("branches", branches, BranchRegion, self._branches), ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords) ): if elements is None: continue elif not isinstance(elements, Iterable): ex = TypeError(f"Parameter '{parameterName}' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") raise ex for element in elements: if not isinstance(element, elementType): ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{elementType.__name__}'.") ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") raise ex elif element._parent is not None: raise AlreadyInCoverageReportError(f"Parameter '{parameterName}' contains a region of another element.") elif element._fileID is not None and element._fileID >= len(self._filePaths): ex = ValueError(f"Parameter '{parameterName}' contains a region of a file ID out of range.") ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.") raise ex elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): ex = ValueError( f"Parameter '{parameterName}' contains an expansion region of an expanded file ID out of range." ) ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.") raise ex element._parent = self element._SetRoot(self._root) target.append(element) if parent is not None: parent._functions.append(self)
[docs] @classmethod def Parse(cls, function: dict[str, Any], *, parent: Nullable[Report] = None) -> Self: """ Parse a function from its JSON object. :param function: The JSON object of the function. :param parent: Optional, the report the function belongs to. Default: ``None``. :returns: The function. """ return cls( function["name"], function["count"], [Region.Parse(region) for region in function["regions"]], [Path(filename.replace("\\", "/")) for filename in function["filenames"]], [BranchRegion.Parse(branch) for branch in function.get("branches", [])], [MCDCRecord.Parse(record) for record in function.get("mcdc_records", [])], parent=parent )
@readonly def Name(self) -> str: """ Read-only property to access the function's name (:attr:`_name`). :returns: The name, as the profile names it: mangled, and prefixed by the file name of its translation unit, if local to it - e.g. ``Statistics.c:Square``. """ return self._name @readonly def Count(self) -> int: """ Read-only property to access how often the function was called (:attr:`_count`). :returns: The count. """ return self._count @readonly def Regions(self) -> list[Region]: """ Read-only property to access the regions (:attr:`_regions`). :returns: The regions. """ return self._regions @readonly def Branches(self) -> list[BranchRegion]: """ Read-only property to access the branch regions (:attr:`_branches`). :returns: The branch regions, also those in macro expansions. """ return self._branches @readonly def MCDCRecords(self) -> list[MCDCRecord]: """ Read-only property to access the MC/DC records (:attr:`_mcdcRecords`). :returns: The MC/DC records. """ return self._mcdcRecords @readonly def FilePaths(self) -> list[Path]: """ Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`). :returns: The paths, as the compiler named them. """ return self._filePaths @readonly def MainFileID(self) -> Nullable[int]: """ Read-only property to return the file ID of the file the function is in: the first file no expansion region expands to. :returns: The index into :attr:`FilePaths`, or ``None`` if every file is expanded to. """ expanded = {region._expandedFileID for region in self._regions if region._kind is RegionKind.Expansion} return next((fileID for fileID in range(len(self._filePaths)) if fileID not in expanded), None) @readonly def MainFilePath(self) -> Nullable[Path]: """ Read-only property to return the path of the file the function is in (:attr:`MainFileID`). :returns: The path, as the compiler named it; ``None`` if every file is expanded to. """ return None if (mainFileID := self.MainFileID) is None else self._filePaths[mainFileID] @readonly def MainFile(self) -> Nullable[File]: """ Read-only property to return the file the function is in, as the report lists it. :returns: The file of :attr:`MainFilePath` in the report's files; ``None`` if the function isn't part of a report, every file is expanded to, or the report doesn't list the file. """ if self._root is None or (path := self.MainFilePath) is None: return None return self._root._files.get(path, None)
[docs] def _SetRoot(self, root: Nullable[Report]) -> None: """ Set the report's root of the function, and of its regions, branch regions and MC/DC records. :param root: The report's root, or ``None`` while the element isn't part of a report. """ self._root = root for element in (*self._regions, *self._branches, *self._mcdcRecords): element._SetRoot(root)
[docs] @export class Report(metaclass=ExtendedType, mixin=True): """ The report's root: the format version, the files, the functions, and the totals. It is the root of every element below it. """ _root: Report #: The report's root: the report itself. _version: Nullable[SemanticVersion] #: Version of the report format. _files: dict[Path, File] #: The files, by path. _functions: list[Function] #: The functions. _totals: Nullable[Summary] #: The counters of the whole report.
[docs] def __init__( self, version: Nullable[SemanticVersion] = None, files: Nullable[Iterable[File]] = None, functions: Nullable[Iterable[Function]] = None, totals: Nullable[Summary] = None ) -> None: """ Initialize a report; empty, without parameters. :param version: Optional, version of the report format. Default: ``None``. :param files: Optional, the files. Default: none. :param functions: Optional, the functions. Default: none. :param totals: Optional, the counters of the whole report. Default: ``None``. :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`. :raises TypeError: If parameter ``files`` isn't iterable. :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`. :raises DuplicateFileError: If parameter ``files`` contains two files of the same path. :raises AlreadyInCoverageReportError: If parameter ``files`` contains an element of another report. :raises TypeError: If parameter ``functions`` isn't iterable. :raises TypeError: If parameter ``functions`` contains an element not of type :class:`Function`. :raises AlreadyInCoverageReportError: If parameter ``functions`` contains an element of another report. :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`. :raises AlreadyInCoverageReportError: If parameter ``totals`` is the summary of another element. """ if version is not None and not isinstance(version, SemanticVersion): ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") raise ex if totals is not None and not isinstance(totals, Summary): ex = TypeError("Parameter 'totals' is not of type 'Summary'.") ex.add_note(f"Got type '{getFullyQualifiedName(totals)}'.") raise ex elif totals is not None and totals._parent is not None: raise AlreadyInCoverageReportError("Parameter 'totals' is the summary of another element.") self._root = self self._version = version self._files = {} self._functions = functionList = [] self._totals = totals if files is not None: if not isinstance(files, Iterable): ex = TypeError("Parameter 'files' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.") raise ex for file in files: if not isinstance(file, File): ex = TypeError("Parameter 'files' contains an element not of type 'File'.") ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") raise ex elif file._parent is not None: raise AlreadyInCoverageReportError("Parameter 'files' contains a file of another report.") elif file._path in self._files: ex = DuplicateFileError("Parameter 'files' contains two files of the same path.") ex.add_note(f"Got value '{file._path.as_posix()}'.") raise ex file._parent = self file._SetRoot(self) self._files[file._path] = file if functions is not None: if not isinstance(functions, Iterable): ex = TypeError("Parameter 'functions' is not iterable.") ex.add_note(f"Got type '{getFullyQualifiedName(functions)}'.") raise ex for function in functions: if not isinstance(function, Function): ex = TypeError("Parameter 'functions' contains an element not of type 'Function'.") ex.add_note(f"Got type '{getFullyQualifiedName(function)}'.") raise ex elif function._parent is not None: raise AlreadyInCoverageReportError("Parameter 'functions' contains a function of another report.") function._parent = self function._SetRoot(self) functionList.append(function) if totals is not None: totals._parent = self totals._SetRoot(self)
@readonly def Version(self) -> Nullable[SemanticVersion]: """ Read-only property to access the version of the report format (:attr:`_version`). :returns: The version, e.g. ``2.0.1``; ``None`` before the report was converted. """ return self._version @readonly def Files(self) -> dict[Path, File]: """ Read-only property to access the files (:attr:`_files`). :returns: The files, by path. """ return self._files @readonly def Functions(self) -> list[Function]: """ Read-only property to access the functions (:attr:`_functions`). :returns: The functions; empty in a report written with ``-skip-functions`` or ``-summary-only``. """ return self._functions @readonly def Totals(self) -> Nullable[Summary]: """ Read-only property to access the counters of the whole report (:attr:`_totals`). :returns: The totals; ``None`` before the report was converted. """ return self._totals
TestVector._PARENT_TYPE = (MCDCRecord, ) Region._PARENT_TYPE = (Function, Expansion) BranchRegion._PARENT_TYPE = (File, Function, Expansion) MCDCRecord._PARENT_TYPE = (File, Function) Counters._PARENT_TYPE = (Summary, ) Summary._PARENT_TYPE = (File, Report) Segment._PARENT_TYPE = (File, ) Expansion._PARENT_TYPE = (File, ) File._PARENT_TYPE = (Report, ) Function._PARENT_TYPE = (Report, )
[docs] @export class Document(cc_Document, Report): """ An LLVM JSON code coverage export: read into the format's model, and converted to the common model. """ _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`.
[docs] def __init__( self, jsonReportFile: Path, analyzeAndConvert: bool = False, *, version: Nullable[SemanticVersion] = None, files: Nullable[Iterable[File]] = None, functions: Nullable[Iterable[Function]] = None, totals: Nullable[Summary] = None ) -> None: """ Initialize the report, and optionally read it; or build it from typed values. :param jsonReportFile: Path to the JSON file. :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``. :param version: Optional, version of the report format. Default: ``None``. :param files: Optional, the files. Default: none. :param functions: Optional, the functions. Default: none. :param totals: Optional, the counters of the whole report. Default: ``None``. :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`. :raises TypeError: If parameter ``files`` isn't iterable. :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`. :raises TypeError: If parameter ``functions`` isn't iterable. :raises TypeError: If parameter ``functions`` contains an element not of type :class:`Function`. :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`. """ super().__init__(jsonReportFile) Report.__init__(self, version, files, functions, totals) self._jsonDocument = None if analyzeAndConvert: self.Analyze() self.Convert()
[docs] def Analyze(self) -> None: """ Parse the JSON file and validate it against the JSON Schema :data:`SCHEMA`. :raises CodeCoverageError: If the file doesn't exist. :raises CodeCoverageError: If the file isn't valid JSON. :raises CodeCoverageError: If the JSON Schema can't be read. :raises CodeCoverageError: If the file isn't valid according to the JSON Schema. """ if not self._path.exists(): raise CodeCoverageError(f"LLVM coverage export file '{self._path}' does not exist.") \ from FileNotFoundError(f"File '{self._path}' not found.") from jsonschema import Draft202012Validator with Stopwatch() as sw: try: jsonDocument = loads(self._path.read_text(encoding="utf-8")) except JSONDecodeError as ex: raise CodeCoverageError(f"JSON syntax error in LLVM coverage export file '{self._path}'.") from ex try: schema = loads(readResourceFile(Resources, SCHEMA)) except (ToolingException, JSONDecodeError) as ex: raise CodeCoverageError(f"Couldn't read JSON Schema '{SCHEMA}' from package resources.") from ex errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path)) if len(errors) > 0: ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{SCHEMA}'.") for error in errors: ex.add_note(f"/{'/'.join(str(part) for part in error.path)}: {error.message}") raise ex self._jsonDocument = jsonDocument self._analysisDuration = sw.Duration
[docs] def Convert(self) -> None: """ Convert the parsed and validated JSON document to the format's model. :raises CodeCoverageError: If the JSON file was not analyzed before. |br| Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'. """ if self._jsonDocument is None: ex = CodeCoverageError( f"LLVM coverage export file '{self._path}' needs to be read and analyzed by a JSON parser." ) ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.") raise ex with Stopwatch() as sw: data = self._jsonDocument["data"][0] self._version = SemanticVersion.Parse(self._jsonDocument["version"]) self._files = {} self._functions = [] self._totals = Summary.Parse(data["totals"]) self._totals._parent = self self._totals._SetRoot(self) for record in data["files"]: File.Parse(record, parent=self) for function in data.get("functions", []): Function.Parse(function, parent=self) self._conversionDuration = sw.Duration
[docs] def ToCoverageSummary(self) -> CoverageSummary: """ Convert the format's model to the common model, and aggregate it. :returns: The report's root of the common model, named after the report file. :raises CodeCoverageError: If a file's path runs through another file. :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. """ parents = [path.parent.parts for path in self._files] first = min(parents, default=()) last = max(parents, default=()) common = first for index, part in enumerate(first): if part != last[index]: common = first[:index] break summary = CoverageSummary(self._path.stem, sourceDirectories=[Path(*common)] if len(common) > 0 else []) commonFiles = {path: summary.GetOrAddFile("/".join(path.parts[len(common):])) for path in self._files} branches = self._CollectBranches() for path, file in self._files.items(): self._ConvertLines(file, commonFiles[path], branches[path]) self._ConvertUnits(commonFiles, summary) summary.Aggregate() return summary
[docs] def _CollectBranches(self) -> dict[Path, dict[tuple[Any, ...], list[int]]]: """ Collect the branch regions of the functions by file; a branch region found in several functions is summed. A branch region is collected at the line it starts at, or at the line its macro is expanded at. It is found in several functions e.g. in the instantiations of a template. :returns: By path, the summed true and false counts of each branch region, keyed by the line number first. :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. """ branches: dict[Path, dict[tuple[Any, ...], list[int]]] = {path: {} for path in self._files} for function in self._functions: mainFileID = function.MainFileID if mainFileID is None or (fileBranches := branches.get(function._filePaths[mainFileID])) is None: continue expansions = { region._expandedFileID: region for region in function._regions if region._kind is RegionKind.Expansion } for branch in function._branches: lineNumber = branch._lineStart fileID = branch._fileID while fileID != mainFileID: if (expansion := expansions.get(fileID)) is None: ex = CodeCoverageError(f"Function '{function._name}' has a branch region in a file no region expands to.") ex.add_note(f"Got file ID {fileID} of file '{function._filePaths[fileID]}'.") raise ex lineNumber = expansion._lineStart fileID = expansion._fileID key = ( lineNumber, function._filePaths[branch._fileID], branch._lineStart, branch._columnStart, branch._lineEnd, branch._columnEnd ) if (counts := fileBranches.get(key)) is None: fileBranches[key] = [branch._trueCount, branch._falseCount] else: counts[0] += branch._trueCount counts[1] += branch._falseCount return branches
[docs] @staticmethod def _ConvertLines(file: File, commonFile: cc_File, branches: dict[tuple[Any, ...], list[int]]) -> None: """ Convert a file's segments to lines of the common model, and add the branches of the file's branch regions. A line's count is derived as ``llvm-cov show`` derives it: of the segments starting on the line, and of the last segment of a previous line, which continues on it. A branch region of a line, which isn't executable, is left out. :param file: The file of the format's model. :param commonFile: The file of the common model. :param branches: The true and false counts of each branch region of the file, keyed by the line number first. """ counts: dict[int, int] = {} wrapped: Nullable[Segment] = None segments = file._segments index = 0 while index < len(segments): lineNumber = segments[index]._line lineSegments = [] while index < len(segments) and segments[index]._line == lineNumber: lineSegments.append(segments[index]) index += 1 starts = [s._count for s in lineSegments if s._hasCount and s._isRegionEntry and not s._isGapRegion] skipped = not lineSegments[0]._hasCount and lineSegments[0]._isRegionEntry if ( (not skipped and ((wrapped is not None and wrapped._hasCount) or len(starts) > 0)) or any(segment._hasCount and segment._isRegionEntry for segment in lineSegments) ): counts[lineNumber] = max((wrapped._count if wrapped is not None else 0, *starts)) # the lines up to the next segment continue the last segment of this line wrapped = lineSegments[-1] if wrapped._hasCount: nextLineNumber = segments[index]._line if index < len(segments) else lineNumber + 1 for number in range(lineNumber + 1, nextLineNumber): counts[number] = wrapped._count partialLines = {key[0] for key, outcomes in branches.items() if 0 in outcomes} for number in sorted(counts): count = counts[number] if count == 0: status = LineCoverageStatus.Uncovered elif number in partialLines: status = LineCoverageStatus.PartiallyCovered else: status = LineCoverageStatus.Covered cc_Line(number, status, count, parent=commonFile) lines = commonFile._lines for key in sorted(branches): if key[0] < len(lines) and (line := lines[key[0]]) is not None: for count in branches[key]: cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=line)
[docs] def _ConvertUnits(self, commonFiles: dict[Path, cc_File], summary: CoverageSummary) -> None: """ Convert the files to source file units, and the functions to function units of their files. A function local to a translation unit is named without the file name prefixing it. Functions of the same name in a file - e.g. a header's inline function compiled in several translation units - become one unit, their counts summed. :param commonFiles: The files of the common model, by path. :param summary: The report's root of the common model. """ functions: dict[tuple[Path, str], list[int]] = {} for function in self._functions: mainFileID = function.MainFileID if mainFileID is None or (path := function._filePaths[mainFileID]) not in commonFiles: continue name = function._name if (local := match(r"(.+?\.[^.:;/\\]+)[:;](.+)", name)) is not None: name = local[2] regions = [region for region in function._regions if region._fileID == mainFileID] firstLineNumber = min((region._lineStart for region in regions), default=0) lastLineNumber = max((region._lineEnd for region in regions), default=0) if (span := functions.get((path, name))) is None: functions[(path, name)] = [function._count, firstLineNumber, lastLineNumber] else: span[0] += function._count span[1] = min(span[1], firstLineNumber) span[2] = max(span[2], lastLineNumber) sourceFiles: dict[Path, cc_SourceFile] = {} for path, commonFile in commonFiles.items(): lastLineNumber = commonFile._lastLineNumber sourceFiles[path] = cc_SourceFile( commonFile.Path.as_posix(), file=commonFile, startLine=next(commonFile.IterateLines(), None), endLine=commonFile._lines[lastLineNumber] if lastLineNumber > 0 else None, parent=summary ) for (path, name), (count, firstLineNumber, lastLineNumber) in functions.items(): commonFile = commonFiles[path] lines = [line for line in commonFile._lines[firstLineNumber:lastLineNumber + 1] if line is not None] cc_Function( name, file=commonFile, startLine=lines[0] if len(lines) > 0 else None, endLine=lines[-1] if len(lines) > 0 else None, status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, coverageCount=count, parent=sourceFiles[path] )