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

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. 

33 

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. 

40 

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. 

43 

44:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`: 

45 

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. 

52 

53.. rubric:: Example 

54 

55.. code-block:: Python 

56 

57 from pathlib import Path 

58 from pyEDAA.Reports.CodeCoverage.LCOV import Document 

59 

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 

66 

67from pathlib import Path 

68from re import Pattern, compile as re_compile 

69from typing import Mapping, Optional as Nullable 

70 

71from pyTooling.Common import getFullyQualifiedName 

72from pyTooling.Decorators import export, readonly 

73from pyTooling.MetaClasses import ExtendedType 

74from pyTooling.Stopwatch import Stopwatch 

75 

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 

79 

80 

81__all__ = ["RECORD_PATTERNS", "RECORD_SYNTAX"] 

82 

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} 

104 

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} 

127 

128 

129@export 

130class Line(metaclass=ExtendedType, slots=True): 

131 """ 

132 A ``DA`` record: how often a line ran, and the line's checksum. 

133 """ 

134 

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. 

139 

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. 

150 

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 

177 

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 

188 

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.") 

195 

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 

200 

201 self._parent = parent 

202 self._number = number 

203 self._count = count 

204 self._checksum = checksum 

205 

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.") 

209 

210 parent._lines[number] = self 

211 

212 @readonly 

213 def Parent(self) -> Nullable[Section]: 

214 """ 

215 Read-only property to access the section the line belongs to (:attr:`_parent`). 

216 

217 :returns: The section; ``None`` if the line belongs to no section. 

218 """ 

219 return self._parent 

220 

221 @readonly 

222 def Number(self) -> int: 

223 """ 

224 Read-only property to access the line number (:attr:`_number`). 

225 

226 :returns: The line number. 

227 """ 

228 return self._number 

229 

230 @readonly 

231 def Count(self) -> int: 

232 """ 

233 Read-only property to access how often the line ran (:attr:`_count`). 

234 

235 :returns: The execution count. 

236 """ 

237 return self._count 

238 

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`). 

243 

244 lcov writes an MD5 hash in base64. 

245 

246 :returns: The checksum, or ``None`` if not stated. 

247 """ 

248 return self._checksum 

249 

250 

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 """ 

256 

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. 

263 

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. 

276 

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 

309 

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 

320 

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.") 

329 

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 

338 

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 

345 

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 

350 

351 self._parent = parent 

352 self._lineNumber = lineNumber 

353 self._block = block 

354 self._expression = expression 

355 self._taken = taken 

356 self._isException = isException 

357 

358 if parent is not None: 

359 parent._branches.append(self) 

360 

361 @readonly 

362 def Parent(self) -> Nullable[Section]: 

363 """ 

364 Read-only property to access the section the branch belongs to (:attr:`_parent`). 

365 

366 :returns: The section; ``None`` if the branch belongs to no section. 

367 """ 

368 return self._parent 

369 

370 @readonly 

371 def LineNumber(self) -> int: 

372 """ 

373 Read-only property to access the line number of the branch (:attr:`_lineNumber`). 

374 

375 :returns: The line number. 

376 """ 

377 return self._lineNumber 

378 

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`). 

383 

384 :returns: The block number, counted from 0. 

385 """ 

386 return self._block 

387 

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`). 

392 

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``. 

395 

396 :returns: The identifier. 

397 """ 

398 return self._expression 

399 

400 @readonly 

401 def Taken(self) -> Nullable[int]: 

402 """ 

403 Read-only property to access how often the branch was taken (:attr:`_taken`). 

404 

405 :returns: The count, or ``None`` if the branch was never evaluated - stated as ``-``. 

406 """ 

407 return self._taken 

408 

409 @readonly 

410 def IsException(self) -> bool: 

411 """ 

412 Read-only property to access whether the branch is taken by an exception (:attr:`_isException`). 

413 

414 :returns: ``True``, if the branch is marked by the prefix ``e``. 

415 """ 

416 return self._isException 

417 

418 

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 """ 

424 

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. 

432 

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. 

446 

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 

484 

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 

495 

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 

502 

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 

513 

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 

524 

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.") 

533 

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 

538 

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 

546 

547 if parent is not None: 

548 parent._conditions.append(self) 

549 

550 @readonly 

551 def Parent(self) -> Nullable[Section]: 

552 """ 

553 Read-only property to access the section the condition belongs to (:attr:`_parent`). 

554 

555 :returns: The section; ``None`` if the condition belongs to no section. 

556 """ 

557 return self._parent 

558 

559 @readonly 

560 def LineNumber(self) -> int: 

561 """ 

562 Read-only property to access the line number of the expression (:attr:`_lineNumber`). 

563 

564 :returns: The line number. 

565 """ 

566 return self._lineNumber 

