Coverage for pyEDAA/Reports/CodeCoverage/Gcov.py: 82%

802 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:26 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ____ _ # 

3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32GCC's gcov JSON code coverage format: a model of the format, read from a report and converted to the common model. 

33 

34gcov writes the format with ``gcov --json-format``: gzip-compressed to a :file:`*.gcov.json.gz` file per data file, or, 

35with ``--stdout``, as plain JSON, one line per data file. A report is read in either form, and each JSON object in 

36it is validated against the JSON Schema of the format version it states (:class:`FormatVersion`), reverse-engineered 

37from GCC: :file:`Gcov-1.schema.json` for format 1 (GCC 9 to 13), :file:`Gcov-2.schema.json` for format 2 (GCC 14 and 

38later). The format's model keeps what the report states: a :class:`Document` holds :class:`DataFile` records, a data 

39file :class:`File` records, a file its :class:`Function` and :class:`Line` records, and a line its :class:`Branch`, 

40:class:`Call` and :class:`Condition` records - in format 2 with the IDs of the basic blocks they belong to. Each 

41record's constructor takes typed values, so the model can be built by hand: a record below the report names its parent 

42with the keyword parameter ``parent`` and is added to it. Its class method ``Parse`` reads the record's JSON object. 

43 

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

45 

46* A file's path is relative to the directory the compiler ran in - a data file's ``current_working_directory`` -, 

47 unless it is absolute. 

48* A line's ``count`` is its count; a line, which ran without taking all of its branches, is partially covered. A line 

49 several functions share - e.g. the instantiations of a template - or several data files state - e.g. a header - 

50 becomes one line, its counts added. 

51* A branch becomes a branch of its line, with its count; exceptional branches (``throw``) are branches too. A branch 

52 leads to a basic block, not to a line, so it has no target. 

53* A file becomes a source file unit, its functions - by demangled name, e.g. ``Containers::Stack::Pop()`` - become 

54 functions, each spanning its first to its last line, with its execution count. 

55* Calls, conditions and basic blocks have no counterpart in the common model. The format has no excluded lines. 

56 

57.. rubric:: Example 

58 

59.. code-block:: Python 

60 

61 from pathlib import Path 

62 from pyEDAA.Reports.CodeCoverage.Gcov import Document 

63 

64 report = Document(Path("main.gcov.json.gz"), analyzeAndConvert=True) 

65 summary = report.ToCoverageSummary() 

66 print(f"{summary.FileCount} files: {summary.LineCoverage:.1%}") 

67""" 

68from __future__ import annotations 

69 

70from enum import IntEnum 

71from gzip import BadGzipFile, decompress 

72from json import JSONDecodeError, JSONDecoder, loads 

73from pathlib import Path 

74from re import compile as re_compile 

75from typing import Any, Iterable, Optional as Nullable, Self 

76from zlib import error as ZLibError 

77 

78from pyTooling.Common import getFullyQualifiedName, readResourceFile 

79from pyTooling.Decorators import export, readonly 

80from pyTooling.Exceptions import ToolingException 

81from pyTooling.MetaClasses import ExtendedType 

82from pyTooling.Stopwatch import Stopwatch 

83from pyTooling.Versioning import SemanticVersion 

84 

85from pyEDAA.Reports import Resources 

86from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, CodeCoverageError, CoverageSummary, \ 

87 Document as cc_Document, File as cc_File, Function as cc_Function, \ 

88 Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile 

89 

90 

91__all__ = ["SCHEMAS"] 

92 

93# A class with a property named like a class - ``Path`` - can't name that class in the annotation of a field: the class 

94# body's namespace, where annotations are evaluated, binds the name to the property. 

95_Path = Path 

96 

97 

98@export 

99class FormatVersion(IntEnum): 

100 """ 

101 Version of the gcov JSON format, as a report's ``format_version`` states it. 

102 

103 GCC 9 to 13 write format 1. GCC 14 and later write format 2, which adds the IDs of the basic blocks of a line and of 

104 a branch, and the calls and conditions of a line; GCC 15 adds the prime paths of a function to format 2. 

105 """ 

106 

107 Version1 = 1 #: Format 1, written by GCC 9 to 13. 

108 Version2 = 2 #: Format 2, written by GCC 14 and later. 

109 

110 @classmethod 

111 def Parse(cls, value: str) -> Self: 

112 """ 

113 Convert the version, as gcov states it, to the member of that version. 

114 

115 gcov states the version as a string, e.g. ``"2"``. 

116 

117 :param value: The version, as a report's ``format_version`` states it. 

118 :returns: The member of that version. 

119 :raises ValueError: If parameter ``value`` is ``None``. 

120 :raises TypeError: If parameter ``value`` isn't of type :class:`str`. 

121 :raises ValueError: If parameter ``value`` isn't a supported format version. 

122 """ 

123 if value is None: 

124 raise ValueError("Parameter 'value' is None.") 

125 elif not isinstance(value, str): 

126 ex = TypeError("Parameter 'value' is not of type 'str'.") 

127 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

128 raise ex 

129 elif value not in [str(member.value) for member in cls]: 

130 ex = ValueError("Parameter 'value' is not a supported gcov JSON format version.") 

131 ex.add_note(f"Got value '{value}'.") 

132 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in cls)}.") 

133 raise ex 

134 

135 return cls(int(value)) 

136 

137 

138# Like '_Path': DataFile has a property named 'FormatVersion'. 

139_FormatVersion = FormatVersion 

140 

141SCHEMAS: dict[FormatVersion, str] = { 

142 FormatVersion.Version1: "Gcov-1.schema.json", 

143 FormatVersion.Version2: "Gcov-2.schema.json" 

144} #: Per format version, the JSON Schema a JSON object of that version is validated against. 

145 

146 

147@export 

148class Branch(metaclass=ExtendedType, slots=True): 

149 """ 

150 A ``branch`` of a line: an edge from a basic block to another, and how often it was taken. 

151 """ 

152 

153 _parent: Nullable[Line] #: The line the branch belongs to. 

154 _count: int #: Number of times the branch was taken. 

155 _throw: bool #: Whether the branch is taken by an exception. 

156 _fallthrough: bool #: Whether the branch falls through. 

157 _sourceBlockID: Nullable[int] #: ID of the basic block the branch starts at, in format 2. 

158 _destinationBlockID: Nullable[int] #: ID of the basic block the branch leads to, in format 2. 

159 

160 def __init__( 

161 self, 

162 count: int, 

163 throw: bool, 

164 fallthrough: bool, 

165 sourceBlockID: Nullable[int] = None, 

166 destinationBlockID: Nullable[int] = None, 

167 *, 

168 parent: Nullable[Line] = None 

169 ) -> None: 

170 """ 

171 Initialize the branch, and add it to the branches of its line. 

172 

173 :param count: Number of times the branch was taken. 

174 :param throw: Whether the branch is taken by an exception. 

175 :param fallthrough: Whether the branch falls through. 

176 :param sourceBlockID: Optional, ID of the basic block the branch starts at. Default: ``None``. 

177 :param destinationBlockID: Optional, ID of the basic block the branch leads to. Default: ``None``. 

178 :param parent: Optional, the line the branch belongs to; the branch is appended to its branches. 

179 Default: ``None``. 

180 :raises ValueError: If parameter ``count`` is ``None``. 

181 :raises TypeError: If parameter ``count`` isn't of type :class:`int`. 

182 :raises ValueError: If parameter ``count`` is negative. 

183 :raises ValueError: If parameter ``throw`` is ``None``. 

184 :raises TypeError: If parameter ``throw`` isn't of type :class:`bool`. 

185 :raises ValueError: If parameter ``fallthrough`` is ``None``. 

186 :raises TypeError: If parameter ``fallthrough`` isn't of type :class:`bool`. 

187 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`. 

188 :raises ValueError: If parameter ``sourceBlockID`` is negative. 

189 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`. 

190 :raises ValueError: If parameter ``destinationBlockID`` is negative. 

