Coverage for pyEDAA/Reports/CodeCoverage/LLVM.py: 83%
1036 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31#
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 jsonschema import Draft202012Validator
89from pyTooling.Common import getFullyQualifiedName, readResourceFile
90from pyTooling.Decorators import export, readonly
91from pyTooling.Exceptions import ToolingException
92from pyTooling.MetaClasses import ExtendedType
93from pyTooling.Stopwatch import Stopwatch
94from pyTooling.Versioning import SemanticVersion
96from pyEDAA.Reports import Resources
97from pyEDAA.Reports.CodeCoverage import AlreadyInCoverageReportError, Branch as cc_Branch, CodeCoverageError
98from pyEDAA.Reports.CodeCoverage import CoverageSummary, DuplicateFileError
99from pyEDAA.Reports.CodeCoverage import Document as cc_Document, File as cc_File, Function as cc_Function
100from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile
103__all__ = ["SCHEMA"]
105SCHEMA = "LLVM-Coverage-JSON.schema.json" #: The JSON Schema a report is validated against.
107# A class with a property named like a class - ``Path``, ``Summary`` - can't name that class in the annotation of a
108# field: the class body's namespace, where annotations are evaluated, binds the name to the property.
109_Path = Path
111ParentType = TypeVar("ParentType")
114@export
115class RegionKind(Enum):
116 """The kind of a source region, as LLVM's coverage mapping numbers it."""
118 Code = 0 #: Code associated with a counter.
119 Expansion = 1 #: The source range of a macro expansion, which maps to a file of its own.
120 Skipped = 2 #: Code skipped by the preprocessor - e.g. by ``#if 0`` -, empty lines and comments.
121 Gap = 3 #: Code between two regions, whose count is a line's count only, if no other region starts on it.
122 Branch = 4 #: A condition with the counts of its true and false outcome.
123 MCDCDecision = 5 #: A decision of conditions, measured for MC/DC.
124 MCDCBranch = 6 #: A condition of a decision measured for MC/DC, with the counts of its true and false outcome.
127@export
128class Element(Generic[ParentType], metaclass=ExtendedType, slots=True):
129 """
130 Base-class of every element of the format's model: a reference to the element containing it, and to the report's
131 root.
133 A container connects the elements given to its constructor; an element created with a ``parent`` is added to the
134 parent's collection. When a file or function joins a report, the report becomes the root of every element below it.
135 """
137 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; assigned below the classes.
139 _parent: Nullable[ParentType] #: The element containing this one, or ``None``.
140 _root: Nullable[Report] #: The report's root, or ``None`` while the element isn't part of a report.
142 def __init__(self, *, parent: Nullable[ParentType] = None) -> None:
143 """
144 Initialize an element with its parent and the parent's root.
146 The parent doesn't list the element yet: the class adds it to the parent's collection, after setting its fields.
148 :param parent: Optional, the element containing this one. Default: ``None``.
149 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
150 """
151 if parent is None:
152 self._parent = None
153 self._root = None
154 elif not isinstance(parent, self._PARENT_TYPE):
155 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE)
156 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.")
157 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
158 raise ex
159 else:
160 self._parent = parent
161 self._root = parent._root
163 @readonly
164 def Parent(self) -> Nullable[ParentType]:
165 """
166 Read-only property to access the element containing this one (:attr:`_parent`).
168 :returns: The parent, or ``None`` if the element belongs to none.
169 """
170 return self._parent
172 @readonly
173 def Root(self) -> Nullable[Report]:
174 """
175 Read-only property to access the report's root (:attr:`_root`).
177 :returns: The report, or ``None`` while the element isn't part of a report.
178 """
179 return self._root
181 def _SetRoot(self, root: Nullable[Report]) -> None:
182 """
183 Set the report's root of the element.
185 A class containing elements overrides it, to set the root of those too.
187 :param root: The report's root, or ``None`` while the element isn't part of a report.
188 """
189 self._root = root
192@export
193class Base(Element[ParentType]):
194 """
195 Base-class of the source regions: a source range, the file it is in, the file it expands to, and its kind.
196 """
198 _lineStart: int #: Line the region starts at.
199 _columnStart: int #: Column the region starts at.
200 _lineEnd: int #: Line the region ends at.
201 _columnEnd: int #: Column the region ends at.
202 _fileID: Nullable[int] #: Index of the region's file in the file paths of its function or expansion.
203 _expandedFileID: int #: Index of the file an expansion region expands to.
204 _kind: RegionKind #: The kind of the region.
206 def __init__(
207 self,
208 lineStart: int,
209 columnStart: int,
210 lineEnd: int,
211 columnEnd: int,
212 fileID: Nullable[int],
213 expandedFileID: int,
214 kind: RegionKind,
215 *,
216 parent: Nullable[ParentType] = None
217 ) -> None:
218 """
219 Initialize the source range, the region's files and its kind.
221 :param lineStart: Line the region starts at.
222 :param columnStart: Column the region starts at.
223 :param lineEnd: Line the region ends at.
224 :param columnEnd: Column the region ends at.
225 :param fileID: Index of the region's file in the file paths of its function or expansion; ``None``, if not
226 stated.
227 :param expandedFileID: Index of the file an expansion region expands to.
228 :param kind: The kind of the region.
229 :param parent: Optional, the element containing the region. Default: ``None``.
230 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`.
231 :raises ValueError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` is ``None``.
232 :raises TypeError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` isn't of type
233 :class:`int`.
234 :raises ValueError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` is less than 1.
235 :raises TypeError: If parameter ``fileID`` isn't of type :class:`int`.
236 :raises ValueError: If parameter ``fileID`` is negative.
237 :raises ValueError: If parameter ``expandedFileID`` is ``None``.
238 :raises TypeError: If parameter ``expandedFileID`` isn't of type :class:`int`.
239 :raises ValueError: If parameter ``expandedFileID`` is negative.
240 :raises ValueError: If parameter ``kind`` is ``None``.
241 :raises TypeError: If parameter ``kind`` isn't of type :class:`RegionKind`.
242 :raises ValueError: If parameter ``fileID``, or ``expandedFileID`` of an expansion region, is no index into the
243 file paths of parameter ``parent``.
244 """
245 super().__init__(parent=parent)
247 for name, position in (
248 ("lineStart", lineStart), ("columnStart", columnStart), ("lineEnd", lineEnd), ("columnEnd", columnEnd)
249 ):
250 if position is None: 250 ↛ 251line 250 didn't jump to line 251 because the condition on line 250 was never true
251 raise ValueError(f"Parameter '{name}' is None.")
252 elif not isinstance(position, int): 252 ↛ 253line 252 didn't jump to line 253 because the condition on line 252 was never true
253 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
254 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.")
255 raise ex
256 elif position < 1: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true
257 ex = ValueError(f"Parameter '{name}' is less than 1.")
258 ex.add_note(f"Got value '{position}'.")
259 raise ex
261 if fileID is not None:
262 if not isinstance(fileID, int): 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true
263 ex = TypeError("Parameter 'fileID' is not of type 'int'.")
264 ex.add_note(f"Got type '{getFullyQualifiedName(fileID)}'.")
265 raise ex
266 elif fileID < 0: 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true
267 ex = ValueError("Parameter 'fileID' is negative.")
268 ex.add_note(f"Got value '{fileID}'.")
269 raise ex
271 if expandedFileID is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 raise ValueError("Parameter 'expandedFileID' is None.")
273 elif not isinstance(expandedFileID, int): 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 ex = TypeError("Parameter 'expandedFileID' is not of type 'int'.")
275 ex.add_note(f"Got type '{getFullyQualifiedName(expandedFileID)}'.")
276 raise ex
277 elif expandedFileID < 0: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 ex = ValueError("Parameter 'expandedFileID' is negative.")
279 ex.add_note(f"Got value '{expandedFileID}'.")
280 raise ex
282 if kind is None: 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true
283 raise ValueError("Parameter 'kind' is None.")
284 elif not isinstance(kind, RegionKind):
285 ex = TypeError("Parameter 'kind' is not of type 'RegionKind'.")
286 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.")
287 raise ex
289 if isinstance(parent, (Function, Expansion)):
290 for name, value in (
291 ("fileID", fileID), ("expandedFileID", expandedFileID if kind is RegionKind.Expansion else None)
292 ):
293 if value is not None and value >= len(parent._filePaths):
294 ex = ValueError(f"Parameter '{name}' is no index into the file paths of parameter 'parent'.")
295 ex.add_note(f"Got value '{value}' for {len(parent._filePaths)} file paths.")
296 raise ex
298 self._lineStart = lineStart
299 self._columnStart = columnStart
300 self._lineEnd = lineEnd
301 self._columnEnd = columnEnd
302 self._fileID = fileID
303 self._expandedFileID = expandedFileID
304 self._kind = kind
306 @readonly
307 def LineStart(self) -> int:
308 """
309 Read-only property to access the line the region starts at (:attr:`_lineStart`).
311 :returns: The line number, counted from 1.
312 """
313 return self._lineStart
315 @readonly
316 def ColumnStart(self) -> int:
317 """
318 Read-only property to access the column the region starts at (:attr:`_columnStart`).
320 :returns: The column number, counted from 1.
321 """
322 return self._columnStart
324 @readonly
325 def LineEnd(self) -> int:
326 """
327 Read-only property to access the line the region ends at (:attr:`_lineEnd`).
329 :returns: The line number, counted from 1.
330 """
331 return self._lineEnd
333 @readonly
334 def ColumnEnd(self) -> int:
335 """
336 Read-only property to access the column the region ends at (:attr:`_columnEnd`).
338 :returns: The column number, counted from 1.
339 """
340 return self._columnEnd
342 @readonly
343 def FileID(self) -> Nullable[int]:
344 """
345 Read-only property to access the index of the region's file (:attr:`_fileID`).
347 For a region listed by a file, the index refers to the file paths of a function the report doesn't name;
348 :attr:`FilePath` is the file's path then.
350 :returns: The index into the file paths of the region's function or expansion; ``None`` for an MC/DC record before
351 format version 3.0.1.
352 """
353 return self._fileID
355 @readonly
356 def ExpandedFileID(self) -> int:
357 """
358 Read-only property to access the index of the file an expansion region expands to (:attr:`_expandedFileID`).
360 :returns: The index into the file paths of the region's function or expansion; ``0`` for other kinds of regions.
361 """
362 return self._expandedFileID
364 @readonly
365 def Kind(self) -> RegionKind:
366 """
367 Read-only property to access the kind of the region (:attr:`_kind`).
369 :returns: The kind.
370 """
371 return self._kind
373 @readonly
374 def FilePath(self) -> Nullable[Path]:
375 """
376 Read-only property to return the path of the region's file.
378 A region of a function or expansion names its file by :attr:`FileID`, an index into the parent's file paths. A
379 region listed by a file lies in that file.
381 :returns: The path, as the compiler named it; ``None`` if the region belongs to no element, or for an MC/DC record
382 of a function before format version 3.0.1, which states no file.
383 """
384 if self._parent is None:
385 return None
386 elif isinstance(self._parent, File):
387 return self._parent._path
388 elif self._fileID is None:
389 return None
391 return self._parent._filePaths[self._fileID]
393 @readonly
394 def File(self) -> Nullable[File]:
395 """
396 Read-only property to return the region's file, as the report lists it.
398 :returns: The file of :attr:`FilePath` in the report's files; ``None`` if the region isn't part of a report, or the
399 report doesn't list the file, e.g. one excluded by ``-ignore-filename-regex``.
400 """
401 if self._root is None or (path := self.FilePath) is None:
402 return None
404 return self._root._files.get(path, None)
406 @readonly
407 def ExpandedFilePath(self) -> Nullable[Path]:
408 """
409 Read-only property to return the path of the file an expansion region expands to (:attr:`ExpandedFileID`).
411 :returns: The path, as the compiler named it; ``None`` for another kind of region, or a region belonging to no
412 function or expansion.
413 """
414 if self._kind is not RegionKind.Expansion or not isinstance(self._parent, (Function, Expansion)):
415 return None
417 return self._parent._filePaths[self._expandedFileID]
419 @readonly
420 def ExpandedFile(self) -> Nullable[File]:
421 """
422 Read-only property to return the file an expansion region expands to, as the report lists it.
424 :returns: The file of :attr:`ExpandedFilePath` in the report's files; ``None`` if there is none, the region isn't
425 part of a report, or the report doesn't list the file.
426 """
427 if self._root is None or (path := self.ExpandedFilePath) is None:
428 return None
430 return self._root._files.get(path, None)
433@export
434class Region(Base["Function | Expansion"]):
435 """
436 A region of a function or an expansion: a source range with a count.
438 A region is ``[lineStart, columnStart, lineEnd, columnEnd, count, fileID, expandedFileID, kind]``.
439 """
441 _count: int #: How often the region's code ran.
443 def __init__(
444 self,
445 lineStart: int,
446 columnStart: int,
447 lineEnd: int,
448 columnEnd: int,
449 count: int,
450 fileID: int,
451 expandedFileID: int,
452 kind: RegionKind,
453 *,
454 parent: Nullable[Function | Expansion] = None
455 ) -> None:
456 """
457 Initialize a region, and add it to the regions of its function or to the target regions of its expansion.
459 :param lineStart: Line the region starts at.
460 :param columnStart: Column the region starts at.
461 :param lineEnd: Line the region ends at.
462 :param columnEnd: Column the region ends at.
463 :param count: How often the region's code ran.
464 :param fileID: Index of the region's file in the file paths of its function or expansion.
465 :param expandedFileID: Index of the file an expansion region expands to.
466 :param kind: The kind of the region.
467 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``.
468 :raises TypeError: If parameter ``parent`` isn't of type :class:`Function` or :class:`Expansion`.
469 :raises ValueError: If parameter ``fileID`` is ``None``.
470 :raises ValueError: If parameter ``count`` is ``None``.
471 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
472 :raises ValueError: If parameter ``count`` is negative.
473 """
474 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
476 if fileID is None:
477 raise ValueError("Parameter 'fileID' is None.")
479 if count is None: 479 ↛ 480line 479 didn't jump to line 480 because the condition on line 479 was never true
480 raise ValueError("Parameter 'count' is None.")
481 elif not isinstance(count, int): 481 ↛ 482line 481 didn't jump to line 482 because the condition on line 481 was never true
482 ex = TypeError("Parameter 'count' is not of type 'int'.")
483 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
484 raise ex
485 elif count < 0: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 ex = ValueError("Parameter 'count' is negative.")
487 ex.add_note(f"Got value '{count}'.")
488 raise ex
490 self._count = count
492 if parent is not None:
493 (parent._regions if isinstance(parent, Function) else parent._targetRegions).append(self)
495 @classmethod
496 def Parse(cls, region: list[Any], *, parent: Nullable[Function | Expansion] = None) -> Self:
497 """
498 Parse a region from its JSON array.
500 :param region: The JSON array of the region.
501 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``.
502 :returns: The region.
503 """
504 return cls(*region[:7], RegionKind(region[7]), parent=parent)
506 @readonly
507 def Count(self) -> int:
508 """
509 Read-only property to access how often the region's code ran (:attr:`_count`).
511 :returns: The count.
512 """
513 return self._count
516@export
517class BranchRegion(Base["File | Function | Expansion"]):
518 """
519 A branch region: a condition with the counts of its true and false outcome.
521 A branch region is ``[lineStart, columnStart, lineEnd, columnEnd, trueCount, falseCount, fileID, expandedFileID,
522 kind]``.
523 """
525 _trueCount: int #: How often the condition was true.
526 _falseCount: int #: How often the condition was false.
528 def __init__(
529 self,
530 lineStart: int,
531 columnStart: int,
532 lineEnd: int,
533 columnEnd: int,
534 trueCount: int,
535 falseCount: int,
536 fileID: int,
537 expandedFileID: int,
538 kind: RegionKind,
539 *,
540 parent: Nullable[File | Function | Expansion] = None
541 ) -> None:
542 """
543 Initialize a branch region, and add it to the branch regions of its file, function or expansion.
545 :param lineStart: Line the branch region starts at.
546 :param columnStart: Column the branch region starts at.
547 :param lineEnd: Line the branch region ends at.
548 :param columnEnd: Column the branch region ends at.
549 :param trueCount: How often the condition was true.
550 :param falseCount: How often the condition was false.
551 :param fileID: Index of the branch region's file in the file paths of its function or expansion.
552 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for a branch region.
553 :param kind: The kind of the region: a branch, or a condition measured for MC/DC.
554 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``.
555 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`, :class:`Function` or
556 :class:`Expansion`.
557 :raises ValueError: If parameter ``fileID`` is ``None``.
558 :raises ValueError: If parameter ``trueCount`` or ``falseCount`` is ``None``.
559 :raises TypeError: If parameter ``trueCount`` or ``falseCount`` isn't of type :class:`int`.
560 :raises ValueError: If parameter ``trueCount`` or ``falseCount`` is negative.
561 """
562 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
564 if fileID is None: 564 ↛ 565line 564 didn't jump to line 565 because the condition on line 564 was never true
565 raise ValueError("Parameter 'fileID' is None.")
567 for name, value in (("trueCount", trueCount), ("falseCount", falseCount)):
568 if value is None: 568 ↛ 569line 568 didn't jump to line 569 because the condition on line 568 was never true
569 raise ValueError(f"Parameter '{name}' is None.")
570 elif not isinstance(value, int): 570 ↛ 571line 570 didn't jump to line 571 because the condition on line 570 was never true
571 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
572 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
573 raise ex
574 elif value < 0:
575 ex = ValueError(f"Parameter '{name}' is negative.")
576 ex.add_note(f"Got value '{value}'.")
577 raise ex
579 self._trueCount = trueCount
580 self._falseCount = falseCount
582 if parent is not None:
583 parent._branches.append(self)
585 @classmethod
586 def Parse(cls, branch: list[Any], *, parent: Nullable[File | Function | Expansion] = None) -> Self:
587 """
588 Parse a branch region from its JSON array.
590 :param branch: The JSON array of the branch region.
591 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``.
592 :returns: The branch region.
593 """
594 return cls(*branch[:8], RegionKind(branch[8]), parent=parent)
596 @readonly
597 def TrueCount(self) -> int:
598 """
599 Read-only property to access how often the condition was true (:attr:`_trueCount`).
601 :returns: The count.
602 """
603 return self._trueCount
605 @readonly
606 def FalseCount(self) -> int:
607 """
608 Read-only property to access how often the condition was false (:attr:`_falseCount`).
610 :returns: The count.
611 """
612 return self._falseCount
615@export
616class TestVector(Element["MCDCRecord"]):
617 """
618 A test vector of an MC/DC decision - in format version 3.1.0 -: the values of its conditions, and the outcome.
619 """
621 _conditions: list[Nullable[bool]] #: The values of the conditions; ``None`` for a condition not evaluated.
622 _executed: bool #: Whether the test vector was executed.
623 _result: Nullable[bool] #: The outcome of the decision.
625 def __init__(
626 self,
627 conditions: Iterable[Nullable[bool]],
628 executed: bool,
629 result: Nullable[bool],
630 *,
631 parent: Nullable[MCDCRecord] = None
632 ) -> None:
633 """
634 Initialize a test vector, and add it to the test vectors of its MC/DC record.
636 :param conditions: The values of the conditions; ``None`` for a condition not evaluated.
637 :param executed: Whether the test vector was executed.
638 :param result: The outcome of the decision.
639 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``.
640 :raises TypeError: If parameter ``parent`` isn't of type :class:`MCDCRecord`.
641 :raises ValueError: If parameter ``conditions`` is ``None``.
642 :raises TypeError: If parameter ``conditions`` isn't iterable.
643 :raises TypeError: If parameter ``conditions`` contains an element not of type :class:`bool`, other than ``None``.
644 :raises ValueError: If parameter ``executed`` is ``None``.
645 :raises TypeError: If parameter ``executed`` isn't of type :class:`bool`.
646 :raises TypeError: If parameter ``result`` isn't of type :class:`bool`.
647 """
648 super().__init__(parent=parent)
650 if conditions is None: 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true
651 raise ValueError("Parameter 'conditions' is None.")
652 elif not isinstance(conditions, Iterable): 652 ↛ 653line 652 didn't jump to line 653 because the condition on line 652 was never true
653 ex = TypeError("Parameter 'conditions' is not iterable.")
654 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.")
655 raise ex
657 if executed is None: 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true
658 raise ValueError("Parameter 'executed' is None.")
659 elif not isinstance(executed, bool): 659 ↛ 660line 659 didn't jump to line 660 because the condition on line 659 was never true
660 ex = TypeError("Parameter 'executed' is not of type 'bool'.")
661 ex.add_note(f"Got type '{getFullyQualifiedName(executed)}'.")
662 raise ex
664 if result is not None and not isinstance(result, bool):
665 ex = TypeError("Parameter 'result' is not of type 'bool'.")
666 ex.add_note(f"Got type '{getFullyQualifiedName(result)}'.")
667 raise ex
669 self._conditions = []
670 self._executed = executed
671 self._result = result
673 for condition in conditions:
674 if condition is not None and not isinstance(condition, bool): 674 ↛ 675line 674 didn't jump to line 675 because the condition on line 674 was never true
675 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.")
676 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.")
677 raise ex
679 self._conditions.append(condition)
681 if parent is not None:
682 parent._testVectors.append(self)
684 @classmethod
685 def Parse(cls, testVector: dict[str, Any], *, parent: Nullable[MCDCRecord] = None) -> Self:
686 """
687 Parse a test vector from its JSON object.
689 :param testVector: The JSON object of the test vector.
690 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``.
691 :returns: The test vector.
692 """
693 return cls(testVector["conditions"], testVector["executed"], testVector["result"], parent=parent)
695 @readonly
696 def Conditions(self) -> list[Nullable[bool]]:
697 """
698 Read-only property to access the values of the conditions (:attr:`_conditions`).
700 :returns: The values, by condition; ``None`` for a condition not evaluated.
701 """
702 return self._conditions
704 @readonly
705 def Executed(self) -> bool:
706 """
707 Read-only property to access whether the test vector was executed (:attr:`_executed`).
709 :returns: ``True`` for an executed test vector; ``False`` for a missing one, listed by
710 ``-show-mcdc-non-executed-vectors``.
711 """
712 return self._executed
714 @readonly
715 def Result(self) -> Nullable[bool]:
716 """
717 Read-only property to access the outcome of the decision (:attr:`_result`).
719 :returns: The outcome.
720 """
721 return self._result
724@export
725class MCDCRecord(Base["File | Function"]):
726 """
727 An MC/DC record: a decision of conditions, and for each condition, whether a pair of test vectors showed its
728 independent effect on the outcome.
730 The record's array grew with the format version: ``[lineStart, columnStart, lineEnd, columnEnd, expandedFileID, kind,
731 conditions]`` in 2.0.1; version 3.0.0 added the numbers of true and false decisions after the source range, version
732 3.0.1 the ``fileID`` before ``expandedFileID``, and version 3.1.0 the test vectors at the end.
733 """
735 _trueDecisions: Nullable[int] #: Number of executed test vectors with the outcome true, if stated.
736 _falseDecisions: Nullable[int] #: Number of executed test vectors with the outcome false, if stated.
737 _conditions: list[bool] #: Whether a pair of test vectors showed the independent effect, by condition.
738 _testVectors: list[TestVector] #: The test vectors; empty before format version 3.1.0.
740 def __init__(
741 self,
742 lineStart: int,
743 columnStart: int,
744 lineEnd: int,
745 columnEnd: int,
746 fileID: Nullable[int],
747 expandedFileID: int,
748 kind: RegionKind,
749 conditions: Iterable[bool],
750 trueDecisions: Nullable[int] = None,
751 falseDecisions: Nullable[int] = None,
752 testVectors: Nullable[Iterable[TestVector]] = None,
753 *,
754 parent: Nullable[File | Function] = None
755 ) -> None:
756 """
757 Initialize an MC/DC record, and add it to the MC/DC records of its file or function.
759 :param lineStart: Line the decision starts at.
760 :param columnStart: Column the decision starts at.
761 :param lineEnd: Line the decision ends at.
762 :param columnEnd: Column the decision ends at.
763 :param fileID: Index of the decision's file in the file paths of its function or
764 expansion; ``None``, if not stated.
765 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for an MC/DC
766 record.
767 :param kind: The kind of the region: an MC/DC decision.
768 :param conditions: Whether a pair of test vectors showed the independent effect, by
769 condition.
770 :param trueDecisions: Optional, number of executed test vectors with the outcome true.
771 Default: ``None``.
772 :param falseDecisions: Optional, number of executed test vectors with the outcome false.
773 Default: ``None``.
774 :param testVectors: Optional, the test vectors. Default: none.
775 :param parent: Optional, the file or function the MC/DC record belongs to. Default:
776 ``None``.
777 :raises TypeError: If parameter ``parent`` isn't of type :class:`File` or
778 :class:`Function`.
779 :raises TypeError: If parameter ``trueDecisions`` or ``falseDecisions`` isn't of type
780 :class:`int`.
781 :raises ValueError: If parameter ``trueDecisions`` or ``falseDecisions`` is negative.
782 :raises ValueError: If parameter ``conditions`` is ``None``.
783 :raises TypeError: If parameter ``conditions`` isn't iterable.
784 :raises TypeError: If parameter ``conditions`` contains an element not of type
785 :class:`bool`.
786 :raises TypeError: If parameter ``testVectors`` isn't iterable.
787 :raises TypeError: If parameter ``testVectors`` contains an element not of type
788 :class:`TestVector`.
789 :raises AlreadyInCoverageReportError: If parameter ``testVectors`` contains a test vector of another MC/DC
790 record.
791 """
792 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent)
794 for name, value in (("trueDecisions", trueDecisions), ("falseDecisions", falseDecisions)):
795 if value is None:
796 continue
797 elif not isinstance(value, int): 797 ↛ 798line 797 didn't jump to line 798 because the condition on line 797 was never true
798 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
799 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
800 raise ex
801 elif value < 0: 801 ↛ 802line 801 didn't jump to line 802 because the condition on line 801 was never true
802 ex = ValueError(f"Parameter '{name}' is negative.")
803 ex.add_note(f"Got value '{value}'.")
804 raise ex
806 if conditions is None:
807 raise ValueError("Parameter 'conditions' is None.")
808 elif not isinstance(conditions, Iterable): 808 ↛ 809line 808 didn't jump to line 809 because the condition on line 808 was never true
809 ex = TypeError("Parameter 'conditions' is not iterable.")
810 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.")
811 raise ex
813 self._trueDecisions = trueDecisions
814 self._falseDecisions = falseDecisions
815 self._conditions = []
816 self._testVectors = []
818 for condition in conditions:
819 if not isinstance(condition, bool):
820 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.")
821 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.")
822 raise ex
824 self._conditions.append(condition)
826 if testVectors is not None:
827 if not isinstance(testVectors, Iterable): 827 ↛ 828line 827 didn't jump to line 828 because the condition on line 827 was never true
828 ex = TypeError("Parameter 'testVectors' is not iterable.")
829 ex.add_note(f"Got type '{getFullyQualifiedName(testVectors)}'.")
830 raise ex
832 for testVector in testVectors:
833 if not isinstance(testVector, TestVector): 833 ↛ 834line 833 didn't jump to line 834 because the condition on line 833 was never true
834 ex = TypeError("Parameter 'testVectors' contains an element not of type 'TestVector'.")
835 ex.add_note(f"Got type '{getFullyQualifiedName(testVector)}'.")
836 raise ex
837 elif testVector._parent is not None: 837 ↛ 838line 837 didn't jump to line 838 because the condition on line 837 was never true
838 raise AlreadyInCoverageReportError("Parameter 'testVectors' contains a test vector of another MC/DC record.")
840 testVector._parent = self
841 testVector._root = self._root
842 self._testVectors.append(testVector)
844 if parent is not None:
845 parent._mcdcRecords.append(self)
847 @classmethod
848 def Parse(cls, record: list[Any], *, parent: Nullable[File | Function] = None) -> Self:
849 """
850 Parse an MC/DC record from its JSON array, in the shape of its format version.
852 :param record: The JSON array of the MC/DC record.
853 :param parent: Optional, the file or function the MC/DC record belongs to. Default: ``None``.
854 :returns: The MC/DC record.
855 """
856 if len(record) == 7:
857 return cls(*record[:4], None, record[4], RegionKind(record[5]), record[6], parent=parent)
858 elif len(record) == 9:
859 return cls(*record[:4], None, record[6], RegionKind(record[7]), record[8], record[4], record[5], parent=parent)
861 return cls(
862 *record[:4], record[6], record[7], RegionKind(record[8]), record[9], record[4], record[5],
863 [TestVector.Parse(testVector) for testVector in record[10]] if len(record) > 10 else None, parent=parent
864 )
866 @readonly
867 def TrueDecisions(self) -> Nullable[int]:
868 """
869 Read-only property to access the number of executed test vectors with the outcome true (:attr:`_trueDecisions`).
871 :returns: The number; ``None`` before format version 3.0.0.
872 """
873 return self._trueDecisions
875 @readonly
876 def FalseDecisions(self) -> Nullable[int]:
877 """
878 Read-only property to access the number of executed test vectors with the outcome false (:attr:`_falseDecisions`).
880 :returns: The number; ``None`` before format version 3.0.0.
881 """
882 return self._falseDecisions
884 @readonly
885 def Conditions(self) -> list[bool]:
886 """
887 Read-only property to access, by condition, whether a pair of test vectors showed its independent effect
888 (:attr:`_conditions`).
890 :returns: ``True`` for each condition shown to affect the outcome on its own.
891 """
892 return self._conditions
894 @readonly
895 def TestVectors(self) -> list[TestVector]:
896 """
897 Read-only property to access the test vectors (:attr:`_testVectors`).
899 :returns: The test vectors; empty before format version 3.1.0.
900 """
901 return self._testVectors
903 def _SetRoot(self, root: Nullable[Report]) -> None:
904 """
905 Set the report's root of the MC/DC record, and of its test vectors.
907 :param root: The report's root, or ``None`` while the element isn't part of a report.
908 """
909 self._root = root
910 for testVector in self._testVectors:
911 testVector._SetRoot(root)
914@export
915class Counters(Element["Summary"]):
916 """
917 The counters of one kind of a summary: how many there are, and how many are covered.
919 A summary counts lines, functions, regions, branches and MC/DC conditions. The summary given the counters becomes
920 their parent.
921 """
923 _count: int #: Number of lines, functions, regions, branches or MC/DC conditions.
924 _covered: int #: Number of the covered ones.
925 _notCovered: Nullable[int] #: Number of the uncovered ones, if stated.
926 _percent: float #: The coverage in percent.
928 def __init__(self, count: int, covered: int, percent: float, notCovered: Nullable[int] = None) -> None:
929 """
930 Initialize counters.
932 :param count: Number of lines, functions, regions, branches or MC/DC conditions.
933 :param covered: Number of the covered ones.
934 :param percent: The coverage in percent.
935 :param notCovered: Optional, number of the uncovered ones. Default: ``None``.
936 :raises ValueError: If parameter ``count`` or ``covered`` is ``None``.
937 :raises TypeError: If parameter ``count`` or ``covered`` isn't of type :class:`int`.
938 :raises ValueError: If parameter ``count`` or ``covered`` is negative.
939 :raises TypeError: If parameter ``notCovered`` isn't of type :class:`int`.
940 :raises ValueError: If parameter ``notCovered`` is negative.
941 :raises ValueError: If parameter ``percent`` is ``None``.
942 :raises TypeError: If parameter ``percent`` isn't of type :class:`float` or :class:`int`.
943 :raises ValueError: If parameter ``percent`` is out of range 0..100.
944 """
945 super().__init__()
947 for name, value in (("count", count), ("covered", covered)):
948 if value is None: 948 ↛ 949line 948 didn't jump to line 949 because the condition on line 948 was never true
949 raise ValueError(f"Parameter '{name}' is None.")
950 elif not isinstance(value, int): 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
952 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
953 raise ex
954 elif value < 0: 954 ↛ 955line 954 didn't jump to line 955 because the condition on line 954 was never true
955 ex = ValueError(f"Parameter '{name}' is negative.")
956 ex.add_note(f"Got value '{value}'.")
957 raise ex
959 if notCovered is not None:
960 if not isinstance(notCovered, int): 960 ↛ 961line 960 didn't jump to line 961 because the condition on line 960 was never true
961 ex = TypeError("Parameter 'notCovered' is not of type 'int'.")
962 ex.add_note(f"Got type '{getFullyQualifiedName(notCovered)}'.")
963 raise ex
964 elif notCovered < 0: 964 ↛ 965line 964 didn't jump to line 965 because the condition on line 964 was never true
965 ex = ValueError("Parameter 'notCovered' is negative.")
966 ex.add_note(f"Got value '{notCovered}'.")
967 raise ex
969 if percent is None: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 raise ValueError("Parameter 'percent' is None.")
971 elif not isinstance(percent, (float, int)): 971 ↛ 972line 971 didn't jump to line 972 because the condition on line 971 was never true
972 ex = TypeError("Parameter 'percent' is not of type 'float'.")
973 ex.add_note(f"Got type '{getFullyQualifiedName(percent)}'.")
974 raise ex
975 elif not 0 <= percent <= 100:
976 ex = ValueError("Parameter 'percent' is out of range 0..100.")
977 ex.add_note(f"Got value '{percent}'.")
978 raise ex
980 self._count = count
981 self._covered = covered
982 self._notCovered = notCovered
983 self._percent = percent
985 @classmethod
986 def Parse(cls, counters: dict[str, Any]) -> Self:
987 """
988 Parse counters from their JSON object.
990 :param counters: The JSON object of the counters.
991 :returns: The counters.
992 """
993 return cls(counters["count"], counters["covered"], counters["percent"], counters.get("notcovered"))
995 @readonly
996 def Count(self) -> int:
997 """
998 Read-only property to access the number of lines, functions, regions, branches or MC/DC conditions (:attr:`_count`).
1000 :returns: The number.
1001 """
1002 return self._count
1004 @readonly
1005 def Covered(self) -> int:
1006 """
1007 Read-only property to access the number of covered ones (:attr:`_covered`).
1009 :returns: The number.
1010 """
1011 return self._covered
1013 @readonly
1014 def NotCovered(self) -> Nullable[int]:
1015 """
1016 Read-only property to access the number of uncovered ones (:attr:`_notCovered`).
1018 :returns: The number, or ``None`` for lines, functions and instantiations, which don't state it.
1019 """
1020 return self._notCovered
1022 @readonly
1023 def Percent(self) -> float:
1024 """
1025 Read-only property to access the coverage (:attr:`_percent`).
1027 :returns: The coverage in percent.
1028 """
1029 return self._percent
1032@export
1033class Summary(Element["File | Report"]):
1034 """
1035 A ``summary``: the counters llvm-cov computed for a file or the whole report.
1037 llvm-cov counts a file's lines, regions and branches per function, and merges a function's instantiations by taking
1038 the best one. The file or report given the summary becomes its parent.
1039 """
1041 _lines: Counters #: The counters of lines.
1042 _functions: Counters #: The counters of functions, a function's instantiations counted once.
1043 _instantiations: Counters #: The counters of function instantiations.
1044 _regions: Counters #: The counters of code regions.
1045 _branches: Nullable[Counters] #: The counters of branch outcomes, if stated.
1046 _mcdc: Nullable[Counters] #: The counters of MC/DC conditions, if stated.
1048 def __init__(
1049 self,
1050 lines: Counters,
1051 functions: Counters,
1052 instantiations: Counters,
1053 regions: Counters,
1054 branches: Nullable[Counters] = None,
1055 mcdc: Nullable[Counters] = None
1056 ) -> None:
1057 """
1058 Initialize a summary.
1060 :param lines: The counters of lines.
1061 :param functions: The counters of functions, a function's instantiations counted once.
1062 :param instantiations: The counters of function instantiations.
1063 :param regions: The counters of code regions.
1064 :param branches: Optional, the counters of branch outcomes. Default: ``None``.
1065 :param mcdc: Optional, the counters of MC/DC conditions. Default: ``None``.
1066 :raises ValueError: If parameter ``lines``, ``functions``, ``instantiations`` or
1067 ``regions`` is ``None``.
1068 :raises TypeError: If parameter ``lines``, ``functions``, ``instantiations``,
1069 ``regions``, ``branches`` or ``mcdc`` isn't of type :class:`Counters`.
1070 :raises AlreadyInCoverageReportError: If parameter ``lines``, ``functions``, ``instantiations``,
1071 ``regions``, ``branches`` or ``mcdc`` are the counters of another
1072 summary.
1073 """
1074 super().__init__()
1076 for name, counters in (
1077 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions)
1078 ):
1079 if counters is None:
1080 raise ValueError(f"Parameter '{name}' is None.")
1081 elif not isinstance(counters, Counters): 1081 ↛ 1082line 1081 didn't jump to line 1082 because the condition on line 1081 was never true
1082 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.")
1083 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.")
1084 raise ex
1086 for name, counters in (("branches", branches), ("mcdc", mcdc)):
1087 if counters is not None and not isinstance(counters, Counters):
1088 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.")
1089 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.")
1090 raise ex
1092 for name, counters in (
1093 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions),
1094 ("branches", branches), ("mcdc", mcdc)
1095 ):
1096 if counters is None:
1097 continue
1098 elif counters._parent is not None: 1098 ↛ 1099line 1098 didn't jump to line 1099 because the condition on line 1098 was never true
1099 raise AlreadyInCoverageReportError(f"Parameter '{name}' are the counters of another summary.")
1101 counters._parent = self
1103 self._lines = lines
1104 self._functions = functions
1105 self._instantiations = instantiations
1106 self._regions = regions
1107 self._branches = branches
1108 self._mcdc = mcdc
1110 @classmethod
1111 def Parse(cls, summary: dict[str, Any]) -> Self:
1112 """
1113 Parse a summary from its JSON object.
1115 :param summary: The JSON object ``summary`` or ``totals``.
1116 :returns: The summary.
1117 """
1118 return cls(
1119 Counters.Parse(summary["lines"]),
1120 Counters.Parse(summary["functions"]),
1121 Counters.Parse(summary["instantiations"]),
1122 Counters.Parse(summary["regions"]),
1123 Counters.Parse(summary["branches"]) if "branches" in summary else None,
1124 Counters.Parse(summary["mcdc"]) if "mcdc" in summary else None
1125 )
1127 @readonly
1128 def Lines(self) -> Counters:
1129 """
1130 Read-only property to access the counters of lines (:attr:`_lines`).
1132 :returns: The counters.
1133 """
1134 return self._lines
1136 @readonly
1137 def Functions(self) -> Counters:
1138 """
1139 Read-only property to access the counters of functions, a function's instantiations counted once
1140 (:attr:`_functions`).
1142 :returns: The counters.
1143 """
1144 return self._functions
1146 @readonly
1147 def Instantiations(self) -> Counters:
1148 """
1149 Read-only property to access the counters of function instantiations (:attr:`_instantiations`).
1151 :returns: The counters.
1152 """
1153 return self._instantiations
1155 @readonly
1156 def Regions(self) -> Counters:
1157 """
1158 Read-only property to access the counters of code regions (:attr:`_regions`).
1160 :returns: The counters.
1161 """
1162 return self._regions
1164 @readonly
1165 def Branches(self) -> Nullable[Counters]:
1166 """
1167 Read-only property to access the counters of branch outcomes - two per branch region - (:attr:`_branches`).
1169 :returns: The counters, or ``None`` before LLVM 12.
1170 """
1171 return self._branches
1173 @readonly
1174 def MCDC(self) -> Nullable[Counters]:
1175 """
1176 Read-only property to access the counters of MC/DC conditions (:attr:`_mcdc`).
1178 :returns: The counters, or ``None`` before LLVM 18.
1179 """
1180 return self._mcdc
1182 def _SetRoot(self, root: Nullable[Report]) -> None:
1183 """
1184 Set the report's root of the summary, and of its counters.
1186 :param root: The report's root, or ``None`` while the element isn't part of a report.
1187 """
1188 self._root = root
1189 for counters in (self._lines, self._functions, self._instantiations, self._regions, self._branches, self._mcdc):
1190 if counters is not None:
1191 counters._SetRoot(root)
1194_Summary = Summary
1197@export
1198class Segment(Element["File"]):
1199 """
1200 A segment of a file: from a position on, the code has a count - until the next segment.
1202 A segment is ``[line, column, count, hasCount, isRegionEntry, isGapRegion]``. Format version 2.0.0 has no
1203 ``isGapRegion``.
1204 """
1206 _line: int #: Line the segment starts at.
1207 _column: int #: Column the segment starts at.
1208 _count: int #: The count of the code from here on.
1209 _hasCount: bool #: Whether the code from here on has a count; false e.g. after a function's end.
1210 _isRegionEntry: bool #: Whether a region starts here; false, where a region continues after a nested one.
1211 _isGapRegion: bool #: Whether a gap region starts here.
1213 def __init__(
1214 self,
1215 line: int,
1216 column: int,
1217 count: int,
1218 hasCount: bool,
1219 isRegionEntry: bool,
1220 isGapRegion: bool = False,
1221 *,
1222 parent: Nullable[File] = None
1223 ) -> None:
1224 """
1225 Initialize a segment, and add it to the segments of its file.
1227 :param line: Line the segment starts at.
1228 :param column: Column the segment starts at.
1229 :param count: The count of the code from here on.
1230 :param hasCount: Whether the code from here on has a count.
1231 :param isRegionEntry: Whether a region starts here.
1232 :param isGapRegion: Optional, whether a gap region starts here. Default: ``False``.
1233 :param parent: Optional, the file the segment belongs to. Default: ``None``.
1234 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
1235 :raises ValueError: If parameter ``line`` or ``column`` is ``None``.
1236 :raises TypeError: If parameter ``line`` or ``column`` isn't of type :class:`int`.
1237 :raises ValueError: If parameter ``line`` or ``column`` is less than 1.
1238 :raises ValueError: If parameter ``count`` is ``None``.
1239 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
1240 :raises ValueError: If parameter ``count`` is negative.
1241 :raises ValueError: If parameter ``hasCount``, ``isRegionEntry`` or ``isGapRegion`` is ``None``.
1242 :raises TypeError: If parameter ``hasCount``, ``isRegionEntry`` or ``isGapRegion`` isn't of type :class:`bool`.
1243 """
1244 super().__init__(parent=parent)
1246 for name, position in (("line", line), ("column", column)):
1247 if position is None:
1248 raise ValueError(f"Parameter '{name}' is None.")
1249 elif not isinstance(position, int):
1250 ex = TypeError(f"Parameter '{name}' is not of type 'int'.")
1251 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.")
1252 raise ex
1253 elif position < 1:
1254 ex = ValueError(f"Parameter '{name}' is less than 1.")
1255 ex.add_note(f"Got value '{position}'.")
1256 raise ex
1258 if count is None: 1258 ↛ 1259line 1258 didn't jump to line 1259 because the condition on line 1258 was never true
1259 raise ValueError("Parameter 'count' is None.")
1260 elif not isinstance(count, int): 1260 ↛ 1261line 1260 didn't jump to line 1261 because the condition on line 1260 was never true
1261 ex = TypeError("Parameter 'count' is not of type 'int'.")
1262 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
1263 raise ex
1264 elif count < 0: 1264 ↛ 1265line 1264 didn't jump to line 1265 because the condition on line 1264 was never true
1265 ex = ValueError("Parameter 'count' is negative.")
1266 ex.add_note(f"Got value '{count}'.")
1267 raise ex
1269 for name, flag in (("hasCount", hasCount), ("isRegionEntry", isRegionEntry), ("isGapRegion", isGapRegion)):
1270 if flag is None: 1270 ↛ 1271line 1270 didn't jump to line 1271 because the condition on line 1270 was never true
1271 raise ValueError(f"Parameter '{name}' is None.")
1272 elif not isinstance(flag, bool):
1273 ex = TypeError(f"Parameter '{name}' is not of type 'bool'.")
1274 ex.add_note(f"Got type '{getFullyQualifiedName(flag)}'.")
1275 raise ex
1277 self._line = line
1278 self._column = column
1279 self._count = count
1280 self._hasCount = hasCount
1281 self._isRegionEntry = isRegionEntry
1282 self._isGapRegion = isGapRegion
1284 if parent is not None:
1285 parent._segments.append(self)
1287 @classmethod
1288 def Parse(cls, segment: list[Any], *, parent: Nullable[File] = None) -> Self:
1289 """
1290 Parse a segment from its JSON array.
1292 :param segment: The JSON array of the segment.
1293 :param parent: Optional, the file the segment belongs to. Default: ``None``.
1294 :returns: The segment.
1295 """
1296 return cls(*segment[:5], segment[5] if len(segment) > 5 else False, parent=parent)
1298 @readonly
1299 def Line(self) -> int:
1300 """
1301 Read-only property to access the line the segment starts at (:attr:`_line`).
1303 :returns: The line number, counted from 1.
1304 """
1305 return self._line
1307 @readonly
1308 def Column(self) -> int:
1309 """
1310 Read-only property to access the column the segment starts at (:attr:`_column`).
1312 :returns: The column number, counted from 1.
1313 """
1314 return self._column
1316 @readonly
1317 def Count(self) -> int:
1318 """
1319 Read-only property to access the count of the code from the segment's start on (:attr:`_count`).
1321 :returns: How often the code ran; ``0`` without a count.
1322 """
1323 return self._count
1325 @readonly
1326 def HasCount(self) -> bool:
1327 """
1328 Read-only property to access whether the code from the segment's start on has a count (:attr:`_hasCount`).
1330 :returns: ``False`` e.g. after the end of a function, or in a skipped region.
1331 """
1332 return self._hasCount
1334 @readonly
1335 def IsRegionEntry(self) -> bool:
1336 """
1337 Read-only property to access whether a region starts at the segment's start (:attr:`_isRegionEntry`).
1339 :returns: ``False``, where a region continues after a region nested in it.
1340 """
1341 return self._isRegionEntry
1343 @readonly
1344 def IsGapRegion(self) -> bool:
1345 """
1346 Read-only property to access whether a gap region starts at the segment's start (:attr:`_isGapRegion`).
1348 :returns: ``True`` for a gap region; ``False`` in format version 2.0.0.
1349 """
1350 return self._isGapRegion
1353@export
1354class Expansion(Element["File"]):
1355 """
1356 A macro expansion in a file: the region it is expanded at, the regions of the function it is in, and its branches.
1357 """
1359 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index.
1360 _sourceRegion: Region #: The region the macro is expanded at.
1361 _targetRegions: list[Region] #: The regions of the function the expansion is in.
1362 _branches: list[BranchRegion] #: The branch regions in the expansion, and in the expansions nested in it.
1364 def __init__(
1365 self,
1366 filePaths: Iterable[Path],
1367 sourceRegion: Region,
1368 targetRegions: Iterable[Region],
1369 branches: Nullable[Iterable[BranchRegion]] = None,
1370 *,
1371 parent: Nullable[File] = None
1372 ) -> None:
1373 """
1374 Initialize an expansion, and add it to the expansions of its file.
1376 :param filePaths: The paths of the files, which the file IDs of the regions index.
1377 :param sourceRegion: The region the macro is expanded at.
1378 :param targetRegions: The regions of the function the expansion is in.
1379 :param branches: Optional, the branch regions in the expansion, and in the expansions
1380 nested in it. Default: none.
1381 :param parent: Optional, the file the expansion belongs to. Default: ``None``.
1382 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
1383 :raises ValueError: If parameter ``filePaths`` or ``targetRegions`` is ``None``.
1384 :raises TypeError: If parameter ``filePaths``, ``targetRegions`` or ``branches`` isn't
1385 iterable.
1386 :raises TypeError: If parameter ``filePaths`` contains an element not of type
1387 :class:`~pathlib.Path`.
1388 :raises ValueError: If parameter ``sourceRegion`` is ``None``.
1389 :raises TypeError: If parameter ``sourceRegion`` isn't of type :class:`Region`.
1390 :raises AlreadyInCoverageReportError: If parameter ``sourceRegion`` is a region of another element.
1391 :raises TypeError: If parameter ``targetRegions`` contains an element not of type
1392 :class:`Region`.
1393 :raises TypeError: If parameter ``branches`` contains an element not of type
1394 :class:`BranchRegion`.
1395 :raises AlreadyInCoverageReportError: If parameter ``targetRegions`` or ``branches`` contains a region of
1396 another element.
1397 :raises ValueError: If parameter ``sourceRegion`` is, or ``targetRegions`` or ``branches``
1398 contains, a region of a file ID or an expanded file ID out of range of
1399 parameter ``filePaths``.
1400 """
1401 super().__init__(parent=parent)
1403 if filePaths is None: 1403 ↛ 1404line 1403 didn't jump to line 1404 because the condition on line 1403 was never true
1404 raise ValueError("Parameter 'filePaths' is None.")
1405 elif not isinstance(filePaths, Iterable): 1405 ↛ 1406line 1405 didn't jump to line 1406 because the condition on line 1405 was never true
1406 ex = TypeError("Parameter 'filePaths' is not iterable.")
1407 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.")
1408 raise ex
1410 if sourceRegion is None: 1410 ↛ 1411line 1410 didn't jump to line 1411 because the condition on line 1410 was never true
1411 raise ValueError("Parameter 'sourceRegion' is None.")
1412 elif not isinstance(sourceRegion, Region): 1412 ↛ 1413line 1412 didn't jump to line 1413 because the condition on line 1412 was never true
1413 ex = TypeError("Parameter 'sourceRegion' is not of type 'Region'.")
1414 ex.add_note(f"Got type '{getFullyQualifiedName(sourceRegion)}'.")
1415 raise ex
1416 elif sourceRegion._parent is not None: 1416 ↛ 1417line 1416 didn't jump to line 1417 because the condition on line 1416 was never true
1417 raise AlreadyInCoverageReportError("Parameter 'sourceRegion' is a region of another element.")
1419 if targetRegions is None: 1419 ↛ 1420line 1419 didn't jump to line 1420 because the condition on line 1419 was never true
1420 raise ValueError("Parameter 'targetRegions' is None.")
1422 self._filePaths = []
1423 self._sourceRegion = sourceRegion
1424 self._targetRegions = []
1425 self._branches = []
1427 for filePath in filePaths:
1428 if not isinstance(filePath, Path):
1429 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.")
1430 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.")
1431 raise ex
1433 self._filePaths.append(filePath)
1435 for name, elements, elementType, target in (
1436 ("targetRegions", targetRegions, Region, self._targetRegions),
1437 ("branches", branches, BranchRegion, self._branches)
1438 ):
1439 if elements is None:
1440 continue
1441 elif not isinstance(elements, Iterable):
1442 ex = TypeError(f"Parameter '{name}' is not iterable.")
1443 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1444 raise ex
1446 for element in elements:
1447 if not isinstance(element, elementType): 1447 ↛ 1448line 1447 didn't jump to line 1448 because the condition on line 1447 was never true
1448 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.")
1449 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1450 raise ex
1451 elif element._parent is not None: 1451 ↛ 1452line 1451 didn't jump to line 1452 because the condition on line 1451 was never true
1452 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains a region of another element.")
1453 elif element._fileID is not None and element._fileID >= len(self._filePaths):
1454 ex = ValueError(f"Parameter '{name}' contains a region of a file ID out of range.")
1455 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.")
1456 raise ex
1457 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): 1457 ↛ 1458line 1457 didn't jump to line 1458 because the condition on line 1457 was never true
1458 ex = ValueError(f"Parameter '{name}' contains an expansion region of an expanded file ID out of range.")
1459 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.")
1460 raise ex
1462 element._parent = self
1463 element._root = self._root
1464 target.append(element)
1466 if sourceRegion._fileID >= len(self._filePaths): 1466 ↛ 1467line 1466 didn't jump to line 1467 because the condition on line 1466 was never true
1467 ex = ValueError("Parameter 'sourceRegion' is a region of a file ID out of range.")
1468 ex.add_note(f"Got value '{sourceRegion._fileID}' for {len(self._filePaths)} file paths.")
1469 raise ex
1470 elif sourceRegion._kind is RegionKind.Expansion and sourceRegion._expandedFileID >= len(self._filePaths):
1471 ex = ValueError("Parameter 'sourceRegion' is an expansion region of an expanded file ID out of range.")
1472 ex.add_note(f"Got value '{sourceRegion._expandedFileID}' for {len(self._filePaths)} file paths.")
1473 raise ex
1475 sourceRegion._parent = self
1476 sourceRegion._root = self._root
1478 if parent is not None:
1479 parent._expansions.append(self)
1481 @classmethod
1482 def Parse(cls, expansion: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
1483 """
1484 Parse an expansion from its JSON object.
1486 :param expansion: The JSON object of the expansion.
1487 :param parent: Optional, the file the expansion belongs to. Default: ``None``.
1488 :returns: The expansion.
1489 """
1490 return cls(
1491 [Path(filename.replace("\\", "/")) for filename in expansion["filenames"]],
1492 Region.Parse(expansion["source_region"]),
1493 [Region.Parse(region) for region in expansion["target_regions"]],
1494 [BranchRegion.Parse(branch) for branch in expansion.get("branches", [])],
1495 parent=parent
1496 )
1498 @readonly
1499 def FilePaths(self) -> list[Path]:
1500 """
1501 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`).
1503 :returns: The paths, as the compiler named them.
1504 """
1505 return self._filePaths
1507 @readonly
1508 def SourceRegion(self) -> Region:
1509 """
1510 Read-only property to access the region the macro is expanded at (:attr:`_sourceRegion`).
1512 :returns: The region.
1513 """
1514 return self._sourceRegion
1516 @readonly
1517 def TargetRegions(self) -> list[Region]:
1518 """
1519 Read-only property to access the regions of the function the expansion is in (:attr:`_targetRegions`).
1521 :returns: The regions.
1522 """
1523 return self._targetRegions
1525 @readonly
1526 def Branches(self) -> list[BranchRegion]:
1527 """
1528 Read-only property to access the branch regions in the expansion, and in the expansions nested in it
1529 (:attr:`_branches`).
1531 :returns: The branch regions; empty before LLVM 12.
1532 """
1533 return self._branches
1535 def _SetRoot(self, root: Nullable[Report]) -> None:
1536 """
1537 Set the report's root of the expansion, and of its source region, target regions and branch regions.
1539 :param root: The report's root, or ``None`` while the element isn't part of a report.
1540 """
1541 self._root = root
1542 for region in (self._sourceRegion, *self._targetRegions, *self._branches):
1543 region._SetRoot(root)
1546@export
1547class File(Element["Report"]):
1548 """
1549 A file: its segments, branch regions, MC/DC records and expansions, and its summary.
1551 A report written with ``-summary-only`` has only the summary, one written with ``-skip-expansions`` no expansions.
1552 """
1554 _path: _Path #: The file's path, as the compiler named it.
1555 _segments: list[Segment] #: The segments, by position.
1556 _branches: list[BranchRegion] #: The branch regions of the file's functions.
1557 _mcdcRecords: list[MCDCRecord] #: The MC/DC records of the file's functions.
1558 _expansions: list[Expansion] #: The macro expansions in the file.
1559 _summary: _Summary #: The counters llvm-cov computed.
1561 def __init__(
1562 self,
1563 path: Path,
1564 summary: Summary,
1565 segments: Nullable[Iterable[Segment]] = None,
1566 branches: Nullable[Iterable[BranchRegion]] = None,
1567 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None,
1568 expansions: Nullable[Iterable[Expansion]] = None,
1569 *,
1570 parent: Nullable[Report] = None
1571 ) -> None:
1572 """
1573 Initialize a file, and add it to the files of its report.
1575 :param path: The file's path, as the compiler named it.
1576 :param summary: The counters llvm-cov computed.
1577 :param segments: Optional, the segments, by position. Default: none.
1578 :param branches: Optional, the branch regions of the file's functions. Default: none.
1579 :param mcdcRecords: Optional, the MC/DC records of the file's functions. Default: none.
1580 :param expansions: Optional, the macro expansions in the file. Default: none.
1581 :param parent: Optional, the report the file belongs to. Default: ``None``.
1582 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`.
1583 :raises ValueError: If parameter ``path`` is ``None``.
1584 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1585 :raises DuplicateFileError: If parameter ``parent`` has a file of the same path.
1586 :raises ValueError: If parameter ``summary`` is ``None``.
1587 :raises TypeError: If parameter ``summary`` isn't of type :class:`Summary`.
1588 :raises AlreadyInCoverageReportError: If parameter ``summary`` is the summary of another element.
1589 :raises TypeError: If parameter ``segments``, ``branches``, ``mcdcRecords`` or
1590 ``expansions`` isn't iterable.
1591 :raises TypeError: If parameter ``segments`` contains an element not of type
1592 :class:`Segment`.
1593 :raises TypeError: If parameter ``branches`` contains an element not of type
1594 :class:`BranchRegion`.
1595 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type
1596 :class:`MCDCRecord`.
1597 :raises TypeError: If parameter ``expansions`` contains an element not of type
1598 :class:`Expansion`.
1599 :raises AlreadyInCoverageReportError: If parameter ``segments``, ``branches``, ``mcdcRecords`` or
1600 ``expansions`` contains an element of another element.
1601 """
1602 super().__init__(parent=parent)
1604 if path is None: 1604 ↛ 1605line 1604 didn't jump to line 1605 because the condition on line 1604 was never true
1605 raise ValueError("Parameter 'path' is None.")
1606 elif not isinstance(path, Path):
1607 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1608 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1609 raise ex
1610 elif parent is not None and path in parent._files:
1611 ex = DuplicateFileError("Parameter 'parent' has a file of the same path.")
1612 ex.add_note(f"Got value '{path.as_posix()}'.")
1613 raise ex
1615 if summary is None: 1615 ↛ 1616line 1615 didn't jump to line 1616 because the condition on line 1615 was never true
1616 raise ValueError("Parameter 'summary' is None.")
1617 elif not isinstance(summary, Summary): 1617 ↛ 1618line 1617 didn't jump to line 1618 because the condition on line 1617 was never true
1618 ex = TypeError("Parameter 'summary' is not of type 'Summary'.")
1619 ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.")
1620 raise ex
1621 elif summary._parent is not None:
1622 raise AlreadyInCoverageReportError("Parameter 'summary' is the summary of another element.")
1624 self._path = path
1625 self._segments = []
1626 self._branches = []
1627 self._mcdcRecords = []
1628 self._expansions = []
1629 self._summary = summary
1631 for name, elements, elementType, target in (
1632 ("segments", segments, Segment, self._segments), ("branches", branches, BranchRegion, self._branches),
1633 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords),
1634 ("expansions", expansions, Expansion, self._expansions)
1635 ):
1636 if elements is None:
1637 continue
1638 elif not isinstance(elements, Iterable): 1638 ↛ 1639line 1638 didn't jump to line 1639 because the condition on line 1638 was never true
1639 ex = TypeError(f"Parameter '{name}' is not iterable.")
1640 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1641 raise ex
1643 for element in elements:
1644 if not isinstance(element, elementType): 1644 ↛ 1645line 1644 didn't jump to line 1645 because the condition on line 1644 was never true
1645 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.")
1646 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1647 raise ex
1648 elif element._parent is not None: 1648 ↛ 1649line 1648 didn't jump to line 1649 because the condition on line 1648 was never true
1649 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains an element of another element.")
1651 element._parent = self
1652 element._SetRoot(self._root)
1654 target.append(element)
1656 summary._parent = self
1657 summary._SetRoot(self._root)
1659 if parent is not None:
1660 parent._files[path] = self
1662 @classmethod
1663 def Parse(cls, file: dict[str, Any], *, parent: Nullable[Report] = None) -> Self:
1664 """
1665 Parse a file from its JSON object.
1667 :param file: The JSON object of the file.
1668 :param parent: Optional, the report the file belongs to. Default: ``None``.
1669 :returns: The file.
1670 """
1671 return cls(
1672 Path(file["filename"].replace("\\", "/")),
1673 Summary.Parse(file["summary"]),
1674 [Segment.Parse(segment) for segment in file.get("segments", [])],
1675 [BranchRegion.Parse(branch) for branch in file.get("branches", [])],
1676 [MCDCRecord.Parse(record) for record in file.get("mcdc_records", [])],
1677 [Expansion.Parse(expansion) for expansion in file.get("expansions", [])],
1678 parent=parent
1679 )
1681 @readonly
1682 def Path(self) -> Path:
1683 """
1684 Read-only property to access the file's path (:attr:`_path`).
1686 :returns: The path, as the compiler named it - usually absolute.
1687 """
1688 return self._path
1690 @readonly
1691 def Segments(self) -> list[Segment]:
1692 """
1693 Read-only property to access the segments (:attr:`_segments`).
1695 :returns: The segments, by position; empty in a summary-only report.
1696 """
1697 return self._segments
1699 @readonly
1700 def Branches(self) -> list[BranchRegion]:
1701 """
1702 Read-only property to access the branch regions of the file's functions (:attr:`_branches`).
1704 Which branch regions of a macro expansion are listed here, depends on the LLVM version.
1706 :returns: The branch regions.
1707 """
1708 return self._branches
1710 @readonly
1711 def MCDCRecords(self) -> list[MCDCRecord]:
1712 """
1713 Read-only property to access the MC/DC records of the file's functions (:attr:`_mcdcRecords`).
1715 :returns: The MC/DC records.
1716 """
1717 return self._mcdcRecords
1719 @readonly
1720 def Expansions(self) -> list[Expansion]:
1721 """
1722 Read-only property to access the macro expansions in the file (:attr:`_expansions`).
1724 :returns: The expansions.
1725 """
1726 return self._expansions
1728 @readonly
1729 def Summary(self) -> Summary:
1730 """
1731 Read-only property to access the counters llvm-cov computed (:attr:`_summary`).
1733 :returns: The summary.
1734 """
1735 return self._summary
1737 def _SetRoot(self, root: Nullable[Report]) -> None:
1738 """
1739 Set the report's root of the file, and of its segments, branch regions, MC/DC records, expansions and summary.
1741 :param root: The report's root, or ``None`` while the element isn't part of a report.
1742 """
1743 self._root = root
1744 for element in (*self._segments, *self._branches, *self._mcdcRecords, *self._expansions, self._summary):
1745 element._SetRoot(root)
1748@export
1749class Function(Element["Report"]):
1750 """
1751 A function - an instantiation of a template is a function of its own -: how often it was called, its regions, branch
1752 regions and MC/DC records, and the files they are in.
1753 """
1755 _name: str #: The function's name, as the profile names it - e.g. mangled.
1756 _count: int #: How often the function was called.
1757 _regions: list[Region] #: The regions.
1758 _branches: list[BranchRegion] #: The branch regions.
1759 _mcdcRecords: list[MCDCRecord] #: The MC/DC records.
1760 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index.
1762 def __init__(
1763 self,
1764 name: str,
1765 count: int,
1766 regions: Iterable[Region],
1767 filePaths: Iterable[Path],
1768 branches: Nullable[Iterable[BranchRegion]] = None,
1769 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None,
1770 *,
1771 parent: Nullable[Report] = None
1772 ) -> None:
1773 """
1774 Initialize a function, and add it to the functions of its report.
1776 :param name: The function's name, as the profile names it - e.g. mangled.
1777 :param count: How often the function was called.
1778 :param regions: The regions.
1779 :param filePaths: The paths of the files, which the file IDs of the regions index.
1780 :param branches: Optional, the branch regions. Default: none.
1781 :param mcdcRecords: Optional, the MC/DC records. Default: none.
1782 :param parent: Optional, the report the function belongs to. Default: ``None``.
1783 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`.
1784 :raises ValueError: If parameter ``name`` is ``None``.
1785 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
1786 :raises ValueError: If parameter ``name`` is empty.
1787 :raises ValueError: If parameter ``count`` is ``None``.
1788 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
1789 :raises ValueError: If parameter ``count`` is negative.
1790 :raises ValueError: If parameter ``regions`` or ``filePaths`` is ``None``.
1791 :raises TypeError: If parameter ``regions``, ``filePaths``, ``branches`` or
1792 ``mcdcRecords`` isn't iterable.
1793 :raises TypeError: If parameter ``regions`` contains an element not of type
1794 :class:`Region`.
1795 :raises TypeError: If parameter ``filePaths`` contains an element not of type
1796 :class:`~pathlib.Path`.
1797 :raises TypeError: If parameter ``branches`` contains an element not of type
1798 :class:`BranchRegion`.
1799 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type
1800 :class:`MCDCRecord`.
1801 :raises AlreadyInCoverageReportError: If parameter ``regions``, ``branches`` or ``mcdcRecords`` contains a
1802 region of another element.
1803 :raises ValueError: If parameter ``regions``, ``branches`` or ``mcdcRecords`` contains a
1804 region of a file ID or an expanded file ID out of range of parameter
1805 ``filePaths``.
1806 """
1807 super().__init__(parent=parent)
1809 if name is None: 1809 ↛ 1810line 1809 didn't jump to line 1810 because the condition on line 1809 was never true
1810 raise ValueError("Parameter 'name' is None.")
1811 elif not isinstance(name, str): 1811 ↛ 1812line 1811 didn't jump to line 1812 because the condition on line 1811 was never true
1812 ex = TypeError("Parameter 'name' is not of type 'str'.")
1813 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
1814 raise ex
1815 elif name == "":
1816 raise ValueError("Parameter 'name' is empty.")
1818 if count is None: 1818 ↛ 1819line 1818 didn't jump to line 1819 because the condition on line 1818 was never true
1819 raise ValueError("Parameter 'count' is None.")
1820 elif not isinstance(count, int): 1820 ↛ 1821line 1820 didn't jump to line 1821 because the condition on line 1820 was never true
1821 ex = TypeError("Parameter 'count' is not of type 'int'.")
1822 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
1823 raise ex
1824 elif count < 0: 1824 ↛ 1825line 1824 didn't jump to line 1825 because the condition on line 1824 was never true
1825 ex = ValueError("Parameter 'count' is negative.")
1826 ex.add_note(f"Got value '{count}'.")
1827 raise ex
1829 if regions is None: 1829 ↛ 1830line 1829 didn't jump to line 1830 because the condition on line 1829 was never true
1830 raise ValueError("Parameter 'regions' is None.")
1832 if filePaths is None: 1832 ↛ 1833line 1832 didn't jump to line 1833 because the condition on line 1832 was never true
1833 raise ValueError("Parameter 'filePaths' is None.")
1834 elif not isinstance(filePaths, Iterable): 1834 ↛ 1835line 1834 didn't jump to line 1835 because the condition on line 1834 was never true
1835 ex = TypeError("Parameter 'filePaths' is not iterable.")
1836 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.")
1837 raise ex
1839 self._name = name
1840 self._count = count
1841 self._regions = []
1842 self._branches = []
1843 self._mcdcRecords = []
1844 self._filePaths = []
1846 for filePath in filePaths:
1847 if not isinstance(filePath, Path): 1847 ↛ 1848line 1847 didn't jump to line 1848 because the condition on line 1847 was never true
1848 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.")
1849 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.")
1850 raise ex
1852 self._filePaths.append(filePath)
1854 for parameterName, elements, elementType, target in (
1855 ("regions", regions, Region, self._regions), ("branches", branches, BranchRegion, self._branches),
1856 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords)
1857 ):
1858 if elements is None:
1859 continue
1860 elif not isinstance(elements, Iterable): 1860 ↛ 1861line 1860 didn't jump to line 1861 because the condition on line 1860 was never true
1861 ex = TypeError(f"Parameter '{parameterName}' is not iterable.")
1862 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.")
1863 raise ex
1865 for element in elements:
1866 if not isinstance(element, elementType): 1866 ↛ 1867line 1866 didn't jump to line 1867 because the condition on line 1866 was never true
1867 ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{elementType.__name__}'.")
1868 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
1869 raise ex
1870 elif element._parent is not None:
1871 raise AlreadyInCoverageReportError(f"Parameter '{parameterName}' contains a region of another element.")
1872 elif element._fileID is not None and element._fileID >= len(self._filePaths):
1873 ex = ValueError(f"Parameter '{parameterName}' contains a region of a file ID out of range.")
1874 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.")
1875 raise ex
1876 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths):
1877 ex = ValueError(
1878 f"Parameter '{parameterName}' contains an expansion region of an expanded file ID out of range."
1879 )
1880 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.")
1881 raise ex
1883 element._parent = self
1884 element._SetRoot(self._root)
1886 target.append(element)
1888 if parent is not None:
1889 parent._functions.append(self)
1891 @classmethod
1892 def Parse(cls, function: dict[str, Any], *, parent: Nullable[Report] = None) -> Self:
1893 """
1894 Parse a function from its JSON object.
1896 :param function: The JSON object of the function.
1897 :param parent: Optional, the report the function belongs to. Default: ``None``.
1898 :returns: The function.
1899 """
1900 return cls(
1901 function["name"],
1902 function["count"],
1903 [Region.Parse(region) for region in function["regions"]],
1904 [Path(filename.replace("\\", "/")) for filename in function["filenames"]],
1905 [BranchRegion.Parse(branch) for branch in function.get("branches", [])],
1906 [MCDCRecord.Parse(record) for record in function.get("mcdc_records", [])],
1907 parent=parent
1908 )
1910 @readonly
1911 def Name(self) -> str:
1912 """
1913 Read-only property to access the function's name (:attr:`_name`).
1915 :returns: The name, as the profile names it: mangled, and prefixed by the file name of its translation unit, if
1916 local to it - e.g. ``Statistics.c:Square``.
1917 """
1918 return self._name
1920 @readonly
1921 def Count(self) -> int:
1922 """
1923 Read-only property to access how often the function was called (:attr:`_count`).
1925 :returns: The count.
1926 """
1927 return self._count
1929 @readonly
1930 def Regions(self) -> list[Region]:
1931 """
1932 Read-only property to access the regions (:attr:`_regions`).
1934 :returns: The regions.
1935 """
1936 return self._regions
1938 @readonly
1939 def Branches(self) -> list[BranchRegion]:
1940 """
1941 Read-only property to access the branch regions (:attr:`_branches`).
1943 :returns: The branch regions, also those in macro expansions.
1944 """
1945 return self._branches
1947 @readonly
1948 def MCDCRecords(self) -> list[MCDCRecord]:
1949 """
1950 Read-only property to access the MC/DC records (:attr:`_mcdcRecords`).
1952 :returns: The MC/DC records.
1953 """
1954 return self._mcdcRecords
1956 @readonly
1957 def FilePaths(self) -> list[Path]:
1958 """
1959 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`).
1961 :returns: The paths, as the compiler named them.
1962 """
1963 return self._filePaths
1965 @readonly
1966 def MainFileID(self) -> Nullable[int]:
1967 """
1968 Read-only property to return the file ID of the file the function is in: the first file no expansion region
1969 expands to.
1971 :returns: The index into :attr:`FilePaths`, or ``None`` if every file is expanded to.
1972 """
1973 expanded = {region._expandedFileID for region in self._regions if region._kind is RegionKind.Expansion}
1974 return next((fileID for fileID in range(len(self._filePaths)) if fileID not in expanded), None)
1976 @readonly
1977 def MainFilePath(self) -> Nullable[Path]:
1978 """
1979 Read-only property to return the path of the file the function is in (:attr:`MainFileID`).
1981 :returns: The path, as the compiler named it; ``None`` if every file is expanded to.
1982 """
1983 return None if (mainFileID := self.MainFileID) is None else self._filePaths[mainFileID]
1985 @readonly
1986 def MainFile(self) -> Nullable[File]:
1987 """
1988 Read-only property to return the file the function is in, as the report lists it.
1990 :returns: The file of :attr:`MainFilePath` in the report's files; ``None`` if the function isn't part of a report,
1991 every file is expanded to, or the report doesn't list the file.
1992 """
1993 if self._root is None or (path := self.MainFilePath) is None: 1993 ↛ 1994line 1993 didn't jump to line 1994 because the condition on line 1993 was never true
1994 return None
1996 return self._root._files.get(path, None)
1998 def _SetRoot(self, root: Nullable[Report]) -> None:
1999 """
2000 Set the report's root of the function, and of its regions, branch regions and MC/DC records.
2002 :param root: The report's root, or ``None`` while the element isn't part of a report.
2003 """
2004 self._root = root
2005 for element in (*self._regions, *self._branches, *self._mcdcRecords):
2006 element._SetRoot(root)
2009@export
2010class Report(metaclass=ExtendedType, mixin=True):
2011 """
2012 The report's root: the format version, the files, the functions, and the totals.
2014 It is the root of every element below it.
2015 """
2017 _root: Report #: The report's root: the report itself.
2018 _version: Nullable[SemanticVersion] #: Version of the report format.
2019 _files: dict[Path, File] #: The files, by path.
2020 _functions: list[Function] #: The functions.
2021 _totals: Nullable[Summary] #: The counters of the whole report.
2023 def __init__(
2024 self,
2025 version: Nullable[SemanticVersion] = None,
2026 files: Nullable[Iterable[File]] = None,
2027 functions: Nullable[Iterable[Function]] = None,
2028 totals: Nullable[Summary] = None
2029 ) -> None:
2030 """
2031 Initialize a report; empty, without parameters.
2033 :param version: Optional, version of the report format. Default: ``None``.
2034 :param files: Optional, the files. Default: none.
2035 :param functions: Optional, the functions. Default: none.
2036 :param totals: Optional, the counters of the whole report. Default: ``None``.
2037 :raises TypeError: If parameter ``version`` isn't of type
2038 :class:`~pyTooling.Versioning.SemanticVersion`.
2039 :raises TypeError: If parameter ``files`` or ``functions`` isn't iterable.
2040 :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`.
2041 :raises DuplicateFileError: If parameter ``files`` contains two files of the same path.
2042 :raises TypeError: If parameter ``functions`` contains an element not of type
2043 :class:`Function`.
2044 :raises AlreadyInCoverageReportError: If parameter ``files`` or ``functions`` contains an element of another
2045 report.
2046 :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`.
2047 :raises AlreadyInCoverageReportError: If parameter ``totals`` is the summary of another element.
2048 """
2049 if version is not None and not isinstance(version, SemanticVersion):
2050 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.")
2051 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2052 raise ex
2054 if totals is not None and not isinstance(totals, Summary): 2054 ↛ 2055line 2054 didn't jump to line 2055 because the condition on line 2054 was never true
2055 ex = TypeError("Parameter 'totals' is not of type 'Summary'.")
2056 ex.add_note(f"Got type '{getFullyQualifiedName(totals)}'.")
2057 raise ex
2058 elif totals is not None and totals._parent is not None: 2058 ↛ 2059line 2058 didn't jump to line 2059 because the condition on line 2058 was never true
2059 raise AlreadyInCoverageReportError("Parameter 'totals' is the summary of another element.")
2061 self._root = self
2062 self._version = version
2063 self._files = {}
2064 self._functions = []
2065 self._totals = totals
2067 if files is not None:
2068 if not isinstance(files, Iterable): 2068 ↛ 2069line 2068 didn't jump to line 2069 because the condition on line 2068 was never true
2069 ex = TypeError("Parameter 'files' is not iterable.")
2070 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.")
2071 raise ex
2073 for file in files:
2074 if not isinstance(file, File):
2075 ex = TypeError("Parameter 'files' contains an element not of type 'File'.")
2076 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
2077 raise ex
2078 elif file._parent is not None:
2079 raise AlreadyInCoverageReportError("Parameter 'files' contains a file of another report.")
2080 elif file._path in self._files:
2081 ex = DuplicateFileError("Parameter 'files' contains two files of the same path.")
2082 ex.add_note(f"Got value '{file._path.as_posix()}'.")
2083 raise ex
2085 file._parent = self
2086 file._SetRoot(self)
2088 self._files[file._path] = file
2090 if functions is not None:
2091 if not isinstance(functions, Iterable): 2091 ↛ 2092line 2091 didn't jump to line 2092 because the condition on line 2091 was never true
2092 ex = TypeError("Parameter 'functions' is not iterable.")
2093 ex.add_note(f"Got type '{getFullyQualifiedName(functions)}'.")
2094 raise ex
2096 for function in functions:
2097 if not isinstance(function, Function): 2097 ↛ 2098line 2097 didn't jump to line 2098 because the condition on line 2097 was never true
2098 ex = TypeError("Parameter 'functions' contains an element not of type 'Function'.")
2099 ex.add_note(f"Got type '{getFullyQualifiedName(function)}'.")
2100 raise ex
2101 elif function._parent is not None: 2101 ↛ 2102line 2101 didn't jump to line 2102 because the condition on line 2101 was never true
2102 raise AlreadyInCoverageReportError("Parameter 'functions' contains a function of another report.")
2104 function._parent = self
2105 function._SetRoot(self)
2107 self._functions.append(function)
2109 if totals is not None:
2110 totals._parent = self
2111 totals._SetRoot(self)
2113 @readonly
2114 def Version(self) -> Nullable[SemanticVersion]:
2115 """
2116 Read-only property to access the version of the report format (:attr:`_version`).
2118 :returns: The version, e.g. ``2.0.1``; ``None`` before the report was converted.
2119 """
2120 return self._version
2122 @readonly
2123 def Files(self) -> dict[Path, File]:
2124 """
2125 Read-only property to access the files (:attr:`_files`).
2127 :returns: The files, by path.
2128 """
2129 return self._files
2131 @readonly
2132 def Functions(self) -> list[Function]:
2133 """
2134 Read-only property to access the functions (:attr:`_functions`).
2136 :returns: The functions; empty in a report written with ``-skip-functions`` or ``-summary-only``.
2137 """
2138 return self._functions
2140 @readonly
2141 def Totals(self) -> Nullable[Summary]:
2142 """
2143 Read-only property to access the counters of the whole report (:attr:`_totals`).
2145 :returns: The totals; ``None`` before the report was converted.
2146 """
2147 return self._totals
2150TestVector._PARENT_TYPE = (MCDCRecord, )
2151Region._PARENT_TYPE = (Function, Expansion)
2152BranchRegion._PARENT_TYPE = (File, Function, Expansion)
2153MCDCRecord._PARENT_TYPE = (File, Function)
2154Counters._PARENT_TYPE = (Summary, )
2155Summary._PARENT_TYPE = (File, Report)
2156Segment._PARENT_TYPE = (File, )
2157Expansion._PARENT_TYPE = (File, )
2158File._PARENT_TYPE = (Report, )
2159Function._PARENT_TYPE = (Report, )
2162@export
2163class Document(cc_Document, Report):
2164 """
2165 An LLVM JSON code coverage export: read into the format's model, and converted to the common model.
2166 """
2168 _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`.
2170 def __init__(
2171 self,
2172 jsonReportFile: Path,
2173 analyzeAndConvert: bool = False,
2174 *,
2175 version: Nullable[SemanticVersion] = None,
2176 files: Nullable[Iterable[File]] = None,
2177 functions: Nullable[Iterable[Function]] = None,
2178 totals: Nullable[Summary] = None
2179 ) -> None:
2180 """
2181 Initialize the report, and optionally read it; or build it from typed values.
2183 :param jsonReportFile: Path to the JSON file.
2184 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
2185 :param version: Optional, version of the report format. Default: ``None``.
2186 :param files: Optional, the files. Default: none.
2187 :param functions: Optional, the functions. Default: none.
2188 :param totals: Optional, the counters of the whole report. Default: ``None``.
2189 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`.
2190 :raises TypeError: If parameter ``files`` or ``functions`` isn't iterable.
2191 :raises TypeError: If parameter ``files`` contains an element not of type
2192 :class:`File`.
2193 :raises TypeError: If parameter ``functions`` contains an element not of type
2194 :class:`Function`.
2195 :raises TypeError: If parameter ``totals`` isn't of type
2196 :class:`Summary`.
2197 """
2198 super().__init__(jsonReportFile)
2199 Report.__init__(self, version, files, functions, totals)
2201 self._jsonDocument = None
2203 if analyzeAndConvert:
2204 self.Analyze()
2205 self.Convert()
2207 def Analyze(self) -> None:
2208 """
2209 Parse the JSON file and validate it against the JSON Schema :data:`SCHEMA`.
2211 :raises CodeCoverageError: If the file doesn't exist.
2212 :raises CodeCoverageError: If the file isn't valid JSON.
2213 :raises CodeCoverageError: If the JSON Schema can't be read.
2214 :raises CodeCoverageError: If the file isn't valid according to the JSON Schema.
2215 """
2216 if not self._path.exists():
2217 raise CodeCoverageError(f"LLVM coverage export file '{self._path}' does not exist.") \
2218 from FileNotFoundError(f"File '{self._path}' not found.")
2220 with Stopwatch() as sw:
2221 try:
2222 jsonDocument = loads(self._path.read_text(encoding="utf-8"))
2223 except JSONDecodeError as ex:
2224 raise CodeCoverageError(f"JSON syntax error in LLVM coverage export file '{self._path}'.") from ex
2226 try:
2227 schema = loads(readResourceFile(Resources, SCHEMA))
2228 except (ToolingException, JSONDecodeError) as ex:
2229 raise CodeCoverageError(f"Couldn't read JSON Schema '{SCHEMA}' from package resources.") from ex
2231 errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path))
2232 if len(errors) > 0:
2233 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{SCHEMA}'.")
2234 for error in errors:
2235 ex.add_note(f"/{'/'.join(str(part) for part in error.path)}: {error.message}")
2236 raise ex
2238 self._jsonDocument = jsonDocument
2240 self._analysisDuration = sw.Duration
2242 def Convert(self) -> None:
2243 """
2244 Convert the parsed and validated JSON document to the format's model.
2246 :raises CodeCoverageError: If the JSON file was not analyzed before. |br|
2247 Call 'Document.Analyze()' or create the document using
2248 'Document(path, analyzeAndConvert=True)'.
2249 """
2250 if self._jsonDocument is None:
2251 ex = CodeCoverageError(
2252 f"LLVM coverage export file '{self._path}' needs to be read and analyzed by a JSON parser."
2253 )
2254 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
2255 raise ex
2257 with Stopwatch() as sw:
2258 data = self._jsonDocument["data"][0]
2259 self._version = SemanticVersion.Parse(self._jsonDocument["version"])
2260 self._files = {}
2261 self._functions = []
2262 self._totals = Summary.Parse(data["totals"])
2264 self._totals._parent = self
2265 self._totals._SetRoot(self)
2267 for record in data["files"]:
2268 File.Parse(record, parent=self)
2270 for function in data.get("functions", []):
2271 Function.Parse(function, parent=self)
2273 self._conversionDuration = sw.Duration
2275 def ToCoverageSummary(self) -> CoverageSummary:
2276 """
2277 Convert the format's model to the common model, and aggregate it.
2279 :returns: The report's root of the common model, named after the report file.
2280 :raises CodeCoverageError: If a file's path runs through another file.
2281 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to.
2282 """
2283 parents = [path.parent.parts for path in self._files]
2284 first = min(parents, default=())
2285 last = max(parents, default=())
2286 common = first
2287 for index, part in enumerate(first): 2287 ↛ 2292line 2287 didn't jump to line 2292 because the loop on line 2287 didn't complete
2288 if part != last[index]:
2289 common = first[:index]
2290 break
2292 summary = CoverageSummary(self._path.stem, sourceDirectories=[Path(*common)] if len(common) > 0 else [])
2293 commonFiles = {path: summary.GetOrAddFile("/".join(path.parts[len(common):])) for path in self._files}
2295 branches = self._CollectBranches()
2296 for path, file in self._files.items():
2297 self._ConvertLines(file, commonFiles[path], branches[path])
2299 self._ConvertUnits(commonFiles, summary)
2301 summary.Aggregate()
2302 return summary
2304 def _CollectBranches(self) -> dict[Path, dict[tuple[Any, ...], list[int]]]:
2305 """
2306 Collect the branch regions of the functions by file; a branch region found in several functions is summed.
2308 A branch region is collected at the line it starts at, or at the line its macro is expanded at. It is found in
2309 several functions e.g. in the instantiations of a template.
2311 :returns: By path, the summed true and false counts of each branch region, keyed by the line
2312 number first.
2313 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to.
2314 """
2315 branches: dict[Path, dict[tuple[Any, ...], list[int]]] = {path: {} for path in self._files}
2316 for function in self._functions:
2317 mainFileID = function.MainFileID
2318 if mainFileID is None or (fileBranches := branches.get(function._filePaths[mainFileID])) is None: 2318 ↛ 2319line 2318 didn't jump to line 2319 because the condition on line 2318 was never true
2319 continue
2321 expansions = {
2322 region._expandedFileID: region for region in function._regions if region._kind is RegionKind.Expansion
2323 }
2324 for branch in function._branches:
2325 lineNumber = branch._lineStart
2326 fileID = branch._fileID
2327 while fileID != mainFileID:
2328 if (expansion := expansions.get(fileID)) is None:
2329 ex = CodeCoverageError(f"Function '{function._name}' has a branch region in a file no region expands to.")
2330 ex.add_note(f"Got file ID {fileID} of file '{function._filePaths[fileID]}'.")
2331 raise ex
2333 lineNumber = expansion._lineStart
2334 fileID = expansion._fileID
2336 key = (
2337 lineNumber, function._filePaths[branch._fileID],
2338 branch._lineStart, branch._columnStart, branch._lineEnd, branch._columnEnd
2339 )
2340 if (counts := fileBranches.get(key)) is None:
2341 fileBranches[key] = [branch._trueCount, branch._falseCount]
2342 else:
2343 counts[0] += branch._trueCount
2344 counts[1] += branch._falseCount
2346 return branches
2348 @staticmethod
2349 def _ConvertLines(file: File, commonFile: cc_File, branches: dict[tuple[Any, ...], list[int]]) -> None:
2350 """
2351 Convert a file's segments to lines of the common model, and add the branches of the file's branch regions.
2353 A line's count is derived as ``llvm-cov show`` derives it: of the segments starting on the line, and of the last
2354 segment of a previous line, which continues on it. A branch region of a line, which isn't executable, is left out.
2356 :param file: The file of the format's model.
2357 :param commonFile: The file of the common model.
2358 :param branches: The true and false counts of each branch region of the file, keyed by the line number first.
2359 """
2360 counts: dict[int, int] = {}
2361 wrapped: Nullable[Segment] = None
2362 segments = file._segments
2363 index = 0
2364 while index < len(segments):
2365 lineNumber = segments[index]._line
2366 lineSegments = []
2367 while index < len(segments) and segments[index]._line == lineNumber:
2368 lineSegments.append(segments[index])
2369 index += 1
2371 starts = [s._count for s in lineSegments if s._hasCount and s._isRegionEntry and not s._isGapRegion]
2372 skipped = not lineSegments[0]._hasCount and lineSegments[0]._isRegionEntry
2373 if (
2374 (not skipped and ((wrapped is not None and wrapped._hasCount) or len(starts) > 0)) or
2375 any(segment._hasCount and segment._isRegionEntry for segment in lineSegments)
2376 ):
2377 counts[lineNumber] = max((wrapped._count if wrapped is not None else 0, *starts))
2379 # the lines up to the next segment continue the last segment of this line
2380 wrapped = lineSegments[-1]
2381 if wrapped._hasCount:
2382 nextLineNumber = segments[index]._line if index < len(segments) else lineNumber + 1
2383 for number in range(lineNumber + 1, nextLineNumber):
2384 counts[number] = wrapped._count
2386 partialLines = {key[0] for key, outcomes in branches.items() if 0 in outcomes}
2387 for number in sorted(counts):
2388 count = counts[number]
2389 if count == 0:
2390 status = LineCoverageStatus.Uncovered
2391 elif number in partialLines:
2392 status = LineCoverageStatus.PartiallyCovered
2393 else:
2394 status = LineCoverageStatus.Covered
2396 cc_Line(number, status, count, parent=commonFile)
2398 lines = commonFile._lines
2399 for key in sorted(branches):
2400 if key[0] < len(lines) and (line := lines[key[0]]) is not None: 2400 ↛ 2399line 2400 didn't jump to line 2399 because the condition on line 2400 was always true
2401 for count in branches[key]:
2402 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=line)
2404 def _ConvertUnits(self, commonFiles: dict[Path, cc_File], summary: CoverageSummary) -> None:
2405 """
2406 Convert the files to source file units, and the functions to function units of their files.
2408 A function local to a translation unit is named without the file name prefixing it. Functions of the same name in a
2409 file - e.g. a header's inline function compiled in several translation units - become one unit, their counts
2410 summed.
2412 :param commonFiles: The files of the common model, by path.
2413 :param summary: The report's root of the common model.
2414 """
2415 functions: dict[tuple[Path, str], list[int]] = {}
2416 for function in self._functions:
2417 mainFileID = function.MainFileID
2418 if mainFileID is None or (path := function._filePaths[mainFileID]) not in commonFiles: 2418 ↛ 2419line 2418 didn't jump to line 2419 because the condition on line 2418 was never true
2419 continue
2421 name = function._name
2422 if (local := match(r"(.+?\.[^.:;/\\]+)[:;](.+)", name)) is not None:
2423 name = local[2]
2425 regions = [region for region in function._regions if region._fileID == mainFileID]
2426 firstLineNumber = min((region._lineStart for region in regions), default=0)
2427 lastLineNumber = max((region._lineEnd for region in regions), default=0)
2428 if (span := functions.get((path, name))) is None: 2428 ↛ 2431line 2428 didn't jump to line 2431 because the condition on line 2428 was always true
2429 functions[(path, name)] = [function._count, firstLineNumber, lastLineNumber]
2430 else:
2431 span[0] += function._count
2432 span[1] = min(span[1], firstLineNumber)
2433 span[2] = max(span[2], lastLineNumber)
2435 sourceFiles: dict[Path, cc_SourceFile] = {}
2436 for path, commonFile in commonFiles.items():
2437 lastLineNumber = commonFile._lastLineNumber
2438 sourceFiles[path] = cc_SourceFile(
2439 commonFile.Path.as_posix(),
2440 file=commonFile,
2441 startLine=next(commonFile.IterateLines(), None),
2442 endLine=commonFile._lines[lastLineNumber] if lastLineNumber > 0 else None,
2443 parent=summary
2444 )
2446 for (path, name), (count, firstLineNumber, lastLineNumber) in functions.items():
2447 commonFile = commonFiles[path]
2448 lines = [line for line in commonFile._lines[firstLineNumber:lastLineNumber + 1] if line is not None]
2449 cc_Function(
2450 name,
2451 file=commonFile,
2452 startLine=lines[0] if len(lines) > 0 else None,
2453 endLine=lines[-1] if len(lines) > 0 else None,
2454 status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered,
2455 coverageCount=count,
2456 parent=sourceFiles[path]
2457 )