Coverage for pyEDAA/Reports/CodeCoverage/LLVM.py: 83%

1033 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# 

32""" 

33LLVM's JSON code coverage export: a model of the format, read from a report and converted to the common model. 

34 

35``llvm-cov export -format=text`` writes the format - type ``llvm.coverage.json.export`` - from the source-based code 

36coverage of Clang, and of other compilers based on LLVM, e.g. rustc or Swift. A report is validated against the JSON 

37Schema :file:`LLVM-Coverage-JSON.schema.json`, reverse-engineered from llvm-cov, which accepts the format versions 

382.0.0 to 3.1.0. The format's model keeps what the report states: a :class:`Document` holds :class:`File` records - each 

39with its segments, branch regions, MC/DC records, expansions and summary -, and :class:`Function` records - each with 

40its regions, branch regions and MC/DC records. 

41 

42* A report states a segment (:class:`Segment`) as a JSON array - a tuple of numbers and flags -, a file, a function or 

43 an expansion (:class:`Expansion`) as a JSON object. 

44* A report states a source region as a JSON array, a tuple of numbers: a :class:`Region`, a :class:`BranchRegion` or a 

45 :class:`MCDCRecord` with its :class:`TestVector` objects. A region names its source range by line and column, and 

46 the file by an index into the file paths of its function or expansion; :attr:`Base.FilePath` and :attr:`Base.File` 

47 look the index up. 

48* A summary (:class:`Summary`) holds the counters (:class:`Counters`) llvm-cov computed for a file or the whole report. 

49 

50Each class of the format's model takes typed values in its constructor, so a model can be built by hand, too. A 

51classmethod ``Parse`` reads the class' JSON element and calls the constructor; :meth:`Document.Convert` parses a 

52report's files, functions and totals with it. Every element (:class:`Element`) knows the element containing it and the 

53report's root. 

54 

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

56 

57* A file's path is relative to the directory common to all files, which becomes the report's source directory. 

58* A line's count is derived from the file's segments, as ``llvm-cov show`` derives it: the line is executable, if a 

59 region with a count starts on it, or a region with a count continues from a previous line - unless a skipped region 

60 starts the line. Its count is the largest count of the regions starting on it - gap regions excluded - and of the 

61 region continuing from a previous line. 

62* A branch region becomes two branches - its true and its false outcome - of the line it starts at, or of the line its 

63 macro is expanded at. The branch regions of a function's instantiations, e.g. of a template, are summed. A line with 

64 an outcome never taken is partially covered. 

65* A file becomes a source file unit, and each function a function of its file's unit, named as the report names it - 

66 without the file name of its translation unit, which prefixes a function local to it, e.g. ``Statistics.c:Square``. 

67 The format doesn't demangle C++ names. 

68 

69.. rubric:: Example 

70 

71.. code-block:: Python 

72 

73 from pathlib import Path 

74 from pyEDAA.Reports.CodeCoverage.LLVM import Document 

75 

76 report = Document(Path("coverage.json"), analyzeAndConvert=True) 

77 summary = report.ToCoverageSummary() 

78 print(f"{summary.FileCount} files: {summary.LineCoverage:.1%} of lines, {summary.BranchCoverage:.1%} of branches") 

79""" 

80from __future__ import annotations 

81 

82from enum import Enum 

83from json import JSONDecodeError, loads 

84from pathlib import Path 

85from re import match 

86from typing import Any, ClassVar, Generic, Iterable, Optional as Nullable, Self, TypeVar 

87 

88from pyTooling.Common import getFullyQualifiedName, readResourceFile 

89from pyTooling.Decorators import export, readonly 

90from pyTooling.Exceptions import ToolingException 

91from pyTooling.MetaClasses import ExtendedType 

92from pyTooling.Stopwatch import Stopwatch 

93from pyTooling.Versioning import SemanticVersion 

94 

95from pyEDAA.Reports import Resources 

96from pyEDAA.Reports.CodeCoverage import AlreadyInCoverageReportError, Branch as cc_Branch, CodeCoverageError, \ 

97 CoverageSummary, DuplicateFileError, Document as cc_Document, File as cc_File, \ 

98 Function as cc_Function, Line as cc_Line, LineCoverageStatus, \ 

99 SourceFile as cc_SourceFile 

100 

101 

102__all__ = ["SCHEMA"] 

103 

104SCHEMA = "LLVM-Coverage-JSON.schema.json" #: The JSON Schema a report is validated against. 

105 

106# A class with a property named like a class - ``Path``, ``Summary`` - can't name that class in the annotation of a 

107# field: the class body's namespace, where annotations are evaluated, binds the name to the property. 

108_Path = Path 

109 

110ParentType = TypeVar("ParentType") 

111 

112 

113@export 

114class RegionKind(Enum): 

115 """The kind of a source region, as LLVM's coverage mapping numbers it.""" 

116 

117 Code = 0 #: Code associated with a counter. 

118 Expansion = 1 #: The source range of a macro expansion, which maps to a file of its own. 

119 Skipped = 2 #: Code skipped by the preprocessor - e.g. by ``#if 0`` -, empty lines and comments. 

120 Gap = 3 #: Code between two regions, whose count is a line's count only, if no other region starts on it. 

121 Branch = 4 #: A condition with the counts of its true and false outcome. 

122 MCDCDecision = 5 #: A decision of conditions, measured for MC/DC. 

123 MCDCBranch = 6 #: A condition of a decision measured for MC/DC, with the counts of its true and false outcome. 

124 

125 

126@export 

127class Element(Generic[ParentType], metaclass=ExtendedType, slots=True): 

128 """ 

129 Base-class of every element of the format's model: a reference to the element containing it, and to the report's 

130 root. 

131 

132 A container connects the elements given to its constructor; an element created with a ``parent`` is added to the 

133 parent's collection. When a file or function joins a report, the report becomes the root of every element below it. 

134 """ 

135 

136 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; assigned below the classes. 

137 

138 _parent: Nullable[ParentType] #: The element containing this one, or ``None``. 

139 _root: Nullable[Report] #: The report's root, or ``None`` while the element isn't part of a report. 

140 

141 def __init__(self, *, parent: Nullable[ParentType] = None) -> None: 

142 """ 

143 Initialize an element with its parent and the parent's root. 

144 

145 The parent doesn't list the element yet: the class adds it to the parent's collection, after setting its fields. 

146 

147 :param parent: Optional, the element containing this one. Default: ``None``. 

148 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`. 

149 """ 

150 if parent is None: 

151 self._parent = None 

152 self._root = None 

153 elif not isinstance(parent, self._PARENT_TYPE): 

154 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE) 

155 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.") 

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

157 raise ex 

158 else: 

159 self._parent = parent 

160 self._root = parent._root 

161 

162 @readonly 

163 def Parent(self) -> Nullable[ParentType]: 

164 """ 

165 Read-only property to access the element containing this one (:attr:`_parent`). 

166 

167 :returns: The parent, or ``None`` if the element belongs to none. 

168 """ 

169 return self._parent 

170 

171 @readonly 

172 def Root(self) -> Nullable[Report]: 

173 """ 

174 Read-only property to access the report's root (:attr:`_root`). 

175 

176 :returns: The report, or ``None`` while the element isn't part of a report. 

177 """ 

178 return self._root 

179 

180 def _SetRoot(self, root: Nullable[Report]) -> None: 

181 """ 

182 Set the report's root of the element. 

183 

184 A class containing elements overrides it, to set the root of those too. 

185 

186 :param root: The report's root, or ``None`` while the element isn't part of a report. 

187 """ 

188 self._root = root 

189 

190 

191@export 

192class Base(Element[ParentType]): 

193 """ 

194 Base-class of the source regions: a source range, the file it is in, the file it expands to, and its kind. 

195 """ 

196 

197 _lineStart: int #: Line the region starts at. 

198 _columnStart: int #: Column the region starts at. 

199 _lineEnd: int #: Line the region ends at. 

200 _columnEnd: int #: Column the region ends at. 

201 _fileID: Nullable[int] #: Index of the region's file in the file paths of its function or expansion. 

202 _expandedFileID: int #: Index of the file an expansion region expands to. 

203 _kind: RegionKind #: The kind of the region. 

204 

205 def __init__( 

206 self, 

207 lineStart: int, 

208 columnStart: int, 

209 lineEnd: int, 

210 columnEnd: int, 

211 fileID: Nullable[int], 

212 expandedFileID: int, 

213 kind: RegionKind, 

214 *, 

215 parent: Nullable[ParentType] = None 

216 ) -> None: 

217 """ 

218 Initialize the source range, the region's files and its kind. 

219 

220 :param lineStart: Line the region starts at. 

221 :param columnStart: Column the region starts at. 

222 :param lineEnd: Line the region ends at. 

223 :param columnEnd: Column the region ends at. 

224 :param fileID: Index of the region's file in the file paths of its function or expansion; ``None``, if not 

225 stated. 

226 :param expandedFileID: Index of the file an expansion region expands to. 

227 :param kind: The kind of the region. 

228 :param parent: Optional, the element containing the region. Default: ``None``. 

229 :raises ValueError: If parameter ``lineStart`` is ``None``. 

230 :raises TypeError: If parameter ``lineStart`` isn't of type :class:`int`. 

231 :raises ValueError: If parameter ``lineStart`` is less than 1. 

232 :raises ValueError: If parameter ``columnStart`` is ``None``. 

233 :raises TypeError: If parameter ``columnStart`` isn't of type :class:`int`. 

234 :raises ValueError: If parameter ``columnStart`` is less than 1. 

235 :raises ValueError: If parameter ``lineEnd`` is ``None``. 

236 :raises TypeError: If parameter ``lineEnd`` isn't of type :class:`int`. 

237 :raises ValueError: If parameter ``lineEnd`` is less than 1. 

238 :raises ValueError: If parameter ``columnEnd`` is ``None``. 

239 :raises TypeError: If parameter ``columnEnd`` isn't of type :class:`int`. 

240 :raises ValueError: If parameter ``columnEnd`` is less than 1. 

241 :raises TypeError: If parameter ``fileID`` isn't of type :class:`int`. 

242 :raises ValueError: If parameter ``fileID`` is negative. 

243 :raises ValueError: If parameter ``fileID`` is no index into the file paths of parameter ``parent``. 

244 :raises ValueError: If parameter ``expandedFileID`` is ``None``. 

245 :raises TypeError: If parameter ``expandedFileID`` isn't of type :class:`int`. 

246 :raises ValueError: If parameter ``expandedFileID`` is negative. 

247 :raises ValueError: If parameter ``expandedFileID`` of an expansion region is no index into the file paths of 

248 parameter ``parent``. 

249 :raises ValueError: If parameter ``kind`` is ``None``. 

250 :raises TypeError: If parameter ``kind`` isn't of type :class:`RegionKind`. 

251 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`. 

252 """ 

253 super().__init__(parent=parent) 

254 

255 for name, position in ( 

256 ("lineStart", lineStart), ("columnStart", columnStart), ("lineEnd", lineEnd), ("columnEnd", columnEnd) 

257 ): 

258 if position is None: 258 ↛ 259line 258 didn't jump to line 259 because the condition on line 258 was never true

259 raise ValueError(f"Parameter '{name}' is None.") 

260 elif not isinstance(position, int): 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true

261 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

262 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.") 

263 raise ex 

264 elif position < 1: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true

265 ex = ValueError(f"Parameter '{name}' is less than 1.") 

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

267 raise ex 

268 

269 if fileID is not None: 

270 if not isinstance(fileID, int): 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true

271 ex = TypeError("Parameter 'fileID' is not of type 'int'.") 

272 ex.add_note(f"Got type '{getFullyQualifiedName(fileID)}'.") 

273 raise ex 

274 elif fileID < 0: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true

275 ex = ValueError("Parameter 'fileID' is negative.") 

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

277 raise ex 

278 

279 if expandedFileID is None: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true

280 raise ValueError("Parameter 'expandedFileID' is None.") 

281 elif not isinstance(expandedFileID, int): 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true

282 ex = TypeError("Parameter 'expandedFileID' is not of type 'int'.") 

283 ex.add_note(f"Got type '{getFullyQualifiedName(expandedFileID)}'.") 

284 raise ex 

285 elif expandedFileID < 0: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true

286 ex = ValueError("Parameter 'expandedFileID' is negative.") 

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

288 raise ex 

289 

290 if kind is None: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true

291 raise ValueError("Parameter 'kind' is None.") 

292 elif not isinstance(kind, RegionKind): 

293 ex = TypeError("Parameter 'kind' is not of type 'RegionKind'.") 

294 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.") 

295 raise ex 

296 

297 if isinstance(parent, (Function, Expansion)): 

298 for name, value in ( 

299 ("fileID", fileID), ("expandedFileID", expandedFileID if kind is RegionKind.Expansion else None) 

300 ): 

301 if value is not None and value >= len(parent._filePaths): 

302 ex = ValueError(f"Parameter '{name}' is no index into the file paths of parameter 'parent'.") 

