Coverage for pyEDAA/Reports/CodeCoverage/LLVM.py: 83%
1033 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31#
32"""
33LLVM's JSON code coverage export: a model of the format, read from a report and converted to the common model.
35``llvm-cov export -format=text`` writes the format - type ``llvm.coverage.json.export`` - from the source-based code
36coverage of Clang, and of other compilers based on LLVM, e.g. rustc or Swift. A report is validated against the JSON
37Schema :file:`LLVM-Coverage-JSON.schema.json`, reverse-engineered from llvm-cov, which accepts the format versions
382.0.0 to 3.1.0. The format's model keeps what the report states: a :class:`Document` holds :class:`File` records - each
39with its segments, branch regions, MC/DC records, expansions and summary -, and :class:`Function` records - each with
40its regions, branch regions and MC/DC records.
42* A report states a segment (:class:`Segment`) as a JSON array - a tuple of numbers and flags -, a file, a function or
43 an expansion (:class:`Expansion`) as a JSON object.
44* A report states a source region as a JSON array, a tuple of numbers: a :class:`Region`, a :class:`BranchRegion` or a
45 :class:`MCDCRecord` with its :class:`TestVector` objects. A region names its source range by line and column, and
46 the file by an index into the file paths of its function or expansion; :attr:`Base.FilePath` and :attr:`Base.File`
47 look the index up.
48* A summary (:class:`Summary`) holds the counters (:class:`Counters`) llvm-cov computed for a file or the whole report.
50Each class of the format's model takes typed values in its constructor, so a model can be built by hand, too. A
51classmethod ``Parse`` reads the class' JSON element and calls the constructor; :meth:`Document.Convert` parses a
52report's files, functions and totals with it. Every element (:class:`Element`) knows the element containing it and the
53report's root.
55:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`:
57* A file's path is relative to the directory common to all files, which becomes the report's source directory.
58* A line's count is derived from the file's segments, as ``llvm-cov show`` derives it: the line is executable, if a
59 region with a count starts on it, or a region with a count continues from a previous line - unless a skipped region
60 starts the line. Its count is the largest count of the regions starting on it - gap regions excluded - and of the
61 region continuing from a previous line.
62* A branch region becomes two branches - its true and its false outcome - of the line it starts at, or of the line its
63 macro is expanded at. The branch regions of a function's instantiations, e.g. of a template, are summed. A line with
64 an outcome never taken is partially covered.
65* A file becomes a source file unit, and each function a function of its file's unit, named as the report names it -
66 without the file name of its translation unit, which prefixes a function local to it, e.g. ``Statistics.c:Square``.
67 The format doesn't demangle C++ names.
69.. rubric:: Example
71.. code-block:: Python
73 from pathlib import Path
74 from pyEDAA.Reports.CodeCoverage.LLVM import Document
76 report = Document(Path("coverage.json"), analyzeAndConvert=True)
77 summary = report.ToCoverageSummary()
78 print(f"{summary.FileCount} files: {summary.LineCoverage:.1%} of lines, {summary.BranchCoverage:.1%} of branches")
79"""
80from __future__ import annotations
82from enum import Enum
83from json import JSONDecodeError, loads
84from pathlib import Path
85from re import match
86from typing import Any, ClassVar, Generic, Iterable, Optional as Nullable, Self, TypeVar
88from pyTooling.Common import getFullyQualifiedName, readResourceFile
89from pyTooling.Decorators import export, readonly
90from pyTooling.Exceptions import ToolingException
91from pyTooling.MetaClasses import ExtendedType
92from pyTooling.Stopwatch import Stopwatch
93from pyTooling.Versioning import SemanticVersion
95from pyEDAA.Reports import Resources
96from pyEDAA.Reports.CodeCoverage import AlreadyInCoverageReportError, Branch as cc_Branch, CodeCoverageError, \
97 CoverageSummary, DuplicateFileError, Document as cc_Document, File as cc_File, \
98 Function as cc_Function, Line as cc_Line, LineCoverageStatus, \
99 SourceFile as cc_SourceFile
102__all__ = ["SCHEMA"]
104SCHEMA = "LLVM-Coverage-JSON.schema.json" #: The JSON Schema a report is validated against.
106# A class with a property named like a class - ``Path``, ``Summary`` - can't name that class in the annotation of a
107# field: the class body's namespace, where annotations are evaluated, binds the name to the property.
108_Path = Path
110ParentType = TypeVar("ParentType")
113@export
114class RegionKind(Enum):
115 """The kind of a source region, as LLVM's coverage mapping numbers it."""
117 Code = 0 #: Code associated with a counter.
118 Expansion = 1 #: The source range of a macro expansion, which maps to a file of its own.
119 Skipped = 2 #: Code skipped by the preprocessor - e.g. by ``#if 0`` -, empty lines and comments.
120 Gap = 3 #: Code between two regions, whose count is a line's count only, if no other region starts on it.
121 Branch = 4 #: A condition with the counts of its true and false outcome.
122 MCDCDecision = 5 #: A decision of conditions, measured for MC/DC.
123 MCDCBranch = 6 #: A condition of a decision measured for MC/DC, with the counts of its true and false outcome.
126@export
127class Element(Generic[ParentType], metaclass=ExtendedType, slots=True):
128 """
129 Base-class of every element of the format's model: a reference to the element containing it, and to the report's
130 root.
132 A container connects the elements given to its constructor; an element created with a ``parent`` is added to the
133 parent's collection. When a file or function joins a report, the report becomes the root of every element below it.
134 """
136 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; assigned below the classes.
138 _parent: Nullable[ParentType] #: The element containing this one, or ``None``.
139 _root: Nullable[Report] #: The report's root, or ``None`` while the element isn't part of a report.
141 def __init__(self, *, parent: Nullable[ParentType] = None) -> None:
142 """
143 Initialize an element with its parent and the parent's root.
145 The parent doesn't list the element yet: the class adds it to the parent's collection, after setting its fields.
147 :param parent: Optional, the element containing this one. Default: ``None``.
148 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
149 """
150 if parent is None:
151 self._parent = None
152 self._root = None
153 elif not isinstance(parent, self._PARENT_TYPE):
154 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE)
155 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.")
156 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
157 raise ex
158 else:
159 self._parent = parent
160 self._root = parent._root
162 @readonly
163 def Parent(self) -> Nullable[ParentType]:
164 """
165 Read-only property to access the element containing this one (:attr:`_parent`).
167 :returns: The parent, or ``None`` if the element belongs to none.
168 """
169 return self._parent
171 @readonly
172 def Root(self) -> Nullable[Report]:
173 """
174 Read-only property to access the report's root (:attr:`_root`).
176 :returns: The report, or ``None`` while the element isn't part of a report.
177 """
178 return self._root
180 def _SetRoot(self, root: Nullable[Report]) -> None:
181 """
182 Set the report's root of the element.
184 A class containing elements overrides it, to set the root of those too.
186 :param root: The report's root, or ``None`` while the element isn't part of a report.
187 """
188 self._root = root
191@export
192class Base(Element[ParentType]):
193 """
194 Base-class of the source regions: a source range, the file it is in, the file it expands to, and its kind.
195 """
197 _lineStart: int #: Line the region starts at.
198 _columnStart: int #: Column the region starts at.
199 _lineEnd: int #: Line the region ends at.
200 _columnEnd: int #: Column the region ends at.
201 _fileID: Nullable[int] #: Index of the region's file in the file paths of its function or expansion.
202 _expandedFileID: int #: Index of the file an expansion region expands to.
203 _kind: RegionKind #: The kind of the region.
205 def __init__(
206 self,
207 lineStart: int,
208 columnStart: int,
209 lineEnd: int,
210 columnEnd: int,
211 fileID: Nullable[int],
212 expandedFileID: int,
213 kind: RegionKind,
214 *,
215 parent: Nullable[ParentType] = None
216 ) -> None:
217 """
218 Initialize the source range, the region's files and its kind.
220 :param lineStart: Line the region starts at.
221 :param columnStart: Column the region starts at.
222 :param lineEnd: Line the region ends at.
223 :param columnEnd: Column the region ends at.
224 :param fileID: Index of the region's file in the file paths of its function or expansion; ``None``, if not
225 stated.
226 :param expandedFileID: Index of the file an expansion region expands to.
227 :param kind: The kind of the region.
228 :param parent: Optional, the element containing the region. Default: ``None``.
229 :raises ValueError: If parameter ``lineStart`` is ``None``.
230 :raises TypeError: If parameter ``lineStart`` isn't of type :class:`int`.
231 :raises ValueError: If parameter ``lineStart`` is less than 1.
232 :raises ValueError: If parameter ``columnStart`` is ``None``.
233 :raises TypeError: If parameter ``columnStart`` isn't of type :class:`int`.
234 :raises ValueError: If parameter ``columnStart`` is less than 1.
235 :raises ValueError: If parameter ``lineEnd`` is ``None``.
236 :raises TypeError: If parameter ``lineEnd`` isn't of type :class:`int`.
237 :raises ValueError: If parameter ``lineEnd`` is less than 1.
238 :raises ValueError: If parameter ``columnEnd`` is ``None``.
239 :raises TypeError: If parameter ``columnEnd`` isn't of type :class:`int`.
240 :raises ValueError: If parameter ``columnEnd`` is less than 1.
241 :raises TypeError: If parameter ``fileID`` isn't of type :class:`int`.
242 :raises ValueError: If parameter ``fileID`` is negative.
243 :raises ValueError: If parameter ``fileID`` is no index into the file paths of parameter ``parent``.
244 :raises ValueError: If parameter ``expandedFileID`` is ``None``.
245 :raises TypeError: If parameter ``expandedFileID`` isn't of type :class:`int`.
246 :raises ValueError: If parameter ``expandedFileID`` is negative.
247 :raises ValueError: If parameter ``expandedFileID`` of an expansion region is no index into the file paths of
248 parameter ``parent``.
249 :raises ValueError: If parameter ``kind`` is ``None``.
250 :raises TypeError: If parameter ``kind`` isn't of type :class:`RegionKind`.
251 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
252 """
253 super().__init__(parent=parent)
255 for name, position in (
256 ("lineStart", lineStart), ("columnStart", columnStart), ("lineEnd", lineEnd), ("columnEnd", columnEnd)
257 ):
258 if position is None: 258 ↛ 259line 258 didn't jump to line 259 because the condition on line 258 was never true
259 raise ValueError(f"Parameter '{name}' is None.")
260 elif not isinstance(position, int): 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true
261 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
262 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.")
263 raise ex
264 elif position < 1: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true
265 ex = ValueError(f"Parameter '{name}' is less than 1.")
266 ex.add_note(f"Got value '{position}'.")
267 raise ex
269 if fileID is not None:
270 if not isinstance(fileID, int): 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true
271 ex = TypeError("Parameter 'fileID' is not of type 'int'.")
272 ex.add_note(f"Got type '{getFullyQualifiedName(fileID)}'.")
273 raise ex
274 elif fileID < 0: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true
275 ex = ValueError("Parameter 'fileID' is negative.")
276 ex.add_note(f"Got value '{fileID}'.")
277 raise ex
279 if expandedFileID is None: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 raise ValueError("Parameter 'expandedFileID' is None.")
281 elif not isinstance(expandedFileID, int): 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true
282 ex = TypeError("Parameter 'expandedFileID' is not of type 'int'.")
283 ex.add_note(f"Got type '{getFullyQualifiedName(expandedFileID)}'.")
284 raise ex
285 elif expandedFileID < 0: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 ex = ValueError("Parameter 'expandedFileID' is negative.")
287 ex.add_note(f"Got value '{expandedFileID}'.")
288 raise ex
290 if kind is None: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 raise ValueError("Parameter 'kind' is None.")
292 elif not isinstance(kind, RegionKind):
293 ex = TypeError("Parameter 'kind' is not of type 'RegionKind'.")
294 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
295 raise ex
297 if isinstance(parent, (Function, Expansion)):
298 for name, value in (
299 ("fileID", fileID), ("expandedFileID", expandedFileID if kind is RegionKind.Expansion else None)
300 ):
301 if value is not None and value >= len(parent._filePaths):
302 ex = ValueError(f"Parameter '{name}' is no index into the file paths of parameter 'parent'.")
303 ex.add_note(f"Got value '{value}' for {len(parent._filePaths)} file paths.")
304 raise ex
306 self._lineStart = lineStart
307 self._columnStart = columnStart
308 self._lineEnd = lineEnd
309 self._columnEnd = columnEnd
310 self._fileID = fileID
311 self._expandedFileID = expandedFileID
312 self._kind = kind
314 @readonly
315 def LineStart(self) -> int:
316 """
317 Read-only property to access the line the region starts at (:attr:`_lineStart`).
319 :returns: The line number, counted from 1.
320 """
321 return self._lineStart
323 @readonly
324 def ColumnStart(self) -> int:
325 """
326 Read-only property to access the column the region starts at (:attr:`_columnStart`).
328 :returns: The column number, counted from 1.
329 """
330 return self._columnStart
332 @readonly
333 def LineEnd(self) -> int:
334 """
335 Read-only property to access the line the region ends at (:attr:`_lineEnd`).
337 :returns: The line number, counted from 1.
338 """
339 return self._lineEnd
341 @readonly
342 def ColumnEnd(self) -> int:
343 """
344 Read-only property to access the column the region ends at (:attr:`_columnEnd`).
346 :returns: The column number, counted from 1.
347 """
348 return self._columnEnd
350 @readonly
351 def FileID(self) -> Nullable[int]:
352 """
353 Read-only property to access the index of the region's file (:attr:`_fileID`).
355 For a region listed by a file, the index refers to the file paths of a function the report doesn't name;
356 :attr:`FilePath` is the file's path then.
358 :returns: The index into the file paths of the region's function or expansion; ``None`` for an MC/DC record before
359 format version 3.0.1.
360 """
361 return self._fileID
363 @readonly
364 def ExpandedFileID(self) -> int:
365 """
366 Read-only property to access the index of the file an expansion region expands to (:attr:`_expandedFileID`).
368 :returns: The index into the file paths of the region's function or expansion; ``0`` for other kinds of regions.
369 """
370 return self._expandedFileID
372 @readonly
373 def Kind(self) -> RegionKind:
374 """
375 Read-only property to access the kind of the region (:attr:`_kind`).
377 :returns: The kind.
378 """
379 return self._kind
381 @readonly
382 def FilePath(self) -> Nullable[Path]:
383 """
384 Read-only property to return the path of the region's file.
386 A region of a function or expansion names its file by :attr:`FileID`, an index into the parent's file paths. A
387 region listed by a file lies in that file.
389 :returns: The path, as the compiler named it; ``None`` if the region belongs to no element, or for an MC/DC record
390 of a function before format version 3.0.1, which states no file.
391 """
392 if self._parent is None:
393 return None
394 elif isinstance(self._parent, File):
395 return self._parent._path
396 elif self._fileID is None:
397 return None
399 return self._parent._filePaths[self._fileID]
401 @readonly
402 def File(self) -> Nullable[File]:
403 """
404 Read-only property to return the region's file, as the report lists it.
406 :returns: The file of :attr:`FilePath` in the report's files; ``None`` if the region isn't part of a report, or the
407 report doesn't list the file, e.g. one excluded by ``-ignore-filename-regex``.
408 """
409 if self._root is None or (path := self.FilePath) is None:
410 return None
412 return self._root._files.get(path, None)
414 @readonly
415 def ExpandedFilePath(self) -> Nullable[Path]:
416 """
417 Read-only property to return the path of the file an expansion region expands to (:attr:`ExpandedFileID`).
419 :returns: The path, as the compiler named it; ``None`` for another kind of region, or a region belonging to no
420 function or expansion.
421 """
422 if self._kind is not RegionKind.Expansion or not isinstance(self._parent, (Function, Expansion)):
423 return None
425 return self._parent._filePaths[self._expandedFileID]
427 @readonly
428 def ExpandedFile(self) -> Nullable[File]:
429 """
430 Read-only property to return the file an expansion region expands to, as the report lists it.
432 :returns: The file of :attr:`ExpandedFilePath` in the report's files; ``None`` if there is none, the region isn't
433 part of a report, or the report doesn't list the file.
434 """
435 if self._root is None or (path := self.ExpandedFilePath) is None:
436 return None
438 return self._root._files.get(path, None)
441@export
442class Region(Base["Function | Expansion"]):
443 """
444 A region of a function or an expansion: a source range with a count.
446 A region is ``[lineStart, columnStart, lineEnd, columnEnd, count, fileID, expandedFileID, kind]``.
447 """
449 _count: int #: How often the region's code ran.
451 def __init__(
452 self,
453 lineStart: int,
454 columnStart: int,
455 lineEnd: int,
456 columnEnd: int,
457 count: int,
458 fileID: int,
459 expandedFileID: int,
460 kind: RegionKind,
461 *,
462 parent: Nullable[Function | Expansion] = None
463 ) -> None:
464 """
465 Initialize a region, and add it to the regions of its function or to the target regions of its expansion.
467 :param lineStart: Line the region starts at.
468 :param columnStart: Column the region starts at.
469 :param lineEnd: Line the region ends at.
470 :param columnEnd: Column the region ends at.
471 :param count: How often the region's code ran.
472 :param fileID: Index of the region's file in the file paths of its function or expansion.
473 :param expandedFileID: Index of the file an expansion region expands to.
474 :param kind: The kind of the region.
475 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``.
476 :raises ValueError: If parameter ``count`` is ``None``.
477 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
478 :raises ValueError: If parameter ``count`` is negative.
479 :raises ValueError: If parameter ``fileID`` is ``None``.
480 :raises TypeError: If parameter ``parent`` isn't of type :class:`Function` or :class:`Expansion`.
481 """
482 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
484 if fileID is None:
485 raise ValueError("Parameter 'fileID' is None.")
487 if count is None: 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true
488 raise ValueError("Parameter 'count' is None.")
489 elif not isinstance(count, int): 489 ↛ 490line 489 didn't jump to line 490 because the condition on line 489 was never true
490 ex = TypeError("Parameter 'count' is not of type 'int'.")
491 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
492 raise ex
493 elif count < 0: 493 ↛ 494line 493 didn't jump to line 494 because the condition on line 493 was never true
494 ex = ValueError("Parameter 'count' is negative.")
495 ex.add_note(f"Got value '{count}'.")
496 raise ex
498 self._count = count
500 if parent is not None:
501 (parent._regions if isinstance(parent, Function) else parent._targetRegions).append(self)
503 @classmethod
504 def Parse(cls, region: list[Any], *, parent: Nullable[Function | Expansion] = None) -> Self:
505 """
506 Parse a region from its JSON array.
508 :param region: The JSON array of the region.
509 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``.
510 :returns: The region.
511 """
512 return cls(*region[:7], RegionKind(region[7]), parent=parent)
514 @readonly
515 def Count(self) -> int:
516 """
517 Read-only property to access how often the region's code ran (:attr:`_count`).
519 :returns: The count.
520 """
521 return self._count
524@export
525class BranchRegion(Base["File | Function | Expansion"]):
526 """
527 A branch region: a condition with the counts of its true and false outcome.
529 A branch region is ``[lineStart, columnStart, lineEnd, columnEnd, trueCount, falseCount, fileID, expandedFileID,
530 kind]``.
531 """
533 _trueCount: int #: How often the condition was true.
534 _falseCount: int #: How often the condition was false.
536 def __init__(
537 self,
538 lineStart: int,
539 columnStart: int,
540 lineEnd: int,
541 columnEnd: int,
542 trueCount: int,
543 falseCount: int,
544 fileID: int,
545 expandedFileID: int,
546 kind: RegionKind,
547 *,
548 parent: Nullable[File | Function | Expansion] = None
549 ) -> None:
550 """
551 Initialize a branch region, and add it to the branch regions of its file, function or expansion.
553 :param lineStart: Line the branch region starts at.
554 :param columnStart: Column the branch region starts at.
555 :param lineEnd: Line the branch region ends at.
556 :param columnEnd: Column the branch region ends at.
557 :param trueCount: How often the condition was true.
558 :param falseCount: How often the condition was false.
559 :param fileID: Index of the branch region's file in the file paths of its function or expansion.
560 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for a branch region.
561 :param kind: The kind of the region: a branch, or a condition measured for MC/DC.
562 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``.
563 :raises ValueError: If parameter ``trueCount`` is ``None``.
564 :raises TypeError: If parameter ``trueCount`` isn't of type :class:`int`.
565 :raises ValueError: If parameter ``trueCount`` is negative.
566 :raises ValueError: If parameter ``falseCount`` is ``None``.
567 :raises TypeError: If parameter ``falseCount`` isn't of type :class:`int`.
568 :raises ValueError: If parameter ``falseCount`` is negative.
569 :raises ValueError: If parameter ``fileID`` is ``None``.
570 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`, :class:`Function` or
571 :class:`Expansion`.
572 """
573 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
575 if fileID is None: 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true
576 raise ValueError("Parameter 'fileID' is None.")
578 for name, value in (("trueCount", trueCount), ("falseCount", falseCount)):
579 if value is None: 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true
580 raise ValueError(f"Parameter '{name}' is None.")
581 elif not isinstance(value, int): 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true
582 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
583 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
584 raise ex
585 elif value < 0:
586 ex = ValueError(f"Parameter '{name}' is negative.")
587 ex.add_note(f"Got value '{value}'.")
588 raise ex
590 self._trueCount = trueCount
591 self._falseCount = falseCount
593 if parent is not None:
594 parent._branches.append(self)
596 @classmethod
597 def Parse(cls, branch: list[Any], *, parent: Nullable[File | Function | Expansion] = None) -> Self:
598 """
599 Parse a branch region from its JSON array.
601 :param branch: The JSON array of the branch region.
602 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``.
603 :returns: The branch region.
604 """
605 return cls(*branch[:8], RegionKind(branch[8]), parent=parent)
607 @readonly
608 def TrueCount(self) -> int:
609 """
610 Read-only property to access how often the condition was true (:attr:`_trueCount`).
612 :returns: The count.
613 """
614 return self._trueCount
616 @readonly
617 def FalseCount(self) -> int:
618 """
619 Read-only property to access how often the condition was false (:attr:`_falseCount`).
621 :returns: The count.
622 """
623 return self._falseCount
626@export
627class TestVector(Element["MCDCRecord"]):
628 """
629 A test vector of an MC/DC decision - in format version 3.1.0 -: the values of its conditions, and the outcome.
630 """
632 _conditions: list[Nullable[bool]] #: The values of the conditions; ``None`` for a condition not evaluated.
633 _executed: bool #: Whether the test vector was executed.
634 _result: Nullable[bool] #: The outcome of the decision.
636 def __init__(
637 self,
638 conditions: Iterable[Nullable[bool]],
639 executed: bool,
640 result: Nullable[bool],
641 *,
642 parent: Nullable[MCDCRecord] = None
643 ) -> None:
644 """
645 Initialize a test vector, and add it to the test vectors of its MC/DC record.
647 :param conditions: The values of the conditions; ``None`` for a condition not evaluated.
648 :param executed: Whether the test vector was executed.
649 :param result: The outcome of the decision.
650 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``.
651 :raises ValueError: If parameter ``conditions`` is ``None``.
652 :raises TypeError: If parameter ``conditions`` isn't iterable.
653 :raises TypeError: If parameter ``conditions`` contains an element not of type :class:`bool`, other than ``None``.
654 :raises ValueError: If parameter ``executed`` is ``None``.
655 :raises TypeError: If parameter ``executed`` isn't of type :class:`bool`.
656 :raises TypeError: If parameter ``result`` isn't of type :class:`bool`.
657 :raises TypeError: If parameter ``parent`` isn't of type :class:`MCDCRecord`.
658 """
659 super().__init__(parent=parent)
661 if conditions is None: 661 ↛ 662line 661 didn't jump to line 662 because the condition on line 661 was never true
662 raise ValueError("Parameter 'conditions' is None.")
663 elif not isinstance(conditions, Iterable): 663 ↛ 664line 663 didn't jump to line 664 because the condition on line 663 was never true
664 ex = TypeError("Parameter 'conditions' is not iterable.")
665 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.")
666 raise ex
668 if executed is None: 668 ↛ 669line 668 didn't jump to line 669 because the condition on line 668 was never true
669 raise ValueError("Parameter 'executed' is None.")
670 elif not isinstance(executed, bool): 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true
671 ex = TypeError("Parameter 'executed' is not of type 'bool'.")
672 ex.add_note(f"Got type '{getFullyQualifiedName(executed)}'.")
673 raise ex
675 if result is not None and not isinstance(result, bool):
676 ex = TypeError("Parameter 'result' is not of type 'bool'.")
677 ex.add_note(f"Got type '{getFullyQualifiedName(result)}'.")
678 raise ex
680 self._conditions = conditionList = []
681 self._executed = executed
682 self._result = result
684 for condition in conditions:
685 if condition is not None and not isinstance(condition, bool): 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true
686 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.")
687 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.")
688 raise ex
690 conditionList.append(condition)
692 if parent is not None:
693 parent._testVectors.append(self)
695 @classmethod
696 def Parse(cls, testVector: dict[str, Any], *, parent: Nullable[MCDCRecord] = None) -> Self:
697 """
698 Parse a test vector from its JSON object.
700 :param testVector: The JSON object of the test vector.
701 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``.
702 :returns: The test vector.
703 """
704 return cls(testVector["conditions"], testVector["executed"], testVector["result"], parent=parent)
706 @readonly
707 def Conditions(self) -> list[Nullable[bool]]:
708 """
709 Read-only property to access the values of the conditions (:attr:`_conditions`).
711 :returns: The values, by condition; ``None`` for a condition not evaluated.
712 """
713 return self._conditions
715 @readonly
716 def Executed(self) -> bool:
717 """
718 Read-only property to access whether the test vector was executed (:attr:`_executed`).
720 :returns: ``True`` for an executed test vector; ``False`` for a missing one, listed by
721 ``-show-mcdc-non-executed-vectors``.
722 """
723 return self._executed
725 @readonly
726 def Result(self) -> Nullable[bool]:
727 """
728 Read-only property to access the outcome of the decision (:attr:`_result`).
730 :returns: The outcome.
731 """
732 return self._result
735@export
736class MCDCRecord(Base["File | Function"]):
737 """
738 An MC/DC record: a decision of conditions, and for each condition, whether a pair of test vectors showed its
739 independent effect on the outcome.
741 The record's array grew with the format version: ``[lineStart, columnStart, lineEnd, columnEnd, expandedFileID, kind,
742 conditions]`` in 2.0.1; version 3.0.0 added the numbers of true and false decisions after the source range, version
743 3.0.1 the ``fileID`` before ``expandedFileID``, and version 3.1.0 the test vectors at the end.
744 """
746 _trueDecisions: Nullable[int] #: Number of executed test vectors with the outcome true, if stated.
747 _falseDecisions: Nullable[int] #: Number of executed test vectors with the outcome false, if stated.
748 _conditions: list[bool] #: Whether a pair of test vectors showed the independent effect, by condition.
749 _testVectors: list[TestVector] #: The test vectors; empty before format version 3.1.0.
751 def __init__(
752 self,
753 lineStart: int,
754 columnStart: int,
755 lineEnd: int,
756 columnEnd: int,
757 fileID: Nullable[int],
758 expandedFileID: int,
759 kind: RegionKind,
760 conditions: Iterable[bool],
761 trueDecisions: Nullable[int] = None,
762 falseDecisions: Nullable[int] = None,
763 testVectors: Nullable[Iterable[TestVector]] = None,
764 *,
765 parent: Nullable[File | Function] = None
766 ) -> None:
767 """
768 Initialize an MC/DC record, and add it to the MC/DC records of its file or function.
770 :param lineStart: Line the decision starts at.
771 :param columnStart: Column the decision starts at.
772 :param lineEnd: Line the decision ends at.
773 :param columnEnd: Column the decision ends at.
774 :param fileID: Index of the decision's file in the file paths of its function or
775 expansion; ``None``, if not stated.
776 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for an MC/DC
777 record.
778 :param kind: The kind of the region: an MC/DC decision.
779 :param conditions: Whether a pair of test vectors showed the independent effect, by
780 condition.
781 :param trueDecisions: Optional, number of executed test vectors with the outcome true.
782 Default: ``None``.
783 :param falseDecisions: Optional, number of executed test vectors with the outcome false.
784 Default: ``None``.
785 :param testVectors: Optional, the test vectors. Default: none.
786 :param parent: Optional, the file or function the MC/DC record belongs to. Default:
787 ``None``.
788 :raises ValueError: If parameter ``conditions`` is ``None``.
789 :raises TypeError: If parameter ``conditions`` isn't iterable.
790 :raises TypeError: If parameter ``conditions`` contains an element not of type
791 :class:`bool`.
792 :raises TypeError: If parameter ``trueDecisions`` isn't of type :class:`int`.
793 :raises ValueError: If parameter ``trueDecisions`` is negative.
794 :raises TypeError: If parameter ``falseDecisions`` isn't of type :class:`int`.
795 :raises ValueError: If parameter ``falseDecisions`` is negative.
796 :raises TypeError: If parameter ``testVectors`` isn't iterable.
797 :raises TypeError: If parameter ``testVectors`` contains an element not of type
798 :class:`TestVector`.
799 :raises AlreadyInCoverageReportError: If parameter ``testVectors`` contains a test vector of another MC/DC
800 record.
801 :raises TypeError: If parameter ``parent`` isn't of type :class:`File` or
802 :class:`Function`.
803 """
804 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
806 for name, value in (("trueDecisions", trueDecisions), ("falseDecisions", falseDecisions)):
807 if value is None:
808 continue
809 elif not isinstance(value, int): 809 ↛ 810line 809 didn't jump to line 810 because the condition on line 809 was never true
810 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
811 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
812 raise ex
813 elif value < 0: 813 ↛ 814line 813 didn't jump to line 814 because the condition on line 813 was never true
814 ex = ValueError(f"Parameter '{name}' is negative.")
815 ex.add_note(f"Got value '{value}'.")
816 raise ex
818 if conditions is None:
819 raise ValueError("Parameter 'conditions' is None.")
820 elif not isinstance(conditions, Iterable): 820 ↛ 821line 820 didn't jump to line 821 because the condition on line 820 was never true
821 ex = TypeError("Parameter 'conditions' is not iterable.")
822 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.")
823 raise ex
825 self._trueDecisions = trueDecisions
826 self._falseDecisions = falseDecisions
827 self._conditions = conditionList = []
828 self._testVectors = testVectorList = []
830 for condition in conditions:
831 if not isinstance(condition, bool):
832 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.")
833 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.")
834 raise ex
836 conditionList.append(condition)
838 if testVectors is not None:
839 if not isinstance(testVectors, Iterable): 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true
840 ex = TypeError("Parameter 'testVectors' is not iterable.")
841 ex.add_note(f"Got type '{getFullyQualifiedName(testVectors)}'.")
842 raise ex
844 for testVector in testVectors:
845 if not isinstance(testVector, TestVector): 845 ↛ 846line 845 didn't jump to line 846 because the condition on line 845 was never true
846 ex = TypeError("Parameter 'testVectors' contains an element not of type 'TestVector'.")
847 ex.add_note(f"Got type '{getFullyQualifiedName(testVector)}'.")
848 raise ex
849 elif testVector._parent is not None: 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true
850 raise AlreadyInCoverageReportError("Parameter 'testVectors' contains a test vector of another MC/DC record.")
852 testVector._parent = self
853 testVector._root = self._root
854 testVectorList.append(testVector)
856 if parent is not None:
857 parent._mcdcRecords.append(self)
859 @classmethod
860 def Parse(cls, record: list[Any], *, parent: Nullable[File | Function] = None) -> Self:
861 """
862 Parse an MC/DC record from its JSON array, in the shape of its format version.
864 :param record: The JSON array of the MC/DC record.
865 :param parent: Optional, the file or function the MC/DC record belongs to. Default: ``None``.
866 :returns: The MC/DC record.
867 """
868 if len(record) == 7:
869 return cls(*record[:4], None, record[4], RegionKind(record[5]), record[6], parent=parent)
870 elif len(record) == 9:
871 return cls(*record[:4], None, record[6], RegionKind(record[7]), record[8], record[4], record[5], parent=parent)
873 return cls(
874 *record[:4], record[6], record[7], RegionKind(record[8]), record[9], record[4], record[5],
875 [TestVector.Parse(testVector) for testVector in record[10]] if len(record) > 10 else None, parent=parent
876 )
878 @readonly
879 def TrueDecisions(self) -> Nullable[int]:
880 """
881 Read-only property to access the number of executed test vectors with the outcome true (:attr:`_trueDecisions`).
883 :returns: The number; ``None`` before format version 3.0.0.
884 """
885 return self._trueDecisions
887 @readonly
888 def FalseDecisions(self) -> Nullable[int]:
889 """
890 Read-only property to access the number of executed test vectors with the outcome false (:attr:`_falseDecisions`).
892 :returns: The number; ``None`` before format version 3.0.0.
893 """
894 return self._falseDecisions
896 @readonly
897 def Conditions(self) -> list[bool]:
898 """
899 Read-only property to access, by condition, whether a pair of test vectors showed its independent effect
900 (:attr:`_conditions`).
902 :returns: ``True`` for each condition shown to affect the outcome on its own.
903 """
904 return self._conditions
906 @readonly
907 def TestVectors(self) -> list[TestVector]:
908 """
909 Read-only property to access the test vectors (:attr:`_testVectors`).
911 :returns: The test vectors; empty before format version 3.1.0.
912 """
913 return self._testVectors
915 def _SetRoot(self, root: Nullable[Report]) -> None:
916 """
917 Set the report's root of the MC/DC record, and of its test vectors.
919 :param root: The report's root, or ``None`` while the element isn't part of a report.
920 """
921 self._root = root
922 for testVector in self._testVectors:
923 testVector._SetRoot(root)
926@export
927class Counters(Element["Summary"]):
928 """
929 The counters of one kind of a summary: how many there are, and how many are covered.
931 A summary counts lines, functions, regions, branches and MC/DC conditions. The summary given the counters becomes
932 their parent.
933 """
935 _count: int #: Number of lines, functions, regions, branches or MC/DC conditions.
936 _covered: int #: Number of the covered ones.
937 _notCovered: Nullable[int] #: Number of the uncovered ones, if stated.
938 _percent: float #: The coverage in percent.
940 def __init__(self, count: int, covered: int, percent: float, notCovered: Nullable[int] = None) -> None:
941 """
942 Initialize counters.
944 :param count: Number of lines, functions, regions, branches or MC/DC conditions.
945 :param covered: Number of the covered ones.
946 :param percent: The coverage in percent.
947 :param notCovered: Optional, number of the uncovered ones. Default: ``None``.
948 :raises ValueError: If parameter ``count`` is ``None``.
949 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
950 :raises ValueError: If parameter ``count`` is negative.
951 :raises ValueError: If parameter ``covered`` is ``None``.
952 :raises TypeError: If parameter ``covered`` isn't of type :class:`int`.
953 :raises ValueError: If parameter ``covered`` is negative.
954 :raises ValueError: If parameter ``percent`` is ``None``.
955 :raises TypeError: If parameter ``percent`` isn't of type :class:`float` or :class:`int`.
956 :raises ValueError: If parameter ``percent`` is out of range 0..100.
957 :raises TypeError: If parameter ``notCovered`` isn't of type :class:`int`.
958 :raises ValueError: If parameter ``notCovered`` is negative.
959 """
960 super().__init__()
962 for name, value in (("count", count), ("covered", covered)):
963 if value is None: 963 ↛ 964line 963 didn't jump to line 964 because the condition on line 963 was never true
964 raise ValueError(f"Parameter '{name}' is None.")
965 elif not isinstance(value, int): 965 ↛ 966line 965 didn't jump to line 966 because the condition on line 965 was never true
966 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
967 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
968 raise ex
969 elif value < 0: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 ex = ValueError(f"Parameter '{name}' is negative.")
971 ex.add_note(f"Got value '{value}'.")
972 raise ex
974 if notCovered is not None:
975 if not isinstance(notCovered, int): 975 ↛ 976line 975 didn't jump to line 976 because the condition on line 975 was never true
976 ex = TypeError("Parameter 'notCovered' is not of type 'int'.")
977 ex.add_note(f"Got type '{getFullyQualifiedName(notCovered)}'.")
978 raise ex
979 elif notCovered < 0: 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true
980 ex = ValueError("Parameter 'notCovered' is negative.")
981 ex.add_note(f"Got value '{notCovered}'.")
982 raise ex
984 if percent is None: 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true
985 raise ValueError("Parameter 'percent' is None.")
986 elif not isinstance(percent, (float, int)): 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true
987 ex = TypeError("Parameter 'percent' is not of type 'float'.")
988 ex.add_note(f"Got type '{getFullyQualifiedName(percent)}'.")
989 raise ex
990 elif not 0 <= percent <= 100:
991 ex = ValueError("Parameter 'percent' is out of range 0..100.")
992 ex.add_note(f"Got value '{percent}'.")
993 raise ex
995 self._count = count
996 self._covered = covered
997 self._notCovered = notCovered
998 self._percent = percent
1000 @classmethod
1001 def Parse(cls, counters: dict[str, Any]) -> Self:
1002 """
1003 Parse counters from their JSON object.
1005 :param counters: The JSON object of the counters.
1006 :returns: The counters.
1007 """
1008 return cls(counters["count"], counters["covered"], counters["percent"], counters.get("notcovered"))
1010 @readonly
1011 def Count(self) -> int:
1012 """
1013 Read-only property to access the number of lines, functions, regions, branches or MC/DC conditions (:attr:`_count`).
1015 :returns: The number.
1016 """
1017 return self._count
1019 @readonly
1020 def Covered(self) -> int:
1021 """
1022 Read-only property to access the number of covered ones (:attr:`_covered`).
1024 :returns: The number.
1025 """
1026 return self._covered
1028 @readonly
1029 def NotCovered(self) -> Nullable[int]:
1030 """
1031 Read-only property to access the number of uncovered ones (:attr:`_notCovered`).
1033 :returns: The number, or ``None`` for lines, functions and instantiations, which don't state it.
1034 """
1035 return self._notCovered
1037 @readonly
1038 def Percent(self) -> float:
1039 """
1040 Read-only property to access the coverage (:attr:`_percent`).
1042 :returns: The coverage in percent.
1043 """
1044 return self._percent
1047@export
1048class Summary(Element["File | Report"]):
1049 """
1050 A ``summary``: the counters llvm-cov computed for a file or the whole report.
1052 llvm-cov counts a file's lines, regions and branches per function, and merges a function's instantiations by taking
1053 the best one. The file or report given the summary becomes its parent.
1054 """
1056 _lines: Counters #: The counters of lines.
1057 _functions: Counters #: The counters of functions, a function's instantiations counted once.
1058 _instantiations: Counters #: The counters of function instantiations.
1059 _regions: Counters #: The counters of code regions.
1060 _branches: Nullable[Counters] #: The counters of branch outcomes, if stated.
1061 _mcdc: Nullable[Counters] #: The counters of MC/DC conditions, if stated.
1063 def __init__(
1064 self,
1065 lines: Counters,
1066 functions: Counters,
1067 instantiations: Counters,
1068 regions: Counters,
1069 branches: Nullable[Counters] = None,
1070 mcdc: Nullable[Counters] = None
1071 ) -> None:
1072 """
1073 Initialize a summary.
1075 :param lines: The counters of lines.
1076 :param functions: The counters of functions, a function's instantiations counted once.
1077 :param instantiations: The counters of function instantiations.
1078 :param regions: The counters of code regions.
1079 :param branches: Optional, the counters of branch outcomes. Default: ``None``.
1080 :param mcdc: Optional, the counters of MC/DC conditions. Default: ``None``.
1081 :raises ValueError: If parameter ``lines`` is ``None``.
1082 :raises TypeError: If parameter ``lines`` isn't of type :class:`Counters`.
1083 :raises AlreadyInCoverageReportError: If parameter ``lines`` are the counters of another summary.
1084 :raises ValueError: If parameter ``functions`` is ``None``.
1085 :raises TypeError: If parameter ``functions`` isn't of type :class:`Counters`.
1086 :raises AlreadyInCoverageReportError: If parameter ``functions`` are the counters of another summary.
1087 :raises ValueError: If parameter ``instantiations`` is ``None``.
1088 :raises TypeError: If parameter ``instantiations`` isn't of type :class:`Counters`.
1089 :raises AlreadyInCoverageReportError: If parameter ``instantiations`` are the counters of another summary.
1090 :raises ValueError: If parameter ``regions`` is ``None``.
1091 :raises TypeError: If parameter ``regions`` isn't of type :class:`Counters`.
1092 :raises AlreadyInCoverageReportError: If parameter ``regions`` are the counters of another summary.
1093 :raises TypeError: If parameter ``branches`` isn't of type :class:`Counters`.
1094 :raises AlreadyInCoverageReportError: If parameter ``branches`` are the counters of another summary.
1095 :raises TypeError: If parameter ``mcdc`` isn't of type :class:`Counters`.
1096 :raises AlreadyInCoverageReportError: If parameter ``mcdc`` are the counters of another summary.
1097 """
1098 super().__init__()
1100 for name, counters in (
1101 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions)
1102 ):
1103 if counters is None:
1104 raise ValueError(f"Parameter '{name}' is None.")
1105 elif not isinstance(counters, Counters): 1105 ↛ 1106line 1105 didn't jump to line 1106 because the condition on line 1105 was never true
1106 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.")
1107 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.")
1108 raise ex
1110 for name, counters in (("branches", branches), ("mcdc", mcdc)):
1111 if counters is not None and not isinstance(counters, Counters):
1112 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.")
1113 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.")
1114 raise ex
1116 for name, counters in (
1117 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions),
1118 ("branches", branches), ("mcdc", mcdc)
1119 ):
1120 if counters is None:
1121 continue
1122 elif counters._parent is not None: 1122 ↛ 1123line 1122 didn't jump to line 1123 because the condition on line 1122 was never true
1123 raise AlreadyInCoverageReportError(f"Parameter '{name}' are the counters of another summary.")
1125 counters._parent = self
1127 self._lines = lines
1128 self._functions = functions
1129 self._instantiations = instantiations
1130 self._regions = regions
1131 self._branches = branches
1132 self._mcdc = mcdc
1134 @classmethod
1135 def Parse(cls, summary: dict[str, Any]) -> Self:
1136 """
1137 Parse a summary from its JSON object.
1139 :param summary: The JSON object ``summary`` or ``totals``.
1140 :returns: The summary.
1141 """
1142 return cls(
1143 Counters.Parse(summary["lines"]),
1144 Counters.Parse(summary["functions"]),
1145 Counters.Parse(summary["instantiations"]),
1146 Counters.Parse(summary["regions"]),
1147 Counters.Parse(summary["branches"]) if "branches" in summary else None,
1148 Counters.Parse(summary["mcdc"]) if "mcdc" in summary else None
1149 )
1151 @readonly
1152 def Lines(self) -> Counters:
1153 """
1154 Read-only property to access the counters of lines (:attr:`_lines`).
1156 :returns: The counters.
1157 """
1158 return self._lines
1160 @readonly
1161 def Functions(self) -> Counters:
1162 """
1163 Read-only property to access the counters of functions, a function's instantiations counted once
1164 (:attr:`_functions`).
1166 :returns: The counters.
1167 """
1168 return self._functions
1170 @readonly
1171 def Instantiations(self) -> Counters:
1172 """
1173 Read-only property to access the counters of function instantiations (:attr:`_instantiations`).
1175 :returns: The counters.
1176 """
1177 return self._instantiations
1179 @readonly
1180 def Regions(self) -> Counters:
1181 """
1182 Read-only property to access the counters of code regions (:attr:`_regions`).
1184 :returns: The counters.
1185 """
1186 return self._regions
1188 @readonly
1189 def Branches(self) -> Nullable[Counters]:
1190 """
1191 Read-only property to access the counters of branch outcomes - two per branch region - (:attr:`_branches`).
1193 :returns: The counters, or ``None`` before LLVM 12.
1194 """
1195 return self._branches
1197 @readonly
1198 def MCDC(self) -> Nullable[Counters]:
1199 """
1200 Read-only property to access the counters of MC/DC conditions (:attr:`_mcdc`).
1202 :returns: The counters, or ``None`` before LLVM 18.
1203 """
1204 return self._mcdc
1206 def _SetRoot(self, root: Nullable[Report]) -> None:
1207 """
1208 Set the report's root of the summary, and of its counters.
1210 :param root: The report's root, or ``None`` while the element isn't part of a report.
1211 """
1212 self._root = root
1213 for counters in (self._lines, self._functions, self._instantiations, self._regions, self._branches, self._mcdc):
1214 if counters is not None:
1215 counters._SetRoot(root)
1218_Summary = Summary
1221@export
1222class Segment(Element["File"]):
1223 """
1224 A segment of a file: from a position on, the code has a count - until the next segment.
1226 A segment is ``[line, column, count, hasCount, isRegionEntry, isGapRegion]``. Format version 2.0.0 has no
1227 ``isGapRegion``.
1228 """
1230 _line: int #: Line the segment starts at.
1231 _column: int #: Column the segment starts at.
1232 _count: int #: The count of the code from here on.
1233 _hasCount: bool #: Whether the code from here on has a count; false e.g. after a function's end.
1234 _isRegionEntry: bool #: Whether a region starts here; false, where a region continues after a nested one.
1235 _isGapRegion: bool #: Whether a gap region starts here.
1237 def __init__(
1238 self,
1239 line: int,
1240 column: int,
1241 count: int,
1242 hasCount: bool,
1243 isRegionEntry: bool,
1244 isGapRegion: bool = False,
1245 *,
1246 parent: Nullable[File] = None
1247 ) -> None:
1248 """
1249 Initialize a segment, and add it to the segments of its file.
1251 :param line: Line the segment starts at.
1252 :param column: Column the segment starts at.
1253 :param count: The count of the code from here on.
1254 :param hasCount: Whether the code from here on has a count.
1255 :param isRegionEntry: Whether a region starts here.
1256 :param isGapRegion: Optional, whether a gap region starts here. Default: ``False``.
1257 :param parent: Optional, the file the segment belongs to. Default: ``None``.
1258 :raises ValueError: If parameter ``line`` is ``None``.
1259 :raises TypeError: If parameter ``line`` isn't of type :class:`int`.
1260 :raises ValueError: If parameter ``line`` is less than 1.
1261 :raises ValueError: If parameter ``column`` is ``None``.
1262 :raises TypeError: If parameter ``column`` isn't of type :class:`int`.
1263 :raises ValueError: If parameter ``column`` is less than 1.
1264 :raises ValueError: If parameter ``count`` is ``None``.
1265 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
1266 :raises ValueError: If parameter ``count`` is negative.
1267 :raises ValueError: If parameter ``hasCount`` is ``None``.
1268 :raises TypeError: If parameter ``hasCount`` isn't of type :class:`bool`.
1269 :raises ValueError: If parameter ``isRegionEntry`` is ``None``.
1270 :raises TypeError: If parameter ``isRegionEntry`` isn't of type :class:`bool`.
1271 :raises ValueError: If parameter ``isGapRegion`` is ``None``.
1272 :raises TypeError: If parameter ``isGapRegion`` isn't of type :class:`bool`.
1273 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
1274 """
1275 super().__init__(parent=parent)
1277 for name, position in (("line", line), ("column", column)):
1278 if position is None:
1279 raise ValueError(f"Parameter '{name}' is None.")
1280 elif not isinstance(position, int):
1281 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
1282 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.")
1283 raise ex
1284 elif position < 1:
1285 ex = ValueError(f"Parameter '{name}' is less than 1.")
1286 ex.add_note(f"Got value '{position}'.")
1287 raise ex
1289 if count is None: 1289 ↛ 1290line 1289 didn't jump to line 1290 because the condition on line 1289 was never true
1290 raise ValueError("Parameter 'count' is None.")
1291 elif not isinstance(count, int): 1291 ↛ 1292line 1291 didn't jump to line 1292 because the condition on line 1291 was never true
1292 ex = TypeError("Parameter 'count' is not of type 'int'.")
1293 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
1294 raise ex
1295 elif count < 0: 1295 ↛ 1296line 1295 didn't jump to line 1296 because the condition on line 1295 was never true
1296 ex = ValueError("Parameter 'count' is negative.")
1297 ex.add_note(f"Got value '{count}'.")
1298 raise ex
1300 for name, flag in (("hasCount", hasCount), ("isRegionEntry", isRegionEntry), ("isGapRegion", isGapRegion)):
1301 if flag is None: 1301 ↛ 1302line 1301 didn't jump to line 1302 because the condition on line 1301 was never true
1302 raise ValueError(f"Parameter '{name}' is None.")
1303 elif not isinstance(flag, bool):
1304 ex = TypeError(f"Parameter '{name}' is not of type 'bool'.")
1305 ex.add_note(f"Got type '{getFullyQualifiedName(flag)}'.")
1306 raise ex
1308 self._line = line
1309 self._column = column
1310 self._count = count
1311 self._hasCount = hasCount
1312 self._isRegionEntry = isRegionEntry
1313 self._isGapRegion = isGapRegion
1315 if parent is not None:
1316 parent._segments.append(self)
1318 @classmethod
1319 def Parse(cls, segment: list[Any], *, parent: Nullable[File] = None) -> Self:
1320 """
1321 Parse a segment from its JSON array.
1323 :param segment: The JSON array of the segment.
1324 :param parent: Optional, the file the segment belongs to. Default: ``None``.
1325 :returns: The segment.
1326 """
1327 return cls(*segment[:5], segment[5] if len(segment) > 5 else False, parent=parent)
1329 @readonly
1330 def Line(self) -> int:
1331 """
1332 Read-only property to access the line the segment starts at (:attr:`_line`).
1334 :returns: The line number, counted from 1.
1335 """
1336 return self._line
1338 @readonly
1339 def Column(self) -> int:
1340 """
1341 Read-only property to access the column the segment starts at (:attr:`_column`).
1343 :returns: The column number, counted from 1.
1344 """
1345 return self._column
1347 @readonly
1348 def Count(self) -> int:
1349 """
1350 Read-only property to access the count of the code from the segment's start on (:attr:`_count`).
1352 :returns: How often the code ran; ``0`` without a count.
1353 """
1354 return self._count
1356 @readonly
1357 def HasCount(self) -> bool:
1358 """
1359 Read-only property to access whether the code from the segment's start on has a count (:attr:`_hasCount`).
1361 :returns: ``False`` e.g. after the end of a function, or in a skipped region.
1362 """
1363 return self._hasCount
1365 @readonly
1366 def IsRegionEntry(self) -> bool:
1367 """
1368 Read-only property to access whether a region starts at the segment's start (:attr:`_isRegionEntry`).
1370 :returns: ``False``, where a region continues after a region nested in it.
1371 """
1372 return self._isRegionEntry
1374 @readonly
1375 def IsGapRegion(self) -> bool:
1376 """
1377 Read-only property to access whether a gap region starts at the segment's start (:attr:`_isGapRegion`).
1379 :returns: ``True`` for a gap region; ``False`` in format version 2.0.0.
1380 """
1381 return self._isGapRegion
1384@export
1385class Expansion(Element["File"]):
1386 """
1387 A macro expansion in a file: the region it is expanded at, the regions of the function it is in, and its branches.
1388 """
1390 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index.
1391 _sourceRegion: Region #: The region the macro is expanded at.
1392 _targetRegions: list[Region] #: The regions of the function the expansion is in.
1393 _branches: list[BranchRegion] #: The branch regions in the expansion, and in the expansions nested in it.
1395 def __init__(
1396 self,
1397 filePaths: Iterable[Path],
1398 sourceRegion: Region,
1399 targetRegions: Iterable[Region],
1400 branches: Nullable[Iterable[BranchRegion]] = None,
1401 *,
1402 parent: Nullable[File] = None
1403 ) -> None:
1404 """
1405 Initialize an expansion, and add it to the expansions of its file.
1407 :param filePaths: The paths of the files, which the file IDs of the regions index.
1408 :param sourceRegion: The region the macro is expanded at.
1409 :param targetRegions: The regions of the function the expansion is in.
1410 :param branches: Optional, the branch regions in the expansion, and in the expansions
1411 nested in it. Default: none.
1412 :param parent: Optional, the file the expansion belongs to. Default: ``None``.
1413 :raises ValueError: If parameter ``filePaths`` is ``None``.
1414 :raises TypeError: If parameter ``filePaths`` isn't iterable.
1415 :raises TypeError: If parameter ``filePaths`` contains an element not of type
1416 :class:`~pathlib.Path`.
1417 :raises ValueError: If parameter ``sourceRegion`` is ``None``.
1418 :raises TypeError: If parameter ``sourceRegion`` isn't of type :class:`Region`.
1419 :raises AlreadyInCoverageReportError: If parameter ``sourceRegion`` is a region of another element.
1420 :raises ValueError: If parameter ``sourceRegion`` is a region of a file ID or an expanded file ID
1421 out of range of parameter ``filePaths``.
1422 :raises ValueError: If parameter ``targetRegions`` is ``None``.
1423 :raises TypeError: If parameter ``targetRegions`` isn't iterable.
1424 :raises TypeError: If parameter ``targetRegions`` contains an element not of type
1425 :class:`Region`.
1426 :raises AlreadyInCoverageReportError: If parameter ``targetRegions`` contains a region of another element.
1427 :raises ValueError: If parameter ``targetRegions`` contains a region of a file ID or an expanded
1428 file ID out of range of parameter ``filePaths``.
1429 :raises TypeError: If parameter ``branches`` isn't iterable.
1430 :raises TypeError: If parameter ``branches`` contains an element not of type
1431 :class:`BranchRegion`.
1432 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element.
1433 :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file
1434 ID out of range of parameter ``filePaths``.
1435 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
1436 """
1437 super().__init__(parent=parent)
1439 if filePaths is None: 1439 ↛ 1440line 1439 didn't jump to line 1440 because the condition on line 1439 was never true
1440 raise ValueError("Parameter 'filePaths' is None.")
1441 elif not isinstance(filePaths, Iterable): 1441 ↛ 1442line 1441 didn't jump to line 1442 because the condition on line 1441 was never true
1442 ex = TypeError("Parameter 'filePaths' is not iterable.")
1443 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.")
1444 raise ex
1446 if sourceRegion is None: 1446 ↛ 1447line 1446 didn't jump to line 1447 because the condition on line 1446 was never true
1447 raise ValueError("Parameter 'sourceRegion' is None.")
1448 elif not isinstance(sourceRegion, Region): 1448 ↛ 1449line 1448 didn't jump to line 1449 because the condition on line 1448 was never true
1449 ex = TypeError("Parameter 'sourceRegion' is not of type 'Region'.")
1450 ex.add_note(f"Got type '{getFullyQualifiedName(sourceRegion)}'.")
1451 raise ex
1452 elif sourceRegion._parent is not None: 1452 ↛ 1453line 1452 didn't jump to line 1453 because the condition on line 1452 was never true
1453 raise AlreadyInCoverageReportError("Parameter 'sourceRegion' is a region of another element.")
1455 if targetRegions is None: 1455 ↛ 1456line 1455 didn't jump to line 1456 because the condition on line 1455 was never true
1456 raise ValueError("Parameter 'targetRegions' is None.")
1458 self._filePaths = filePathList = []
1459 self._sourceRegion = sourceRegion
1460 self._targetRegions = []
1461 self._branches = []
1463 for filePath in filePaths:
1464 if not isinstance(filePath, Path):
1465 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.")
1466 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.")
1467 raise ex
1469 filePathList.append(filePath)
1471 for name, elements, elementType, target in (
1472 ("targetRegions", targetRegions, Region, self._targetRegions),
1473 ("branches", branches, BranchRegion, self._branches)
1474 ):
1475 if elements is None:
1476 continue
1477 elif not isinstance(elements, Iterable):
1478 ex = TypeError(f"Parameter '{name}' is not iterable.")
1479 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1480 raise ex
1482 for element in elements:
1483 if not isinstance(element, elementType): 1483 ↛ 1484line 1483 didn't jump to line 1484 because the condition on line 1483 was never true
1484 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.")
1485 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1486 raise ex
1487 elif element._parent is not None: 1487 ↛ 1488line 1487 didn't jump to line 1488 because the condition on line 1487 was never true
1488 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains a region of another element.")
1489 elif element._fileID is not None and element._fileID >= len(self._filePaths):
1490 ex = ValueError(f"Parameter '{name}' contains a region of a file ID out of range.")
1491 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.")
1492 raise ex
1493 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): 1493 ↛ 1494line 1493 didn't jump to line 1494 because the condition on line 1493 was never true
1494 ex = ValueError(f"Parameter '{name}' contains an expansion region of an expanded file ID out of range.")
1495 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.")
1496 raise ex
1498 element._parent = self
1499 element._root = self._root
1500 target.append(element)
1502 if sourceRegion._fileID >= len(self._filePaths): 1502 ↛ 1503line 1502 didn't jump to line 1503 because the condition on line 1502 was never true
1503 ex = ValueError("Parameter 'sourceRegion' is a region of a file ID out of range.")
1504 ex.add_note(f"Got value '{sourceRegion._fileID}' for {len(self._filePaths)} file paths.")
1505 raise ex
1506 elif sourceRegion._kind is RegionKind.Expansion and sourceRegion._expandedFileID >= len(self._filePaths):
1507 ex = ValueError("Parameter 'sourceRegion' is an expansion region of an expanded file ID out of range.")
1508 ex.add_note(f"Got value '{sourceRegion._expandedFileID}' for {len(self._filePaths)} file paths.")
1509 raise ex
1511 sourceRegion._parent = self
1512 sourceRegion._root = self._root
1514 if parent is not None:
1515 parent._expansions.append(self)
1517 @classmethod
1518 def Parse(cls, expansion: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
1519 """
1520 Parse an expansion from its JSON object.
1522 :param expansion: The JSON object of the expansion.
1523 :param parent: Optional, the file the expansion belongs to. Default: ``None``.
1524 :returns: The expansion.
1525 """
1526 return cls(
1527 [Path(filename.replace("\\", "/")) for filename in expansion["filenames"]],
1528 Region.Parse(expansion["source_region"]),
1529 [Region.Parse(region) for region in expansion["target_regions"]],
1530 [BranchRegion.Parse(branch) for branch in expansion.get("branches", [])],
1531 parent=parent
1532 )
1534 @readonly
1535 def FilePaths(self) -> list[Path]:
1536 """
1537 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`).
1539 :returns: The paths, as the compiler named them.
1540 """
1541 return self._filePaths
1543 @readonly
1544 def SourceRegion(self) -> Region:
1545 """
1546 Read-only property to access the region the macro is expanded at (:attr:`_sourceRegion`).
1548 :returns: The region.
1549 """
1550 return self._sourceRegion
1552 @readonly
1553 def TargetRegions(self) -> list[Region]:
1554 """
1555 Read-only property to access the regions of the function the expansion is in (:attr:`_targetRegions`).
1557 :returns: The regions.
1558 """
1559 return self._targetRegions
1561 @readonly
1562 def Branches(self) -> list[BranchRegion]:
1563 """
1564 Read-only property to access the branch regions in the expansion, and in the expansions nested in it
1565 (:attr:`_branches`).
1567 :returns: The branch regions; empty before LLVM 12.
1568 """
1569 return self._branches
1571 def _SetRoot(self, root: Nullable[Report]) -> None:
1572 """
1573 Set the report's root of the expansion, and of its source region, target regions and branch regions.
1575 :param root: The report's root, or ``None`` while the element isn't part of a report.
1576 """
1577 self._root = root
1578 for region in (self._sourceRegion, *self._targetRegions, *self._branches):
1579 region._SetRoot(root)
1582@export
1583class File(Element["Report"]):
1584 """
1585 A file: its segments, branch regions, MC/DC records and expansions, and its summary.
1587 A report written with ``-summary-only`` has only the summary, one written with ``-skip-expansions`` no expansions.
1588 """
1590 _path: _Path #: The file's path, as the compiler named it.
1591 _segments: list[Segment] #: The segments, by position.
1592 _branches: list[BranchRegion] #: The branch regions of the file's functions.
1593 _mcdcRecords: list[MCDCRecord] #: The MC/DC records of the file's functions.
1594 _expansions: list[Expansion] #: The macro expansions in the file.
1595 _summary: _Summary #: The counters llvm-cov computed.
1597 def __init__(
1598 self,
1599 path: Path,
1600 summary: Summary,
1601 segments: Nullable[Iterable[Segment]] = None,
1602 branches: Nullable[Iterable[BranchRegion]] = None,
1603 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None,
1604 expansions: Nullable[Iterable[Expansion]] = None,
1605 *,
1606 parent: Nullable[Report] = None
1607 ) -> None:
1608 """
1609 Initialize a file, and add it to the files of its report.
1611 :param path: The file's path, as the compiler named it.
1612 :param summary: The counters llvm-cov computed.
1613 :param segments: Optional, the segments, by position. Default: none.
1614 :param branches: Optional, the branch regions of the file's functions. Default: none.
1615 :param mcdcRecords: Optional, the MC/DC records of the file's functions. Default: none.
1616 :param expansions: Optional, the macro expansions in the file. Default: none.
1617 :param parent: Optional, the report the file belongs to. Default: ``None``.
1618 :raises ValueError: If parameter ``path`` is ``None``.
1619 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1620 :raises ValueError: If parameter ``summary`` is ``None``.
1621 :raises TypeError: If parameter ``summary`` isn't of type :class:`Summary`.
1622 :raises AlreadyInCoverageReportError: If parameter ``summary`` is the summary of another element.
1623 :raises TypeError: If parameter ``segments`` isn't iterable.
1624 :raises TypeError: If parameter ``segments`` contains an element not of type
1625 :class:`Segment`.
1626 :raises AlreadyInCoverageReportError: If parameter ``segments`` contains an element of another element.
1627 :raises TypeError: If parameter ``branches`` isn't iterable.
1628 :raises TypeError: If parameter ``branches`` contains an element not of type
1629 :class:`BranchRegion`.
1630 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains an element of another element.
1631 :raises TypeError: If parameter ``mcdcRecords`` isn't iterable.
1632 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type
1633 :class:`MCDCRecord`.
1634 :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains an element of another element.
1635 :raises TypeError: If parameter ``expansions`` isn't iterable.
1636 :raises TypeError: If parameter ``expansions`` contains an element not of type
1637 :class:`Expansion`.
1638 :raises AlreadyInCoverageReportError: If parameter ``expansions`` contains an element of another element.
1639 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`.
1640 :raises DuplicateFileError: If parameter ``parent`` has a file of the same path.
1641 """
1642 super().__init__(parent=parent)
1644 if path is None: 1644 ↛ 1645line 1644 didn't jump to line 1645 because the condition on line 1644 was never true
1645 raise ValueError("Parameter 'path' is None.")
1646 elif not isinstance(path, Path):
1647 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1648 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1649 raise ex
1650 elif parent is not None and path in parent._files:
1651 ex = DuplicateFileError("Parameter 'parent' has a file of the same path.")
1652 ex.add_note(f"Got value '{path.as_posix()}'.")
1653 raise ex
1655 if summary is None: 1655 ↛ 1656line 1655 didn't jump to line 1656 because the condition on line 1655 was never true
1656 raise ValueError("Parameter 'summary' is None.")
1657 elif not isinstance(summary, Summary): 1657 ↛ 1658line 1657 didn't jump to line 1658 because the condition on line 1657 was never true
1658 ex = TypeError("Parameter 'summary' is not of type 'Summary'.")
1659 ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.")
1660 raise ex
1661 elif summary._parent is not None:
1662 raise AlreadyInCoverageReportError("Parameter 'summary' is the summary of another element.")
1664 self._path = path
1665 self._segments = []
1666 self._branches = []
1667 self._mcdcRecords = []
1668 self._expansions = []
1669 self._summary = summary
1671 for name, elements, elementType, target in (
1672 ("segments", segments, Segment, self._segments), ("branches", branches, BranchRegion, self._branches),
1673 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords),
1674 ("expansions", expansions, Expansion, self._expansions)
1675 ):
1676 if elements is None:
1677 continue
1678 elif not isinstance(elements, Iterable): 1678 ↛ 1679line 1678 didn't jump to line 1679 because the condition on line 1678 was never true
1679 ex = TypeError(f"Parameter '{name}' is not iterable.")
1680 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1681 raise ex
1683 for element in elements:
1684 if not isinstance(element, elementType): 1684 ↛ 1685line 1684 didn't jump to line 1685 because the condition on line 1684 was never true
1685 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.")
1686 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1687 raise ex
1688 elif element._parent is not None: 1688 ↛ 1689line 1688 didn't jump to line 1689 because the condition on line 1688 was never true
1689 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains an element of another element.")
1691 element._parent = self
1692 element._SetRoot(self._root)
1694 target.append(element)
1696 summary._parent = self
1697 summary._SetRoot(self._root)
1699 if parent is not None:
1700 parent._files[path] = self
1702 @classmethod
1703 def Parse(cls, file: dict[str, Any], *, parent: Nullable[Report] = None) -> Self:
1704 """
1705 Parse a file from its JSON object.
1707 :param file: The JSON object of the file.
1708 :param parent: Optional, the report the file belongs to. Default: ``None``.
1709 :returns: The file.
1710 """
1711 return cls(
1712 Path(file["filename"].replace("\\", "/")),
1713 Summary.Parse(file["summary"]),
1714 [Segment.Parse(segment) for segment in file.get("segments", [])],
1715 [BranchRegion.Parse(branch) for branch in file.get("branches", [])],
1716 [MCDCRecord.Parse(record) for record in file.get("mcdc_records", [])],
1717 [Expansion.Parse(expansion) for expansion in file.get("expansions", [])],
1718 parent=parent
1719 )
1721 @readonly
1722 def Path(self) -> Path:
1723 """
1724 Read-only property to access the file's path (:attr:`_path`).
1726 :returns: The path, as the compiler named it - usually absolute.
1727 """
1728 return self._path
1730 @readonly
1731 def Segments(self) -> list[Segment]:
1732 """
1733 Read-only property to access the segments (:attr:`_segments`).
1735 :returns: The segments, by position; empty in a summary-only report.
1736 """
1737 return self._segments
1739 @readonly
1740 def Branches(self) -> list[BranchRegion]:
1741 """
1742 Read-only property to access the branch regions of the file's functions (:attr:`_branches`).
1744 Which branch regions of a macro expansion are listed here, depends on the LLVM version.
1746 :returns: The branch regions.
1747 """
1748 return self._branches
1750 @readonly
1751 def MCDCRecords(self) -> list[MCDCRecord]:
1752 """
1753 Read-only property to access the MC/DC records of the file's functions (:attr:`_mcdcRecords`).
1755 :returns: The MC/DC records.
1756 """
1757 return self._mcdcRecords
1759 @readonly
1760 def Expansions(self) -> list[Expansion]:
1761 """
1762 Read-only property to access the macro expansions in the file (:attr:`_expansions`).
1764 :returns: The expansions.
1765 """
1766 return self._expansions
1768 @readonly
1769 def Summary(self) -> Summary:
1770 """
1771 Read-only property to access the counters llvm-cov computed (:attr:`_summary`).
1773 :returns: The summary.
1774 """
1775 return self._summary
1777 def _SetRoot(self, root: Nullable[Report]) -> None:
1778 """
1779 Set the report's root of the file, and of its segments, branch regions, MC/DC records, expansions and summary.
1781 :param root: The report's root, or ``None`` while the element isn't part of a report.
1782 """
1783 self._root = root
1784 for element in (*self._segments, *self._branches, *self._mcdcRecords, *self._expansions, self._summary):
1785 element._SetRoot(root)
1788@export
1789class Function(Element["Report"]):
1790 """
1791 A function - an instantiation of a template is a function of its own -: how often it was called, its regions, branch
1792 regions and MC/DC records, and the files they are in.
1793 """
1795 _name: str #: The function's name, as the profile names it - e.g. mangled.
1796 _count: int #: How often the function was called.
1797 _regions: list[Region] #: The regions.
1798 _branches: list[BranchRegion] #: The branch regions.
1799 _mcdcRecords: list[MCDCRecord] #: The MC/DC records.
1800 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index.
1802 def __init__(
1803 self,
1804 name: str,
1805 count: int,
1806 regions: Iterable[Region],
1807 filePaths: Iterable[Path],
1808 branches: Nullable[Iterable[BranchRegion]] = None,
1809 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None,
1810 *,
1811 parent: Nullable[Report] = None
1812 ) -> None:
1813 """
1814 Initialize a function, and add it to the functions of its report.
1816 :param name: The function's name, as the profile names it - e.g. mangled.
1817 :param count: How often the function was called.
1818 :param regions: The regions.
1819 :param filePaths: The paths of the files, which the file IDs of the regions index.
1820 :param branches: Optional, the branch regions. Default: none.
1821 :param mcdcRecords: Optional, the MC/DC records. Default: none.
1822 :param parent: Optional, the report the function belongs to. Default: ``None``.
1823 :raises ValueError: If parameter ``name`` is ``None``.
1824 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
1825 :raises ValueError: If parameter ``name`` is empty.
1826 :raises ValueError: If parameter ``count`` is ``None``.
1827 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
1828 :raises ValueError: If parameter ``count`` is negative.
1829 :raises ValueError: If parameter ``regions`` is ``None``.
1830 :raises TypeError: If parameter ``regions`` isn't iterable.
1831 :raises TypeError: If parameter ``regions`` contains an element not of type
1832 :class:`Region`.
1833 :raises AlreadyInCoverageReportError: If parameter ``regions`` contains a region of another element.
1834 :raises ValueError: If parameter ``regions`` contains a region of a file ID or an expanded file ID
1835 out of range of parameter ``filePaths``.
1836 :raises ValueError: If parameter ``filePaths`` is ``None``.
1837 :raises TypeError: If parameter ``filePaths`` isn't iterable.
1838 :raises TypeError: If parameter ``filePaths`` contains an element not of type
1839 :class:`~pathlib.Path`.
1840 :raises TypeError: If parameter ``branches`` isn't iterable.
1841 :raises TypeError: If parameter ``branches`` contains an element not of type
1842 :class:`BranchRegion`.
1843 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element.
1844 :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file
1845 ID out of range of parameter ``filePaths``.
1846 :raises TypeError: If parameter ``mcdcRecords`` isn't iterable.
1847 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type
1848 :class:`MCDCRecord`.
1849 :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains a region of another element.
1850 :raises ValueError: If parameter ``mcdcRecords`` contains a region of a file ID or an expanded
1851 file ID out of range of parameter ``filePaths``.
1852 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`.
1853 """
1854 super().__init__(parent=parent)
1856 if name is None: 1856 ↛ 1857line 1856 didn't jump to line 1857 because the condition on line 1856 was never true
1857 raise ValueError("Parameter 'name' is None.")
1858 elif not isinstance(name, str): 1858 ↛ 1859line 1858 didn't jump to line 1859 because the condition on line 1858 was never true
1859 ex = TypeError("Parameter 'name' is not of type 'str'.")
1860 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
1861 raise ex
1862 elif name == "":
1863 raise ValueError("Parameter 'name' is empty.")
1865 if count is None: 1865 ↛ 1866line 1865 didn't jump to line 1866 because the condition on line 1865 was never true
1866 raise ValueError("Parameter 'count' is None.")
1867 elif not isinstance(count, int): 1867 ↛ 1868line 1867 didn't jump to line 1868 because the condition on line 1867 was never true
1868 ex = TypeError("Parameter 'count' is not of type 'int'.")
1869 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
1870 raise ex
1871 elif count < 0: 1871 ↛ 1872line 1871 didn't jump to line 1872 because the condition on line 1871 was never true
1872 ex = ValueError("Parameter 'count' is negative.")
1873 ex.add_note(f"Got value '{count}'.")
1874 raise ex
1876 if regions is None: 1876 ↛ 1877line 1876 didn't jump to line 1877 because the condition on line 1876 was never true
1877 raise ValueError("Parameter 'regions' is None.")
1879 if filePaths is None: 1879 ↛ 1880line 1879 didn't jump to line 1880 because the condition on line 1879 was never true
1880 raise ValueError("Parameter 'filePaths' is None.")
1881 elif not isinstance(filePaths, Iterable): 1881 ↛ 1882line 1881 didn't jump to line 1882 because the condition on line 1881 was never true
1882 ex = TypeError("Parameter 'filePaths' is not iterable.")
1883 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.")
1884 raise ex
1886 self._name = name
1887 self._count = count
1888 self._regions = []
1889 self._branches = []
1890 self._mcdcRecords = []
1891 self._filePaths = filePathList = []
1893 for filePath in filePaths:
1894 if not isinstance(filePath, Path): 1894 ↛ 1895line 1894 didn't jump to line 1895 because the condition on line 1894 was never true
1895 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.")
1896 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.")
1897 raise ex
1899 filePathList.append(filePath)
1901 for parameterName, elements, elementType, target in (
1902 ("regions", regions, Region, self._regions), ("branches", branches, BranchRegion, self._branches),
1903 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords)
1904 ):
1905 if elements is None:
1906 continue
1907 elif not isinstance(elements, Iterable): 1907 ↛ 1908line 1907 didn't jump to line 1908 because the condition on line 1907 was never true
1908 ex = TypeError(f"Parameter '{parameterName}' is not iterable.")
1909 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1910 raise ex
1912 for element in elements:
1913 if not isinstance(element, elementType): 1913 ↛ 1914line 1913 didn't jump to line 1914 because the condition on line 1913 was never true
1914 ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{elementType.__name__}'.")
1915 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1916 raise ex
1917 elif element._parent is not None:
1918 raise AlreadyInCoverageReportError(f"Parameter '{parameterName}' contains a region of another element.")
1919 elif element._fileID is not None and element._fileID >= len(self._filePaths):
1920 ex = ValueError(f"Parameter '{parameterName}' contains a region of a file ID out of range.")
1921 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.")
1922 raise ex
1923 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths):
1924 ex = ValueError(
1925 f"Parameter '{parameterName}' contains an expansion region of an expanded file ID out of range."
1926 )
1927 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.")
1928 raise ex
1930 element._parent = self
1931 element._SetRoot(self._root)
1933 target.append(element)
1935 if parent is not None:
1936 parent._functions.append(self)
1938 @classmethod
1939 def Parse(cls, function: dict[str, Any], *, parent: Nullable[Report] = None) -> Self:
1940 """
1941 Parse a function from its JSON object.
1943 :param function: The JSON object of the function.
1944 :param parent: Optional, the report the function belongs to. Default: ``None``.
1945 :returns: The function.
1946 """
1947 return cls(
1948 function["name"],
1949 function["count"],
1950 [Region.Parse(region) for region in function["regions"]],
1951 [Path(filename.replace("\\", "/")) for filename in function["filenames"]],
1952 [BranchRegion.Parse(branch) for branch in function.get("branches", [])],
1953 [MCDCRecord.Parse(record) for record in function.get("mcdc_records", [])],
1954 parent=parent
1955 )
1957 @readonly
1958 def Name(self) -> str:
1959 """
1960 Read-only property to access the function's name (:attr:`_name`).
1962 :returns: The name, as the profile names it: mangled, and prefixed by the file name of its translation unit, if
1963 local to it - e.g. ``Statistics.c:Square``.
1964 """
1965 return self._name
1967 @readonly
1968 def Count(self) -> int:
1969 """
1970 Read-only property to access how often the function was called (:attr:`_count`).
1972 :returns: The count.
1973 """
1974 return self._count
1976 @readonly
1977 def Regions(self) -> list[Region]:
1978 """
1979 Read-only property to access the regions (:attr:`_regions`).
1981 :returns: The regions.
1982 """
1983 return self._regions
1985 @readonly
1986 def Branches(self) -> list[BranchRegion]:
1987 """
1988 Read-only property to access the branch regions (:attr:`_branches`).
1990 :returns: The branch regions, also those in macro expansions.
1991 """
1992 return self._branches
1994 @readonly
1995 def MCDCRecords(self) -> list[MCDCRecord]:
1996 """
1997 Read-only property to access the MC/DC records (:attr:`_mcdcRecords`).
1999 :returns: The MC/DC records.
2000 """
2001 return self._mcdcRecords
2003 @readonly
2004 def FilePaths(self) -> list[Path]:
2005 """
2006 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`).
2008 :returns: The paths, as the compiler named them.
2009 """
2010 return self._filePaths
2012 @readonly
2013 def MainFileID(self) -> Nullable[int]:
2014 """
2015 Read-only property to return the file ID of the file the function is in: the first file no expansion region
2016 expands to.
2018 :returns: The index into :attr:`FilePaths`, or ``None`` if every file is expanded to.
2019 """
2020 expanded = {region._expandedFileID for region in self._regions if region._kind is RegionKind.Expansion}
2021 return next((fileID for fileID in range(len(self._filePaths)) if fileID not in expanded), None)
2023 @readonly
2024 def MainFilePath(self) -> Nullable[Path]:
2025 """
2026 Read-only property to return the path of the file the function is in (:attr:`MainFileID`).
2028 :returns: The path, as the compiler named it; ``None`` if every file is expanded to.
2029 """
2030 return None if (mainFileID := self.MainFileID) is None else self._filePaths[mainFileID]
2032 @readonly
2033 def MainFile(self) -> Nullable[File]:
2034 """
2035 Read-only property to return the file the function is in, as the report lists it.
2037 :returns: The file of :attr:`MainFilePath` in the report's files; ``None`` if the function isn't part of a report,
2038 every file is expanded to, or the report doesn't list the file.
2039 """
2040 if self._root is None or (path := self.MainFilePath) is None: 2040 ↛ 2041line 2040 didn't jump to line 2041 because the condition on line 2040 was never true
2041 return None
2043 return self._root._files.get(path, None)
2045 def _SetRoot(self, root: Nullable[Report]) -> None:
2046 """
2047 Set the report's root of the function, and of its regions, branch regions and MC/DC records.
2049 :param root: The report's root, or ``None`` while the element isn't part of a report.
2050 """
2051 self._root = root
2052 for element in (*self._regions, *self._branches, *self._mcdcRecords):
2053 element._SetRoot(root)
2056@export
2057class Report(metaclass=ExtendedType, mixin=True):
2058 """
2059 The report's root: the format version, the files, the functions, and the totals.
2061 It is the root of every element below it.
2062 """
2064 _root: Report #: The report's root: the report itself.
2065 _version: Nullable[SemanticVersion] #: Version of the report format.
2066 _files: dict[Path, File] #: The files, by path.
2067 _functions: list[Function] #: The functions.
2068 _totals: Nullable[Summary] #: The counters of the whole report.
2070 def __init__(
2071 self,
2072 version: Nullable[SemanticVersion] = None,
2073 files: Nullable[Iterable[File]] = None,
2074 functions: Nullable[Iterable[Function]] = None,
2075 totals: Nullable[Summary] = None
2076 ) -> None:
2077 """
2078 Initialize a report; empty, without parameters.
2080 :param version: Optional, version of the report format. Default: ``None``.
2081 :param files: Optional, the files. Default: none.
2082 :param functions: Optional, the functions. Default: none.
2083 :param totals: Optional, the counters of the whole report. Default: ``None``.
2084 :raises TypeError: If parameter ``version`` isn't of type
2085 :class:`~pyTooling.Versioning.SemanticVersion`.
2086 :raises TypeError: If parameter ``files`` isn't iterable.
2087 :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`.
2088 :raises DuplicateFileError: If parameter ``files`` contains two files of the same path.
2089 :raises AlreadyInCoverageReportError: If parameter ``files`` contains an element of another report.
2090 :raises TypeError: If parameter ``functions`` isn't iterable.
2091 :raises TypeError: If parameter ``functions`` contains an element not of type
2092 :class:`Function`.
2093 :raises AlreadyInCoverageReportError: If parameter ``functions`` contains an element of another report.
2094 :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`.
2095 :raises AlreadyInCoverageReportError: If parameter ``totals`` is the summary of another element.
2096 """
2097 if version is not None and not isinstance(version, SemanticVersion):
2098 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.")
2099 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2100 raise ex
2102 if totals is not None and not isinstance(totals, Summary): 2102 ↛ 2103line 2102 didn't jump to line 2103 because the condition on line 2102 was never true
2103 ex = TypeError("Parameter 'totals' is not of type 'Summary'.")
2104 ex.add_note(f"Got type '{getFullyQualifiedName(totals)}'.")
2105 raise ex
2106 elif totals is not None and totals._parent is not None: 2106 ↛ 2107line 2106 didn't jump to line 2107 because the condition on line 2106 was never true
2107 raise AlreadyInCoverageReportError("Parameter 'totals' is the summary of another element.")
2109 self._root = self
2110 self._version = version
2111 self._files = {}
2112 self._functions = functionList = []
2113 self._totals = totals
2115 if files is not None:
2116 if not isinstance(files, Iterable): 2116 ↛ 2117line 2116 didn't jump to line 2117 because the condition on line 2116 was never true
2117 ex = TypeError("Parameter 'files' is not iterable.")
2118 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.")
2119 raise ex
2121 for file in files:
2122 if not isinstance(file, File):
2123 ex = TypeError("Parameter 'files' contains an element not of type 'File'.")
2124 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
2125 raise ex
2126 elif file._parent is not None:
2127 raise AlreadyInCoverageReportError("Parameter 'files' contains a file of another report.")
2128 elif file._path in self._files:
2129 ex = DuplicateFileError("Parameter 'files' contains two files of the same path.")
2130 ex.add_note(f"Got value '{file._path.as_posix()}'.")
2131 raise ex
2133 file._parent = self
2134 file._SetRoot(self)
2136 self._files[file._path] = file
2138 if functions is not None:
2139 if not isinstance(functions, Iterable): 2139 ↛ 2140line 2139 didn't jump to line 2140 because the condition on line 2139 was never true
2140 ex = TypeError("Parameter 'functions' is not iterable.")
2141 ex.add_note(f"Got type '{getFullyQualifiedName(functions)}'.")
2142 raise ex
2144 for function in functions:
2145 if not isinstance(function, Function): 2145 ↛ 2146line 2145 didn't jump to line 2146 because the condition on line 2145 was never true
2146 ex = TypeError("Parameter 'functions' contains an element not of type 'Function'.")
2147 ex.add_note(f"Got type '{getFullyQualifiedName(function)}'.")
2148 raise ex
2149 elif function._parent is not None: 2149 ↛ 2150line 2149 didn't jump to line 2150 because the condition on line 2149 was never true
2150 raise AlreadyInCoverageReportError("Parameter 'functions' contains a function of another report.")
2152 function._parent = self
2153 function._SetRoot(self)
2155 functionList.append(function)
2157 if totals is not None:
2158 totals._parent = self
2159 totals._SetRoot(self)
2161 @readonly
2162 def Version(self) -> Nullable[SemanticVersion]:
2163 """
2164 Read-only property to access the version of the report format (:attr:`_version`).
2166 :returns: The version, e.g. ``2.0.1``; ``None`` before the report was converted.
2167 """
2168 return self._version
2170 @readonly
2171 def Files(self) -> dict[Path, File]:
2172 """
2173 Read-only property to access the files (:attr:`_files`).
2175 :returns: The files, by path.
2176 """
2177 return self._files
2179 @readonly
2180 def Functions(self) -> list[Function]:
2181 """
2182 Read-only property to access the functions (:attr:`_functions`).
2184 :returns: The functions; empty in a report written with ``-skip-functions`` or ``-summary-only``.
2185 """
2186 return self._functions
2188 @readonly
2189 def Totals(self) -> Nullable[Summary]:
2190 """
2191 Read-only property to access the counters of the whole report (:attr:`_totals`).
2193 :returns: The totals; ``None`` before the report was converted.
2194 """
2195 return self._totals
2198TestVector._PARENT_TYPE = (MCDCRecord, )
2199Region._PARENT_TYPE = (Function, Expansion)
2200BranchRegion._PARENT_TYPE = (File, Function, Expansion)
2201MCDCRecord._PARENT_TYPE = (File, Function)
2202Counters._PARENT_TYPE = (Summary, )
2203Summary._PARENT_TYPE = (File, Report)
2204Segment._PARENT_TYPE = (File, )
2205Expansion._PARENT_TYPE = (File, )
2206File._PARENT_TYPE = (Report, )
2207Function._PARENT_TYPE = (Report, )
2210@export
2211class Document(cc_Document, Report):
2212 """
2213 An LLVM JSON code coverage export: read into the format's model, and converted to the common model.
2214 """
2216 _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`.
2218 def __init__(
2219 self,
2220 jsonReportFile: Path,
2221 analyzeAndConvert: bool = False,
2222 *,
2223 version: Nullable[SemanticVersion] = None,
2224 files: Nullable[Iterable[File]] = None,
2225 functions: Nullable[Iterable[Function]] = None,
2226 totals: Nullable[Summary] = None
2227 ) -> None:
2228 """
2229 Initialize the report, and optionally read it; or build it from typed values.
2231 :param jsonReportFile: Path to the JSON file.
2232 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
2233 :param version: Optional, version of the report format. Default: ``None``.
2234 :param files: Optional, the files. Default: none.
2235 :param functions: Optional, the functions. Default: none.
2236 :param totals: Optional, the counters of the whole report. Default: ``None``.
2237 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`.
2238 :raises TypeError: If parameter ``files`` isn't iterable.
2239 :raises TypeError: If parameter ``files`` contains an element not of type
2240 :class:`File`.
2241 :raises TypeError: If parameter ``functions`` isn't iterable.
2242 :raises TypeError: If parameter ``functions`` contains an element not of type
2243 :class:`Function`.
2244 :raises TypeError: If parameter ``totals`` isn't of type
2245 :class:`Summary`.
2246 """
2247 super().__init__(jsonReportFile)
2248 Report.__init__(self, version, files, functions, totals)
2250 self._jsonDocument = None
2252 if analyzeAndConvert:
2253 self.Analyze()
2254 self.Convert()
2256 def Analyze(self) -> None:
2257 """
2258 Parse the JSON file and validate it against the JSON Schema :data:`SCHEMA`.
2260 :raises CodeCoverageError: If the file doesn't exist.
2261 :raises CodeCoverageError: If the file isn't valid JSON.
2262 :raises CodeCoverageError: If the JSON Schema can't be read.
2263 :raises CodeCoverageError: If the file isn't valid according to the JSON Schema.
2264 """
2265 if not self._path.exists():
2266 raise CodeCoverageError(f"LLVM coverage export file '{self._path}' does not exist.") \
2267 from FileNotFoundError(f"File '{self._path}' not found.")
2269 from jsonschema import Draft202012Validator
2271 with Stopwatch() as sw:
2272 try:
2273 jsonDocument = loads(self._path.read_text(encoding="utf-8"))
2274 except JSONDecodeError as ex:
2275 raise CodeCoverageError(f"JSON syntax error in LLVM coverage export file '{self._path}'.") from ex
2277 try:
2278 schema = loads(readResourceFile(Resources, SCHEMA))
2279 except (ToolingException, JSONDecodeError) as ex:
2280 raise CodeCoverageError(f"Couldn't read JSON Schema '{SCHEMA}' from package resources.") from ex
2282 errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path))
2283 if len(errors) > 0:
2284 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{SCHEMA}'.")
2285 for error in errors:
2286 ex.add_note(f"/{'/'.join(str(part) for part in error.path)}: {error.message}")
2287 raise ex
2289 self._jsonDocument = jsonDocument
2291 self._analysisDuration = sw.Duration
2293 def Convert(self) -> None:
2294 """
2295 Convert the parsed and validated JSON document to the format's model.
2297 :raises CodeCoverageError: If the JSON file was not analyzed before. |br|
2298 Call 'Document.Analyze()' or create the document using
2299 'Document(path, analyzeAndConvert=True)'.
2300 """
2301 if self._jsonDocument is None:
2302 ex = CodeCoverageError(
2303 f"LLVM coverage export file '{self._path}' needs to be read and analyzed by a JSON parser."
2304 )
2305 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
2306 raise ex
2308 with Stopwatch() as sw:
2309 data = self._jsonDocument["data"][0]
2310 self._version = SemanticVersion.Parse(self._jsonDocument["version"])
2311 self._files = {}
2312 self._functions = []
2313 self._totals = Summary.Parse(data["totals"])
2315 self._totals._parent = self
2316 self._totals._SetRoot(self)
2318 for record in data["files"]:
2319 File.Parse(record, parent=self)
2321 for function in data.get("functions", []):
2322 Function.Parse(function, parent=self)
2324 self._conversionDuration = sw.Duration
2326 def ToCoverageSummary(self) -> CoverageSummary:
2327 """
2328 Convert the format's model to the common model, and aggregate it.
2330 :returns: The report's root of the common model, named after the report file.
2331 :raises CodeCoverageError: If a file's path runs through another file.
2332 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to.
2333 """
2334 parents = [path.parent.parts for path in self._files]
2335 first = min(parents, default=())
2336 last = max(parents, default=())
2337 common = first
2338 for index, part in enumerate(first): 2338 ↛ 2343line 2338 didn't jump to line 2343 because the loop on line 2338 didn't complete
2339 if part != last[index]:
2340 common = first[:index]
2341 break
2343 summary = CoverageSummary(self._path.stem, sourceDirectories=[Path(*common)] if len(common) > 0 else [])
2344 commonFiles = {path: summary.GetOrAddFile("/".join(path.parts[len(common):])) for path in self._files}
2346 branches = self._CollectBranches()
2347 for path, file in self._files.items():
2348 self._ConvertLines(file, commonFiles[path], branches[path])
2350 self._ConvertUnits(commonFiles, summary)
2352 summary.Aggregate()
2353 return summary
2355 def _CollectBranches(self) -> dict[Path, dict[tuple[Any, ...], list[int]]]:
2356 """
2357 Collect the branch regions of the functions by file; a branch region found in several functions is summed.
2359 A branch region is collected at the line it starts at, or at the line its macro is expanded at. It is found in
2360 several functions e.g. in the instantiations of a template.
2362 :returns: By path, the summed true and false counts of each branch region, keyed by the line
2363 number first.
2364 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to.
2365 """
2366 branches: dict[Path, dict[tuple[Any, ...], list[int]]] = {path: {} for path in self._files}
2367 for function in self._functions:
2368 mainFileID = function.MainFileID
2369 if mainFileID is None or (fileBranches := branches.get(function._filePaths[mainFileID])) is None: 2369 ↛ 2370line 2369 didn't jump to line 2370 because the condition on line 2369 was never true
2370 continue
2372 expansions = {
2373 region._expandedFileID: region for region in function._regions if region._kind is RegionKind.Expansion
2374 }
2375 for branch in function._branches:
2376 lineNumber = branch._lineStart
2377 fileID = branch._fileID
2378 while fileID != mainFileID:
2379 if (expansion := expansions.get(fileID)) is None:
2380 ex = CodeCoverageError(f"Function '{function._name}' has a branch region in a file no region expands to.")
2381 ex.add_note(f"Got file ID {fileID} of file '{function._filePaths[fileID]}'.")
2382 raise ex
2384 lineNumber = expansion._lineStart
2385 fileID = expansion._fileID
2387 key = (
2388 lineNumber, function._filePaths[branch._fileID],
2389 branch._lineStart, branch._columnStart, branch._lineEnd, branch._columnEnd
2390 )
2391 if (counts := fileBranches.get(key)) is None:
2392 fileBranches[key] = [branch._trueCount, branch._falseCount]
2393 else:
2394 counts[0] += branch._trueCount
2395 counts[1] += branch._falseCount
2397 return branches
2399 @staticmethod
2400 def _ConvertLines(file: File, commonFile: cc_File, branches: dict[tuple[Any, ...], list[int]]) -> None:
2401 """
2402 Convert a file's segments to lines of the common model, and add the branches of the file's branch regions.
2404 A line's count is derived as ``llvm-cov show`` derives it: of the segments starting on the line, and of the last
2405 segment of a previous line, which continues on it. A branch region of a line, which isn't executable, is left out.
2407 :param file: The file of the format's model.
2408 :param commonFile: The file of the common model.
2409 :param branches: The true and false counts of each branch region of the file, keyed by the line number first.
2410 """
2411 counts: dict[int, int] = {}
2412 wrapped: Nullable[Segment] = None
2413 segments = file._segments
2414 index = 0
2415 while index < len(segments):
2416 lineNumber = segments[index]._line
2417 lineSegments = []
2418 while index < len(segments) and segments[index]._line == lineNumber:
2419 lineSegments.append(segments[index])
2420 index += 1
2422 starts = [s._count for s in lineSegments if s._hasCount and s._isRegionEntry and not s._isGapRegion]
2423 skipped = not lineSegments[0]._hasCount and lineSegments[0]._isRegionEntry
2424 if (
2425 (not skipped and ((wrapped is not None and wrapped._hasCount) or len(starts) > 0)) or
2426 any(segment._hasCount and segment._isRegionEntry for segment in lineSegments)
2427 ):
2428 counts[lineNumber] = max((wrapped._count if wrapped is not None else 0, *starts))
2430 # the lines up to the next segment continue the last segment of this line
2431 wrapped = lineSegments[-1]
2432 if wrapped._hasCount:
2433 nextLineNumber = segments[index]._line if index < len(segments) else lineNumber + 1
2434 for number in range(lineNumber + 1, nextLineNumber):
2435 counts[number] = wrapped._count
2437 partialLines = {key[0] for key, outcomes in branches.items() if 0 in outcomes}
2438 for number in sorted(counts):
2439 count = counts[number]
2440 if count == 0:
2441 status = LineCoverageStatus.Uncovered
2442 elif number in partialLines:
2443 status = LineCoverageStatus.PartiallyCovered
2444 else:
2445 status = LineCoverageStatus.Covered
2447 cc_Line(number, status, count, parent=commonFile)
2449 lines = commonFile._lines
2450 for key in sorted(branches):
2451 if key[0] < len(lines) and (line := lines[key[0]]) is not None: 2451 ↛ 2450line 2451 didn't jump to line 2450 because the condition on line 2451 was always true
2452 for count in branches[key]:
2453 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=line)
2455 def _ConvertUnits(self, commonFiles: dict[Path, cc_File], summary: CoverageSummary) -> None:
2456 """
2457 Convert the files to source file units, and the functions to function units of their files.
2459 A function local to a translation unit is named without the file name prefixing it. Functions of the same name in a
2460 file - e.g. a header's inline function compiled in several translation units - become one unit, their counts
2461 summed.
2463 :param commonFiles: The files of the common model, by path.
2464 :param summary: The report's root of the common model.
2465 """
2466 functions: dict[tuple[Path, str], list[int]] = {}
2467 for function in self._functions:
2468 mainFileID = function.MainFileID
2469 if mainFileID is None or (path := function._filePaths[mainFileID]) not in commonFiles: 2469 ↛ 2470line 2469 didn't jump to line 2470 because the condition on line 2469 was never true
2470 continue
2472 name = function._name
2473 if (local := match(r"(.+?\.[^.:;/\\]+)[:;](.+)", name)) is not None:
2474 name = local[2]
2476 regions = [region for region in function._regions if region._fileID == mainFileID]
2477 firstLineNumber = min((region._lineStart for region in regions), default=0)
2478 lastLineNumber = max((region._lineEnd for region in regions), default=0)
2479 if (span := functions.get((path, name))) is None: 2479 ↛ 2482line 2479 didn't jump to line 2482 because the condition on line 2479 was always true
2480 functions[(path, name)] = [function._count, firstLineNumber, lastLineNumber]
2481 else:
2482 span[0] += function._count
2483 span[1] = min(span[1], firstLineNumber)
2484 span[2] = max(span[2], lastLineNumber)
2486 sourceFiles: dict[Path, cc_SourceFile] = {}
2487 for path, commonFile in commonFiles.items():
2488 lastLineNumber = commonFile._lastLineNumber
2489 sourceFiles[path] = cc_SourceFile(
2490 commonFile.Path.as_posix(),
2491 file=commonFile,
2492 startLine=next(commonFile.IterateLines(), None),
2493 endLine=commonFile._lines[lastLineNumber] if lastLineNumber > 0 else None,
2494 parent=summary
2495 )
2497 for (path, name), (count, firstLineNumber, lastLineNumber) in functions.items():
2498 commonFile = commonFiles[path]
2499 lines = [line for line in commonFile._lines[firstLineNumber:lastLineNumber + 1] if line is not None]
2500 cc_Function(
2501 name,
2502 file=commonFile,
2503 startLine=lines[0] if len(lines) > 0 else None,
2504 endLine=lines[-1] if len(lines) > 0 else None,
2505 status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered,
2506 coverageCount=count,
2507 parent=sourceFiles[path]
2508 )