Coverage for pyEDAA/Reports/CodeCoverage/LCOV.py: 100%
674 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"""
32lcov's tracefile format: a model of the format, read from a tracefile and converted to the common model.
34lcov writes the format with ``lcov --capture``; other tools write it too - e.g. ``llvm-cov export -format=lcov``,
35coverage.py (``coverage lcov``) or GHDL (``ghdl coverage --format=lcov``). The format is specified by lcov's manual
36page :manpage:`geninfo(1)`, section *TRACEFILE FORMAT*: a text file of records, one per line, e.g.
37``DA:<line number>,<execution count>``. The format's model keeps what the tracefile states: a :class:`Document` holds
38:class:`Section` records - a source file, as a test measured it -, a section :class:`Function`, :class:`Line`,
39:class:`Branch` and :class:`Condition` records, and the summaries it states.
41A tracefile is read by a strict line parser: an unknown, malformed or misplaced record raises an exception, which notes
42the record's line number.
44:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`:
46* A section's source file is a file; sections of the same file - one per test - are merged, their counts added.
47* A line's execution count is its count; a branching line is partially covered, if one of its branches wasn't taken. A
48 branch never evaluated - ``-`` - is uncovered without count.
49* A file becomes a source file unit, its functions function units, each spanning its start to its end line.
50* The format has no excluded lines, no branch targets and no units but functions; the conditions of MC/DC coverage
51 have no counterpart in the common model.
53.. rubric:: Example
55.. code-block:: Python
57 from pathlib import Path
58 from pyEDAA.Reports.CodeCoverage.LCOV import Document
60 tracefile = Document(Path("coverage.info"), analyzeAndConvert=True)
61 summary = tracefile.ToCoverageSummary()
62 for file in summary.IterateFiles():
63 print(f"{file.Path}: {file.LineCoverage:.1%}")
64"""
65from __future__ import annotations
67from pathlib import Path
68from re import Pattern, compile as re_compile
69from typing import Mapping, Optional as Nullable
71from pyTooling.Common import getFullyQualifiedName
72from pyTooling.Decorators import export, readonly
73from pyTooling.MetaClasses import ExtendedType
74from pyTooling.Stopwatch import Stopwatch
76from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, CodeCoverageError, CoverageSummary
77from pyEDAA.Reports.CodeCoverage import Document as cc_Document, File as cc_File, Function as cc_Function
78from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile
81__all__ = ["RECORD_PATTERNS", "RECORD_SYNTAX"]
83#: The pattern of each record's value - the text after ``<key>:`` -, by key.
84RECORD_PATTERNS: dict[str, Pattern[str]] = {
85 "TN": re_compile(r"(.*)"),
86 "SF": re_compile(r"(.+)"),
87 "VER": re_compile(r"(.+)"),
88 "FN": re_compile(r"([1-9]\d*),(?:([1-9]\d*),)?(.+)"),
89 "FNDA": re_compile(r"(\d+),(.+)"),
90 "FNL": re_compile(r"(\d+),([1-9]\d*)(?:,([1-9]\d*))?"),
91 "FNA": re_compile(r"(\d+),(\d+),(.+)"),
92 "FNF": re_compile(r"(\d+)"),
93 "FNH": re_compile(r"(\d+)"),
94 "BRDA": re_compile(r"([1-9]\d*),(e?)(\d+),(.+),(\d+|-)"),
95 "BRF": re_compile(r"(\d+)"),
96 "BRH": re_compile(r"(\d+)"),
97 "MCDC": re_compile(r"([1-9]\d*),(\d+),([tf]),(\d+),(\d+),(.+)"),
98 "MCF": re_compile(r"(\d+)"),
99 "MCH": re_compile(r"(\d+)"),
100 "DA": re_compile(r"([1-9]\d*),(\d+)(?:,([^,\s]+))?"),
101 "LF": re_compile(r"(\d+)"),
102 "LH": re_compile(r"(\d+)"),
103}
105#: The syntax of each record, as :manpage:`geninfo(1)` states it, by key.
106RECORD_SYNTAX: dict[str, str] = {
107 "TN": "TN:<test name>",
108 "SF": "SF:<path to the source file>",
109 "VER": "VER:<version ID>",
110 "FN": "FN:<line number of function start>,[<line number of function end>,]<function name>",
111 "FNDA": "FNDA:<execution count>,<function name>",
112 "FNL": "FNL:<index>,<line number of function start>[,<line number of function end>]",
113 "FNA": "FNA:<index>,<execution count>,<name>",
114 "FNF": "FNF:<number of functions found>",
115 "FNH": "FNH:<number of functions hit>",
116 "BRDA": "BRDA:<line number>,[<exception>]<block>,<branch>,<taken>",
117 "BRF": "BRF:<number of branches found>",
118 "BRH": "BRH:<number of branches hit>",
119 "MCDC": "MCDC:<line number>,<group size>,<sense>,<taken>,<index>,<expression>",
120 "MCF": "MCF:<number of conditions found>",
121 "MCH": "MCH:<number of conditions hit>",
122 "DA": "DA:<line number>,<execution count>[,<checksum>]",
123 "LF": "LF:<number of instrumented lines>",
124 "LH": "LH:<number of lines with a non-zero execution count>",
125 "end_of_record": "end_of_record",
126}
129@export
130class Line(metaclass=ExtendedType, slots=True):
131 """
132 A ``DA`` record: how often a line ran, and the line's checksum.
133 """
135 _parent: Nullable[Section] #: The section the line belongs to.
136 _number: int #: Line number.
137 _count: int #: How often the line ran; the sum, if the section lists the line twice.
138 _checksum: Nullable[str] #: Checksum of the line's source text, if stated.
140 def __init__(
141 self,
142 number: int,
143 count: int,
144 checksum: Nullable[str] = None,
145 *,
146 parent: Nullable[Section] = None
147 ) -> None:
148 """
149 Initialize a line, and add it to the lines of its section.
151 :param number: Line number.
152 :param count: How often the line ran.
153 :param checksum: Optional, checksum of the line's source text. Default: ``None``.
154 :param parent: Optional, the section the line belongs to; the line is added to its lines by
155 :attr:`Number`. Default: ``None``.
156 :raises ValueError: If parameter ``number`` is ``None``.
157 :raises TypeError: If parameter ``number`` isn't of type :class:`int`.
158 :raises ValueError: If parameter ``number`` is less than 1.
159 :raises ValueError: If parameter ``count`` is ``None``.
160 :raises TypeError: If parameter ``count`` isn't of type :class:`int`.
161 :raises ValueError: If parameter ``count`` is negative.
162 :raises TypeError: If parameter ``checksum`` isn't of type :class:`str`.
163 :raises ValueError: If parameter ``checksum`` is empty.
164 :raises TypeError: If parameter ``parent`` isn't of type :class:`Section`.
165 :raises CodeCoverageError: If the section already has a line of this number.
166 """
167 if number is None:
168 raise ValueError("Parameter 'number' is None.")
169 elif not isinstance(number, int):
170 ex = TypeError("Parameter 'number' is not of type 'int'.")
171 ex.add_note(f"Got type '{getFullyQualifiedName(number)}'.")
172 raise ex
173 elif number < 1:
174 ex = ValueError("Parameter 'number' is less than 1.")
175 ex.add_note(f"Got value '{number}'.")
176 raise ex
178 if count is None:
179 raise ValueError("Parameter 'count' is None.")
180 elif not isinstance(count, int):
181 ex = TypeError("Parameter 'count' is not of type 'int'.")
182 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
183 raise ex
184 elif count < 0:
185 ex = ValueError("Parameter 'count' is negative.")
186 ex.add_note(f"Got value '{count}'.")
187 raise ex
189 if checksum is not None and not isinstance(checksum, str):
190 ex = TypeError("Parameter 'checksum' is not of type 'str'.")
191 ex.add_note(f"Got type '{getFullyQualifiedName(checksum)}'.")
192 raise ex
193 elif checksum == "":
194 raise ValueError("Parameter 'checksum' is empty.")
196 if parent is not None and not isinstance(parent, Section):
197 ex = TypeError("Parameter 'parent' is not of type 'Section'.")
198 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
199 raise ex
201 self._parent = parent
202 self._number = number
203 self._count = count
204 self._checksum = checksum
206 if parent is not None:
207 if number in parent._lines:
208 raise CodeCoverageError(f"Line {number} of the section of '{parent._sourceFile.as_posix()}' is added twice.")
210 parent._lines[number] = self
212 @readonly
213 def Parent(self) -> Nullable[Section]:
214 """
215 Read-only property to access the section the line belongs to (:attr:`_parent`).
217 :returns: The section; ``None`` if the line belongs to no section.
218 """
219 return self._parent
221 @readonly
222 def Number(self) -> int:
223 """
224 Read-only property to access the line number (:attr:`_number`).
226 :returns: The line number.
227 """
228 return self._number
230 @readonly
231 def Count(self) -> int:
232 """
233 Read-only property to access how often the line ran (:attr:`_count`).
235 :returns: The execution count.
236 """
237 return self._count
239 @readonly
240 def Checksum(self) -> Nullable[str]:
241 """
242 Read-only property to access the checksum of the line's source text (:attr:`_checksum`).
244 lcov writes an MD5 hash in base64.
246 :returns: The checksum, or ``None`` if not stated.
247 """
248 return self._checksum
251@export
252class Branch(metaclass=ExtendedType, slots=True):
253 """
254 A ``BRDA`` record: a branch of a line, and how often it was taken.
255 """
257 _parent: Nullable[Section] #: The section the branch belongs to.
258 _lineNumber: int #: Line number of the branch.
259 _block: int #: Number of the branch's block - the conditional - in its line.
260 _expression: str #: Index or expression identifying the branch in its block.
261 _taken: Nullable[int] #: How often the branch was taken; ``None``, if it was never evaluated.
262 _isException: bool #: Whether the branch is taken by an exception.
264 def __init__(
265 self,
266 lineNumber: int,
267 block: int,
268 expression: str,
269 taken: Nullable[int],
270 isException: bool,
271 *,
272 parent: Nullable[Section] = None
273 ) -> None:
274 """
275 Initialize a branch, and append it to the branches of its section.
277 :param lineNumber: Line number of the branch.
278 :param block: Number of the branch's block in its line.
279 :param expression: Index or expression identifying the branch in its block.
280 :param taken: How often the branch was taken; ``None``, if it was never evaluated.
281 :param isException: Whether the branch is taken by an exception.
282 :param parent: Optional, the section the branch belongs to; the branch is appended to its branches.
283 Default: ``None``.
284 :raises ValueError: If parameter ``lineNumber`` is ``None``.
285 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
286 :raises ValueError: If parameter ``lineNumber`` is less than 1.
287 :raises ValueError: If parameter ``block`` is ``None``.
288 :raises TypeError: If parameter ``block`` isn't of type :class:`int`.
289 :raises ValueError: If parameter ``block`` is negative.
290 :raises ValueError: If parameter ``expression`` is ``None``.
291 :raises TypeError: If parameter ``expression`` isn't of type :class:`str`.
292 :raises ValueError: If parameter ``expression`` is empty.
293 :raises TypeError: If parameter ``taken`` isn't of type :class:`int`.
294 :raises ValueError: If parameter ``taken`` is negative.
295 :raises ValueError: If parameter ``isException`` is ``None``.
296 :raises TypeError: If parameter ``isException`` isn't of type :class:`bool`.
297 :raises TypeError: If parameter ``parent`` isn't of type :class:`Section`.
298 """
299 if lineNumber is None:
300 raise ValueError("Parameter 'lineNumber' is None.")
301 elif not isinstance(lineNumber, int):
302 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
303 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
304 raise ex
305 elif lineNumber < 1:
306 ex = ValueError("Parameter 'lineNumber' is less than 1.")
307 ex.add_note(f"Got value '{lineNumber}'.")
308 raise ex
310 if block is None:
311 raise ValueError("Parameter 'block' is None.")
312 elif not isinstance(block, int):
313 ex = TypeError("Parameter 'block' is not of type 'int'.")
314 ex.add_note(f"Got type '{getFullyQualifiedName(block)}'.")
315 raise ex
316 elif block < 0:
317 ex = ValueError("Parameter 'block' is negative.")
318 ex.add_note(f"Got value '{block}'.")
319 raise ex
321 if expression is None:
322 raise ValueError("Parameter 'expression' is None.")
323 elif not isinstance(expression, str):
324 ex = TypeError("Parameter 'expression' is not of type 'str'.")
325 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.")
326 raise ex
327 elif expression == "":
328 raise ValueError("Parameter 'expression' is empty.")
330 if taken is not None and not isinstance(taken, int):
331 ex = TypeError("Parameter 'taken' is not of type 'int'.")
332 ex.add_note(f"Got type '{getFullyQualifiedName(taken)}'.")
333 raise ex
334 elif taken is not None and taken < 0:
335 ex = ValueError("Parameter 'taken' is negative.")
336 ex.add_note(f"Got value '{taken}'.")
337 raise ex
339 if isException is None:
340 raise ValueError("Parameter 'isException' is None.")
341 elif not isinstance(isException, bool):
342 ex = TypeError("Parameter 'isException' is not of type 'bool'.")
343 ex.add_note(f"Got type '{getFullyQualifiedName(isException)}'.")
344 raise ex
346 if parent is not None and not isinstance(parent, Section):
347 ex = TypeError("Parameter 'parent' is not of type 'Section'.")
348 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
349 raise ex
351 self._parent = parent
352 self._lineNumber = lineNumber
353 self._block = block
354 self._expression = expression
355 self._taken = taken
356 self._isException = isException
358 if parent is not None:
359 parent._branches.append(self)
361 @readonly
362 def Parent(self) -> Nullable[Section]:
363 """
364 Read-only property to access the section the branch belongs to (:attr:`_parent`).
366 :returns: The section; ``None`` if the branch belongs to no section.
367 """
368 return self._parent
370 @readonly
371 def LineNumber(self) -> int:
372 """
373 Read-only property to access the line number of the branch (:attr:`_lineNumber`).
375 :returns: The line number.
376 """
377 return self._lineNumber
379 @readonly
380 def Block(self) -> int:
381 """
382 Read-only property to access the number of the branch's block in its line (:attr:`_block`).
384 :returns: The block number, counted from 0.
385 """
386 return self._block
388 @readonly
389 def Expression(self) -> str:
390 """
391 Read-only property to access the index or expression identifying the branch in its block (:attr:`_expression`).
393 Each tool identifies a branch its own way, e.g. GCC by an index like ``1``, coverage.py by a text like
394 ``jump to line 8``.
396 :returns: The identifier.
397 """
398 return self._expression
400 @readonly
401 def Taken(self) -> Nullable[int]:
402 """
403 Read-only property to access how often the branch was taken (:attr:`_taken`).
405 :returns: The count, or ``None`` if the branch was never evaluated - stated as ``-``.
406 """
407 return self._taken
409 @readonly
410 def IsException(self) -> bool:
411 """
412 Read-only property to access whether the branch is taken by an exception (:attr:`_isException`).
414 :returns: ``True``, if the branch is marked by the prefix ``e``.
415 """
416 return self._isException
419@export
420class Condition(metaclass=ExtendedType, slots=True):
421 """
422 An ``MCDC`` record: whether the outcome of a line's expression changed, when one of its conditions changed.
423 """
425 _parent: Nullable[Section] #: The section the condition belongs to.
426 _lineNumber: int #: Line number of the expression.
427 _groupSize: int #: Number of conditions in the expression's group.
428 _sense: bool #: ``True`` for a change from false to true; ``False`` for the converse.
429 _taken: int #: How often - or whether - the condition was sensitized.
430 _index: int #: Index of the condition in its group.
431 _expression: str #: The condition's expression.
433 def __init__(
434 self,
435 lineNumber: int,
436 groupSize: int,
437 sense: bool,
438 taken: int,
439 index: int,
440 expression: str,
441 *,
442 parent: Nullable[Section] = None
443 ) -> None:
444 """
445 Initialize a condition, and append it to the conditions of its section.
447 :param lineNumber: Line number of the expression.
448 :param groupSize: Number of conditions in the expression's group.
449 :param sense: ``True`` for a change of the condition from false to true; ``False`` for the converse.
450 :param taken: How often - or whether - the condition was sensitized.
451 :param index: Index of the condition in its group.
452 :param expression: The condition's expression.
453 :param parent: Optional, the section the condition belongs to; the condition is appended to its conditions.
454 Default: ``None``.
455 :raises ValueError: If parameter ``lineNumber`` is ``None``.
456 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`.
457 :raises ValueError: If parameter ``lineNumber`` is less than 1.
458 :raises ValueError: If parameter ``groupSize`` is ``None``.
459 :raises TypeError: If parameter ``groupSize`` isn't of type :class:`int`.
460 :raises ValueError: If parameter ``groupSize`` is negative.
461 :raises ValueError: If parameter ``sense`` is ``None``.
462 :raises TypeError: If parameter ``sense`` isn't of type :class:`bool`.
463 :raises ValueError: If parameter ``taken`` is ``None``.
464 :raises TypeError: If parameter ``taken`` isn't of type :class:`int`.
465 :raises ValueError: If parameter ``taken`` is negative.
466 :raises ValueError: If parameter ``index`` is ``None``.
467 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
468 :raises ValueError: If parameter ``index`` is negative.
469 :raises ValueError: If parameter ``expression`` is ``None``.
470 :raises TypeError: If parameter ``expression`` isn't of type :class:`str`.
471 :raises ValueError: If parameter ``expression`` is empty.
472 :raises TypeError: If parameter ``parent`` isn't of type :class:`Section`.
473 """
474 if lineNumber is None:
475 raise ValueError("Parameter 'lineNumber' is None.")
476 elif not isinstance(lineNumber, int):
477 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.")
478 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.")
479 raise ex
480 elif lineNumber < 1:
481 ex = ValueError("Parameter 'lineNumber' is less than 1.")
482 ex.add_note(f"Got value '{lineNumber}'.")
483 raise ex
485 if groupSize is None:
486 raise ValueError("Parameter 'groupSize' is None.")
487 elif not isinstance(groupSize, int):
488 ex = TypeError("Parameter 'groupSize' is not of type 'int'.")
489 ex.add_note(f"Got type '{getFullyQualifiedName(groupSize)}'.")
490 raise ex
491 elif groupSize < 0:
492 ex = ValueError("Parameter 'groupSize' is negative.")
493 ex.add_note(f"Got value '{groupSize}'.")
494 raise ex
496 if sense is None:
497 raise ValueError("Parameter 'sense' is None.")
498 elif not isinstance(sense, bool):
499 ex = TypeError("Parameter 'sense' is not of type 'bool'.")
500 ex.add_note(f"Got type '{getFullyQualifiedName(sense)}'.")
501 raise ex
503 if taken is None:
504 raise ValueError("Parameter 'taken' is None.")
505 elif not isinstance(taken, int):
506 ex = TypeError("Parameter 'taken' is not of type 'int'.")
507 ex.add_note(f"Got type '{getFullyQualifiedName(taken)}'.")
508 raise ex
509 elif taken < 0:
510 ex = ValueError("Parameter 'taken' is negative.")
511 ex.add_note(f"Got value '{taken}'.")
512 raise ex
514 if index is None:
515 raise ValueError("Parameter 'index' is None.")
516 elif not isinstance(index, int):
517 ex = TypeError("Parameter 'index' is not of type 'int'.")
518 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.")
519 raise ex
520 elif index < 0:
521 ex = ValueError("Parameter 'index' is negative.")
522 ex.add_note(f"Got value '{index}'.")
523 raise ex
525 if expression is None:
526 raise ValueError("Parameter 'expression' is None.")
527 elif not isinstance(expression, str):
528 ex = TypeError("Parameter 'expression' is not of type 'str'.")
529 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.")
530 raise ex
531 elif expression == "":
532 raise ValueError("Parameter 'expression' is empty.")
534 if parent is not None and not isinstance(parent, Section):
535 ex = TypeError("Parameter 'parent' is not of type 'Section'.")
536 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
537 raise ex
539 self._parent = parent
540 self._lineNumber = lineNumber
541 self._groupSize = groupSize
542 self._sense = sense
543 self._taken = taken
544 self._index = index
545 self._expression = expression
547 if parent is not None:
548 parent._conditions.append(self)
550 @readonly
551 def Parent(self) -> Nullable[Section]:
552 """
553 Read-only property to access the section the condition belongs to (:attr:`_parent`).
555 :returns: The section; ``None`` if the condition belongs to no section.
556 """
557 return self._parent
559 @readonly
560 def LineNumber(self) -> int:
561 """
562 Read-only property to access the line number of the expression (:attr:`_lineNumber`).
564 :returns: The line number.
565 """
566 return self._lineNumber
568 @readonly
569 def GroupSize(self) -> int:
570 """
571 Read-only property to access the number of conditions in the expression's group (:attr:`_groupSize`).
573 :returns: The group size.
574 """
575 return self._groupSize
577 @readonly
578 def Sense(self) -> bool:
579 """
580 Read-only property to access the sense of the condition's change (:attr:`_sense`).
582 :returns: ``True`` for a change from false to true - stated as ``t`` -, ``False`` for the converse - ``f``.
583 """
584 return self._sense
586 @readonly
587 def Taken(self) -> int:
588 """
589 Read-only property to access how often - or whether - the condition was sensitized (:attr:`_taken`).
591 :returns: The count; ``0``, if the condition was never sensitized.
592 """
593 return self._taken
595 @readonly
596 def Index(self) -> int:
597 """
598 Read-only property to access the index of the condition in its group (:attr:`_index`).
600 :returns: The index, counted from 0.
601 """
602 return self._index
604 @readonly
605 def Expression(self) -> str:
606 """
607 Read-only property to access the condition's expression (:attr:`_expression`).
609 The text depends on the tool, e.g. GCC states ``0``.
611 :returns: The expression.
612 """
613 return self._expression
616@export
617class Function(metaclass=ExtendedType, slots=True):
618 """
619 A function: an ``FN`` record with the count of its ``FNDA`` record, or an ``FNL`` record with its ``FNA`` aliases.
621 The aliases of a function - e.g. instances of a C++ template - share its lines.
622 """
624 _parent: Nullable[Section] #: The section the function belongs to.
625 _index: Nullable[int] #: Index of the ``FNL`` record; ``None`` for an ``FN`` record.
626 _startLine: int #: The function's first line.
627 _endLine: Nullable[int] #: The function's last line, if stated.
628 _aliases: dict[str, Nullable[int]] #: The function's names, and how often each was called, if stated.
630 def __init__(
631 self,
632 startLine: int,
633 endLine: Nullable[int],
634 aliases: Mapping[str, Nullable[int]],
635 index: Nullable[int] = None,
636 *,
637 parent: Nullable[Section] = None
638 ) -> None:
639 """
640 Initialize a function, and append it to the functions of its section.
642 :param startLine: The function's first line.
643 :param endLine: The function's last line, or ``None`` if not stated.
644 :param aliases: The function's names, and how often each was called - ``None``, if not stated -; the first
645 name is the function's name.
646 :param index: Optional, index of the ``FNL`` record. Default: ``None``, for an ``FN`` record.
647 :param parent: Optional, the section the function belongs to; the function is appended to its functions.
648 Default: ``None``.
649 :raises ValueError: If parameter ``startLine`` is ``None``.
650 :raises TypeError: If parameter ``startLine`` isn't of type :class:`int`.
651 :raises ValueError: If parameter ``startLine`` is less than 1.
652 :raises TypeError: If parameter ``endLine`` isn't of type :class:`int`.
653 :raises ValueError: If parameter ``endLine`` is less than 1.
654 :raises ValueError: If parameter ``aliases`` is ``None``.
655 :raises TypeError: If parameter ``aliases`` isn't a mapping.
656 :raises ValueError: If parameter ``aliases`` is empty.
657 :raises TypeError: If parameter ``aliases`` contains a name not of type :class:`str`.
658 :raises ValueError: If parameter ``aliases`` contains an empty name.
659 :raises TypeError: If parameter ``aliases`` contains a count not of type :class:`int`.
660 :raises ValueError: If parameter ``aliases`` contains a negative count.
661 :raises TypeError: If parameter ``index`` isn't of type :class:`int`.
662 :raises ValueError: If parameter ``index`` is negative.
663 :raises TypeError: If parameter ``parent`` isn't of type :class:`Section`.
664 """
665 if startLine is None:
666 raise ValueError("Parameter 'startLine' is None.")
667 elif not isinstance(startLine, int):
668 ex = TypeError("Parameter 'startLine' is not of type 'int'.")
669 ex.add_note(f"Got type '{getFullyQualifiedName(startLine)}'.")
670 raise ex
671 elif startLine < 1:
672 ex = ValueError("Parameter 'startLine' is less than 1.")
673 ex.add_note(f"Got value '{startLine}'.")
674 raise ex
676 if endLine is not None and not isinstance(endLine, int):
677 ex = TypeError("Parameter 'endLine' is not of type 'int'.")
678 ex.add_note(f"Got type '{getFullyQualifiedName(endLine)}'.")
679 raise ex
680 elif endLine is not None and endLine < 1:
681 ex = ValueError("Parameter 'endLine' is less than 1.")
682 ex.add_note(f"Got value '{endLine}'.")
683 raise ex
685 if aliases is None:
686 raise ValueError("Parameter 'aliases' is None.")
687 elif not isinstance(aliases, Mapping):
688 ex = TypeError("Parameter 'aliases' is not a mapping.")
689 ex.add_note(f"Got type '{getFullyQualifiedName(aliases)}'.")
690 raise ex
691 elif len(aliases) == 0:
692 raise ValueError("Parameter 'aliases' is empty.")
694 if index is not None and not isinstance(index, int):
695 ex = TypeError("Parameter 'index' is not of type 'int'.")
696 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.")
697 raise ex
698 elif index is not None and index < 0:
699 ex = ValueError("Parameter 'index' is negative.")
700 ex.add_note(f"Got value '{index}'.")
701 raise ex
703 if parent is not None and not isinstance(parent, Section):
704 ex = TypeError("Parameter 'parent' is not of type 'Section'.")
705 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
706 raise ex
708 self._parent = parent
709 self._index = index
710 self._startLine = startLine
711 self._endLine = endLine
712 self._aliases = {}
714 for name, count in aliases.items():
715 if not isinstance(name, str):
716 ex = TypeError("Parameter 'aliases' contains a name not of type 'str'.")
717 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
718 raise ex
719 elif name == "":
720 raise ValueError("Parameter 'aliases' contains an empty name.")
721 elif count is not None and not isinstance(count, int):
722 ex = TypeError("Parameter 'aliases' contains a count not of type 'int'.")
723 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.")
724 raise ex
725 elif count is not None and count < 0:
726 ex = ValueError("Parameter 'aliases' contains a negative count.")
727 ex.add_note(f"Got value '{count}'.")
728 raise ex
730 self._aliases[name] = count
732 if parent is not None:
733 parent._functions.append(self)
735 @readonly
736 def Parent(self) -> Nullable[Section]:
737 """
738 Read-only property to access the section the function belongs to (:attr:`_parent`).
740 :returns: The section; ``None`` if the function belongs to no section.
741 """
742 return self._parent
744 @readonly
745 def Index(self) -> Nullable[int]:
746 """
747 Read-only property to access the index of the ``FNL`` record (:attr:`_index`).
749 :returns: The index, or ``None`` for an ``FN`` record.
750 """
751 return self._index
753 @readonly
754 def StartLine(self) -> int:
755 """
756 Read-only property to access the function's first line (:attr:`_startLine`).
758 :returns: The line number.
759 """
760 return self._startLine
762 @readonly
763 def EndLine(self) -> Nullable[int]:
764 """
765 Read-only property to access the function's last line (:attr:`_endLine`).
767 Not every tool states it, e.g. llvm-cov doesn't.
769 :returns: The line number, or ``None`` if not stated.
770 """
771 return self._endLine
773 @readonly
774 def Aliases(self) -> dict[str, Nullable[int]]:
775 """
776 Read-only property to access the function's names, and how often each was called (:attr:`_aliases`).
778 :returns: The counts by name, in the tracefile's order; a count is ``None``, if no ``FNDA`` record states it.
779 """
780 return self._aliases
782 @readonly
783 def Name(self) -> str:
784 """
785 Read-only property to return the function's name: its first alias.
787 :returns: The name.
788 """
789 return next(iter(self._aliases))
791 @readonly
792 def Count(self) -> Nullable[int]:
793 """
794 Read-only property to return how often the function was called: the sum over its aliases.
796 :returns: The count, or ``None`` if no alias' count is stated.
797 """
798 counts = [count for count in self._aliases.values() if count is not None]
799 return sum(counts) if len(counts) > 0 else None
802@export
803class Section(metaclass=ExtendedType, slots=True):
804 """
805 A section from ``SF`` to ``end_of_record``: a source file's coverage, as a test measured it, and the summaries the
806 section states.
807 """
809 _parent: Nullable[Tracefile] #: The tracefile the section belongs to.
810 _testName: str #: Name of the test, stated by the last ``TN`` record before the section.
811 _sourceFile: Path #: Path of the source file.
812 _version: Nullable[str] #: Version ID of the source file, if stated.
813 _functions: list[Function] #: The functions, in the tracefile's order.
814 _lines: dict[int, Line] #: The lines, by number.
815 _branches: list[Branch] #: The branches, in the tracefile's order.
816 _conditions: list[Condition] #: The conditions of MC/DC coverage, in the tracefile's order.
817 _functionsFound: Nullable[int] #: Number of functions, as the section states it.
818 _functionsHit: Nullable[int] #: Number of functions called, as the section states it.
819 _branchesFound: Nullable[int] #: Number of branches, as the section states it.
820 _branchesHit: Nullable[int] #: Number of branches taken, as the section states it.
821 _conditionsFound: Nullable[int] #: Number of conditions, as the section states it.
822 _conditionsHit: Nullable[int] #: Number of conditions sensitized, as the section states it.
823 _linesFound: Nullable[int] #: Number of instrumented lines, as the section states it.
824 _linesHit: Nullable[int] #: Number of lines, which ran, as the section states it.
826 def __init__(self, testName: str, sourceFile: Path, *, parent: Nullable[Tracefile] = None) -> None:
827 """
828 Initialize an empty section, and append it to the sections of its tracefile.
830 :param testName: Name of the test; empty, if not stated.
831 :param sourceFile: Path of the source file.
832 :param parent: Optional, the tracefile the section belongs to; the section is appended to its sections.
833 Default: ``None``.
834 :raises ValueError: If parameter ``testName`` is ``None``.
835 :raises TypeError: If parameter ``testName`` isn't of type :class:`str`.
836 :raises ValueError: If parameter ``sourceFile`` is ``None``.
837 :raises TypeError: If parameter ``sourceFile`` isn't of type :class:`~pathlib.Path`.
838 :raises TypeError: If parameter ``parent`` isn't of type :class:`Tracefile`.
839 """
840 if testName is None:
841 raise ValueError("Parameter 'testName' is None.")
842 elif not isinstance(testName, str):
843 ex = TypeError("Parameter 'testName' is not of type 'str'.")
844 ex.add_note(f"Got type '{getFullyQualifiedName(testName)}'.")
845 raise ex
847 if sourceFile is None:
848 raise ValueError("Parameter 'sourceFile' is None.")
849 elif not isinstance(sourceFile, Path):
850 ex = TypeError("Parameter 'sourceFile' is not of type 'Path'.")
851 ex.add_note(f"Got type '{getFullyQualifiedName(sourceFile)}'.")
852 raise ex
854 if parent is not None and not isinstance(parent, Tracefile):
855 ex = TypeError("Parameter 'parent' is not of type 'Tracefile'.")
856 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
857 raise ex
859 self._parent = parent
860 self._testName = testName
861 self._sourceFile = sourceFile
862 self._version = None
863 self._functions = []
864 self._lines = {}
865 self._branches = []
866 self._conditions = []
867 self._functionsFound = None
868 self._functionsHit = None
869 self._branchesFound = None
870 self._branchesHit = None
871 self._conditionsFound = None
872 self._conditionsHit = None
873 self._linesFound = None
874 self._linesHit = None
876 if parent is not None:
877 parent._sections.append(self)
879 @readonly
880 def Parent(self) -> Nullable[Tracefile]:
881 """
882 Read-only property to access the tracefile the section belongs to (:attr:`_parent`).
884 :returns: The tracefile; ``None`` if the section belongs to no tracefile.
885 """
886 return self._parent
888 @readonly
889 def TestName(self) -> str:
890 """
891 Read-only property to access the name of the test (:attr:`_testName`).
893 :returns: The name; empty, if no ``TN`` record states it.
894 """
895 return self._testName
897 @readonly
898 def SourceFile(self) -> Path:
899 """
900 Read-only property to access the path of the source file (:attr:`_sourceFile`).
902 :returns: The path, as the tracefile states it: absolute, or relative to the directory the tool ran in; a
903 backslash of a tracefile written on Windows is a separator.
904 """
905 return self._sourceFile
907 @readonly
908 def Version(self) -> Nullable[str]:
909 """
910 Read-only property to access the version ID of the source file (:attr:`_version`).
912 :returns: The version ID, or ``None`` if not stated.
913 """
914 return self._version
916 @readonly
917 def Functions(self) -> list[Function]:
918 """
919 Read-only property to access the functions (:attr:`_functions`).
921 :returns: The functions, in the tracefile's order.
922 """
923 return self._functions
925 @readonly
926 def Lines(self) -> dict[int, Line]:
927 """
928 Read-only property to access the lines (:attr:`_lines`).
930 :returns: The lines, by number.
931 """
932 return self._lines
934 @readonly
935 def Branches(self) -> list[Branch]:
936 """
937 Read-only property to access the branches (:attr:`_branches`).
939 :returns: The branches, in the tracefile's order.
940 """
941 return self._branches
943 @readonly
944 def Conditions(self) -> list[Condition]:
945 """
946 Read-only property to access the conditions of MC/DC coverage (:attr:`_conditions`).
948 :returns: The conditions, in the tracefile's order.
949 """
950 return self._conditions
952 @readonly
953 def FunctionsFound(self) -> Nullable[int]:
954 """
955 Read-only property to access the number of functions, as the section states it (:attr:`_functionsFound`).
957 :returns: The number from record ``FNF``, or ``None`` if not stated.
958 """
959 return self._functionsFound
961 @readonly
962 def FunctionsHit(self) -> Nullable[int]:
963 """
964 Read-only property to access the number of functions called, as the section states it (:attr:`_functionsHit`).
966 :returns: The number from record ``FNH``, or ``None`` if not stated.
967 """
968 return self._functionsHit
970 @readonly
971 def BranchesFound(self) -> Nullable[int]:
972 """
973 Read-only property to access the number of branches, as the section states it (:attr:`_branchesFound`).
975 :returns: The number from record ``BRF``, or ``None`` if not stated.
976 """
977 return self._branchesFound
979 @readonly
980 def BranchesHit(self) -> Nullable[int]:
981 """
982 Read-only property to access the number of branches taken, as the section states it (:attr:`_branchesHit`).
984 :returns: The number from record ``BRH``, or ``None`` if not stated.
985 """
986 return self._branchesHit
988 @readonly
989 def ConditionsFound(self) -> Nullable[int]:
990 """
991 Read-only property to access the number of conditions, as the section states it (:attr:`_conditionsFound`).
993 :returns: The number from record ``MCF``, or ``None`` if not stated.
994 """
995 return self._conditionsFound
997 @readonly
998 def ConditionsHit(self) -> Nullable[int]:
999 """
1000 Read-only property to access the number of conditions sensitized, as the section states it
1001 (:attr:`_conditionsHit`).
1003 :returns: The number from record ``MCH``, or ``None`` if not stated.
1004 """
1005 return self._conditionsHit
1007 @readonly
1008 def LinesFound(self) -> Nullable[int]:
1009 """
1010 Read-only property to access the number of instrumented lines, as the section states it (:attr:`_linesFound`).
1012 :returns: The number from record ``LF``, or ``None`` if not stated.
1013 """
1014 return self._linesFound
1016 @readonly
1017 def LinesHit(self) -> Nullable[int]:
1018 """
1019 Read-only property to access the number of lines, which ran, as the section states it (:attr:`_linesHit`).
1021 :returns: The number from record ``LH``, or ``None`` if not stated.
1022 """
1023 return self._linesHit
1026@export
1027class Tracefile(metaclass=ExtendedType, mixin=True):
1028 """
1029 A tracefile: its comments and sections.
1030 """
1032 _comments: list[str] #: The comments, without the leading ``#``.
1033 _sections: list[Section] #: The sections, in the tracefile's order.
1035 def __init__(self) -> None:
1036 """
1037 Initialize an empty tracefile.
1038 """
1039 self._comments = []
1040 self._sections = []
1042 @readonly
1043 def Comments(self) -> list[str]:
1044 """
1045 Read-only property to access the comments (:attr:`_comments`).
1047 A tool writes them on request, e.g. ``lcov --comment``.
1049 :returns: The comments, without the leading ``#``.
1050 """
1051 return self._comments
1053 @readonly
1054 def Sections(self) -> list[Section]:
1055 """
1056 Read-only property to access the sections (:attr:`_sections`).
1058 :returns: The sections, in the tracefile's order; a source file has a section per test.
1059 """
1060 return self._sections
1063@export
1064class Document(cc_Document, Tracefile):
1065 """
1066 An lcov tracefile: read into the format's model, and converted to the common model.
1067 """
1069 _records: Nullable[list[tuple[int, str, str, tuple[Nullable[str], ...]]]] #: The records, after :meth:`Analyze`.
1071 def __init__(self, tracefile: Path, analyzeAndConvert: bool = False) -> None:
1072 """
1073 Initialize the tracefile, and optionally read it.
1075 :param tracefile: Path to the tracefile.
1076 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``.
1077 """
1078 super().__init__(tracefile)
1079 Tracefile.__init__(self)
1081 self._records = None
1083 if analyzeAndConvert:
1084 self.Analyze()
1085 self.Convert()
1087 def Analyze(self) -> None:
1088 """
1089 Read the tracefile and parse each line into a record: a comment, or a key and its values.
1091 Empty lines are skipped.
1093 :raises CodeCoverageError: If the file doesn't exist.
1094 :raises CodeCoverageError: If the file can't be read.
1095 :raises CodeCoverageError: If the file isn't UTF-8 encoded.
1096 :raises CodeCoverageError: If a line is no record lcov knows. |br|
1097 The exception notes the line and the known records.
1098 :raises CodeCoverageError: If a record's values don't match its syntax. |br|
1099 The exception notes the line and the record's syntax.
1100 """
1101 if not self._path.exists():
1102 raise CodeCoverageError(f"lcov tracefile '{self._path}' does not exist.") \
1103 from FileNotFoundError(f"File '{self._path}' not found.")
1105 with Stopwatch() as sw:
1106 try:
1107 content = self._path.read_text(encoding="utf-8")
1108 except OSError as ex:
1109 raise CodeCoverageError(f"Couldn't read lcov tracefile '{self._path}'.") from ex
1110 except UnicodeDecodeError as ex:
1111 raise CodeCoverageError(f"lcov tracefile '{self._path}' is not UTF-8 encoded.") from ex
1113 records: list[tuple[int, str, str, tuple[Nullable[str], ...]]] = []
1114 for lineNumber, text in enumerate(content.splitlines(), start=1):
1115 text = text.rstrip()
1116 if text == "":
1117 continue
1118 elif text.startswith("#"):
1119 records.append((lineNumber, text, "#", (text[1:], )))
1120 continue
1121 elif text == "end_of_record":
1122 records.append((lineNumber, text, text, ()))
1123 continue
1125 key, colon, value = text.partition(":")
1126 if colon == "" or (pattern := RECORD_PATTERNS.get(key)) is None:
1127 ex = CodeCoverageError(f"Unknown record '{key}' in lcov tracefile '{self._path}'.")
1128 ex.add_note(f"Line {lineNumber}: '{text}'")
1129 ex.add_note(f"Known records: {', '.join(RECORD_SYNTAX)}")
1130 raise ex
1131 elif (match := pattern.fullmatch(value)) is None:
1132 ex = CodeCoverageError(f"Malformed record '{key}' in lcov tracefile '{self._path}'.")
1133 ex.add_note(f"Line {lineNumber}: '{text}'")
1134 ex.add_note(f"Expected '{RECORD_SYNTAX[key]}'.")
1135 raise ex
1137 records.append((lineNumber, text, key, match.groups()))
1139 self._records = records
1141 self._analysisDuration = sw.Duration
1143 def Convert(self) -> None:
1144 """
1145 Convert the parsed records to the format's model: comments, and sections from ``SF`` to ``end_of_record``.
1147 A ``TN`` record names the test of the sections after it. A line listed twice in a section - as geninfo may write
1148 it - counts the sum.
1150 :raises CodeCoverageError: If the tracefile was not analyzed before. |br|
1151 Call 'Document.Analyze()' or create the document using
1152 'Document(path, analyzeAndConvert=True)'.
1153 :raises CodeCoverageError: If a section starts before the previous one ended. |br|
1154 The exception notes the line.
1155 :raises CodeCoverageError: If a record other than a comment, ``TN`` or ``SF`` is outside of a section. |br|
1156 The exception notes the line.
1157 :raises CodeCoverageError: If a ``TN`` record is inside of a section. |br|
1158 The exception notes the line.
1159 :raises CodeCoverageError: If a section states a function name or ``FNL`` index twice. |br|
1160 The exception notes the line.
1161 :raises CodeCoverageError: If an ``FNDA`` or ``FNA`` record refers to an unknown function. |br|
1162 The exception notes the line.
1163 :raises CodeCoverageError: If an ``FNL`` record has no ``FNA`` record. |br|
1164 The exception notes the section's last line.
1165 :raises CodeCoverageError: If the last section doesn't end with ``end_of_record``. |br|
1166 The exception notes the section's first line.
1167 """
1168 if self._records is None:
1169 ex = CodeCoverageError(f"lcov tracefile '{self._path}' needs to be read and analyzed by a line parser.")
1170 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
1171 raise ex
1173 with Stopwatch() as sw:
1174 testName = ""
1175 section: Nullable[Section] = None
1176 sectionLineNumber = 0
1177 functions: list[tuple[int, Nullable[int], Nullable[int], dict[str, Nullable[int]]]] = []
1178 functionsByName: dict[str, dict[str, Nullable[int]]] = {}
1179 functionsByIndex: dict[int, dict[str, Nullable[int]]] = {}
1180 for lineNumber, text, key, values in self._records:
1181 if key == "#":
1182 self._comments.append(values[0])
1183 elif key == "SF":
1184 if section is not None:
1185 ex = CodeCoverageError(f"Record 'SF' inside the section of '{section._sourceFile}' in '{self._path}'.")
1186 ex.add_note(f"Line {lineNumber}: '{text}'")
1187 ex.add_note(f"End the section of line {sectionLineNumber} with 'end_of_record'.")
1188 raise ex
1190 section = Section(testName, Path(values[0].replace("\\", "/")), parent=self)
1191 sectionLineNumber = lineNumber
1192 functions = []
1193 functionsByName = {}
1194 functionsByIndex = {}
1195 elif key == "TN":
1196 if section is not None:
1197 ex = CodeCoverageError(f"Record 'TN' inside the section of '{section._sourceFile}' in '{self._path}'.")
1198 ex.add_note(f"Line {lineNumber}: '{text}'")
1199 raise ex
1201 testName = values[0]
1202 elif section is None:
1203 ex = CodeCoverageError(f"Record '{key}' outside of a section in lcov tracefile '{self._path}'.")
1204 ex.add_note(f"Line {lineNumber}: '{text}'")
1205 ex.add_note(f"A section starts with '{RECORD_SYNTAX['SF']}'.")
1206 raise ex
1207 elif key == "end_of_record":
1208 for index, aliases in functionsByIndex.items():
1209 if len(aliases) == 0:
1210 ex = CodeCoverageError(f"Function index {index} has no alias in the section of '{section._sourceFile}'.")
1211 ex.add_note(f"The section ends in line {lineNumber}.")
1212 ex.add_note(f"Name the function by '{RECORD_SYNTAX['FNA']}'.")
1213 raise ex
1215 for startLine, endLine, index, aliases in functions:
1216 Function(startLine, endLine, aliases, index, parent=section)
1218 section = None
1219 elif key == "DA":
1220 number = int(values[0])
1221 count = int(values[1])
1222 if (line := section._lines.get(number)) is None:
1223 Line(number, count, values[2], parent=section)
1224 else:
1225 line._count += count
1226 elif key == "BRDA":
1227 taken = None if values[4] == "-" else int(values[4])
1228 Branch(int(values[0]), int(values[2]), values[3], taken, values[1] == "e", parent=section)
1229 elif key == "MCDC":
1230 Condition(
1231 int(values[0]), int(values[1]), values[2] == "t", int(values[3]), int(values[4]), values[5], parent=section
1232 )
1233 elif key == "FN":
1234 if (name := values[2]) in functionsByName:
1235 ex = CodeCoverageError(f"Function '{name}' is stated twice in the section of '{section._sourceFile}'.")
1236 ex.add_note(f"Line {lineNumber}: '{text}'")
1237 raise ex
1239 aliases = {name: None}
1240 functions.append((int(values[0]), None if values[1] is None else int(values[1]), None, aliases))
1241 functionsByName[name] = aliases
1242 elif key == "FNL":
1243 if (index := int(values[0])) in functionsByIndex:
1244 ex = CodeCoverageError(f"Function index {index} is stated twice in the section of '{section._sourceFile}'.")
1245 ex.add_note(f"Line {lineNumber}: '{text}'")
1246 raise ex
1248 aliases = {}
1249 functions.append((int(values[1]), None if values[2] is None else int(values[2]), index, aliases))
1250 functionsByIndex[index] = aliases
1251 elif key == "FNDA" or key == "FNA":
1252 if key == "FNDA":
1253 count = int(values[0])
1254 name = values[1]
1255 aliases = functionsByName.get(name)
1256 else:
1257 count = int(values[1])
1258 name = values[2]
1259 aliases = functionsByIndex.get(int(values[0]))
1261 if aliases is None:
1262 ex = CodeCoverageError(f"Record '{key}' refers to an unknown function in '{self._path}'.")
1263 ex.add_note(f"Line {lineNumber}: '{text}'")
1264 raise ex
1266 previous = aliases.get(name)
1267 aliases[name] = count if previous is None else previous + count
1268 elif key == "VER":
1269 section._version = values[0]
1270 elif key == "FNF":
1271 section._functionsFound = int(values[0])
1272 elif key == "FNH":
1273 section._functionsHit = int(values[0])
1274 elif key == "BRF":
1275 section._branchesFound = int(values[0])
1276 elif key == "BRH":
1277 section._branchesHit = int(values[0])
1278 elif key == "MCF":
1279 section._conditionsFound = int(values[0])
1280 elif key == "MCH":
1281 section._conditionsHit = int(values[0])
1282 elif key == "LF":
1283 section._linesFound = int(values[0])
1284 else:
1285 section._linesHit = int(values[0])
1287 if section is not None:
1288 ex = CodeCoverageError(f"Missing 'end_of_record' at the end of lcov tracefile '{self._path}'.")
1289 ex.add_note(f"The section of '{section._sourceFile}' starts in line {sectionLineNumber}.")
1290 raise ex
1292 self._conversionDuration = sw.Duration
1294 def ToCoverageSummary(self) -> CoverageSummary:
1295 """
1296 Convert the format's model to the common model, and aggregate it.
1298 The sections of one source file - one per test - are one file: the counts of a line, of a branch and of a function
1299 are added. A line is partially covered, if one of its branches wasn't taken; a line with branches, but without
1300 ``DA`` record, has an unknown state. A file becomes a source file unit, spanning its first to its last line, its
1301 functions become function units, each spanning its start to its end line; a function without end line - e.g. from
1302 llvm-cov - names its start line only.
1304 :returns: The report's root of the common model, named after the tracefile.
1305 :raises CodeCoverageError: If a file's path runs through another file.
1306 """
1307 summary = CoverageSummary(self._path.stem)
1309 files: dict[int, cc_File] = {}
1310 lineCounts: dict[int, dict[int, int]] = {}
1311 branchCounts: dict[int, dict[tuple[int, int, str, bool], Nullable[int]]] = {}
1312 functionCounts: dict[int, dict[str, tuple[int, Nullable[int], Nullable[int]]]] = {}
1313 for section in self._sections:
1314 file = summary.GetOrAddFile(section._sourceFile)
1315 files[id(file)] = file
1317 lines = lineCounts.setdefault(id(file), {})
1318 for number, line in section._lines.items():
1319 lines[number] = lines.get(number, 0) + line._count
1321 branches = branchCounts.setdefault(id(file), {})
1322 for branch in section._branches:
1323 key = (branch._lineNumber, branch._block, branch._expression, branch._isException)
1324 taken = branches.get(key)
1325 if branch._taken is not None:
1326 branches[key] = branch._taken if taken is None else taken + branch._taken
1327 elif key not in branches:
1328 branches[key] = None
1330 functions = functionCounts.setdefault(id(file), {})
1331 for function in section._functions:
1332 count = function.Count
1333 if (merged := functions.get(function.Name)) is not None and merged[2] is not None:
1334 count = merged[2] if count is None else merged[2] + count
1335 functions[function.Name] = (function._startLine, function._endLine, count)
1337 for fileID, file in files.items():
1338 self._ConvertLines(file, lineCounts[fileID], branchCounts[fileID])
1339 self._ConvertUnits(file, functionCounts[fileID], summary)
1341 summary.Aggregate()
1342 return summary
1344 @staticmethod
1345 def _ConvertLines(
1346 file: cc_File,
1347 lineCounts: dict[int, int],
1348 branchCounts: dict[tuple[int, int, str, bool], Nullable[int]]
1349 ) -> None:
1350 """
1351 Convert a file's merged line and branch counts to lines of the common model.
1353 :param file: The file of the common model.
1354 :param lineCounts: The execution counts, by line number.
1355 :param branchCounts: How often each branch was taken - ``None``, if never evaluated -, by line number, block,
1356 expression and exception flag.
1357 """
1358 branchesByLine: dict[int, list[Nullable[int]]] = {}
1359 for (lineNumber, *_), taken in branchCounts.items():
1360 branchesByLine.setdefault(lineNumber, []).append(taken)
1362 for lineNumber in sorted({*lineCounts, *branchesByLine}):
1363 takenCounts = branchesByLine.get(lineNumber, [])
1364 statuses = [
1365 LineCoverageStatus.Covered if taken is not None and taken > 0 else LineCoverageStatus.Uncovered
1366 for taken in takenCounts
1367 ]
1368 branches = [cc_Branch(status, taken) for status, taken in zip(statuses, takenCounts)]
1370 if (count := lineCounts.get(lineNumber)) is None:
1371 status = LineCoverageStatus.Unknown
1372 elif count == 0:
1373 status = LineCoverageStatus.Uncovered
1374 elif LineCoverageStatus.Uncovered in statuses:
1375 status = LineCoverageStatus.PartiallyCovered
1376 else:
1377 status = LineCoverageStatus.Covered
1379 cc_Line(lineNumber, status, count, branches, parent=file)
1381 @staticmethod
1382 def _ConvertUnits(
1383 file: cc_File,
1384 functionCounts: dict[str, tuple[int, Nullable[int], Nullable[int]]],
1385 summary: CoverageSummary
1386 ) -> None:
1387 """
1388 Convert a file to a source file unit, and its functions to function units.
1390 :param file: The file of the common model.
1391 :param functionCounts: The start line, end line and count of each function, by name.
1392 :param summary: The report's root of the common model.
1393 """
1394 lines = file._lines
1395 lastLineNumber = file._lastLineNumber
1396 sourceFile = cc_SourceFile(
1397 file.Path.as_posix(),
1398 file=file,
1399 startLine=next(file.IterateLines(), None),
1400 endLine=lines[lastLineNumber] if lastLineNumber > 0 else None,
1401 parent=summary
1402 )
1404 for name, (start, end, count) in functionCounts.items():
1405 if end is None:
1406 startLine = lines[start] if start <= lastLineNumber else None
1407 endLine = None
1408 else:
1409 numbers = [number for number in range(start, min(end, lastLineNumber) + 1) if lines[number] is not None]
1410 startLine = lines[numbers[0]] if len(numbers) > 0 else None
1411 endLine = lines[numbers[-1]] if len(numbers) > 0 else None
1413 if count is None:
1414 status = LineCoverageStatus.Unknown
1415 else:
1416 status = LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered
1418 cc_Function(
1419 name, file=file, startLine=startLine, endLine=endLine, status=status, coverageCount=count, parent=sourceFile
1420 )