191 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`. 

192 """ 

193 if count is None: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true

194 raise ValueError("Parameter 'count' is None.") 

195 elif not isinstance(count, int): 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

196 ex = TypeError("Parameter 'count' is not of type 'int'.") 

197 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") 

198 raise ex 

199 elif count < 0: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 ex = ValueError("Parameter 'count' is negative.") 

201 ex.add_note(f"Got value '{count}'.") 

202 raise ex 

203 

204 if throw is None: 

205 raise ValueError("Parameter 'throw' is None.") 

206 elif not isinstance(throw, bool): 

207 ex = TypeError("Parameter 'throw' is not of type 'bool'.") 

208 ex.add_note(f"Got type '{getFullyQualifiedName(throw)}'.") 

209 raise ex 

210 

211 if fallthrough is None: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true

212 raise ValueError("Parameter 'fallthrough' is None.") 

213 elif not isinstance(fallthrough, bool): 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 ex = TypeError("Parameter 'fallthrough' is not of type 'bool'.") 

215 ex.add_note(f"Got type '{getFullyQualifiedName(fallthrough)}'.") 

216 raise ex 

217 

218 if sourceBlockID is not None: 

219 if not isinstance(sourceBlockID, int): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true

220 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.") 

221 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.") 

222 raise ex 

223 elif sourceBlockID < 0: 

224 ex = ValueError("Parameter 'sourceBlockID' is negative.") 

225 ex.add_note(f"Got value '{sourceBlockID}'.") 

226 raise ex 

227 

228 if destinationBlockID is not None: 

229 if not isinstance(destinationBlockID, int): 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true

230 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.") 

231 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.") 

232 raise ex 

233 elif destinationBlockID < 0: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 ex = ValueError("Parameter 'destinationBlockID' is negative.") 

235 ex.add_note(f"Got value '{destinationBlockID}'.") 

236 raise ex 

237 

238 if parent is not None and not isinstance(parent, Line): 

239 ex = TypeError("Parameter 'parent' is not of type 'Line'.") 

240 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

241 raise ex 

242 

243 self._parent = parent 

244 self._count = count 

245 self._throw = throw 

246 self._fallthrough = fallthrough 

247 self._sourceBlockID = sourceBlockID 

248 self._destinationBlockID = destinationBlockID 

249 

250 if parent is not None: 

251 parent._branches.append(self) 

252 

253 @classmethod 

254 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self: 

255 """ 

256 Parse a branch from its JSON object. 

257 

258 :param record: The JSON object of the branch. 

259 :param parent: Optional, the line the branch belongs to. Default: ``None``. 

260 :returns: The branch. 

261 :raises ValueError: If parameter ``record`` is ``None``. 

262 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

263 """ 

264 if record is None: 

265 raise ValueError("Parameter 'record' is None.") 

266 elif not isinstance(record, dict): 

267 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

268 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

269 raise ex 

270 

271 return cls( 

272 record["count"], 

273 record["throw"], 

274 record["fallthrough"], 

275 record.get("source_block_id"), 

276 record.get("destination_block_id"), 

277 parent=parent 

278 ) 

279 

280 @readonly 

281 def Parent(self) -> Nullable[Line]: 

282 """ 

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

284 

285 :returns: The line; ``None`` if the branch belongs to no line. 

286 """ 

287 return self._parent 

288 

289 @readonly 

290 def Count(self) -> int: 

291 """ 

292 Read-only property to access the number of times the branch was taken (:attr:`_count`). 

293 

294 :returns: The count. 

295 """ 

296 return self._count 

297 

298 @readonly 

299 def Throw(self) -> bool: 

300 """ 

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

302 

303 :returns: ``True``, if the branch is exceptional. 

304 """ 

305 return self._throw 

306 

307 @readonly 

308 def Fallthrough(self) -> bool: 

309 """ 

310 Read-only property to access whether the branch falls through (:attr:`_fallthrough`). 

311 

312 :returns: ``True``, if the branch falls through. 

313 """ 

314 return self._fallthrough 

315 

316 @readonly 

317 def SourceBlockID(self) -> Nullable[int]: 

318 """ 

319 Read-only property to access the ID of the basic block the branch starts at (:attr:`_sourceBlockID`). 

320 

321 :returns: The block's ID, unique within its function; ``None`` in format 1. 

322 """ 

323 return self._sourceBlockID 

324 

325 @readonly 

326 def DestinationBlockID(self) -> Nullable[int]: 

327 """ 

328 Read-only property to access the ID of the basic block the branch leads to (:attr:`_destinationBlockID`). 

329 

330 :returns: The block's ID, unique within its function; ``None`` in format 1. 

331 """ 

332 return self._destinationBlockID 

333 

334 

335@export 

336class Call(metaclass=ExtendedType, slots=True): 

337 """ 

338 A ``call`` of a line - in format 2 -: from a basic block to the block continuing after the return. 

339 """ 

340 

341 _parent: Nullable[Line] #: The line the call belongs to. 

342 _sourceBlockID: int #: ID of the basic block the call is in. 

343 _destinationBlockID: int #: ID of the basic block continuing after the return. 

344 _returned: int #: Number of times the call returned. 

345 

346 def __init__( 

347 self, 

348 sourceBlockID: int, 

349 destinationBlockID: int, 

350 returned: int, 

351 *, 

352 parent: Nullable[Line] = None 

353 ) -> None: 

354 """ 

355 Initialize the call, and add it to the calls of its line. 

356 

357 :param sourceBlockID: ID of the basic block the call is in. 

358 :param destinationBlockID: ID of the basic block continuing after the return. 

359 :param returned: Number of times the call returned. 

360 :param parent: Optional, the line the call belongs to; the call is appended to its calls. 

361 Default: ``None``. 

362 :raises ValueError: If parameter ``sourceBlockID`` is ``None``. 

363 :raises TypeError: If parameter ``sourceBlockID`` isn't of type :class:`int`. 

364 :raises ValueError: If parameter ``sourceBlockID`` is negative. 

365 :raises ValueError: If parameter ``destinationBlockID`` is ``None``. 

366 :raises TypeError: If parameter ``destinationBlockID`` isn't of type :class:`int`. 

367 :raises ValueError: If parameter ``destinationBlockID`` is negative. 

368 :raises ValueError: If parameter ``returned`` is ``None``. 

369 :raises TypeError: If parameter ``returned`` isn't of type :class:`int`. 

370 :raises ValueError: If parameter ``returned`` is negative. 

371 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`. 

372 """ 

373 if sourceBlockID is None: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true

374 raise ValueError("Parameter 'sourceBlockID' is None.") 

375 elif not isinstance(sourceBlockID, int): 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true

376 ex = TypeError("Parameter 'sourceBlockID' is not of type 'int'.") 

377 ex.add_note(f"Got type '{getFullyQualifiedName(sourceBlockID)}'.") 

378 raise ex 

379 elif sourceBlockID < 0: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true

380 ex = ValueError("Parameter 'sourceBlockID' is negative.") 

381 ex.add_note(f"Got value '{sourceBlockID}'.") 

382 raise ex 

383 

384 if destinationBlockID is None: 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true

385 raise ValueError("Parameter 'destinationBlockID' is None.") 

386 elif not isinstance(destinationBlockID, int): 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 ex = TypeError("Parameter 'destinationBlockID' is not of type 'int'.") 

388 ex.add_note(f"Got type '{getFullyQualifiedName(destinationBlockID)}'.") 

389 raise ex 

390 elif destinationBlockID < 0: 390 ↛ 391line 390 didn't jump to line 391 because the condition on line 390 was never true

391 ex = ValueError("Parameter 'destinationBlockID' is negative.") 

392 ex.add_note(f"Got value '{destinationBlockID}'.") 

393 raise ex 

394 

395 if returned is None: 

396 raise ValueError("Parameter 'returned' is None.") 

397 elif not isinstance(returned, int): 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true

398 ex = TypeError("Parameter 'returned' is not of type 'int'.") 

399 ex.add_note(f"Got type '{getFullyQualifiedName(returned)}'.") 

400 raise ex 

401 elif returned < 0: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true

402 ex = ValueError("Parameter 'returned' is negative.") 

403 ex.add_note(f"Got value '{returned}'.") 

404 raise ex 

405 

406 if parent is not None and not isinstance(parent, Line): 

407 ex = TypeError("Parameter 'parent' is not of type 'Line'.") 

408 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

409 raise ex 

410 

411 self._parent = parent 

412 self._sourceBlockID = sourceBlockID 

413 self._destinationBlockID = destinationBlockID 

414 self._returned = returned 

415 

416 if parent is not None: 

417 parent._calls.append(self) 

418 

419 @classmethod 

420 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self: 

421 """ 

422 Parse a call from its JSON object. 

423 

424 :param record: The JSON object of the call. 

425 :param parent: Optional, the line the call belongs to. Default: ``None``. 

426 :returns: The call. 

427 :raises ValueError: If parameter ``record`` is ``None``. 

428 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

429 """ 

430 if record is None: 

431 raise ValueError("Parameter 'record' is None.") 

432 elif not isinstance(record, dict): 

433 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

434 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

435 raise ex 

436 

437 return cls(record["source_block_id"], record["destination_block_id"], record["returned"], parent=parent) 

438 

439 @readonly 

440 def Parent(self) -> Nullable[Line]: 

441 """ 

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

443 

444 :returns: The line; ``None`` if the call belongs to no line. 

445 """ 

446 return self._parent 

447 

448 @readonly 

449 def SourceBlockID(self) -> int: 

450 """ 

451 Read-only property to access the ID of the basic block the call is in (:attr:`_sourceBlockID`). 

452 

453 :returns: The block's ID, unique within its function. 

454 """ 

455 return self._sourceBlockID 

456 

457 @readonly 

458 def DestinationBlockID(self) -> int: 

459 """ 

460 Read-only property to access the ID of the basic block continuing after the return (:attr:`_destinationBlockID`). 

461 

462 :returns: The block's ID, unique within its function. 

463 """ 

464 return self._destinationBlockID 

465 

466 @readonly 

467 def Returned(self) -> int: 

468 """ 

469 Read-only property to access the number of times the call returned (:attr:`_returned`). 

470 

471 :returns: The count; how often the call was made is the line's count. 

472 """ 

473 return self._returned 

474 

475 

476@export 

477class Condition(metaclass=ExtendedType, slots=True): 

478 """ 

