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

804 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:49 +0000

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 jsonschema import Draft202012Validator 

79from pyTooling.Common import getFullyQualifiedName, readResourceFile 

80from pyTooling.Decorators import export, readonly 

81from pyTooling.Exceptions import ToolingException 

82from pyTooling.MetaClasses import ExtendedType 

83from pyTooling.Stopwatch import Stopwatch 

84from pyTooling.Versioning import SemanticVersion 

85 

86from pyEDAA.Reports import Resources 

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

88from pyEDAA.Reports.CodeCoverage import Document as cc_Document, File as cc_File, Function as cc_Function 

89from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus, SourceFile as cc_SourceFile 

90 

91 

92__all__ = ["SCHEMAS"] 

93 

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

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

96_Path = Path 

97 

98 

99@export 

100class FormatVersion(IntEnum): 

101 """ 

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

103 

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

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

106 """ 

107 

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

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

110 

111 @classmethod 

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

113 """ 

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

115 

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

117 

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

119 :returns: The member of that version. 

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

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

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

123 """ 

124 if value is None: 

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

126 elif not isinstance(value, str): 

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

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

129 raise ex 

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

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

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

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

134 raise ex 

135 

136 return cls(int(value)) 

137 

138 

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

140_FormatVersion = FormatVersion 

141 

