Coverage for pyEDAA/Reports/CodeCoverage/QuestaSim.py: 97%

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

32QuestaSim's coverage report XML: a model of the format, read from a report and converted to the common model. 

33 

34Questa SIM keeps coverage in a coverage database (UCDB), whose format is proprietary. The command 

35``vcover report -xml -details`` - or ``coverage report -xml -details`` in a simulation - writes a report of it as XML, 

36with the coverage items of each instance: 

37 

38.. code-block:: bash 

39 

40 vcover report -xml -details -output CoverageReport.xml Testsuite.ucdb 

41 

42The XML's root is ``<coverage_report>``, without namespace and without format version; it isn't the XML interchange 

43format of the UCIS standard. Questa states its version - e.g. ``2026.2`` - and the command, which wrote the report. A 

44report is validated against :file:`QuestaSim-Coverage.xsd` (:data:`SCHEMA`). 

45 

46The format's model keeps what the report states: a :class:`Document` holds the report's mode (:class:`ReportMode`) and 

47its scopes - instances, design units or source files (:class:`Scope`) -, each with its coverage statistics 

48(:class:`Statistics`), its source files and its coverage items: the statements, the ``if`` and ``case`` statements with 

49their branches, and the states and transitions of finite state machines. Each element's constructor takes typed values, 

50so the model can be built by hand: an element below the report names its parent with the keyword parameter ``parent`` 

51and is added to it. Its class method ``Parse`` reads the element's XML element. 

52 

53* A coverage statistics element states how many coverage items of a kind - e.g. statements or branches - a scope or a 

54 summary record (:class:`Totals`) has, and how many of them were hit. 

55* Which kind of scope a report lists, its mode decides: :class:`InstanceData`, :class:`DesignUnitData` or 

56 :class:`FileData`. With details (``vcover report -details``), a scope names its source files in a source table and 

57 lists its coverage items. 

58* ``-code`` chooses the kinds of coverage items, e.g. ``-code bcesf`` for branches, conditions, expressions, statements 

59 and finite state machines. A coverage item - :class:`Statement`, :class:`IfBranch`, :class:`CaseBranch` - names its 

60 source file by the number the scope's source table gives it, its line, and its index among the items of its kind in 

61 that line. 

62* Unlike the other coverage items, a state (:class:`FSMState`) and a transition (:class:`FSMTransition`) of a finite 

63 state machine name no file number: their file is the scope's, if the scope has a single file. 

64 

65:meth:`Document.ToCoverageSummary` converts the statements and branches to the common model of 

66:mod:`pyEDAA.Reports.CodeCoverage`: 

67 

68* Coverage items are identified by their file, their line and their index in the line. The items of all scopes are 

69 merged: an item of a design unit with several instances counts as often as in all instances together. 

70* A line ran as often as its least often run statement or branching statement. An ``if`` or ``case`` statement adds 

71 its branches to the line of its first branch - the ``if``, or the first case item -, each taken as often as Questa 

72 counted. 

73* A line, which ran, is covered, if all its branches were taken, otherwise partially covered. 

74 

75The coverage statistics, the states and transitions of finite state machines, conditions, expressions and toggles 

76aren't converted. 

77 

78.. rubric:: Example 

79 

80.. code-block:: Python 

81 

82 from pathlib import Path 

83 from pyEDAA.Reports.CodeCoverage.QuestaSim import Document 

84 

85 report = Document(Path("CoverageReport.xml"), analyzeAndConvert=True) 

86 summary = report.ToCoverageSummary() 

87 print(f"Lines: {summary.CoveredLines} of {summary.TotalLines}") 

88 print(f"Branches: {summary.CoveredBranches} of {summary.TotalBranches}") 

89""" 

90from __future__ import annotations 

91 

92from io import BytesIO 

93from pathlib import Path 

94from typing import Generator, Mapping, Optional as Nullable, Self 

95 

96from lxml.etree import XMLParser, XMLSchema, XMLSchemaParseError, XMLSyntaxError 

97from lxml.etree import _Element, _ElementTree, parse 

98from pyTooling.Common import getFullyQualifiedName, getResourceFile, StringEnum 

99from pyTooling.Decorators import export, readonly 

100from pyTooling.Exceptions import ToolingException 

101from pyTooling.MetaClasses import ExtendedType 

102from pyTooling.Stopwatch import Stopwatch 

103 

104from pyEDAA.Reports import Resources 

105from pyEDAA.Reports.CodeCoverage import CodeCoverageError, CoverageSummary, Document as cc_Document 

106from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, Line as cc_Line, LineCoverageStatus 

107 

108 

109__all__ = ["SCHEMA"] 

110 

111SCHEMA = "QuestaSim-Coverage.xsd" #: The XML schema a report is validated against. 

112 

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

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

115_Path = Path 

116 

117 

118@export 

119class ReportMode(StringEnum): 

120 """ 

121 How a report groups its coverage data, as the attribute of ``<code_coverage_report>`` set to ``1`` states. 

122 """ 

123 

124 ByInstance = "byInstance" #: Per instance (``<instanceData>``), ``vcover report -setdefault byinstance``. 

125 ByDesignUnit = "byDU" #: Per design unit (``<DuData>``), ``vcover report -setdefault bydu``. 

126 ByFile = "byFile" #: Per source file (``<fileData>``), ``vcover report -setdefault byfile``. 

127 

128 

129@export 

130class CoverageKind(StringEnum): 

131 """ 

132 Kind of coverage items a coverage statistics element counts, as its element name states. 

133 """ 

134 

135 Statements = "statements" #: Statements. 

136 Branches = "branches" #: Branches of ``if`` and ``case`` statements. 

137 Conditions = "conditions" #: Conditions. 

138 Expressions = "expressions" #: Expressions. 

139 States = "states" #: States of finite state machines. 

140 Transitions = "transitions" #: Transitions of finite state machines. 

141 Toggles = "toggleSummary" #: Signal toggles. 

142 

143 

144@export 

145class Statistics(metaclass=ExtendedType, slots=True): 

146 """ 

147 A coverage statistics element of a scope or a summary record: how many coverage items of a kind it has, and how many 

148 of them were hit. 

149 

150 The element is named after the kind of coverage items, e.g. ``<statements>`` or ``<branches>``. 

151 """ 

152 

153 _parent: Nullable[StatisticsMixin] #: The scope or summary record the coverage statistics belong to. 

154 _kind: CoverageKind #: Kind of the coverage items. 

155 _active: int #: Number of coverage items. 

156 _hits: int #: Number of coverage items, which were hit. 

157 _percent: float #: Percentage of coverage items, which were hit, as the report states it. 

158 

159 def __init__( 

160 self, 

161 kind: CoverageKind, 

162 active: int, 

163 hits: int, 

164 percent: float, 

165 *, 

166 parent: Nullable[StatisticsMixin] = None 

167 ) -> None: 

168 """ 

169 Initialize the coverage statistics, and add them to the coverage statistics of their element. 

170 

171 :param kind: Kind of the coverage items. 

172 :param active: Number of coverage items. 

173 :param hits: Number of coverage items, which were hit. 

174 :param percent: Percentage of coverage items, which were hit, as the report states it. 

175 :param parent: Optional, the scope or summary record the coverage statistics belong to; they are added 

176 to its coverage statistics by :attr:`Kind`. Default: ``None``. 

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

178 :raises TypeError: If parameter ``kind`` isn't of type :class:`CoverageKind`. 

179 :raises ValueError: If parameter ``active`` is ``None``. 

180 :raises TypeError: If parameter ``active`` isn't of type :class:`int`. 

181 :raises ValueError: If parameter ``active`` is negative. 

182 :raises ValueError: If parameter ``hits`` is ``None``. 