303 ex.add_note(f"Got value '{value}' for {len(parent._filePaths)} file paths.") 

304 raise ex 

305 

306 self._lineStart = lineStart 

307 self._columnStart = columnStart 

308 self._lineEnd = lineEnd 

309 self._columnEnd = columnEnd 

310 self._fileID = fileID 

311 self._expandedFileID = expandedFileID 

312 self._kind = kind 

313 

314 @readonly 

315 def LineStart(self) -> int: 

316 """ 

317 Read-only property to access the line the region starts at (:attr:`_lineStart`). 

318 

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

320 """ 

321 return self._lineStart 

322 

323 @readonly 

324 def ColumnStart(self) -> int: 

325 """ 

326 Read-only property to access the column the region starts at (:attr:`_columnStart`). 

327 

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

329 """ 

330 return self._columnStart 

331 

332 @readonly 

333 def LineEnd(self) -> int: 

334 """ 

335 Read-only property to access the line the region ends at (:attr:`_lineEnd`). 

336 

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

338 """ 

339 return self._lineEnd 

340 

341 @readonly 

342 def ColumnEnd(self) -> int: 

343 """ 

344 Read-only property to access the column the region ends at (:attr:`_columnEnd`). 

345 

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

347 """ 

348 return self._columnEnd 

349 

350 @readonly 

351 def FileID(self) -> Nullable[int]: 

352 """ 

353 Read-only property to access the index of the region's file (:attr:`_fileID`). 

354 

355 For a region listed by a file, the index refers to the file paths of a function the report doesn't name; 

356 :attr:`FilePath` is the file's path then. 

357 

358 :returns: The index into the file paths of the region's function or expansion; ``None`` for an MC/DC record before 

359 format version 3.0.1. 

360 """ 

361 return self._fileID 

362 

363 @readonly 

364 def ExpandedFileID(self) -> int: 

365 """ 

366 Read-only property to access the index of the file an expansion region expands to (:attr:`_expandedFileID`). 

367 

368 :returns: The index into the file paths of the region's function or expansion; ``0`` for other kinds of regions. 

369 """ 

370 return self._expandedFileID 

371 

372 @readonly 

373 def Kind(self) -> RegionKind: 

374 """ 

375 Read-only property to access the kind of the region (:attr:`_kind`). 

376 

377 :returns: The kind. 

378 """ 

379 return self._kind 

380 

381 @readonly 

382 def FilePath(self) -> Nullable[Path]: 

383 """ 

384 Read-only property to return the path of the region's file. 

385 

386 A region of a function or expansion names its file by :attr:`FileID`, an index into the parent's file paths. A 

387 region listed by a file lies in that file. 

388 

389 :returns: The path, as the compiler named it; ``None`` if the region belongs to no element, or for an MC/DC record 

390 of a function before format version 3.0.1, which states no file. 

391 """ 

392 if self._parent is None: 

393 return None 

394 elif isinstance(self._parent, File): 

395 return self._parent._path 

396 elif self._fileID is None: 

397 return None 

398 

399 return self._parent._filePaths[self._fileID] 

400 

401 @readonly 

402 def File(self) -> Nullable[File]: 

403 """ 

404 Read-only property to return the region's file, as the report lists it. 

405 

406 :returns: The file of :attr:`FilePath` in the report's files; ``None`` if the region isn't part of a report, or the 

407 report doesn't list the file, e.g. one excluded by ``-ignore-filename-regex``. 

408 """ 

409 if self._root is None or (path := self.FilePath) is None: 

410 return None 

411 

412 return self._root._files.get(path, None) 

413 

414 @readonly 

415 def ExpandedFilePath(self) -> Nullable[Path]: 

416 """ 

417 Read-only property to return the path of the file an expansion region expands to (:attr:`ExpandedFileID`). 

418 

419 :returns: The path, as the compiler named it; ``None`` for another kind of region, or a region belonging to no 

420 function or expansion. 

421 """ 

422 if self._kind is not RegionKind.Expansion or not isinstance(self._parent, (Function, Expansion)): 

423 return None 

424 

425 return self._parent._filePaths[self._expandedFileID] 

426 

427 @readonly 

428 def ExpandedFile(self) -> Nullable[File]: 

429 """ 

430 Read-only property to return the file an expansion region expands to, as the report lists it. 

431 

432 :returns: The file of :attr:`ExpandedFilePath` in the report's files; ``None`` if there is none, the region isn't 

433 part of a report, or the report doesn't list the file. 

434 """ 

435 if self._root is None or (path := self.ExpandedFilePath) is None: 

436 return None 

437 

438 return self._root._files.get(path, None) 

439 

440 

441@export 

442class Region(Base["Function | Expansion"]): 

443 """ 

444 A region of a function or an expansion: a source range with a count. 

445 

446 A region is ``[lineStart, columnStart, lineEnd, columnEnd, count, fileID, expandedFileID, kind]``. 

447 """ 

448 

449 _count: int #: How often the region's code ran. 

450 

451 def __init__( 

452 self, 

453 lineStart: int, 

454 columnStart: int, 

455 lineEnd: int, 

456 columnEnd: int, 

457 count: int, 

458 fileID: int, 

459 expandedFileID: int, 

460 kind: RegionKind, 

461 *, 

462 parent: Nullable[Function | Expansion] = None 

463 ) -> None: 

464 """ 

465 Initialize a region, and add it to the regions of its function or to the target regions of its expansion. 

466 

467 :param lineStart: Line the region starts at. 

468 :param columnStart: Column the region starts at. 

469 :param lineEnd: Line the region ends at. 

470 :param columnEnd: Column the region ends at. 

471 :param count: How often the region's code ran. 

472 :param fileID: Index of the region's file in the file paths of its function or expansion. 

473 :param expandedFileID: Index of the file an expansion region expands to. 

474 :param kind: The kind of the region. 

475 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``. 

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

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

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

479 :raises ValueError: If parameter ``fileID`` is ``None``. 

480 :raises TypeError: If parameter ``parent`` isn't of type :class:`Function` or :class:`Expansion`. 

481 """ 

482 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) 

483 

484 if fileID is None: 

485 raise ValueError("Parameter 'fileID' is None.") 

486 

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

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

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

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

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

492 raise ex 

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

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

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

496 raise ex 

497 

498 self._count = count 

499 

500 if parent is not None: 

501 (parent._regions if isinstance(parent, Function) else parent._targetRegions).append(self) 

502 

503 @classmethod 

504 def Parse(cls, region: list[Any], *, parent: Nullable[Function | Expansion] = None) -> Self: 

505 """ 

506 Parse a region from its JSON array. 

507 

508 :param region: The JSON array of the region. 

509 :param parent: Optional, the function or expansion the region belongs to. Default: ``None``. 

510 :returns: The region. 

511 """ 

512 return cls(*region[:7], RegionKind(region[7]), parent=parent) 

513 

514 @readonly 

515 def Count(self) -> int: 

516 """ 

517 Read-only property to access how often the region's code ran (:attr:`_count`). 

518 

519 :returns: The count. 

520 """ 

521 return self._count 

522 

523 

524@export 

525class BranchRegion(Base["File | Function | Expansion"]): 

526 """ 

527 A branch region: a condition with the counts of its true and false outcome. 

528 

529 A branch region is ``[lineStart, columnStart, lineEnd, columnEnd, trueCount, falseCount, fileID, expandedFileID, 

530 kind]``. 

531 """ 

532 

533 _trueCount: int #: How often the condition was true. 

534 _falseCount: int #: How often the condition was false. 

535 

536 def __init__( 

537 self, 

538 lineStart: int, 

539 columnStart: int, 

540 lineEnd: int, 

541 columnEnd: int, 

542 trueCount: int, 

543 falseCount: int, 

544 fileID: int, 

545 expandedFileID: int, 

546 kind: RegionKind, 

547 *, 

548 parent: Nullable[File | Function | Expansion] = None 

549 ) -> None: 

550 """ 

551 Initialize a branch region, and add it to the branch regions of its file, function or expansion. 

552 

553 :param lineStart: Line the branch region starts at. 

554 :param columnStart: Column the branch region starts at. 

555 :param lineEnd: Line the branch region ends at. 

556 :param columnEnd: Column the branch region ends at. 

557 :param trueCount: How often the condition was true. 

558 :param falseCount: How often the condition was false. 

559 :param fileID: Index of the branch region's file in the file paths of its function or expansion. 

560 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for a branch region. 

561 :param kind: The kind of the region: a branch, or a condition measured for MC/DC. 

562 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``. 

563 :raises ValueError: If parameter ``trueCount`` is ``None``. 

564 :raises TypeError: If parameter ``trueCount`` isn't of type :class:`int`. 

565 :raises ValueError: If parameter ``trueCount`` is negative. 

566 :raises ValueError: If parameter ``falseCount`` is ``None``. 

567 :raises TypeError: If parameter ``falseCount`` isn't of type :class:`int`. 

568 :raises ValueError: If parameter ``falseCount`` is negative. 

569 :raises ValueError: If parameter ``fileID`` is ``None``. 

570 :raises TypeError: If parameter ``parent`` isn't of type :class:`File`, :class:`Function` or 

571 :class:`Expansion`. 

572 """ 

573 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) 

574 

575 if fileID is None: 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true

576 raise ValueError("Parameter 'fileID' is None.") 

577 

578 for name, value in (("trueCount", trueCount), ("falseCount", falseCount)): 

579 if value is None: 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true

580 raise ValueError(f"Parameter '{name}' is None.") 

581 elif not isinstance(value, int): 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true

582 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

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

584 raise ex 

585 elif value < 0: 

586 ex = ValueError(f"Parameter '{name}' is negative.") 

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

588 raise ex 

589 

590 self._trueCount = trueCount 

591 self._falseCount = falseCount 

592 

593 if parent is not None: 

594 parent._branches.append(self) 

595 

596 @classmethod 

597 def Parse(cls, branch: list[Any], *, parent: Nullable[File | Function | Expansion] = None) -> Self: 

598 """ 

599 Parse a branch region from its JSON array. 

600 

601 :param branch: The JSON array of the branch region. 

602 :param parent: Optional, the file, function or expansion the branch region belongs to. Default: ``None``. 

603 :returns: The branch region. 

604 """ 

605 return cls(*branch[:8], RegionKind(branch[8]), parent=parent) 

606 

607 @readonly 

608 def TrueCount(self) -> int: 

609 """ 

610 Read-only property to access how often the condition was true (:attr:`_trueCount`). 

611 

612 :returns: The count. 

613 """ 

614 return self._trueCount 

615 

616 @readonly 

617 def FalseCount(self) -> int: 

618 """ 

619 Read-only property to access how often the condition was false (:attr:`_falseCount`). 

620 

621 :returns: The count. 

622 """ 

623 return self._falseCount 

624 

625 

626@export 

627class TestVector(Element["MCDCRecord"]): 

628 """ 

629 A test vector of an MC/DC decision - in format version 3.1.0 -: the values of its conditions, and the outcome. 

630 """ 

631 

632 _conditions: list[Nullable[bool]] #: The values of the conditions; ``None`` for a condition not evaluated. 

633 _executed: bool #: Whether the test vector was executed. 

634 _result: Nullable[bool] #: The outcome of the decision. 

635 

636 def __init__( 

637 self, 

638 conditions: Iterable[Nullable[bool]], 

639 executed: bool, 

640 result: Nullable[bool], 

641 *, 

642 parent: Nullable[MCDCRecord] = None 

643 ) -> None: 

644 """ 

645 Initialize a test vector, and add it to the test vectors of its MC/DC record. 

646 

647 :param conditions: The values of the conditions; ``None`` for a condition not evaluated. 

648 :param executed: Whether the test vector was executed. 

649 :param result: The outcome of the decision. 

650 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``. 

651 :raises ValueError: If parameter ``conditions`` is ``None``. 

652 :raises TypeError: If parameter ``conditions`` isn't iterable. 

653 :raises TypeError: If parameter ``conditions`` contains an element not of type :class:`bool`, other than ``None``. 

654 :raises ValueError: If parameter ``executed`` is ``None``. 

655 :raises TypeError: If parameter ``executed`` isn't of type :class:`bool`. 

656 :raises TypeError: If parameter ``result`` isn't of type :class:`bool`. 

657 :raises TypeError: If parameter ``parent`` isn't of type :class:`MCDCRecord`. 

658 """ 

659 super().__init__(parent=parent) 

660 

661 if conditions is None: 661 ↛ 662line 661 didn't jump to line 662 because the condition on line 661 was never true

662 raise ValueError("Parameter 'conditions' is None.") 

663 elif not isinstance(conditions, Iterable): 663 ↛ 664line 663 didn't jump to line 664 because the condition on line 663 was never true

664 ex = TypeError("Parameter 'conditions' is not iterable.") 

665 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.") 

666 raise ex 

667 

668 if executed is None: 668 ↛ 669line 668 didn't jump to line 669 because the condition on line 668 was never true

669 raise ValueError("Parameter 'executed' is None.") 

