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

1036 statements  

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

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

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

15# # 

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

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

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

19# # 

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

21# # 

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

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

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

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

26# limitations under the License. # 

27# # 

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

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

30# 

31# 

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

89from pyTooling.Common import getFullyQualifiedName, readResourceFile 

90from pyTooling.Decorators import export, readonly 

91from pyTooling.Exceptions import ToolingException 

92from pyTooling.MetaClasses import ExtendedType 

93from pyTooling.Stopwatch import Stopwatch 

94from pyTooling.Versioning import SemanticVersion 

95 

96from pyEDAA.Reports import Resources 

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

98from pyEDAA.Reports.CodeCoverage import CoverageSummary, DuplicateFileError 

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

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

101 

102 

103__all__ = ["SCHEMA"] 

104 

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

106 

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

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

109_Path = Path 

110 

111ParentType = TypeVar("ParentType") 

112 

113 

114@export 

115class RegionKind(Enum): 

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

117 

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

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

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

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

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

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

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

125 

126 

127@export 

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

129 """ 

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

131 root. 

132 

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

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

135 """ 

136 

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

138 

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

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

141 

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

143 """ 

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

145 

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

147 

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

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

150 """ 

151 if parent is None: 

152 self._parent = None 

153 self._root = None 

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

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

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

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

158 raise ex 

159 else: 

160 self._parent = parent 

161 self._root = parent._root 

162 

163 @readonly 

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

165 """ 

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

167 

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

169 """ 

170 return self._parent 

171 

172 @readonly 

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

174 """ 

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

176 

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

178 """ 

179 return self._root 

180 

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

182 """ 

183 Set the report's root of the element. 

184 

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

186 

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

188 """ 

189 self._root = root 

190 

191 

192@export 

193class Base(Element[ParentType]): 

194 """ 

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

196 """ 

197 

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

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

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

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

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

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

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

205 

206 def __init__( 

207 self, 

208 lineStart: int, 

209 columnStart: int, 

210 lineEnd: int, 

211 columnEnd: int, 

212 fileID: Nullable[int], 

213 expandedFileID: int, 

214 kind: RegionKind, 

215 *, 

216 parent: Nullable[ParentType] = None 

217 ) -> None: 

218 """ 

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

220 

221 :param lineStart: Line the region starts at. 

222 :param columnStart: Column the region starts at. 

223 :param lineEnd: Line the region ends at. 

224 :param columnEnd: Column the region ends at. 

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

226 stated. 

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

228 :param kind: The kind of the region. 

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

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

231 :raises ValueError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` is ``None``. 

232 :raises TypeError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` isn't of type 

233 :class:`int`. 

234 :raises ValueError: If parameter ``lineStart``, ``columnStart``, ``lineEnd`` or ``columnEnd`` is less than 1. 

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

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

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

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

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

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

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

242 :raises ValueError: If parameter ``fileID``, or ``expandedFileID`` of an expansion region, is no index into the 

243 file paths of parameter ``parent``. 

244 """ 

245 super().__init__(parent=parent) 

246 

247 for name, position in ( 

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

249 ): 

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

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

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

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

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

255 raise ex 

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

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

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

259 raise ex 

260 

261 if fileID is not None: 

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

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

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

265 raise ex 

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

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

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

269 raise ex 

270 

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

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

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

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

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

276 raise ex 

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

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

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

280 raise ex 

281 

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

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

284 elif not isinstance(kind, RegionKind): 

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

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

287 raise ex 

288 

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

290 for name, value in ( 

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

292 ): 

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

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

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

296 raise ex 

297 

298 self._lineStart = lineStart 

299 self._columnStart = columnStart 

300 self._lineEnd = lineEnd 

301 self._columnEnd = columnEnd 

302 self._fileID = fileID 

303 self._expandedFileID = expandedFileID 

304 self._kind = kind 

305 

306 @readonly 

307 def LineStart(self) -> int: 

308 """ 

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

310 

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

312 """ 

313 return self._lineStart 

314 

315 @readonly 

316 def ColumnStart(self) -> int: 

317 """ 

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

319 

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

321 """ 

322 return self._columnStart 

323 

324 @readonly 

325 def LineEnd(self) -> int: 

326 """ 

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

328 

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

330 """ 

331 return self._lineEnd 

332 

333 @readonly 

334 def ColumnEnd(self) -> int: 

335 """ 

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

337 

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

339 """ 

340 return self._columnEnd 

341 

342 @readonly 

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

344 """ 

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

346 

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

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

349 

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

351 format version 3.0.1. 

352 """ 

353 return self._fileID 

354 

355 @readonly 

356 def ExpandedFileID(self) -> int: 

357 """ 

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

359 

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

361 """ 

362 return self._expandedFileID 

363 

364 @readonly 

365 def Kind(self) -> RegionKind: 

366 """ 

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

368 

369 :returns: The kind. 

370 """ 

371 return self._kind 

372 

373 @readonly 

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

375 """ 

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

377 

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

379 region listed by a file lies in that file. 

380 

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

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

383 """ 

384 if self._parent is None: 

385 return None 

386 elif isinstance(self._parent, File): 

387 return self._parent._path 

388 elif self._fileID is None: 

389 return None 

390 

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

392 

393 @readonly 

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

395 """ 

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

397 

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

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

400 """ 

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

402 return None 

403 

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

405 

406 @readonly 

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

408 """ 

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

410 

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

412 function or expansion. 

413 """ 

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

415 return None 

416 

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

418 

419 @readonly 

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

421 """ 

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

423 

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

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

426 """ 

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

428 return None 

429 

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

431 

432 

433@export 

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

435 """ 

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

437 

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

439 """ 

440 

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

442 

443 def __init__( 

444 self, 

445 lineStart: int, 

446 columnStart: int, 

447 lineEnd: int, 

448 columnEnd: int, 

449 count: int, 

450 fileID: int, 

451 expandedFileID: int, 

452 kind: RegionKind, 

453 *, 

454 parent: Nullable[Function | Expansion] = None 

455 ) -> None: 

456 """ 

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

458 

459 :param lineStart: Line the region starts at. 

460 :param columnStart: Column the region starts at. 

461 :param lineEnd: Line the region ends at. 

462 :param columnEnd: Column the region ends at. 

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

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

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

466 :param kind: The kind of the region. 

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

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

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

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

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

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

473 """ 

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

475 

476 if fileID is None: 

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

478 

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

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

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

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

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

484 raise ex 

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

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

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

488 raise ex 

489 

490 self._count = count 

491 

492 if parent is not None: 

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

494 

495 @classmethod 

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

497 """ 

498 Parse a region from its JSON array. 

499 

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

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

502 :returns: The region. 

503 """ 

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

505 

506 @readonly 

507 def Count(self) -> int: 

508 """ 

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

510 

511 :returns: The count. 

512 """ 