183 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`. 

184 :raises ValueError: If parameter ``hits`` is negative or greater than parameter ``active``. 

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

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

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

188 :raises TypeError: If parameter ``parent`` isn't of type :class:`StatisticsMixin`. 

189 :raises CodeCoverageError: If the parent already has coverage statistics of this kind. 

190 """ 

191 if kind is None: 

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

193 elif not isinstance(kind, CoverageKind): 

194 ex = TypeError("Parameter 'kind' is not of type 'CoverageKind'.") 

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

196 raise ex 

197 

198 if active is None: 

199 raise ValueError("Parameter 'active' is None.") 

200 elif not isinstance(active, int): 

201 ex = TypeError("Parameter 'active' is not of type 'int'.") 

202 ex.add_note(f"Got type '{getFullyQualifiedName(active)}'.") 

203 raise ex 

204 elif active < 0: 

205 ex = ValueError("Parameter 'active' is negative.") 

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

207 raise ex 

208 

209 if hits is None: 

210 raise ValueError("Parameter 'hits' is None.") 

211 elif not isinstance(hits, int): 

212 ex = TypeError("Parameter 'hits' is not of type 'int'.") 

213 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.") 

214 raise ex 

215 elif not (0 <= hits <= active): 

216 ex = ValueError(f"Parameter 'hits' is out of range 0..{active}.") 

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

218 raise ex 

219 

220 if percent is None: 

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

222 elif not isinstance(percent, (float, int)): 

223 ex = TypeError("Parameter 'percent' is not of type 'float' or 'int'.") 

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

225 raise ex 

226 elif not (0.0 <= percent <= 100.0): 

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

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

229 raise ex 

230 

231 if parent is not None: 

232 if not isinstance(parent, StatisticsMixin): 

233 ex = TypeError("Parameter 'parent' is not of type 'StatisticsMixin'.") 

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

235 raise ex 

236 elif kind in parent._statistics: 

237 raise CodeCoverageError(f"Coverage statistics '{kind}' are stated twice.") 

238 

239 self._parent = parent 

240 self._kind = kind 

241 self._active = active 

242 self._hits = hits 

243 self._percent = float(percent) 

244 

245 if parent is not None: 

246 parent._statistics[kind] = self 

247 

248 @classmethod 

249 def Parse(cls, element: _Element, *, parent: Nullable[StatisticsMixin] = None) -> Self: 

250 """ 

251 Parse coverage statistics from their element. 

252 

253 :param element: The coverage statistics element. 

254 :param parent: Optional, the scope or summary record the coverage statistics belong to. Default: 

255 ``None``. 

256 :returns: The coverage statistics. 

257 :raises CodeCoverageError: If the element states more hit items than items. 

258 :raises CodeCoverageError: If the parent already has coverage statistics of this kind. 

259 """ 

260 active = int(element.attrib["active"]) 

261 hits = int(element.attrib["hits"]) 

262 if hits > active: 

263 ex = CodeCoverageError(f"Coverage statistics '<{element.tag}>' state more hit items than items.") 

264 ex.add_note(f"Got '{hits}' hit items of '{active}' items in line {element.sourceline}.") 

265 raise ex 

266 

267 return cls(CoverageKind(element.tag), active, hits, float(element.attrib["percent"]), parent=parent) 

268 

269 @readonly 

270 def Parent(self) -> Nullable[StatisticsMixin]: 

271 """ 

272 Read-only property to access the scope or summary record the coverage statistics belong to (:attr:`_parent`). 

273 

274 :returns: The scope or summary record; ``None`` if the coverage statistics belong to none. 

275 """ 

276 return self._parent 

277 

278 @readonly 

279 def Kind(self) -> CoverageKind: 

280 """ 

281 Read-only property to access the kind of the coverage items (:attr:`_kind`). 

282 

283 :returns: The kind of the coverage items. 

284 """ 

285 return self._kind 

286 

287 @readonly 

288 def Active(self) -> int: 

289 """ 

290 Read-only property to access the number of coverage items (:attr:`_active`). 

291 

292 :returns: The number of coverage items. 

293 """ 

294 return self._active 

295 

296 @readonly 

297 def Hits(self) -> int: 

298 """ 

299 Read-only property to access the number of coverage items, which were hit (:attr:`_hits`). 

300 

301 :returns: The number of hit coverage items. 

302 """ 

303 return self._hits 

304 

305 @readonly 

306 def Percent(self) -> float: 

307 """ 

308 Read-only property to access the percentage of coverage items, which were hit, as the report states it 

309 (:attr:`_percent`). 

310 

311 Questa truncates the percentage to two decimals: 66 of 74 items are ``89.18`` percent. 

312 

313 :returns: The percentage, from 0 to 100. 

314 """ 

315 return self._percent 

316 

317 

318@export 

319class StatisticsMixin(metaclass=ExtendedType, mixin=True): 

320 """ 

321 A mixin-class adding the coverage statistics an element states: per kind of coverage items, how many it has and how 

322 many were hit. 

323 """ 

324 

325 _statistics: dict[CoverageKind, Statistics] #: The coverage statistics, by kind. 

326 

327 def __init__(self) -> None: 

328 """ 

329 Initialize the coverage statistics to none. 

330 

331 Coverage statistics are added by creating them with the element as their parent. 

332 """ 

333 self._statistics = {} 

334 

335 def _ParseStatistics(self, element: _Element) -> None: 

336 """ 

337 Parse the coverage statistics elements below an element, with this element as their parent. 

338 

339 :param element: The element holding the coverage statistics elements. 

340 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

341 :raises CodeCoverageError: If the element states a kind of coverage statistics twice. 

342 """ 

343 kinds = [kind.value for kind in CoverageKind] 

344 for child in element: 

345 if child.tag in kinds: 

346 Statistics.Parse(child, parent=self) 

347 

348 @readonly 

349 def Statistics(self) -> dict[CoverageKind, Statistics]: 

350 """ 

351 Read-only property to access the coverage statistics (:attr:`_statistics`). 

352 

353 :returns: The coverage statistics, by kind; a kind the element doesn't state has none. 

354 """ 

355 return self._statistics 

356 

357 

358@export 

359class SourceItem(metaclass=ExtendedType, slots=True): 

360 """ 

361 Base-class of the coverage items, which name where they are in the sources: a file, a line and an index in the line. 

362 """ 

363 

364 _file: Path #: Path of the source file, as the report states it. 

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

366 _index: int #: Index of the item among the items of its kind in its line, counted from 1. 

367 

368 def __init__(self, file: Path, lineNumber: int, index: int) -> None: 

369 """ 

370 Initialize where the coverage item is in the sources. 

371 

372 :param file: Path of the source file, as the report states it. 

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

374 :param index: Index of the item among the items of its kind in its line, counted from 1. 

375 :raises ValueError: If parameter ``file`` is ``None``. 

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

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

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

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

380 :raises ValueError: If parameter ``index`` is ``None``. 

381 :raises TypeError: If parameter ``index`` isn't of type :class:`int`. 

382 :raises ValueError: If parameter ``index`` is less than 1. 

383 """ 

384 if file is None: 

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

386 elif not isinstance(file, Path): 

387 ex = TypeError("Parameter 'file' is not of type 'Path'.") 

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

389 raise ex 

390 

391 if lineNumber is None: 

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

393 elif not isinstance(lineNumber, int): 

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

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

396 raise ex 

397 elif lineNumber < 1: 

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

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

400 raise ex 

401 

402 if index is None: 

403 raise ValueError("Parameter 'index' is None.") 

404 elif not isinstance(index, int): 

405 ex = TypeError("Parameter 'index' is not of type 'int'.") 

406 ex.add_note(f"Got type '{getFullyQualifiedName(index)}'.") 

407 raise ex 

408 elif index < 1: 

409 ex = ValueError("Parameter 'index' is less than 1.") 

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

411 raise ex 

412 

413 self._file = file 

414 self._lineNumber = lineNumber 

415 self._index = index 

416 

417 @readonly 

418 def File(self) -> Path: 

419 """ 

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

421 

422 :returns: The path, as the report states it. 

423 """ 

424 return self._file 

425 

426 @readonly 

427 def LineNumber(self) -> int: 

428 """ 

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

430 

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

432 """ 

433 return self._lineNumber 

434 

435 @readonly 

436 def Index(self) -> int: 

437 """ 

438 Read-only property to access the index of the item among the items of its kind in its line (:attr:`_index`). 

439 

440 :returns: The index, counted from 1. 

441 """ 

442 return self._index 

443 

444 

445@export 

446class Statement(SourceItem): 

447 """ 

448 A ``<stmt>`` of a scope: a statement, where it is in the sources, and how often it ran. 

449 """ 

450 

451 _parent: Nullable[Scope] #: The scope the statement belongs to. 

452 _hits: int #: How often the statement ran. 

453 

454 def __init__(self, file: Path, lineNumber: int, index: int, hits: int, *, parent: Nullable[Scope] = None) -> None: 

455 """ 

456 Initialize the statement, and append it to the statements of its scope. 

457 

458 :param file: Path of the source file, as the report states it. 

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

460 :param index: Index of the statement in its line, counted from 1. 

461 :param hits: How often the statement ran. 

462 :param parent: Optional, the scope the statement belongs to; the statement is appended to its statements. 

463 Default: ``None``. 

464 :raises ValueError: If parameter ``file`` is ``None``. 

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

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

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

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

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

470 :raises TypeError: If parameter ``index`` isn't of type :class:`int`. 

471 :raises ValueError: If parameter ``index`` is less than 1. 

472 :raises ValueError: If parameter ``hits`` is ``None``. 

473 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`. 

474 :raises ValueError: If parameter ``hits`` is negative. 

475 :raises TypeError: If parameter ``parent`` isn't of type 

476 :class:`Scope`. 

477 """ 

478 super().__init__(file, lineNumber, index) 

479 

480 if hits is None: 

481 raise ValueError("Parameter 'hits' is None.") 

482 elif not isinstance(hits, int): 

483 ex = TypeError("Parameter 'hits' is not of type 'int'.") 

484 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.") 

485 raise ex 

486 elif hits < 0: 

487 ex = ValueError("Parameter 'hits' is negative.") 

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

489 raise ex 

490 

491 if parent is not None and not isinstance(parent, Scope): 

492 ex = TypeError("Parameter 'parent' is not of type 'Scope'.") 

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

494 raise ex 

495 

496 self._parent = parent 

497 self._hits = hits 

498 

499 if parent is not None: 499 ↛ exitline 499 didn't return from function '__init__' because the condition on line 499 was always true

500 parent._statements.append(self) 

501 

502 @classmethod 

503 def Parse(cls, element: _Element, *, parent: Scope) -> Self: 

504 """ 

505 Parse a statement from its ``<stmt>`` element; its file is the one its file number names in the scope's source 

506 table. 

507 

508 :param element: The ``<stmt>`` element. 

509 :param parent: The scope the statement belongs to. 

510 :returns: The statement. 

511 :raises CodeCoverageError: If the statement's file number isn't in the scope's source table. 

512 :raises CodeCoverageError: If the statement states no file number and its scope has no single file. 

513 """ 

514 return cls( 

515 parent._ResolveFile(element), 

516 int(element.attrib["ln"]), 

517 int(element.attrib["st"]), 

518 int(element.attrib["hits"]), 

519 parent=parent 

520 ) 

521 

522 @readonly 

523 def Parent(self) -> Nullable[Scope]: 

524 """ 

525 Read-only property to access the scope the statement belongs to (:attr:`_parent`). 

526 

527 :returns: The scope; ``None`` if the statement belongs to no scope. 

528 """ 

529 return self._parent 

530 

531 @readonly 

532 def Hits(self) -> int: 

533 """ 

534 Read-only property to access how often the statement ran (:attr:`_hits`). 

535 

536 :returns: The count. 

537 """ 

538 return self._hits 

539 

540 

541@export 

542class IfBranch(SourceItem): 

543 """ 

544 An ``<ielem>`` of an ``if`` statement: a branch - ``if``, ``elsif``, ``else`` or the implicit *AllFalse* branch -, how 

545 often it was taken, and how often its condition was false. 

546 """ 

547 

548 _parent: Nullable[IfStatement] #: The ``if`` statement the branch belongs to. 

549 _trueCount: int #: How often the branch was taken. 

550 _falseCount: int #: How often the branch's condition was false: the evaluation went on. 

551 

552 def __init__( 

553 self, 

554 file: Path, 

555 lineNumber: int, 

556 index: int, 

557 trueCount: int, 

558 falseCount: int, 

559 *, 

560 parent: Nullable[IfStatement] = None 

561 ) -> None: 

562 """ 

563 Initialize the branch, and append it to the branches of its ``if`` statement. 

564 

565 :param file: Path of the source file, as the report states it. 

566 :param lineNumber: Line number of the branch's condition, counted from 1. 

567 :param index: Index of the ``if`` statement in its line, counted from 1. 

568 :param trueCount: How often the branch was taken. 

569 :param falseCount: How often the branch's condition was false; ``0`` for an ``else`` or *AllFalse* branch. 

570 :param parent: Optional, the ``if`` statement the branch belongs to. Default: ``None``. 

571 :raises ValueError: If parameter ``file`` is ``None``. 

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

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

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

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

576 :raises ValueError: If parameter ``index`` is ``None``. 

577 :raises TypeError: If parameter ``index`` isn't of type :class:`int`. 

578 :raises ValueError: If parameter ``index`` is less than 1. 

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

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

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

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

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

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

585 :raises TypeError: If parameter ``parent`` isn't of type :class:`IfStatement`. 

586 """ 

587 super().__init__(file, lineNumber, index) 

588 

589 if trueCount is None: 

590 raise ValueError("Parameter 'trueCount' is None.") 

591 elif not isinstance(trueCount, int): 

592 ex = TypeError("Parameter 'trueCount' is not of type 'int'.") 

593 ex.add_note(f"Got type '{getFullyQualifiedName(trueCount)}'.") 

594 raise ex 

595 elif trueCount < 0: 

596 ex = ValueError("Parameter 'trueCount' is negative.") 

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

598 raise ex 

599 

600 if falseCount is None: 

601 raise ValueError("Parameter 'falseCount' is None.") 

602 elif not isinstance(falseCount, int): 

603 ex = TypeError("Parameter 'falseCount' is not of type 'int'.") 

604 ex.add_note(f"Got type '{getFullyQualifiedName(falseCount)}'.") 

605 raise ex 

606 elif falseCount < 0: 

607 ex = ValueError("Parameter 'falseCount' is negative.") 

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

609 raise ex 

610 

611 if parent is not None and not isinstance(parent, IfStatement): 

612 ex = TypeError("Parameter 'parent' is not of type 'IfStatement'.") 

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

614 raise ex 

615 

616 self._parent = parent 

617 self._trueCount = trueCount 

618 self._falseCount = falseCount 

619 

620 if parent is not None: 620 ↛ exitline 620 didn't return from function '__init__' because the condition on line 620 was always true

621 parent._branches.append(self) 

622 

623 @classmethod 

624 def Parse(cls, element: _Element, *, parent: IfStatement) -> Self: 

625 """ 