479 A ``condition`` of a line - in format 2, with ``gcc -fcondition-coverage`` -: the condition outcomes of a boolean 

480 expression (MC/DC). 

481 """ 

482 

483 _parent: Nullable[Line] #: The line the condition belongs to. 

484 _count: int #: Number of condition outcomes: twice the number of terms. 

485 _covered: int #: Number of covered condition outcomes. 

486 _notCoveredTrue: list[int] #: The terms, by index, never seen as true. 

487 _notCoveredFalse: list[int] #: The terms, by index, never seen as false. 

488 

489 def __init__( 

490 self, 

491 count: int, 

492 covered: int, 

493 notCoveredTrue: Nullable[Iterable[int]] = None, 

494 notCoveredFalse: Nullable[Iterable[int]] = None, 

495 *, 

496 parent: Nullable[Line] = None 

497 ) -> None: 

498 """ 

499 Initialize the condition, and add it to the conditions of its line. 

500 

501 :param count: Number of condition outcomes: twice the number of terms. 

502 :param covered: Number of covered condition outcomes. 

503 :param notCoveredTrue: Optional, the terms, by index, never seen as true. Default: none. 

504 :param notCoveredFalse: Optional, the terms, by index, never seen as false. Default: none. 

505 :param parent: Optional, the line the condition belongs to; the condition is appended to its conditions. 

506 Default: ``None``. 

507 :raises ValueError: If parameter ``count`` is ``None``. 

508 :raises TypeError: If parameter ``count`` isn't of type :class:`int`. 

509 :raises ValueError: If parameter ``count`` is negative. 

510 :raises ValueError: If parameter ``covered`` is ``None``. 

511 :raises TypeError: If parameter ``covered`` isn't of type :class:`int`. 

512 :raises ValueError: If parameter ``covered`` is negative. 

513 :raises TypeError: If parameter ``notCoveredTrue`` isn't iterable. 

514 :raises TypeError: If parameter ``notCoveredTrue`` contains an element not of type :class:`int`. 

515 :raises TypeError: If parameter ``notCoveredFalse`` isn't iterable. 

516 :raises TypeError: If parameter ``notCoveredFalse`` contains an element not of type :class:`int`. 

517 :raises TypeError: If parameter ``parent`` isn't of type :class:`Line`. 

518 """ 

519 if count is None: 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true

520 raise ValueError("Parameter 'count' is None.") 

521 elif not isinstance(count, int): 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true

522 ex = TypeError("Parameter 'count' is not of type 'int'.") 

523 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") 

524 raise ex 

525 elif count < 0: 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true

526 ex = ValueError("Parameter 'count' is negative.") 

527 ex.add_note(f"Got value '{count}'.") 

528 raise ex 

529 

530 if covered is None: 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true

531 raise ValueError("Parameter 'covered' is None.") 

532 elif not isinstance(covered, int): 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true

533 ex = TypeError("Parameter 'covered' is not of type 'int'.") 

534 ex.add_note(f"Got type '{getFullyQualifiedName(covered)}'.") 

535 raise ex 

536 elif covered < 0: 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true

537 ex = ValueError("Parameter 'covered' is negative.") 

538 ex.add_note(f"Got value '{covered}'.") 

539 raise ex 

540 

541 notCoveredTrueList: list[int] = [] 

542 if notCoveredTrue is not None: 

543 if not isinstance(notCoveredTrue, Iterable): 

544 ex = TypeError("Parameter 'notCoveredTrue' is not iterable.") 

545 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredTrue)}'.") 

546 raise ex 

547 

548 for term in notCoveredTrue: 

549 if not isinstance(term, int): 549 ↛ 550line 549 didn't jump to line 550 because the condition on line 549 was never true

550 ex = TypeError("Parameter 'notCoveredTrue' contains an element not of type 'int'.") 

551 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.") 

552 raise ex 

553 

554 notCoveredTrueList.append(term) 

555 

556 notCoveredFalseList: list[int] = [] 

557 if notCoveredFalse is not None: 

558 if not isinstance(notCoveredFalse, Iterable): 

559 ex = TypeError("Parameter 'notCoveredFalse' is not iterable.") 

560 ex.add_note(f"Got type '{getFullyQualifiedName(notCoveredFalse)}'.") 

561 raise ex 

562 

563 for term in notCoveredFalse: 

564 if not isinstance(term, int): 

565 ex = TypeError("Parameter 'notCoveredFalse' contains an element not of type 'int'.") 

566 ex.add_note(f"Got type '{getFullyQualifiedName(term)}'.") 

567 raise ex 

568 

569 notCoveredFalseList.append(term) 

570 

571 if parent is not None and not isinstance(parent, Line): 

572 ex = TypeError("Parameter 'parent' is not of type 'Line'.") 

573 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

574 raise ex 

575 

576 self._parent = parent 

577 self._count = count 

578 self._covered = covered 

579 self._notCoveredTrue = notCoveredTrueList 

580 self._notCoveredFalse = notCoveredFalseList 

581 

582 if parent is not None: 

583 parent._conditions.append(self) 

584 

585 @classmethod 

586 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Line] = None) -> Self: 

587 """ 

588 Parse a condition from its JSON object. 

589 

590 :param record: The JSON object of the condition. 

591 :param parent: Optional, the line the condition belongs to. Default: ``None``. 

592 :returns: The condition. 

593 :raises ValueError: If parameter ``record`` is ``None``. 

594 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

595 """ 

596 if record is None: 

597 raise ValueError("Parameter 'record' is None.") 

598 elif not isinstance(record, dict): 

599 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

600 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

601 raise ex 

602 

603 return cls( 

604 record["count"], record["covered"], record["not_covered_true"], record["not_covered_false"], parent=parent 

605 ) 

606 

607 @readonly 

608 def Parent(self) -> Nullable[Line]: 

609 """ 

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

611 

612 :returns: The line; ``None`` if the condition belongs to no line. 

613 """ 

614 return self._parent 

615 

616 @readonly 

617 def Count(self) -> int: 

618 """ 

619 Read-only property to access the number of condition outcomes (:attr:`_count`). 

620 

621 :returns: The number of outcomes: twice the number of terms. 

622 """ 

623 return self._count 

624 

625 @readonly 

626 def Covered(self) -> int: 

627 """ 

628 Read-only property to access the number of covered condition outcomes (:attr:`_covered`). 

629 

630 :returns: The number of covered outcomes. 

631 """ 

632 return self._covered 

633 

634 @readonly 

635 def NotCoveredTrue(self) -> list[int]: 

636 """ 

637 Read-only property to access the terms never seen as true (:attr:`_notCoveredTrue`). 

638 

639 :returns: The terms' indices. 

640 """ 

641 return self._notCoveredTrue 

642 

643 @readonly 

644 def NotCoveredFalse(self) -> list[int]: 

645 """ 

646 Read-only property to access the terms never seen as false (:attr:`_notCoveredFalse`). 

647 

648 :returns: The terms' indices. 

649 """ 

650 return self._notCoveredFalse 

651 

652 

653@export 

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

655 """ 

656 A ``line`` of a file: how often it ran, its basic blocks, branches, calls and conditions. 

657 """ 

658 

659 _parent: Nullable[File] #: The file the line belongs to. 

660 _lineNumber: int #: Line number, counted from 1. 

661 _functionName: Nullable[str] #: Mangled name of the function the line belongs to, if the report says. 

662 _count: int #: Number of times the line ran. 

663 _unexecutedBlock: bool #: Whether a basic block, not only reached by exceptions, never ran. 

664 _blockIDs: list[int] #: IDs of the basic blocks ending on this line, in format 2. 

665 _branches: list[Branch] #: The branches. 

666 _calls: list[Call] #: The calls, in format 2. 

667 _conditions: list[Condition] #: The conditions, in format 2. 

668 

669 def __init__( 

670 self, 

671 lineNumber: int, 

672 count: int, 

673 unexecutedBlock: bool, 

674 functionName: Nullable[str] = None, 

675 blockIDs: Nullable[Iterable[int]] = None, 

676 *, 

677 parent: Nullable[File] = None 

678 ) -> None: 

679 """ 

680 Initialize the line, and add it to the lines of its file. 

681 

682 Its branches, calls and conditions are added by creating them with this line as their parent. 

683 

684 :param lineNumber: Line number, counted from 1. 

685 :param count: Number of times the line ran. 

686 :param unexecutedBlock: Whether a basic block of the line, not only reached by exceptions, never ran. 

687 :param functionName: Optional, mangled name of the function the line belongs to. Default: ``None``. 

688 :param blockIDs: Optional, IDs of the basic blocks ending on this line. Default: none. 

689 :param parent: Optional, the file the line belongs to; the line is appended to its lines. 

690 Default: ``None``. 

691 :raises ValueError: If parameter ``lineNumber`` is ``None``. 

692 :raises TypeError: If parameter ``lineNumber`` isn't of type :class:`int`. 

693 :raises ValueError: If parameter ``lineNumber`` is less than 1. 

694 :raises ValueError: If parameter ``count`` is ``None``. 

695 :raises TypeError: If parameter ``count`` isn't of type :class:`int`. 

696 :raises ValueError: If parameter ``count`` is negative. 