513 return self._count 

514 

515 

516@export 

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

518 """ 

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

520 

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

522 kind]``. 

523 """ 

524 

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

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

527 

528 def __init__( 

529 self, 

530 lineStart: int, 

531 columnStart: int, 

532 lineEnd: int, 

533 columnEnd: int, 

534 trueCount: int, 

535 falseCount: int, 

536 fileID: int, 

537 expandedFileID: int, 

538 kind: RegionKind, 

539 *, 

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

541 ) -> None: 

542 """ 

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

544 

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

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

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

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

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

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

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

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

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

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

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

556 :class:`Expansion`. 

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

558 :raises ValueError: If parameter ``trueCount`` or ``falseCount`` is ``None``. 

559 :raises TypeError: If parameter ``trueCount`` or ``falseCount`` isn't of type :class:`int`. 

560 :raises ValueError: If parameter ``trueCount`` or ``falseCount`` is negative. 

561 """ 

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

563 

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

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

566 

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

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

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

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

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

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

573 raise ex 

574 elif value < 0: 

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

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

577 raise ex 

578 

579 self._trueCount = trueCount 

580 self._falseCount = falseCount 

581 

582 if parent is not None: 

583 parent._branches.append(self) 

584 

585 @classmethod 

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

587 """ 

588 Parse a branch region from its JSON array. 

589 

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

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

592 :returns: The branch region. 

593 """ 

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

595 

596 @readonly 

597 def TrueCount(self) -> int: 

598 """ 

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

600 

601 :returns: The count. 

602 """ 

603 return self._trueCount 

604 

605 @readonly 

606 def FalseCount(self) -> int: 

607 """ 

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

609 

610 :returns: The count. 

611 """ 

612 return self._falseCount 

613 

614 

615@export 

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

617 """ 

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

619 """ 

620 

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

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

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

624 

625 def __init__( 

626 self, 

627 conditions: Iterable[Nullable[bool]], 

628 executed: bool, 

629 result: Nullable[bool], 

630 *, 

631 parent: Nullable[MCDCRecord] = None 

632 ) -> None: 

633 """ 

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

635 

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

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

638 :param result: The outcome of the decision. 

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

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

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

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

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

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

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

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

647 """ 

648 super().__init__(parent=parent) 

649 

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

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

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

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

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

655 raise ex 

656 

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

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

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

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

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

662 raise ex 

663 

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

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

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

667 raise ex 

668 

669 self._conditions = [] 

670 self._executed = executed 

671 self._result = result 

672 

673 for condition in conditions: 

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

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

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

677 raise ex 

678 

679 self._conditions.append(condition) 

680 

681 if parent is not None: 

682 parent._testVectors.append(self) 

683 

684 @classmethod 

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

686 """ 

687 Parse a test vector from its JSON object. 

688 

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

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

691 :returns: The test vector. 

692 """ 

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

694 

695 @readonly 

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

697 """ 

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

699 

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

701 """ 

702 return self._conditions 

703 

704 @readonly 

705 def Executed(self) -> bool: 

706 """ 

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

708 

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

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

711 """ 

712 return self._executed 

713 

714 @readonly 

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

716 """ 

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

718 

719 :returns: The outcome. 

720 """ 

721 return self._result 

722 

723 

724@export 

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

726 """ 

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

728 independent effect on the outcome. 

729 

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

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

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

733 """ 

734 

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

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

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

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

739 

740 def __init__( 

741 self, 

742 lineStart: int, 

743 columnStart: int, 

744 lineEnd: int, 

745 columnEnd: int, 

746 fileID: Nullable[int], 

747 expandedFileID: int, 

748 kind: RegionKind, 

749 conditions: Iterable[bool], 

750 trueDecisions: Nullable[int] = None, 

751 falseDecisions: Nullable[int] = None, 

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

753 *, 

754 parent: Nullable[File | Function] = None 

755 ) -> None: 

756 """ 

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

758 

759 :param lineStart: Line the decision starts at. 

760 :param columnStart: Column the decision starts at. 

761 :param lineEnd: Line the decision ends at. 

762 :param columnEnd: Column the decision ends at. 

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

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

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

766 record. 

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

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

769 condition. 

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

771 Default: ``None``. 

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

773 Default: ``None``. 

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

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

776 ``None``. 

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

778 :class:`Function`. 

779 :raises TypeError: If parameter ``trueDecisions`` or ``falseDecisions`` isn't of type 

780 :class:`int`. 

781 :raises ValueError: If parameter ``trueDecisions`` or ``falseDecisions`` is negative. 

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

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

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

785 :class:`bool`. 

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

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

788 :class:`TestVector`. 

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

790 record. 

791 """ 

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

793 

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

795 if value is None: 

796 continue 

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

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

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

800 raise ex 

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

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

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

804 raise ex 

805 

806 if conditions is None: 

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

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

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

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

811 raise ex 

812 

813 self._trueDecisions = trueDecisions 

814 self._falseDecisions = falseDecisions 

815 self._conditions = [] 

816 self._testVectors = [] 

817 

818 for condition in conditions: 

819 if not isinstance(condition, bool): 

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

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

822 raise ex 

823 

824 self._conditions.append(condition) 

825 

826 if testVectors is not None: 

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

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

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

830 raise ex 

831 

832 for testVector in testVectors: 

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

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

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

836 raise ex 

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

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

839 

840 testVector._parent = self 

841 testVector._root = self._root 

842 self._testVectors.append(testVector) 

843 

844 if parent is not None: 

845 parent._mcdcRecords.append(self) 

846 

847 @classmethod 

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

849 """ 

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

851 

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

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

854 :returns: The MC/DC record. 

855 """ 

856 if len(record) == 7: 

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

858 elif len(record) == 9: 

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

860 

861 return cls( 

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

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

864 ) 

865 

866 @readonly 

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

868 """ 

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

870 

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

872 """ 

873 return self._trueDecisions 

874 

875 @readonly 

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

877 """ 

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

879 

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

881 """ 

882 return self._falseDecisions 

883 

884 @readonly 

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

886 """ 

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

888 (:attr:`_conditions`). 

889 

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

891 """ 

892 return self._conditions 

893 

894 @readonly 

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

896 """ 

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

898 

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

900 """ 

901 return self._testVectors 

902 

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

904 """ 

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

906 

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

908 """ 

909 self._root = root 

910 for testVector in self._testVectors: 

911 testVector._SetRoot(root) 

912 

913 

914@export 

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

916 """ 

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

918 

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

920 their parent. 

921 """ 

922 

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

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

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

926 _percent: float #: The coverage in percent. 

927 

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

929 """ 

930 Initialize counters. 

931 

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

933 :param covered: Number of the covered ones. 

934 :param percent: The coverage in percent. 

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

936 :raises ValueError: If parameter ``count`` or ``covered`` is ``None``. 

937 :raises TypeError: If parameter ``count`` or ``covered`` isn't of type :class:`int`. 

938 :raises ValueError: If parameter ``count`` or ``covered`` is negative. 

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

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

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

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

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

944 """ 