626 Parse a branch from its ``<ielem>`` element. 

627 

628 :param element: The ``<ielem>`` element. 

629 :param parent: The ``if`` statement the branch belongs to; it belongs to a scope. 

630 :returns: The branch. 

631 :raises CodeCoverageError: If the branch's file number isn't in the scope's source table, or it states none and 

632 the scope has no single file. 

633 """ 

634 return cls( 

635 parent._parent._ResolveFile(element), 

636 int(element.attrib["ln"]), 

637 int(element.attrib["st"]), 

638 int(element.attrib["true"]), 

639 int(element.attrib["false"]), 

640 parent=parent 

641 ) 

642 

643 @readonly 

644 def Parent(self) -> Nullable[IfStatement]: 

645 """ 

646 Read-only property to access the ``if`` statement the branch belongs to (:attr:`_parent`). 

647 

648 :returns: The ``if`` statement; ``None`` if the branch belongs to none. 

649 """ 

650 return self._parent 

651 

652 @readonly 

653 def TrueCount(self) -> int: 

654 """ 

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

656 

657 :returns: The count. 

658 """ 

659 return self._trueCount 

660 

661 @readonly 

662 def FalseCount(self) -> int: 

663 """ 

664 Read-only property to access how often the branch's condition was false (:attr:`_falseCount`). 

665 

666 Then the evaluation went on to the next branch: the count is the sum of the next branches' true counts. 

667 

668 :returns: The count; ``0`` for an ``else`` or *AllFalse* branch. 

669 """ 

670 return self._falseCount 

671 

672 

673@export 

674class IfStatement(metaclass=ExtendedType, slots=True): 

675 """ 

676 An ``<if>`` of a scope: an ``if`` statement - with its ``elsif`` and ``else`` branches - and how often each branch was 

677 taken. 

678 

679 Each branch is an ``<ielem>`` (:class:`IfBranch`). An ``if`` statement without ``else`` has an implicit branch, the 

680 *AllFalse* branch, taken whenever no condition was true; Questa lists it as the last ``<ielem>``, in the line of the 

681 ``if``. 

682 """ 

683 

684 _parent: Nullable[Scope] #: The scope the ``if`` statement belongs to. 

685 _hasElse: bool #: Whether the ``if`` statement has an ``else`` branch. 

686 _branches: list[IfBranch] #: The branches, in the order of their conditions. 

687 

688 def __init__(self, hasElse: bool, *, parent: Nullable[Scope] = None) -> None: 

689 """ 

690 Initialize the ``if`` statement, and append it to the ``if`` statements of its scope. 

691 

692 Its branches are added by creating them with this ``if`` statement as their parent. 

693 

694 :param hasElse: Whether the ``if`` statement has an ``else`` branch; otherwise its last branch is the implicit 

695 *AllFalse* branch. 

696 :param parent: Optional, the scope the ``if`` statement belongs to. Default: ``None``. 

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

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

699 :raises TypeError: If parameter ``parent`` isn't of type 

700 :class:`Scope`. 

701 """ 

702 if hasElse is None: 

703 raise ValueError("Parameter 'hasElse' is None.") 

704 elif not isinstance(hasElse, bool): 

705 ex = TypeError("Parameter 'hasElse' is not of type 'bool'.") 

706 ex.add_note(f"Got type '{getFullyQualifiedName(hasElse)}'.") 

707 raise ex 

708 

709 if parent is not None and not isinstance(parent, Scope): 

710 ex = TypeError("Parameter 'parent' is not of type 'Scope'.") 

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

712 raise ex 

713 

714 self._parent = parent 

715 self._hasElse = hasElse 

716 self._branches = [] 

717 

718 if parent is not None: 

719 parent._ifStatements.append(self) 

720 

721 @classmethod 

722 def Parse(cls, element: _Element, *, parent: Scope) -> Self: 

723 """ 

724 Parse an ``if`` statement and its branches from its ``<if>`` element. 

725 

726 :param element: The ``<if>`` element. 

727 :param parent: The scope the ``if`` statement belongs to. 

728 :returns: The ``if`` statement. 

729 :raises CodeCoverageError: If a branch's file number isn't in the scope's source table, or it states none and the 

730 scope has no single file. 

731 :raises CodeCoverageError: If the ``if`` statement states other numbers of branches and taken branches than its 

732 branches have. 

733 """ 

734 ifStatement = cls(element.attrib["hasElse"] == "1", parent=parent) 

735 for branchElement in element.iterfind("ielem"): 

736 IfBranch.Parse(branchElement, parent=ifStatement) 

737 

738 active = int(element.attrib["active"]) 

739 hits = int(element.attrib["hits"]) 

740 if active != len(ifStatement._branches) or hits != ifStatement.TakenBranches: 

741 ex = CodeCoverageError(f"'<if>' in line {element.sourceline} states other figures than its branches have.") 

742 ex.add_note(f"Got '{hits}' of '{active}' branches taken, its branches have {ifStatement.TakenBranches} of " 

743 f"{len(ifStatement._branches)}.") 

744 raise ex 

745 

746 return ifStatement 

747 

748 @readonly 

749 def Parent(self) -> Nullable[Scope]: 

750 """ 

751 Read-only property to access the scope the ``if`` statement belongs to (:attr:`_parent`). 

752 

753 :returns: The scope; ``None`` if the ``if`` statement belongs to no scope. 

754 """ 

755 return self._parent 

756 

757 @readonly 

758 def HasElse(self) -> bool: 

759 """ 

760 Read-only property to access whether the ``if`` statement has an ``else`` branch (:attr:`_hasElse`). 

761 

762 :returns: ``True``, if it has an ``else`` branch; otherwise its last branch is the implicit *AllFalse* branch. 

763 """ 

764 return self._hasElse 

765 

766 @readonly 

767 def Branches(self) -> list[IfBranch]: 

768 """ 

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

770 

771 :returns: The branches, in the order of their conditions; the ``else`` or *AllFalse* branch last. 

772 """ 

773 return self._branches 

774 

775 @readonly 

776 def TakenBranches(self) -> int: 

777 """ 

778 Read-only property to return the number of branches, which were taken. 

779 

780 :returns: The number of branches with a positive :attr:`IfBranch.TrueCount`. 

781 """ 

782 return sum(1 for branch in self._branches if branch._trueCount > 0) 

783 

784 @readonly 

785 def EvaluationCount(self) -> int: 

786 """ 

787 Read-only property to return how often the ``if`` statement was evaluated. 

788 

789 Each evaluation takes exactly one branch, so it is the sum of the branches' :attr:`IfBranch.TrueCount`. 

790 

791 :returns: The number of evaluations. 

792 """ 

793 return sum(branch._trueCount for branch in self._branches) 

794 

795 

796@export 

797class CaseBranch(SourceItem): 

798 """ 

799 A ``<celem>`` of a ``case`` statement: a case item, and how often it was taken. 

800 """ 

801 

802 _parent: Nullable[CaseStatement] #: The ``case`` statement the branch belongs to. 

803 _hits: int #: How often the branch was taken. 

804 

805 def __init__( 

806 self, 

807 file: Path, 

808 lineNumber: int, 

809 index: int, 

810 hits: int, 

811 *, 

812 parent: Nullable[CaseStatement] = None 

813 ) -> None: 

814 """ 

815 Initialize the branch, and append it to the branches of its ``case`` statement. 

816 

817 :param file: Path of the source file, as the report states it. 

818 :param lineNumber: Line number of the case item, counted from 1. 

819 :param index: Index of the case item in its line, counted from 1. 

820 :param hits: How often the branch was taken. 

821 :param parent: Optional, the ``case`` statement the branch belongs to. Default: ``None``. 

822 :raises ValueError: If parameter ``file`` is ``None``. 

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

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

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

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

827 :raises ValueError: If parameter ``index`` is ``None``. 

828 :raises TypeError: If parameter ``index`` isn't of type :class:`int`. 

829 :raises ValueError: If parameter ``index`` is less than 1. 

830 :raises ValueError: If parameter ``hits`` is ``None``. 