567 

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`). 

572 

573 :returns: The group size. 

574 """ 

575 return self._groupSize 

576 

577 @readonly 

578 def Sense(self) -> bool: 

579 """ 

580 Read-only property to access the sense of the condition's change (:attr:`_sense`). 

581 

582 :returns: ``True`` for a change from false to true - stated as ``t`` -, ``False`` for the converse - ``f``. 

583 """ 

584 return self._sense 

585 

586 @readonly 

587 def Taken(self) -> int: 

588 """ 

589 Read-only property to access how often - or whether - the condition was sensitized (:attr:`_taken`). 

590 

591 :returns: The count; ``0``, if the condition was never sensitized. 

592 """ 

593 return self._taken 

594 

595 @readonly 

596 def Index(self) -> int: 

597 """ 

598 Read-only property to access the index of the condition in its group (:attr:`_index`). 

599 

600 :returns: The index, counted from 0. 

601 """ 

602 return self._index 

603 

604 @readonly 

605 def Expression(self) -> str: 

606 """ 

607 Read-only property to access the condition's expression (:attr:`_expression`). 

608 

609 The text depends on the tool, e.g. GCC states ``0``. 

610 

611 :returns: The expression. 

612 """ 

613 return self._expression 

614 

615 

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. 

620 

621 The aliases of a function - e.g. instances of a C++ template - share its lines. 

622 """ 

623 

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. 

629 

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. 

641 

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 

675 

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 

684 

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.") 

693 

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 

702 

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 

707 

708 self._parent = parent 

709 self._index = index 

710 self._startLine = startLine 

711 self._endLine = endLine 

712 self._aliases = {} 

713 

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 

729 

730 self._aliases[name] = count 

731 

732 if parent is not None: 

733 parent._functions.append(self) 

734 

735 @readonly 

736 def Parent(self) -> Nullable[Section]: 

737 """ 

738 Read-only property to access the section the function belongs to (:attr:`_parent`). 

739 

740 :returns: The section; ``None`` if the function belongs to no section. 

741 """ 

742 return self._parent 

743 

744 @readonly 

745 def Index(self) -> Nullable[int]: 

746 """ 

747 Read-only property to access the index of the ``FNL`` record (:attr:`_index`). 

748 

749 :returns: The index, or ``None`` for an ``FN`` record. 

750 """ 

751 return self._index 

752 

753 @readonly 

754 def StartLine(self) -> int: 

755 """ 

756 Read-only property to access the function's first line (:attr:`_startLine`). 

757 

758 :returns: The line number. 

759 """ 

760 return self._startLine 

761 

762 @readonly 

763 def EndLine(self) -> Nullable[int]: 

764 """ 

765 Read-only property to access the function's last line (:attr:`_endLine`). 

766 

767 Not every tool states it, e.g. llvm-cov doesn't. 

768 

769 :returns: The line number, or ``None`` if not stated. 

770 """ 

771 return self._endLine 

772 

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`). 

777 

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 

781 

782 @readonly 

783 def Name(self) -> str: 

784 """ 

785 Read-only property to return the function's name: its first alias. 

786 

787 :returns: The name. 

788 """ 

789 return next(iter(self._aliases)) 

790 

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. 

795 

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 

800 

801 

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 """ 

808 

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. 

825 

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. 

829 

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 

846 

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 

853 

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 

858 

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 

875 

876 if parent is not None: 

877 parent._sections.append(self) 

878 

879 @readonly 

880 def Parent(self) -> Nullable[Tracefile]: 

881 """ 

882 Read-only property to access the tracefile the section belongs to (:attr:`_parent`). 

883 

884 :returns: The tracefile; ``None`` if the section belongs to no tracefile. 

885 """ 

886 return self._parent 

887 

888 @readonly 

889 def TestName(self) -> str: 

890 """ 

891 Read-only property to access the name of the test (:attr:`_testName`). 

892 

893 :returns: The name; empty, if no ``TN`` record states it. 

894 """ 

895 return self._testName 

896 

897 @readonly 

898 def SourceFile(self) -> Path: 

899 """ 

900 Read-only property to access the path of the source file (:attr:`_sourceFile`). 

901 

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 

906 

907 @readonly 

908 def Version(self) -> Nullable[str]: 

909 """ 

910 Read-only property to access the version ID of the source file (:attr:`_version`). 

911 

912 :returns: The version ID, or ``None`` if not stated. 

913 """ 

914 return self._version 

915 

916 @readonly 

917 def Functions(self) -> list[Function]: 

918 """ 

919 Read-only property to access the functions (:attr:`_functions`). 

920 

921 :returns: The functions, in the tracefile's order. 

922 """ 

923 return self._functions 

924 

925 @readonly 

926 def Lines(self) -> dict[int, Line]: 

927 """ 

928 Read-only property to access the lines (:attr:`_lines`). 

929 

930 :returns: The lines, by number. 

931 """ 

932 return self._lines 

933 

934 @readonly 

935 def Branches(self) -> list[Branch]: 

936 """ 

937 Read-only property to access the branches (:attr:`_branches`). 

938 

939 :returns: The branches, in the tracefile's order. 

940 """ 

941 return self._branches 

942 

943 @readonly 

944 def Conditions(self) -> list[Condition]: 