945 super().__init__() 

946 

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

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

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

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

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

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

953 raise ex 

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

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

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

957 raise ex 

958 

959 if notCovered is not None: 

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

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

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

963 raise ex 

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

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

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

967 raise ex 

968 

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

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

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

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

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

974 raise ex 

975 elif not 0 <= percent <= 100: 

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

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

978 raise ex 

979 

980 self._count = count 

981 self._covered = covered 

982 self._notCovered = notCovered 

983 self._percent = percent 

984 

985 @classmethod 

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

987 """ 

988 Parse counters from their JSON object. 

989 

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

991 :returns: The counters. 

992 """ 

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

994 

995 @readonly 

996 def Count(self) -> int: 

997 """ 

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

999 

1000 :returns: The number. 

1001 """ 

1002 return self._count 

1003 

1004 @readonly 

1005 def Covered(self) -> int: 

1006 """ 

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

1008 

1009 :returns: The number. 

1010 """ 

1011 return self._covered 

1012 

1013 @readonly 

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

1015 """ 

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

1017 

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

1019 """ 

1020 return self._notCovered 

1021 

1022 @readonly 

1023 def Percent(self) -> float: 

1024 """ 

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

1026 

1027 :returns: The coverage in percent. 

1028 """ 

1029 return self._percent 

1030 

1031 

1032@export 

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

1034 """ 

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

1036 

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

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

1039 """ 

1040 

1041 _lines: Counters #: The counters of lines. 

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

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

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

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

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

1047 

1048 def __init__( 

1049 self, 

1050 lines: Counters, 

1051 functions: Counters, 

1052 instantiations: Counters, 

1053 regions: Counters, 

1054 branches: Nullable[Counters] = None, 

1055 mcdc: Nullable[Counters] = None 

1056 ) -> None: 

1057 """ 

1058 Initialize a summary. 

1059 

1060 :param lines: The counters of lines. 

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

1062 :param instantiations: The counters of function instantiations. 

1063 :param regions: The counters of code regions. 

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

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

1066 :raises ValueError: If parameter ``lines``, ``functions``, ``instantiations`` or 

1067 ``regions`` is ``None``. 

1068 :raises TypeError: If parameter ``lines``, ``functions``, ``instantiations``, 

1069 ``regions``, ``branches`` or ``mcdc`` isn't of type :class:`Counters`. 

1070 :raises AlreadyInCoverageReportError: If parameter ``lines``, ``functions``, ``instantiations``, 

1071 ``regions``, ``branches`` or ``mcdc`` are the counters of another 

1072 summary. 

1073 """ 

1074 super().__init__() 

1075 

1076 for name, counters in ( 

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

1078 ): 

1079 if counters is None: 

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

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

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

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

1084 raise ex 

1085 

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

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

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

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

1090 raise ex 

1091 

1092 for name, counters in ( 

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

1094 ("branches", branches), ("mcdc", mcdc) 

1095 ): 

1096 if counters is None: 

1097 continue 

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

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

1100 

1101 counters._parent = self 

1102 

1103 self._lines = lines 

1104 self._functions = functions 

1105 self._instantiations = instantiations 

1106 self._regions = regions 

1107 self._branches = branches 

1108 self._mcdc = mcdc 

1109 

1110 @classmethod 

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

1112 """ 

1113 Parse a summary from its JSON object. 

1114 

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

1116 :returns: The summary. 

1117 """ 

1118 return cls( 

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

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

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

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

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

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

1125 ) 

1126 

1127 @readonly 

1128 def Lines(self) -> Counters: 

1129 """ 

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

1131 

1132 :returns: The counters. 

1133 """ 

1134 return self._lines 

1135 

1136 @readonly 

1137 def Functions(self) -> Counters: 

1138 """ 

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

1140 (:attr:`_functions`). 

1141 

1142 :returns: The counters. 

1143 """ 

1144 return self._functions 

1145 

1146 @readonly 

1147 def Instantiations(self) -> Counters: 

1148 """ 

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

1150 

1151 :returns: The counters. 

1152 """ 

1153 return self._instantiations 

1154 

1155 @readonly 

1156 def Regions(self) -> Counters: 

1157 """ 

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

1159 

1160 :returns: The counters. 

1161 """ 

1162 return self._regions 

1163 

1164 @readonly 

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

1166 """ 

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

1168 

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

1170 """ 

1171 return self._branches 

1172 

1173 @readonly 

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

1175 """ 

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

1177 

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

1179 """ 

1180 return self._mcdc 

1181 

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

1183 """ 

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

1185 

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

1187 """ 

1188 self._root = root 

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

1190 if counters is not None: 

1191 counters._SetRoot(root) 

1192 

1193 

1194_Summary = Summary 

1195 

1196 

1197@export 

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

1199 """ 

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

1201 

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

1203 ``isGapRegion``. 

1204 """ 

1205 

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

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

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

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

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

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

1212 

1213 def __init__( 

1214 self, 

1215 line: int, 

1216 column: int, 

1217 count: int, 

1218 hasCount: bool, 

1219 isRegionEntry: bool, 

1220 isGapRegion: bool = False, 

1221 *, 

1222 parent: Nullable[File] = None 

1223 ) -> None: 

1224 """ 

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

1226 

1227 :param line: Line the segment starts at. 

1228 :param column: Column the segment starts at. 

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

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

1231 :param isRegionEntry: Whether a region starts here. 

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

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

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

1235 :raises ValueError: If parameter ``line`` or ``column`` is ``None``. 

1236 :raises TypeError: If parameter ``line`` or ``column`` isn't of type :class:`int`. 

1237 :raises ValueError: If parameter ``line`` or ``column`` is less than 1. 

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

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

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

1241 :raises ValueError: If parameter ``hasCount``, ``isRegionEntry`` or ``isGapRegion`` is ``None``. 

1242 :raises TypeError: If parameter ``hasCount``, ``isRegionEntry`` or ``isGapRegion`` isn't of type :class:`bool`. 

1243 """ 

1244 super().__init__(parent=parent) 

1245 

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

1247 if position is None: 

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

1249 elif not isinstance(position, int): 

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

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

1252 raise ex 

1253 elif position < 1: 

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

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

1256 raise ex 

1257 

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

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

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

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

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

1263 raise ex 

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

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

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

1267 raise ex 

1268 

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

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

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

1272 elif not isinstance(flag, bool): 

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

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

1275 raise ex 

1276 

1277 self._line = line 

1278 self._column = column 

1279 self._count = count 

1280 self._hasCount = hasCount 

1281 self._isRegionEntry = isRegionEntry 

1282 self._isGapRegion = isGapRegion 