831 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`. 

832 :raises ValueError: If parameter ``hits`` is negative. 

833 :raises TypeError: If parameter ``parent`` isn't of type :class:`CaseStatement`. 

834 """ 

835 super().__init__(file, lineNumber, index) 

836 

837 if hits is None: 

838 raise ValueError("Parameter 'hits' is None.") 

839 elif not isinstance(hits, int): 

840 ex = TypeError("Parameter 'hits' is not of type 'int'.") 

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

842 raise ex 

843 elif hits < 0: 

844 ex = ValueError("Parameter 'hits' is negative.") 

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

846 raise ex 

847 

848 if parent is not None and not isinstance(parent, CaseStatement): 

849 ex = TypeError("Parameter 'parent' is not of type 'CaseStatement'.") 

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

851 raise ex 

852 

853 self._parent = parent 

854 self._hits = hits 

855 

856 if parent is not None: 856 ↛ exitline 856 didn't return from function '__init__' because the condition on line 856 was always true

857 parent._branches.append(self) 

858 

859 @classmethod 

860 def Parse(cls, element: _Element, *, parent: CaseStatement) -> Self: 

861 """ 

862 Parse a branch from its ``<celem>`` element. 

863 

864 :param element: The ``<celem>`` element. 

865 :param parent: The ``case`` statement the branch belongs to; it belongs to a scope. 

866 :returns: The branch. 

867 :raises CodeCoverageError: If the branch's file number isn't in the scope's source table, or it states none and 

868 the scope has no single file. 

869 """ 

870 return cls( 

871 parent._parent._ResolveFile(element), 

872 int(element.attrib["ln"]), 

873 int(element.attrib["st"]), 

874 int(element.attrib["hits"]), 

875 parent=parent 

876 ) 

877 

878 @readonly 

879 def Parent(self) -> Nullable[CaseStatement]: 

880 """ 

881 Read-only property to access the ``case`` statement the branch belongs to (:attr:`_parent`). 

882 

883 :returns: The ``case`` statement; ``None`` if the branch belongs to none. 

884 """ 

885 return self._parent 

886 

887 @readonly 

888 def Hits(self) -> int: 

889 """ 

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

891 

892 :returns: The count. 

893 """ 

894 return self._hits 

895 

896 

897@export 

898class CaseStatement(metaclass=ExtendedType, slots=True): 

899 """ 

900 A ``<case>`` of a scope: a ``case`` statement, and how often each of its branches was taken. 

901 

902 Each case item is a branch (``<celem>``, :class:`CaseBranch`); the ``case`` statement's line isn't stated. 

903 """ 

904 

905 _parent: Nullable[Scope] #: The scope the ``case`` statement belongs to. 

906 _branches: list[CaseBranch] #: The branches, in the order of the case items. 

907 

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

909 """ 

910 Initialize the ``case`` statement, and append it to the ``case`` statements of its scope. 

911 

912 Its branches are added by creating them with this ``case`` statement as their parent. 

913 

914 :param parent: Optional, the scope the ``case`` statement belongs to. Default: ``None``. 

915 :raises TypeError: If parameter ``parent`` isn't of type 

916 :class:`Scope`. 

917 """ 

918 if parent is not None and not isinstance(parent, Scope): 

919 ex = TypeError("Parameter 'parent' is not of type 'Scope'.") 

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

921 raise ex 

922 

923 self._parent = parent 

924 self._branches = [] 

925 

926 if parent is not None: 

927 parent._caseStatements.append(self) 

928 

929 @classmethod 

930 def Parse(cls, element: _Element, *, parent: Scope) -> Self: 

931 """ 

932 Parse a ``case`` statement and its branches from its ``<case>`` element. 

933 

934 :param element: The ``<case>`` element. 

935 :param parent: The scope the ``case`` statement belongs to. 

936 :returns: The ``case`` statement. 

937 :raises CodeCoverageError: If a branch's file number isn't in the scope's source table, or it states none and the 

938 scope has no single file. 

939 :raises CodeCoverageError: If the ``case`` statement states other numbers of branches and taken branches than its 

940 branches have. 

941 """ 

942 caseStatement = cls(parent=parent) 

943 for branchElement in element.iterfind("celem"): 

944 CaseBranch.Parse(branchElement, parent=caseStatement) 

945 

946 active = int(element.attrib["active"]) 

947 hits = int(element.attrib["hits"]) 

948 if active != len(caseStatement._branches) or hits != caseStatement.TakenBranches: 

949 ex = CodeCoverageError(f"'<case>' in line {element.sourceline} states other figures than its branches have.") 

950 ex.add_note(f"Got '{hits}' of '{active}' branches taken, its branches have {caseStatement.TakenBranches} of " 

951 f"{len(caseStatement._branches)}.") 

952 raise ex 

953 

954 return caseStatement 

955 

956 @readonly 

957 def Parent(self) -> Nullable[Scope]: 

958 """ 

959 Read-only property to access the scope the ``case`` statement belongs to (:attr:`_parent`). 

960 

961 :returns: The scope; ``None`` if the ``case`` statement belongs to no scope. 

962 """ 

963 return self._parent 

964 

965 @readonly 

966 def Branches(self) -> list[CaseBranch]: 

967 """ 

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

969 

970 :returns: The branches, in the order of the case items. 

971 """ 

972 return self._branches 

973 

974 @readonly 

975 def TakenBranches(self) -> int: 

976 """ 

977 Read-only property to return the number of branches, which were taken. 

978 

979 :returns: The number of branches with a positive :attr:`CaseBranch.Hits`. 

980 """ 

981 return sum(1 for branch in self._branches if branch._hits > 0) 

982 

983 @readonly 

984 def EvaluationCount(self) -> int: 

985 """ 

986 Read-only property to return how often the ``case`` statement was evaluated. 

987 

988 Each evaluation takes exactly one branch, so it is the sum of the branches' :attr:`CaseBranch.Hits`. 

989 

990 :returns: The number of evaluations. 

991 """ 

992 return sum(branch._hits for branch in self._branches) 

993 

994 

995@export 

996class FSMState(metaclass=ExtendedType, slots=True): 

997 """ 

998 A ``<state>`` of a scope: a state of a finite state machine, and how often it was entered. 

999 """ 

1000 

1001 _parent: Nullable[Scope] #: The scope the state belongs to. 

1002 _name: str #: Name of the state. 

1003 _file: Nullable[Path] #: Path of the source file, if known. 

1004 _lineNumber: int #: Line number of the state, counted from 1. 

1005 _hits: int #: How often the state was entered. 

1006 

1007 def __init__( 

1008 self, 

1009 name: str, 

1010 file: Nullable[Path], 

1011 lineNumber: int, 

1012 hits: int, 

1013 *, 

1014 parent: Nullable[Scope] = None 

1015 ) -> None: 

1016 """ 

1017 Initialize the state, and append it to the states of its scope. 

1018 

1019 :param name: Name of the state. 

1020 :param file: Path of the source file, as the report states it; ``None``, if unknown. 

1021 :param lineNumber: Line number of the state, counted from 1. 

1022 :param hits: How often the state was entered. 

1023 :param parent: Optional, the scope the state belongs to. Default: ``None``. 

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

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

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

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

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

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

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

1031 :raises ValueError: If parameter ``hits`` is ``None``. 

1032 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`. 

1033 :raises ValueError: If parameter ``hits`` is negative. 

1034 :raises TypeError: If parameter ``parent`` isn't of type 

1035 :class:`Scope`. 

1036 """ 

1037 if name is None: 

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

1039 elif not isinstance(name, str): 

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

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

1042 raise ex 

1043 elif name == "": 

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

1045 

1046 if file is not None and not isinstance(file, Path): 

1047 ex = TypeError("Parameter 'file' is not of type 'Path'.") 

1048 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

1049 raise ex 

1050 

1051 if lineNumber is None: 

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

1053 elif not isinstance(lineNumber, int): 

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

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

1056 raise ex 

1057 elif lineNumber < 1: 

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

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

1060 raise ex 

1061 

1062 if hits is None: 

1063 raise ValueError("Parameter 'hits' is None.") 

1064 elif not isinstance(hits, int): 

1065 ex = TypeError("Parameter 'hits' is not of type 'int'.") 

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

1067 raise ex 

1068 elif hits < 0: 

1069 ex = ValueError("Parameter 'hits' is negative.") 

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

1071 raise ex 

1072 

1073 if parent is not None and not isinstance(parent, Scope): 

1074 ex = TypeError("Parameter 'parent' is not of type 'Scope'.") 

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

1076 raise ex 

1077 

1078 self._parent = parent 

1079 self._name = name 

1080 self._file = file 

1081 self._lineNumber = lineNumber 

1082 self._hits = hits 

1083 

1084 if parent is not None: 1084 ↛ exitline 1084 didn't return from function '__init__' because the condition on line 1084 was always true

1085 parent._states.append(self) 

1086 

1087 @classmethod 

1088 def Parse(cls, element: _Element, *, parent: Scope) -> Self: 

1089 """ 

1090 Parse a state from its ``<state>`` element. 

1091 

1092 A ``<state>`` names no file number: its file is the scope's, if the scope has a single file. 

1093 

1094 :param element: The ``<state>`` element. 

1095 :param parent: The scope the state belongs to. 

1096 :returns: The state. 

1097 """ 

1098 file = next(iter(parent._files.values())) if len(parent._files) == 1 else None 

1099 return cls( 

1100 element.attrib["state"], file, int(element.attrib["ln"]), int(element.attrib["hits"]), parent=parent 

1101 ) 

1102 

1103 @readonly 

1104 def Parent(self) -> Nullable[Scope]: 

1105 """ 

1106 Read-only property to access the scope the state belongs to (:attr:`_parent`). 

1107 

1108 :returns: The scope; ``None`` if the state belongs to no scope. 

1109 """ 

1110 return self._parent 

1111 

1112 @readonly 

1113 def Name(self) -> str: 

1114 """ 

1115 Read-only property to access the name of the state (:attr:`_name`). 

1116 

1117 :returns: The state's name, as the enumeration of the state machine names it. 

1118 """ 

1119 return self._name 

1120 

1121 @readonly 

1122 def File(self) -> Nullable[Path]: 

1123 """ 

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

1125 

1126 :returns: The path, as the report states it; ``None``, if the scope has several files. 

1127 """ 

1128 return self._file 

1129 

1130 @readonly 

1131 def LineNumber(self) -> int: 

1132 """ 

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

1134 

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

1136 """ 

1137 return self._lineNumber 

1138 

1139 @readonly 

1140 def Hits(self) -> int: 

1141 """ 

1142 Read-only property to access how often the state was entered (:attr:`_hits`). 

1143 

1144 :returns: The count. 

1145 """ 

1146 return self._hits 

1147 

1148 

1149@export 

1150class FSMTransition(metaclass=ExtendedType, slots=True): 

1151 """ 

1152 A ``<trans>`` of a scope: a transition of a finite state machine from one state to another, and how often it was 

1153 taken. 

1154 """ 

1155 

1156 _parent: Nullable[Scope] #: The scope the transition belongs to. 

1157 _identifier: int #: Number of the transition in its state machine, counted from 0. 

1158 _fromState: str #: Name of the state the transition leaves. 

1159 _toState: str #: Name of the state the transition enters. 

1160 _file: Nullable[Path] #: Path of the source file, if known. 

1161 _lineNumber: int #: Line number of the transition, counted from 1. 

1162 _hits: int #: How often the transition was taken. 

1163 

1164 def __init__( 

1165 self, 

1166 identifier: int, 

1167 fromState: str, 

1168 toState: str, 

1169 file: Nullable[Path], 

1170 lineNumber: int, 

1171 hits: int, 

1172 *, 

1173 parent: Nullable[Scope] = None 

1174 ) -> None: 

1175 """ 

1176 Initialize the transition, and append it to the transitions of its scope. 

1177 

1178 :param identifier: Number of the transition in its state machine, counted from 0. 

1179 :param fromState: Name of the state the transition leaves. 

1180 :param toState: Name of the state the transition enters. 

1181 :param file: Path of the source file, as the report states it; ``None``, if unknown. 

1182 :param lineNumber: Line number of the transition, counted from 1. 

1183 :param hits: How often the transition was taken. 

1184 :param parent: Optional, the scope the transition belongs to. Default: ``None``. 

1185 :raises ValueError: If parameter ``identifier`` is ``None``. 

1186 :raises TypeError: If parameter ``identifier`` isn't of type :class:`int`. 

1187 :raises ValueError: If parameter ``identifier`` is negative. 

1188 :raises ValueError: If parameter ``fromState`` is ``None``. 

1189 :raises TypeError: If parameter ``fromState`` isn't of type :class:`str`. 

1190 :raises ValueError: If parameter ``fromState`` is empty. 

1191 :raises ValueError: If parameter ``toState`` is ``None``. 

1192 :raises TypeError: If parameter ``toState`` isn't of type :class:`str`. 

1193 :raises ValueError: If parameter ``toState`` is empty. 

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

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

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

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

1198 :raises ValueError: If parameter ``hits`` is ``None``. 

1199 :raises TypeError: If parameter ``hits`` isn't of type :class:`int`. 

1200 :raises ValueError: If parameter ``hits`` is negative. 

1201 :raises TypeError: If parameter ``parent`` isn't of type 

1202 :class:`Scope`. 