697 :raises ValueError: If parameter ``unexecutedBlock`` is ``None``. 

698 :raises TypeError: If parameter ``unexecutedBlock`` isn't of type :class:`bool`. 

699 :raises TypeError: If parameter ``functionName`` isn't of type :class:`str`. 

700 :raises TypeError: If parameter ``blockIDs`` isn't iterable. 

701 :raises TypeError: If parameter ``blockIDs`` contains an element not of type :class:`int`. 

702 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`. 

703 """ 

704 if lineNumber is None: 

705 raise ValueError("Parameter 'lineNumber' is None.") 

706 elif not isinstance(lineNumber, int): 

707 ex = TypeError("Parameter 'lineNumber' is not of type 'int'.") 

708 ex.add_note(f"Got type '{getFullyQualifiedName(lineNumber)}'.") 

709 raise ex 

710 elif lineNumber < 1: 

711 ex = ValueError("Parameter 'lineNumber' is less than 1.") 

712 ex.add_note(f"Got value '{lineNumber}'.") 

713 raise ex 

714 

715 if count is None: 715 ↛ 716line 715 didn't jump to line 716 because the condition on line 715 was never true

716 raise ValueError("Parameter 'count' is None.") 

717 elif not isinstance(count, int): 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true

718 ex = TypeError("Parameter 'count' is not of type 'int'.") 

719 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") 

720 raise ex 

721 elif count < 0: 721 ↛ 722line 721 didn't jump to line 722 because the condition on line 721 was never true

722 ex = ValueError("Parameter 'count' is negative.") 

723 ex.add_note(f"Got value '{count}'.") 

724 raise ex 

725 

726 if unexecutedBlock is None: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true

727 raise ValueError("Parameter 'unexecutedBlock' is None.") 

728 elif not isinstance(unexecutedBlock, bool): 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true

729 ex = TypeError("Parameter 'unexecutedBlock' is not of type 'bool'.") 

730 ex.add_note(f"Got type '{getFullyQualifiedName(unexecutedBlock)}'.") 

731 raise ex 

732 

733 if functionName is not None and not isinstance(functionName, str): 733 ↛ 734line 733 didn't jump to line 734 because the condition on line 733 was never true

734 ex = TypeError("Parameter 'functionName' is not of type 'str'.") 

735 ex.add_note(f"Got type '{getFullyQualifiedName(functionName)}'.") 

736 raise ex 

737 

738 blockIDList: list[int] = [] 

739 if blockIDs is not None: 

740 if not isinstance(blockIDs, Iterable): 

741 ex = TypeError("Parameter 'blockIDs' is not iterable.") 

742 ex.add_note(f"Got type '{getFullyQualifiedName(blockIDs)}'.") 

743 raise ex 

744 

745 for blockID in blockIDs: 

746 if not isinstance(blockID, int): 

747 ex = TypeError("Parameter 'blockIDs' contains an element not of type 'int'.") 

748 ex.add_note(f"Got type '{getFullyQualifiedName(blockID)}'.") 

749 raise ex 

750 

751 blockIDList.append(blockID) 

752 

753 if parent is not None and not isinstance(parent, File): 

754 ex = TypeError("Parameter 'parent' is not of type 'File'.") 

755 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

756 raise ex 

757 

758 self._parent = parent 

759 self._lineNumber = lineNumber 

760 self._functionName = functionName 

761 self._count = count 

762 self._unexecutedBlock = unexecutedBlock 

763 self._blockIDs = blockIDList 

764 self._branches = [] 

765 self._calls = [] 

766 self._conditions = [] 

767 

768 if parent is not None: 

769 parent._lines.append(self) 

770 

771 @classmethod 

772 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self: 

773 """ 

774 Parse a line, its branches, calls and conditions from its JSON object. 

775 

776 :param record: The JSON object of the line. 

777 :param parent: Optional, the file the line belongs to. Default: ``None``. 

778 :returns: The line. 

779 :raises ValueError: If parameter ``record`` is ``None``. 

780 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

781 """ 

782 if record is None: 

783 raise ValueError("Parameter 'record' is None.") 

784 elif not isinstance(record, dict): 

785 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

786 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

787 raise ex 

788 

789 line = cls( 

790 record["line_number"], 

791 record["count"], 

792 record["unexecuted_block"], 

793 record.get("function_name"), 

794 record.get("block_ids"), 

795 parent=parent 

796 ) 

797 

798 for branch in record["branches"]: 

799 Branch.Parse(branch, parent=line) 

800 

801 for call in record.get("calls", []): 

802 Call.Parse(call, parent=line) 

803 

804 for condition in record.get("conditions", []): 

805 Condition.Parse(condition, parent=line) 

806 

807 return line 

808 

809 @readonly 

810 def Parent(self) -> Nullable[File]: 

811 """ 

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

813 

814 :returns: The file; ``None`` if the line belongs to no file. 

815 """ 

816 return self._parent 

817 

818 @readonly 

819 def LineNumber(self) -> int: 

820 """ 

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

822 

823 :returns: The line number, counted from 1. 

824 """ 

825 return self._lineNumber 

826 

827 @readonly 

828 def FunctionName(self) -> Nullable[str]: 

829 """ 

830 Read-only property to access the mangled name of the function the line belongs to (:attr:`_functionName`). 

831 

832 :returns: The function's name, or ``None`` for a line of inlined statements. 

833 """ 

834 return self._functionName 

835 

836 @readonly 

837 def Count(self) -> int: 

838 """ 

839 Read-only property to access the number of times the line ran (:attr:`_count`). 

840 

841 :returns: The count. 

842 """ 

843 return self._count 

844 

845 @readonly 

846 def UnexecutedBlock(self) -> bool: 

847 """ 

848 Read-only property to access whether a basic block of the line never ran (:attr:`_unexecutedBlock`). 

849 

850 A block only reached by exceptions doesn't count. 

851 

852 :returns: ``True``, if not every statement of the line ran. 

853 """ 

854 return self._unexecutedBlock 

855 

856 @readonly 

857 def BlockIDs(self) -> list[int]: 

858 """ 

859 Read-only property to access the IDs of the basic blocks ending on this line (:attr:`_blockIDs`). 

860 

861 :returns: The blocks' IDs, unique within the function; empty in format 1. 

862 """ 

863 return self._blockIDs 

864 

865 @readonly 

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

867 """ 

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

869 

870 :returns: The branches; empty, if gcov ran without ``--branch-probabilities``. 

871 """ 

872 return self._branches 

873 

874 @readonly 

875 def Calls(self) -> list[Call]: 

876 """ 

877 Read-only property to access the calls (:attr:`_calls`). 

878 

879 :returns: The calls; empty in format 1, or if gcov ran without ``--branch-probabilities``. 

880 """ 

881 return self._calls 

882 

883 @readonly 

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

885 """ 

886 Read-only property to access the conditions (:attr:`_conditions`). 

887 

888 :returns: The conditions; empty in format 1, or if gcov ran without ``--conditions``. 

889 """ 

890 return self._conditions 

891 

892 

893@export 

894class Function(metaclass=ExtendedType, slots=True): 

895 """ 

896 A ``function`` of a file: its names, its position, its basic blocks and how often it ran. 

897 """ 

898 

899 _parent: Nullable[File] #: The file the function belongs to. 

900 _name: str #: Name of the function, mangled. 

901 _demangledName: str #: Name of the function, demangled. 

902 _startLine: int #: The function's first line. 

903 _startColumn: int #: The function's first column. 

904 _endLine: int #: The function's last line. 

905 _endColumn: int #: The function's last column. 

906 _blocks: int #: Number of basic blocks. 

907 _blocksExecuted: int #: Number of executed basic blocks. 

908 _executionCount: int #: Number of times the function ran. 

909 

910 def __init__( 

911 self, 

912 name: str, 

913 demangledName: str, 

914 startLine: int, 

915 startColumn: int, 

916 endLine: int, 

917 endColumn: int, 

918 blocks: int, 

919 blocksExecuted: int, 

920 executionCount: int, 

921 *, 

922 parent: Nullable[File] = None 

923 ) -> None: 

924 """ 

925 Initialize the function, and add it to the functions of its file. 

926 

927 :param name: Name of the function, mangled. 

928 :param demangledName: Name of the function, demangled. 

929 :param startLine: The function's first line. 

930 :param startColumn: The function's first column. 

931 :param endLine: The function's last line. 

932 :param endColumn: The function's last column. 

933 :param blocks: Number of basic blocks. 

934 :param blocksExecuted: Number of executed basic blocks. 

935 :param executionCount: Number of times the function ran. 

936 :param parent: Optional, the file the function belongs to; the function is added to its functions by 

937 :attr:`Name`. Default: ``None``. 

938 :raises ValueError: If parameter ``name`` is ``None``. 

939 :raises TypeError: If parameter ``name`` isn't of type :class:`str`. 

940 :raises ValueError: If parameter ``name`` is empty. 

941 :raises ValueError: If parameter ``demangledName`` is ``None``. 

942 :raises TypeError: If parameter ``demangledName`` isn't of type :class:`str`. 

943 :raises ValueError: If parameter ``demangledName`` is empty. 

944 :raises ValueError: If parameter ``startLine`` is ``None``. 

945 :raises TypeError: If parameter ``startLine`` isn't of type :class:`int`. 

946 :raises ValueError: If parameter ``startLine`` is negative. 

947 :raises ValueError: If parameter ``startColumn`` is ``None``. 

948 :raises TypeError: If parameter ``startColumn`` isn't of type :class:`int`. 

949 :raises ValueError: If parameter ``startColumn`` is negative. 

950 :raises ValueError: If parameter ``endLine`` is ``None``. 

951 :raises TypeError: If parameter ``endLine`` isn't of type :class:`int`. 

952 :raises ValueError: If parameter ``endLine`` is negative. 

953 :raises ValueError: If parameter ``endColumn`` is ``None``. 

954 :raises TypeError: If parameter ``endColumn`` isn't of type :class:`int`. 

955 :raises ValueError: If parameter ``endColumn`` is negative. 

956 :raises ValueError: If parameter ``blocks`` is ``None``. 

957 :raises TypeError: If parameter ``blocks`` isn't of type :class:`int`. 

958 :raises ValueError: If parameter ``blocks`` is negative. 

959 :raises ValueError: If parameter ``blocksExecuted`` is ``None``. 

960 :raises TypeError: If parameter ``blocksExecuted`` isn't of type :class:`int`. 

961 :raises ValueError: If parameter ``blocksExecuted`` is negative. 

962 :raises ValueError: If parameter ``executionCount`` is ``None``. 

963 :raises TypeError: If parameter ``executionCount`` isn't of type :class:`int`. 

964 :raises ValueError: If parameter ``executionCount`` is negative. 

965 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`. 