1283 

1284 if parent is not None: 

1285 parent._segments.append(self) 

1286 

1287 @classmethod 

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

1289 """ 

1290 Parse a segment from its JSON array. 

1291 

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

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

1294 :returns: The segment. 

1295 """ 

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

1297 

1298 @readonly 

1299 def Line(self) -> int: 

1300 """ 

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

1302 

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

1304 """ 

1305 return self._line 

1306 

1307 @readonly 

1308 def Column(self) -> int: 

1309 """ 

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

1311 

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

1313 """ 

1314 return self._column 

1315 

1316 @readonly 

1317 def Count(self) -> int: 

1318 """ 

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

1320 

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

1322 """ 

1323 return self._count 

1324 

1325 @readonly 

1326 def HasCount(self) -> bool: 

1327 """ 

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

1329 

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

1331 """ 

1332 return self._hasCount 

1333 

1334 @readonly 

1335 def IsRegionEntry(self) -> bool: 

1336 """ 

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

1338 

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

1340 """ 

1341 return self._isRegionEntry 

1342 

1343 @readonly 

1344 def IsGapRegion(self) -> bool: 

1345 """ 

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

1347 

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

1349 """ 

1350 return self._isGapRegion 

1351 

1352 

1353@export 

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

1355 """ 

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

1357 """ 

1358 

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

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

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

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

1363 

1364 def __init__( 

1365 self, 

1366 filePaths: Iterable[Path], 

1367 sourceRegion: Region, 

1368 targetRegions: Iterable[Region], 

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

1370 *, 

1371 parent: Nullable[File] = None 

1372 ) -> None: 

1373 """ 

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

1375 

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

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

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

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

1380 nested in it. Default: none. 

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

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

1383 :raises ValueError: If parameter ``filePaths`` or ``targetRegions`` is ``None``. 

1384 :raises TypeError: If parameter ``filePaths``, ``targetRegions`` or ``branches`` isn't 

1385 iterable. 

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

1387 :class:`~pathlib.Path`. 

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

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

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

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

1392 :class:`Region`. 

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

1394 :class:`BranchRegion`. 

1395 :raises AlreadyInCoverageReportError: If parameter ``targetRegions`` or ``branches`` contains a region of 

1396 another element. 

1397 :raises ValueError: If parameter ``sourceRegion`` is, or ``targetRegions`` or ``branches`` 

1398 contains, a region of a file ID or an expanded file ID out of range of 

1399 parameter ``filePaths``. 

1400 """ 

1401 super().__init__(parent=parent) 

1402 

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

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

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

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

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

1408 raise ex 

1409 

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

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

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

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

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

1415 raise ex 

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

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

1418 

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

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

1421 

1422 self._filePaths = [] 

1423 self._sourceRegion = sourceRegion 

1424 self._targetRegions = [] 

1425 self._branches = [] 

1426 

1427 for filePath in filePaths: 

1428 if not isinstance(filePath, Path): 

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

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

1431 raise ex 

1432 

1433 self._filePaths.append(filePath) 

1434 

1435 for name, elements, elementType, target in ( 

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

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

1438 ): 

1439 if elements is None: 

1440 continue 

1441 elif not isinstance(elements, Iterable): 

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

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

1444 raise ex 

1445 

1446 for element in elements: 

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

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

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

1450 raise ex 

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

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

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

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

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

1456 raise ex 

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

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

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

1460 raise ex 

1461 

1462 element._parent = self 

1463 element._root = self._root 

1464 target.append(element) 

1465 

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

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

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

1469 raise ex 

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

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

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

1473 raise ex 

1474 

1475 sourceRegion._parent = self 

1476 sourceRegion._root = self._root 

1477 

1478 if parent is not None: 

1479 parent._expansions.append(self) 

1480 

1481 @classmethod 

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

1483 """ 

1484 Parse an expansion from its JSON object. 

1485 

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

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

1488 :returns: The expansion. 

1489 """ 

1490 return cls( 

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

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

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

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

1495 parent=parent 

1496 ) 

1497 

1498 @readonly 

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

1500 """ 

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

1502 

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

1504 """ 

1505 return self._filePaths 

1506 

1507 @readonly 

1508 def SourceRegion(self) -> Region: 

1509 """ 

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

1511 

1512 :returns: The region. 

1513 """ 

1514 return self._sourceRegion 

1515 

1516 @readonly 

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

1518 """ 

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

1520 

1521 :returns: The regions. 

1522 """ 

1523 return self._targetRegions 

1524 

1525 @readonly 

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

1527 """ 

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

1529 (:attr:`_branches`). 

1530 

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

1532 """ 

1533 return self._branches 

1534 

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

1536 """ 

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

1538 

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

1540 """ 

1541 self._root = root 

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

1543 region._SetRoot(root) 

1544 

1545 

1546@export 

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

1548 """ 

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

1550 

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

1552 """ 

1553 

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

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

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

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

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

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

1560 

1561 def __init__( 

1562 self, 

1563 path: Path, 

1564 summary: Summary, 

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

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

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

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

1569 *, 

1570 parent: Nullable[Report] = None 

1571 ) -> None: 

1572 """ 

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

1574 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1589 :raises TypeError: If parameter ``segments``, ``branches``, ``mcdcRecords`` or 

1590 ``expansions`` isn't iterable. 

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

1592 :class:`Segment`. 

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

1594 :class:`BranchRegion`. 

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

1596 :class:`MCDCRecord`. 

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

1598 :class:`Expansion`. 

1599 :raises AlreadyInCoverageReportError: If parameter ``segments``, ``branches``, ``mcdcRecords`` or 

1600 ``expansions`` contains an element of another element. 

1601 """ 

1602 super().__init__(parent=parent) 

1603 

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

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

1606 elif not isinstance(path, Path): 

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

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

1609 raise ex 

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

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

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

1613 raise ex 

1614 

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

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

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

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

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

1620 raise ex 

1621 elif summary._parent is not None: 

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

1623 

1624 self._path = path 

1625 self._segments = [] 

1626 self._branches = [] 

1627 self._mcdcRecords = [] 

1628 self._expansions = [] 

1629 self._summary = summary 

1630 

1631 for name, elements, elementType, target in ( 

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

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

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

1635 ): 

1636 if elements is None: 

1637 continue 

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

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

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

1641 raise ex 

1642 

1643 for element in elements: 

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

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

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

1647 raise ex 

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

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

1650 

1651 element._parent = self 

1652 element._SetRoot(self._root) 

1653 

1654 target.append(element) 

1655 

1656 summary._parent = self 

1657 summary._SetRoot(self._root) 

1658 

1659 if parent is not None: 

1660 parent._files[path] = self 

1661 

1662 @classmethod 

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

1664 """ 

1665 Parse a file from its JSON object. 

1666 

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

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

1669 :returns: The file. 

1670 """ 