1203 """ 

1204 if identifier is None: 

1205 raise ValueError("Parameter 'identifier' is None.") 

1206 elif not isinstance(identifier, int): 

1207 ex = TypeError("Parameter 'identifier' is not of type 'int'.") 

1208 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.") 

1209 raise ex 

1210 elif identifier < 0: 

1211 ex = ValueError("Parameter 'identifier' is negative.") 

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

1213 raise ex 

1214 

1215 if fromState is None: 

1216 raise ValueError("Parameter 'fromState' is None.") 

1217 elif not isinstance(fromState, str): 

1218 ex = TypeError("Parameter 'fromState' is not of type 'str'.") 

1219 ex.add_note(f"Got type '{getFullyQualifiedName(fromState)}'.") 

1220 raise ex 

1221 elif fromState == "": 

1222 raise ValueError("Parameter 'fromState' is empty.") 

1223 

1224 if toState is None: 

1225 raise ValueError("Parameter 'toState' is None.") 

1226 elif not isinstance(toState, str): 

1227 ex = TypeError("Parameter 'toState' is not of type 'str'.") 

1228 ex.add_note(f"Got type '{getFullyQualifiedName(toState)}'.") 

1229 raise ex 

1230 elif toState == "": 

1231 raise ValueError("Parameter 'toState' is empty.") 

1232 

1233 if file is not None and not isinstance(file, Path): 

1234 ex = TypeError("Parameter 'file' is not of type 'Path'.") 

1235 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

1236 raise ex 

1237 

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

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

1240 elif not isinstance(lineNumber, int): 1240 ↛ 1241line 1240 didn't jump to line 1241 because the condition on line 1240 was never true

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

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

1243 raise ex 

1244 elif lineNumber < 1: 

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

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

1247 raise ex 

1248 

1249 if hits is None: 1249 ↛ 1250line 1249 didn't jump to line 1250 because the condition on line 1249 was never true

1250 raise ValueError("Parameter 'hits' is None.") 

1251 elif not isinstance(hits, int): 1251 ↛ 1252line 1251 didn't jump to line 1252 because the condition on line 1251 was never true

1252 ex = TypeError("Parameter 'hits' is not of type 'int'.") 

1253 ex.add_note(f"Got type '{getFullyQualifiedName(hits)}'.") 

1254 raise ex 

1255 elif hits < 0: 

1256 ex = ValueError("Parameter 'hits' is negative.") 

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

1258 raise ex 

1259 

1260 if parent is not None and not isinstance(parent, Scope): 

1261 ex = TypeError("Parameter 'parent' is not of type 'Scope'.") 

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

1263 raise ex 

1264 

1265 self._parent = parent 

1266 self._identifier = identifier 

1267 self._fromState = fromState 

1268 self._toState = toState 

1269 self._file = file 

1270 self._lineNumber = lineNumber 

1271 self._hits = hits 

1272 

1273 if parent is not None: 1273 ↛ exitline 1273 didn't return from function '__init__' because the condition on line 1273 was always true

1274 parent._transitions.append(self) 

1275 

1276 @classmethod 

1277 def Parse(cls, element: _Element, *, parent: Scope) -> Self: 

1278 """ 

1279 Parse a transition from its ``<trans>`` element, whose attribute ``state`` names both states. 

1280 

1281 A ``<trans>`` names no file number: its file is the scope's, if the scope has a single file. 

1282 

1283 :param element: The ``<trans>`` element. 

1284 :param parent: The scope the transition belongs to. 

1285 :returns: The transition. 

1286 :raises CodeCoverageError: If attribute ``state`` doesn't name two states separated by ``->``. |br| 

1287 Questa writes it as ``<state> -> <state>``. 

1288 """ 

1289 fromState, separator, toState = element.attrib["state"].partition(" -> ") 

1290 if separator == "" or fromState == "" or toState == "": 

1291 ex = CodeCoverageError(f"'<trans>' in line {element.sourceline} doesn't name the states it connects.") 

1292 ex.add_note(f"Got state='{element.attrib['state']}'.") 

1293 ex.add_note("Questa writes it as '<state> -> <state>'.") 

1294 raise ex 

1295 

1296 file = next(iter(parent._files.values())) if len(parent._files) == 1 else None 

1297 return cls( 

1298 int(element.attrib["tid"]), 

1299 fromState, 

1300 toState, 

1301 file, 

1302 int(element.attrib["ln"]), 

1303 int(element.attrib["hits"]), 

1304 parent=parent 

1305 ) 

1306 

1307 @readonly 

1308 def Parent(self) -> Nullable[Scope]: 

1309 """ 

1310 Read-only property to access the scope the transition belongs to (:attr:`_parent`). 

1311 

1312 :returns: The scope; ``None`` if the transition belongs to no scope. 

1313 """ 

1314 return self._parent 

1315 

1316 @readonly 

1317 def Identifier(self) -> int: 

1318 """ 

1319 Read-only property to access the number of the transition in its state machine (:attr:`_identifier`). 

1320 

1321 :returns: The number, counted from 0. 

1322 """ 

1323 return self._identifier 

1324 

1325 @readonly 

1326 def FromState(self) -> str: 

1327 """ 

1328 Read-only property to access the name of the state the transition leaves (:attr:`_fromState`). 

1329 

1330 :returns: The state's name. 

1331 """ 

1332 return self._fromState 

1333 

1334 @readonly 

1335 def ToState(self) -> str: 

1336 """ 

1337 Read-only property to access the name of the state the transition enters (:attr:`_toState`). 

1338 

1339 :returns: The state's name. 

1340 """ 

1341 return self._toState 

1342 

1343 @readonly 

1344 def File(self) -> Nullable[Path]: 

1345 """ 

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

1347 

1348 :returns: The path, as the report states it; ``None``, if the scope has several files. 

1349 """ 

1350 return self._file 

1351 

1352 @readonly 

1353 def LineNumber(self) -> int: 

1354 """ 

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

1356 

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

1358 """ 

1359 return self._lineNumber 

1360 

1361 @readonly 

1362 def Hits(self) -> int: 

1363 """ 

1364 Read-only property to access how often the transition was taken (:attr:`_hits`). 

1365 

1366 :returns: The count. 

1367 """ 

1368 return self._hits 

1369 

1370 

1371@export 

1372class Scope(StatisticsMixin, slots=True): 

1373 """ 

1374 Base-class of the scopes: an instance, a design unit or a source file, with its coverage statistics, its source files 

1375 and its statements. 

1376 """ 

1377 

1378 _parent: Nullable[Report] #: The report the scope belongs to. 

1379 _files: dict[int, Path] #: The source files, by the number the report gives them. 

1380 _statements: list[Statement] #: The statements. 

1381 _ifStatements: list[IfStatement] #: The ``if`` statements. 

1382 _caseStatements: list[CaseStatement] #: The ``case`` statements. 

1383 _states: list[FSMState] #: The states of finite state machines. 

1384 _transitions: list[FSMTransition] #: The transitions of finite state machines. 

1385 

1386 def __init__(self, files: Nullable[Mapping[int, Path]] = None, *, parent: Nullable[Report] = None) -> None: 

1387 """ 

1388 Initialize the scope, and append it to the scopes of its report. 

1389 

1390 Its coverage statistics and coverage items are added by creating them with this scope as their parent. 

1391 

1392 :param files: Optional, the source files, by the number the report gives them. Default: ``None``. 

1393 :param parent: Optional, the report the scope belongs to; the scope is appended to its scopes. Default: 

1394 ``None``. 

1395 :raises TypeError: If parameter ``files`` isn't a mapping. 

1396 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`. 

1397 :raises ValueError: If parameter ``files`` contains a negative key. 

1398 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`. 

1399 :raises TypeError: If parameter ``parent`` isn't of type 

1400 :class:`Report`. 

1401 """ 

1402 super().__init__() 

1403 

1404 self._files = {} 

1405 if files is not None: 

1406 if not isinstance(files, Mapping): 

1407 ex = TypeError("Parameter 'files' is not a mapping.") 

1408 ex.add_note(f"Got type '{getFullyQualifiedName(files)}'.") 

1409 raise ex 

1410 

1411 for fileNumber, path in files.items(): 

1412 if not isinstance(fileNumber, int): 

1413 ex = TypeError("Parameter 'files' contains a key not of type 'int'.") 

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

1415 raise ex 

1416 elif fileNumber < 0: 

1417 ex = ValueError("Parameter 'files' contains a negative key.") 

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

1419 raise ex 

1420 elif not isinstance(path, Path): 

1421 ex = TypeError("Parameter 'files' contains a value not of type 'Path'.") 

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

1423 raise ex 

1424 

1425 self._files[fileNumber] = path 

1426 

1427 if parent is not None and not isinstance(parent, Report): 

1428 ex = TypeError("Parameter 'parent' is not of type 'Report'.") 

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

1430 raise ex 

1431 

1432 self._parent = parent 

1433 self._statements = [] 

1434 self._ifStatements = [] 

1435 self._caseStatements = [] 

1436 self._states = [] 

1437 self._transitions = [] 

1438 

1439 if parent is not None: 

1440 parent._scopes.append(self) 

1441 

1442 @staticmethod 

1443 def _ParseSourceTable(element: _Element) -> dict[int, Path]: 

1444 """ 

1445 Parse the source files of a scope's ``<sourceTable>``. 

1446 

1447 :param element: The scope's element. 

1448 :returns: The source files, by their number; none, if the scope has no source table. 

1449 """ 

1450 return { 

1451 int(fileMap.attrib["fn"]): Path(fileMap.attrib["path"]) for fileMap in element.iterfind("sourceTable/fileMap") 

1452 } 

1453 

1454 def _ParseItems(self, element: _Element) -> None: 

1455 """ 

1456 Parse the coverage statistics and the coverage items of the scope from its element. 

1457 

1458 :param element: The scope's element. 

1459 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

1460 :raises CodeCoverageError: If the scope states a kind of coverage statistics twice. 

1461 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in a scope of several files. 

1462 :raises CodeCoverageError: If an ``if`` or ``case`` statement states other figures than its branches have. 

1463 :raises CodeCoverageError: If a transition's name doesn't name its two states. 

1464 """ 

1465 self._ParseStatistics(element) 

1466 for child in element: 

1467 if child.tag == "stmt": 

1468 Statement.Parse(child, parent=self) 

1469 elif child.tag == "if": 

1470 IfStatement.Parse(child, parent=self) 

1471 elif child.tag == "case": 

1472 CaseStatement.Parse(child, parent=self) 

1473 elif child.tag == "state": 

1474 FSMState.Parse(child, parent=self) 

1475 elif child.tag == "trans": 

1476 FSMTransition.Parse(child, parent=self) 

1477 

1478 def _ResolveFile(self, element: _Element) -> Path: 

1479 """ 

1480 Return the source file a coverage item's element names by its file number. 

1481 

1482 An element without file number is in the scope's file: the path of a ``<fileData>``, or the only file of the source 

1483 table. 

1484 

1485 :param element: The coverage item's element, e.g. a ``<stmt>``. 

1486 :returns: The source file's path, as the report states it. 

1487 :raises CodeCoverageError: If the file number isn't in the scope's source table. 

1488 :raises CodeCoverageError: If the element names no file number, and the scope has no single file. 