670 elif not isinstance(executed, bool): 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true

671 ex = TypeError("Parameter 'executed' is not of type 'bool'.") 

672 ex.add_note(f"Got type '{getFullyQualifiedName(executed)}'.") 

673 raise ex 

674 

675 if result is not None and not isinstance(result, bool): 

676 ex = TypeError("Parameter 'result' is not of type 'bool'.") 

677 ex.add_note(f"Got type '{getFullyQualifiedName(result)}'.") 

678 raise ex 

679 

680 self._conditions = conditionList = [] 

681 self._executed = executed 

682 self._result = result 

683 

684 for condition in conditions: 

685 if condition is not None and not isinstance(condition, bool): 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true

686 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.") 

687 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.") 

688 raise ex 

689 

690 conditionList.append(condition) 

691 

692 if parent is not None: 

693 parent._testVectors.append(self) 

694 

695 @classmethod 

696 def Parse(cls, testVector: dict[str, Any], *, parent: Nullable[MCDCRecord] = None) -> Self: 

697 """ 

698 Parse a test vector from its JSON object. 

699 

700 :param testVector: The JSON object of the test vector. 

701 :param parent: Optional, the MC/DC record the test vector belongs to. Default: ``None``. 

702 :returns: The test vector. 

703 """ 

704 return cls(testVector["conditions"], testVector["executed"], testVector["result"], parent=parent) 

705 

706 @readonly 

707 def Conditions(self) -> list[Nullable[bool]]: 

708 """ 

709 Read-only property to access the values of the conditions (:attr:`_conditions`). 

710 

711 :returns: The values, by condition; ``None`` for a condition not evaluated. 

712 """ 

713 return self._conditions 

714 

715 @readonly 

716 def Executed(self) -> bool: 

717 """ 

718 Read-only property to access whether the test vector was executed (:attr:`_executed`). 

719 

720 :returns: ``True`` for an executed test vector; ``False`` for a missing one, listed by 

721 ``-show-mcdc-non-executed-vectors``. 

722 """ 

723 return self._executed 

724 

725 @readonly 

726 def Result(self) -> Nullable[bool]: 

727 """ 

728 Read-only property to access the outcome of the decision (:attr:`_result`). 

729 

730 :returns: The outcome. 

731 """ 

732 return self._result 

733 

734 

735@export 

736class MCDCRecord(Base["File | Function"]): 

737 """ 

738 An MC/DC record: a decision of conditions, and for each condition, whether a pair of test vectors showed its 

739 independent effect on the outcome. 

740 

741 The record's array grew with the format version: ``[lineStart, columnStart, lineEnd, columnEnd, expandedFileID, kind, 

742 conditions]`` in 2.0.1; version 3.0.0 added the numbers of true and false decisions after the source range, version 

743 3.0.1 the ``fileID`` before ``expandedFileID``, and version 3.1.0 the test vectors at the end. 

744 """ 

745 

746 _trueDecisions: Nullable[int] #: Number of executed test vectors with the outcome true, if stated. 

747 _falseDecisions: Nullable[int] #: Number of executed test vectors with the outcome false, if stated. 

748 _conditions: list[bool] #: Whether a pair of test vectors showed the independent effect, by condition. 

749 _testVectors: list[TestVector] #: The test vectors; empty before format version 3.1.0. 

750 

751 def __init__( 

752 self, 

753 lineStart: int, 

754 columnStart: int, 

755 lineEnd: int, 

756 columnEnd: int, 

757 fileID: Nullable[int], 

758 expandedFileID: int, 

759 kind: RegionKind, 

760 conditions: Iterable[bool], 

761 trueDecisions: Nullable[int] = None, 

762 falseDecisions: Nullable[int] = None, 

763 testVectors: Nullable[Iterable[TestVector]] = None, 

764 *, 

765 parent: Nullable[File | Function] = None 

766 ) -> None: 

767 """ 

768 Initialize an MC/DC record, and add it to the MC/DC records of its file or function. 

769 

770 :param lineStart: Line the decision starts at. 

771 :param columnStart: Column the decision starts at. 

772 :param lineEnd: Line the decision ends at. 

773 :param columnEnd: Column the decision ends at. 

774 :param fileID: Index of the decision's file in the file paths of its function or 

775 expansion; ``None``, if not stated. 

776 :param expandedFileID: Index of the file an expansion region expands to; ``0`` for an MC/DC 

777 record. 

778 :param kind: The kind of the region: an MC/DC decision. 

779 :param conditions: Whether a pair of test vectors showed the independent effect, by 

780 condition. 

781 :param trueDecisions: Optional, number of executed test vectors with the outcome true. 

782 Default: ``None``. 

783 :param falseDecisions: Optional, number of executed test vectors with the outcome false. 

784 Default: ``None``. 

785 :param testVectors: Optional, the test vectors. Default: none. 

786 :param parent: Optional, the file or function the MC/DC record belongs to. Default: 

787 ``None``. 

788 :raises ValueError: If parameter ``conditions`` is ``None``. 

789 :raises TypeError: If parameter ``conditions`` isn't iterable. 

790 :raises TypeError: If parameter ``conditions`` contains an element not of type 

791 :class:`bool`. 

792 :raises TypeError: If parameter ``trueDecisions`` isn't of type :class:`int`. 

793 :raises ValueError: If parameter ``trueDecisions`` is negative. 

794 :raises TypeError: If parameter ``falseDecisions`` isn't of type :class:`int`. 

795 :raises ValueError: If parameter ``falseDecisions`` is negative. 

796 :raises TypeError: If parameter ``testVectors`` isn't iterable. 

797 :raises TypeError: If parameter ``testVectors`` contains an element not of type 

798 :class:`TestVector`. 

799 :raises AlreadyInCoverageReportError: If parameter ``testVectors`` contains a test vector of another MC/DC 

800 record. 

801 :raises TypeError: If parameter ``parent`` isn't of type :class:`File` or 

802 :class:`Function`. 

803 """ 

804 super().__init__(lineStart, columnStart, lineEnd, columnEnd, fileID, expandedFileID, kind, parent=parent) 

805 

806 for name, value in (("trueDecisions", trueDecisions), ("falseDecisions", falseDecisions)): 

807 if value is None: 

808 continue 

809 elif not isinstance(value, int): 809 ↛ 810line 809 didn't jump to line 810 because the condition on line 809 was never true

810 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

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

812 raise ex 

813 elif value < 0: 813 ↛ 814line 813 didn't jump to line 814 because the condition on line 813 was never true

814 ex = ValueError(f"Parameter '{name}' is negative.") 

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

816 raise ex 

817 

818 if conditions is None: 

819 raise ValueError("Parameter 'conditions' is None.") 

820 elif not isinstance(conditions, Iterable): 820 ↛ 821line 820 didn't jump to line 821 because the condition on line 820 was never true

821 ex = TypeError("Parameter 'conditions' is not iterable.") 

822 ex.add_note(f"Got type '{getFullyQualifiedName(conditions)}'.") 

823 raise ex 

824 

825 self._trueDecisions = trueDecisions 

826 self._falseDecisions = falseDecisions 

827 self._conditions = conditionList = [] 

828 self._testVectors = testVectorList = [] 

829 

830 for condition in conditions: 

831 if not isinstance(condition, bool): 

832 ex = TypeError("Parameter 'conditions' contains an element not of type 'bool'.") 

833 ex.add_note(f"Got type '{getFullyQualifiedName(condition)}'.") 

834 raise ex 

835 

836 conditionList.append(condition) 

837 

838 if testVectors is not None: 

839 if not isinstance(testVectors, Iterable): 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true

840 ex = TypeError("Parameter 'testVectors' is not iterable.") 

841 ex.add_note(f"Got type '{getFullyQualifiedName(testVectors)}'.") 

842 raise ex 

843 

844 for testVector in testVectors: 

845 if not isinstance(testVector, TestVector): 845 ↛ 846line 845 didn't jump to line 846 because the condition on line 845 was never true

846 ex = TypeError("Parameter 'testVectors' contains an element not of type 'TestVector'.") 

847 ex.add_note(f"Got type '{getFullyQualifiedName(testVector)}'.") 

848 raise ex 

849 elif testVector._parent is not None: 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true

850 raise AlreadyInCoverageReportError("Parameter 'testVectors' contains a test vector of another MC/DC record.") 

851 

852 testVector._parent = self 

853 testVector._root = self._root 

854 testVectorList.append(testVector) 

855 

856 if parent is not None: 

857 parent._mcdcRecords.append(self) 

858 

859 @classmethod 

860 def Parse(cls, record: list[Any], *, parent: Nullable[File | Function] = None) -> Self: 

861 """ 

862 Parse an MC/DC record from its JSON array, in the shape of its format version. 

863 

864 :param record: The JSON array of the MC/DC record. 

865 :param parent: Optional, the file or function the MC/DC record belongs to. Default: ``None``. 

866 :returns: The MC/DC record. 

867 """ 

868 if len(record) == 7: 

869 return cls(*record[:4], None, record[4], RegionKind(record[5]), record[6], parent=parent) 

870 elif len(record) == 9: 

871 return cls(*record[:4], None, record[6], RegionKind(record[7]), record[8], record[4], record[5], parent=parent) 

872 

873 return cls( 

874 *record[:4], record[6], record[7], RegionKind(record[8]), record[9], record[4], record[5], 

875 [TestVector.Parse(testVector) for testVector in record[10]] if len(record) > 10 else None, parent=parent 

876 ) 

877 

878 @readonly 

879 def TrueDecisions(self) -> Nullable[int]: 

880 """ 

881 Read-only property to access the number of executed test vectors with the outcome true (:attr:`_trueDecisions`). 

882 

883 :returns: The number; ``None`` before format version 3.0.0. 

884 """ 

885 return self._trueDecisions 

886 

887 @readonly 

888 def FalseDecisions(self) -> Nullable[int]: 

889 """ 

890 Read-only property to access the number of executed test vectors with the outcome false (:attr:`_falseDecisions`). 

891 

892 :returns: The number; ``None`` before format version 3.0.0. 

893 """ 

894 return self._falseDecisions 

895 

896 @readonly 

897 def Conditions(self) -> list[bool]: 

898 """ 

899 Read-only property to access, by condition, whether a pair of test vectors showed its independent effect 

900 (:attr:`_conditions`). 

901 

902 :returns: ``True`` for each condition shown to affect the outcome on its own. 

903 """ 

904 return self._conditions 

905 

906 @readonly 

907 def TestVectors(self) -> list[TestVector]: 

908 """ 

909 Read-only property to access the test vectors (:attr:`_testVectors`). 

910 

911 :returns: The test vectors; empty before format version 3.1.0. 

912 """ 

913 return self._testVectors 

914 

915 def _SetRoot(self, root: Nullable[Report]) -> None: 

916 """ 

917 Set the report's root of the MC/DC record, and of its test vectors. 

918 

919 :param root: The report's root, or ``None`` while the element isn't part of a report. 

920 """ 

921 self._root = root 

922 for testVector in self._testVectors: 

923 testVector._SetRoot(root) 

924 

925 

926@export 

927class Counters(Element["Summary"]): 

928 """ 

929 The counters of one kind of a summary: how many there are, and how many are covered. 

930 

931 A summary counts lines, functions, regions, branches and MC/DC conditions. The summary given the counters becomes 

932 their parent. 

933 """ 

934 

935 _count: int #: Number of lines, functions, regions, branches or MC/DC conditions. 

936 _covered: int #: Number of the covered ones. 

937 _notCovered: Nullable[int] #: Number of the uncovered ones, if stated. 

938 _percent: float #: The coverage in percent. 

939 

940 def __init__(self, count: int, covered: int, percent: float, notCovered: Nullable[int] = None) -> None: 

941 """ 

942 Initialize counters. 

943 

944 :param count: Number of lines, functions, regions, branches or MC/DC conditions. 

945 :param covered: Number of the covered ones. 

946 :param percent: The coverage in percent. 

947 :param notCovered: Optional, number of the uncovered ones. Default: ``None``. 

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

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

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

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

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

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

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

955 :raises TypeError: If parameter ``percent`` isn't of type :class:`float` or :class:`int`. 

956 :raises ValueError: If parameter ``percent`` is out of range 0..100. 

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

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

959 """ 

960 super().__init__() 

961 

962 for name, value in (("count", count), ("covered", covered)): 

963 if value is None: 963 ↛ 964line 963 didn't jump to line 964 because the condition on line 963 was never true

964 raise ValueError(f"Parameter '{name}' is None.") 

965 elif not isinstance(value, int): 965 ↛ 966line 965 didn't jump to line 966 because the condition on line 965 was never true

966 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

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

968 raise ex 

969 elif value < 0: 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true

970 ex = ValueError(f"Parameter '{name}' is negative.") 

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

972 raise ex 

973 

974 if notCovered is not None: 

975 if not isinstance(notCovered, int): 975 ↛ 976line 975 didn't jump to line 976 because the condition on line 975 was never true

