Coverage for pyEDAA/Reports/CodeCoverage/Gcov.py: 82%
802 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
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 pyTooling.Common import getFullyQualifiedName, readResourceFile
79from pyTooling.Decorators import export, readonly
80from pyTooling.Exceptions import ToolingException
81from pyTooling.MetaClasses import ExtendedType
82from pyTooling.Stopwatch import Stopwatch
83from pyTooling.Versioning import SemanticVersion
85from pyEDAA.Reports import Resources
86from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, CodeCoverageError, CoverageSummary, \
87 Document as cc_Document, File as cc_File, Function as cc_Function, \
88 Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile
91__all__ = ["SCHEMAS"]
93# A class with a property named like a class - ``Path`` - can't name that class in the annotation of a field: the class
94# body's namespace, where annotations are evaluated, binds the name to the property.
95_Path = Path
98@export
99class FormatVersion(IntEnum):
100 """
101 Version of the gcov JSON format, as a report's ``format_version`` states it.
103 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
104 a branch, and the calls and conditions of a line; GCC 15 adds the prime paths of a function to format 2.
105 """
107 Version1 = 1 #: Format 1, written by GCC 9 to 13.
108 Version2 = 2 #: Format 2, written by GCC 14 and later.
110 @classmethod
111 def Parse(cls, value: str) -> Self:
112 """
113 Convert the version, as gcov states it, to the member of that version.
115 gcov states the version as a string, e.g. ``"2"``.
117 :param value: The version, as a report's ``format_version`` states it.
118 :returns: The member of that version.
119 :raises ValueError: If parameter ``value`` is ``None``.
120 :raises TypeError: If parameter ``value`` isn't of type :class:`str`.
121 :raises ValueError: If parameter ``value`` isn't a supported format version.
122 """
123 if value is None:
124 raise ValueError("Parameter 'value' is None.")
125 elif not isinstance(value, str):
126 ex = TypeError("Parameter 'value' is not of type 'str'.")
127 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
128 raise ex
129 elif value not in [str(member.value) for member in cls]:
130 ex = ValueError("Parameter 'value' is not a supported gcov JSON format version.")
131 ex.add_note(f"Got value '{value}'.")
132 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in cls)}.")
133 raise ex
135 return cls(int(value))
138# Like '_Path': DataFile has a property named 'FormatVersion'.
139_FormatVersion = FormatVersion
141SCHEMAS: dict[FormatVersion, str] = {
142 FormatVersion.Version1: "Gcov-1.schema.json",
143 FormatVersion.Version2: "Gcov-2.schema.json"
144} #: Per format version, the JSON Schema a JSON object of that version is validated against.
147@export
148class Branch(metaclass=ExtendedType, slots=True):
149 """
150 A ``branch`` of a line: an edge from a basic block to another, and how often it was taken.
151 """
153 _parent: Nullable[Line] #: The line the branch belongs to.
154 _count: int #: Number of times the branch was taken.
155 _throw: bool #: Whether the branch is taken by an exception.
156 _fallthrough: bool #: Whether the branch falls through.
157 _sourceBlockID: Nullable[int] #: ID of the basic block the branch starts at, in format 2.
158 _destinationBlockID: Nullable[int] #: ID of the basic block the branch leads to, in format 2.
160 def __init__(
161 self,
162 count: int,
163 throw: bool,
164 fallthrough: bool,
165 sourceBlockID: Nullable[int] = None,
166 destinationBlockID: Nullable[int] = None,
167 *,
168 parent: Nullable[Line] = None
169 ) -> None:
170 """
171 Initialize the branch, and add it to the branches of its line.
173 :param count: Number of times the branch was taken.
174 :param throw: Whether the branch is taken by an exception.
175 :param fallthrough: Whether the branch falls through.
176 :param sourceBlockID: Optional, ID of the basic block the branch starts at. Default: ``None``.
177 :param destinationBlockID: Optional, ID of the basic block the branch leads to. Default: ``None``.
178 :param parent: Optional, the line the branch belongs to; the branch is appended to its branches.
179 Default: ``None``.
180 :raises ValueError: If parameter ``count`` is ``None``.
181 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
182 :raises ValueError: If parameter ``count`` is negative.
183 :raises ValueError: If parameter ``throw`` is ``None``.
184 :raises TypeError: If parameter ``throw`` isn't of type :class:`bool`.
185 :raises ValueError: If parameter ``fallthrough`` is ``None``.
186 :raises TypeError: If parameter ``fallthrough`` isn't of type :class:`bool`.
187 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`.
188 :raises ValueError: If parameter ``sourceBlockID`` is negative.
189 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`.
190 :raises ValueError: If parameter ``destinationBlockID`` is negative.
191 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
192 """
193 if count is None: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 raise ValueError("Parameter 'count' is None.")
195 elif not isinstance(count, int): 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 ex = TypeError("Parameter 'count' is not of type 'int'.")
197 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
198 raise ex
199 elif count < 0: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 ex = ValueError("Parameter 'count' is negative.")
201 ex.add_note(f"Got value '{count}'.")
202 raise ex
204 if throw is None:
205 raise ValueError("Parameter 'throw' is None.")
206 elif not isinstance(throw, bool):
207 ex = TypeError("Parameter 'throw' is not of type 'bool'.")
208 ex.add_note(f"Got type '{getFullyQualifiedName(throw)}'.")
209 raise ex
211 if fallthrough is None: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 raise ValueError("Parameter 'fallthrough' is None.")
213 elif not isinstance(fallthrough, bool): 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 ex = TypeError("Parameter 'fallthrough' is not of type 'bool'.")
215 ex.add_note(f"Got type '{getFullyQualifiedName(fallthrough)}'.")
216 raise ex
218 if sourceBlockID is not None:
219 if not isinstance(sourceBlockID, int): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.")
221 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.")
222 raise ex
223 elif sourceBlockID < 0:
224 ex = ValueError("Parameter 'sourceBlockID' is negative.")
225 ex.add_note(f"Got value '{sourceBlockID}'.")
226 raise ex
228 if destinationBlockID is not None:
229 if not isinstance(destinationBlockID, int): 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.")
231 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.")
232 raise ex
233 elif destinationBlockID < 0: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true
234 ex = ValueError("Parameter 'destinationBlockID' is negative.")
235 ex.add_note(f"Got value '{destinationBlockID}'.")
236 raise ex
238 if parent is not None and not isinstance(parent, Line):
239 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
240 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
241 raise ex
243 self._parent = parent
244 self._count = count
245 self._throw = throw
246 self._fallthrough = fallthrough
247 self._sourceBlockID = sourceBlockID
248 self._destinationBlockID = destinationBlockID
250 if parent is not None:
251 parent._branches.append(self)
253 @classmethod
254 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
255 """
256 Parse a branch from its JSON object.
258 :param record: The JSON object of the branch.
259 :param parent: Optional, the line the branch belongs to. Default: ``None``.
260 :returns: The branch.
261 :raises ValueError: If parameter ``record`` is ``None``.
262 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
263 """
264 if record is None:
265 raise ValueError("Parameter 'record' is None.")
266 elif not isinstance(record, dict):
267 ex = TypeError("Parameter 'record' is not of type 'dict'.")
268 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
269 raise ex
271 return cls(
272 record["count"],
273 record["throw"],
274 record["fallthrough"],
275 record.get("source_block_id"),
276 record.get("destination_block_id"),
277 parent=parent
278 )
280 @readonly
281 def Parent(self) -> Nullable[Line]:
282 """
283 Read-only property to access the line the branch belongs to (:attr:`_parent`).
285 :returns: The line; ``None`` if the branch belongs to no line.
286 """
287 return self._parent
289 @readonly
290 def Count(self) -> int:
291 """
292 Read-only property to access the number of times the branch was taken (:attr:`_count`).
294 :returns: The count.
295 """
296 return self._count
298 @readonly
299 def Throw(self) -> bool:
300 """
301 Read-only property to access whether the branch is taken by an exception (:attr:`_throw`).
303 :returns: ``True``, if the branch is exceptional.
304 """
305 return self._throw
307 @readonly
308 def Fallthrough(self) -> bool:
309 """
310 Read-only property to access whether the branch falls through (:attr:`_fallthrough`).
312 :returns: ``True``, if the branch falls through.
313 """
314 return self._fallthrough
316 @readonly
317 def SourceBlockID(self) -> Nullable[int]:
318 """
319 Read-only property to access the ID of the basic block the branch starts at (:attr:`_sourceBlockID`).
321 :returns: The block's ID, unique within its function; ``None`` in format 1.
322 """
323 return self._sourceBlockID
325 @readonly
326 def DestinationBlockID(self) -> Nullable[int]:
327 """
328 Read-only property to access the ID of the basic block the branch leads to (:attr:`_destinationBlockID`).
330 :returns: The block's ID, unique within its function; ``None`` in format 1.
331 """
332 return self._destinationBlockID
335@export
336class Call(metaclass=ExtendedType, slots=True):
337 """
338 A ``call`` of a line - in format 2 -: from a basic block to the block continuing after the return.
339 """
341 _parent: Nullable[Line] #: The line the call belongs to.
342 _sourceBlockID: int #: ID of the basic block the call is in.
343 _destinationBlockID: int #: ID of the basic block continuing after the return.
344 _returned: int #: Number of times the call returned.
346 def __init__(
347 self,
348 sourceBlockID: int,
349 destinationBlockID: int,
350 returned: int,
351 *,
352 parent: Nullable[Line] = None
353 ) -> None:
354 """
355 Initialize the call, and add it to the calls of its line.
357 :param sourceBlockID: ID of the basic block the call is in.
358 :param destinationBlockID: ID of the basic block continuing after the return.
359 :param returned: Number of times the call returned.
360 :param parent: Optional, the line the call belongs to; the call is appended to its calls.
361 Default: ``None``.
362 :raises ValueError: If parameter ``sourceBlockID`` is ``None``.
363 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`.
364 :raises ValueError: If parameter ``sourceBlockID`` is negative.
365 :raises ValueError: If parameter ``destinationBlockID`` is ``None``.
366 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`.
367 :raises ValueError: If parameter ``destinationBlockID`` is negative.
368 :raises ValueError: If parameter ``returned`` is ``None``.
369 :raises TypeError: If parameter ``returned`` isn't of type :class:`int`.
370 :raises ValueError: If parameter ``returned`` is negative.
371 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
372 """
373 if sourceBlockID is None: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true
374 raise ValueError("Parameter 'sourceBlockID' is None.")
375 elif not isinstance(sourceBlockID, int): 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true
376 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.")
377 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.")
378 raise ex
379 elif sourceBlockID < 0: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 ex = ValueError("Parameter 'sourceBlockID' is negative.")
381 ex.add_note(f"Got value '{sourceBlockID}'.")
382 raise ex
384 if destinationBlockID is None: 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true
385 raise ValueError("Parameter 'destinationBlockID' is None.")
386 elif not isinstance(destinationBlockID, int): 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true
387 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.")
388 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.")
389 raise ex
390 elif destinationBlockID < 0: 390 ↛ 391line 390 didn't jump to line 391 because the condition on line 390 was never true
391 ex = ValueError("Parameter 'destinationBlockID' is negative.")
392 ex.add_note(f"Got value '{destinationBlockID}'.")
393 raise ex
395 if returned is None:
396 raise ValueError("Parameter 'returned' is None.")
397 elif not isinstance(returned, int): 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true
398 ex = TypeError("Parameter 'returned' is not of type 'int'.")
399 ex.add_note(f"Got type '{getFullyQualifiedName(returned)}'.")
400 raise ex
401 elif returned < 0: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true
402 ex = ValueError("Parameter 'returned' is negative.")
403 ex.add_note(f"Got value '{returned}'.")
404 raise ex
406 if parent is not None and not isinstance(parent, Line):
407 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
408 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
409 raise ex
411 self._parent = parent
412 self._sourceBlockID = sourceBlockID
413 self._destinationBlockID = destinationBlockID
414 self._returned = returned
416 if parent is not None:
417 parent._calls.append(self)
419 @classmethod
420 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
421 """
422 Parse a call from its JSON object.
424 :param record: The JSON object of the call.
425 :param parent: Optional, the line the call belongs to. Default: ``None``.
426 :returns: The call.
427 :raises ValueError: If parameter ``record`` is ``None``.
428 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
429 """
430 if record is None:
431 raise ValueError("Parameter 'record' is None.")
432 elif not isinstance(record, dict):
433 ex = TypeError("Parameter 'record' is not of type 'dict'.")
434 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
435 raise ex
437 return cls(record["source_block_id"], record["destination_block_id"], record["returned"], parent=parent)
439 @readonly
440 def Parent(self) -> Nullable[Line]:
441 """
442 Read-only property to access the line the call belongs to (:attr:`_parent`).
444 :returns: The line; ``None`` if the call belongs to no line.
445 """
446 return self._parent
448 @readonly
449 def SourceBlockID(self) -> int:
450 """
451 Read-only property to access the ID of the basic block the call is in (:attr:`_sourceBlockID`).
453 :returns: The block's ID, unique within its function.
454 """
455 return self._sourceBlockID
457 @readonly
458 def DestinationBlockID(self) -> int:
459 """
460 Read-only property to access the ID of the basic block continuing after the return (:attr:`_destinationBlockID`).
462 :returns: The block's ID, unique within its function.
463 """
464 return self._destinationBlockID
466 @readonly
467 def Returned(self) -> int:
468 """
469 Read-only property to access the number of times the call returned (:attr:`_returned`).
471 :returns: The count; how often the call was made is the line's count.
472 """
473 return self._returned
476@export
477class Condition(metaclass=ExtendedType, slots=True):
478 """
479 A ``condition`` of a line - in format 2, with ``gcc -fcondition-coverage`` -: the condition outcomes of a boolean
480 expression (MC/DC).
481 """
483 _parent: Nullable[Line] #: The line the condition belongs to.
484 _count: int #: Number of condition outcomes: twice the number of terms.
485 _covered: int #: Number of covered condition outcomes.
486 _notCoveredTrue: list[int] #: The terms, by index, never seen as true.
487 _notCoveredFalse: list[int] #: The terms, by index, never seen as false.
489 def __init__(
490 self,
491 count: int,
492 covered: int,
493 notCoveredTrue: Nullable[Iterable[int]] = None,
494 notCoveredFalse: Nullable[Iterable[int]] = None,
495 *,
496 parent: Nullable[Line] = None
497 ) -> None:
498 """
499 Initialize the condition, and add it to the conditions of its line.
501 :param count: Number of condition outcomes: twice the number of terms.
502 :param covered: Number of covered condition outcomes.
503 :param notCoveredTrue: Optional, the terms, by index, never seen as true. Default: none.
504 :param notCoveredFalse: Optional, the terms, by index, never seen as false. Default: none.
505 :param parent: Optional, the line the condition belongs to; the condition is appended to its conditions.
506 Default: ``None``.
507 :raises ValueError: If parameter ``count`` is ``None``.
508 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
509 :raises ValueError: If parameter ``count`` is negative.
510 :raises ValueError: If parameter ``covered`` is ``None``.
511 :raises TypeError: If parameter ``covered`` isn't of type :class:`int`.
512 :raises ValueError: If parameter ``covered`` is negative.
513 :raises TypeError: If parameter ``notCoveredTrue`` isn't iterable.
514 :raises TypeError: If parameter ``notCoveredTrue`` contains an element not of type :class:`int`.
515 :raises TypeError: If parameter ``notCoveredFalse`` isn't iterable.
516 :raises TypeError: If parameter ``notCoveredFalse`` contains an element not of type :class:`int`.
517 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`.
518 """
519 if count is None: 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true
520 raise ValueError("Parameter 'count' is None.")
521 elif not isinstance(count, int): 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true
522 ex = TypeError("Parameter 'count' is not of type 'int'.")
523 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
524 raise ex
525 elif count < 0: 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true
526 ex = ValueError("Parameter 'count' is negative.")
527 ex.add_note(f"Got value '{count}'.")
528 raise ex
530 if covered is None: 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true
531 raise ValueError("Parameter 'covered' is None.")
532 elif not isinstance(covered, int): 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true
533 ex = TypeError("Parameter 'covered' is not of type 'int'.")
534 ex.add_note(f"Got type '{getFullyQualifiedName(covered)}'.")
535 raise ex
536 elif covered < 0: 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true
537 ex = ValueError("Parameter 'covered' is negative.")
538 ex.add_note(f"Got value '{covered}'.")
539 raise ex
541 notCoveredTrueList: list[int] = []
542 if notCoveredTrue is not None:
543 if not isinstance(notCoveredTrue, Iterable):
544 ex = TypeError("Parameter 'notCoveredTrue' is not iterable.")
545 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredTrue)}'.")
546 raise ex
548 for term in notCoveredTrue:
549 if not isinstance(term, int): 549 ↛ 550line 549 didn't jump to line 550 because the condition on line 549 was never true
550 ex = TypeError("Parameter 'notCoveredTrue' contains an element not of type 'int'.")
551 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.")
552 raise ex
554 notCoveredTrueList.append(term)
556 notCoveredFalseList: list[int] = []
557 if notCoveredFalse is not None:
558 if not isinstance(notCoveredFalse, Iterable):
559 ex = TypeError("Parameter 'notCoveredFalse' is not iterable.")
560 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredFalse)}'.")
561 raise ex
563 for term in notCoveredFalse:
564 if not isinstance(term, int):
565 ex = TypeError("Parameter 'notCoveredFalse' contains an element not of type 'int'.")
566 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.")
567 raise ex
569 notCoveredFalseList.append(term)
571 if parent is not None and not isinstance(parent, Line):
572 ex = TypeError("Parameter 'parent' is not of type 'Line'.")
573 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
574 raise ex
576 self._parent = parent
577 self._count = count
578 self._covered = covered
579 self._notCoveredTrue = notCoveredTrueList
580 self._notCoveredFalse = notCoveredFalseList
582 if parent is not None:
583 parent._conditions.append(self)
585 @classmethod
586 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self:
587 """
588 Parse a condition from its JSON object.
590 :param record: The JSON object of the condition.
591 :param parent: Optional, the line the condition belongs to. Default: ``None``.
592 :returns: The condition.
593 :raises ValueError: If parameter ``record`` is ``None``.
594 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
595 """
596 if record is None:
597 raise ValueError("Parameter 'record' is None.")
598 elif not isinstance(record, dict):
599 ex = TypeError("Parameter 'record' is not of type 'dict'.")
600 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
601 raise ex
603 return cls(
604 record["count"], record["covered"], record["not_covered_true"], record["not_covered_false"], parent=parent
605 )
607 @readonly
608 def Parent(self) -> Nullable[Line]:
609 """
610 Read-only property to access the line the condition belongs to (:attr:`_parent`).
612 :returns: The line; ``None`` if the condition belongs to no line.
613 """
614 return self._parent
616 @readonly
617 def Count(self) -> int:
618 """
619 Read-only property to access the number of condition outcomes (:attr:`_count`).
621 :returns: The number of outcomes: twice the number of terms.
622 """
623 return self._count
625 @readonly
626 def Covered(self) -> int:
627 """
628 Read-only property to access the number of covered condition outcomes (:attr:`_covered`).
630 :returns: The number of covered outcomes.
631 """
632 return self._covered
634 @readonly
635 def NotCoveredTrue(self) -> list[int]:
636 """
637 Read-only property to access the terms never seen as true (:attr:`_notCoveredTrue`).
639 :returns: The terms' indices.
640 """
641 return self._notCoveredTrue
643 @readonly
644 def NotCoveredFalse(self) -> list[int]:
645 """
646 Read-only property to access the terms never seen as false (:attr:`_notCoveredFalse`).
648 :returns: The terms' indices.
649 """
650 return self._notCoveredFalse
653@export
654class Line(metaclass=ExtendedType, slots=True):
655 """
656 A ``line`` of a file: how often it ran, its basic blocks, branches, calls and conditions.
657 """
659 _parent: Nullable[File] #: The file the line belongs to.
660 _lineNumber: int #: Line number, counted from 1.
661 _functionName: Nullable[str] #: Mangled name of the function the line belongs to, if the report says.
662 _count: int #: Number of times the line ran.
663 _unexecutedBlock: bool #: Whether a basic block, not only reached by exceptions, never ran.
664 _blockIDs: list[int] #: IDs of the basic blocks ending on this line, in format 2.
665 _branches: list[Branch] #: The branches.
666 _calls: list[Call] #: The calls, in format 2.
667 _conditions: list[Condition] #: The conditions, in format 2.
669 def __init__(
670 self,
671 lineNumber: int,
672 count: int,
673 unexecutedBlock: bool,
674 functionName: Nullable[str] = None,
675 blockIDs: Nullable[Iterable[int]] = None,
676 *,
677 parent: Nullable[File] = None
678 ) -> None:
679 """
680 Initialize the line, and add it to the lines of its file.
682 Its branches, calls and conditions are added by creating them with this line as their parent.
684 :param lineNumber: Line number, counted from 1.
685 :param count: Number of times the line ran.
686 :param unexecutedBlock: Whether a basic block of the line, not only reached by exceptions, never ran.
687 :param functionName: Optional, mangled name of the function the line belongs to. Default: ``None``.
688 :param blockIDs: Optional, IDs of the basic blocks ending on this line. Default: none.
689 :param parent: Optional, the file the line belongs to; the line is appended to its lines.
690 Default: ``None``.
691 :raises ValueError: If parameter ``lineNumber`` is ``None``.
692 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
693 :raises ValueError: If parameter ``lineNumber`` is less than 1.
694 :raises ValueError: If parameter ``count`` is ``None``.
695 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
696 :raises ValueError: If parameter ``count`` is negative.
697 :raises ValueError: If parameter ``unexecutedBlock`` is ``None``.
698 :raises TypeError: If parameter ``unexecutedBlock`` isn't of type :class:`bool`.
699 :raises TypeError: If parameter ``functionName`` isn't of type :class:`str`.
700 :raises TypeError: If parameter ``blockIDs`` isn't iterable.
701 :raises TypeError: If parameter ``blockIDs`` contains an element not of type :class:`int`.
702 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
703 """
704 if lineNumber is None:
705 raise ValueError("Parameter 'lineNumber' is None.")
706 elif not isinstance(lineNumber, int):
707 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
708 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
709 raise ex
710 elif lineNumber < 1:
711 ex = ValueError("Parameter 'lineNumber' is less than 1.")
712 ex.add_note(f"Got value '{lineNumber}'.")
713 raise ex
715 if count is None: 715 ↛ 716line 715 didn't jump to line 716 because the condition on line 715 was never true
716 raise ValueError("Parameter 'count' is None.")
717 elif not isinstance(count, int): 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true
718 ex = TypeError("Parameter 'count' is not of type 'int'.")
719 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
720 raise ex
721 elif count < 0: 721 ↛ 722line 721 didn't jump to line 722 because the condition on line 721 was never true
722 ex = ValueError("Parameter 'count' is negative.")
723 ex.add_note(f"Got value '{count}'.")
724 raise ex
726 if unexecutedBlock is None: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true
727 raise ValueError("Parameter 'unexecutedBlock' is None.")
728 elif not isinstance(unexecutedBlock, bool): 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true
729 ex = TypeError("Parameter 'unexecutedBlock' is not of type 'bool'.")
730 ex.add_note(f"Got type '{getFullyQualifiedName(unexecutedBlock)}'.")
731 raise ex
733 if functionName is not None and not isinstance(functionName, str): 733 ↛ 734line 733 didn't jump to line 734 because the condition on line 733 was never true
734 ex = TypeError("Parameter 'functionName' is not of type 'str'.")
735 ex.add_note(f"Got type '{getFullyQualifiedName(functionName)}'.")
736 raise ex
738 blockIDList: list[int] = []
739 if blockIDs is not None:
740 if not isinstance(blockIDs, Iterable):
741 ex = TypeError("Parameter 'blockIDs' is not iterable.")
742 ex.add_note(f"Got type '{getFullyQualifiedName(blockIDs)}'.")
743 raise ex
745 for blockID in blockIDs:
746 if not isinstance(blockID, int):
747 ex = TypeError("Parameter 'blockIDs' contains an element not of type 'int'.")
748 ex.add_note(f"Got type '{getFullyQualifiedName(blockID)}'.")
749 raise ex
751 blockIDList.append(blockID)
753 if parent is not None and not isinstance(parent, File):
754 ex = TypeError("Parameter 'parent' is not of type 'File'.")
755 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
756 raise ex
758 self._parent = parent
759 self._lineNumber = lineNumber
760 self._functionName = functionName
761 self._count = count
762 self._unexecutedBlock = unexecutedBlock
763 self._blockIDs = blockIDList
764 self._branches = []
765 self._calls = []
766 self._conditions = []
768 if parent is not None:
769 parent._lines.append(self)
771 @classmethod
772 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
773 """
774 Parse a line, its branches, calls and conditions from its JSON object.
776 :param record: The JSON object of the line.
777 :param parent: Optional, the file the line belongs to. Default: ``None``.
778 :returns: The line.
779 :raises ValueError: If parameter ``record`` is ``None``.
780 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
781 """
782 if record is None:
783 raise ValueError("Parameter 'record' is None.")
784 elif not isinstance(record, dict):
785 ex = TypeError("Parameter 'record' is not of type 'dict'.")
786 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
787 raise ex
789 line = cls(
790 record["line_number"],
791 record["count"],
792 record["unexecuted_block"],
793 record.get("function_name"),
794 record.get("block_ids"),
795 parent=parent
796 )
798 for branch in record["branches"]:
799 Branch.Parse(branch, parent=line)
801 for call in record.get("calls", []):
802 Call.Parse(call, parent=line)
804 for condition in record.get("conditions", []):
805 Condition.Parse(condition, parent=line)
807 return line
809 @readonly
810 def Parent(self) -> Nullable[File]:
811 """
812 Read-only property to access the file the line belongs to (:attr:`_parent`).
814 :returns: The file; ``None`` if the line belongs to no file.
815 """
816 return self._parent
818 @readonly
819 def LineNumber(self) -> int:
820 """
821 Read-only property to access the line number (:attr:`_lineNumber`).
823 :returns: The line number, counted from 1.
824 """
825 return self._lineNumber
827 @readonly
828 def FunctionName(self) -> Nullable[str]:
829 """
830 Read-only property to access the mangled name of the function the line belongs to (:attr:`_functionName`).
832 :returns: The function's name, or ``None`` for a line of inlined statements.
833 """
834 return self._functionName
836 @readonly
837 def Count(self) -> int:
838 """
839 Read-only property to access the number of times the line ran (:attr:`_count`).
841 :returns: The count.
842 """
843 return self._count
845 @readonly
846 def UnexecutedBlock(self) -> bool:
847 """
848 Read-only property to access whether a basic block of the line never ran (:attr:`_unexecutedBlock`).
850 A block only reached by exceptions doesn't count.
852 :returns: ``True``, if not every statement of the line ran.
853 """
854 return self._unexecutedBlock
856 @readonly
857 def BlockIDs(self) -> list[int]:
858 """
859 Read-only property to access the IDs of the basic blocks ending on this line (:attr:`_blockIDs`).
861 :returns: The blocks' IDs, unique within the function; empty in format 1.
862 """
863 return self._blockIDs
865 @readonly
866 def Branches(self) -> list[Branch]:
867 """
868 Read-only property to access the branches (:attr:`_branches`).
870 :returns: The branches; empty, if gcov ran without ``--branch-probabilities``.
871 """
872 return self._branches
874 @readonly
875 def Calls(self) -> list[Call]:
876 """
877 Read-only property to access the calls (:attr:`_calls`).
879 :returns: The calls; empty in format 1, or if gcov ran without ``--branch-probabilities``.
880 """
881 return self._calls
883 @readonly
884 def Conditions(self) -> list[Condition]:
885 """
886 Read-only property to access the conditions (:attr:`_conditions`).
888 :returns: The conditions; empty in format 1, or if gcov ran without ``--conditions``.
889 """
890 return self._conditions
893@export
894class Function(metaclass=ExtendedType, slots=True):
895 """
896 A ``function`` of a file: its names, its position, its basic blocks and how often it ran.
897 """
899 _parent: Nullable[File] #: The file the function belongs to.
900 _name: str #: Name of the function, mangled.
901 _demangledName: str #: Name of the function, demangled.
902 _startLine: int #: The function's first line.
903 _startColumn: int #: The function's first column.
904 _endLine: int #: The function's last line.
905 _endColumn: int #: The function's last column.
906 _blocks: int #: Number of basic blocks.
907 _blocksExecuted: int #: Number of executed basic blocks.
908 _executionCount: int #: Number of times the function ran.
910 def __init__(
911 self,
912 name: str,
913 demangledName: str,
914 startLine: int,
915 startColumn: int,
916 endLine: int,
917 endColumn: int,
918 blocks: int,
919 blocksExecuted: int,
920 executionCount: int,
921 *,
922 parent: Nullable[File] = None
923 ) -> None:
924 """
925 Initialize the function, and add it to the functions of its file.
927 :param name: Name of the function, mangled.
928 :param demangledName: Name of the function, demangled.
929 :param startLine: The function's first line.
930 :param startColumn: The function's first column.
931 :param endLine: The function's last line.
932 :param endColumn: The function's last column.
933 :param blocks: Number of basic blocks.
934 :param blocksExecuted: Number of executed basic blocks.
935 :param executionCount: Number of times the function ran.
936 :param parent: Optional, the file the function belongs to; the function is added to its functions by
937 :attr:`Name`. Default: ``None``.
938 :raises ValueError: If parameter ``name`` is ``None``.
939 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
940 :raises ValueError: If parameter ``name`` is empty.
941 :raises ValueError: If parameter ``demangledName`` is ``None``.
942 :raises TypeError: If parameter ``demangledName`` isn't of type :class:`str`.
943 :raises ValueError: If parameter ``demangledName`` is empty.
944 :raises ValueError: If parameter ``startLine`` is ``None``.
945 :raises TypeError: If parameter ``startLine`` isn't of type :class:`int`.
946 :raises ValueError: If parameter ``startLine`` is negative.
947 :raises ValueError: If parameter ``startColumn`` is ``None``.
948 :raises TypeError: If parameter ``startColumn`` isn't of type :class:`int`.
949 :raises ValueError: If parameter ``startColumn`` is negative.
950 :raises ValueError: If parameter ``endLine`` is ``None``.
951 :raises TypeError: If parameter ``endLine`` isn't of type :class:`int`.
952 :raises ValueError: If parameter ``endLine`` is negative.
953 :raises ValueError: If parameter ``endColumn`` is ``None``.
954 :raises TypeError: If parameter ``endColumn`` isn't of type :class:`int`.
955 :raises ValueError: If parameter ``endColumn`` is negative.
956 :raises ValueError: If parameter ``blocks`` is ``None``.
957 :raises TypeError: If parameter ``blocks`` isn't of type :class:`int`.
958 :raises ValueError: If parameter ``blocks`` is negative.
959 :raises ValueError: If parameter ``blocksExecuted`` is ``None``.
960 :raises TypeError: If parameter ``blocksExecuted`` isn't of type :class:`int`.
961 :raises ValueError: If parameter ``blocksExecuted`` is negative.
962 :raises ValueError: If parameter ``executionCount`` is ``None``.
963 :raises TypeError: If parameter ``executionCount`` isn't of type :class:`int`.
964 :raises ValueError: If parameter ``executionCount`` is negative.
965 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`.
966 :raises ValueError: If parameter ``parent`` contains a function of the same name already.
967 """
968 if name is None: 968 ↛ 969line 968 didn't jump to line 969 because the condition on line 968 was never true
969 raise ValueError("Parameter 'name' is None.")
970 elif not isinstance(name, str): 970 ↛ 971line 970 didn't jump to line 971 because the condition on line 970 was never true
971 ex = TypeError("Parameter 'name' is not of type 'str'.")
972 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
973 raise ex
974 elif name == "":
975 raise ValueError("Parameter 'name' is empty.")
977 if demangledName is None: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true
978 raise ValueError("Parameter 'demangledName' is None.")
979 elif not isinstance(demangledName, str): 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true
980 ex = TypeError("Parameter 'demangledName' is not of type 'str'.")
981 ex.add_note(f"Got type '{getFullyQualifiedName(demangledName)}'.")
982 raise ex
983 elif demangledName == "": 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true
984 raise ValueError("Parameter 'demangledName' is empty.")
986 if startLine is None: 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true
987 raise ValueError("Parameter 'startLine' is None.")
988 elif not isinstance(startLine, int): 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true
989 ex = TypeError("Parameter 'startLine' is not of type 'int'.")
990 ex.add_note(f"Got type '{getFullyQualifiedName(startLine)}'.")
991 raise ex
992 elif startLine < 0: 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true
993 ex = ValueError("Parameter 'startLine' is negative.")
994 ex.add_note(f"Got value '{startLine}'.")
995 raise ex
997 if startColumn is None: 997 ↛ 998line 997 didn't jump to line 998 because the condition on line 997 was never true
998 raise ValueError("Parameter 'startColumn' is None.")
999 elif not isinstance(startColumn, int): 999 ↛ 1000line 999 didn't jump to line 1000 because the condition on line 999 was never true
1000 ex = TypeError("Parameter 'startColumn' is not of type 'int'.")
1001 ex.add_note(f"Got type '{getFullyQualifiedName(startColumn)}'.")
1002 raise ex
1003 elif startColumn < 0: 1003 ↛ 1004line 1003 didn't jump to line 1004 because the condition on line 1003 was never true
1004 ex = ValueError("Parameter 'startColumn' is negative.")
1005 ex.add_note(f"Got value '{startColumn}'.")
1006 raise ex
1008 if endLine is None: 1008 ↛ 1009line 1008 didn't jump to line 1009 because the condition on line 1008 was never true
1009 raise ValueError("Parameter 'endLine' is None.")
1010 elif not isinstance(endLine, int): 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true
1011 ex = TypeError("Parameter 'endLine' is not of type 'int'.")
1012 ex.add_note(f"Got type '{getFullyQualifiedName(endLine)}'.")
1013 raise ex
1014 elif endLine < 0: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true
1015 ex = ValueError("Parameter 'endLine' is negative.")
1016 ex.add_note(f"Got value '{endLine}'.")
1017 raise ex
1019 if endColumn is None: 1019 ↛ 1020line 1019 didn't jump to line 1020 because the condition on line 1019 was never true
1020 raise ValueError("Parameter 'endColumn' is None.")
1021 elif not isinstance(endColumn, int): 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true
1022 ex = TypeError("Parameter 'endColumn' is not of type 'int'.")
1023 ex.add_note(f"Got type '{getFullyQualifiedName(endColumn)}'.")
1024 raise ex
1025 elif endColumn < 0: 1025 ↛ 1026line 1025 didn't jump to line 1026 because the condition on line 1025 was never true
1026 ex = ValueError("Parameter 'endColumn' is negative.")
1027 ex.add_note(f"Got value '{endColumn}'.")
1028 raise ex
1030 if blocks is None: 1030 ↛ 1031line 1030 didn't jump to line 1031 because the condition on line 1030 was never true
1031 raise ValueError("Parameter 'blocks' is None.")
1032 elif not isinstance(blocks, int): 1032 ↛ 1033line 1032 didn't jump to line 1033 because the condition on line 1032 was never true
1033 ex = TypeError("Parameter 'blocks' is not of type 'int'.")
1034 ex.add_note(f"Got type '{getFullyQualifiedName(blocks)}'.")
1035 raise ex
1036 elif blocks < 0: 1036 ↛ 1037line 1036 didn't jump to line 1037 because the condition on line 1036 was never true
1037 ex = ValueError("Parameter 'blocks' is negative.")
1038 ex.add_note(f"Got value '{blocks}'.")
1039 raise ex
1041 if blocksExecuted is None: 1041 ↛ 1042line 1041 didn't jump to line 1042 because the condition on line 1041 was never true
1042 raise ValueError("Parameter 'blocksExecuted' is None.")
1043 elif not isinstance(blocksExecuted, int): 1043 ↛ 1044line 1043 didn't jump to line 1044 because the condition on line 1043 was never true
1044 ex = TypeError("Parameter 'blocksExecuted' is not of type 'int'.")
1045 ex.add_note(f"Got type '{getFullyQualifiedName(blocksExecuted)}'.")
1046 raise ex
1047 elif blocksExecuted < 0: 1047 ↛ 1048line 1047 didn't jump to line 1048 because the condition on line 1047 was never true
1048 ex = ValueError("Parameter 'blocksExecuted' is negative.")
1049 ex.add_note(f"Got value '{blocksExecuted}'.")
1050 raise ex
1052 if executionCount is None: 1052 ↛ 1053line 1052 didn't jump to line 1053 because the condition on line 1052 was never true
1053 raise ValueError("Parameter 'executionCount' is None.")
1054 elif not isinstance(executionCount, int): 1054 ↛ 1055line 1054 didn't jump to line 1055 because the condition on line 1054 was never true
1055 ex = TypeError("Parameter 'executionCount' is not of type 'int'.")
1056 ex.add_note(f"Got type '{getFullyQualifiedName(executionCount)}'.")
1057 raise ex
1058 elif executionCount < 0:
1059 ex = ValueError("Parameter 'executionCount' is negative.")
1060 ex.add_note(f"Got value '{executionCount}'.")
1061 raise ex
1063 if parent is not None:
1064 if not isinstance(parent, File):
1065 ex = TypeError("Parameter 'parent' is not of type 'File'.")
1066 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1067 raise ex
1068 elif name in parent._functions:
1069 raise ValueError(f"Parameter 'parent' contains function '{name}' already.")
1071 self._parent = parent
1072 self._name = name
1073 self._demangledName = demangledName
1074 self._startLine = startLine
1075 self._startColumn = startColumn
1076 self._endLine = endLine
1077 self._endColumn = endColumn
1078 self._blocks = blocks
1079 self._blocksExecuted = blocksExecuted
1080 self._executionCount = executionCount
1082 if parent is not None:
1083 parent._functions[self._name] = self
1085 @classmethod
1086 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self:
1087 """
1088 Parse a function from its JSON object.
1090 :param record: The JSON object of the function.
1091 :param parent: Optional, the file the function belongs to. Default: ``None``.
1092 :returns: The function.
1093 :raises ValueError: If parameter ``record`` is ``None``.
1094 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1095 """
1096 if record is None:
1097 raise ValueError("Parameter 'record' is None.")
1098 elif not isinstance(record, dict):
1099 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1100 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1101 raise ex
1103 return cls(
1104 record["name"],
1105 record["demangled_name"],
1106 record["start_line"],
1107 record["start_column"],
1108 record["end_line"],
1109 record["end_column"],
1110 record["blocks"],
1111 record["blocks_executed"],
1112 record["execution_count"],
1113 parent=parent
1114 )
1116 @readonly
1117 def Parent(self) -> Nullable[File]:
1118 """
1119 Read-only property to access the file the function belongs to (:attr:`_parent`).
1121 :returns: The file; ``None`` if the function belongs to no file.
1122 """
1123 return self._parent
1125 @readonly
1126 def Name(self) -> str:
1127 """
1128 Read-only property to access the mangled name of the function (:attr:`_name`).
1130 The method ``Containers::Stack::Pop()`` is mangled to ``_ZN10Containers5Stack3PopEv``, a C function keeps its name.
1132 :returns: The mangled name.
1133 """
1134 return self._name
1136 @readonly
1137 def DemangledName(self) -> str:
1138 """
1139 Read-only property to access the demangled name of the function (:attr:`_demangledName`).
1141 The demangled name of ``_ZN10Containers5Stack3PopEv`` is ``Containers::Stack::Pop()``.
1143 :returns: The demangled name.
1144 """
1145 return self._demangledName
1147 @readonly
1148 def StartLine(self) -> int:
1149 """
1150 Read-only property to access the function's first line (:attr:`_startLine`).
1152 :returns: The line number.
1153 """
1154 return self._startLine
1156 @readonly
1157 def StartColumn(self) -> int:
1158 """
1159 Read-only property to access the function's first column (:attr:`_startColumn`).
1161 :returns: The column number, counted from 1.
1162 """
1163 return self._startColumn
1165 @readonly
1166 def EndLine(self) -> int:
1167 """
1168 Read-only property to access the function's last line (:attr:`_endLine`).
1170 :returns: The line number.
1171 """
1172 return self._endLine
1174 @readonly
1175 def EndColumn(self) -> int:
1176 """
1177 Read-only property to access the function's last column (:attr:`_endColumn`).
1179 :returns: The column number, counted from 1.
1180 """
1181 return self._endColumn
1183 @readonly
1184 def Blocks(self) -> int:
1185 """
1186 Read-only property to access the number of basic blocks (:attr:`_blocks`).
1188 :returns: The number of blocks.
1189 """
1190 return self._blocks
1192 @readonly
1193 def BlocksExecuted(self) -> int:
1194 """
1195 Read-only property to access the number of executed basic blocks (:attr:`_blocksExecuted`).
1197 :returns: The number of executed blocks.
1198 """
1199 return self._blocksExecuted
1201 @readonly
1202 def ExecutionCount(self) -> int:
1203 """
1204 Read-only property to access the number of times the function ran (:attr:`_executionCount`).
1206 :returns: The count.
1207 """
1208 return self._executionCount
1211@export
1212class File(metaclass=ExtendedType, slots=True):
1213 """
1214 A source ``file`` of a data file: its functions and lines.
1216 A line several functions share - e.g. the instantiations of a template - is listed once per function.
1217 """
1219 _parent: Nullable[DataFile] #: The data file the file belongs to.
1220 _path: _Path #: The file's path, as the compiler named it.
1221 _functions: dict[str, Function] #: The functions, by mangled name.
1222 _lines: list[Line] #: The executable lines, in order; a line of several functions once per function.
1224 def __init__(self, path: Path, *, parent: Nullable[DataFile] = None) -> None:
1225 """
1226 Initialize the file, and add it to the files of its data file.
1228 Its functions and lines are added by creating them with this file as their parent.
1230 :param path: The file's path, as the compiler named it.
1231 :param parent: Optional, the data file the file belongs to; the file is added to its files by :attr:`Path`.
1232 Default: ``None``.
1233 :raises ValueError: If parameter ``path`` is ``None``.
1234 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1235 :raises TypeError: If parameter ``parent`` isn't of type :class:`DataFile`.
1236 :raises ValueError: If parameter ``parent`` contains a file of the same path already.
1237 """
1238 if path is None: 1238 ↛ 1239line 1238 didn't jump to line 1239 because the condition on line 1238 was never true
1239 raise ValueError("Parameter 'path' is None.")
1240 elif not isinstance(path, Path):
1241 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1242 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1243 raise ex
1245 if parent is not None:
1246 if not isinstance(parent, DataFile):
1247 ex = TypeError("Parameter 'parent' is not of type 'DataFile'.")
1248 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1249 raise ex
1250 elif path in parent._files:
1251 raise ValueError(f"Parameter 'parent' contains file '{path.as_posix()}' already.")
1253 self._parent = parent
1254 self._path = path
1255 self._functions = {}
1256 self._lines = []
1258 if parent is not None:
1259 parent._files[self._path] = self
1261 @classmethod
1262 def Parse(cls, record: dict[str, Any], *, parent: Nullable[DataFile] = None) -> Self:
1263 """
1264 Parse a file, its functions and lines from its JSON object.
1266 A backslash in the path - as a report written on Windows has them - separates directories.
1268 :param record: The JSON object of the file.
1269 :param parent: Optional, the data file the file belongs to. Default: ``None``.
1270 :returns: The file.
1271 :raises ValueError: If parameter ``record`` is ``None``.
1272 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1273 :raises CodeCoverageError: If the file names a function twice.
1274 """
1275 if record is None:
1276 raise ValueError("Parameter 'record' is None.")
1277 elif not isinstance(record, dict):
1278 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1279 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1280 raise ex
1282 file = cls(Path(record["file"].replace("\\", "/")), parent=parent)
1284 for function in record["functions"]:
1285 if (name := function["name"]) in file._functions:
1286 raise CodeCoverageError(f"gcov source file '{file._path.as_posix()}' names function '{name}' twice.")
1288 Function.Parse(function, parent=file)
1290 for line in record["lines"]:
1291 Line.Parse(line, parent=file)
1293 return file
1295 @readonly
1296 def Parent(self) -> Nullable[DataFile]:
1297 """
1298 Read-only property to access the data file the file belongs to (:attr:`_parent`).
1300 :returns: The data file; ``None`` if the file belongs to no data file.
1301 """
1302 return self._parent
1304 @readonly
1305 def Path(self) -> Path:
1306 """
1307 Read-only property to access the file's path (:attr:`_path`).
1309 :returns: The path, relative to the directory the compiler ran in, unless absolute.
1310 """
1311 return self._path
1313 @readonly
1314 def Functions(self) -> dict[str, Function]:
1315 """
1316 Read-only property to access the functions (:attr:`_functions`).
1318 :returns: The functions, by mangled name.
1319 """
1320 return self._functions
1322 @readonly
1323 def Lines(self) -> list[Line]:
1324 """
1325 Read-only property to access the executable lines (:attr:`_lines`).
1327 :returns: The lines; a line several functions share is listed once per function.
1328 """
1329 return self._lines
1332@export
1333class DataFile(metaclass=ExtendedType, slots=True):
1334 """
1335 The coverage measured in a data file (GCDA): the versions of the format and of GCC, and the source files.
1336 """
1338 _parent: Nullable[Coverage] #: The report the data file belongs to.
1339 _path: _Path #: Path of the data file, as gcov was called with it.
1340 _formatVersion: _FormatVersion #: Version of the report format.
1341 _gccVersion: SemanticVersion #: Version of GCC.
1342 _currentWorkingDirectory: Nullable[_Path] #: The directory the compiler ran in, if the report says.
1343 _files: dict[_Path, File] #: The source files, by path.
1345 def __init__(
1346 self,
1347 path: Path,
1348 formatVersion: FormatVersion,
1349 gccVersion: SemanticVersion,
1350 currentWorkingDirectory: Nullable[Path] = None,
1351 *,
1352 parent: Nullable[Coverage] = None
1353 ) -> None:
1354 """
1355 Initialize the data file, and add it to the data files of its report.
1357 Its source files are added by creating them with this data file as their parent.
1359 :param path: Path of the data file, as gcov was called with it.
1360 :param formatVersion: Version of the report format.
1361 :param gccVersion: Version of GCC.
1362 :param currentWorkingDirectory: Optional, the directory the compiler ran in. Default: ``None``.
1363 :param parent: Optional, the report the data file belongs to; the data file is appended to its
1364 data files. Default: ``None``.
1365 :raises ValueError: If parameter ``path`` is ``None``.
1366 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`.
1367 :raises ValueError: If parameter ``formatVersion`` is ``None``.
1368 :raises TypeError: If parameter ``formatVersion`` isn't of type :class:`FormatVersion`.
1369 :raises ValueError: If parameter ``gccVersion`` is ``None``.
1370 :raises TypeError: If parameter ``gccVersion`` isn't of type
1371 :class:`~pyTooling.Versioning.SemanticVersion`.
1372 :raises TypeError: If parameter ``currentWorkingDirectory`` isn't of type :class:`~pathlib.Path`.
1373 :raises TypeError: If parameter ``parent`` isn't of type :class:`Coverage`.
1374 """
1375 if path is None: 1375 ↛ 1376line 1375 didn't jump to line 1376 because the condition on line 1375 was never true
1376 raise ValueError("Parameter 'path' is None.")
1377 elif not isinstance(path, Path): 1377 ↛ 1378line 1377 didn't jump to line 1378 because the condition on line 1377 was never true
1378 ex = TypeError("Parameter 'path' is not of type 'Path'.")
1379 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
1380 raise ex
1382 if formatVersion is None:
1383 raise ValueError("Parameter 'formatVersion' is None.")
1384 elif not isinstance(formatVersion, FormatVersion):
1385 ex = TypeError("Parameter 'formatVersion' is not of type 'FormatVersion'.")
1386 ex.add_note(f"Got type '{getFullyQualifiedName(formatVersion)}'.")
1387 raise ex
1389 if gccVersion is None: 1389 ↛ 1390line 1389 didn't jump to line 1390 because the condition on line 1389 was never true
1390 raise ValueError("Parameter 'gccVersion' is None.")
1391 elif not isinstance(gccVersion, SemanticVersion):
1392 ex = TypeError("Parameter 'gccVersion' is not of type 'SemanticVersion'.")
1393 ex.add_note(f"Got type '{getFullyQualifiedName(gccVersion)}'.")
1394 raise ex
1396 if currentWorkingDirectory is not None and not isinstance(currentWorkingDirectory, Path): 1396 ↛ 1397line 1396 didn't jump to line 1397 because the condition on line 1396 was never true
1397 ex = TypeError("Parameter 'currentWorkingDirectory' is not of type 'Path'.")
1398 ex.add_note(f"Got type '{getFullyQualifiedName(currentWorkingDirectory)}'.")
1399 raise ex
1401 if parent is not None and not isinstance(parent, Coverage):
1402 ex = TypeError("Parameter 'parent' is not of type 'Coverage'.")
1403 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1404 raise ex
1406 self._parent = parent
1407 self._path = path
1408 self._formatVersion = formatVersion
1409 self._gccVersion = gccVersion
1410 self._currentWorkingDirectory = currentWorkingDirectory
1411 self._files = {}
1413 if parent is not None:
1414 parent._dataFiles.append(self)
1416 @classmethod
1417 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Coverage] = None) -> Self:
1418 """
1419 Parse a data file and its source files from its JSON object.
1421 The format version is converted by :meth:`FormatVersion.Parse`; a development build of GCC states its date and
1422 phase behind its version, e.g. ``15.0.1 20250418 (experimental)``. A backslash in a path - as a report written on
1423 Windows has them - separates directories.
1425 :param record: The JSON object of the data file: a report's root object.
1426 :param parent: Optional, the report the data file belongs to. Default: ``None``.
1427 :returns: The data file.
1428 :raises ValueError: If parameter ``record`` is ``None``.
1429 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`.
1430 :raises CodeCoverageError: If the data file names a source file twice.
1431 """
1432 if record is None:
1433 raise ValueError("Parameter 'record' is None.")
1434 elif not isinstance(record, dict):
1435 ex = TypeError("Parameter 'record' is not of type 'dict'.")
1436 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.")
1437 raise ex
1439 directory = record.get("current_working_directory")
1440 dataFile = cls(
1441 Path(record["data_file"].replace("\\", "/")),
1442 FormatVersion.Parse(record["format_version"]),
1443 SemanticVersion.Parse(record["gcc_version"].split(" ", 1)[0]),
1444 Path(directory.replace("\\", "/")) if directory is not None else None,
1445 parent=parent
1446 )
1448 for file in record["files"]:
1449 if (path := Path(file["file"].replace("\\", "/"))) in dataFile._files:
1450 raise CodeCoverageError(
1451 f"gcov data file '{dataFile._path.as_posix()}' names source file '{path.as_posix()}' twice."
1452 )
1454 File.Parse(file, parent=dataFile)
1456 return dataFile
1458 @readonly
1459 def Parent(self) -> Nullable[Coverage]:
1460 """
1461 Read-only property to access the report the data file belongs to (:attr:`_parent`).
1463 :returns: The report; ``None`` if the data file belongs to no report.
1464 """
1465 return self._parent
1467 @readonly
1468 def Path(self) -> Path:
1469 """
1470 Read-only property to access the path of the data file (:attr:`_path`).
1472 It is the path gcov was called with, e.g. ``main.c`` for ``gcov --json-format main.c``.
1474 :returns: The path, as gcov was called with it.
1475 """
1476 return self._path
1478 @readonly
1479 def FormatVersion(self) -> FormatVersion:
1480 """
1481 Read-only property to access the version of the report format (:attr:`_formatVersion`).
1483 :returns: The format version.
1484 """
1485 return self._formatVersion
1487 @readonly
1488 def GCCVersion(self) -> SemanticVersion:
1489 """
1490 Read-only property to access the version of GCC (:attr:`_gccVersion`).
1492 A development build of GCC states its date and phase behind the version, e.g. ``15.0.1 20250418 (experimental)``;
1493 they aren't kept.
1495 :returns: The version.
1496 """
1497 return self._gccVersion
1499 @readonly
1500 def CurrentWorkingDirectory(self) -> Nullable[Path]:
1501 """
1502 Read-only property to access the directory the compiler ran in (:attr:`_currentWorkingDirectory`).
1504 :returns: The directory, on the machine the code was compiled on, or ``None`` if the report doesn't say.
1505 """
1506 return self._currentWorkingDirectory
1508 @readonly
1509 def Files(self) -> dict[Path, File]:
1510 """
1511 Read-only property to access the source files (:attr:`_files`).
1513 :returns: The files, by path.
1514 """
1515 return self._files
1518@export
1519class Coverage(metaclass=ExtendedType, mixin=True):
1520 """
1521 The content of a report file: the data files.
1523 The root of the format's model: a data file names it as its parent.
1524 """
1526 _dataFiles: list[DataFile] #: The data files, in the order the report lists them.
1528 def __init__(self) -> None:
1529 """
1530 Initialize an empty report.
1531 """
1532 self._dataFiles = []
1534 @readonly
1535 def DataFiles(self) -> list[DataFile]:
1536 """
1537 Read-only property to access the data files (:attr:`_dataFiles`).
1539 :returns: The data files: one of a :file:`*.gcov.json.gz` file; one per line of gcov's standard output.
1540 """
1541 return self._dataFiles
1544@export
1545class Document(cc_Document, Coverage):
1546 """
1547 A gcov JSON code coverage report: read into the format's model, and converted to the common model.
1548 """
1550 _jsonDocuments: Nullable[list[dict[str, Any]]] #: The parsed and validated JSON objects, after :meth:`Analyze`.
1552 def __init__(self, jsonReportFile: Path, analyzeAndConvert: bool = False) -> None:
1553 """
1554 Initialize the report, and optionally read it.
1556 :param jsonReportFile: Path to the JSON file, gzip-compressed or plain.
1557 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
1558 """
1559 super().__init__(jsonReportFile)
1560 Coverage.__init__(self)
1562 self._jsonDocuments = None
1564 if analyzeAndConvert:
1565 self.Analyze()
1566 self.Convert()
1568 def Analyze(self) -> None:
1569 """
1570 Read the file - decompressing it, if gzip-compressed -, parse its JSON objects and validate each against the JSON
1571 Schema of the format version it states (:data:`SCHEMAS`).
1573 :raises CodeCoverageError: If the file doesn't exist.
1574 :raises CodeCoverageError: If the file can't be read.
1575 :raises CodeCoverageError: If the file is gzip-compressed, but corrupt.
1576 :raises CodeCoverageError: If the file isn't valid JSON.
1577 :raises CodeCoverageError: If the file holds no JSON object.
1578 :raises CodeCoverageError: If a JSON object states no supported format version.
1579 :raises CodeCoverageError: If a JSON Schema can't be read.
1580 :raises CodeCoverageError: If a JSON object isn't valid according to the JSON Schema of its format version.
1581 """
1582 if not self._path.exists():
1583 raise CodeCoverageError(f"gcov report file '{self._path}' does not exist.") \
1584 from FileNotFoundError(f"File '{self._path}' not found.")
1586 from jsonschema import Draft202012Validator
1588 with Stopwatch() as sw:
1589 try:
1590 content = self._path.read_bytes()
1591 except OSError as ex:
1592 raise CodeCoverageError(f"Couldn't read gcov report file '{self._path}'.") from ex
1594 if content[:2] == b"\x1f\x8b":
1595 try:
1596 content = decompress(content)
1597 except (BadGzipFile, EOFError, ZLibError) as ex:
1598 raise CodeCoverageError(f"gzip error in gcov report file '{self._path}'.") from ex
1600 # gcov's standard output holds one JSON object per data file
1601 jsonDocuments = []
1602 try:
1603 text = content.decode("utf-8")
1604 decoder = JSONDecoder()
1605 nonWhitespace = re_compile(r"\S")
1606 position = 0
1607 while (match := nonWhitespace.search(text, position)) is not None:
1608 jsonDocument, position = decoder.raw_decode(text, match.start())
1609 jsonDocuments.append(jsonDocument)
1610 except (UnicodeDecodeError, JSONDecodeError) as ex:
1611 raise CodeCoverageError(f"JSON syntax error in gcov report file '{self._path}'.") from ex
1613 if len(jsonDocuments) == 0:
1614 raise CodeCoverageError(f"gcov report file '{self._path}' holds no JSON object.")
1616 # a note's path is prefixed by the object's index, if the file holds several
1617 validators: dict[FormatVersion, Draft202012Validator] = {}
1618 for index, jsonDocument in enumerate(jsonDocuments):
1619 prefix = f"[{index}]" if len(jsonDocuments) > 1 else ""
1621 version = jsonDocument.get("format_version") if isinstance(jsonDocument, dict) else None
1622 try:
1623 formatVersion = FormatVersion.Parse(version)
1624 except (ValueError, TypeError) as cause:
1625 ex = CodeCoverageError(f"gcov report file '{self._path}' states an unsupported format version.")
1626 got = f"value '{version}'" if version is not None else "no value"
1627 ex.add_note(f"Got {got} at '{prefix}/format_version'.")
1628 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in FormatVersion)}.")
1629 raise ex from cause
1631 schemaFile = SCHEMAS[formatVersion]
1632 if (validator := validators.get(formatVersion)) is None:
1633 try:
1634 schema = loads(readResourceFile(Resources, schemaFile))
1635 except (ToolingException, JSONDecodeError) as ex:
1636 raise CodeCoverageError(f"Couldn't read JSON Schema '{schemaFile}' from package resources.") from ex
1638 validator = validators[formatVersion] = Draft202012Validator(schema)
1640 errors = sorted(validator.iter_errors(jsonDocument), key=lambda error: list(error.path))
1641 if len(errors) > 0:
1642 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{schemaFile}'.")
1643 for error in errors:
1644 ex.add_note(f"{prefix}/{'/'.join(str(part) for part in error.path)}: {error.message}")
1645 raise ex
1647 self._jsonDocuments = jsonDocuments
1649 self._analysisDuration = sw.Duration
1651 def Convert(self) -> None:
1652 """
1653 Convert the parsed and validated JSON objects to the format's model.
1655 :raises CodeCoverageError: If the JSON file was not analyzed before. |br|
1656 Call 'Document.Analyze()' or create the document using
1657 'Document(path, analyzeAndConvert=True)'.
1658 :raises CodeCoverageError: If a data file names a source file twice.
1659 :raises CodeCoverageError: If a source file names a function twice.
1660 """
1661 if self._jsonDocuments is None:
1662 ex = CodeCoverageError(f"gcov report file '{self._path}' needs to be read and analyzed by a JSON parser.")
1663 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
1664 raise ex
1666 with Stopwatch() as sw:
1667 for jsonDocument in self._jsonDocuments:
1668 DataFile.Parse(jsonDocument, parent=self)
1670 self._conversionDuration = sw.Duration
1672 def ToCoverageSummary(self) -> CoverageSummary:
1673 """
1674 Convert the format's model to the common model, and aggregate it.
1676 A source file several data files state - e.g. a header - becomes one file: the counts of its lines and branches,
1677 and of its functions, are added. The directories the compiler ran in become the source directories.
1679 :returns: The report's root of the common model, named after the report file without the
1680 extensions ``.gcov``, ``.json`` and ``.gz``.
1681 :raises CodeCoverageError: If a file's path runs through another file.
1682 """
1683 name = self._path.name.removesuffix(".gz").removesuffix(".json").removesuffix(".gcov")
1684 directories = {
1685 dataFile._currentWorkingDirectory for dataFile in self._dataFiles if dataFile._currentWorkingDirectory is not None
1686 }
1687 summary = CoverageSummary(name if name != "" else self._path.name, sourceDirectories=sorted(directories))
1689 # per file of the common model: the counts of its lines, of its lines' branches, and of its functions
1690 lineCounts: dict[cc_File, dict[int, int]] = {}
1691 branchCounts: dict[cc_File, dict[int, dict[tuple[Nullable[str], int, Nullable[int], Nullable[int]], int]]] = {}
1692 functionCounts: dict[cc_File, dict[str, tuple[int, int, int]]] = {}
1693 for dataFile in self._dataFiles:
1694 for file in dataFile._files.values():
1695 commonFile = summary.GetOrAddFile(file._path)
1696 counts = lineCounts.setdefault(commonFile, {})
1697 branches = branchCounts.setdefault(commonFile, {})
1698 functions = functionCounts.setdefault(commonFile, {})
1700 for line in file._lines:
1701 counts[line._lineNumber] = counts.get(line._lineNumber, 0) + line._count
1702 lineBranches = branches.setdefault(line._lineNumber, {})
1703 for index, branch in enumerate(line._branches):
1704 key = (line._functionName, index, branch._sourceBlockID, branch._destinationBlockID)
1705 lineBranches[key] = lineBranches.get(key, 0) + branch._count
1707 for function in file._functions.values():
1708 startLine, endLine, count = functions.get(
1709 function._demangledName, (function._startLine, function._endLine, 0)
1710 )
1711 functions[function._demangledName] = (startLine, endLine, count + function._executionCount)
1713 for commonFile, counts in lineCounts.items():
1714 for number in sorted(counts):
1715 lineBranches = branchCounts[commonFile][number]
1716 if counts[number] == 0:
1717 status = LineCoverageStatus.Uncovered
1718 elif 0 in lineBranches.values():
1719 status = LineCoverageStatus.PartiallyCovered
1720 else:
1721 status = LineCoverageStatus.Covered
1723 commonLine = cc_Line(number, status, counts[number], parent=commonFile)
1724 for count in lineBranches.values():
1725 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=commonLine)
1727 lines = commonFile._lines
1728 lastLineNumber = commonFile._lastLineNumber
1729 sourceFile = cc_SourceFile(
1730 commonFile.Path.as_posix(),
1731 file=commonFile,
1732 startLine=next(commonFile.IterateLines(), None),
1733 endLine=lines[lastLineNumber] if lastLineNumber > 0 else None,
1734 parent=summary
1735 )
1737 for functionName, (startLine, endLine, count) in functionCounts[commonFile].items():
1738 numbers = [number for number in range(startLine, min(endLine, lastLineNumber) + 1) if lines[number] is not None]
1739 cc_Function(
1740 functionName,
1741 file=commonFile,
1742 startLine=lines[numbers[0]] if len(numbers) > 0 else None,
1743 endLine=lines[numbers[-1]] if len(numbers) > 0 else None,
1744 status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered,
1745 coverageCount=count,
1746 parent=sourceFile
1747 )
1749 summary.Aggregate()
1750 return summary