1489 """ 

1490 if (fileNumber := element.attrib.get("fn")) is not None: 

1491 if (file := self._files.get(int(fileNumber))) is None: 

1492 ex = CodeCoverageError( 

1493 f"'<{element.tag}>' in line {element.sourceline} names file number '{fileNumber}', which is unknown." 

1494 ) 

1495 ex.add_note(f"Known file numbers: {', '.join(str(number) for number in self._files) or 'none'}.") 

1496 raise ex 

1497 

1498 return file 

1499 elif isinstance(self, FileData): 

1500 return self._path 

1501 elif len(self._files) == 1: 

1502 return next(iter(self._files.values())) 

1503 

1504 ex = CodeCoverageError(f"'<{element.tag}>' in line {element.sourceline} names no file number.") 

1505 ex.add_note(f"Its scope names {len(self._files)} files.") 

1506 raise ex 

1507 

1508 @readonly 

1509 def Parent(self) -> Nullable[Report]: 

1510 """ 

1511 Read-only property to access the report the scope belongs to (:attr:`_parent`). 

1512 

1513 :returns: The report; ``None`` if the scope belongs to none. 

1514 """ 

1515 return self._parent 

1516 

1517 @readonly 

1518 def Files(self) -> dict[int, Path]: 

1519 """ 

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

1521 

1522 :returns: The source files, by the number the report gives them; none, if the report has no details. 

1523 """ 

1524 return self._files 

1525 

1526 @readonly 

1527 def Statements(self) -> list[Statement]: 

1528 """ 

1529 Read-only property to access the statements (:attr:`_statements`). 

1530 

1531 :returns: The statements, in the order the report lists them; none, if the report has no details. 

1532 """ 

1533 return self._statements 

1534 

1535 @readonly 

1536 def IfStatements(self) -> list[IfStatement]: 

1537 """ 

1538 Read-only property to access the ``if`` statements (:attr:`_ifStatements`). 

1539 

1540 :returns: The ``if`` statements, in the order the report lists them; none, if the report has no details. 

1541 """ 

1542 return self._ifStatements 

1543 

1544 @readonly 

1545 def CaseStatements(self) -> list[CaseStatement]: 

1546 """ 

1547 Read-only property to access the ``case`` statements (:attr:`_caseStatements`). 

1548 

1549 :returns: The ``case`` statements, in the order the report lists them; none, if the report has no details. 

1550 """ 

1551 return self._caseStatements 

1552 

1553 @readonly 

1554 def States(self) -> list[FSMState]: 

1555 """ 

1556 Read-only property to access the states of finite state machines (:attr:`_states`). 

1557 

1558 :returns: The states, in the order the report lists them; none, if the report has no details. 

1559 """ 

1560 return self._states 

1561 

1562 @readonly 

1563 def Transitions(self) -> list[FSMTransition]: 

1564 """ 

1565 Read-only property to access the transitions of finite state machines (:attr:`_transitions`). 

1566 

1567 :returns: The transitions, in the order the report lists them; none, if the report has no details. 

1568 """ 

1569 return self._transitions 

1570 

1571 

1572@export 

1573class InstanceData(Scope): 

1574 """ 

1575 An ``<instanceData>``: an instance of a design unit, or a package. 

1576 """ 

1577 

1578 _path: str #: Hierarchical path of the instance. 

1579 _designUnit: str #: Name of the design unit. 

1580 _secondaryUnit: Nullable[str] #: Name of the secondary unit (the architecture), if the design unit has one. 

1581 

1582 def __init__( 

1583 self, 

1584 path: str, 

1585 designUnit: str, 

1586 secondaryUnit: Nullable[str] = None, 

1587 files: Nullable[Mapping[int, Path]] = None, 

1588 *, 

1589 parent: Nullable[Report] = None 

1590 ) -> None: 

1591 """ 

1592 Initialize the instance, and append it to the scopes of its report. 

1593 

1594 :param path: Hierarchical path of the instance, separated by ``/``. 

1595 :param designUnit: Name of the design unit. 

1596 :param secondaryUnit: Optional, name of the secondary unit (the architecture). Default: ``None``. 

1597 :param files: Optional, the source files, by the number the report gives them. Default: ``None``. 

1598 :param parent: Optional, the report the instance belongs to. Default: ``None``. 

1599 :raises TypeError: If parameter ``files`` isn't a mapping. 

1600 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`. 

1601 :raises ValueError: If parameter ``files`` contains a negative key. 

1602 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`. 

1603 :raises TypeError: If parameter ``parent`` isn't of type 

1604 :class:`Report`. 

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

1606 :raises TypeError: If parameter ``path`` isn't of type :class:`str`. 

1607 :raises ValueError: If parameter ``path`` is empty. 

1608 :raises ValueError: If parameter ``designUnit`` is ``None``. 

1609 :raises TypeError: If parameter ``designUnit`` isn't of type :class:`str`. 

1610 :raises ValueError: If parameter ``designUnit`` is empty. 

1611 :raises TypeError: If parameter ``secondaryUnit`` isn't of type :class:`str`. 

1612 :raises ValueError: If parameter ``secondaryUnit`` is empty. 

1613 """ 

1614 super().__init__(files, parent=parent) 

1615 

1616 if path is None: 

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

1618 elif not isinstance(path, str): 

1619 ex = TypeError("Parameter 'path' is not of type 'str'.") 

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

1621 raise ex 

1622 elif path == "": 

1623 raise ValueError("Parameter 'path' is empty.") 

1624 

1625 if designUnit is None: 

1626 raise ValueError("Parameter 'designUnit' is None.") 

1627 elif not isinstance(designUnit, str): 

1628 ex = TypeError("Parameter 'designUnit' is not of type 'str'.") 

1629 ex.add_note(f"Got type '{getFullyQualifiedName(designUnit)}'.") 

1630 raise ex 

1631 elif designUnit == "": 

1632 raise ValueError("Parameter 'designUnit' is empty.") 

1633 

1634 if secondaryUnit is not None: 

1635 if not isinstance(secondaryUnit, str): 

1636 ex = TypeError("Parameter 'secondaryUnit' is not of type 'str'.") 

1637 ex.add_note(f"Got type '{getFullyQualifiedName(secondaryUnit)}'.") 

1638 raise ex 

1639 elif secondaryUnit == "": 

1640 raise ValueError("Parameter 'secondaryUnit' is empty.") 

1641 

1642 self._path = path 

1643 self._designUnit = designUnit 

1644 self._secondaryUnit = secondaryUnit 

1645 

1646 @classmethod 

1647 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self: 

1648 """ 

1649 Parse an instance, its source files, coverage statistics and statements from its ``<instanceData>`` element. 

1650 

1651 :param element: The ``<instanceData>`` element. 

1652 :param parent: Optional, the report the instance belongs to. Default: ``None``. 

1653 :returns: The instance. 

1654 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

1655 :raises CodeCoverageError: If the instance states a kind of coverage statistics twice. 

1656 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in an instance of several 

1657 files. 

1658 """ 

1659 instance = cls( 

1660 element.attrib["path"], 

1661 element.attrib["du"], 

1662 element.attrib.get("sec"), 

1663 cls._ParseSourceTable(element), 

1664 parent=parent 

1665 ) 

1666 instance._ParseItems(element) 

1667 return instance 

1668 

1669 @readonly 

1670 def Path(self) -> str: 

1671 """ 

1672 Read-only property to access the hierarchical path of the instance (:attr:`_path`). 

1673 

1674 :returns: The path, separated by ``/``. 

1675 """ 

1676 return self._path 

1677 

1678 @readonly 

1679 def DesignUnit(self) -> str: 

1680 """ 

1681 Read-only property to access the name of the design unit (:attr:`_designUnit`). 

1682 

1683 :returns: The design unit's name, as Questa writes it: in lower case for VHDL. 

1684 """ 

1685 return self._designUnit 

1686 

1687 @readonly 

1688 def SecondaryUnit(self) -> Nullable[str]: 

1689 """ 

1690 Read-only property to access the name of the secondary unit (:attr:`_secondaryUnit`). 

1691 

1692 :returns: The architecture's name, or ``None`` - e.g. for a package. 

1693 """ 

1694 return self._secondaryUnit 

1695 

1696 

1697@export 

1698class DesignUnitData(Scope): 

1699 """ 

1700 A ``<DuData>``: a design unit, its instances' coverage combined. 

1701 """ 

1702 

1703 _designUnit: str #: Name of the design unit. 

1704 _secondaryUnit: Nullable[str] #: Name of the secondary unit (the architecture), if the design unit has one. 

1705 

1706 def __init__( 

1707 self, 

1708 designUnit: str, 

1709 secondaryUnit: Nullable[str] = None, 

1710 files: Nullable[Mapping[int, Path]] = None, 

1711 *, 

1712 parent: Nullable[Report] = None 

1713 ) -> None: 

1714 """ 

1715 Initialize the design unit, and append it to the scopes of its report. 

1716 

1717 :param designUnit: Name of the design unit. 

1718 :param secondaryUnit: Optional, name of the secondary unit (the architecture). Default: ``None``. 

1719 :param files: Optional, the source files, by the number the report gives them. Default: ``None``. 

1720 :param parent: Optional, the report the design unit belongs to. Default: ``None``. 

1721 :raises TypeError: If parameter ``files`` isn't a mapping. 

1722 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`. 

1723 :raises ValueError: If parameter ``files`` contains a negative key. 

1724 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`. 

1725 :raises TypeError: If parameter ``parent`` isn't of type 

1726 :class:`Report`. 

1727 :raises ValueError: If parameter ``designUnit`` is ``None``. 

1728 :raises TypeError: If parameter ``designUnit`` isn't of type :class:`str`. 

1729 :raises ValueError: If parameter ``designUnit`` is empty. 

1730 :raises TypeError: If parameter ``secondaryUnit`` isn't of type :class:`str`. 

1731 :raises ValueError: If parameter ``secondaryUnit`` is empty. 

1732 """ 

1733 super().__init__(files, parent=parent) 

1734 

1735 if designUnit is None: 

1736 raise ValueError("Parameter 'designUnit' is None.") 

1737 elif not isinstance(designUnit, str): 

1738 ex = TypeError("Parameter 'designUnit' is not of type 'str'.") 

1739 ex.add_note(f"Got type '{getFullyQualifiedName(designUnit)}'.") 

1740 raise ex 

1741 elif designUnit == "": 

1742 raise ValueError("Parameter 'designUnit' is empty.") 

1743 

1744 if secondaryUnit is not None: 

1745 if not isinstance(secondaryUnit, str): 

1746 ex = TypeError("Parameter 'secondaryUnit' is not of type 'str'.") 

1747 ex.add_note(f"Got type '{getFullyQualifiedName(secondaryUnit)}'.") 

1748 raise ex 

1749 elif secondaryUnit == "": 1749 ↛ 1752line 1749 didn't jump to line 1752 because the condition on line 1749 was always true

1750 raise ValueError("Parameter 'secondaryUnit' is empty.") 

1751 

1752 self._designUnit = designUnit 

1753 self._secondaryUnit = secondaryUnit 

1754 

1755 @classmethod 

1756 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self: 

1757 """ 

1758 Parse a design unit, its source files, coverage statistics and statements from its ``<DuData>`` element. 

1759 

1760 :param element: The ``<DuData>`` element. 

1761 :param parent: Optional, the report the design unit belongs to. Default: ``None``. 

1762 :returns: The design unit. 

1763 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