976 ex = TypeError("Parameter 'notCovered' is not of type 'int'.") 

977 ex.add_note(f"Got type '{getFullyQualifiedName(notCovered)}'.") 

978 raise ex 

979 elif notCovered < 0: 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true

980 ex = ValueError("Parameter 'notCovered' is negative.") 

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

982 raise ex 

983 

984 if percent is None: 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true

985 raise ValueError("Parameter 'percent' is None.") 

986 elif not isinstance(percent, (float, int)): 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true

987 ex = TypeError("Parameter 'percent' is not of type 'float'.") 

988 ex.add_note(f"Got type '{getFullyQualifiedName(percent)}'.") 

989 raise ex 

990 elif not 0 <= percent <= 100: 

991 ex = ValueError("Parameter 'percent' is out of range 0..100.") 

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

993 raise ex 

994 

995 self._count = count 

996 self._covered = covered 

997 self._notCovered = notCovered 

998 self._percent = percent 

999 

1000 @classmethod 

1001 def Parse(cls, counters: dict[str, Any]) -> Self: 

1002 """ 

1003 Parse counters from their JSON object. 

1004 

1005 :param counters: The JSON object of the counters. 

1006 :returns: The counters. 

1007 """ 

1008 return cls(counters["count"], counters["covered"], counters["percent"], counters.get("notcovered")) 

1009 

1010 @readonly 

1011 def Count(self) -> int: 

1012 """ 

1013 Read-only property to access the number of lines, functions, regions, branches or MC/DC conditions (:attr:`_count`). 

1014 

1015 :returns: The number. 

1016 """ 

1017 return self._count 

1018 

1019 @readonly 

1020 def Covered(self) -> int: 

1021 """ 

1022 Read-only property to access the number of covered ones (:attr:`_covered`). 

1023 

1024 :returns: The number. 

1025 """ 

1026 return self._covered 

1027 

1028 @readonly 

1029 def NotCovered(self) -> Nullable[int]: 

1030 """ 

1031 Read-only property to access the number of uncovered ones (:attr:`_notCovered`). 

1032 

1033 :returns: The number, or ``None`` for lines, functions and instantiations, which don't state it. 

1034 """ 

1035 return self._notCovered 

1036 

1037 @readonly 

1038 def Percent(self) -> float: 

1039 """ 

1040 Read-only property to access the coverage (:attr:`_percent`). 

1041 

1042 :returns: The coverage in percent. 

1043 """ 

1044 return self._percent 

1045 

1046 

1047@export 

1048class Summary(Element["File | Report"]): 

1049 """ 

1050 A ``summary``: the counters llvm-cov computed for a file or the whole report. 

1051 

1052 llvm-cov counts a file's lines, regions and branches per function, and merges a function's instantiations by taking 

1053 the best one. The file or report given the summary becomes its parent. 

1054 """ 

1055 

1056 _lines: Counters #: The counters of lines. 

1057 _functions: Counters #: The counters of functions, a function's instantiations counted once. 

1058 _instantiations: Counters #: The counters of function instantiations. 

1059 _regions: Counters #: The counters of code regions. 

1060 _branches: Nullable[Counters] #: The counters of branch outcomes, if stated. 

1061 _mcdc: Nullable[Counters] #: The counters of MC/DC conditions, if stated. 

1062 

1063 def __init__( 

1064 self, 

1065 lines: Counters, 

1066 functions: Counters, 

1067 instantiations: Counters, 

1068 regions: Counters, 

1069 branches: Nullable[Counters] = None, 

1070 mcdc: Nullable[Counters] = None 

1071 ) -> None: 

1072 """ 

1073 Initialize a summary. 

1074 

1075 :param lines: The counters of lines. 

1076 :param functions: The counters of functions, a function's instantiations counted once. 

1077 :param instantiations: The counters of function instantiations. 

1078 :param regions: The counters of code regions. 

1079 :param branches: Optional, the counters of branch outcomes. Default: ``None``. 

1080 :param mcdc: Optional, the counters of MC/DC conditions. Default: ``None``. 

1081 :raises ValueError: If parameter ``lines`` is ``None``. 

1082 :raises TypeError: If parameter ``lines`` isn't of type :class:`Counters`. 

1083 :raises AlreadyInCoverageReportError: If parameter ``lines`` are the counters of another summary. 

1084 :raises ValueError: If parameter ``functions`` is ``None``. 

1085 :raises TypeError: If parameter ``functions`` isn't of type :class:`Counters`. 

1086 :raises AlreadyInCoverageReportError: If parameter ``functions`` are the counters of another summary. 

1087 :raises ValueError: If parameter ``instantiations`` is ``None``. 

1088 :raises TypeError: If parameter ``instantiations`` isn't of type :class:`Counters`. 

1089 :raises AlreadyInCoverageReportError: If parameter ``instantiations`` are the counters of another summary. 

1090 :raises ValueError: If parameter ``regions`` is ``None``. 

1091 :raises TypeError: If parameter ``regions`` isn't of type :class:`Counters`. 

1092 :raises AlreadyInCoverageReportError: If parameter ``regions`` are the counters of another summary. 

1093 :raises TypeError: If parameter ``branches`` isn't of type :class:`Counters`. 

1094 :raises AlreadyInCoverageReportError: If parameter ``branches`` are the counters of another summary. 

1095 :raises TypeError: If parameter ``mcdc`` isn't of type :class:`Counters`. 

1096 :raises AlreadyInCoverageReportError: If parameter ``mcdc`` are the counters of another summary. 

1097 """ 

1098 super().__init__() 

1099 

1100 for name, counters in ( 

1101 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions) 

1102 ): 

1103 if counters is None: 

1104 raise ValueError(f"Parameter '{name}' is None.") 

1105 elif not isinstance(counters, Counters): 1105 ↛ 1106line 1105 didn't jump to line 1106 because the condition on line 1105 was never true

1106 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.") 

1107 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.") 

1108 raise ex 

1109 

1110 for name, counters in (("branches", branches), ("mcdc", mcdc)): 

1111 if counters is not None and not isinstance(counters, Counters): 

1112 ex = TypeError(f"Parameter '{name}' is not of type 'Counters'.") 

1113 ex.add_note(f"Got type '{getFullyQualifiedName(counters)}'.") 

1114 raise ex 

1115 

1116 for name, counters in ( 

1117 ("lines", lines), ("functions", functions), ("instantiations", instantiations), ("regions", regions), 

1118 ("branches", branches), ("mcdc", mcdc) 

1119 ): 

1120 if counters is None: 

1121 continue 

1122 elif counters._parent is not None: 1122 ↛ 1123line 1122 didn't jump to line 1123 because the condition on line 1122 was never true

1123 raise AlreadyInCoverageReportError(f"Parameter '{name}' are the counters of another summary.") 

1124 

1125 counters._parent = self 

1126 

1127 self._lines = lines 

1128 self._functions = functions 

1129 self._instantiations = instantiations 

1130 self._regions = regions 

1131 self._branches = branches 

1132 self._mcdc = mcdc 

1133 

1134 @classmethod 

1135 def Parse(cls, summary: dict[str, Any]) -> Self: 

1136 """ 

1137 Parse a summary from its JSON object. 

1138 

1139 :param summary: The JSON object ``summary`` or ``totals``. 

1140 :returns: The summary. 

1141 """ 

1142 return cls( 

1143 Counters.Parse(summary["lines"]), 

1144 Counters.Parse(summary["functions"]), 

1145 Counters.Parse(summary["instantiations"]), 

1146 Counters.Parse(summary["regions"]), 

1147 Counters.Parse(summary["branches"]) if "branches" in summary else None, 

1148 Counters.Parse(summary["mcdc"]) if "mcdc" in summary else None 

1149 ) 

1150 

1151 @readonly 

1152 def Lines(self) -> Counters: 

1153 """ 

1154 Read-only property to access the counters of lines (:attr:`_lines`). 

1155 

1156 :returns: The counters. 

1157 """ 

1158 return self._lines 

1159 

1160 @readonly 

1161 def Functions(self) -> Counters: 

1162 """ 

1163 Read-only property to access the counters of functions, a function's instantiations counted once 

1164 (:attr:`_functions`). 

1165 

1166 :returns: The counters. 

1167 """ 

1168 return self._functions 

1169 

1170 @readonly 

1171 def Instantiations(self) -> Counters: 

1172 """ 

1173 Read-only property to access the counters of function instantiations (:attr:`_instantiations`). 

1174 

1175 :returns: The counters. 

1176 """ 

1177 return self._instantiations 

1178 

1179 @readonly 

1180 def Regions(self) -> Counters: 

1181 """ 

1182 Read-only property to access the counters of code regions (:attr:`_regions`). 

1183 

1184 :returns: The counters. 

1185 """ 

1186 return self._regions 

1187 

1188 @readonly 

1189 def Branches(self) -> Nullable[Counters]: 

1190 """ 

1191 Read-only property to access the counters of branch outcomes - two per branch region - (:attr:`_branches`). 

1192 

1193 :returns: The counters, or ``None`` before LLVM 12. 

1194 """ 

1195 return self._branches 

1196 

1197 @readonly 

1198 def MCDC(self) -> Nullable[Counters]: 

1199 """ 

1200 Read-only property to access the counters of MC/DC conditions (:attr:`_mcdc`). 

1201 

1202 :returns: The counters, or ``None`` before LLVM 18. 

1203 """ 

1204 return self._mcdc 

1205 

1206 def _SetRoot(self, root: Nullable[Report]) -> None: 

1207 """ 

1208 Set the report's root of the summary, and of its counters. 

1209 

1210 :param root: The report's root, or ``None`` while the element isn't part of a report. 

1211 """ 

1212 self._root = root 

1213 for counters in (self._lines, self._functions, self._instantiations, self._regions, self._branches, self._mcdc): 

1214 if counters is not None: 

1215 counters._SetRoot(root) 

1216 

1217 

1218_Summary = Summary 

1219 

1220 

1221@export 

1222class Segment(Element["File"]): 

1223 """ 

1224 A segment of a file: from a position on, the code has a count - until the next segment. 

1225 

1226 A segment is ``[line, column, count, hasCount, isRegionEntry, isGapRegion]``. Format version 2.0.0 has no 

1227 ``isGapRegion``. 

1228 """ 

1229 

1230 _line: int #: Line the segment starts at. 

1231 _column: int #: Column the segment starts at. 

1232 _count: int #: The count of the code from here on. 

1233 _hasCount: bool #: Whether the code from here on has a count; false e.g. after a function's end. 

1234 _isRegionEntry: bool #: Whether a region starts here; false, where a region continues after a nested one. 

1235 _isGapRegion: bool #: Whether a gap region starts here. 

1236 

1237 def __init__( 

1238 self, 

1239 line: int, 

1240 column: int, 

1241 count: int, 

1242 hasCount: bool, 

1243 isRegionEntry: bool, 

1244 isGapRegion: bool = False, 

1245 *, 

1246 parent: Nullable[File] = None 

1247 ) -> None: 

1248 """ 

1249 Initialize a segment, and add it to the segments of its file. 

1250 

1251 :param line: Line the segment starts at. 

1252 :param column: Column the segment starts at. 

1253 :param count: The count of the code from here on. 

1254 :param hasCount: Whether the code from here on has a count. 

1255 :param isRegionEntry: Whether a region starts here. 

1256 :param isGapRegion: Optional, whether a gap region starts here. Default: ``False``. 

1257 :param parent: Optional, the file the segment belongs to. Default: ``None``. 

1258 :raises ValueError: If parameter ``line`` is ``None``. 

1259 :raises TypeError: If parameter ``line`` isn't of type :class:`int`. 

1260 :raises ValueError: If parameter ``line`` is less than 1. 

1261 :raises ValueError: If parameter ``column`` is ``None``. 

1262 :raises TypeError: If parameter ``column`` isn't of type :class:`int`. 

1263 :raises ValueError: If parameter ``column`` is less than 1. 

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

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

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

1267 :raises ValueError: If parameter ``hasCount`` is ``None``. 

1268 :raises TypeError: If parameter ``hasCount`` isn't of type :class:`bool`. 

1269 :raises ValueError: If parameter ``isRegionEntry`` is ``None``. 

1270 :raises TypeError: If parameter ``isRegionEntry`` isn't of type :class:`bool`. 

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

1272 :raises TypeError: If parameter ``isGapRegion`` isn't of type :class:`bool`. 

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

1274 """ 

1275 super().__init__(parent=parent) 

1276 

1277 for name, position in (("line", line), ("column", column)): 

1278 if position is None: 

1279 raise ValueError(f"Parameter '{name}' is None.") 

1280 elif not isinstance(position, int): 

1281 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

1282 ex.add_note(f"Got type '{getFullyQualifiedName(position)}'.") 

1283 raise ex 

1284 elif position < 1: 

1285 ex = ValueError(f"Parameter '{name}' is less than 1.") 

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

1287 raise ex 

1288 

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

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

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

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

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

1294 raise ex 

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

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

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

1298 raise ex 

1299 