1671 return cls( 

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

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

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

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

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

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

1678 parent=parent 

1679 ) 

1680 

1681 @readonly 

1682 def Path(self) -> Path: 

1683 """ 

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

1685 

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

1687 """ 

1688 return self._path 

1689 

1690 @readonly 

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

1692 """ 

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

1694 

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

1696 """ 

1697 return self._segments 

1698 

1699 @readonly 

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

1701 """ 

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

1703 

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

1705 

1706 :returns: The branch regions. 

1707 """ 

1708 return self._branches 

1709 

1710 @readonly 

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

1712 """ 

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

1714 

1715 :returns: The MC/DC records. 

1716 """ 

1717 return self._mcdcRecords 

1718 

1719 @readonly 

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

1721 """ 

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

1723 

1724 :returns: The expansions. 

1725 """ 

1726 return self._expansions 

1727 

1728 @readonly 

1729 def Summary(self) -> Summary: 

1730 """ 

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

1732 

1733 :returns: The summary. 

1734 """ 

1735 return self._summary 

1736 

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

1738 """ 

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

1740 

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

1742 """ 

1743 self._root = root 

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

1745 element._SetRoot(root) 

1746 

1747 

1748@export 

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

1750 """ 

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

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

1753 """ 

1754 

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

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

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

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

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

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

1761 

1762 def __init__( 

1763 self, 

1764 name: str, 

1765 count: int, 

1766 regions: Iterable[Region], 

1767 filePaths: Iterable[Path], 

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

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

1770 *, 

1771 parent: Nullable[Report] = None 

1772 ) -> None: 

1773 """ 

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

1775 

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

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

1778 :param regions: The regions. 

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

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

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

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

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

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

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

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

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

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

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

1790 :raises ValueError: If parameter ``regions`` or ``filePaths`` is ``None``. 

1791 :raises TypeError: If parameter ``regions``, ``filePaths``, ``branches`` or 

1792 ``mcdcRecords`` isn't iterable. 

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

1794 :class:`Region`. 

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

1796 :class:`~pathlib.Path`. 

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

1798 :class:`BranchRegion`. 

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

1800 :class:`MCDCRecord`. 

1801 :raises AlreadyInCoverageReportError: If parameter ``regions``, ``branches`` or ``mcdcRecords`` contains a 

1802 region of another element. 

1803 :raises ValueError: If parameter ``regions``, ``branches`` or ``mcdcRecords`` contains a 

1804 region of a file ID or an expanded file ID out of range of parameter 

1805 ``filePaths``. 

1806 """ 

1807 super().__init__(parent=parent) 

1808 

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

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

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

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

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

1814 raise ex 

1815 elif name == "": 

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

1817 

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

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

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

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

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

1823 raise ex 

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

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

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

1827 raise ex 

1828 

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

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

1831 

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

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

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

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

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

1837 raise ex 

1838 

1839 self._name = name 

1840 self._count = count 

1841 self._regions = [] 

1842 self._branches = [] 

1843 self._mcdcRecords = [] 

1844 self._filePaths = [] 

1845 

1846 for filePath in filePaths: 

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

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

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

1850 raise ex 

1851 

1852 self._filePaths.append(filePath) 

1853 

1854 for parameterName, elements, elementType, target in ( 

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

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

1857 ): 

1858 if elements is None: 

1859 continue 

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

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

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

1863 raise ex 

1864 

1865 for element in elements: 

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

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

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

1869 raise ex 

1870 elif element._parent is not None: 

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

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

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

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

1875 raise ex 

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

1877 ex = ValueError( 

1878 f"Parameter '{parameterName}' contains an expansion region of an expanded file ID out of range." 

1879 ) 

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

1881 raise ex 

1882 

1883 element._parent = self 

1884 element._SetRoot(self._root) 

1885 

1886 target.append(element) 

1887 

1888 if parent is not None: 

1889 parent._functions.append(self) 

1890 

1891 @classmethod 

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

1893 """ 

1894 Parse a function from its JSON object. 

1895 

1896 :param function: The JSON object of the function. 

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

1898 :returns: The function. 

1899 """ 

1900 return cls( 

1901 function["name"], 

1902 function["count"], 

1903 [Region.Parse(region) for region in function["regions"]], 

1904 [Path(filename.replace("\\", "/")) for filename in function["filenames"]], 

1905 [BranchRegion.Parse(branch) for branch in function.get("branches", [])], 

1906 [MCDCRecord.Parse(record) for record in function.get("mcdc_records", [])], 

1907 parent=parent 

1908 ) 

1909 

1910 @readonly 

1911 def Name(self) -> str: 

1912 """ 

1913 Read-only property to access the function's name (:attr:`_name`). 

1914 

1915 :returns: The name, as the profile names it: mangled, and prefixed by the file name of its translation unit, if 

1916 local to it - e.g. ``Statistics.c:Square``. 

1917 """ 

1918 return self._name 

1919 

1920 @readonly 

1921 def Count(self) -> int: 

1922 """ 

1923 Read-only property to access how often the function was called (:attr:`_count`). 

1924 

1925 :returns: The count. 

1926 """ 

1927 return self._count 

1928 

1929 @readonly 

1930 def Regions(self) -> list[Region]: 

1931 """ 

1932 Read-only property to access the regions (:attr:`_regions`). 

1933 

1934 :returns: The regions. 

1935 """ 

1936 return self._regions 

1937 

1938 @readonly 

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

1940 """ 

1941 Read-only property to access the branch regions (:attr:`_branches`). 

1942 

1943 :returns: The branch regions, also those in macro expansions. 

1944 """ 

1945 return self._branches 

1946 

1947 @readonly 

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

1949 """ 

1950 Read-only property to access the MC/DC records (:attr:`_mcdcRecords`). 

1951 

1952 :returns: The MC/DC records. 

1953 """ 

1954 return self._mcdcRecords 

1955 

1956 @readonly 

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

1958 """ 

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

1960 

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

1962 """ 

1963 return self._filePaths 

1964 

1965 @readonly 

1966 def MainFileID(self) -> Nullable[int]: 

1967 """ 

1968 Read-only property to return the file ID of the file the function is in: the first file no expansion region 

1969 expands to. 

1970 

1971 :returns: The index into :attr:`FilePaths`, or ``None`` if every file is expanded to. 

1972 """ 

1973 expanded = {region._expandedFileID for region in self._regions if region._kind is RegionKind.Expansion} 

1974 return next((fileID for fileID in range(len(self._filePaths)) if fileID not in expanded), None) 

1975 

1976 @readonly 

1977 def MainFilePath(self) -> Nullable[Path]: 

1978 """ 

1979 Read-only property to return the path of the file the function is in (:attr:`MainFileID`). 

1980 

1981 :returns: The path, as the compiler named it; ``None`` if every file is expanded to. 

1982 """ 