1764 :raises CodeCoverageError: If the design unit states a kind of coverage statistics twice. 

1765 :raises CodeCoverageError: If a coverage item names an unknown file number, or none in a design unit of several 

1766 files. 

1767 """ 

1768 designUnit = cls(element.attrib["du"], element.attrib.get("sec"), cls._ParseSourceTable(element), parent=parent) 

1769 designUnit._ParseItems(element) 

1770 return designUnit 

1771 

1772 @readonly 

1773 def DesignUnit(self) -> str: 

1774 """ 

1775 Read-only property to access the name of the design unit (:attr:`_designUnit`). 

1776 

1777 :returns: The design unit's name, as Questa writes it: in lower case for VHDL. 

1778 """ 

1779 return self._designUnit 

1780 

1781 @readonly 

1782 def SecondaryUnit(self) -> Nullable[str]: 

1783 """ 

1784 Read-only property to access the name of the secondary unit (:attr:`_secondaryUnit`). 

1785 

1786 :returns: The architecture's name, or ``None`` - e.g. for a package. 

1787 """ 

1788 return self._secondaryUnit 

1789 

1790 

1791@export 

1792class FileData(Scope): 

1793 """ 

1794 A ``<fileData>``: a source file. 

1795 """ 

1796 

1797 _path: _Path #: Path of the source file, as the report states it. 

1798 

1799 def __init__( 

1800 self, 

1801 path: Path, 

1802 files: Nullable[Mapping[int, Path]] = None, 

1803 *, 

1804 parent: Nullable[Report] = None 

1805 ) -> None: 

1806 """ 

1807 Initialize the source file, and append it to the scopes of its report. 

1808 

1809 :param path: Path of the source file, as the report states it. 

1810 :param files: Optional, the source files, by the number the report gives them. Default: ``None``. 

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

1812 :raises TypeError: If parameter ``files`` isn't a mapping. 

1813 :raises TypeError: If parameter ``files`` contains a key not of type :class:`int`. 

1814 :raises ValueError: If parameter ``files`` contains a negative key. 

1815 :raises TypeError: If parameter ``files`` contains a value not of type :class:`~pathlib.Path`. 

1816 :raises TypeError: If parameter ``parent`` isn't of type 

1817 :class:`Report`. 

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

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

1820 """ 

1821 super().__init__(files, parent=parent) 

1822 

1823 if path is None: 

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

1825 elif not isinstance(path, Path): 

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

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

1828 raise ex 

1829 

1830 self._path = path 

1831 

1832 @classmethod 

1833 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self: 

1834 """ 

1835 Parse a source file, its coverage statistics and statements from its ``<fileData>`` element. 

1836 

1837 :param element: The ``<fileData>`` element. 

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

1839 :returns: The source file. 

1840 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

1841 :raises CodeCoverageError: If the source file states a kind of coverage statistics twice. 

1842 :raises CodeCoverageError: If a coverage item names an unknown file number. 

1843 """ 

1844 fileData = cls(Path(element.attrib["path"]), cls._ParseSourceTable(element), parent=parent) 

1845 fileData._ParseItems(element) 

1846 return fileData 

1847 

1848 @readonly 

1849 def Path(self) -> _Path: 

1850 """ 

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

1852 

1853 :returns: The path, as the report states it. 

1854 """ 

1855 return self._path 

1856 

1857 

1858@export 

1859class Totals(StatisticsMixin): 

1860 """ 

1861 A summary record of a report - ``<summaryByFile>`` or ``<summaryByInstance>`` -: the coverage statistics of all its 

1862 files or instances together. 

1863 """ 

1864 

1865 _parent: Nullable[Report] #: The report the summary record belongs to. 

1866 _mode: ReportMode #: What the summary record sums up: files or instances. 

1867 _count: int #: Number of files or instances summed up. 

1868 

1869 def __init__(self, mode: ReportMode, count: int, *, parent: Nullable[Report] = None) -> None: 

1870 """ 

1871 Initialize the summary record, and add it to the summary records of its report. 

1872 

1873 Its coverage statistics are added by creating them with this summary record as their parent. 

1874 

1875 :param mode: What the summary record sums up: :attr:`ReportMode.ByFile` or 

1876 :attr:`ReportMode.ByInstance`. 

1877 :param count: Number of files or instances summed up. 

1878 :param parent: Optional, the report the summary record belongs to; it is added to its summary records 

1879 by :attr:`Mode`. Default: ``None``. 

1880 :raises ValueError: If parameter ``mode`` is ``None``. 

1881 :raises TypeError: If parameter ``mode`` isn't of type :class:`ReportMode`. 

1882 :raises ValueError: If parameter ``mode`` is :attr:`ReportMode.ByDesignUnit`. 

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

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

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

1886 :raises TypeError: If parameter ``parent`` isn't of type 

1887 :class:`Report`. 

1888 :raises CodeCoverageError: If the report already has a summary record of this mode. 

1889 """ 

1890 super().__init__() 

1891 

1892 if mode is None: 

1893 raise ValueError("Parameter 'mode' is None.") 

1894 elif not isinstance(mode, ReportMode): 

1895 ex = TypeError("Parameter 'mode' is not of type 'ReportMode'.") 

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

1897 raise ex 

1898 elif mode is ReportMode.ByDesignUnit: 

1899 ex = ValueError("Parameter 'mode' is not 'ByFile' or 'ByInstance'.") 

1900 ex.add_note(f"Got value '{mode.name}'.") 

1901 raise ex 

1902 

1903 if count is None: 

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

1905 elif not isinstance(count, int): 

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

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

1908 raise ex 

1909 elif count < 0: 

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

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

1912 raise ex 

1913 

1914 if parent is not None: 

1915 if not isinstance(parent, Report): 

1916 ex = TypeError("Parameter 'parent' is not of type 'Report'.") 

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

1918 raise ex 

1919 elif mode in parent._totals: 

1920 raise CodeCoverageError(f"Summary record '{mode}' is stated twice.") 

1921 

1922 self._parent = parent 

1923 self._mode = mode 

1924 self._count = count 

1925 

1926 if parent is not None: 

1927 parent._totals[mode] = self 

1928 

1929 @classmethod 

1930 def Parse(cls, element: _Element, *, parent: Nullable[Report] = None) -> Self: 

1931 """ 

1932 Parse a summary record and its coverage statistics from its ``<summaryByFile>`` or ``<summaryByInstance>`` 

1933 element. 

1934 

1935 :param element: The summary record's element. 

1936 :param parent: Optional, the report the summary record belongs to. Default: ``None``. 

1937 :returns: The summary record. 

1938 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

1939 """ 

1940 if element.tag == "summaryByFile": 1940 ↛ 1943line 1940 didn't jump to line 1943 because the condition on line 1940 was always true

1941 totals = cls(ReportMode.ByFile, int(element.attrib["files"]), parent=parent) 

1942 else: 

1943 totals = cls(ReportMode.ByInstance, int(element.attrib["instances"]), parent=parent) 

1944 

1945 totals._ParseStatistics(element) 

1946 return totals 

1947 

1948 @readonly 

1949 def Parent(self) -> Nullable[Report]: 

1950 """ 

1951 Read-only property to access the report the summary record belongs to (:attr:`_parent`). 

1952 

1953 :returns: The report; ``None`` if the summary record belongs to none. 

1954 """ 

1955 return self._parent 

1956 

1957 @readonly 

1958 def Mode(self) -> ReportMode: 

1959 """ 

1960 Read-only property to access what the summary record sums up (:attr:`_mode`). 

1961 

1962 :returns: :attr:`ReportMode.ByFile` or :attr:`ReportMode.ByInstance`. 

1963 """ 

1964 return self._mode 

1965 

1966 @readonly 

1967 def Count(self) -> int: 

1968 """ 

1969 Read-only property to access the number of files or instances summed up (:attr:`_count`). 

1970 

1971 :returns: The number of files or instances. 

1972 """ 

1973 return self._count 

1974 

1975 

1976_Totals = Totals 

1977 

1978 

1979@export 

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

1981 """ 

1982 The root element ``<coverage_report>`` and its ``<code_coverage_report>``: the Questa version and the command, which 

1983 wrote it, its mode, the summary records and the scopes. 

1984 

1985 The root of the format's model: a summary record or a scope names it as its parent. 

1986 """ 

1987 

1988 _toolVersion: Nullable[str] #: Version of Questa, which wrote the report. 

1989 _command: Nullable[str] #: The command, which wrote the report. 

1990 _mode: Nullable[ReportMode] #: How the report groups its coverage data. 

1991 _hasLineDetails: bool #: Whether the report has details: source files and coverage items. 

1992 _totals: dict[ReportMode, _Totals] #: The summary records, by what they sum up. 

1993 _scopes: list[Scope] #: The scopes. 

1994 

1995 def __init__(self) -> None: 

1996 """ 

1997 Initialize an empty report. 

1998 """ 

1999 self._toolVersion = None 

2000 self._command = None 

2001 self._mode = None 

2002 self._hasLineDetails = False 

2003 self._totals = {} 

2004 self._scopes = [] 

2005 

2006 @readonly 

2007 def ToolVersion(self) -> Nullable[str]: 

2008 """ 

2009 Read-only property to access the version of Questa, which wrote the report (:attr:`_toolVersion`). 

2010 

2011 Questa states its version since a release between 10.x and 2023.3, e.g. ``2026.2``. 

2012 

2013 :returns: The version, as the report states it; ``None`` if the report doesn't state it, or before it was converted. 

2014 """ 

2015 return self._toolVersion 

2016 

2017 @readonly 

2018 def Command(self) -> Nullable[str]: 

2019 """ 

2020 Read-only property to access the command, which wrote the report (:attr:`_command`). 

2021 

2022 :returns: The command, as the report states it; ``None`` if the report doesn't state it, or before it was converted. 

2023 """ 

2024 return self._command 

2025 

2026 @readonly 

2027 def Mode(self) -> Nullable[ReportMode]: 

2028 """ 

2029 Read-only property to access how the report groups its coverage data (:attr:`_mode`). 

2030 

2031 :returns: The mode; ``None`` before the report was converted. 

2032 """ 

2033 return self._mode 

2034 

2035 @readonly 

2036 def HasLineDetails(self) -> bool: 

2037 """ 

2038 Read-only property to access whether the report has details (:attr:`_hasLineDetails`). 

2039 

2040 A report written with ``-details`` names the source files of its scopes and lists their coverage items; it states 

2041 ``lines="1"``. 

2042 

2043 :returns: ``True``, if the report states details. 

2044 """ 

2045 return self._hasLineDetails 

2046 

2047 @readonly 

2048 def Totals(self) -> dict[ReportMode, _Totals]: 

2049 """ 

2050 Read-only property to access the summary records (:attr:`_totals`). 

2051 

2052 :returns: The summary records, by what they sum up; none, if the report has no summary records. 

2053 """ 

2054 return self._totals 

2055 

2056 @readonly 

2057 def Scopes(self) -> list[Scope]: 

2058 """ 

2059 Read-only property to access the scopes (:attr:`_scopes`). 

2060 

2061 :returns: The scopes, in the order the report lists them. 