945 """ 

946 Read-only property to access the conditions of MC/DC coverage (:attr:`_conditions`). 

947 

948 :returns: The conditions, in the tracefile's order. 

949 """ 

950 return self._conditions 

951 

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`). 

956 

957 :returns: The number from record ``FNF``, or ``None`` if not stated. 

958 """ 

959 return self._functionsFound 

960 

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`). 

965 

966 :returns: The number from record ``FNH``, or ``None`` if not stated. 

967 """ 

968 return self._functionsHit 

969 

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`). 

974 

975 :returns: The number from record ``BRF``, or ``None`` if not stated. 

976 """ 

977 return self._branchesFound 

978 

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`). 

983 

984 :returns: The number from record ``BRH``, or ``None`` if not stated. 

985 """ 

986 return self._branchesHit 

987 

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`). 

992 

993 :returns: The number from record ``MCF``, or ``None`` if not stated. 

994 """ 

995 return self._conditionsFound 

996 

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`). 

1002 

1003 :returns: The number from record ``MCH``, or ``None`` if not stated. 

1004 """ 

1005 return self._conditionsHit 

1006 

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`). 

1011 

1012 :returns: The number from record ``LF``, or ``None`` if not stated. 

1013 """ 

1014 return self._linesFound 

1015 

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`). 

1020 

1021 :returns: The number from record ``LH``, or ``None`` if not stated. 

1022 """ 

1023 return self._linesHit 

1024 

1025 

1026@export 

1027class Tracefile(metaclass=ExtendedType, mixin=True): 

1028 """ 

1029 A tracefile: its comments and sections. 

1030 """ 

1031 

1032 _comments: list[str] #: The comments, without the leading ``#``. 

1033 _sections: list[Section] #: The sections, in the tracefile's order. 

1034 

1035 def __init__(self) -> None: 

1036 """ 

1037 Initialize an empty tracefile. 

1038 """ 

1039 self._comments = [] 

1040 self._sections = [] 

1041 

1042 @readonly 

1043 def Comments(self) -> list[str]: 

1044 """ 

1045 Read-only property to access the comments (:attr:`_comments`). 

1046 

1047 A tool writes them on request, e.g. ``lcov --comment``. 

1048 

1049 :returns: The comments, without the leading ``#``. 

1050 """ 

1051 return self._comments 

1052 

1053 @readonly 

1054 def Sections(self) -> list[Section]: 

1055 """ 

1056 Read-only property to access the sections (:attr:`_sections`). 

1057 

1058 :returns: The sections, in the tracefile's order; a source file has a section per test. 

1059 """ 

1060 return self._sections 

1061 

1062 

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 """ 

1068 

1069 _records: Nullable[list[tuple[int, str, str, tuple[Nullable[str], ...]]]] #: The records, after :meth:`Analyze`. 

1070 

1071 def __init__(self, tracefile: Path, analyzeAndConvert: bool = False) -> None: 

1072 """ 

1073 Initialize the tracefile, and optionally read it. 

1074 

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) 

1080 

1081 self._records = None 

1082 

1083 if analyzeAndConvert: 

1084 self.Analyze() 

1085 self.Convert() 

1086 

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. 

1090 

1091 Empty lines are skipped. 

1092 

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.") 

1104 

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 

1112 

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 

1124 

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 

1136 

1137 records.append((lineNumber, text, key, match.groups())) 

1138 

1139 self._records = records 

1140 

1141 self._analysisDuration = sw.Duration 

1142 

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``. 

1146 

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. 

1149 

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 

1172 

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 

1189 

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 

1200 

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 

1214 

1215 for startLine, endLine, index, aliases in functions: 

1216 Function(startLine, endLine, aliases, index, parent=section) 

1217 

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 

1238 

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 

1247 

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])) 

1260 

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 

1265 

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]) 

1286 

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 

1291 

1292 self._conversionDuration = sw.Duration 

1293 

1294 def ToCoverageSummary(self) -> CoverageSummary: 

1295 """ 

1296 Convert the format's model to the common model, and aggregate it. 

1297 

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. 

1303 

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) 

1308 

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 

1316 

1317 lines = lineCounts.setdefault(id(file), {}) 

1318 for number, line in section._lines.items(): 

1319 lines[number] = lines.get(number, 0) + line._count 

1320 

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 

1329 

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) 

1336 

1337 for fileID, file in files.items(): 

1338 self._ConvertLines(file, lineCounts[fileID], branchCounts[fileID]) 

1339 self._ConvertUnits(file, functionCounts[fileID], summary) 

1340 

1341 summary.Aggregate() 

1342 return summary 

1343 

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. 

1352 

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) 

1361 

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)] 

1369 

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 

1378 

1379 cc_Line(lineNumber, status, count, branches, parent=file) 

1380 

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. 

1389 

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 ) 

1403 

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 

1412 

1413 if count is None: 

1414 status = LineCoverageStatus.Unknown 

1415 else: 

1416 status = LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered 

1417 

1418 cc_Function( 

1419 name, file=file, startLine=startLine, endLine=endLine, status=status, coverageCount=count, parent=sourceFile 

1420 )