142SCHEMAS: dict[FormatVersion, str] = { 

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

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

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

146 

147 

148@export 

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

150 """ 

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

152 """ 

153 

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

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

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

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

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

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

160 

161 def __init__( 

162 self, 

163 count: int, 

164 throw: bool, 

165 fallthrough: bool, 

166 sourceBlockID: Nullable[int] = None, 

167 destinationBlockID: Nullable[int] = None, 

168 *, 

169 parent: Nullable[Line] = None 

170 ) -> None: 

171 """ 

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

173 

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

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

176 :param fallthrough: Whether the branch falls through. 

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

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

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

180 Default: ``None``. 

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

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

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

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

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

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

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

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

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

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

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

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

193 """ 

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

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

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

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

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

199 raise ex 

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

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

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

203 raise ex 

204 

205 if throw is None: 

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

207 elif not isinstance(throw, bool): 

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

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

210 raise ex 

211 

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

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

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

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

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

217 raise ex 

218 

219 if sourceBlockID is not None: 

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

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

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

223 raise ex 

224 elif sourceBlockID < 0: 

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

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

227 raise ex 

228 

229 if destinationBlockID is not None: 

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

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

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

233 raise ex 

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

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

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

237 raise ex 

238 

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

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

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

242 raise ex 

243 

244 self._parent = parent 

245 self._count = count 

246 self._throw = throw 

247 self._fallthrough = fallthrough 

248 self._sourceBlockID = sourceBlockID 

249 self._destinationBlockID = destinationBlockID 

250 

251 if parent is not None: 

252 parent._branches.append(self) 

253 

254 @classmethod 

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

256 """ 

257 Parse a branch from its JSON object. 

258 

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

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

261 :returns: The branch. 

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

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

264 """ 

265 if record is None: 

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

267 elif not isinstance(record, dict): 

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

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

270 raise ex 

271 

272 return cls( 

273 record["count"], 

274 record["throw"], 

275 record["fallthrough"], 

276 record.get("source_block_id"), 

277 record.get("destination_block_id"), 

278 parent=parent 

279 ) 

280 

281 @readonly 

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

283 """ 

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

285 

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

287 """ 

288 return self._parent 

289 

290 @readonly 

291 def Count(self) -> int: 

292 """ 

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

294 

295 :returns: The count. 

296 """ 

297 return self._count 

298 

299 @readonly 

300 def Throw(self) -> bool: 

301 """ 

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

303 

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

305 """ 

306 return self._throw 

307 

308 @readonly 

309 def Fallthrough(self) -> bool: 

310 """ 

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

312 

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

314 """ 

315 return self._fallthrough 

316 

317 @readonly 

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

319 """ 

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

321 

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

323 """ 

324 return self._sourceBlockID 

325 

326 @readonly 

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

328 """ 

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

330 

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

332 """ 

333 return self._destinationBlockID 

334 

335 

336@export 

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

338 """ 

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

340 """ 

341 

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

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

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

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

346 

347 def __init__( 

348 self, 

349 sourceBlockID: int, 

350 destinationBlockID: int, 

351 returned: int, 

352 *, 

353 parent: Nullable[Line] = None 

354 ) -> None: 

355 """ 

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

357 

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

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

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

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

362 Default: ``None``. 

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

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

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

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

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

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

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

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

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

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

373 """ 

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

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

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

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

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

379 raise ex 

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

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

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

383 raise ex 

384 

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

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

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

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

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

390 raise ex 

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

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

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

394 raise ex 

395 

396 if returned is None: 

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

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

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

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

401 raise ex 

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

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

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

405 raise ex 

406 

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

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

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

410 raise ex 

411 

412 self._parent = parent 

413 self._sourceBlockID = sourceBlockID 

414 self._destinationBlockID = destinationBlockID 

415 self._returned = returned 

416 

417 if parent is not None: 

418 parent._calls.append(self) 

419 

420 @classmethod 

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

422 """ 

423 Parse a call from its JSON object. 

424 

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

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

427 :returns: The call. 

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

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

430 """ 

431 if record is None: 

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

433 elif not isinstance(record, dict): 

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

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

436 raise ex 

437 

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

439 

440 @readonly 

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

442 """ 

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

444 

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

446 """ 

447 return self._parent 

448 

449 @readonly 

450 def SourceBlockID(self) -> int: 

451 """ 

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

453 

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

455 """ 

456 return self._sourceBlockID 

457 

458 @readonly 

459 def DestinationBlockID(self) -> int: 

460 """ 

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

462 

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

464 """ 

465 return self._destinationBlockID 

466 

467 @readonly 

468 def Returned(self) -> int: 

469 """ 

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

471 

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

473 """ 

474 return self._returned 

475 

476 

477@export 

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

479 """ 

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

481 expression (MC/DC). 

482 """ 

483 

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

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

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

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

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

489 

490 def __init__( 

491 self, 

492 count: int, 

493 covered: int, 

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

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

496 *, 

497 parent: Nullable[Line] = None 

498 ) -> None: 

499 """ 

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

501 

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

503 :param covered: Number of covered condition outcomes. 

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

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

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

507 Default: ``None``. 

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

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

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

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

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

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

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

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

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

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

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

519 """ 

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

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

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

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

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

525 raise ex 

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

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

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

529 raise ex 

530 

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

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

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

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

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

536 raise ex 

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

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

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

540 raise ex 

541 

542 notCoveredTrueList: list[int] = [] 

543 if notCoveredTrue is not None: 

544 if not isinstance(notCoveredTrue, Iterable): 

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

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

547 raise ex 

548 

549 for term in notCoveredTrue: 

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

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

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

553 raise ex 

554 

555 notCoveredTrueList.append(term) 

556 

557 notCoveredFalseList: list[int] = [] 

558 if notCoveredFalse is not None: 

559 if not isinstance(notCoveredFalse, Iterable): 

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

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

562 raise ex 

563 

564 for term in notCoveredFalse: 

565 if not isinstance(term, int): 

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

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

568 raise ex 

569 

570 notCoveredFalseList.append(term) 

571 

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

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

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

575 raise ex 

576 

577 self._parent = parent 

578 self._count = count 

579 self._covered = covered 

580 self._notCoveredTrue = notCoveredTrueList 

581 self._notCoveredFalse = notCoveredFalseList 

582 

583 if parent is not None: 

584 parent._conditions.append(self) 

585 

586 @classmethod 

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

588 """ 

589 Parse a condition from its JSON object. 

590 

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

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

593 :returns: The condition. 

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

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

596 """ 

597 if record is None: 

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

599 elif not isinstance(record, dict): 

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

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

602 raise ex 

603 

604 return cls( 

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

606 ) 

607 

608 @readonly 

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

610 """ 

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

612 

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

614 """ 

615 return self._parent 

616 

617 @readonly 

618 def Count(self) -> int: 

619 """ 

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

621 

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

623 """ 

624 return self._count 

625 

626 @readonly 

627 def Covered(self) -> int: 

628 """ 

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

630 

631 :returns: The number of covered outcomes. 

632 """ 

633 return self._covered 

634 

635 @readonly 

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

637 """ 

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

639 

640 :returns: The terms' indices. 

641 """ 

642 return self._notCoveredTrue 

643 

644 @readonly 

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

646 """ 

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

648 

649 :returns: The terms' indices. 

650 """ 

651 return self._notCoveredFalse 

652 

653 

654@export 

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

656 """ 

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

658 """ 

659 

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

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

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

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

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

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

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

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

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

669 

670 def __init__( 

671 self, 

672 lineNumber: int, 

673 count: int, 

674 unexecutedBlock: bool, 

675 functionName: Nullable[str] = None, 

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

677 *, 

678 parent: Nullable[File] = None 

679 ) -> None: 

680 """ 

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

682 

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

684 

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

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

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

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

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

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

691 Default: ``None``. 

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

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

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

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

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

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

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

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

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

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

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

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

704 """ 

705 if lineNumber is None: 

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

707 elif not isinstance(lineNumber, int): 

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

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

710 raise ex 

711 elif lineNumber < 1: 

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

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

714 raise ex 

715 

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

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

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

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

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

721 raise ex 

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

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

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

725 raise ex 

726 

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

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

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

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

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

732 raise ex 

733 

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

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

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

737 raise ex 

738 

739 blockIDList: list[int] = [] 

740 if blockIDs is not None: 

741 if not isinstance(blockIDs, Iterable): 

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

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

744 raise ex 

745 

746 for blockID in blockIDs: 

747 if not isinstance(blockID, int): 

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

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

750 raise ex 

751 

752 blockIDList.append(blockID) 

753 

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

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

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

757 raise ex 

758 

759 self._parent = parent 

760 self._lineNumber = lineNumber 

761 self._functionName = functionName 

762 self._count = count 

763 self._unexecutedBlock = unexecutedBlock 

764 self._blockIDs = blockIDList 

765 self._branches = [] 

766 self._calls = [] 

767 self._conditions = [] 

768 

769 if parent is not None: 

770 parent._lines.append(self) 

771 

772 @classmethod 

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

774 """ 

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

776 

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

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

779 :returns: The line. 

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

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

782 """ 

783 if record is None: 

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

785 elif not isinstance(record, dict): 

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

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

788 raise ex 

789 

790 line = cls( 

791 record["line_number"], 

792 record["count"], 

793 record["unexecuted_block"], 

794 record.get("function_name"), 

795 record.get("block_ids"), 

796 parent=parent 

797 ) 

798 

799 for branch in record["branches"]: 

800 Branch.Parse(branch, parent=line) 

801 

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

803 Call.Parse(call, parent=line) 

804 

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

806 Condition.Parse(condition, parent=line) 

807 

808 return line 

809 

810 @readonly 

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

812 """ 

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

814 

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

816 """ 

817 return self._parent 

818 

819 @readonly 

820 def LineNumber(self) -> int: 

821 """ 

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

823 

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

825 """ 

826 return self._lineNumber 

827 

828 @readonly 

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

830 """ 

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

832 

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

834 """ 

835 return self._functionName 

836 

837 @readonly 

838 def Count(self) -> int: 

839 """ 

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

841 

842 :returns: The count. 

843 """ 

844 return self._count 

845 

846 @readonly 

847 def UnexecutedBlock(self) -> bool: 

848 """ 

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

850 

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

852 

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

854 """ 

855 return self._unexecutedBlock 

856 

857 @readonly 

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

859 """ 

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

861 

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

863 """ 

864 return self._blockIDs 

865 

866 @readonly 

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

868 """ 

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

870 

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

872 """ 

873 return self._branches 

874 

875 @readonly 

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

877 """ 

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

879 

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

881 """ 

882 return self._calls 

883 

884 @readonly 

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

886 """ 

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

888 

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

890 """ 

891 return self._conditions 

892 

893 

894@export 

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

896 """ 

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

898 """ 

899 

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

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

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

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

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

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

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

907 _blocks: int #: Number of basic blocks. 

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

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

910 

911 def __init__( 

912 self, 

913 name: str, 

914 demangledName: str, 

915 startLine: int, 

916 startColumn: int, 

917 endLine: int, 

918 endColumn: int, 

919 blocks: int, 

920 blocksExecuted: int, 

921 executionCount: int, 

922 *, 

923 parent: Nullable[File] = None 

924 ) -> None: 

925 """ 

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

927 

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

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

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

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

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

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

934 :param blocks: Number of basic blocks. 

935 :param blocksExecuted: Number of executed basic blocks. 

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

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

938 :attr:`Name`. Default: ``None``. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

968 """ 

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

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

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

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

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

974 raise ex 

975 elif name == "": 

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

977 

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

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

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

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

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

983 raise ex 

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

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

986 

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

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

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

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

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

992 raise ex 

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

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

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

996 raise ex 

997 

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

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

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

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

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

1003 raise ex 

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

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

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

1007 raise ex 

1008 

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

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

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

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

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

1014 raise ex 

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

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

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

1018 raise ex 

1019 

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

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

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

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

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

1025 raise ex 

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

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

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

1029 raise ex 

1030 

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

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

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

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

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

1036 raise ex 

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

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

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

1040 raise ex 

1041 

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

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

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

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

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

1047 raise ex 

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

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

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

1051 raise ex 

1052 

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

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

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

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

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

1058 raise ex 

1059 elif executionCount < 0: 

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

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

1062 raise ex 

1063 

1064 if parent is not None: 

1065 if not isinstance(parent, File): 

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

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

1068 raise ex 

1069 elif name in parent._functions: 

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

1071 

1072 self._parent = parent 

1073 self._name = name 

1074 self._demangledName = demangledName 

1075 self._startLine = startLine 

1076 self._startColumn = startColumn 

1077 self._endLine = endLine 

1078 self._endColumn = endColumn 

1079 self._blocks = blocks 

1080 self._blocksExecuted = blocksExecuted 

1081 self._executionCount = executionCount 

1082 

1083 if parent is not None: 

1084 parent._functions[self._name] = self 

1085 

1086 @classmethod 

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

1088 """ 

1089 Parse a function from its JSON object. 

1090 

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

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

1093 :returns: The function. 

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

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

1096 """ 

1097 if record is None: 

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

1099 elif not isinstance(record, dict): 

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

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

1102 raise ex 

1103 

1104 return cls( 

1105 record["name"], 

1106 record["demangled_name"], 

1107 record["start_line"], 

1108 record["start_column"], 

1109 record["end_line"], 

1110 record["end_column"], 

1111 record["blocks"], 

1112 record["blocks_executed"], 

1113 record["execution_count"], 

1114 parent=parent 

1115 ) 

1116 

1117 @readonly 

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

1119 """ 

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

1121 

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

1123 """ 

1124 return self._parent 

1125 

1126 @readonly 

1127 def Name(self) -> str: 

1128 """ 

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

1130 

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

1132 

1133 :returns: The mangled name. 

1134 """ 

1135 return self._name 

1136 

1137 @readonly 

1138 def DemangledName(self) -> str: 

1139 """ 

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

1141 

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

1143 

1144 :returns: The demangled name. 

1145 """ 

1146 return self._demangledName 

1147 

1148 @readonly 

1149 def StartLine(self) -> int: 

1150 """ 

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

1152 

1153 :returns: The line number. 

1154 """ 

1155 return self._startLine 

1156 

1157 @readonly 

1158 def StartColumn(self) -> int: 

1159 """ 

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

1161 

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

1163 """ 

1164 return self._startColumn 

1165 

1166 @readonly 

1167 def EndLine(self) -> int: 

1168 """ 

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

1170 

1171 :returns: The line number. 

1172 """ 

1173 return self._endLine 

1174 

1175 @readonly 

1176 def EndColumn(self) -> int: 

1177 """ 

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

1179 

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

1181 """ 

1182 return self._endColumn 

1183 

1184 @readonly 

1185 def Blocks(self) -> int: 

1186 """ 

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

1188 

1189 :returns: The number of blocks. 

1190 """ 

1191 return self._blocks 

1192 

1193 @readonly 

1194 def BlocksExecuted(self) -> int: 

1195 """ 

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

1197 

1198 :returns: The number of executed blocks. 

1199 """ 

1200 return self._blocksExecuted 

1201 

1202 @readonly 

1203 def ExecutionCount(self) -> int: 

1204 """ 

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

1206 

1207 :returns: The count. 

1208 """ 

1209 return self._executionCount 

1210 

1211 

1212@export 

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

1214 """ 

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

1216 

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

1218 """ 

1219 

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

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

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

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

1224 

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

1226 """ 

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

1228 

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

1230 

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

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

1233 Default: ``None``. 

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

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

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

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

1238 """ 

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

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

1241 elif not isinstance(path, Path): 

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

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

1244 raise ex 

1245 

1246 if parent is not None: 

1247 if not isinstance(parent, DataFile): 

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

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

1250 raise ex 

1251 elif path in parent._files: 

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

1253 

1254 self._parent = parent 

1255 self._path = path 

1256 self._functions = {} 

1257 self._lines = [] 

1258 

1259 if parent is not None: 

1260 parent._files[self._path] = self 

1261 

1262 @classmethod 

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

1264 """ 

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

1266 

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

1268 

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

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

1271 :returns: The file. 

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

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

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

1275 """ 

1276 if record is None: 

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

1278 elif not isinstance(record, dict): 

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

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

1281 raise ex 

1282 

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

1284 

1285 for function in record["functions"]: 

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

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

1288 

1289 Function.Parse(function, parent=file) 

1290 

1291 for line in record["lines"]: 

1292 Line.Parse(line, parent=file) 

1293 

1294 return file 

1295 

1296 @readonly 

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

1298 """ 

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

1300 

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

1302 """ 

1303 return self._parent 

1304 

1305 @readonly 

1306 def Path(self) -> Path: 

1307 """ 

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

1309 

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

1311 """ 

1312 return self._path 

1313 

1314 @readonly 

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

1316 """ 

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

1318 

1319 :returns: The functions, by mangled name. 

1320 """ 

1321 return self._functions 

1322 

1323 @readonly 

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

1325 """ 

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

1327 

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

1329 """ 

1330 return self._lines 

1331 

1332 

1333@export 

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

1335 """ 

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

1337 """ 

1338 

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

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

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

1342 _gccVersion: SemanticVersion #: Version of GCC. 

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

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

1345 

1346 def __init__( 

1347 self, 

1348 path: Path, 

1349 formatVersion: FormatVersion, 

1350 gccVersion: SemanticVersion, 

1351 currentWorkingDirectory: Nullable[Path] = None, 

1352 *, 

1353 parent: Nullable[Coverage] = None 

1354 ) -> None: 

1355 """ 

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

1357 

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

1359 

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

1361 :param formatVersion: Version of the report format. 

1362 :param gccVersion: Version of GCC. 

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

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

1365 data files. Default: ``None``. 

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

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

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

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

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

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

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

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

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

1375 """ 

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

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

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

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

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

1381 raise ex 

1382 

1383 if formatVersion is None: 

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

1385 elif not isinstance(formatVersion, FormatVersion): 

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

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

1388 raise ex 

1389 

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

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

1392 elif not isinstance(gccVersion, SemanticVersion): 

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

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

1395 raise ex 

1396 

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

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

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

1400 raise ex 

1401 

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

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

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

1405 raise ex 

1406 

1407 self._parent = parent 

1408 self._path = path 

1409 self._formatVersion = formatVersion 

1410 self._gccVersion = gccVersion 

1411 self._currentWorkingDirectory = currentWorkingDirectory 

1412 self._files = {} 

1413 

1414 if parent is not None: 

1415 parent._dataFiles.append(self) 

1416 

1417 @classmethod 

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

1419 """ 

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

1421 

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

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

1424 Windows has them - separates directories. 

1425 

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

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

1428 :returns: The data file. 

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

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

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

1432 """ 

1433 if record is None: 

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

1435 elif not isinstance(record, dict): 

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

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

1438 raise ex 

1439 

1440 directory = record.get("current_working_directory") 

1441 dataFile = cls( 

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

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

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

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

1446 parent=parent 

1447 ) 

1448 

1449 for file in record["files"]: 

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

1451 raise CodeCoverageError( 

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

1453 ) 

1454 

1455 File.Parse(file, parent=dataFile) 

1456 

1457 return dataFile 

1458 

1459 @readonly 

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

1461 """ 

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

1463 

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

1465 """ 

1466 return self._parent 

1467 

1468 @readonly 

1469 def Path(self) -> Path: 

1470 """ 

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

1472 

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

1474 

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

1476 """ 

1477 return self._path 

1478 

1479 @readonly 

1480 def FormatVersion(self) -> FormatVersion: 

1481 """ 

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

1483 

1484 :returns: The format version. 

1485 """ 

1486 return self._formatVersion 

1487 

1488 @readonly 

1489 def GCCVersion(self) -> SemanticVersion: 

1490 """ 

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

1492 

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

1494 they aren't kept. 

1495 

1496 :returns: The version. 

1497 """ 

1498 return self._gccVersion 

1499 

1500 @readonly 

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

1502 """ 

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

1504 

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

1506 """ 

1507 return self._currentWorkingDirectory 

1508 

1509 @readonly 

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

1511 """ 

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

1513 

1514 :returns: The files, by path. 

1515 """ 

1516 return self._files 

1517 

1518 

1519@export 

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

1521 """ 

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

1523 

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

1525 """ 

1526 

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

1528 

1529 def __init__(self) -> None: 

1530 """ 

1531 Initialize an empty report. 

1532 """ 

1533 self._dataFiles = [] 

1534 

1535 @readonly 

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

1537 """ 

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

1539 

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

1541 """ 

1542 return self._dataFiles 

1543 

1544 

1545@export 

1546class Document(cc_Document, Coverage): 

1547 """ 

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

1549 """ 

1550 

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

1552 

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

1554 """ 

1555 Initialize the report, and optionally read it. 

1556 

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

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

1559 """ 

1560 super().__init__(jsonReportFile) 

1561 Coverage.__init__(self) 

1562 

1563 self._jsonDocuments = None 

1564 

1565 if analyzeAndConvert: 

1566 self.Analyze() 

1567 self.Convert() 

1568 

1569 def Analyze(self) -> None: 

1570 """ 

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

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

1573 

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

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

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

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

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

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

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

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

1582 """ 

1583 if not self._path.exists(): 

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

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

1586 

1587 with Stopwatch() as sw: 

1588 try: 

1589 content = self._path.read_bytes() 

1590 except OSError as ex: 

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

1592 

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

1594 try: 

1595 content = decompress(content) 

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

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

1598 

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

1600 jsonDocuments = [] 

1601 try: 

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

1603 decoder = JSONDecoder() 

1604 nonWhitespace = re_compile(r"\S") 

1605 position = 0 

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

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

1608 jsonDocuments.append(jsonDocument) 

1609 except (UnicodeDecodeError, JSONDecodeError) as ex: 

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

1611 

1612 if len(jsonDocuments) == 0: 

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

1614 

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

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

1617 for index, jsonDocument in enumerate(jsonDocuments): 

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

1619 

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

1621 try: 

1622 formatVersion = FormatVersion.Parse(version) 

1623 except (ValueError, TypeError) as cause: 

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

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

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

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

1628 raise ex from cause 

1629 

1630 schemaFile = SCHEMAS[formatVersion] 

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

1632 try: 

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

1634 except (ToolingException, JSONDecodeError) as ex: 

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

1636 

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

1638 

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

1640 if len(errors) > 0: 

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

1642 for error in errors: 

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

1644 raise ex 

1645 

1646 self._jsonDocuments = jsonDocuments 

1647 

1648 self._analysisDuration = sw.Duration 

1649 

1650 def Convert(self) -> None: 

1651 """ 

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

1653 

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

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

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

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

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

1659 """ 

1660 if self._jsonDocuments is None: 

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

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

1663 raise ex 

1664 

1665 with Stopwatch() as sw: 

1666 for jsonDocument in self._jsonDocuments: 

1667 DataFile.Parse(jsonDocument, parent=self) 

1668 

1669 self._conversionDuration = sw.Duration 

1670 

1671 def ToCoverageSummary(self) -> CoverageSummary: 

1672 """ 

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

1674 

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

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

1677 

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

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

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

1681 """ 

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

1683 directories = { 

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

1685 } 

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

1687 

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

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

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

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

1692 for dataFile in self._dataFiles: 

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

1694 commonFile = summary.GetOrAddFile(file._path) 

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

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

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

1698 

1699 for line in file._lines: 

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

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

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

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

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

1705 

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

1707 startLine, endLine, count = functions.get( 

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

1709 ) 

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

1711 

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

1713 for number in sorted(counts): 

1714 lineBranches = branchCounts[commonFile][number] 

1715 if counts[number] == 0: 

1716 status = LineCoverageStatus.Uncovered 

1717 elif 0 in lineBranches.values(): 

1718 status = LineCoverageStatus.PartiallyCovered 

1719 else: 

1720 status = LineCoverageStatus.Covered 

1721 

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

1723 for count in lineBranches.values(): 

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

1725 

1726 lines = commonFile._lines 

1727 lastLineNumber = commonFile._lastLineNumber 

1728 sourceFile = cc_SourceFile( 

1729 commonFile.Path.as_posix(), 

1730 file=commonFile, 

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

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

1733 parent=summary 

1734 ) 

1735 

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

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

1738 cc_Function( 

1739 functionName, 

1740 file=commonFile, 

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

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

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

1744 coverageCount=count, 

1745 parent=sourceFile 

1746 ) 

1747 

1748 summary.Aggregate() 

1749 return summary