Coverage for pyEDAA/Reports/CodeCoverage/Gcov.py: 82%
804 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"""
32GCC's gcov JSON code coverage format: a model of the format, read from a report and converted to the common model.
34gcov writes the format with ``gcov --json-format``: gzip-compressed to a :file:`*.gcov.json.gz` file per data file, or,
35with ``--stdout``, as plain JSON, one line per data file. A report is read in either form, and each JSON object in
36it is validated against the JSON Schema of the format version it states (:class:`FormatVersion`), reverse-engineered
37from GCC: :file:`Gcov-1.schema.json` for format 1 (GCC 9 to 13), :file:`Gcov-2.schema.json` for format 2 (GCC 14 and
38later). The format's model keeps what the report states: a :class:`Document` holds :class:`DataFile` records, a data
39file :class:`File` records, a file its :class:`Function` and :class:`Line` records, and a line its :class:`Branch`,
40:class:`Call` and :class:`Condition` records - in format 2 with the IDs of the basic blocks they belong to. Each
41record's constructor takes typed values, so the model can be built by hand: a record below the report names its parent
42with the keyword parameter ``parent`` and is added to it. Its class method ``Parse`` reads the record's JSON object.
44:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`:
46* A file's path is relative to the directory the compiler ran in - a data file's ``current_working_directory`` -,
47 unless it is absolute.
48* A line's ``count`` is its count; a line, which ran without taking all of its branches, is partially covered. A line
49 several functions share - e.g. the instantiations of a template - or several data files state - e.g. a header -
50 becomes one line, its counts added.
51* A branch becomes a branch of its line, with its count; exceptional branches (``throw``) are branches too. A branch
52 leads to a basic block, not to a line, so it has no target.
53* A file becomes a source file unit, its functions - by demangled name, e.g. ``Containers::Stack::Pop()`` - become
54 functions, each spanning its first to its last line, with its execution count.
55* Calls, conditions and basic blocks have no counterpart in the common model. The format has no excluded lines.
57.. rubric:: Example
59.. code-block:: Python
61 from pathlib import Path
62 from pyEDAA.Reports.CodeCoverage.Gcov import Document
64 report = Document(Path("main.gcov.json.gz"), analyzeAndConvert=True)
65 summary = report.ToCoverageSummary()
66 print(f"{summary.FileCount} files: {summary.LineCoverage:.1%}")
67"""
68from __future__ import annotations
70from enum import IntEnum
71from gzip import BadGzipFile, decompress
72from json import JSONDecodeError, JSONDecoder, loads
73from pathlib import Path
74from re import compile as re_compile
75from typing import Any, Iterable, Optional as Nullable, Self
76from zlib import error as ZLibError
78from jsonschema import Draft202012Validator
79from pyTooling.Common import getFullyQualifiedName, readResourceFile
80from pyTooling.Decorators import export, readonly
81from pyTooling.Exceptions import ToolingException
82from pyTooling.MetaClasses import ExtendedType
83from pyTooling.Stopwatch import Stopwatch
84from pyTooling.Versioning import SemanticVersion
86from pyEDAA.Reports import Resources
87from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, CodeCoverageError, CoverageSummary
88from pyEDAA.Reports.CodeCoverage import Document as cc_Document, File as cc_File, Function as cc_Function
89from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile
92__all__ = ["SCHEMAS"]
94# A class with a property named like a class - ``Path`` - can't name that class in the annotation of a field: the class
95# body's namespace, where annotations are evaluated, binds the name to the property.
96_Path = Path
99@export
100class FormatVersion(IntEnum):
101 """
102 Version of the gcov JSON format, as a report's ``format_version`` states it.
104 GCC 9 to 13 write format 1. GCC 14 and later write format 2, which adds the IDs of the basic blocks of a line and of
105 a branch, and the calls and conditions of a line; GCC 15 adds the prime paths of a function to format 2.
106 """
108 Version1 = 1 #: Format 1, written by GCC 9 to 13.
109 Version2 = 2 #: Format 2, written by GCC 14 and later.
111 @classmethod
112 def Parse(cls, value: str) -> Self:
113 """
114 Convert the version, as gcov states it, to the member of that version.
116 gcov states the version as a string, e.g. ``"2"``.
118 :param value: The version, as a report's ``format_version`` states it.
119 :returns: The member of that version.
120 :raises ValueError: If parameter ``value`` is ``None``.
121 :raises TypeError: If parameter ``value`` isn't of type :class:`str`.
122 :raises ValueError: If parameter ``value`` isn't a supported format version.
123 """
124 if value is None:
125 raise ValueError("Parameter 'value' is None.")
126 elif not isinstance(value, str):
127 ex = TypeError("Parameter 'value' is not of type 'str'.")
128 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
129 raise ex
130 elif value not in [str(member.value) for member in cls]:
131 ex = ValueError("Parameter 'value' is not a supported gcov JSON format version.")
132 ex.add_note(f"Got value '{value}'.")
133 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in cls)}.")
134 raise ex
136 return cls(int(value))
139# Like '_Path': DataFile has a property named 'FormatVersion'.
140_FormatVersion = FormatVersion
142SCHEMAS: dict[FormatVersion, str] = {
143 FormatVersion.Version1: "Gcov-1.schema.json",
144 FormatVersion.Version2: "Gcov-2.schema.json"
145} #: Per format version, the JSON Schema a JSON object of that version is validated against.
148@export
149class Branch(metaclass=ExtendedType, slots=True):
150 """
151 A ``branch`` of a line: an edge from a basic block to another, and how often it was taken.
152 """
154 _parent: Nullable[Line] #: The line the branch belongs to.
155 _count: int #: Number of times the branch was taken.
156 _throw: bool #: Whether the branch is taken by an exception.
157 _fallthrough: bool #: Whether the branch falls through.
158 _sourceBlockID: Nullable[int] #: ID of the basic block the branch starts at, in format 2.
159 _destinationBlockID: Nullable[int] #: ID of the basic block the branch leads to, in format 2.
161 def __init__(
162 self,
163 count: int,
164 throw: bool,
165 fallthrough: bool,
166 sourceBlockID: Nullable[int] = None,
167 destinationBlockID: Nullable[int] = None,
168 *,
169 parent: Nullable[Line] = None
170 ) -> None:
171 """
172 Initialize the branch, and add it to the branches of its line.
174 :param count: Number of times the branch was taken.
175 :param throw: Whether the branch is taken by an exception.
176 :param fallthrough: Whether the branch falls through.
177 :param sourceBlockID: Optional, ID of the basic block the branch starts at. Default: ``None``.
178 :param destinationBlockID: Optional, ID of the basic block the branch leads to. Default: ``None``.
179 :param parent: Optional, the line the branch belongs to; the branch is appended to its branches.
180 Default: ``None``.
181 :raises ValueError: If parameter ``count`` is ``None``.
182 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
183 :raises ValueError: If parameter ``count`` is negative.
184 :raises ValueError: If parameter ``throw`` is ``None``.
185 :raises TypeError: If parameter ``throw`` isn't of type :class:`bool`.
186 :raises ValueError: If parameter ``fallthrough`` is ``None``.
187 :raises TypeError: If parameter ``fallthrough`` isn't of type :class:`bool`.
188 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`.
189 :raises ValueError: If parameter ``sourceBlockID`` is negative.
190 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`.
191 :raises ValueError: If parameter ``destinationBlockID`` is negative.
192 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
193 """
194 if count is None: 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true
195 raise ValueError("Parameter 'count' is None.")
196 elif not isinstance(count, int): 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 ex = TypeError("Parameter 'count' is not of type 'int'.")
198 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
199 raise ex
200 elif count < 0: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 ex = ValueError("Parameter 'count' is negative.")
202 ex.add_note(f"Got value '{count}'.")
203 raise ex
205 if throw is None:
206 raise ValueError("Parameter 'throw' is None.")
207 elif not isinstance(throw, bool):
208 ex = TypeError("Parameter 'throw' is not of type 'bool'.")
209 ex.add_note(f"Got type '{getFullyQualifiedName(throw)}'.")
210 raise ex
212 if fallthrough is None: 212 ↛ 213line 212 didn't jump to line 213 because the condition on line 212 was never true
213 raise ValueError("Parameter 'fallthrough' is None.")
214 elif not isinstance(fallthrough, bool): 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 ex = TypeError("Parameter 'fallthrough' is not of type 'bool'.")
216 ex.add_note(f"Got type '{getFullyQualifiedName(fallthrough)}'.")
217 raise ex
219 if sourceBlockID is not None:
220 if not isinstance(sourceBlockID, int): 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true
221 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.")
222 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.")
223 raise ex
224 elif sourceBlockID < 0:
225 ex = ValueError("Parameter 'sourceBlockID' is negative.")
226 ex.add_note(f"Got value '{sourceBlockID}'.")
227 raise ex
229 if destinationBlockID is not None:
230 if not isinstance(destinationBlockID, int): 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.")
232 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.")
233 raise ex
234 elif destinationBlockID < 0: 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true
235 ex = ValueError("Parameter 'destinationBlockID' is negative.")
236 ex.add_note(f"Got value '{destinationBlockID}'.")
237 raise ex
239 if parent is not None and not isinstance(parent, Line):
240 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
241 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
242 raise ex
244 self._parent = parent
245 self._count = count
246 self._throw = throw
247 self._fallthrough = fallthrough
248 self._sourceBlockID = sourceBlockID
249 self._destinationBlockID = destinationBlockID
251 if parent is not None:
252 parent._branches.append(self)
254 @classmethod
255 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
256 """
257 Parse a branch from its JSON object.
259 :param record: The JSON object of the branch.
260 :param parent: Optional, the line the branch belongs to. Default: ``None``.
261 :returns: The branch.
262 :raises ValueError: If parameter ``record`` is ``None``.
263 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
264 """
265 if record is None:
266 raise ValueError("Parameter 'record' is None.")
267 elif not isinstance(record, dict):
268 ex = TypeError("Parameter 'record' is not of type 'dict'.")
269 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
270 raise ex
272 return cls(
273 record["count"],
274 record["throw"],
275 record["fallthrough"],
276 record.get("source_block_id"),
277 record.get("destination_block_id"),
278 parent=parent
279 )
281 @readonly
282 def Parent(self) -> Nullable[Line]:
283 """
284 Read-only property to access the line the branch belongs to (:attr:`_parent`).
286 :returns: The line; ``None`` if the branch belongs to no line.
287 """
288 return self._parent
290 @readonly
291 def Count(self) -> int:
292 """
293 Read-only property to access the number of times the branch was taken (:attr:`_count`).
295 :returns: The count.
296 """
297 return self._count
299 @readonly
300 def Throw(self) -> bool:
301 """
302 Read-only property to access whether the branch is taken by an exception (:attr:`_throw`).
304 :returns: ``True``, if the branch is exceptional.
305 """
306 return self._throw
308 @readonly
309 def Fallthrough(self) -> bool:
310 """
311 Read-only property to access whether the branch falls through (:attr:`_fallthrough`).
313 :returns: ``True``, if the branch falls through.
314 """
315 return self._fallthrough
317 @readonly
318 def SourceBlockID(self) -> Nullable[int]:
319 """
320 Read-only property to access the ID of the basic block the branch starts at (:attr:`_sourceBlockID`).
322 :returns: The block's ID, unique within its function; ``None`` in format 1.
323 """
324 return self._sourceBlockID
326 @readonly
327 def DestinationBlockID(self) -> Nullable[int]:
328 """
329 Read-only property to access the ID of the basic block the branch leads to (:attr:`_destinationBlockID`).
331 :returns: The block's ID, unique within its function; ``None`` in format 1.
332 """
333 return self._destinationBlockID
336@export
337class Call(metaclass=ExtendedType, slots=True):
338 """
339 A ``call`` of a line - in format 2 -: from a basic block to the block continuing after the return.
340 """
342 _parent: Nullable[Line] #: The line the call belongs to.
343 _sourceBlockID: int #: ID of the basic block the call is in.
344 _destinationBlockID: int #: ID of the basic block continuing after the return.
345 _returned: int #: Number of times the call returned.
347 def __init__(
348 self,
349 sourceBlockID: int,
350 destinationBlockID: int,
351 returned: int,
352 *,
353 parent: Nullable[Line] = None
354 ) -> None:
355 """
356 Initialize the call, and add it to the calls of its line.
358 :param sourceBlockID: ID of the basic block the call is in.
359 :param destinationBlockID: ID of the basic block continuing after the return.
360 :param returned: Number of times the call returned.
361 :param parent: Optional, the line the call belongs to; the call is appended to its calls.
362 Default: ``None``.
363 :raises ValueError: If parameter ``sourceBlockID`` is ``None``.
364 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`.
365 :raises ValueError: If parameter ``sourceBlockID`` is negative.
366 :raises ValueError: If parameter ``destinationBlockID`` is ``None``.
367 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`.
368 :raises ValueError: If parameter ``destinationBlockID`` is negative.
369 :raises ValueError: If parameter ``returned`` is ``None``.
370 :raises TypeError: If parameter ``returned`` isn't of type :class:`int`.
371 :raises ValueError: If parameter ``returned`` is negative.
372 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
373 """
374 if sourceBlockID is None: 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true
375 raise ValueError("Parameter 'sourceBlockID' is None.")
376 elif not isinstance(sourceBlockID, int): 376 ↛ 377line 376 didn't jump to line 377 because the condition on line 376 was never true
377 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.")
378 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.")
379 raise ex
380 elif sourceBlockID < 0: 380 ↛ 381line 380 didn't jump to line 381 because the condition on line 380 was never true
381 ex = ValueError("Parameter 'sourceBlockID' is negative.")
382 ex.add_note(f"Got value '{sourceBlockID}'.")
383 raise ex
385 if destinationBlockID is None: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true
386 raise ValueError("Parameter 'destinationBlockID' is None.")
387 elif not isinstance(destinationBlockID, int): 387 ↛ 388line 387 didn't jump to line 388 because the condition on line 387 was never true
388 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.")
389 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.")
390 raise ex
391 elif destinationBlockID < 0: 391 ↛ 392line 391 didn't jump to line 392 because the condition on line 391 was never true
392 ex = ValueError("Parameter 'destinationBlockID' is negative.")
393 ex.add_note(f"Got value '{destinationBlockID}'.")
394 raise ex
396 if returned is None:
397 raise ValueError("Parameter 'returned' is None.")
398 elif not isinstance(returned, int): 398 ↛ 399line 398 didn't jump to line 399 because the condition on line 398 was never true
399 ex = TypeError("Parameter 'returned' is not of type 'int'.")
400 ex.add_note(f"Got type '{getFullyQualifiedName(returned)}'.")
401 raise ex
402 elif returned < 0: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true
403 ex = ValueError("Parameter 'returned' is negative.")
404 ex.add_note(f"Got value '{returned}'.")
405 raise ex
407 if parent is not None and not isinstance(parent, Line):
408 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
409 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
410 raise ex
412 self._parent = parent
413 self._sourceBlockID = sourceBlockID
414 self._destinationBlockID = destinationBlockID
415 self._returned = returned
417 if parent is not None:
418 parent._calls.append(self)
420 @classmethod
421 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
422 """
423 Parse a call from its JSON object.
425 :param record: The JSON object of the call.
426 :param parent: Optional, the line the call belongs to. Default: ``None``.
427 :returns: The call.
428 :raises ValueError: If parameter ``record`` is ``None``.
429 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
430 """
431 if record is None:
432 raise ValueError("Parameter 'record' is None.")
433 elif not isinstance(record, dict):
434 ex = TypeError("Parameter 'record' is not of type 'dict'.")
435 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
436 raise ex
438 return cls(record["source_block_id"], record["destination_block_id"], record["returned"], parent=parent)
440 @readonly
441 def Parent(self) -> Nullable[Line]:
442 """
443 Read-only property to access the line the call belongs to (:attr:`_parent`).
445 :returns: The line; ``None`` if the call belongs to no line.
446 """
447 return self._parent
449 @readonly
450 def SourceBlockID(self) -> int:
451 """
452 Read-only property to access the ID of the basic block the call is in (:attr:`_sourceBlockID`).
454 :returns: The block's ID, unique within its function.
455 """
456 return self._sourceBlockID
458 @readonly
459 def DestinationBlockID(self) -> int:
460 """
461 Read-only property to access the ID of the basic block continuing after the return (:attr:`_destinationBlockID`).
463 :returns: The block's ID, unique within its function.
464 """
465 return self._destinationBlockID
467 @readonly
468 def Returned(self) -> int:
469 """
470 Read-only property to access the number of times the call returned (:attr:`_returned`).
472 :returns: The count; how often the call was made is the line's count.
473 """
474 return self._returned
477@export
478class Condition(metaclass=ExtendedType, slots=True):
479 """
480 A ``condition`` of a line - in format 2, with ``gcc -fcondition-coverage`` -: the condition outcomes of a boolean
481 expression (MC/DC).
482 """
484 _parent: Nullable[Line] #: The line the condition belongs to.
485 _count: int #: Number of condition outcomes: twice the number of terms.
486 _covered: int #: Number of covered condition outcomes.
487 _notCoveredTrue: list[int] #: The terms, by index, never seen as true.
488 _notCoveredFalse: list[int] #: The terms, by index, never seen as false.
490 def __init__(
491 self,
492 count: int,
493 covered: int,
494 notCoveredTrue: Nullable[Iterable[int]] = None,
495 notCoveredFalse: Nullable[Iterable[int]] = None,
496 *,
497 parent: Nullable[Line] = None
498 ) -> None:
499 """
500 Initialize the condition, and add it to the conditions of its line.
502 :param count: Number of condition outcomes: twice the number of terms.
503 :param covered: Number of covered condition outcomes.
504 :param notCoveredTrue: Optional, the terms, by index, never seen as true. Default: none.
505 :param notCoveredFalse: Optional, the terms, by index, never seen as false. Default: none.
506 :param parent: Optional, the line the condition belongs to; the condition is appended to its conditions.
507 Default: ``None``.
508 :raises ValueError: If parameter ``count`` is ``None``.
509 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
510 :raises ValueError: If parameter ``count`` is negative.
511 :raises ValueError: If parameter ``covered`` is ``None``.
512 :raises TypeError: If parameter ``covered`` isn't of type :class:`int`.
513 :raises ValueError: If parameter ``covered`` is negative.
514 :raises TypeError: If parameter ``notCoveredTrue`` isn't iterable.
515 :raises TypeError: If parameter ``notCoveredTrue`` contains an element not of type :class:`int`.
516 :raises TypeError: If parameter ``notCoveredFalse`` isn't iterable.
517 :raises TypeError: If parameter ``notCoveredFalse`` contains an element not of type :class:`int`.
518 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
519 """
520 if count is None: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true
521 raise ValueError("Parameter 'count' is None.")
522 elif not isinstance(count, int): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true
523 ex = TypeError("Parameter 'count' is not of type 'int'.")
524 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
525 raise ex
526 elif count < 0: 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 ex = ValueError("Parameter 'count' is negative.")
528 ex.add_note(f"Got value '{count}'.")
529 raise ex
531 if covered is None: 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true
532 raise ValueError("Parameter 'covered' is None.")
533 elif not isinstance(covered, int): 533 ↛ 534line 533 didn't jump to line 534 because the condition on line 533 was never true
534 ex = TypeError("Parameter 'covered' is not of type 'int'.")
535 ex.add_note(f"Got type '{getFullyQualifiedName(covered)}'.")
536 raise ex
537 elif covered < 0: 537 ↛ 538line 537 didn't jump to line 538 because the condition on line 537 was never true
538 ex = ValueError("Parameter 'covered' is negative.")
539 ex.add_note(f"Got value '{covered}'.")
540 raise ex
542 notCoveredTrueList: list[int] = []
543 if notCoveredTrue is not None:
544 if not isinstance(notCoveredTrue, Iterable):
545 ex = TypeError("Parameter 'notCoveredTrue' is not iterable.")
546 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredTrue)}'.")
547 raise ex
549 for term in notCoveredTrue:
550 if not isinstance(term, int): 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true
551 ex = TypeError("Parameter 'notCoveredTrue' contains an element not of type 'int'.")
552 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.")
553 raise ex
555 notCoveredTrueList.append(term)
557 notCoveredFalseList: list[int] = []
558 if notCoveredFalse is not None:
559 if not isinstance(notCoveredFalse, Iterable):
560 ex = TypeError("Parameter 'notCoveredFalse' is not iterable.")
561 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredFalse)}'.")
562 raise ex
564 for term in notCoveredFalse:
565 if not isinstance(term, int):
566 ex = TypeError("Parameter 'notCoveredFalse' contains an element not of type 'int'.")
567 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.")
568 raise ex
570 notCoveredFalseList.append(term)
572 if parent is not None and not isinstance(parent, Line):
573 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
574 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
575 raise ex
577 self._parent = parent
578 self._count = count
579 self._covered = covered
580 self._notCoveredTrue = notCoveredTrueList
581 self._notCoveredFalse = notCoveredFalseList
583 if parent is not None:
584 parent._conditions.append(self)
586 @classmethod
587 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
588 """
589 Parse a condition from its JSON object.
591 :param record: The JSON object of the condition.
592 :param parent: Optional, the line the condition belongs to. Default: ``None``.
593 :returns: The condition.
594 :raises ValueError: If parameter ``record`` is ``None``.
595 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
596 """
597 if record is None:
598 raise ValueError("Parameter 'record' is None.")
599 elif not isinstance(record, dict):
600 ex = TypeError("Parameter 'record' is not of type 'dict'.")
601 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
602 raise ex
604 return cls(
605 record["count"], record["covered"], record["not_covered_true"], record["not_covered_false"], parent=parent
606 )
608 @readonly
609 def Parent(self) -> Nullable[Line]:
610 """
611 Read-only property to access the line the condition belongs to (:attr:`_parent`).
613 :returns: The line; ``None`` if the condition belongs to no line.
614 """
615 return self._parent
617 @readonly
618 def Count(self) -> int:
619 """
620 Read-only property to access the number of condition outcomes (:attr:`_count`).
622 :returns: The number of outcomes: twice the number of terms.
623 """
624 return self._count
626 @readonly
627 def Covered(self) -> int:
628 """
629 Read-only property to access the number of covered condition outcomes (:attr:`_covered`).
631 :returns: The number of covered outcomes.
632 """
633 return self._covered
635 @readonly
636 def NotCoveredTrue(self) -> list[int]:
637 """
638 Read-only property to access the terms never seen as true (:attr:`_notCoveredTrue`).
640 :returns: The terms' indices.
641 """
642 return self._notCoveredTrue
644 @readonly
645 def NotCoveredFalse(self) -> list[int]:
646 """
647 Read-only property to access the terms never seen as false (:attr:`_notCoveredFalse`).
649 :returns: The terms' indices.
650 """
651 return self._notCoveredFalse
654@export
655class Line(metaclass=ExtendedType, slots=True):
656 """
657 A ``line`` of a file: how often it ran, its basic blocks, branches, calls and conditions.
658 """
660 _parent: Nullable[File] #: The file the line belongs to.
661 _lineNumber: int #: Line number, counted from 1.
662 _functionName: Nullable[str] #: Mangled name of the function the line belongs to, if the report says.
663 _count: int #: Number of times the line ran.
664 _unexecutedBlock: bool #: Whether a basic block, not only reached by exceptions, never ran.
665 _blockIDs: list[int] #: IDs of the basic blocks ending on this line, in format 2.
666 _branches: list[Branch] #: The branches.
667 _calls: list[Call] #: The calls, in format 2.
668 _conditions: list[Condition] #: The conditions, in format 2.
670 def __init__(
671 self,
672 lineNumber: int,
673 count: int,
674 unexecutedBlock: bool,
675 functionName: Nullable[str] = None,
676 blockIDs: Nullable[Iterable[int]] = None,
677 *,
678 parent: Nullable[File] = None
679 ) -> None:
680 """
681 Initialize the line, and add it to the lines of its file.
683 Its branches, calls and conditions are added by creating them with this line as their parent.
685 :param lineNumber: Line number, counted from 1.
686 :param count: Number of times the line ran.
687 :param unexecutedBlock: Whether a basic block of the line, not only reached by exceptions, never ran.
688 :param functionName: Optional, mangled name of the function the line belongs to. Default: ``None``.
689 :param blockIDs: Optional, IDs of the basic blocks ending on this line. Default: none.
690 :param parent: Optional, the file the line belongs to; the line is appended to its lines.
691 Default: ``None``.
692 :raises ValueError: If parameter ``lineNumber`` is ``None``.
693 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
694 :raises ValueError: If parameter ``lineNumber`` is less than 1.
695 :raises ValueError: If parameter ``count`` is ``None``.
696 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
697 :raises ValueError: If parameter ``count`` is negative.
698 :raises ValueError: If parameter ``unexecutedBlock`` is ``None``.
699 :raises TypeError: If parameter ``unexecutedBlock`` isn't of type :class:`bool`.
700 :raises TypeError: If parameter ``functionName`` isn't of type :class:`str`.
701 :raises TypeError: If parameter ``blockIDs`` isn't iterable.
702 :raises TypeError: If parameter ``blockIDs`` contains an element not of type :class:`int`.
703 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
704 """
705 if lineNumber is None:
706 raise ValueError("Parameter 'lineNumber' is None.")
707 elif not isinstance(lineNumber, int):
708 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
709 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
710 raise ex
711 elif lineNumber < 1:
712 ex = ValueError("Parameter 'lineNumber' is less than 1.")
713 ex.add_note(f"Got value '{lineNumber}'.")
714 raise ex
716 if count is None: 716 ↛ 717line 716 didn't jump to line 717 because the condition on line 716 was never true
717 raise ValueError("Parameter 'count' is None.")
718 elif not isinstance(count, int): 718 ↛ 719line 718 didn't jump to line 719 because the condition on line 718 was never true
719 ex = TypeError("Parameter 'count' is not of type 'int'.")
720 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
721 raise ex
722 elif count < 0: 722 ↛ 723line 722 didn't jump to line 723 because the condition on line 722 was never true
723 ex = ValueError("Parameter 'count' is negative.")
724 ex.add_note(f"Got value '{count}'.")
725 raise ex
727 if unexecutedBlock is None: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true
728 raise ValueError("Parameter 'unexecutedBlock' is None.")
729 elif not isinstance(unexecutedBlock, bool): 729 ↛ 730line 729 didn't jump to line 730 because the condition on line 729 was never true
730 ex = TypeError("Parameter 'unexecutedBlock' is not of type 'bool'.")
731 ex.add_note(f"Got type '{getFullyQualifiedName(unexecutedBlock)}'.")
732 raise ex
734 if functionName is not None and not isinstance(functionName, str): 734 ↛ 735line 734 didn't jump to line 735 because the condition on line 734 was never true
735 ex = TypeError("Parameter 'functionName' is not of type 'str'.")
736 ex.add_note(f"Got type '{getFullyQualifiedName(functionName)}'.")
737 raise ex
739 blockIDList: list[int] = []
740 if blockIDs is not None:
741 if not isinstance(blockIDs, Iterable):
742 ex = TypeError("Parameter 'blockIDs' is not iterable.")
743 ex.add_note(f"Got type '{getFullyQualifiedName(blockIDs)}'.")
744 raise ex
746 for blockID in blockIDs:
747 if not isinstance(blockID, int):
748 ex = TypeError("Parameter 'blockIDs' contains an element not of type 'int'.")
749 ex.add_note(f"Got type '{getFullyQualifiedName(blockID)}'.")
750 raise ex
752 blockIDList.append(blockID)
754 if parent is not None and not isinstance(parent, File):
755 ex = TypeError("Parameter 'parent' is not of type 'File'.")
756 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
757 raise ex
759 self._parent = parent
760 self._lineNumber = lineNumber
761 self._functionName = functionName
762 self._count = count
763 self._unexecutedBlock = unexecutedBlock
764 self._blockIDs = blockIDList
765 self._branches = []
766 self._calls = []
767 self._conditions = []
769 if parent is not None:
770 parent._lines.append(self)
772 @classmethod
773 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
774 """
775 Parse a line, its branches, calls and conditions from its JSON object.
777 :param record: The JSON object of the line.
778 :param parent: Optional, the file the line belongs to. Default: ``None``.
779 :returns: The line.
780 :raises ValueError: If parameter ``record`` is ``None``.
781 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
782 """
783 if record is None:
784 raise ValueError("Parameter 'record' is None.")
785 elif not isinstance(record, dict):
786 ex = TypeError("Parameter 'record' is not of type 'dict'.")
787 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
788 raise ex
790 line = cls(
791 record["line_number"],
792 record["count"],
793 record["unexecuted_block"],
794 record.get("function_name"),
795 record.get("block_ids"),
796 parent=parent
797 )
799 for branch in record["branches"]:
800 Branch.Parse(branch, parent=line)
802 for call in record.get("calls", []):
803 Call.Parse(call, parent=line)
805 for condition in record.get("conditions", []):
806 Condition.Parse(condition, parent=line)
808 return line
810 @readonly
811 def Parent(self) -> Nullable[File]:
812 """
813 Read-only property to access the file the line belongs to (:attr:`_parent`).
815 :returns: The file; ``None`` if the line belongs to no file.
816 """
817 return self._parent
819 @readonly
820 def LineNumber(self) -> int:
821 """
822 Read-only property to access the line number (:attr:`_lineNumber`).
824 :returns: The line number, counted from 1.
825 """
826 return self._lineNumber
828 @readonly
829 def FunctionName(self) -> Nullable[str]:
830 """
831 Read-only property to access the mangled name of the function the line belongs to (:attr:`_functionName`).
833 :returns: The function's name, or ``None`` for a line of inlined statements.
834 """
835 return self._functionName
837 @readonly
838 def Count(self) -> int:
839 """
840 Read-only property to access the number of times the line ran (:attr:`_count`).
842 :returns: The count.
843 """
844 return self._count
846 @readonly
847 def UnexecutedBlock(self) -> bool:
848 """
849 Read-only property to access whether a basic block of the line never ran (:attr:`_unexecutedBlock`).
851 A block only reached by exceptions doesn't count.
853 :returns: ``True``, if not every statement of the line ran.
854 """
855 return self._unexecutedBlock
857 @readonly
858 def BlockIDs(self) -> list[int]:
859 """
860 Read-only property to access the IDs of the basic blocks ending on this line (:attr:`_blockIDs`).
862 :returns: The blocks' IDs, unique within the function; empty in format 1.
863 """
864 return self._blockIDs
866 @readonly
867 def Branches(self) -> list[Branch]:
868 """
869 Read-only property to access the branches (:attr:`_branches`).
871 :returns: The branches; empty, if gcov ran without ``--branch-probabilities``.
872 """
873 return self._branches
875 @readonly
876 def Calls(self) -> list[Call]:
877 """
878 Read-only property to access the calls (:attr:`_calls`).
880 :returns: The calls; empty in format 1, or if gcov ran without ``--branch-probabilities``.
881 """
882 return self._calls
884 @readonly
885 def Conditions(self) -> list[Condition]:
886 """
887 Read-only property to access the conditions (:attr:`_conditions`).
889 :returns: The conditions; empty in format 1, or if gcov ran without ``--conditions``.
890 """
891 return self._conditions
894@export
895class Function(metaclass=ExtendedType, slots=True):
896 """
897 A ``function`` of a file: its names, its position, its basic blocks and how often it ran.
898 """
900 _parent: Nullable[File] #: The file the function belongs to.
901 _name: str #: Name of the function, mangled.
902 _demangledName: str #: Name of the function, demangled.
903 _startLine: int #: The function's first line.
904 _startColumn: int #: The function's first column.
905 _endLine: int #: The function's last line.
906 _endColumn: int #: The function's last column.
907 _blocks: int #: Number of basic blocks.
908 _blocksExecuted: int #: Number of executed basic blocks.
909 _executionCount: int #: Number of times the function ran.
911 def __init__(
912 self,
913 name: str,
914 demangledName: str,
915 startLine: int,
916 startColumn: int,
917 endLine: int,
918 endColumn: int,
919 blocks: int,
920 blocksExecuted: int,
921 executionCount: int,
922 *,
923 parent: Nullable[File] = None
924 ) -> None:
925 """
926 Initialize the function, and add it to the functions of its file.
928 :param name: Name of the function, mangled.
929 :param demangledName: Name of the function, demangled.
930 :param startLine: The function's first line.
931 :param startColumn: The function's first column.
932 :param endLine: The function's last line.
933 :param endColumn: The function's last column.
934 :param blocks: Number of basic blocks.
935 :param blocksExecuted: Number of executed basic blocks.
936 :param executionCount: Number of times the function ran.
937 :param parent: Optional, the file the function belongs to; the function is added to its functions by
938 :attr:`Name`. Default: ``None``.
939 :raises ValueError: If parameter ``name`` is ``None``.
940 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
941 :raises ValueError: If parameter ``name`` is empty.
942 :raises ValueError: If parameter ``demangledName`` is ``None``.
943 :raises TypeError: If parameter ``demangledName`` isn't of type :class:`str`.
944 :raises ValueError: If parameter ``demangledName`` is empty.
945 :raises ValueError: If parameter ``startLine`` is ``None``.
946 :raises TypeError: If parameter ``startLine`` isn't of type :class:`int`.
947 :raises ValueError: If parameter ``startLine`` is negative.
948 :raises ValueError: If parameter ``startColumn`` is ``None``.
949 :raises TypeError: If parameter ``startColumn`` isn't of type :class:`int`.
950 :raises ValueError: If parameter ``startColumn`` is negative.
951 :raises ValueError: If parameter ``endLine`` is ``None``.
952 :raises TypeError: If parameter ``endLine`` isn't of type :class:`int`.
953 :raises ValueError: If parameter ``endLine`` is negative.
954 :raises ValueError: If parameter ``endColumn`` is ``None``.
955 :raises TypeError: If parameter ``endColumn`` isn't of type :class:`int`.
956 :raises ValueError: If parameter ``endColumn`` is negative.
957 :raises ValueError: If parameter ``blocks`` is ``None``.
958 :raises TypeError: If parameter ``blocks`` isn't of type :class:`int`.
959 :raises ValueError: If parameter ``blocks`` is negative.
960 :raises ValueError: If parameter ``blocksExecuted`` is ``None``.
961 :raises TypeError: If parameter ``blocksExecuted`` isn't of type :class:`int`.
962 :raises ValueError: If parameter ``blocksExecuted`` is negative.
963 :raises ValueError: If parameter ``executionCount`` is ``None``.
964 :raises TypeError: If parameter ``executionCount`` isn't of type :class:`int`.
965 :raises ValueError: If parameter ``executionCount`` is negative.
966 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
967 :raises ValueError: If parameter ``parent`` contains a function of the same name already.
968 """
969 if name is None: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 raise ValueError("Parameter 'name' is None.")
971 elif not isinstance(name, str): 971 ↛ 972line 971 didn't jump to line 972 because the condition on line 971 was never true
972 ex = TypeError("Parameter 'name' is not of type 'str'.")
973 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
974 raise ex
975 elif name == "":
976 raise ValueError("Parameter 'name' is empty.")
978 if demangledName is None: 978 ↛ 979line 978 didn't jump to line 979 because the condition on line 978 was never true
979 raise ValueError("Parameter 'demangledName' is None.")
980 elif not isinstance(demangledName, str): 980 ↛ 981line 980 didn't jump to line 981 because the condition on line 980 was never true
981 ex = TypeError("Parameter 'demangledName' is not of type 'str'.")
982 ex.add_note(f"Got type '{getFullyQualifiedName(demangledName)}'.")
983 raise ex
984 elif demangledName == "": 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true
985 raise ValueError("Parameter 'demangledName' is empty.")
987 if startLine is None: 987 ↛ 988line 987 didn't jump to line 988 because the condition on line 987 was never true
988 raise ValueError("Parameter 'startLine' is None.")
989 elif not isinstance(startLine, int): 989 ↛ 990line 989 didn't jump to line 990 because the condition on line 989 was never true
990 ex = TypeError("Parameter 'startLine' is not of type 'int'.")
991 ex.add_note(f"Got type '{getFullyQualifiedName(startLine)}'.")
992 raise ex
993 elif startLine < 0: 993 ↛ 994line 993 didn't jump to line 994 because the condition on line 993 was never true
994 ex = ValueError("Parameter 'startLine' is negative.")
995 ex.add_note(f"Got value '{startLine}'.")
996 raise ex
998 if startColumn is None: 998 ↛ 999line 998 didn't jump to line 999 because the condition on line 998 was never true
999 raise ValueError("Parameter 'startColumn' is None.")
1000 elif not isinstance(startColumn, int): 1000 ↛ 1001line 1000 didn't jump to line 1001 because the condition on line 1000 was never true
1001 ex = TypeError("Parameter 'startColumn' is not of type 'int'.")
1002 ex.add_note(f"Got type '{getFullyQualifiedName(startColumn)}'.")
1003 raise ex
1004 elif startColumn < 0: 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true
1005 ex = ValueError("Parameter 'startColumn' is negative.")
1006 ex.add_note(f"Got value '{startColumn}'.")
1007 raise ex
1009 if endLine is None: 1009 ↛ 1010line 1009 didn't jump to line 1010 because the condition on line 1009 was never true
1010 raise ValueError("Parameter 'endLine' is None.")
1011 elif not isinstance(endLine, int): 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true
1012 ex = TypeError("Parameter 'endLine' is not of type 'int'.")
1013 ex.add_note(f"Got type '{getFullyQualifiedName(endLine)}'.")
1014 raise ex
1015 elif endLine < 0: 1015 ↛ 1016line 1015 didn't jump to line 1016 because the condition on line 1015 was never true
1016 ex = ValueError("Parameter 'endLine' is negative.")
1017 ex.add_note(f"Got value '{endLine}'.")
1018 raise ex
1020 if endColumn is None: 1020 ↛ 1021line 1020 didn't jump to line 1021 because the condition on line 1020 was never true
1021 raise ValueError("Parameter 'endColumn' is None.")
1022 elif not isinstance(endColumn, int): 1022 ↛ 1023line 1022 didn't jump to line 1023 because the condition on line 1022 was never true
1023 ex = TypeError("Parameter 'endColumn' is not of type 'int'.")
1024 ex.add_note(f"Got type '{getFullyQualifiedName(endColumn)}'.")
1025 raise ex
1026 elif endColumn < 0: 1026 ↛ 1027line 1026 didn't jump to line 1027 because the condition on line 1026 was never true
1027 ex = ValueError("Parameter 'endColumn' is negative.")
1028 ex.add_note(f"Got value '{endColumn}'.")
1029 raise ex
1031 if blocks is None: 1031 ↛ 1032line 1031 didn't jump to line 1032 because the condition on line 1031 was never true
1032 raise ValueError("Parameter 'blocks' is None.")
1033 elif not isinstance(blocks, int): 1033 ↛ 1034line 1033 didn't jump to line 1034 because the condition on line 1033 was never true
1034 ex = TypeError("Parameter 'blocks' is not of type 'int'.")
1035 ex.add_note(f"Got type '{getFullyQualifiedName(blocks)}'.")
1036 raise ex
1037 elif blocks < 0: 1037 ↛ 1038line 1037 didn't jump to line 1038 because the condition on line 1037 was never true
1038 ex = ValueError("Parameter 'blocks' is negative.")
1039 ex.add_note(f"Got value '{blocks}'.")
1040 raise ex
1042 if blocksExecuted is None: 1042 ↛ 1043line 1042 didn't jump to line 1043 because the condition on line 1042 was never true
1043 raise ValueError("Parameter 'blocksExecuted' is None.")
1044 elif not isinstance(blocksExecuted, int): 1044 ↛ 1045line 1044 didn't jump to line 1045 because the condition on line 1044 was never true
1045 ex = TypeError("Parameter 'blocksExecuted' is not of type 'int'.")
1046 ex.add_note(f"Got type '{getFullyQualifiedName(blocksExecuted)}'.")
1047 raise ex
1048 elif blocksExecuted < 0: 1048 ↛ 1049line 1048 didn't jump to line 1049 because the condition on line 1048 was never true
1049 ex = ValueError("Parameter 'blocksExecuted' is negative.")
1050 ex.add_note(f"Got value '{blocksExecuted}'.")
1051 raise ex
1053 if executionCount is None: 1053 ↛ 1054line 1053 didn't jump to line 1054 because the condition on line 1053 was never true
1054 raise ValueError("Parameter 'executionCount' is None.")
1055 elif not isinstance(executionCount, int): 1055 ↛ 1056line 1055 didn't jump to line 1056 because the condition on line 1055 was never true
1056 ex = TypeError("Parameter 'executionCount' is not of type 'int'.")
1057 ex.add_note(f"Got type '{getFullyQualifiedName(executionCount)}'.")
1058 raise ex
1059 elif executionCount < 0:
1060 ex = ValueError("Parameter 'executionCount' is negative.")
1061 ex.add_note(f"Got value '{executionCount}'.")
1062 raise ex
1064 if parent is not None:
1065 if not isinstance(parent, File):
1066 ex = TypeError("Parameter 'parent' is not of type 'File'.")
1067 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1068 raise ex
1069 elif name in parent._functions:
1070 raise ValueError(f"Parameter 'parent' contains function '{name}' already.")
1072 self._parent = parent
1073 self._name = name
1074 self._demangledName = demangledName
1075 self._startLine = startLine
1076 self._startColumn = startColumn
1077 self._endLine = endLine
1078 self._endColumn = endColumn
1079 self._blocks = blocks
1080 self._blocksExecuted = blocksExecuted
1081 self._executionCount = executionCount
1083 if parent is not None:
1084 parent._functions[self._name] = self
1086 @classmethod
1087 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
1088 """
1089 Parse a function from its JSON object.
1091 :param record: The JSON object of the function.
1092 :param parent: Optional, the file the function belongs to. Default: ``None``.
1093 :returns: The function.
1094 :raises ValueError: If parameter ``record`` is ``None``.
1095 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1096 """
1097 if record is None:
1098 raise ValueError("Parameter 'record' is None.")
1099 elif not isinstance(record, dict):
1100 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1101 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1102 raise ex
1104 return cls(
1105 record["name"],
1106 record["demangled_name"],
1107 record["start_line"],
1108 record["start_column"],
1109 record["end_line"],
1110 record["end_column"],
1111 record["blocks"],
1112 record["blocks_executed"],
1113 record["execution_count"],
1114 parent=parent
1115 )
1117 @readonly
1118 def Parent(self) -> Nullable[File]:
1119 """
1120 Read-only property to access the file the function belongs to (:attr:`_parent`).
1122 :returns: The file; ``None`` if the function belongs to no file.
1123 """
1124 return self._parent
1126 @readonly
1127 def Name(self) -> str:
1128 """
1129 Read-only property to access the mangled name of the function (:attr:`_name`).
1131 The method ``Containers::Stack::Pop()`` is mangled to ``_ZN10Containers5Stack3PopEv``, a C function keeps its name.
1133 :returns: The mangled name.
1134 """
1135 return self._name
1137 @readonly
1138 def DemangledName(self) -> str:
1139 """
1140 Read-only property to access the demangled name of the function (:attr:`_demangledName`).
1142 The demangled name of ``_ZN10Containers5Stack3PopEv`` is ``Containers::Stack::Pop()``.
1144 :returns: The demangled name.
1145 """
1146 return self._demangledName
1148 @readonly
1149 def StartLine(self) -> int:
1150 """
1151 Read-only property to access the function's first line (:attr:`_startLine`).
1153 :returns: The line number.
1154 """
1155 return self._startLine
1157 @readonly
1158 def StartColumn(self) -> int:
1159 """
1160 Read-only property to access the function's first column (:attr:`_startColumn`).
1162 :returns: The column number, counted from 1.
1163 """
1164 return self._startColumn
1166 @readonly
1167 def EndLine(self) -> int:
1168 """
1169 Read-only property to access the function's last line (:attr:`_endLine`).
1171 :returns: The line number.
1172 """
1173 return self._endLine
1175 @readonly
1176 def EndColumn(self) -> int:
1177 """
1178 Read-only property to access the function's last column (:attr:`_endColumn`).
1180 :returns: The column number, counted from 1.
1181 """
1182 return self._endColumn
1184 @readonly
1185 def Blocks(self) -> int:
1186 """
1187 Read-only property to access the number of basic blocks (:attr:`_blocks`).
1189 :returns: The number of blocks.
1190 """
1191 return self._blocks
1193 @readonly
1194 def BlocksExecuted(self) -> int:
1195 """
1196 Read-only property to access the number of executed basic blocks (:attr:`_blocksExecuted`).
1198 :returns: The number of executed blocks.
1199 """
1200 return self._blocksExecuted
1202 @readonly
1203 def ExecutionCount(self) -> int:
1204 """
1205 Read-only property to access the number of times the function ran (:attr:`_executionCount`).
1207 :returns: The count.
1208 """
1209 return self._executionCount
1212@export
1213class File(metaclass=ExtendedType, slots=True):
1214 """
1215 A source ``file`` of a data file: its functions and lines.
1217 A line several functions share - e.g. the instantiations of a template - is listed once per function.
1218 """
1220 _parent: Nullable[DataFile] #: The data file the file belongs to.
1221 _path: _Path #: The file's path, as the compiler named it.
1222 _functions: dict[str, Function] #: The functions, by mangled name.
1223 _lines: list[Line] #: The executable lines, in order; a line of several functions once per function.
1225 def __init__(self, path: Path, *, parent: Nullable[DataFile] = None) -> None:
1226 """
1227 Initialize the file, and add it to the files of its data file.
1229 Its functions and lines are added by creating them with this file as their parent.
1231 :param path: The file's path, as the compiler named it.
1232 :param parent: Optional, the data file the file belongs to; the file is added to its files by :attr:`Path`.
1233 Default: ``None``.
1234 :raises ValueError: If parameter ``path`` is ``None``.
1235 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1236 :raises TypeError: If parameter ``parent`` isn't of type :class:`DataFile`.
1237 :raises ValueError: If parameter ``parent`` contains a file of the same path already.
1238 """
1239 if path is None: 1239 ↛ 1240line 1239 didn't jump to line 1240 because the condition on line 1239 was never true
1240 raise ValueError("Parameter 'path' is None.")
1241 elif not isinstance(path, Path):
1242 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1243 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1244 raise ex
1246 if parent is not None:
1247 if not isinstance(parent, DataFile):
1248 ex = TypeError("Parameter 'parent' is not of type 'DataFile'.")
1249 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1250 raise ex
1251 elif path in parent._files:
1252 raise ValueError(f"Parameter 'parent' contains file '{path.as_posix()}' already.")
1254 self._parent = parent
1255 self._path = path
1256 self._functions = {}
1257 self._lines = []
1259 if parent is not None:
1260 parent._files[self._path] = self
1262 @classmethod
1263 def Parse(cls, record: dict[str, Any], *, parent: Nullable[DataFile] = None) -> Self:
1264 """
1265 Parse a file, its functions and lines from its JSON object.
1267 A backslash in the path - as a report written on Windows has them - separates directories.
1269 :param record: The JSON object of the file.
1270 :param parent: Optional, the data file the file belongs to. Default: ``None``.
1271 :returns: The file.
1272 :raises ValueError: If parameter ``record`` is ``None``.
1273 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1274 :raises CodeCoverageError: If the file names a function twice.
1275 """
1276 if record is None:
1277 raise ValueError("Parameter 'record' is None.")
1278 elif not isinstance(record, dict):
1279 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1280 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1281 raise ex
1283 file = cls(Path(record["file"].replace("\\", "/")), parent=parent)
1285 for function in record["functions"]:
1286 if (name := function["name"]) in file._functions:
1287 raise CodeCoverageError(f"gcov source file '{file._path.as_posix()}' names function '{name}' twice.")
1289 Function.Parse(function, parent=file)
1291 for line in record["lines"]:
1292 Line.Parse(line, parent=file)
1294 return file
1296 @readonly
1297 def Parent(self) -> Nullable[DataFile]:
1298 """
1299 Read-only property to access the data file the file belongs to (:attr:`_parent`).
1301 :returns: The data file; ``None`` if the file belongs to no data file.
1302 """
1303 return self._parent
1305 @readonly
1306 def Path(self) -> Path:
1307 """
1308 Read-only property to access the file's path (:attr:`_path`).
1310 :returns: The path, relative to the directory the compiler ran in, unless absolute.
1311 """
1312 return self._path
1314 @readonly
1315 def Functions(self) -> dict[str, Function]:
1316 """
1317 Read-only property to access the functions (:attr:`_functions`).
1319 :returns: The functions, by mangled name.
1320 """
1321 return self._functions
1323 @readonly
1324 def Lines(self) -> list[Line]:
1325 """
1326 Read-only property to access the executable lines (:attr:`_lines`).
1328 :returns: The lines; a line several functions share is listed once per function.
1329 """
1330 return self._lines
1333@export
1334class DataFile(metaclass=ExtendedType, slots=True):
1335 """
1336 The coverage measured in a data file (GCDA): the versions of the format and of GCC, and the source files.
1337 """
1339 _parent: Nullable[Coverage] #: The report the data file belongs to.
1340 _path: _Path #: Path of the data file, as gcov was called with it.
1341 _formatVersion: _FormatVersion #: Version of the report format.
1342 _gccVersion: SemanticVersion #: Version of GCC.
1343 _currentWorkingDirectory: Nullable[_Path] #: The directory the compiler ran in, if the report says.
1344 _files: dict[_Path, File] #: The source files, by path.
1346 def __init__(
1347 self,
1348 path: Path,
1349 formatVersion: FormatVersion,
1350 gccVersion: SemanticVersion,
1351 currentWorkingDirectory: Nullable[Path] = None,
1352 *,
1353 parent: Nullable[Coverage] = None
1354 ) -> None:
1355 """
1356 Initialize the data file, and add it to the data files of its report.
1358 Its source files are added by creating them with this data file as their parent.
1360 :param path: Path of the data file, as gcov was called with it.
1361 :param formatVersion: Version of the report format.
1362 :param gccVersion: Version of GCC.
1363 :param currentWorkingDirectory: Optional, the directory the compiler ran in. Default: ``None``.
1364 :param parent: Optional, the report the data file belongs to; the data file is appended to its
1365 data files. Default: ``None``.
1366 :raises ValueError: If parameter ``path`` is ``None``.
1367 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1368 :raises ValueError: If parameter ``formatVersion`` is ``None``.
1369 :raises TypeError: If parameter ``formatVersion`` isn't of type :class:`FormatVersion`.
1370 :raises ValueError: If parameter ``gccVersion`` is ``None``.
1371 :raises TypeError: If parameter ``gccVersion`` isn't of type
1372 :class:`~pyTooling.Versioning.SemanticVersion`.
1373 :raises TypeError: If parameter ``currentWorkingDirectory`` isn't of type :class:`~pathlib.Path`.
1374 :raises TypeError: If parameter ``parent`` isn't of type :class:`Coverage`.
1375 """
1376 if path is None: 1376 ↛ 1377line 1376 didn't jump to line 1377 because the condition on line 1376 was never true
1377 raise ValueError("Parameter 'path' is None.")
1378 elif not isinstance(path, Path): 1378 ↛ 1379line 1378 didn't jump to line 1379 because the condition on line 1378 was never true
1379 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1380 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1381 raise ex
1383 if formatVersion is None:
1384 raise ValueError("Parameter 'formatVersion' is None.")
1385 elif not isinstance(formatVersion, FormatVersion):
1386 ex = TypeError("Parameter 'formatVersion' is not of type 'FormatVersion'.")
1387 ex.add_note(f"Got type '{getFullyQualifiedName(formatVersion)}'.")
1388 raise ex
1390 if gccVersion is None: 1390 ↛ 1391line 1390 didn't jump to line 1391 because the condition on line 1390 was never true
1391 raise ValueError("Parameter 'gccVersion' is None.")
1392 elif not isinstance(gccVersion, SemanticVersion):
1393 ex = TypeError("Parameter 'gccVersion' is not of type 'SemanticVersion'.")
1394 ex.add_note(f"Got type '{getFullyQualifiedName(gccVersion)}'.")
1395 raise ex
1397 if currentWorkingDirectory is not None and not isinstance(currentWorkingDirectory, Path): 1397 ↛ 1398line 1397 didn't jump to line 1398 because the condition on line 1397 was never true
1398 ex = TypeError("Parameter 'currentWorkingDirectory' is not of type 'Path'.")
1399 ex.add_note(f"Got type '{getFullyQualifiedName(currentWorkingDirectory)}'.")
1400 raise ex
1402 if parent is not None and not isinstance(parent, Coverage):
1403 ex = TypeError("Parameter 'parent' is not of type 'Coverage'.")
1404 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1405 raise ex
1407 self._parent = parent
1408 self._path = path
1409 self._formatVersion = formatVersion
1410 self._gccVersion = gccVersion
1411 self._currentWorkingDirectory = currentWorkingDirectory
1412 self._files = {}
1414 if parent is not None:
1415 parent._dataFiles.append(self)
1417 @classmethod
1418 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Coverage] = None) -> Self:
1419 """
1420 Parse a data file and its source files from its JSON object.
1422 The format version is converted by :meth:`FormatVersion.Parse`; a development build of GCC states its date and
1423 phase behind its version, e.g. ``15.0.1 20250418 (experimental)``. A backslash in a path - as a report written on
1424 Windows has them - separates directories.
1426 :param record: The JSON object of the data file: a report's root object.
1427 :param parent: Optional, the report the data file belongs to. Default: ``None``.
1428 :returns: The data file.
1429 :raises ValueError: If parameter ``record`` is ``None``.
1430 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1431 :raises CodeCoverageError: If the data file names a source file twice.
1432 """
1433 if record is None:
1434 raise ValueError("Parameter 'record' is None.")
1435 elif not isinstance(record, dict):
1436 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1437 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1438 raise ex
1440 directory = record.get("current_working_directory")
1441 dataFile = cls(
1442 Path(record["data_file"].replace("\\", "/")),
1443 FormatVersion.Parse(record["format_version"]),
1444 SemanticVersion.Parse(record["gcc_version"].split(" ", 1)[0]),
1445 Path(directory.replace("\\", "/")) if directory is not None else None,
1446 parent=parent
1447 )
1449 for file in record["files"]:
1450 if (path := Path(file["file"].replace("\\", "/"))) in dataFile._files:
1451 raise CodeCoverageError(
1452 f"gcov data file '{dataFile._path.as_posix()}' names source file '{path.as_posix()}' twice."
1453 )
1455 File.Parse(file, parent=dataFile)
1457 return dataFile
1459 @readonly
1460 def Parent(self) -> Nullable[Coverage]:
1461 """
1462 Read-only property to access the report the data file belongs to (:attr:`_parent`).
1464 :returns: The report; ``None`` if the data file belongs to no report.
1465 """
1466 return self._parent
1468 @readonly
1469 def Path(self) -> Path:
1470 """
1471 Read-only property to access the path of the data file (:attr:`_path`).
1473 It is the path gcov was called with, e.g. ``main.c`` for ``gcov --json-format main.c``.
1475 :returns: The path, as gcov was called with it.
1476 """
1477 return self._path
1479 @readonly
1480 def FormatVersion(self) -> FormatVersion:
1481 """
1482 Read-only property to access the version of the report format (:attr:`_formatVersion`).
1484 :returns: The format version.
1485 """
1486 return self._formatVersion
1488 @readonly
1489 def GCCVersion(self) -> SemanticVersion:
1490 """
1491 Read-only property to access the version of GCC (:attr:`_gccVersion`).
1493 A development build of GCC states its date and phase behind the version, e.g. ``15.0.1 20250418 (experimental)``;
1494 they aren't kept.
1496 :returns: The version.
1497 """
1498 return self._gccVersion
1500 @readonly
1501 def CurrentWorkingDirectory(self) -> Nullable[Path]:
1502 """
1503 Read-only property to access the directory the compiler ran in (:attr:`_currentWorkingDirectory`).
1505 :returns: The directory, on the machine the code was compiled on, or ``None`` if the report doesn't say.
1506 """
1507 return self._currentWorkingDirectory
1509 @readonly
1510 def Files(self) -> dict[Path, File]:
1511 """
1512 Read-only property to access the source files (:attr:`_files`).
1514 :returns: The files, by path.
1515 """
1516 return self._files
1519@export
1520class Coverage(metaclass=ExtendedType, mixin=True):
1521 """
1522 The content of a report file: the data files.
1524 The root of the format's model: a data file names it as its parent.
1525 """
1527 _dataFiles: list[DataFile] #: The data files, in the order the report lists them.
1529 def __init__(self) -> None:
1530 """
1531 Initialize an empty report.
1532 """
1533 self._dataFiles = []
1535 @readonly
1536 def DataFiles(self) -> list[DataFile]:
1537 """
1538 Read-only property to access the data files (:attr:`_dataFiles`).
1540 :returns: The data files: one of a :file:`*.gcov.json.gz` file; one per line of gcov's standard output.
1541 """
1542 return self._dataFiles
1545@export
1546class Document(cc_Document, Coverage):
1547 """
1548 A gcov JSON code coverage report: read into the format's model, and converted to the common model.
1549 """
1551 _jsonDocuments: Nullable[list[dict[str, Any]]] #: The parsed and validated JSON objects, after :meth:`Analyze`.
1553 def __init__(self, jsonReportFile: Path, analyzeAndConvert: bool = False) -> None:
1554 """
1555 Initialize the report, and optionally read it.
1557 :param jsonReportFile: Path to the JSON file, gzip-compressed or plain.
1558 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
1559 """
1560 super().__init__(jsonReportFile)
1561 Coverage.__init__(self)
1563 self._jsonDocuments = None
1565 if analyzeAndConvert:
1566 self.Analyze()
1567 self.Convert()
1569 def Analyze(self) -> None:
1570 """
1571 Read the file - decompressing it, if gzip-compressed -, parse its JSON objects and validate each against the JSON
1572 Schema of the format version it states (:data:`SCHEMAS`).
1574 :raises CodeCoverageError: If the file doesn't exist.
1575 :raises CodeCoverageError: If the file can't be read.
1576 :raises CodeCoverageError: If the file is gzip-compressed, but corrupt.
1577 :raises CodeCoverageError: If the file isn't valid JSON.
1578 :raises CodeCoverageError: If the file holds no JSON object.
1579 :raises CodeCoverageError: If a JSON object states no supported format version.
1580 :raises CodeCoverageError: If a JSON Schema can't be read.
1581 :raises CodeCoverageError: If a JSON object isn't valid according to the JSON Schema of its format version.
1582 """
1583 if not self._path.exists():
1584 raise CodeCoverageError(f"gcov report file '{self._path}' does not exist.") \
1585 from FileNotFoundError(f"File '{self._path}' not found.")
1587 with Stopwatch() as sw:
1588 try:
1589 content = self._path.read_bytes()
1590 except OSError as ex:
1591 raise CodeCoverageError(f"Couldn't read gcov report file '{self._path}'.") from ex
1593 if content[:2] == b"\x1f\x8b":
1594 try:
1595 content = decompress(content)
1596 except (BadGzipFile, EOFError, ZLibError) as ex:
1597 raise CodeCoverageError(f"gzip error in gcov report file '{self._path}'.") from ex
1599 # gcov's standard output holds one JSON object per data file
1600 jsonDocuments = []
1601 try:
1602 text = content.decode("utf-8")
1603 decoder = JSONDecoder()
1604 nonWhitespace = re_compile(r"\S")
1605 position = 0
1606 while (match := nonWhitespace.search(text, position)) is not None:
1607 jsonDocument, position = decoder.raw_decode(text, match.start())
1608 jsonDocuments.append(jsonDocument)
1609 except (UnicodeDecodeError, JSONDecodeError) as ex:
1610 raise CodeCoverageError(f"JSON syntax error in gcov report file '{self._path}'.") from ex
1612 if len(jsonDocuments) == 0:
1613 raise CodeCoverageError(f"gcov report file '{self._path}' holds no JSON object.")
1615 # a note's path is prefixed by the object's index, if the file holds several
1616 validators: dict[FormatVersion, Draft202012Validator] = {}
1617 for index, jsonDocument in enumerate(jsonDocuments):
1618 prefix = f"[{index}]" if len(jsonDocuments) > 1 else ""
1620 version = jsonDocument.get("format_version") if isinstance(jsonDocument, dict) else None
1621 try:
1622 formatVersion = FormatVersion.Parse(version)
1623 except (ValueError, TypeError) as cause:
1624 ex = CodeCoverageError(f"gcov report file '{self._path}' states an unsupported format version.")
1625 got = f"value '{version}'" if version is not None else "no value"
1626 ex.add_note(f"Got {got} at '{prefix}/format_version'.")
1627 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in FormatVersion)}.")
1628 raise ex from cause
1630 schemaFile = SCHEMAS[formatVersion]
1631 if (validator := validators.get(formatVersion)) is None:
1632 try:
1633 schema = loads(readResourceFile(Resources, schemaFile))
1634 except (ToolingException, JSONDecodeError) as ex:
1635 raise CodeCoverageError(f"Couldn't read JSON Schema '{schemaFile}' from package resources.") from ex
1637 validator = validators[formatVersion] = Draft202012Validator(schema)
1639 errors = sorted(validator.iter_errors(jsonDocument), key=lambda error: list(error.path))
1640 if len(errors) > 0:
1641 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{schemaFile}'.")
1642 for error in errors:
1643 ex.add_note(f"{prefix}/{'/'.join(str(part) for part in error.path)}: {error.message}")
1644 raise ex
1646 self._jsonDocuments = jsonDocuments
1648 self._analysisDuration = sw.Duration
1650 def Convert(self) -> None:
1651 """
1652 Convert the parsed and validated JSON objects to the format's model.
1654 :raises CodeCoverageError: If the JSON file was not analyzed before. |br|
1655 Call 'Document.Analyze()' or create the document using
1656 'Document(path, analyzeAndConvert=True)'.
1657 :raises CodeCoverageError: If a data file names a source file twice.
1658 :raises CodeCoverageError: If a source file names a function twice.
1659 """
1660 if self._jsonDocuments is None:
1661 ex = CodeCoverageError(f"gcov report file '{self._path}' needs to be read and analyzed by a JSON parser.")
1662 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
1663 raise ex
1665 with Stopwatch() as sw:
1666 for jsonDocument in self._jsonDocuments:
1667 DataFile.Parse(jsonDocument, parent=self)
1669 self._conversionDuration = sw.Duration
1671 def ToCoverageSummary(self) -> CoverageSummary:
1672 """
1673 Convert the format's model to the common model, and aggregate it.
1675 A source file several data files state - e.g. a header - becomes one file: the counts of its lines and branches,
1676 and of its functions, are added. The directories the compiler ran in become the source directories.
1678 :returns: The report's root of the common model, named after the report file without the
1679 extensions ``.gcov``, ``.json`` and ``.gz``.
1680 :raises CodeCoverageError: If a file's path runs through another file.
1681 """
1682 name = self._path.name.removesuffix(".gz").removesuffix(".json").removesuffix(".gcov")
1683 directories = {
1684 dataFile._currentWorkingDirectory for dataFile in self._dataFiles if dataFile._currentWorkingDirectory is not None
1685 }
1686 summary = CoverageSummary(name if name != "" else self._path.name, sourceDirectories=sorted(directories))
1688 # per file of the common model: the counts of its lines, of its lines' branches, and of its functions
1689 lineCounts: dict[cc_File, dict[int, int]] = {}
1690 branchCounts: dict[cc_File, dict[int, dict[tuple[Nullable[str], int, Nullable[int], Nullable[int]], int]]] = {}
1691 functionCounts: dict[cc_File, dict[str, tuple[int, int, int]]] = {}
1692 for dataFile in self._dataFiles:
1693 for file in dataFile._files.values():
1694 commonFile = summary.GetOrAddFile(file._path)
1695 counts = lineCounts.setdefault(commonFile, {})
1696 branches = branchCounts.setdefault(commonFile, {})
1697 functions = functionCounts.setdefault(commonFile, {})
1699 for line in file._lines:
1700 counts[line._lineNumber] = counts.get(line._lineNumber, 0) + line._count
1701 lineBranches = branches.setdefault(line._lineNumber, {})
1702 for index, branch in enumerate(line._branches):
1703 key = (line._functionName, index, branch._sourceBlockID, branch._destinationBlockID)
1704 lineBranches[key] = lineBranches.get(key, 0) + branch._count
1706 for function in file._functions.values():
1707 startLine, endLine, count = functions.get(
1708 function._demangledName, (function._startLine, function._endLine, 0)
1709 )
1710 functions[function._demangledName] = (startLine, endLine, count + function._executionCount)
1712 for commonFile, counts in lineCounts.items():
1713 for number in sorted(counts):
1714 lineBranches = branchCounts[commonFile][number]
1715 if counts[number] == 0:
1716 status = LineCoverageStatus.Uncovered
1717 elif 0 in lineBranches.values():
1718 status = LineCoverageStatus.PartiallyCovered
1719 else:
1720 status = LineCoverageStatus.Covered
1722 commonLine = cc_Line(number, status, counts[number], parent=commonFile)
1723 for count in lineBranches.values():
1724 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=commonLine)
1726 lines = commonFile._lines
1727 lastLineNumber = commonFile._lastLineNumber
1728 sourceFile = cc_SourceFile(
1729 commonFile.Path.as_posix(),
1730 file=commonFile,
1731 startLine=next(commonFile.IterateLines(), None),
1732 endLine=lines[lastLineNumber] if lastLineNumber > 0 else None,
1733 parent=summary
1734 )
1736 for functionName, (startLine, endLine, count) in functionCounts[commonFile].items():
1737 numbers = [number for number in range(startLine, min(endLine, lastLineNumber) + 1) if lines[number] is not None]
1738 cc_Function(
1739 functionName,
1740 file=commonFile,
1741 startLine=lines[numbers[0]] if len(numbers) > 0 else None,
1742 endLine=lines[numbers[-1]] if len(numbers) > 0 else None,
1743 status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered,
1744 coverageCount=count,
1745 parent=sourceFile
1746 )
1748 summary.Aggregate()
1749 return summary