1983 return None if (mainFileID := self.MainFileID) is None else self._filePaths[mainFileID] 

1984 

1985 @readonly 

1986 def MainFile(self) -> Nullable[File]: 

1987 """ 

1988 Read-only property to return the file the function is in, as the report lists it. 

1989 

1990 :returns: The file of :attr:`MainFilePath` in the report's files; ``None`` if the function isn't part of a report, 

1991 every file is expanded to, or the report doesn't list the file. 

1992 """ 

1993 if self._root is None or (path := self.MainFilePath) is None: 1993 ↛ 1994line 1993 didn't jump to line 1994 because the condition on line 1993 was never true

1994 return None 

1995 

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

1997 

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

1999 """ 

2000 Set the report's root of the function, and of its regions, branch regions and MC/DC records. 

2001 

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

2003 """ 

2004 self._root = root 

2005 for element in (*self._regions, *self._branches, *self._mcdcRecords): 

2006 element._SetRoot(root) 

2007 

2008 

2009@export 

2010class Report(metaclass=ExtendedType, mixin=True): 

2011 """ 

2012 The report's root: the format version, the files, the functions, and the totals. 

2013 

2014 It is the root of every element below it. 

2015 """ 

2016 

2017 _root: Report #: The report's root: the report itself. 

2018 _version: Nullable[SemanticVersion] #: Version of the report format. 

2019 _files: dict[Path, File] #: The files, by path. 

2020 _functions: list[Function] #: The functions. 

2021 _totals: Nullable[Summary] #: The counters of the whole report. 

2022 

2023 def __init__( 

2024 self, 

2025 version: Nullable[SemanticVersion] = None, 

2026 files: Nullable[Iterable[File]] = None, 

2027 functions: Nullable[Iterable[Function]] = None, 

2028 totals: Nullable[Summary] = None 

2029 ) -> None: 

2030 """ 

2031 Initialize a report; empty, without parameters. 

2032 

2033 :param version: Optional, version of the report format. Default: ``None``. 

2034 :param files: Optional, the files. Default: none. 

2035 :param functions: Optional, the functions. Default: none. 

2036 :param totals: Optional, the counters of the whole report. Default: ``None``. 

2037 :raises TypeError: If parameter ``version`` isn't of type 

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

2039 :raises TypeError: If parameter ``files`` or ``functions`` isn't iterable. 

2040 :raises TypeError: If parameter ``files`` contains an element not of type :class:`File`. 

2041 :raises DuplicateFileError: If parameter ``files`` contains two files of the same path. 

2042 :raises TypeError: If parameter ``functions`` contains an element not of type 

2043 :class:`Function`. 

2044 :raises AlreadyInCoverageReportError: If parameter ``files`` or ``functions`` contains an element of another 

2045 report. 

2046 :raises TypeError: If parameter ``totals`` isn't of type :class:`Summary`. 

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

2048 """ 

2049 if version is not None and not isinstance(version, SemanticVersion): 

2050 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") 

2051 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

2052 raise ex 

2053 

2054 if totals is not None and not isinstance(totals, Summary): 2054 ↛ 2055line 2054 didn't jump to line 2055 because the condition on line 2054 was never true

2055 ex = TypeError("Parameter 'totals' is not of type 'Summary'.") 

2056 ex.add_note(f"Got type '{getFullyQualifiedName(totals)}'.") 

2057 raise ex 

2058 elif totals is not None and totals._parent is not None: 2058 ↛ 2059line 2058 didn't jump to line 2059 because the condition on line 2058 was never true

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

2060 

2061 self._root = self 

2062 self._version = version 

2063 self._files = {} 

2064 self._functions = [] 

2065 self._totals = totals 

2066 

2067 if files is not None: 

2068 if not isinstance(files, Iterable): 2068 ↛ 2069line 2068 didn't jump to line 2069 because the condition on line 2068 was never true

2069 ex = TypeError("Parameter 'files' is not iterable.") 

2070 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.") 

2071 raise ex 

2072 

2073 for file in files: 

2074 if not isinstance(file, File): 

2075 ex = TypeError("Parameter 'files' contains an element not of type 'File'.") 

2076 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

2077 raise ex 

2078 elif file._parent is not None: 

2079 raise AlreadyInCoverageReportError("Parameter 'files' contains a file of another report.") 

2080 elif file._path in self._files: 

2081 ex = DuplicateFileError("Parameter 'files' contains two files of the same path.") 

2082 ex.add_note(f"Got value '{file._path.as_posix()}'.") 

2083 raise ex 

2084 

2085 file._parent = self 

2086 file._SetRoot(self) 

2087 

2088 self._files[file._path] = file 

2089 

2090 if functions is not None: 

2091 if not isinstance(functions, Iterable): 2091 ↛ 2092line 2091 didn't jump to line 2092 because the condition on line 2091 was never true

2092 ex = TypeError("Parameter 'functions' is not iterable.") 

2093 ex.add_note(f"Got type '{getFullyQualifiedName(functions)}'.") 

2094 raise ex 

2095 

2096 for function in functions: 

2097 if not isinstance(function, Function): 2097 ↛ 2098line 2097 didn't jump to line 2098 because the condition on line 2097 was never true

2098 ex = TypeError("Parameter 'functions' contains an element not of type 'Function'.") 

2099 ex.add_note(f"Got type '{getFullyQualifiedName(function)}'.") 

2100 raise ex 

2101 elif function._parent is not None: 2101 ↛ 2102line 2101 didn't jump to line 2102 because the condition on line 2101 was never true

2102 raise AlreadyInCoverageReportError("Parameter 'functions' contains a function of another report.") 

2103 

2104 function._parent = self 

2105 function._SetRoot(self) 

2106 

2107 self._functions.append(function) 

2108 

2109 if totals is not None: 

2110 totals._parent = self 

2111 totals._SetRoot(self) 

2112 

2113 @readonly 

2114 def Version(self) -> Nullable[SemanticVersion]: 

2115 """ 

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

2117 

2118 :returns: The version, e.g. ``2.0.1``; ``None`` before the report was converted. 

2119 """ 

2120 return self._version 

2121 

2122 @readonly 

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

2124 """ 

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

2126 

2127 :returns: The files, by path. 

2128 """ 

2129 return self._files 

2130 

2131 @readonly 

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

2133 """ 

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

2135 

2136 :returns: The functions; empty in a report written with ``-skip-functions`` or ``-summary-only``. 

2137 """ 

2138 return self._functions 

2139 

2140 @readonly 

2141 def Totals(self) -> Nullable[Summary]: 

2142 """ 

2143 Read-only property to access the counters of the whole report (:attr:`_totals`). 

2144 

2145 :returns: The totals; ``None`` before the report was converted. 

2146 """ 

2147 return self._totals 

2148 

2149 