966 :raises ValueError: If parameter ``parent`` contains a function of the same name already. 

967 """ 

968 if name is None: 968 ↛ 969line 968 didn't jump to line 969 because the condition on line 968 was never true

969 raise ValueError("Parameter 'name' is None.") 

970 elif not isinstance(name, str): 970 ↛ 971line 970 didn't jump to line 971 because the condition on line 970 was never true

971 ex = TypeError("Parameter 'name' is not of type 'str'.") 

972 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

973 raise ex 

974 elif name == "": 

975 raise ValueError("Parameter 'name' is empty.") 

976 

977 if demangledName is None: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true

978 raise ValueError("Parameter 'demangledName' is None.") 

979 elif not isinstance(demangledName, str): 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true

980 ex = TypeError("Parameter 'demangledName' is not of type 'str'.") 

981 ex.add_note(f"Got type '{getFullyQualifiedName(demangledName)}'.") 

982 raise ex 

983 elif demangledName == "": 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true

984 raise ValueError("Parameter 'demangledName' is empty.") 

985 

986 if startLine is None: 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true

987 raise ValueError("Parameter 'startLine' is None.") 

988 elif not isinstance(startLine, int): 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true

989 ex = TypeError("Parameter 'startLine' is not of type 'int'.") 

990 ex.add_note(f"Got type '{getFullyQualifiedName(startLine)}'.") 

991 raise ex 

992 elif startLine < 0: 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true

993 ex = ValueError("Parameter 'startLine' is negative.") 

994 ex.add_note(f"Got value '{startLine}'.") 

995 raise ex 

996 

997 if startColumn is None: 997 ↛ 998line 997 didn't jump to line 998 because the condition on line 997 was never true

998 raise ValueError("Parameter 'startColumn' is None.") 

999 elif not isinstance(startColumn, int): 999 ↛ 1000line 999 didn't jump to line 1000 because the condition on line 999 was never true

1000 ex = TypeError("Parameter 'startColumn' is not of type 'int'.") 

1001 ex.add_note(f"Got type '{getFullyQualifiedName(startColumn)}'.") 

1002 raise ex 

1003 elif startColumn < 0: 1003 ↛ 1004line 1003 didn't jump to line 1004 because the condition on line 1003 was never true

1004 ex = ValueError("Parameter 'startColumn' is negative.") 

1005 ex.add_note(f"Got value '{startColumn}'.") 

1006 raise ex 

1007 

1008 if endLine is None: 1008 ↛ 1009line 1008 didn't jump to line 1009 because the condition on line 1008 was never true

1009 raise ValueError("Parameter 'endLine' is None.") 

1010 elif not isinstance(endLine, int): 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true

1011 ex = TypeError("Parameter 'endLine' is not of type 'int'.") 

1012 ex.add_note(f"Got type '{getFullyQualifiedName(endLine)}'.") 

1013 raise ex 

1014 elif endLine < 0: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true

1015 ex = ValueError("Parameter 'endLine' is negative.") 

1016 ex.add_note(f"Got value '{endLine}'.") 

1017 raise ex 

1018 

1019 if endColumn is None: 1019 ↛ 1020line 1019 didn't jump to line 1020 because the condition on line 1019 was never true

1020 raise ValueError("Parameter 'endColumn' is None.") 

1021 elif not isinstance(endColumn, int): 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true

1022 ex = TypeError("Parameter 'endColumn' is not of type 'int'.") 

1023 ex.add_note(f"Got type '{getFullyQualifiedName(endColumn)}'.") 

1024 raise ex 

1025 elif endColumn < 0: 1025 ↛ 1026line 1025 didn't jump to line 1026 because the condition on line 1025 was never true

1026 ex = ValueError("Parameter 'endColumn' is negative.") 

1027 ex.add_note(f"Got value '{endColumn}'.") 

1028 raise ex 

1029 

1030 if blocks is None: 1030 ↛ 1031line 1030 didn't jump to line 1031 because the condition on line 1030 was never true

1031 raise ValueError("Parameter 'blocks' is None.") 

1032 elif not isinstance(blocks, int): 1032 ↛ 1033line 1032 didn't jump to line 1033 because the condition on line 1032 was never true

1033 ex = TypeError("Parameter 'blocks' is not of type 'int'.") 

1034 ex.add_note(f"Got type '{getFullyQualifiedName(blocks)}'.") 

1035 raise ex 

1036 elif blocks < 0: 1036 ↛ 1037line 1036 didn't jump to line 1037 because the condition on line 1036 was never true

1037 ex = ValueError("Parameter 'blocks' is negative.") 

1038 ex.add_note(f"Got value '{blocks}'.") 

1039 raise ex 

1040 

1041 if blocksExecuted is None: 1041 ↛ 1042line 1041 didn't jump to line 1042 because the condition on line 1041 was never true

1042 raise ValueError("Parameter 'blocksExecuted' is None.") 

1043 elif not isinstance(blocksExecuted, int): 1043 ↛ 1044line 1043 didn't jump to line 1044 because the condition on line 1043 was never true

1044 ex = TypeError("Parameter 'blocksExecuted' is not of type 'int'.") 

1045 ex.add_note(f"Got type '{getFullyQualifiedName(blocksExecuted)}'.") 

1046 raise ex 

1047 elif blocksExecuted < 0: 1047 ↛ 1048line 1047 didn't jump to line 1048 because the condition on line 1047 was never true

1048 ex = ValueError("Parameter 'blocksExecuted' is negative.") 

1049 ex.add_note(f"Got value '{blocksExecuted}'.") 

1050 raise ex 

1051 

1052 if executionCount is None: 1052 ↛ 1053line 1052 didn't jump to line 1053 because the condition on line 1052 was never true

1053 raise ValueError("Parameter 'executionCount' is None.") 

1054 elif not isinstance(executionCount, int): 1054 ↛ 1055line 1054 didn't jump to line 1055 because the condition on line 1054 was never true

1055 ex = TypeError("Parameter 'executionCount' is not of type 'int'.") 

1056 ex.add_note(f"Got type '{getFullyQualifiedName(executionCount)}'.") 

1057 raise ex 

1058 elif executionCount < 0: 

1059 ex = ValueError("Parameter 'executionCount' is negative.") 

1060 ex.add_note(f"Got value '{executionCount}'.") 

1061 raise ex 

1062 

1063 if parent is not None: 

1064 if not isinstance(parent, File): 

1065 ex = TypeError("Parameter 'parent' is not of type 'File'.") 

1066 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

1067 raise ex 

1068 elif name in parent._functions: 

1069 raise ValueError(f"Parameter 'parent' contains function '{name}' already.") 

1070 

1071 self._parent = parent 

1072 self._name = name 

1073 self._demangledName = demangledName 

1074 self._startLine = startLine 

1075 self._startColumn = startColumn 

1076 self._endLine = endLine 

1077 self._endColumn = endColumn 

1078 self._blocks = blocks 

1079 self._blocksExecuted = blocksExecuted 

1080 self._executionCount = executionCount 

1081 

1082 if parent is not None: 

1083 parent._functions[self._name] = self 

1084 

1085 @classmethod 

1086 def Parse(cls, record: dict[str, Any], *, parent: Nullable[File] = None) -> Self: 

1087 """ 

1088 Parse a function from its JSON object. 

1089 

1090 :param record: The JSON object of the function. 

1091 :param parent: Optional, the file the function belongs to. Default: ``None``. 

1092 :returns: The function. 

1093 :raises ValueError: If parameter ``record`` is ``None``. 

1094 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