1300 for name, flag in (("hasCount", hasCount), ("isRegionEntry", isRegionEntry), ("isGapRegion", isGapRegion)): 

1301 if flag is None: 1301 ↛ 1302line 1301 didn't jump to line 1302 because the condition on line 1301 was never true

1302 raise ValueError(f"Parameter '{name}' is None.") 

1303 elif not isinstance(flag, bool): 

1304 ex = TypeError(f"Parameter '{name}' is not of type 'bool'.") 

1305 ex.add_note(f"Got type '{getFullyQualifiedName(flag)}'.") 

1306 raise ex 

1307 

1308 self._line = line 

1309 self._column = column 

1310 self._count = count 

1311 self._hasCount = hasCount 

1312 self._isRegionEntry = isRegionEntry 

1313 self._isGapRegion = isGapRegion 

1314 

1315 if parent is not None: 

1316 parent._segments.append(self) 

1317 

1318 @classmethod 

1319 def Parse(cls, segment: list[Any], *, parent: Nullable[File] = None) -> Self: 

1320 """ 

1321 Parse a segment from its JSON array. 

1322 

1323 :param segment: The JSON array of the segment. 

1324 :param parent: Optional, the file the segment belongs to. Default: ``None``. 

1325 :returns: The segment. 

1326 """ 

1327 return cls(*segment[:5], segment[5] if len(segment) > 5 else False, parent=parent) 

1328 

1329 @readonly 

1330 def Line(self) -> int: 

1331 """ 

1332 Read-only property to access the line the segment starts at (:attr:`_line`). 

1333 

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

1335 """ 

1336 return self._line 

1337 

1338 @readonly 

1339 def Column(self) -> int: 

1340 """ 

1341 Read-only property to access the column the segment starts at (:attr:`_column`). 

1342 

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

1344 """ 

1345 return self._column 

1346 

1347 @readonly 

1348 def Count(self) -> int: 

1349 """ 

1350 Read-only property to access the count of the code from the segment's start on (:attr:`_count`). 

1351 

1352 :returns: How often the code ran; ``0`` without a count. 

1353 """ 

1354 return self._count 

1355 

1356 @readonly 

1357 def HasCount(self) -> bool: 

1358 """ 

1359 Read-only property to access whether the code from the segment's start on has a count (:attr:`_hasCount`). 

1360 

1361 :returns: ``False`` e.g. after the end of a function, or in a skipped region. 

1362 """ 

1363 return self._hasCount 

1364 

1365 @readonly 

1366 def IsRegionEntry(self) -> bool: 

1367 """ 

1368 Read-only property to access whether a region starts at the segment's start (:attr:`_isRegionEntry`). 

1369 

1370 :returns: ``False``, where a region continues after a region nested in it. 

1371 """ 

1372 return self._isRegionEntry 

1373 

1374 @readonly 

1375 def IsGapRegion(self) -> bool: 

1376 """ 

1377 Read-only property to access whether a gap region starts at the segment's start (:attr:`_isGapRegion`). 

1378 

1379 :returns: ``True`` for a gap region; ``False`` in format version 2.0.0. 

1380 """ 

1381 return self._isGapRegion 

1382 

1383 

1384@export 

1385class Expansion(Element["File"]): 

1386 """ 

1387 A macro expansion in a file: the region it is expanded at, the regions of the function it is in, and its branches. 

1388 """ 

1389 

1390 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index. 

1391 _sourceRegion: Region #: The region the macro is expanded at. 

1392 _targetRegions: list[Region] #: The regions of the function the expansion is in. 

1393 _branches: list[BranchRegion] #: The branch regions in the expansion, and in the expansions nested in it. 

1394 

1395 def __init__( 

1396 self, 

1397 filePaths: Iterable[Path], 

1398 sourceRegion: Region, 

1399 targetRegions: Iterable[Region], 

1400 branches: Nullable[Iterable[BranchRegion]] = None, 

1401 *, 

1402 parent: Nullable[File] = None 

1403 ) -> None: 

1404 """ 

1405 Initialize an expansion, and add it to the expansions of its file. 

1406 

1407 :param filePaths: The paths of the files, which the file IDs of the regions index. 

1408 :param sourceRegion: The region the macro is expanded at. 

1409 :param targetRegions: The regions of the function the expansion is in. 

1410 :param branches: Optional, the branch regions in the expansion, and in the expansions 

1411 nested in it. Default: none. 

1412 :param parent: Optional, the file the expansion belongs to. Default: ``None``. 

1413 :raises ValueError: If parameter ``filePaths`` is ``None``. 

1414 :raises TypeError: If parameter ``filePaths`` isn't iterable. 

1415 :raises TypeError: If parameter ``filePaths`` contains an element not of type 

1416 :class:`~pathlib.Path`. 

1417 :raises ValueError: If parameter ``sourceRegion`` is ``None``. 

1418 :raises TypeError: If parameter ``sourceRegion`` isn't of type :class:`Region`. 

1419 :raises AlreadyInCoverageReportError: If parameter ``sourceRegion`` is a region of another element. 

1420 :raises ValueError: If parameter ``sourceRegion`` is a region of a file ID or an expanded file ID 

1421 out of range of parameter ``filePaths``. 

1422 :raises ValueError: If parameter ``targetRegions`` is ``None``. 

1423 :raises TypeError: If parameter ``targetRegions`` isn't iterable. 

1424 :raises TypeError: If parameter ``targetRegions`` contains an element not of type 

1425 :class:`Region`. 

1426 :raises AlreadyInCoverageReportError: If parameter ``targetRegions`` contains a region of another element. 

1427 :raises ValueError: If parameter ``targetRegions`` contains a region of a file ID or an expanded 

1428 file ID out of range of parameter ``filePaths``. 

1429 :raises TypeError: If parameter ``branches`` isn't iterable. 

1430 :raises TypeError: If parameter ``branches`` contains an element not of type 

1431 :class:`BranchRegion`. 

1432 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element. 

1433 :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file 

1434 ID out of range of parameter ``filePaths``. 

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

1436 """ 

1437 super().__init__(parent=parent) 

1438 

1439 if filePaths is None: 1439 ↛ 1440line 1439 didn't jump to line 1440 because the condition on line 1439 was never true

1440 raise ValueError("Parameter 'filePaths' is None.") 

1441 elif not isinstance(filePaths, Iterable): 1441 ↛ 1442line 1441 didn't jump to line 1442 because the condition on line 1441 was never true

1442 ex = TypeError("Parameter 'filePaths' is not iterable.") 

1443 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.") 

1444 raise ex 

1445 

1446 if sourceRegion is None: 1446 ↛ 1447line 1446 didn't jump to line 1447 because the condition on line 1446 was never true

1447 raise ValueError("Parameter 'sourceRegion' is None.") 

1448 elif not isinstance(sourceRegion, Region): 1448 ↛ 1449line 1448 didn't jump to line 1449 because the condition on line 1448 was never true

1449 ex = TypeError("Parameter 'sourceRegion' is not of type 'Region'.") 

1450 ex.add_note(f"Got type '{getFullyQualifiedName(sourceRegion)}'.") 

1451 raise ex 

1452 elif sourceRegion._parent is not None: 1452 ↛ 1453line 1452 didn't jump to line 1453 because the condition on line 1452 was never true

1453 raise AlreadyInCoverageReportError("Parameter 'sourceRegion' is a region of another element.") 

1454 

1455 if targetRegions is None: 1455 ↛ 1456line 1455 didn't jump to line 1456 because the condition on line 1455 was never true

1456 raise ValueError("Parameter 'targetRegions' is None.") 

1457 

1458 self._filePaths = filePathList = [] 

1459 self._sourceRegion = sourceRegion 

1460 self._targetRegions = [] 

1461 self._branches = [] 

1462 

1463 for filePath in filePaths: 

1464 if not isinstance(filePath, Path): 

1465 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.") 

1466 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.") 

1467 raise ex 

1468 

1469 filePathList.append(filePath) 

1470 

1471 for name, elements, elementType, target in ( 

1472 ("targetRegions", targetRegions, Region, self._targetRegions), 

1473 ("branches", branches, BranchRegion, self._branches) 

1474 ): 

1475 if elements is None: 

1476 continue 

1477 elif not isinstance(elements, Iterable): 

1478 ex = TypeError(f"Parameter '{name}' is not iterable.") 

1479 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") 

1480 raise ex 

1481 

1482 for element in elements: 

1483 if not isinstance(element, elementType): 1483 ↛ 1484line 1483 didn't jump to line 1484 because the condition on line 1483 was never true

1484 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.") 

1485 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

1486 raise ex 

1487 elif element._parent is not None: 1487 ↛ 1488line 1487 didn't jump to line 1488 because the condition on line 1487 was never true

1488 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains a region of another element.") 

1489 elif element._fileID is not None and element._fileID >= len(self._filePaths): 

1490 ex = ValueError(f"Parameter '{name}' contains a region of a file ID out of range.") 

1491 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.") 

1492 raise ex 

1493 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): 1493 ↛ 1494line 1493 didn't jump to line 1494 because the condition on line 1493 was never true

1494 ex = ValueError(f"Parameter '{name}' contains an expansion region of an expanded file ID out of range.") 

1495 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.") 

1496 raise ex 

1497 

1498 element._parent = self 

1499 element._root = self._root 

1500 target.append(element) 

1501 

1502 if sourceRegion._fileID >= len(self._filePaths): 1502 ↛ 1503line 1502 didn't jump to line 1503 because the condition on line 1502 was never true

1503 ex = ValueError("Parameter 'sourceRegion' is a region of a file ID out of range.") 

1504 ex.add_note(f"Got value '{sourceRegion._fileID}' for {len(self._filePaths)} file paths.") 

1505 raise ex 

1506 elif sourceRegion._kind is RegionKind.Expansion and sourceRegion._expandedFileID >= len(self._filePaths): 

1507 ex = ValueError("Parameter 'sourceRegion' is an expansion region of an expanded file ID out of range.") 

1508 ex.add_note(f"Got value '{sourceRegion._expandedFileID}' for {len(self._filePaths)} file paths.") 

1509 raise ex 

1510 

1511 sourceRegion._parent = self 

1512 sourceRegion._root = self._root 

1513 

1514 if parent is not None: 

1515 parent._expansions.append(self) 

1516 

1517 @classmethod 

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

1519 """ 

1520 Parse an expansion from its JSON object. 

1521 

1522 :param expansion: The JSON object of the expansion. 

1523 :param parent: Optional, the file the expansion belongs to. Default: ``None``. 

1524 :returns: The expansion. 

1525 """ 

1526 return cls( 

1527 [Path(filename.replace("\\", "/")) for filename in expansion["filenames"]], 

1528 Region.Parse(expansion["source_region"]), 

1529 [Region.Parse(region) for region in expansion["target_regions"]], 

1530 [BranchRegion.Parse(branch) for branch in expansion.get("branches", [])], 

1531 parent=parent 

1532 ) 

1533 

1534 @readonly 

1535 def FilePaths(self) -> list[Path]: 

1536 """ 

1537 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`). 

1538 

1539 :returns: The paths, as the compiler named them. 

1540 """ 

1541 return self._filePaths 

1542 

1543 @readonly 

1544 def SourceRegion(self) -> Region: 

1545 """ 

1546 Read-only property to access the region the macro is expanded at (:attr:`_sourceRegion`). 

1547 

1548 :returns: The region. 

1549 """ 

1550 return self._sourceRegion 

1551 

1552 @readonly 

1553 def TargetRegions(self) -> list[Region]: 

1554 """ 

1555 Read-only property to access the regions of the function the expansion is in (:attr:`_targetRegions`). 

1556 

1557 :returns: The regions. 

1558 """ 

1559 return self._targetRegions 

1560 

1561 @readonly 

1562 def Branches(self) -> list[BranchRegion]: 

1563 """ 

1564 Read-only property to access the branch regions in the expansion, and in the expansions nested in it 

1565 (:attr:`_branches`). 

1566 

1567 :returns: The branch regions; empty before LLVM 12. 

1568 """ 

1569 return self._branches 

1570 

1571 def _SetRoot(self, root: Nullable[Report]) -> None: 

1572 """ 

1573 Set the report's root of the expansion, and of its source region, target regions and branch regions. 

1574 

1575 :param root: The report's root, or ``None`` while the element isn't part of a report. 

1576 """ 

1577 self._root = root 

1578 for region in (self._sourceRegion, *self._targetRegions, *self._branches): 

1579 region._SetRoot(root) 

1580 

1581 

1582@export 

1583class File(Element["Report"]): 

1584 """ 

1585 A file: its segments, branch regions, MC/DC records and expansions, and its summary. 

1586 

1587 A report written with ``-summary-only`` has only the summary, one written with ``-skip-expansions`` no expansions. 

1588 """ 

1589 

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

1591 _segments: list[Segment] #: The segments, by position. 

1592 _branches: list[BranchRegion] #: The branch regions of the file's functions. 

1593 _mcdcRecords: list[MCDCRecord] #: The MC/DC records of the file's functions. 