2150TestVector._PARENT_TYPE = (MCDCRecord, ) 

2151Region._PARENT_TYPE = (Function, Expansion) 

2152BranchRegion._PARENT_TYPE = (File, Function, Expansion) 

2153MCDCRecord._PARENT_TYPE = (File, Function) 

2154Counters._PARENT_TYPE = (Summary, ) 

2155Summary._PARENT_TYPE = (File, Report) 

2156Segment._PARENT_TYPE = (File, ) 

2157Expansion._PARENT_TYPE = (File, ) 

2158File._PARENT_TYPE = (Report, ) 

2159Function._PARENT_TYPE = (Report, ) 

2160 

2161 

2162@export 

2163class Document(cc_Document, Report): 

2164 """ 

2165 An LLVM JSON code coverage export: read into the format's model, and converted to the common model. 

2166 """ 

2167 

2168 _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`. 

2169 

2170 def __init__( 

2171 self, 

2172 jsonReportFile: Path, 

2173 analyzeAndConvert: bool = False, 

2174 *, 

2175 version: Nullable[SemanticVersion] = None, 

2176 files: Nullable[Iterable[File]] = None, 

2177 functions: Nullable[Iterable[Function]] = None, 

2178 totals: Nullable[Summary] = None 

2179 ) -> None: 

2180 """ 

2181 Initialize the report, and optionally read it; or build it from typed values. 

2182 

2183 :param jsonReportFile: Path to the JSON file. 

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

2185 :param version: Optional, version of the report format. Default: ``None``. 

2186 :param files: Optional, the files. Default: none. 

2187 :param functions: Optional, the functions. Default: none. 

2188 :param totals: Optional, the counters of the whole report. Default: ``None``. 

2189 :raises TypeError: If parameter ``version`` isn't of type :class:`~pyTooling.Versioning.SemanticVersion`. 

2190 :raises TypeError: If parameter ``files`` or ``functions`` isn't iterable. 

2191 :raises TypeError: If parameter ``files`` contains an element not of type 

2192 :class:`File`. 

2193 :raises TypeError: If parameter ``functions`` contains an element not of type 

2194 :class:`Function`. 

2195 :raises TypeError: If parameter ``totals`` isn't of type 

2196 :class:`Summary`. 

2197 """ 

2198 super().__init__(jsonReportFile) 

2199 Report.__init__(self, version, files, functions, totals) 

2200 

2201 self._jsonDocument = None 

2202 

2203 if analyzeAndConvert: 

2204 self.Analyze() 

2205 self.Convert() 

2206 

2207 def Analyze(self) -> None: 

2208 """ 

2209 Parse the JSON file and validate it against the JSON Schema :data:`SCHEMA`. 

2210 

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

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

2213 :raises CodeCoverageError: If the JSON Schema can't be read. 

2214 :raises CodeCoverageError: If the file isn't valid according to the JSON Schema. 

2215 """ 

2216 if not self._path.exists(): 

2217 raise CodeCoverageError(f"LLVM coverage export file '{self._path}' does not exist.") \ 

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

2219 

2220 with Stopwatch() as sw: 

2221 try: 

2222 jsonDocument = loads(self._path.read_text(encoding="utf-8")) 

2223 except JSONDecodeError as ex: 

2224 raise CodeCoverageError(f"JSON syntax error in LLVM coverage export file '{self._path}'.") from ex 

2225 

2226 try: 

2227 schema = loads(readResourceFile(Resources, SCHEMA)) 

2228 except (ToolingException, JSONDecodeError) as ex: 

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

2230 

2231 errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path)) 

2232 if len(errors) > 0: 

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

2234 for error in errors: 

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

2236 raise ex 

2237 

2238 self._jsonDocument = jsonDocument 

2239 

2240 self._analysisDuration = sw.Duration 

2241 

2242 def Convert(self) -> None: 

2243 """ 

2244 Convert the parsed and validated JSON document to the format's model. 

2245 

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

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

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

2249 """ 

2250 if self._jsonDocument is None: 

2251 ex = CodeCoverageError( 

2252 f"LLVM coverage export file '{self._path}' needs to be read and analyzed by a JSON parser." 

2253 ) 

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

2255 raise ex 

2256 

2257 with Stopwatch() as sw: 

2258 data = self._jsonDocument["data"][0] 

2259 self._version = SemanticVersion.Parse(self._jsonDocument["version"]) 

2260 self._files = {} 

2261 self._functions = [] 

2262 self._totals = Summary.Parse(data["totals"]) 

2263 

2264 self._totals._parent = self 

2265 self._totals._SetRoot(self) 

2266 

2267 for record in data["files"]: 

2268 File.Parse(record, parent=self) 

2269 

2270 for function in data.get("functions", []): 

2271 Function.Parse(function, parent=self) 

2272 

2273 self._conversionDuration = sw.Duration 

2274 

2275 def ToCoverageSummary(self) -> CoverageSummary: 

2276 """ 

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

2278 

2279 :returns: The report's root of the common model, named after the report file. 

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

2281 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. 

2282 """ 

2283 parents = [path.parent.parts for path in self._files] 

2284 first = min(parents, default=()) 

2285 last = max(parents, default=()) 

2286 common = first 

2287 for index, part in enumerate(first): 2287 ↛ 2292line 2287 didn't jump to line 2292 because the loop on line 2287 didn't complete

2288 if part != last[index]: 

2289 common = first[:index] 

2290 break 

2291 

2292 summary = CoverageSummary(self._path.stem, sourceDirectories=[Path(*common)] if len(common) > 0 else []) 

2293 commonFiles = {path: summary.GetOrAddFile("/".join(path.parts[len(common):])) for path in self._files} 

2294 

2295 branches = self._CollectBranches() 

2296 for path, file in self._files.items(): 

2297 self._ConvertLines(file, commonFiles[path], branches[path]) 

2298 

2299 self._ConvertUnits(commonFiles, summary) 

2300 

2301 summary.Aggregate() 

2302 return summary 

2303 

2304 def _CollectBranches(self) -> dict[Path, dict[tuple[Any, ...], list[int]]]: 

2305 """ 

2306 Collect the branch regions of the functions by file; a branch region found in several functions is summed. 

2307 

2308 A branch region is collected at the line it starts at, or at the line its macro is expanded at. It is found in 

2309 several functions e.g. in the instantiations of a template. 

2310 

2311 :returns: By path, the summed true and false counts of each branch region, keyed by the line 

2312 number first. 

2313 :raises CodeCoverageError: If a function has a branch region in a file, which none of its regions expands to. 

2314 """ 

2315 branches: dict[Path, dict[tuple[Any, ...], list[int]]] = {path: {} for path in self._files} 

2316 for function in self._functions: 

2317 mainFileID = function.MainFileID 

2318 if mainFileID is None or (fileBranches := branches.get(function._filePaths[mainFileID])) is None: 2318 ↛ 2319line 2318 didn't jump to line 2319 because the condition on line 2318 was never true

2319 continue 

2320 

2321 expansions = { 

2322 region._expandedFileID: region for region in function._regions if region._kind is RegionKind.Expansion 

2323 } 

2324 for branch in function._branches: 

2325 lineNumber = branch._lineStart 

2326 fileID = branch._fileID 

2327 while fileID != mainFileID: 

2328 if (expansion := expansions.get(fileID)) is None: 

2329 ex = CodeCoverageError(f"Function '{function._name}' has a branch region in a file no region expands to.") 

2330 ex.add_note(f"Got file ID {fileID} of file '{function._filePaths[fileID]}'.") 

2331 raise ex 

2332 

2333 lineNumber = expansion._lineStart 

2334 fileID = expansion._fileID 

2335 

2336 key = ( 

2337 lineNumber, function._filePaths[branch._fileID], 

2338 branch._lineStart, branch._columnStart, branch._lineEnd, branch._columnEnd 

2339 ) 

2340 if (counts := fileBranches.get(key)) is None: 

2341 fileBranches[key] = [branch._trueCount, branch._falseCount] 

2342 else: 

2343 counts[0] += branch._trueCount 

2344 counts[1] += branch._falseCount 

2345 

2346 return branches 

2347 

2348 @staticmethod 

2349 def _ConvertLines(file: File, commonFile: cc_File, branches: dict[tuple[Any, ...], list[int]]) -> None: 

2350 """ 

2351 Convert a file's segments to lines of the common model, and add the branches of the file's branch regions. 

2352 

2353 A line's count is derived as ``llvm-cov show`` derives it: of the segments starting on the line, and of the last 

2354 segment of a previous line, which continues on it. A branch region of a line, which isn't executable, is left out. 

2355 

2356 :param file: The file of the format's model. 

2357 :param commonFile: The file of the common model. 

2358 :param branches: The true and false counts of each branch region of the file, keyed by the line number first. 

2359 """ 

2360 counts: dict[int, int] = {} 

2361 wrapped: Nullable[Segment] = None 

2362 segments = file._segments 

2363 index = 0 

2364 while index < len(segments): 

2365 lineNumber = segments[index]._line 

2366 lineSegments = [] 

2367 while index < len(segments) and segments[index]._line == lineNumber: 

2368 lineSegments.append(segments[index]) 

2369 index += 1 

2370 

2371 starts = [s._count for s in lineSegments if s._hasCount and s._isRegionEntry and not s._isGapRegion] 

2372 skipped = not lineSegments[0]._hasCount and lineSegments[0]._isRegionEntry 

2373 if ( 

2374 (not skipped and ((wrapped is not None and wrapped._hasCount) or len(starts) > 0)) or 

2375 any(segment._hasCount and segment._isRegionEntry for segment in lineSegments) 

2376 ): 

2377 counts[lineNumber] = max((wrapped._count if wrapped is not None else 0, *starts)) 

2378 

2379 # the lines up to the next segment continue the last segment of this line 

2380 wrapped = lineSegments[-1] 

2381 if wrapped._hasCount: 

2382 nextLineNumber = segments[index]._line if index < len(segments) else lineNumber + 1 

2383 for number in range(lineNumber + 1, nextLineNumber): 

2384 counts[number] = wrapped._count 

2385 

2386 partialLines = {key[0] for key, outcomes in branches.items() if 0 in outcomes} 

2387 for number in sorted(counts): 

2388 count = counts[number] 

2389 if count == 0: 

2390 status = LineCoverageStatus.Uncovered 

2391 elif number in partialLines: 

2392 status = LineCoverageStatus.PartiallyCovered 

2393 else: 

2394 status = LineCoverageStatus.Covered 

2395 

2396 cc_Line(number, status, count, parent=commonFile) 

2397 

2398 lines = commonFile._lines 

2399 for key in sorted(branches): 

2400 if key[0] < len(lines) and (line := lines[key[0]]) is not None: 2400 ↛ 2399line 2400 didn't jump to line 2399 because the condition on line 2400 was always true

2401 for count in branches[key]: 

2402 cc_Branch(LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered, count, parent=line) 

2403 

2404 def _ConvertUnits(self, commonFiles: dict[Path, cc_File], summary: CoverageSummary) -> None: 

2405 """ 

2406 Convert the files to source file units, and the functions to function units of their files. 

2407 

2408 A function local to a translation unit is named without the file name prefixing it. Functions of the same name in a 

2409 file - e.g. a header's inline function compiled in several translation units - become one unit, their counts 

2410 summed. 

2411 

2412 :param commonFiles: The files of the common model, by path. 

2413 :param summary: The report's root of the common model. 

2414 """ 

2415 functions: dict[tuple[Path, str], list[int]] = {} 

2416 for function in self._functions: 

2417 mainFileID = function.MainFileID 

2418 if mainFileID is None or (path := function._filePaths[mainFileID]) not in commonFiles: 2418 ↛ 2419line 2418 didn't jump to line 2419 because the condition on line 2418 was never true

2419 continue 

2420 

2421 name = function._name 

2422 if (local := match(r"(.+?\.[^.:;/\\]+)[:;](.+)", name)) is not None: 

2423 name = local[2] 

2424 

2425 regions = [region for region in function._regions if region._fileID == mainFileID] 

2426 firstLineNumber = min((region._lineStart for region in regions), default=0) 

2427 lastLineNumber = max((region._lineEnd for region in regions), default=0) 

2428 if (span := functions.get((path, name))) is None: 2428 ↛ 2431line 2428 didn't jump to line 2431 because the condition on line 2428 was always true

2429 functions[(path, name)] = [function._count, firstLineNumber, lastLineNumber] 

2430 else: 

2431 span[0] += function._count 

2432 span[1] = min(span[1], firstLineNumber) 

2433 span[2] = max(span[2], lastLineNumber) 

2434 

2435 sourceFiles: dict[Path, cc_SourceFile] = {} 

2436 for path, commonFile in commonFiles.items(): 

2437 lastLineNumber = commonFile._lastLineNumber 

2438 sourceFiles[path] = cc_SourceFile( 

2439 commonFile.Path.as_posix(), 

2440 file=commonFile, 

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

2442 endLine=commonFile._lines[lastLineNumber] if lastLineNumber > 0 else None, 

2443 parent=summary 

2444 ) 

2445 

2446 for (path, name), (count, firstLineNumber, lastLineNumber) in functions.items(): 

2447 commonFile = commonFiles[path] 

2448 lines = [line for line in commonFile._lines[firstLineNumber:lastLineNumber + 1] if line is not None] 

2449 cc_Function( 

2450 name, 

2451 file=commonFile, 

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

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

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

2455 coverageCount=count, 

2456 parent=sourceFiles[path] 

2457 )