1095 """ 

1096 if record is None: 

1097 raise ValueError("Parameter 'record' is None.") 

1098 elif not isinstance(record, dict): 

1099 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

1100 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

1101 raise ex 

1102 

1103 return cls( 

1104 record["name"], 

1105 record["demangled_name"], 

1106 record["start_line"], 

1107 record["start_column"], 

1108 record["end_line"], 

1109 record["end_column"], 

1110 record["blocks"], 

1111 record["blocks_executed"], 

1112 record["execution_count"], 

1113 parent=parent 

1114 ) 

1115 

1116 @readonly 

1117 def Parent(self) -> Nullable[File]: 

1118 """ 

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

1120 

1121 :returns: The file; ``None`` if the function belongs to no file. 

1122 """ 

1123 return self._parent 

1124 

1125 @readonly 

1126 def Name(self) -> str: 

1127 """ 

1128 Read-only property to access the mangled name of the function (:attr:`_name`). 

1129 

1130 The method ``Containers::Stack::Pop()`` is mangled to ``_ZN10Containers5Stack3PopEv``, a C function keeps its name. 

1131 

1132 :returns: The mangled name. 

1133 """ 

1134 return self._name 

1135 

1136 @readonly 

1137 def DemangledName(self) -> str: 

1138 """ 

1139 Read-only property to access the demangled name of the function (:attr:`_demangledName`). 

1140 

1141 The demangled name of ``_ZN10Containers5Stack3PopEv`` is ``Containers::Stack::Pop()``. 

1142 

1143 :returns: The demangled name. 

1144 """ 

1145 return self._demangledName 

1146 

1147 @readonly 

1148 def StartLine(self) -> int: 

1149 """ 

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

1151 

1152 :returns: The line number. 

1153 """ 

1154 return self._startLine 

1155 

1156 @readonly 

1157 def StartColumn(self) -> int: 

1158 """ 

1159 Read-only property to access the function's first column (:attr:`_startColumn`). 

1160 

1161 :returns: The column number, counted from 1. 

1162 """ 

1163 return self._startColumn 

1164 

1165 @readonly 

1166 def EndLine(self) -> int: 

1167 """ 

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

1169 

1170 :returns: The line number. 

1171 """ 

1172 return self._endLine 

1173 

1174 @readonly 

1175 def EndColumn(self) -> int: 

1176 """ 

1177 Read-only property to access the function's last column (:attr:`_endColumn`). 

1178 

1179 :returns: The column number, counted from 1. 

1180 """ 

1181 return self._endColumn 

1182 

1183 @readonly 

1184 def Blocks(self) -> int: 

1185 """ 

1186 Read-only property to access the number of basic blocks (:attr:`_blocks`). 

1187 

1188 :returns: The number of blocks. 

1189 """ 

1190 return self._blocks 

1191 

1192 @readonly 

1193 def BlocksExecuted(self) -> int: 

1194 """ 

1195 Read-only property to access the number of executed basic blocks (:attr:`_blocksExecuted`). 

1196 

1197 :returns: The number of executed blocks. 

1198 """ 

1199 return self._blocksExecuted 

1200 

1201 @readonly 

1202 def ExecutionCount(self) -> int: 

1203 """ 

1204 Read-only property to access the number of times the function ran (:attr:`_executionCount`). 

1205 

1206 :returns: The count. 

1207 """ 

1208 return self._executionCount 

1209 

1210 

1211@export 

1212class File(metaclass=ExtendedType, slots=True): 

1213 """ 

1214 A source ``file`` of a data file: its functions and lines. 

1215 

1216 A line several functions share - e.g. the instantiations of a template - is listed once per function. 

1217 """ 

1218 

1219 _parent: Nullable[DataFile] #: The data file the file belongs to. 

1220 _path: _Path #: The file's path, as the compiler named it. 

1221 _functions: dict[str, Function] #: The functions, by mangled name. 

1222 _lines: list[Line] #: The executable lines, in order; a line of several functions once per function. 

1223 

1224 def __init__(self, path: Path, *, parent: Nullable[DataFile] = None) -> None: 

1225 """ 

1226 Initialize the file, and add it to the files of its data file. 

1227 

1228 Its functions and lines are added by creating them with this file as their parent. 

1229 

1230 :param path: The file's path, as the compiler named it. 

1231 :param parent: Optional, the data file the file belongs to; the file is added to its files by :attr:`Path`. 

1232 Default: ``None``. 

1233 :raises ValueError: If parameter ``path`` is ``None``. 

1234 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`. 

1235 :raises TypeError: If parameter ``parent`` isn't of type :class:`DataFile`. 

1236 :raises ValueError: If parameter ``parent`` contains a file of the same path already. 

1237 """ 

1238 if path is None: 1238 ↛ 1239line 1238 didn't jump to line 1239 because the condition on line 1238 was never true

1239 raise ValueError("Parameter 'path' is None.") 

1240 elif not isinstance(path, Path): 

1241 ex = TypeError("Parameter 'path' is not of type 'Path'.") 

1242 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

1243 raise ex 

1244 

1245 if parent is not None: 

1246 if not isinstance(parent, DataFile): 

1247 ex = TypeError("Parameter 'parent' is not of type 'DataFile'.") 

1248 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

1249 raise ex 

1250 elif path in parent._files: 

1251 raise ValueError(f"Parameter 'parent' contains file '{path.as_posix()}' already.") 

1252 

1253 self._parent = parent 

1254 self._path = path 

1255 self._functions = {} 

1256 self._lines = [] 

1257 

1258 if parent is not None: 

1259 parent._files[self._path] = self 

1260 

1261 @classmethod 

1262 def Parse(cls, record: dict[str, Any], *, parent: Nullable[DataFile] = None) -> Self: 

1263 """ 

1264 Parse a file, its functions and lines from its JSON object. 

1265 

1266 A backslash in the path - as a report written on Windows has them - separates directories. 

1267 

1268 :param record: The JSON object of the file. 

1269 :param parent: Optional, the data file the file belongs to. Default: ``None``. 

1270 :returns: The file. 

1271 :raises ValueError: If parameter ``record`` is ``None``. 

1272 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

1273 :raises CodeCoverageError: If the file names a function twice. 

1274 """ 

1275 if record is None: 

1276 raise ValueError("Parameter 'record' is None.") 

1277 elif not isinstance(record, dict): 

1278 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

1279 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

1280 raise ex 

1281 

1282 file = cls(Path(record["file"].replace("\\", "/")), parent=parent) 

1283 

1284 for function in record["functions"]: 

1285 if (name := function["name"]) in file._functions: 

1286 raise CodeCoverageError(f"gcov source file '{file._path.as_posix()}' names function '{name}' twice.") 

1287 

1288 Function.Parse(function, parent=file) 

1289 

1290 for line in record["lines"]: 

1291 Line.Parse(line, parent=file) 

1292 

1293 return file 

1294 

1295 @readonly 

1296 def Parent(self) -> Nullable[DataFile]: 

1297 """ 

1298 Read-only property to access the data file the file belongs to (:attr:`_parent`). 

1299 

1300 :returns: The data file; ``None`` if the file belongs to no data file. 

1301 """ 

1302 return self._parent 

1303 

1304 @readonly 

1305 def Path(self) -> Path: 

1306 """ 

1307 Read-only property to access the file's path (:attr:`_path`). 

1308 

1309 :returns: The path, relative to the directory the compiler ran in, unless absolute. 

1310 """ 

1311 return self._path 

1312 

1313 @readonly 

1314 def Functions(self) -> dict[str, Function]: 

1315 """ 

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

1317 

1318 :returns: The functions, by mangled name. 

1319 """ 

1320 return self._functions 

1321 

1322 @readonly 

1323 def Lines(self) -> list[Line]: 

1324 """ 

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

1326 

1327 :returns: The lines; a line several functions share is listed once per function. 

1328 """ 

1329 return self._lines 

1330 

1331 

1332@export 

1333class DataFile(metaclass=ExtendedType, slots=True): 

1334 """ 

1335 The coverage measured in a data file (GCDA): the versions of the format and of GCC, and the source files. 

1336 """ 

1337 

1338 _parent: Nullable[Coverage] #: The report the data file belongs to. 

1339 _path: _Path #: Path of the data file, as gcov was called with it. 

1340 _formatVersion: _FormatVersion #: Version of the report format. 

1341 _gccVersion: SemanticVersion #: Version of GCC. 

1342 _currentWorkingDirectory: Nullable[_Path] #: The directory the compiler ran in, if the report says. 

1343 _files: dict[_Path, File] #: The source files, by path. 

1344 

1345 def __init__( 

1346 self, 

1347 path: Path, 

1348 formatVersion: FormatVersion, 

1349 gccVersion: SemanticVersion, 

1350 currentWorkingDirectory: Nullable[Path] = None, 

1351 *, 

1352 parent: Nullable[Coverage] = None 

1353 ) -> None: 

1354 """ 

1355 Initialize the data file, and add it to the data files of its report. 

1356 

1357 Its source files are added by creating them with this data file as their parent. 

1358 

1359 :param path: Path of the data file, as gcov was called with it. 

1360 :param formatVersion: Version of the report format. 

1361 :param gccVersion: Version of GCC. 

1362 :param currentWorkingDirectory: Optional, the directory the compiler ran in. Default: ``None``. 

1363 :param parent: Optional, the report the data file belongs to; the data file is appended to its 

1364 data files. Default: ``None``. 

1365 :raises ValueError: If parameter ``path`` is ``None``. 

1366 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`. 