1594 _expansions: list[Expansion] #: The macro expansions in the file. 

1595 _summary: _Summary #: The counters llvm-cov computed. 

1596 

1597 def __init__( 

1598 self, 

1599 path: Path, 

1600 summary: Summary, 

1601 segments: Nullable[Iterable[Segment]] = None, 

1602 branches: Nullable[Iterable[BranchRegion]] = None, 

1603 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None, 

1604 expansions: Nullable[Iterable[Expansion]] = None, 

1605 *, 

1606 parent: Nullable[Report] = None 

1607 ) -> None: 

1608 """ 

1609 Initialize a file, and add it to the files of its report. 

1610 

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

1612 :param summary: The counters llvm-cov computed. 

1613 :param segments: Optional, the segments, by position. Default: none. 

1614 :param branches: Optional, the branch regions of the file's functions. Default: none. 

1615 :param mcdcRecords: Optional, the MC/DC records of the file's functions. Default: none. 

1616 :param expansions: Optional, the macro expansions in the file. Default: none. 

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

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

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

1620 :raises ValueError: If parameter ``summary`` is ``None``. 

1621 :raises TypeError: If parameter ``summary`` isn't of type :class:`Summary`. 

1622 :raises AlreadyInCoverageReportError: If parameter ``summary`` is the summary of another element. 

1623 :raises TypeError: If parameter ``segments`` isn't iterable. 

1624 :raises TypeError: If parameter ``segments`` contains an element not of type 

1625 :class:`Segment`. 

1626 :raises AlreadyInCoverageReportError: If parameter ``segments`` contains an element of another element. 

1627 :raises TypeError: If parameter ``branches`` isn't iterable. 

1628 :raises TypeError: If parameter ``branches`` contains an element not of type 

1629 :class:`BranchRegion`. 

1630 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains an element of another element. 

1631 :raises TypeError: If parameter ``mcdcRecords`` isn't iterable. 

1632 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type 

1633 :class:`MCDCRecord`. 

1634 :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains an element of another element. 

1635 :raises TypeError: If parameter ``expansions`` isn't iterable. 

1636 :raises TypeError: If parameter ``expansions`` contains an element not of type 

1637 :class:`Expansion`. 

1638 :raises AlreadyInCoverageReportError: If parameter ``expansions`` contains an element of another element. 

1639 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`. 

1640 :raises DuplicateFileError: If parameter ``parent`` has a file of the same path. 

1641 """ 

1642 super().__init__(parent=parent) 

1643 

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

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

1646 elif not isinstance(path, Path): 

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

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

1649 raise ex 

1650 elif parent is not None and path in parent._files: 

1651 ex = DuplicateFileError("Parameter 'parent' has a file of the same path.") 

1652 ex.add_note(f"Got value '{path.as_posix()}'.") 

1653 raise ex 

1654 

1655 if summary is None: 1655 ↛ 1656line 1655 didn't jump to line 1656 because the condition on line 1655 was never true

1656 raise ValueError("Parameter 'summary' is None.") 

1657 elif not isinstance(summary, Summary): 1657 ↛ 1658line 1657 didn't jump to line 1658 because the condition on line 1657 was never true

1658 ex = TypeError("Parameter 'summary' is not of type 'Summary'.") 

1659 ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.") 

1660 raise ex 

1661 elif summary._parent is not None: 

1662 raise AlreadyInCoverageReportError("Parameter 'summary' is the summary of another element.") 

1663 

1664 self._path = path 

1665 self._segments = [] 

1666 self._branches = [] 

1667 self._mcdcRecords = [] 

1668 self._expansions = [] 

1669 self._summary = summary 

1670 

1671 for name, elements, elementType, target in ( 

1672 ("segments", segments, Segment, self._segments), ("branches", branches, BranchRegion, self._branches), 

1673 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords), 

1674 ("expansions", expansions, Expansion, self._expansions) 

1675 ): 

1676 if elements is None: 

1677 continue 

1678 elif not isinstance(elements, Iterable): 1678 ↛ 1679line 1678 didn't jump to line 1679 because the condition on line 1678 was never true

1679 ex = TypeError(f"Parameter '{name}' is not iterable.") 

1680 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") 

1681 raise ex 

1682 

1683 for element in elements: 

1684 if not isinstance(element, elementType): 1684 ↛ 1685line 1684 didn't jump to line 1685 because the condition on line 1684 was never true

1685 ex = TypeError(f"Parameter '{name}' contains an element not of type '{elementType.__name__}'.") 

1686 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

1687 raise ex 

1688 elif element._parent is not None: 1688 ↛ 1689line 1688 didn't jump to line 1689 because the condition on line 1688 was never true

1689 raise AlreadyInCoverageReportError(f"Parameter '{name}' contains an element of another element.") 

1690 

1691 element._parent = self 

1692 element._SetRoot(self._root) 

1693 

1694 target.append(element) 

1695 

1696 summary._parent = self 

1697 summary._SetRoot(self._root) 

1698 

1699 if parent is not None: 

1700 parent._files[path] = self 

1701 

1702 @classmethod 

1703 def Parse(cls, file: dict[str, Any], *, parent: Nullable[Report] = None) -> Self: 

1704 """ 

1705 Parse a file from its JSON object. 

1706 

1707 :param file: The JSON object of the file. 

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

1709 :returns: The file. 

1710 """ 

1711 return cls( 

1712 Path(file["filename"].replace("\\", "/")), 

1713 Summary.Parse(file["summary"]), 

1714 [Segment.Parse(segment) for segment in file.get("segments", [])], 

1715 [BranchRegion.Parse(branch) for branch in file.get("branches", [])], 

1716 [MCDCRecord.Parse(record) for record in file.get("mcdc_records", [])], 

1717 [Expansion.Parse(expansion) for expansion in file.get("expansions", [])], 

1718 parent=parent 

1719 ) 

1720 

1721 @readonly 

1722 def Path(self) -> Path: 

1723 """ 

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

1725 

1726 :returns: The path, as the compiler named it - usually absolute. 

1727 """ 

1728 return self._path 

1729 

1730 @readonly 

1731 def Segments(self) -> list[Segment]: 

1732 """ 

1733 Read-only property to access the segments (:attr:`_segments`). 

1734 

1735 :returns: The segments, by position; empty in a summary-only report. 

1736 """ 

1737 return self._segments 

1738 

1739 @readonly 

1740 def Branches(self) -> list[BranchRegion]: 

1741 """ 

1742 Read-only property to access the branch regions of the file's functions (:attr:`_branches`). 

1743 

1744 Which branch regions of a macro expansion are listed here, depends on the LLVM version. 

1745 

1746 :returns: The branch regions. 

1747 """ 

1748 return self._branches 

1749 

1750 @readonly 

1751 def MCDCRecords(self) -> list[MCDCRecord]: 

1752 """ 

1753 Read-only property to access the MC/DC records of the file's functions (:attr:`_mcdcRecords`). 

1754 

1755 :returns: The MC/DC records. 

1756 """ 

1757 return self._mcdcRecords 

1758 

1759 @readonly 

1760 def Expansions(self) -> list[Expansion]: 

1761 """ 

1762 Read-only property to access the macro expansions in the file (:attr:`_expansions`). 

1763 

1764 :returns: The expansions. 

1765 """ 

1766 return self._expansions 

1767 

1768 @readonly 

1769 def Summary(self) -> Summary: 

1770 """ 

1771 Read-only property to access the counters llvm-cov computed (:attr:`_summary`). 

1772 

1773 :returns: The summary. 

1774 """ 

1775 return self._summary 

1776 

1777 def _SetRoot(self, root: Nullable[Report]) -> None: 

1778 """ 

1779 Set the report's root of the file, and of its segments, branch regions, MC/DC records, expansions and summary. 

1780 

1781 :param root: The report's root, or ``None`` while the element isn't part of a report. 

1782 """ 

1783 self._root = root 

1784 for element in (*self._segments, *self._branches, *self._mcdcRecords, *self._expansions, self._summary): 

1785 element._SetRoot(root) 

1786 

1787 

1788@export 

1789class Function(Element["Report"]): 

1790 """ 

1791 A function - an instantiation of a template is a function of its own -: how often it was called, its regions, branch 

1792 regions and MC/DC records, and the files they are in. 

1793 """ 

1794 

1795 _name: str #: The function's name, as the profile names it - e.g. mangled. 

1796 _count: int #: How often the function was called. 

1797 _regions: list[Region] #: The regions. 

1798 _branches: list[BranchRegion] #: The branch regions. 

1799 _mcdcRecords: list[MCDCRecord] #: The MC/DC records. 

1800 _filePaths: list[Path] #: The paths of the files, which the file IDs of the regions index. 

1801 

1802 def __init__( 

1803 self, 

1804 name: str, 

1805 count: int, 

1806 regions: Iterable[Region], 

1807 filePaths: Iterable[Path], 

1808 branches: Nullable[Iterable[BranchRegion]] = None, 

1809 mcdcRecords: Nullable[Iterable[MCDCRecord]] = None, 

1810 *, 

1811 parent: Nullable[Report] = None 

1812 ) -> None: 

1813 """ 

1814 Initialize a function, and add it to the functions of its report. 

1815 

1816 :param name: The function's name, as the profile names it - e.g. mangled. 

1817 :param count: How often the function was called. 

1818 :param regions: The regions. 

1819 :param filePaths: The paths of the files, which the file IDs of the regions index. 

1820 :param branches: Optional, the branch regions. Default: none. 

1821 :param mcdcRecords: Optional, the MC/DC records. Default: none. 

1822 :param parent: Optional, the report the function belongs to. Default: ``None``. 

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

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

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

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

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

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

1829 :raises ValueError: If parameter ``regions`` is ``None``. 

1830 :raises TypeError: If parameter ``regions`` isn't iterable. 

1831 :raises TypeError: If parameter ``regions`` contains an element not of type 

1832 :class:`Region`. 

1833 :raises AlreadyInCoverageReportError: If parameter ``regions`` contains a region of another element. 

1834 :raises ValueError: If parameter ``regions`` contains a region of a file ID or an expanded file ID 

1835 out of range of parameter ``filePaths``. 

1836 :raises ValueError: If parameter ``filePaths`` is ``None``. 

1837 :raises TypeError: If parameter ``filePaths`` isn't iterable. 

1838 :raises TypeError: If parameter ``filePaths`` contains an element not of type 

1839 :class:`~pathlib.Path`. 

1840 :raises TypeError: If parameter ``branches`` isn't iterable. 

1841 :raises TypeError: If parameter ``branches`` contains an element not of type 

1842 :class:`BranchRegion`. 

1843 :raises AlreadyInCoverageReportError: If parameter ``branches`` contains a region of another element. 

1844 :raises ValueError: If parameter ``branches`` contains a region of a file ID or an expanded file 

1845 ID out of range of parameter ``filePaths``. 

1846 :raises TypeError: If parameter ``mcdcRecords`` isn't iterable. 

1847 :raises TypeError: If parameter ``mcdcRecords`` contains an element not of type 

1848 :class:`MCDCRecord`. 

1849 :raises AlreadyInCoverageReportError: If parameter ``mcdcRecords`` contains a region of another element. 

1850 :raises ValueError: If parameter ``mcdcRecords`` contains a region of a file ID or an expanded 

1851 file ID out of range of parameter ``filePaths``. 

1852 :raises TypeError: If parameter ``parent`` isn't of type :class:`Report`. 

1853 """ 

1854 super().__init__(parent=parent) 

1855 

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

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

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

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

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

1861 raise ex 

1862 elif name == "": 

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

1864 

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

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

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

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

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

1870 raise ex 

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

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

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

1874 raise ex 

1875 

1876 if regions is None: 1876 ↛ 1877line 1876 didn't jump to line 1877 because the condition on line 1876 was never true

1877 raise ValueError("Parameter 'regions' is None.") 

1878 

1879 if filePaths is None: 1879 ↛ 1880line 1879 didn't jump to line 1880 because the condition on line 1879 was never true

1880 raise ValueError("Parameter 'filePaths' is None.") 

1881 elif not isinstance(filePaths, Iterable): 1881 ↛ 1882line 1881 didn't jump to line 1882 because the condition on line 1881 was never true

1882 ex = TypeError("Parameter 'filePaths' is not iterable.") 

1883 ex.add_note(f"Got type '{getFullyQualifiedName(filePaths)}'.") 

1884 raise ex 

1885 

1886 self._name = name 

1887 self._count = count 

1888 self._regions = [] 

1889 self._branches = [] 

1890 self._mcdcRecords = [] 

1891 self._filePaths = filePathList = [] 

1892 

1893 for filePath in filePaths: 

1894 if not isinstance(filePath, Path): 1894 ↛ 1895line 1894 didn't jump to line 1895 because the condition on line 1894 was never true

1895 ex = TypeError("Parameter 'filePaths' contains an element not of type 'Path'.") 

1896 ex.add_note(f"Got type '{getFullyQualifiedName(filePath)}'.") 