2062 """ 

2063 return self._scopes 

2064 

2065 def IterateStatements(self) -> Generator[Statement, None, None]: 

2066 """ 

2067 Iterate the statements of all scopes. 

2068 

2069 :returns: A generator of the statements, scope by scope, in the order the report lists them. 

2070 """ 

2071 for scope in self._scopes: 

2072 yield from scope._statements 

2073 

2074 

2075@export 

2076class Document(cc_Document, Report): 

2077 """ 

2078 A QuestaSim coverage report XML file: read into the format's model, and converted to the common model. 

2079 """ 

2080 

2081 _xmlDocument: Nullable[_ElementTree] #: The parsed and validated XML document, after :meth:`Analyze`. 

2082 

2083 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False) -> None: 

2084 """ 

2085 Initialize the report, and optionally read it. 

2086 

2087 :param xmlReportFile: Path to the coverage report XML file. 

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

2089 """ 

2090 super().__init__(xmlReportFile) 

2091 Report.__init__(self) 

2092 

2093 self._xmlDocument = None 

2094 

2095 if analyzeAndConvert: 

2096 self.Analyze() 

2097 self.Convert() 

2098 

2099 def Analyze(self) -> None: 

2100 """ 

2101 Parse the XML file, check its root element, and validate the file against the XML schema (:data:`SCHEMA`). 

2102 

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

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

2105 :raises CodeCoverageError: If the file isn't well-formed XML. 

2106 :raises CodeCoverageError: If the root element isn't ``<coverage_report>``. 

2107 :raises CodeCoverageError: If the report is a functional coverage report. |br| 

2108 Write the report without '-cvg', '-directive' and '-assert'. 

2109 :raises CodeCoverageError: If the XML schema can't be located or parsed. 

2110 :raises CodeCoverageError: If the file isn't valid according to the XML schema. 

2111 """ 

2112 if not self._path.exists(): 

2113 raise CodeCoverageError(f"QuestaSim coverage report '{self._path}' does not exist.") \ 

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

2115 

2116 with Stopwatch() as sw: 

2117 try: 

2118 content = self._path.read_bytes() 

2119 except OSError as ex: 

2120 raise CodeCoverageError(f"Couldn't read QuestaSim coverage report '{self._path}'.") from ex 

2121 

2122 try: 

2123 xmlDocument = parse(BytesIO(content), XMLParser(ns_clean=True), base_url=str(self._path)) 

2124 except XMLSyntaxError as ex: 

2125 raise CodeCoverageError(f"XML syntax error in QuestaSim coverage report '{self._path}'.") from ex 

2126 

2127 rootElement: _Element = xmlDocument.getroot() 

2128 if rootElement.tag != "coverage_report": 

2129 ex = CodeCoverageError(f"Root element of '{self._path}' is not '<coverage_report>'.") 

2130 ex.add_note(f"Got root element '<{rootElement.tag}>'.") 

2131 raise ex 

2132 elif rootElement.find("functional_coverage_report") is not None: 

2133 ex = CodeCoverageError(f"'{self._path}' is a functional coverage report, not a code coverage report.") 

2134 ex.add_note("Write the report without '-cvg', '-directive' and '-assert'.") 

2135 raise ex 

2136 

2137 try: 

2138 schemaResourceFile = getResourceFile(Resources, SCHEMA) 

2139 except ToolingException as ex: 

2140 raise CodeCoverageError(f"Couldn't locate XML Schema '{SCHEMA}' in package resources.") from ex 

2141 

2142 try: 

2143 xmlSchema = XMLSchema(parse(schemaResourceFile, XMLParser(ns_clean=True))) 

2144 except (XMLSyntaxError, XMLSchemaParseError) as ex: 

2145 raise CodeCoverageError(f"Error while parsing XML Schema '{SCHEMA}'.") from ex 

2146 

2147 if not xmlSchema.validate(xmlDocument): 

2148 ex = CodeCoverageError(f"Validation error for '{self._path}' using XSD schema '{SCHEMA}'.") 

2149 for logEntry in xmlSchema.error_log: 

2150 ex.add_note(str(logEntry)) 

2151 raise ex 

2152 

2153 self._xmlDocument = xmlDocument 

2154 

2155 self._analysisDuration = sw.Duration 

2156 

2157 def Convert(self) -> None: 

2158 """ 

2159 Convert the parsed and validated XML document to the format's model. 

2160 

2161 :raises CodeCoverageError: If the XML file was not analyzed before. |br| 

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

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

2164 :raises CodeCoverageError: If the report doesn't state exactly one mode. 

2165 :raises CodeCoverageError: If a coverage statistics element states more hit items than items. 

2166 :raises CodeCoverageError: If a scope or summary record states a kind of coverage statistics twice. 

2167 :raises CodeCoverageError: If a statement names an unknown file number, or none in a scope of several files. 

2168 """ 

2169 if self._xmlDocument is None: 

2170 ex = CodeCoverageError( 

2171 f"QuestaSim coverage report '{self._path}' needs to be read and analyzed by an XML parser." 

2172 ) 

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

2174 raise ex 

2175 

2176 with Stopwatch() as sw: 

2177 root: _Element = self._xmlDocument.getroot() 

2178 codeCoverage: _Element = root.find("code_coverage_report") 

2179 

2180 modes = [mode for mode in ReportMode if codeCoverage.attrib.get(mode.value) == "1"] 

2181 if len(modes) != 1: 

2182 ex = CodeCoverageError(f"QuestaSim coverage report '{self._path}' doesn't state exactly one mode.") 

2183 ex.add_note(f"Got modes: {', '.join(mode.value for mode in modes) or 'none'}.") 

2184 ex.add_note(f"One of the attributes {', '.join(mode.value for mode in ReportMode)} is '1'.") 

2185 raise ex 

2186 

2187 self._toolVersion = root.attrib.get("questa_version") 

2188 self._command = root.attrib.get("command") 

2189 self._mode = modes[0] 

2190 self._hasLineDetails = codeCoverage.attrib.get("lines") == "1" 

2191 

2192 for element in codeCoverage: 

2193 if element.tag in ("summaryByFile", "summaryByInstance"): 2193 ↛ 2194line 2193 didn't jump to line 2194 because the condition on line 2193 was never true

2194 Totals.Parse(element, parent=self) 

2195 elif element.tag == "instanceData": 2195 ↛ 2197line 2195 didn't jump to line 2197 because the condition on line 2195 was always true

2196 InstanceData.Parse(element, parent=self) 

2197 elif element.tag == "DuData": 

2198 DesignUnitData.Parse(element, parent=self) 

2199 else: 

2200 FileData.Parse(element, parent=self) 

2201 

2202 self._conversionDuration = sw.Duration 

2203 

2204 def ToCoverageSummary(self) -> CoverageSummary: 

2205 """ 

2206 Convert the statements and branches of the format's model to the common model, and aggregate it. 

2207 

2208 The statements of all scopes are merged by file, line and index, the ``if`` and ``case`` statements by the file, 

2209 line and index of their first branch; merged items count as often as their items together. Each branch of an ``if`` 

2210 or ``case`` statement becomes a branch of the line of its first branch, taken as often as the branch's true count 

2211 or hits. A line ran as often as its least often run statement or branching statement - the sum of its branches' 

2212 counts. A line, which ran, is covered, if all its branches were taken, otherwise partially covered. 

2213 

2214 :returns: The report's root of the common model, named after the report file. 

2215 :raises CodeCoverageError: If the report has no statements: it was written without details. |br| 

2216 Write the report with details: 'vcover report -xml -details ...'. 

2217 :raises CodeCoverageError: If two scopes state the same ``if`` or ``case`` statement with different numbers of 

2218 branches. 

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

2220 """ 

2221 hitsPerStatement: dict[tuple[Path, int, int], int] = {} 

2222 branchesPerDecision: dict[tuple[Path, int, int, str], list[int]] = {} 

2223 for scope in self._scopes: 

2224 for statement in scope._statements: 

2225 key = (statement._file, statement._lineNumber, statement._index) 

2226 hitsPerStatement[key] = hitsPerStatement.get(key, 0) + statement._hits 

2227 

2228 decisions = [ 

2229 ("if", [branch._trueCount for branch in ifStatement._branches], ifStatement._branches[0]) 

2230 for ifStatement in scope._ifStatements 

2231 ] + [ 

2232 ("case", [branch._hits for branch in caseStatement._branches], caseStatement._branches[0]) 

2233 for caseStatement in scope._caseStatements 

2234 ] 

2235 for kind, counts, first in decisions: 

2236 key = (first._file, first._lineNumber, first._index, kind) 

2237 if (merged := branchesPerDecision.get(key)) is None: 

2238 branchesPerDecision[key] = counts 

2239 elif len(merged) != len(counts): 2239 ↛ 2240line 2239 didn't jump to line 2240 because the condition on line 2239 was never true

2240 ex = CodeCoverageError( 

2241 f"The '{kind}' statement at '{first._file.as_posix()}:{first._lineNumber}' has {len(counts)} branches in " 

2242 f"one scope, but {len(merged)} in another." 

2243 ) 

2244 ex.add_note("The scopes merge their branches by position.") 

2245 raise ex 

2246 else: 

2247 branchesPerDecision[key] = [total + count for total, count in zip(merged, counts)] 

2248 

2249 if len(hitsPerStatement) == 0: 

2250 ex = CodeCoverageError(f"QuestaSim coverage report '{self._path}' has no statements to convert.") 

2251 ex.add_note("Write the report with details: 'vcover report -xml -details ...'.") 

2252 raise ex 

2253 

2254 countsPerLine: dict[Path, dict[int, list[int]]] = {} 

2255 branchesPerLine: dict[Path, dict[int, list[int]]] = {} 

2256 for (file, lineNumber, _), hits in hitsPerStatement.items(): 

2257 countsPerLine.setdefault(file, {}).setdefault(lineNumber, []).append(hits) 

2258 

2259 for (file, lineNumber, _, _), counts in branchesPerDecision.items(): 

2260 countsPerLine.setdefault(file, {}).setdefault(lineNumber, []).append(sum(counts)) 

2261 branchesPerLine.setdefault(file, {}).setdefault(lineNumber, []).extend(counts) 

2262 

2263 summary = CoverageSummary(self._path.stem) 

2264 for path, counts in countsPerLine.items(): 

2265 file = summary.GetOrAddFile(path) 

2266 branches = branchesPerLine.get(path, {}) 

2267 for lineNumber in sorted(counts): 

2268 count = min(counts[lineNumber]) 

2269 branchCounts = branches.get(lineNumber, []) 

2270 if count == 0: 

2271 status = LineCoverageStatus.Uncovered 

2272 elif any(branchCount == 0 for branchCount in branchCounts): 

2273 status = LineCoverageStatus.PartiallyCovered 

2274 else: 

2275 status = LineCoverageStatus.Covered 

2276 

2277 line = cc_Line(lineNumber, status, count, parent=file) 

2278 for branchCount in branchCounts: 

2279 branchStatus = LineCoverageStatus.Covered if branchCount > 0 else LineCoverageStatus.Uncovered 

2280 cc_Branch(branchStatus, branchCount, parent=line) 

2281 

2282 summary.Aggregate() 

2283 return summary