1367 :raises ValueError: If parameter ``formatVersion`` is ``None``. 

1368 :raises TypeError: If parameter ``formatVersion`` isn't of type :class:`FormatVersion`. 

1369 :raises ValueError: If parameter ``gccVersion`` is ``None``. 

1370 :raises TypeError: If parameter ``gccVersion`` isn't of type 

1371 :class:`~pyTooling.Versioning.SemanticVersion`. 

1372 :raises TypeError: If parameter ``currentWorkingDirectory`` isn't of type :class:`~pathlib.Path`. 

1373 :raises TypeError: If parameter ``parent`` isn't of type :class:`Coverage`. 

1374 """ 

1375 if path is None: 1375 ↛ 1376line 1375 didn't jump to line 1376 because the condition on line 1375 was never true

1376 raise ValueError("Parameter 'path' is None.") 

1377 elif not isinstance(path, Path): 1377 ↛ 1378line 1377 didn't jump to line 1378 because the condition on line 1377 was never true

1378 ex = TypeError("Parameter 'path' is not of type 'Path'.") 

1379 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

1380 raise ex 

1381 

1382 if formatVersion is None: 

1383 raise ValueError("Parameter 'formatVersion' is None.") 

1384 elif not isinstance(formatVersion, FormatVersion): 

1385 ex = TypeError("Parameter 'formatVersion' is not of type 'FormatVersion'.") 

1386 ex.add_note(f"Got type '{getFullyQualifiedName(formatVersion)}'.") 

1387 raise ex 

1388 

1389 if gccVersion is None: 1389 ↛ 1390line 1389 didn't jump to line 1390 because the condition on line 1389 was never true

1390 raise ValueError("Parameter 'gccVersion' is None.") 

1391 elif not isinstance(gccVersion, SemanticVersion): 

1392 ex = TypeError("Parameter 'gccVersion' is not of type 'SemanticVersion'.") 

1393 ex.add_note(f"Got type '{getFullyQualifiedName(gccVersion)}'.") 

1394 raise ex 

1395 

1396 if currentWorkingDirectory is not None and not isinstance(currentWorkingDirectory, Path): 1396 ↛ 1397line 1396 didn't jump to line 1397 because the condition on line 1396 was never true

1397 ex = TypeError("Parameter 'currentWorkingDirectory' is not of type 'Path'.") 

1398 ex.add_note(f"Got type '{getFullyQualifiedName(currentWorkingDirectory)}'.") 

1399 raise ex 

1400 

1401 if parent is not None and not isinstance(parent, Coverage): 

1402 ex = TypeError("Parameter 'parent' is not of type 'Coverage'.") 

1403 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

1404 raise ex 

1405 

1406 self._parent = parent 

1407 self._path = path 

1408 self._formatVersion = formatVersion 

1409 self._gccVersion = gccVersion 

1410 self._currentWorkingDirectory = currentWorkingDirectory 

1411 self._files = {} 

1412 

1413 if parent is not None: 

1414 parent._dataFiles.append(self) 

1415 

1416 @classmethod 

1417 def Parse(cls, record: dict[str, Any], *, parent: Nullable[Coverage] = None) -> Self: 

1418 """ 

1419 Parse a data file and its source files from its JSON object. 

1420 

1421 The format version is converted by :meth:`FormatVersion.Parse`; a development build of GCC states its date and 

1422 phase behind its version, e.g. ``15.0.1 20250418 (experimental)``. A backslash in a path - as a report written on 

1423 Windows has them - separates directories. 

1424 

1425 :param record: The JSON object of the data file: a report's root object. 

1426 :param parent: Optional, the report the data file belongs to. Default: ``None``. 

1427 :returns: The data file. 

1428 :raises ValueError: If parameter ``record`` is ``None``. 

1429 :raises TypeError: If parameter ``record`` isn't of type :class:`dict`. 

1430 :raises CodeCoverageError: If the data file names a source file twice. 

1431 """ 

1432 if record is None: 

1433 raise ValueError("Parameter 'record' is None.") 

1434 elif not isinstance(record, dict): 

1435 ex = TypeError("Parameter 'record' is not of type 'dict'.") 

1436 ex.add_note(f"Got type '{getFullyQualifiedName(record)}'.") 

1437 raise ex 

1438 

1439 directory = record.get("current_working_directory") 

1440 dataFile = cls( 

1441 Path(record["data_file"].replace("\\", "/")), 

1442 FormatVersion.Parse(record["format_version"]), 

1443 SemanticVersion.Parse(record["gcc_version"].split(" ", 1)[0]), 

1444 Path(directory.replace("\\", "/")) if directory is not None else None, 

1445 parent=parent 

1446 ) 

1447 

1448 for file in record["files"]: 

1449 if (path := Path(file["file"].replace("\\", "/"))) in dataFile._files: 

1450 raise CodeCoverageError( 

1451 f"gcov data file '{dataFile._path.as_posix()}' names source file '{path.as_posix()}' twice." 

1452 ) 

1453 

1454 File.Parse(file, parent=dataFile) 

1455 

1456 return dataFile 

1457 

1458 @readonly 

1459 def Parent(self) -> Nullable[Coverage]: 

1460 """ 

1461 Read-only property to access the report the data file belongs to (:attr:`_parent`). 

1462 

1463 :returns: The report; ``None`` if the data file belongs to no report. 

1464 """ 

1465 return self._parent 

1466 

1467 @readonly 

1468 def Path(self) -> Path: 

1469 """ 

1470 Read-only property to access the path of the data file (:attr:`_path`). 

1471 

1472 It is the path gcov was called with, e.g. ``main.c`` for ``gcov --json-format main.c``. 

1473 

1474 :returns: The path, as gcov was called with it. 

1475 """ 

1476 return self._path 

1477 

1478 @readonly 

1479 def FormatVersion(self) -> FormatVersion: 

1480 """ 

1481 Read-only property to access the version of the report format (:attr:`_formatVersion`). 

1482 

1483 :returns: The format version. 

1484 """ 

1485 return self._formatVersion 

1486 

1487 @readonly 

1488 def GCCVersion(self) -> SemanticVersion: 

1489 """ 

1490 Read-only property to access the version of GCC (:attr:`_gccVersion`). 

1491 

1492 A development build of GCC states its date and phase behind the version, e.g. ``15.0.1 20250418 (experimental)``; 

1493 they aren't kept. 

1494 

1495 :returns: The version. 

1496 """ 

1497 return self._gccVersion 

1498 

1499 @readonly 

1500 def CurrentWorkingDirectory(self) -> Nullable[Path]: 

1501 """ 

1502 Read-only property to access the directory the compiler ran in (:attr:`_currentWorkingDirectory`). 

1503 

1504 :returns: The directory, on the machine the code was compiled on, or ``None`` if the report doesn't say. 

1505 """ 

1506 return self._currentWorkingDirectory 

1507 

1508 @readonly 

1509 def Files(self) -> dict[Path, File]: 

1510 """ 

1511 Read-only property to access the source files (:attr:`_files`). 

1512 

1513 :returns: The files, by path. 

1514 """ 

1515 return self._files 

1516 

1517 

1518@export 

1519class Coverage(metaclass=ExtendedType, mixin=True): 

1520 """ 

1521 The content of a report file: the data files. 

1522 

1523 The root of the format's model: a data file names it as its parent. 

1524 """ 

1525 

1526 _dataFiles: list[DataFile] #: The data files, in the order the report lists them. 

1527 

1528 def __init__(self) -> None: 

1529 """ 

1530 Initialize an empty report. 

1531 """ 

1532 self._dataFiles = [] 

1533 

1534 @readonly 

1535 def DataFiles(self) -> list[DataFile]: 

1536 """ 

1537 Read-only property to access the data files (:attr:`_dataFiles`). 

1538 

1539 :returns: The data files: one of a :file:`*.gcov.json.gz` file; one per line of gcov's standard output. 

1540 """ 

1541 return self._dataFiles 

1542 

1543 

1544@export 

1545class Document(cc_Document, Coverage): 

1546 """ 

1547 A gcov JSON code coverage report: read into the format's model, and converted to the common model. 

1548 """ 

1549 

1550 _jsonDocuments: Nullable[list[dict[str, Any]]] #: The parsed and validated JSON objects, after :meth:`Analyze`. 

1551 

1552 def __init__(self, jsonReportFile: Path, analyzeAndConvert: bool = False) -> None: 

1553 """ 

1554 Initialize the report, and optionally read it. 

1555 

1556 :param jsonReportFile: Path to the JSON file, gzip-compressed or plain. 

1557 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``. 

1558 """ 

1559 super().__init__(jsonReportFile) 

1560 Coverage.__init__(self) 

1561 

1562 self._jsonDocuments = None 

1563 

1564 if analyzeAndConvert: 

1565 self.Analyze() 

1566 self.Convert() 

1567 

1568 def Analyze(self) -> None: 

1569 """ 