1897 raise ex 

1898 

1899 filePathList.append(filePath) 

1900 

1901 for parameterName, elements, elementType, target in ( 

1902 ("regions", regions, Region, self._regions), ("branches", branches, BranchRegion, self._branches), 

1903 ("mcdcRecords", mcdcRecords, MCDCRecord, self._mcdcRecords) 

1904 ): 

1905 if elements is None: 

1906 continue 

1907 elif not isinstance(elements, Iterable): 1907 ↛ 1908line 1907 didn't jump to line 1908 because the condition on line 1907 was never true

1908 ex = TypeError(f"Parameter '{parameterName}' is not iterable.") 

1909 ex.add_note(f"Got type '{getFullyQualifiedName(elements)}'.") 

1910 raise ex 

1911 

1912 for element in elements: 

1913 if not isinstance(element, elementType): 1913 ↛ 1914line 1913 didn't jump to line 1914 because the condition on line 1913 was never true

1914 ex = TypeError(f"Parameter '{parameterName}' contains an element not of type '{elementType.__name__}'.") 

1915 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

1916 raise ex 

1917 elif element._parent is not None: 

1918 raise AlreadyInCoverageReportError(f"Parameter '{parameterName}' contains a region of another element.") 

1919 elif element._fileID is not None and element._fileID >= len(self._filePaths): 

1920 ex = ValueError(f"Parameter '{parameterName}' contains a region of a file ID out of range.") 

1921 ex.add_note(f"Got value '{element._fileID}' for {len(self._filePaths)} file paths.") 

1922 raise ex 

1923 elif element._kind is RegionKind.Expansion and element._expandedFileID >= len(self._filePaths): 

1924 ex = ValueError( 

1925 f"Parameter '{parameterName}' contains an expansion region of an expanded file ID out of range." 

1926 ) 

1927 ex.add_note(f"Got value '{element._expandedFileID}' for {len(self._filePaths)} file paths.") 

1928 raise ex 

1929 

1930 element._parent = self 

1931 element._SetRoot(self._root) 

1932 

1933 target.append(element) 

1934 

1935 if parent is not None: 

1936 parent._functions.append(self) 

1937 

1938 @classmethod 

1939 def Parse(cls, function: dict[str, Any], *, parent: Nullable[Report] = None) -> Self: 

1940 """ 

1941 Parse a function from its JSON object. 

1942 

1943 :param function: The JSON object of the function. 

1944 :param parent: Optional, the report the function belongs to. Default: ``None``. 

1945 :returns: The function. 

1946 """ 

1947 return cls( 

1948 function["name"], 

1949 function["count"], 

1950 [Region.Parse(region) for region in function["regions"]], 

1951 [Path(filename.replace("\\", "/")) for filename in function["filenames"]], 

1952 [BranchRegion.Parse(branch) for branch in function.get("branches", [])], 

1953 [MCDCRecord.Parse(record) for record in function.get("mcdc_records", [])], 

1954 parent=parent 

1955 ) 

1956 

1957 @readonly 

1958 def Name(self) -> str: 

1959 """ 

1960 Read-only property to access the function's name (:attr:`_name`). 

1961 

1962 :returns: The name, as the profile names it: mangled, and prefixed by the file name of its translation unit, if 

1963 local to it - e.g. ``Statistics.c:Square``. 

1964 """ 

1965 return self._name 

1966 

1967 @readonly 

1968 def Count(self) -> int: 

1969 """ 

1970 Read-only property to access how often the function was called (:attr:`_count`). 

1971 

1972 :returns: The count. 

1973 """ 

1974 return self._count 

1975 

1976 @readonly 

1977 def Regions(self) -> list[Region]: 

1978 """ 

1979 Read-only property to access the regions (:attr:`_regions`). 

1980 

1981 :returns: The regions. 

1982 """ 

1983 return self._regions 

1984 

1985 @readonly 

1986 def Branches(self) -> list[BranchRegion]: 

1987 """ 

1988 Read-only property to access the branch regions (:attr:`_branches`). 

1989 

1990 :returns: The branch regions, also those in macro expansions. 

1991 """ 

1992 return self._branches 

1993 

1994 @readonly 

1995 def MCDCRecords(self) -> list[MCDCRecord]: 

1996 """ 

1997 Read-only property to access the MC/DC records (:attr:`_mcdcRecords`). 

1998 

1999 :returns: The MC/DC records. 

2000 """ 

2001 return self._mcdcRecords 

2002 

2003 @readonly 

2004 def FilePaths(self) -> list[Path]: 

2005 """ 

2006 Read-only property to access the paths of the files, which the file IDs of the regions index (:attr:`_filePaths`). 

2007 

2008 :returns: The paths, as the compiler named them. 

2009 """ 

2010 return self._filePaths 

2011 

2012 @readonly 

2013 def MainFileID(self) -> Nullable[int]: 

2014 """ 

2015 Read-only property to return the file ID of the file the function is in: the first file no expansion region 

2016 expands to. 

2017 

2018 :returns: The index into :attr:`FilePaths`, or ``None`` if every file is expanded to. 

2019 """ 

2020 expanded = {region._expandedFileID for region in self._regions if region._kind is RegionKind.Expansion} 

2021 return next((fileID for fileID in range(len(self._filePaths)) if fileID not in expanded), None) 

2022 

2023 @readonly 

2024 def MainFilePath(self) -> Nullable[Path]: 

2025 """ 

2026 Read-only property to return the path of the file the function is in (:attr:`MainFileID`). 

2027 

2028 :returns: The path, as the compiler named it; ``None`` if every file is expanded to. 

2029 """ 

2030 return None if (mainFileID := self.MainFileID) is None else self._filePaths[mainFileID] 

2031 

2032 @readonly 

2033 def MainFile(self) -> Nullable[File]: 

2034 """ 

2035 Read-only property to return the file the function is in, as the report lists it. 

2036 

2037 :returns: The file of :attr:`MainFilePath` in the report's files; ``None`` if the function isn't part of a report, 

2038 every file is expanded to, or the report doesn't list the file. 

2039 """ 

2040 if self._root is None or (path := self.MainFilePath) is None: 2040 ↛ 2041line 2040 didn't jump to line 2041 because the condition on line 2040 was never true

2041 return None 

2042 

2043 return self._root._files.get(path, None) 

2044 

2045 def _SetRoot(self, root: Nullable[Report]) -> None: 

2046 """ 

2047 Set the report's root of the function, and of its regions, branch regions and MC/DC records. 

2048 

2049 :param root: The report's root, or ``None`` while the element isn't part of a report. 

2050 """ 

2051 self._root = root 

2052 for element in (*self._regions, *self._branches, *self._mcdcRecords): 

2053 element._SetRoot(root) 

2054 

2055 

2056@export 

2057class Report(metaclass=ExtendedType, mixin=True): 

2058 """ 

2059 The report's root: the format version, the files, the functions, and the totals. 

2060 

2061 It is the root of every element below it. 

2062 """ 

2063 

2064 _root: Report #: The report's root: the report itself. 

2065 _version: Nullable[SemanticVersion] #: Version of the report format. 

2066 _files: dict[Path, File] #: The files, by path. 

2067 _functions: list[Function] #: The functions. 

2068 _totals: Nullable[Summary] #: The counters of the whole report. 

2069 

2070 def __init__( 

2071 self, 

2072 version: Nullable[SemanticVersion] = None, 

2073 files: Nullable[Iterable[File]] = None, 

2074 functions: Nullable[Iterable[Function]] = None, 

2075 totals: Nullable[Summary] = None 

2076 ) -> None: 

2077 """ 

2078 Initialize a report; empty, without parameters. 

2079 

2080 :param version: Optional, version of the report format. Default: ``None``. 

2081 :param files: Optional, the files. Default: none. 

2082 :param functions: Optional, the functions. Default: none. 

2083 :param totals: Optional, the counters of the whole report. Default: ``None``. 

2084 :raises TypeError: If parameter ``version`` isn't of type 

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

2086 :raises TypeError: If parameter ``files`` isn't iterable. 

2087 :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`. 

2088 :raises DuplicateFileError: If parameter ``files`` contains two files of the same path. 

2089 :raises AlreadyInCoverageReportError: If parameter ``files`` contains an element of another report. 

2090 :raises TypeError: If parameter ``functions`` isn't iterable. 

2091 :raises TypeError: If parameter ``functions`` contains an element not of type 

2092 :class:`Function`. 

2093 :raises AlreadyInCoverageReportError: If parameter ``functions`` contains an element of another report. 

2094 :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`. 

2095 :raises AlreadyInCoverageReportError: If parameter ``totals`` is the summary of another element. 

2096 """ 

2097 if version is not None and not isinstance(version, SemanticVersion): 

2098 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") 

2099 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

2100 raise ex 

2101 

2102 if totals is not None and not isinstance(totals, Summary): 2102 ↛ 2103line 2102 didn't jump to line 2103 because the condition on line 2102 was never true

2103 ex = TypeError("Parameter 'totals' is not of type 'Summary'.") 

2104 ex.add_note(f"Got type '{getFullyQualifiedName(totals)}'.") 

2105 raise ex 

2106 elif totals is not None and totals._parent is not None: 2106 ↛ 2107line 2106 didn't jump to line 2107 because the condition on line 2106 was never true

2107 raise AlreadyInCoverageReportError("Parameter 'totals' is the summary of another element.") 

2108 

2109 self._root = self 

2110 self._version = version 

2111 self._files = {} 

2112 self._functions = functionList = [] 

2113 self._totals = totals 

2114 

2115 if files is not None: 

2116 if not isinstance(files, Iterable): 2116 ↛ 2117line 2116 didn't jump to line 2117 because the condition on line 2116 was never true

2117 ex = TypeError("Parameter 'files' is not iterable.") 

2118 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.") 

2119 raise ex 

2120 

2121 for file in files: 

2122 if not isinstance(file, File): 

2123 ex = TypeError("Parameter 'files' contains an element not of type 'File'.") 

2124 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

2125 raise ex 

2126 elif file._parent is not None: 

2127 raise AlreadyInCoverageReportError("Parameter 'files' contains a file of another report.") 

2128 elif file._path in self._files: 

2129 ex = DuplicateFileError("Parameter 'files' contains two files of the same path.") 

2130 ex.add_note(f"Got value '{file._path.as_posix()}'.") 

2131 raise ex 

2132 

2133 file._parent = self 

2134 file._SetRoot(self) 

2135 

2136 self._files[file._path] = file 

2137 

2138 if functions is not None: 

2139 if not isinstance(functions, Iterable): 2139 ↛ 2140line 2139 didn't jump to line 2140 because the condition on line 2139 was never true

2140 ex = TypeError("Parameter 'functions' is not iterable.") 

2141 ex.add_note(f"Got type '{getFullyQualifiedName(functions)}'.") 

2142 raise ex 

2143 

2144 for function in functions: 

2145 if not isinstance(function, Function): 2145 ↛ 2146line 2145 didn't jump to line 2146 because the condition on line 2145 was never true

2146 ex = TypeError("Parameter 'functions' contains an element not of type 'Function'.") 

2147 ex.add_note(f"Got type '{getFullyQualifiedName(function)}'.") 

2148 raise ex 

2149 elif function._parent is not None: 2149 ↛ 2150line 2149 didn't jump to line 2150 because the condition on line 2149 was never true

2150 raise AlreadyInCoverageReportError("Parameter 'functions' contains a function of another report.") 

2151 

2152 function._parent = self 

2153 function._SetRoot(self) 

2154 

2155 functionList.append(function) 

2156 

2157 if totals is not None: 

2158 totals._parent = self 

2159 totals._SetRoot(self) 

2160 

2161 @readonly 

2162 def Version(self) -> Nullable[SemanticVersion]: 

2163 """ 

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

2165 

2166 :returns: The version, e.g. ``2.0.1``; ``None`` before the report was converted. 

2167 """ 

2168 return self._version 

2169 

2170 @readonly 

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

2172 """ 

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

2174 

2175 :returns: The files, by path. 

2176 """ 

2177 return self._files 

2178 

2179 @readonly 

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

2181 """ 

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

2183 

2184 :returns: The functions; empty in a report written with ``-skip-functions`` or ``-summary-only``. 

2185 """ 

2186 return self._functions 

2187 

2188 @readonly 

2189 def Totals(self) -> Nullable[Summary]: 

2190 """ 

2191 Read-only property to access the counters of the whole report (:attr:`_totals`). 

2192 

2193 :returns: The totals; ``None`` before the report was converted. 

2194 """ 

2195 return self._totals 

2196 

2197 

2198TestVector._PARENT_TYPE = (MCDCRecord, ) 

2199Region._PARENT_TYPE = (Function, Expansion) 

2200BranchRegion._PARENT_TYPE = (File, Function, Expansion) 

2201MCDCRecord._PARENT_TYPE = (File, Function) 

2202Counters._PARENT_TYPE = (Summary, ) 

2203Summary._PARENT_TYPE = (File, Report) 