1570 Read the file - decompressing it, if gzip-compressed -, parse its JSON objects and validate each against the JSON 

1571 Schema of the format version it states (:data:`SCHEMAS`). 

1572 

1573 :raises CodeCoverageError: If the file doesn't exist. 

1574 :raises CodeCoverageError: If the file can't be read. 

1575 :raises CodeCoverageError: If the file is gzip-compressed, but corrupt. 

1576 :raises CodeCoverageError: If the file isn't valid JSON. 

1577 :raises CodeCoverageError: If the file holds no JSON object. 

1578 :raises CodeCoverageError: If a JSON object states no supported format version. 

1579 :raises CodeCoverageError: If a JSON Schema can't be read. 

1580 :raises CodeCoverageError: If a JSON object isn't valid according to the JSON Schema of its format version. 

1581 """ 

1582 if not self._path.exists(): 

1583 raise CodeCoverageError(f"gcov report file '{self._path}' does not exist.") \ 

1584 from FileNotFoundError(f"File '{self._path}' not found.") 

1585 

1586 from jsonschema import Draft202012Validator 

1587 

1588 with Stopwatch() as sw: 

1589 try: 

1590 content = self._path.read_bytes() 

1591 except OSError as ex: 

1592 raise CodeCoverageError(f"Couldn't read gcov report file '{self._path}'.") from ex 

1593 

1594 if content[:2] == b"\x1f\x8b": 

1595 try: 

1596 content = decompress(content) 

1597 except (BadGzipFile, EOFError, ZLibError) as ex: 

1598 raise CodeCoverageError(f"gzip error in gcov report file '{self._path}'.") from ex 

1599 

1600 # gcov's standard output holds one JSON object per data file 

1601 jsonDocuments = [] 

1602 try: 

1603 text = content.decode("utf-8") 

1604 decoder = JSONDecoder() 

1605 nonWhitespace = re_compile(r"\S") 

1606 position = 0 

1607 while (match := nonWhitespace.search(text, position)) is not None: 

1608 jsonDocument, position = decoder.raw_decode(text, match.start()) 

1609 jsonDocuments.append(jsonDocument) 

1610 except (UnicodeDecodeError, JSONDecodeError) as ex: 

1611 raise CodeCoverageError(f"JSON syntax error in gcov report file '{self._path}'.") from ex 

1612 

1613 if len(jsonDocuments) == 0: 

1614 raise CodeCoverageError(f"gcov report file '{self._path}' holds no JSON object.") 

1615 

1616 # a note's path is prefixed by the object's index, if the file holds several 

1617 validators: dict[FormatVersion, Draft202012Validator] = {} 

1618 for index, jsonDocument in enumerate(jsonDocuments): 

1619 prefix = f"[{index}]" if len(jsonDocuments) > 1 else "" 

1620 

1621 version = jsonDocument.get("format_version") if isinstance(jsonDocument, dict) else None 

1622 try: 

1623 formatVersion = FormatVersion.Parse(version) 

1624 except (ValueError, TypeError) as cause: 

1625 ex = CodeCoverageError(f"gcov report file '{self._path}' states an unsupported format version.") 

1626 got = f"value '{version}'" if version is not None else "no value" 

1627 ex.add_note(f"Got {got} at '{prefix}/format_version'.") 

1628 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in FormatVersion)}.") 

1629 raise ex from cause 

1630 

1631 schemaFile = SCHEMAS[formatVersion] 

1632 if (validator := validators.get(formatVersion)) is None: 

1633 try: 

1634 schema = loads(readResourceFile(Resources, schemaFile)) 

1635 except (ToolingException, JSONDecodeError) as ex: 

1636 raise CodeCoverageError(f"Couldn't read JSON Schema '{schemaFile}' from package resources.") from ex 

1637 

1638 validator = validators[formatVersion] = Draft202012Validator(schema) 

1639 

1640 errors = sorted(validator.iter_errors(jsonDocument), key=lambda error: list(error.path)) 

1641 if len(errors) > 0: 

1642 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{schemaFile}'.") 

1643 for error in errors: 

1644 ex.add_note(f"{prefix}/{'/'.join(str(part) for part in error.path)}: {error.message}") 

1645 raise ex 

1646 

1647 self._jsonDocuments = jsonDocuments 

1648 

1649 self._analysisDuration = sw.Duration 

1650 

1651 def Convert(self) -> None: 

1652 """ 

1653 Convert the parsed and validated JSON objects to the format's model. 

1654 

1655 :raises CodeCoverageError: If the JSON file was not analyzed before. |br| 

1656 Call 'Document.Analyze()' or create the document using 

1657 'Document(path, analyzeAndConvert=True)'. 

1658 :raises CodeCoverageError: If a data file names a source file twice. 

1659 :raises CodeCoverageError: If a source file names a function twice. 

1660 """ 

1661 if self._jsonDocuments is None: 

1662 ex = CodeCoverageError(f"gcov report file '{self._path}' needs to be read and analyzed by a JSON parser.") 

1663 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.") 

1664 raise ex 

1665 

1666 with Stopwatch() as sw: 

1667 for jsonDocument in self._jsonDocuments: 

1668 DataFile.Parse(jsonDocument, parent=self) 

1669 

1670 self._conversionDuration = sw.Duration 

1671 

1672 def ToCoverageSummary(self) -> CoverageSummary: 

1673 """ 

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

1675 

1676 A source file several data files state - e.g. a header - becomes one file: the counts of its lines and branches, 

1677 and of its functions, are added. The directories the compiler ran in become the source directories. 

1678 

1679 :returns: The report's root of the common model, named after the report file without the 

1680 extensions ``.gcov``, ``.json`` and ``.gz``. 

1681 :raises CodeCoverageError: If a file's path runs through another file. 

1682 """ 

1683 name = self._path.name.removesuffix(".gz").removesuffix(".json").removesuffix(".gcov") 

1684 directories = { 

1685 dataFile._currentWorkingDirectory for dataFile in self._dataFiles if dataFile._currentWorkingDirectory is not None 

1686 } 

1687 summary = CoverageSummary(name if name != "" else self._path.name, sourceDirectories=sorted(directories)) 

1688 

1689 # per file of the common model: the counts of its lines, of its lines' branches, and of its functions 

1690 lineCounts: dict[cc_File, dict[int, int]] = {} 

1691 branchCounts: dict[cc_File, dict[int, dict[tuple[Nullable[str], int, Nullable[int], Nullable[int]], int]]] = {} 

1692 functionCounts: dict[cc_File, dict[str, tuple[int, int, int]]] = {} 

1693 for dataFile in self._dataFiles: 

1694 for file in dataFile._files.values(): 

1695 commonFile = summary.GetOrAddFile(file._path) 

1696 counts = lineCounts.setdefault(commonFile, {}) 

1697 branches = branchCounts.setdefault(commonFile, {}) 

1698 functions = functionCounts.setdefault(commonFile, {}) 

1699 

1700 for line in file._lines: 

1701 counts[line._lineNumber] = counts.get(line._lineNumber, 0) + line._count 

1702 lineBranches = branches.setdefault(line._lineNumber, {}) 

1703 for index, branch in enumerate(line._branches): 

1704 key = (line._functionName, index, branch._sourceBlockID, branch._destinationBlockID) 

1705 lineBranches[key] = lineBranches.get(key, 0) + branch._count 

1706 

1707 for function in file._functions.values(): 

1708 startLine, endLine, count = functions.get( 

1709 function._demangledName, (function._startLine, function._endLine, 0) 

1710 ) 

1711 functions[function._demangledName] = (startLine, endLine, count + function._executionCount) 

1712 

1713 for commonFile, counts in lineCounts.items(): 

1714 for number in sorted(counts): 

1715 lineBranches = branchCounts[commonFile][number] 

1716 if counts[number] == 0: 

1717 status = LineCoverageStatus.Uncovered 

1718 elif 0 in lineBranches.values(): 

1719 status = LineCoverageStatus.PartiallyCovered 

1720 else: 

1721 status = LineCoverageStatus.Covered 

1722 

1723 commonLine = cc_Line(number, status, counts[number], parent=commonFile) 

1724 for count in lineBranches.values(): 

1725 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=commonLine) 

1726 

1727 lines = commonFile._lines 

1728 lastLineNumber = commonFile._lastLineNumber 

1729 sourceFile = cc_SourceFile( 

1730 commonFile.Path.as_posix(), 

1731 file=commonFile, 

1732 startLine=next(commonFile.IterateLines(), None), 

1733 endLine=lines[lastLineNumber] if lastLineNumber > 0 else None, 

1734 parent=summary 

1735 ) 

1736 

1737 for functionName, (startLine, endLine, count) in functionCounts[commonFile].items(): 

1738 numbers = [number for number in range(startLine, min(endLine, lastLineNumber) + 1) if lines[number] is not None] 

1739 cc_Function( 

1740 functionName, 

1741 file=commonFile, 

1742 startLine=lines[numbers[0]] if len(numbers) > 0 else None, 

1743 endLine=lines[numbers[-1]] if len(numbers) > 0 else None, 

1744 status=LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, 

1745 coverageCount=count, 

1746 parent=sourceFile 

1747 ) 

1748 

1749 summary.Aggregate() 

1750 return summary