2204Segment._PARENT_TYPE = (File, ) 

2205Expansion._PARENT_TYPE = (File, ) 

2206File._PARENT_TYPE = (Report, ) 

2207Function._PARENT_TYPE = (Report, ) 

2208 

2209 

2210@export 

2211class Document(cc_Document, Report): 

2212 """ 

2213 An LLVM JSON code coverage export: read into the format's model, and converted to the common model. 

2214 """ 

2215 

2216 _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`. 

2217 

2218 def __init__( 

2219 self, 

2220 jsonReportFile: Path, 

2221 analyzeAndConvert: bool = False, 

2222 *, 

2223 version: Nullable[SemanticVersion] = None, 

2224 files: Nullable[Iterable[File]] = None, 

2225 functions: Nullable[Iterable[Function]] = None, 

2226 totals: Nullable[Summary] = None 

2227 ) -> None: 

2228 """ 

2229 Initialize the report, and optionally read it; or build it from typed values. 

2230 

2231 :param jsonReportFile: Path to the JSON file. 

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

2233 :param version: Optional, version of the report format. Default: ``None``. 

2234 :param files: Optional, the files. Default: none. 

2235 :param functions: Optional, the functions. Default: none. 

2236 :param totals: Optional, the counters of the whole report. Default: ``None``. 

2237 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`. 

2238 :raises TypeError: If parameter ``files`` isn't iterable. 

2239 :raises TypeError: If parameter ``files`` contains an element not of type 

2240 :class:`File`. 

2241 :raises TypeError: If parameter ``functions`` isn't iterable. 

2242 :raises TypeError: If parameter ``functions`` contains an element not of type 

2243 :class:`Function`. 

2244 :raises TypeError: If parameter ``totals`` isn't of type 

2245 :class:`Summary`. 

2246 """ 

2247 super().__init__(jsonReportFile) 

2248 Report.__init__(self, version, files, functions, totals) 

2249 

2250 self._jsonDocument = None 

2251 

2252 if analyzeAndConvert: 

2253 self.Analyze() 

2254 self.Convert() 

2255 

2256 def Analyze(self) -> None: 

2257 """ 

2258 Parse the JSON file and validate it against the JSON Schema :data:`SCHEMA`. 

2259 

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

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

2262 :raises CodeCoverageError: If the JSON Schema can't be read. 

2263 :raises CodeCoverageError: If the file isn't valid according to the JSON Schema. 

2264 """ 

2265 if not self._path.exists(): 

2266 raise CodeCoverageError(f"LLVM coverage export file '{self._path}' does not exist.") \ 

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

2268 

2269 from jsonschema import Draft202012Validator 

2270 

2271 with Stopwatch() as sw: 

2272 try: 

2273 jsonDocument = loads(self._path.read_text(encoding="utf-8")) 

2274 except JSONDecodeError as ex: 

2275 raise CodeCoverageError(f"JSON syntax error in LLVM coverage export file '{self._path}'.") from ex 

2276 

2277 try: 

2278 schema = loads(readResourceFile(Resources, SCHEMA)) 

2279 except (ToolingException, JSONDecodeError) as ex: 

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

2281 

2282 errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path)) 

2283 if len(errors) > 0: 

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

2285 for error in errors: 

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

2287 raise ex 

2288 

2289 self._jsonDocument = jsonDocument 

2290 

2291 self._analysisDuration = sw.Duration 

2292 

2293 def Convert(self) -> None: 

2294 """ 

2295 Convert the parsed and validated JSON document to the format's model. 

2296 

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

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

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

2300 """ 

2301 if self._jsonDocument is None: 

2302 ex = CodeCoverageError( 

2303 f"LLVM coverage export file '{self._path}' needs to be read and analyzed by a JSON parser." 

2304 ) 

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

2306 raise ex 

2307 

2308 with Stopwatch() as sw: 

2309 data = self._jsonDocument["data"][0] 

2310 self._version = SemanticVersion.Parse(self._jsonDocument["version"]) 

2311 self._files = {} 

2312 self._functions = [] 

2313 self._totals = Summary.Parse(data["totals"]) 

2314 

2315 self._totals._parent = self 

2316 self._totals._SetRoot(self) 

2317 

2318 for record in data["files"]: 

2319 File.Parse(record, parent=self) 

2320 

2321 for function in data.get("functions", []): 

2322 Function.Parse(function, parent=self) 

2323 

2324 self._conversionDuration = sw.Duration 

2325 

2326 def ToCoverageSummary(self) -> CoverageSummary: 

2327 """ 

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

2329 

2330 :returns: The report's root of the common model, named after the report file. 

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

2332 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. 

2333 """ 

2334 parents = [path.parent.parts for path in self._files] 

2335 first = min(parents, default=()) 

2336 last = max(parents, default=()) 

2337 common = first 

2338 for index, part in enumerate(first): 2338 ↛ 2343line 2338 didn't jump to line 2343 because the loop on line 2338 didn't complete

2339 if part != last[index]: 

2340 common = first[:index] 

2341 break 

2342 

2343 summary = CoverageSummary(self._path.stem, sourceDirectories=[Path(*common)] if len(common) > 0 else []) 

2344 commonFiles = {path: summary.GetOrAddFile("/".join(path.parts[len(common):])) for path in self._files} 

2345 

2346 branches = self._CollectBranches() 

2347 for path, file in self._files.items(): 

2348 self._ConvertLines(file, commonFiles[path], branches[path]) 

2349 

2350 self._ConvertUnits(commonFiles, summary) 

2351 

2352 summary.Aggregate() 

2353 return summary 

2354 

2355 def _CollectBranches(self) -> dict[Path, dict[tuple[Any, ...], list[int]]]: 

2356 """ 

2357 Collect the branch regions of the functions by file; a branch region found in several functions is summed. 

2358 

2359 A branch region is collected at the line it starts at, or at the line its macro is expanded at. It is found in 

2360 several functions e.g. in the instantiations of a template. 

2361 

2362 :returns: By path, the summed true and false counts of each branch region, keyed by the line 

2363 number first. 

2364 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. 

2365 """ 

2366 branches: dict[Path, dict[tuple[Any, ...], list[int]]] = {path: {} for path in self._files} 

2367 for function in self._functions: 

2368 mainFileID = function.MainFileID 

2369 if mainFileID is None or (fileBranches := branches.get(function._filePaths[mainFileID])) is None: 2369 ↛ 2370line 2369 didn't jump to line 2370 because the condition on line 2369 was never true

2370 continue 

2371 

2372 expansions = { 

2373 region._expandedFileID: region for region in function._regions if region._kind is RegionKind.Expansion 

2374 } 

2375 for branch in function._branches: 

2376 lineNumber = branch._lineStart 

2377 fileID = branch._fileID 

2378 while fileID != mainFileID: 

2379 if (expansion := expansions.get(fileID)) is None: 

2380 ex = CodeCoverageError(f"Function '{function._name}' has a branch region in a file no region expands to.") 

2381 ex.add_note(f"Got file ID {fileID} of file '{function._filePaths[fileID]}'.") 

2382 raise ex 

2383 

2384 lineNumber = expansion._lineStart 

2385 fileID = expansion._fileID 

2386 

2387 key = ( 

2388 lineNumber, function._filePaths[branch._fileID], 

2389 branch._lineStart, branch._columnStart, branch._lineEnd, branch._columnEnd 

2390 ) 

2391 if (counts := fileBranches.get(key)) is None: 

2392 fileBranches[key] = [branch._trueCount, branch._falseCount] 

2393 else: 

2394 counts[0] += branch._trueCount 

2395 counts[1] += branch._falseCount 

2396 

2397 return branches 

2398 

2399 @staticmethod 

2400 def _ConvertLines(file: File, commonFile: cc_File, branches: dict[tuple[Any, ...], list[int]]) -> None: 

2401 """ 

2402 Convert a file's segments to lines of the common model, and add the branches of the file's branch regions. 

2403 

2404 A line's count is derived as ``llvm-cov show`` derives it: of the segments starting on the line, and of the last 

2405 segment of a previous line, which continues on it. A branch region of a line, which isn't executable, is left out. 

2406 

2407 :param file: The file of the format's model. 

2408 :param commonFile: The file of the common model. 

2409 :param branches: The true and false counts of each branch region of the file, keyed by the line number first. 

2410 """ 

2411 counts: dict[int, int] = {} 

2412 wrapped: Nullable[Segment] = None 

2413 segments = file._segments 

2414 index = 0 

2415 while index < len(segments): 

2416 lineNumber = segments[index]._line 

2417 lineSegments = [] 

2418 while index < len(segments) and segments[index]._line == lineNumber: 

2419 lineSegments.append(segments[index]) 

2420 index += 1 

2421 

2422 starts = [s._count for s in lineSegments if s._hasCount and s._isRegionEntry and not s._isGapRegion] 

2423 skipped = not lineSegments[0]._hasCount and lineSegments[0]._isRegionEntry 

2424 if ( 

2425 (not skipped and ((wrapped is not None and wrapped._hasCount) or len(starts) > 0)) or 

2426 any(segment._hasCount and segment._isRegionEntry for segment in lineSegments) 

2427 ): 

2428 counts[lineNumber] = max((wrapped._count if wrapped is not None else 0, *starts)) 

2429 

2430 # the lines up to the next segment continue the last segment of this line 

2431 wrapped = lineSegments[-1] 

2432 if wrapped._hasCount: 

2433 nextLineNumber = segments[index]._line if index < len(segments) else lineNumber + 1 

2434 for number in range(lineNumber + 1, nextLineNumber): 

2435 counts[number] = wrapped._count 

2436 

2437 partialLines = {key[0] for key, outcomes in branches.items() if 0 in outcomes} 

2438 for number in sorted(counts): 

2439 count = counts[number] 

2440 if count == 0: 

2441 status = LineCoverageStatus.Uncovered 

2442 elif number in partialLines: 

2443 status = LineCoverageStatus.PartiallyCovered 

2444 else: 

2445 status = LineCoverageStatus.Covered 

2446 

2447 cc_Line(number, status, count, parent=commonFile) 

2448 

2449 lines = commonFile._lines 

2450 for key in sorted(branches): 

2451 if key[0] < len(lines) and (line := lines[key[0]]) is not None: 2451 ↛ 2450line 2451 didn't jump to line 2450 because the condition on line 2451 was always true

2452 for count in branches[key]: 

2453 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=line) 

2454 

2455 def _ConvertUnits(self, commonFiles: dict[Path, cc_File], summary: CoverageSummary) -> None: 

2456 """ 

2457 Convert the files to source file units, and the functions to function units of their files. 

2458 

2459 A function local to a translation unit is named without the file name prefixing it. Functions of the same name in a 

2460 file - e.g. a header's inline function compiled in several translation units - become one unit, their counts 

2461 summed. 

2462 

2463 :param commonFiles: The files of the common model, by path. 

2464 :param summary: The report's root of the common model. 

2465 """ 

2466 functions: dict[tuple[Path, str], list[int]] = {} 

2467 for function in self._functions: 

2468 mainFileID = function.MainFileID 

2469 if mainFileID is None or (path := function._filePaths[mainFileID]) not in commonFiles: 2469 ↛ 2470line 2469 didn't jump to line 2470 because the condition on line 2469 was never true

2470 continue 

2471 

2472 name = function._name 

2473 if (local := match(r"(.+?\.[^.:;/\\]+)[:;](.+)", name)) is not None: 

2474 name = local[2] 

2475 

2476 regions = [region for region in function._regions if region._fileID == mainFileID] 

2477 firstLineNumber = min((region._lineStart for region in regions), default=0) 

2478 lastLineNumber = max((region._lineEnd for region in regions), default=0) 

2479 if (span := functions.get((path, name))) is None: 2479 ↛ 2482line 2479 didn't jump to line 2482 because the condition on line 2479 was always true

2480 functions[(path, name)] = [function._count, firstLineNumber, lastLineNumber] 

2481 else: 

2482 span[0] += function._count 

2483 span[1] = min(span[1], firstLineNumber) 

2484 span[2] = max(span[2], lastLineNumber) 

2485 

2486 sourceFiles: dict[Path, cc_SourceFile] = {} 

2487 for path, commonFile in commonFiles.items(): 

2488 lastLineNumber = commonFile._lastLineNumber 

2489 sourceFiles[path] = cc_SourceFile( 

2490 commonFile.Path.as_posix(), 

2491 file=commonFile, 

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

2493 endLine=commonFile._lines[lastLineNumber] if lastLineNumber > 0 else None, 

2494 parent=summary 

2495 ) 

2496 

2497 for (path, name), (count, firstLineNumber, lastLineNumber) in functions.items(): 

2498 commonFile = commonFiles[path] 

2499 lines = [line for line in commonFile._lines[firstLineNumber:lastLineNumber + 1] if line is not None] 

2500 cc_Function( 

2501 name, 

2502 file=commonFile, 

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

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

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

2506 coverageCount=count, 

2507 parent=sourceFiles[path] 

2508 )