Coverage for pyEDAA/Reports/CodeCoverage/UCIS/__init__.py: 96%

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

32The XML interchange format of the Accellera UCIS standard: a model of the format, read from a report and converted to 

33the common model. 

34 

35The `Unified Coverage Interoperability Standard (UCIS) <https://www.accellera.org/downloads/standards/ucis>`__ 1.0 

36specifies a coverage database's data model, a C API and an XML interchange format. An XML report names the namespace 

37``UCIS`` and its root ``<UCIS>``, which states the UCIS version (:class:`FormatVersion`), the tool, which wrote the 

38report, and when. It is validated against the XML schema of that version (:data:`SCHEMAS`): :file:`UCIS-1.0.xsd`, 

39written after the schema the standard specifies. 

40 

41The format's model keeps what the report states: a :class:`Document` holds 

42:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.SourceFile` elements - by their ID -, 

43:class:`~pyEDAA.Reports.CodeCoverage.UCIS.HistoryNodes.HistoryNode` elements - the tests and merges, by their ID - and 

44:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage` elements. An instance holds its coverage of each 

45kind, once per metric mode: :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Blocks.BlockCoverage` - statements, blocks, 

46processes -, :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Branches.BranchCoverage` - branching statements and their 

47branches -, :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Toggles.ToggleCoverage`, 

48:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Conditions.ConditionCoverage`, 

49:class:`~pyEDAA.Reports.CodeCoverage.UCIS.FSMs.FSMCoverage`, 

50:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Assertions.AssertionCoverage` and 

51:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Covergroups.CovergroupCoverage`. A coverage item states its count in a 

52:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`, and where it is in a 

53:class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`: the source file's ID, the line and the index of the 

54statement in the line. Each element's constructor takes typed values, so the model can be built by hand: an element 

55below the report names its parent with the keyword parameter ``parent`` and is added to it. Its class method ``Parse`` 

56reads the element's XML element. 

57 

58:meth:`Document.ToCoverageSummary` converts the statement and branch coverage to the common model of 

59:mod:`pyEDAA.Reports.CodeCoverage`: 

60 

61* The statements are the statements of the block coverages, and the statements of their blocks, which ran as often as 

62 their block. A statement is identified by its file, its line and its index in the line; with ``mergeInstances``, the 

63 statements of all instances are merged into one, which ran as often as in all instances together. 

64* A file's path is the one its source file states. 

65* A line ran as often as its least often run statement: it is covered, if all its statements ran. A line, whose 

66 statements are all excluded, is excluded; an excluded statement doesn't count for its line. 

67* A branching statement's branches - with ``mergeInstances`` summed over the instances - become branches of its line; a 

68 line, which ran without taking all its branches, is partially covered. A branch goes to the line of its first 

69 statement. An excluded branch or branching statement isn't converted. 

70* A design unit, whose instances name it, becomes a :class:`~pyEDAA.Reports.CodeCoverage.Module`, spanning the lines 

71 of its statements, if they are in one file. 

72* The toggle, condition, expression, FSM, assertion and covergroup coverage stays in the format's model. 

73 

74.. rubric:: Example 

75 

76.. code-block:: Python 

77 

78 from pathlib import Path 

79 from pyEDAA.Reports.CodeCoverage.UCIS import Document 

80 

81 report = Document(Path("coverage.xml"), analyzeAndConvert=True) 

82 summary = report.ToCoverageSummary(mergeInstances=True) 

83 for file in summary.IterateFiles(): 

84 print(f"{file.Path}: {file.LineCoverage:.1%}") 

85""" 

86from __future__ import annotations 

87 

88from datetime import datetime 

89from io import BytesIO 

90from pathlib import Path 

91from typing import ClassVar, Generator, Optional as Nullable 

92 

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

94from lxml.etree import _Element, _ElementTree, parse 

95from pyTooling.Common import getFullyQualifiedName, getResourceFile, StringEnum 

96from pyTooling.Decorators import export, readonly 

97from pyTooling.Exceptions import ToolingException 

98from pyTooling.MetaClasses import ExtendedType 

99from pyTooling.Stopwatch import Stopwatch 

100 

101from pyEDAA.Reports import Resources 

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

103from pyEDAA.Reports.CodeCoverage import Document as cc_Document, Line as cc_Line, LineCoverageStatus 

104from pyEDAA.Reports.CodeCoverage import Module as cc_Module 

105from pyEDAA.Reports.CodeCoverage.UCIS.Blocks import Statement 

106from pyEDAA.Reports.CodeCoverage.UCIS.Branches import BranchStatement 

107from pyEDAA.Reports.CodeCoverage.UCIS.HistoryNodes import HistoryNode 

108from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage, SourceFile 

109 

110 

111__all__ = ["NAMESPACE", "SCHEMAS"] 

112 

113NAMESPACE = "UCIS" #: The XML namespace of the format's elements. 

114 

115 

116@export 

117class FormatVersion(StringEnum): 

118 """ 

119 Version of the UCIS XML interchange format: the UCIS version a report's root states in ``ucisVersion``. 

120 """ 

121 

122 Version1_0 = "1.0" #: UCIS 1.0, June 2012. 

123 

124 

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

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

127_FormatVersion = FormatVersion 

128 

129SCHEMAS: dict[FormatVersion, str] = { 

130 FormatVersion.Version1_0: "UCIS-1.0.xsd" 

131} #: Per format version, the XML schema a report of that version is validated against. 

132 

133 

134@export 

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

136 """ 

137 The root element ``<UCIS>``: its UCIS version, the tool, which wrote it, and when, its source files, history nodes and 

138 instances. 

139 

140 The root of the format's model: a source file, a history node or an instance names it as its parent. 

141 """ 

142 

143 _formatVersion: Nullable[_FormatVersion] #: The UCIS version the report states. 

144 _writtenBy: Nullable[str] #: Name of the tool, which wrote the report. 

145 _writtenTime: Nullable[datetime] #: When the report was written. 

146 _sourceFiles: dict[int, SourceFile] #: The source files, by ID. 

147 _historyNodes: dict[int, HistoryNode] #: The history nodes, by ID. 

148 _instances: list[InstanceCoverage] #: The instances. 

149 

150 def __init__(self) -> None: 

151 """ 

152 Initialize an empty report. 

153 """ 

154 self._formatVersion = None 

155 self._writtenBy = None 

156 self._writtenTime = None 

157 self._sourceFiles = {} 

158 self._historyNodes = {} 

159 self._instances = [] 

160 

161 @readonly 

162 def FormatVersion(self) -> Nullable[_FormatVersion]: 

163 """ 

164 Read-only property to access the UCIS version the report states (:attr:`_formatVersion`). 

165 

166 :returns: The version; ``None`` before the report was analyzed. 

167 """ 

168 return self._formatVersion 

169 

170 @readonly 

171 def WrittenBy(self) -> Nullable[str]: 

172 """ 

173 Read-only property to access the name of the tool, which wrote the report (:attr:`_writtenBy`). 

174 

175 :returns: The tool's name, as stated; ``None`` before the report was converted. 

176 """ 

177 return self._writtenBy 

178 

179 @readonly 

180 def WrittenTime(self) -> Nullable[datetime]: 

181 """ 

182 Read-only property to access when the report was written (:attr:`_writtenTime`). 

183 

184 :returns: The date and time; ``None`` before the report was converted. 

185 """ 

186 return self._writtenTime 

187 

188 @readonly 

189 def SourceFiles(self) -> dict[int, SourceFile]: 

190 """ 

191 Read-only property to access the source files (:attr:`_sourceFiles`). 

192 

193 :returns: The source files, by the ID statement identifiers name them by. 

194 """ 

195 return self._sourceFiles 

196 

197 @readonly 

198 def HistoryNodes(self) -> dict[int, HistoryNode]: 

199 """ 

200 Read-only property to access the history nodes (:attr:`_historyNodes`). 

201 

202 :returns: The history nodes, by the ID bins name them by. 

203 """ 

204 return self._historyNodes 

205 

206 @readonly 

207 def Instances(self) -> list[InstanceCoverage]: 

208 """ 

209 Read-only property to access the instances (:attr:`_instances`). 

210 

211 :returns: The instances, in the order the report lists them. 

212 """ 

213 return self._instances 

214 

215 def IterateStatements(self) -> Generator[Statement, None, None]: 

216 """ 

217 Iterate the statements of all instances' block coverages. 

218 

219 :returns: A generator of the statements, in the order the report lists them. 

220 """ 

221 for instance in self._instances: 

222 for blockCoverage in instance._blockCoverages: 

223 yield from blockCoverage._statements 

224 

225 def IterateBranchStatements(self) -> Generator[BranchStatement, None, None]: 

226 """ 

227 Iterate the branching statements of all instances' branch coverages, nested ones included. 

228 

229 :returns: A generator of the branching statements, each followed by the branching statements nested in it. 

230 """ 

231 for instance in self._instances: 

232 for branchCoverage in instance._branchCoverages: 

233 yield from branchCoverage.IterateStatements() 

234 

235 

236@export 

237class Document(cc_Document, Report): 

238 """ 

239 A UCIS XML code coverage report: read into the format's model, and converted to the common model. 

240 """ 

241 

242 _NAMESPACE: ClassVar[Nullable[str]] = NAMESPACE #: The XML namespace of the elements; ``None`` for none. 

243 _SCHEMAS: ClassVar[dict[FormatVersion, str]] = SCHEMAS #: Per format version, the XML schema to validate with. 

244 

245 _xmlDocument: Nullable[_ElementTree] #: The parsed and validated XML document, after :meth:`Analyze`. 

246 

247 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False) -> None: 

248 """ 

249 Initialize the report, and optionally read it. 

250 

251 :param xmlReportFile: Path to the UCIS XML file. 

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

253 """ 

254 super().__init__(xmlReportFile) 

255 Report.__init__(self) 

256 

257 self._xmlDocument = None 

258 

259 if analyzeAndConvert: 

260 self.Analyze() 

261 self.Convert() 

262 

263 def Analyze(self) -> None: 

264 """ 

265 Parse the XML file, check its root element, read its UCIS version, and validate the file against the XML schema of 

266 that version (:attr:`_SCHEMAS`). 

267 

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

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

270 :raises CodeCoverageError: If the file isn't well-formed XML. 

271 :raises CodeCoverageError: If the root element isn't ``<UCIS>`` of the namespace :attr:`_NAMESPACE`. 

272 :raises CodeCoverageError: If the file states no supported UCIS version. 

273 :raises CodeCoverageError: If the XML schema can't be located or parsed. 

274 :raises CodeCoverageError: If the file isn't valid according to the XML schema. 

275 """ 

276 if not self._path.exists(): 

277 raise CodeCoverageError(f"UCIS XML file '{self._path}' does not exist.") \ 

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

279 

280 with Stopwatch() as sw: 

281 try: 

282 content = self._path.read_bytes() 

283 except OSError as ex: 

284 raise CodeCoverageError(f"Couldn't read UCIS XML file '{self._path}'.") from ex 

285 

286 try: 

287 xmlDocument = parse(BytesIO(content), XMLParser(ns_clean=True), base_url=str(self._path)) 

288 except XMLSyntaxError as ex: 

289 raise CodeCoverageError(f"XML syntax error in UCIS XML file '{self._path}'.") from ex 

290 

291 rootElement: _Element = xmlDocument.getroot() 

292 rootTag = "UCIS" if self._NAMESPACE is None else f"{{{self._NAMESPACE}}}UCIS" 

293 if rootElement.tag != rootTag: 

294 ex = CodeCoverageError(f"Root element of '{self._path}' is not '<{rootTag}>'.") 

295 ex.add_note(f"Got root element '<{rootElement.tag}>'.") 

296 raise ex 

297 

298 version = rootElement.attrib.get("ucisVersion") 

299 if version not in [member.value for member in FormatVersion]: 

300 ex = CodeCoverageError(f"UCIS XML file '{self._path}' states an unsupported UCIS version.") 

301 ex.add_note(f"Got version '{version}'." if version is not None else "Got no version.") 

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

303 raise ex 

304 

305 formatVersion = FormatVersion(version) 

306 schemaFile = self._SCHEMAS[formatVersion] 

307 try: 

308 schemaResourceFile = getResourceFile(Resources, schemaFile) 

309 except ToolingException as ex: 

310 raise CodeCoverageError(f"Couldn't locate XML Schema '{schemaFile}' in package resources.") from ex 

311 

312 try: 

313 xmlSchema = XMLSchema(parse(schemaResourceFile, XMLParser(ns_clean=True))) 

314 except (XMLSyntaxError, XMLSchemaParseError) as ex: 

315 raise CodeCoverageError(f"Error while parsing XML Schema '{schemaFile}'.") from ex 

316 

317 if not xmlSchema.validate(xmlDocument): 

318 ex = CodeCoverageError(f"Validation error for '{self._path}' using XSD schema '{schemaFile}'.") 

319 for logEntry in xmlSchema.error_log: 

320 ex.add_note(str(logEntry)) 

321 raise ex 

322 

323 self._xmlDocument = xmlDocument 

324 self._formatVersion = formatVersion 

325 

326 self._analysisDuration = sw.Duration 

327 

328 def Convert(self) -> None: 

329 """ 

330 Convert the parsed and validated XML document to the format's model. 

331 

332 :raises CodeCoverageError: If the XML file was not analyzed before. |br| 

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

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

335 :raises CodeCoverageError: If the report states a source file's or a history node's ID twice. 

336 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states. 

337 """ 

338 if self._xmlDocument is None: 

339 ex = CodeCoverageError(f"UCIS XML file '{self._path}' needs to be read and analyzed by an XML parser.") 

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

341 raise ex 

342 

343 with Stopwatch() as sw: 

344 root: _Element = self._xmlDocument.getroot() 

345 self._writtenBy = root.attrib["writtenBy"] 

346 self._writtenTime = datetime.fromisoformat(root.attrib["writtenTime"]) 

347 

348 for sourceFileElement in root.iterfind("{*}sourceFiles"): 

349 if (sourceFileID := int(sourceFileElement.attrib["id"])) in self._sourceFiles: 

350 raise CodeCoverageError(f"UCIS XML file '{self._path}' states source file ID '{sourceFileID}' twice.") 

351 

352 SourceFile.Parse(sourceFileElement, parent=self) 

353 

354 for historyNodeElement in root.iterfind("{*}historyNodes"): 

355 if (historyNodeID := int(historyNodeElement.attrib["historyNodeId"])) in self._historyNodes: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true

356 raise CodeCoverageError(f"UCIS XML file '{self._path}' states history node ID '{historyNodeID}' twice.") 

357 

358 HistoryNode.Parse(historyNodeElement, parent=self) 

359 

360 for instanceElement in root.iterfind("{*}instanceCoverages"): 

361 InstanceCoverage.Parse(instanceElement, parent=self) 

362 

363 self._conversionDuration = sw.Duration 

364 

365 def ToCoverageSummary(self, mergeInstances: bool = False) -> CoverageSummary: 

366 """ 

367 Convert the statement and branch coverage of the format's model to the common model, and aggregate it. 

368 

369 A line ran as often as its least often run statement: it is covered, if all its statements ran, otherwise 

370 uncovered; partially covered, if it ran without taking all its branches. A line, whose statements are all excluded, 

371 is excluded. A design unit, whose statements are in one file, becomes a module spanning their lines. 

372 

373 :param mergeInstances: Optional, if true, merge the statements and branches of all instances. Default: 

374 ``False``. 

375 :returns: The report's root of the common model, named after the report file. Its source 

376 directories are the directories the tests ran in. 

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

378 :raises TypeError: If parameter ``mergeInstances`` isn't of type :class:`bool`. 

379 :raises CodeCoverageError: If a statement identifier names a source file the report doesn't state. 

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

381 """ 

382 if mergeInstances is None: 

383 raise ValueError("Parameter 'mergeInstances' is None.") 

384 elif not isinstance(mergeInstances, bool): 

385 ex = TypeError("Parameter 'mergeInstances' is not of type 'bool'.") 

386 ex.add_note(f"Got type '{getFullyQualifiedName(mergeInstances)}'.") 

387 raise ex 

388 

389 # A statement's or branching statement's key: the instance's index - 0, if merged -, file ID, line, index in line. 

390 statementCounts: dict[tuple[int, int, int, int], Nullable[int]] = {} # None: excluded 

391 branchCounts: dict[tuple[int, int, int, int], list[Nullable[int]]] = {} # per branch; None: excluded 

392 branchTargets: dict[tuple[int, int, int, int], list[int]] = {} # per branch: the line it goes to 

393 moduleLines: dict[str, set[tuple[int, int]]] = {} # file ID and line, per design unit 

394 

395 for instanceIndex, instance in enumerate(self._instances): 

396 scope = 0 if mergeInstances else instanceIndex 

397 lines = moduleLines.setdefault(instance._moduleName, set()) if instance._moduleName is not None else set() 

398 for blockCoverage in instance._blockCoverages: 

399 counted = [(statement._id, statement._bin, statement._isExcluded) for statement in blockCoverage._statements] 

400 for block in blockCoverage.IterateBlocks(): 

401 for statementID in block._statementIDs if len(block._statementIDs) > 0 else (block._id, ): 

402 counted.append((statementID, block._bin, block._isExcluded)) 

403 

404 for statementID, coverBin, isExcluded in counted: 

405 if statementID._fileID not in self._sourceFiles: 

406 ex = CodeCoverageError(f"UCIS statement identifier names source file ID '{statementID._fileID}'.") 

407 ex.add_note(f"The report states source file IDs: {', '.join(str(i) for i in self._sourceFiles)}.") 

408 raise ex 

409 

410 key = (scope, statementID._fileID, statementID._lineNumber, statementID._inlineCount) 

411 if isExcluded or coverBin._isExcluded: 

412 statementCounts.setdefault(key, None) 

413 else: 

414 previous = statementCounts.get(key) 

415 statementCounts[key] = coverBin._contents._coverageCount + (0 if previous is None else previous) 

416 

417 lines.add((statementID._fileID, statementID._lineNumber)) 

418 

419 for branchCoverage in instance._branchCoverages: 

420 for branchStatement in branchCoverage.IterateStatements(): 

421 statementID = branchStatement._id 

422 if statementID._fileID not in self._sourceFiles: 422 ↛ 423line 422 didn't jump to line 423 because the condition on line 422 was never true

423 ex = CodeCoverageError(f"UCIS statement identifier names source file ID '{statementID._fileID}'.") 

424 ex.add_note(f"The report states source file IDs: {', '.join(str(i) for i in self._sourceFiles)}.") 

425 raise ex 

426 elif branchStatement._isExcluded: 

427 continue 

428 

429 lines.add((statementID._fileID, statementID._lineNumber)) 

430 key = (scope, statementID._fileID, statementID._lineNumber, statementID._inlineCount) 

431 counts = branchCounts.setdefault(key, []) 

432 targets = branchTargets.setdefault(key, []) 

433 for index, branch in enumerate(branchStatement._branches): 

434 count = None if branch._bin._isExcluded else branch._bin._contents._coverageCount 

435 if index < len(counts): 

436 counts[index] = count if counts[index] is None else counts[index] + (0 if count is None else count) 

437 else: 

438 counts.append(count) 

439 targets.append(branch._id._lineNumber if branch._id._fileID == statementID._fileID else 0) 

440 

441 lineCounts: dict[tuple[int, int], list[Nullable[int]]] = {} 

442 for (_, fileID, lineNumber, _), count in statementCounts.items(): 

443 lineCounts.setdefault((fileID, lineNumber), []).append(count) 

444 

445 lineBranches: dict[tuple[int, int], list[tuple[int, int]]] = {} 

446 for key, counts in branchCounts.items(): 

447 branches = lineBranches.setdefault((key[1], key[2]), []) 

448 branches.extend( 

449 (count, target) for count, target in zip(counts, branchTargets[key]) if count is not None 

450 ) 

451 if (key[1], key[2]) not in lineCounts: 

452 lineCounts[(key[1], key[2])] = [sum(count for count in counts if count is not None)] 

453 

454 sourceDirectories = { 

455 historyNode._runDirectory: None 

456 for historyNode in self._historyNodes.values() if historyNode._runDirectory is not None 

457 } 

458 summary = CoverageSummary(self._path.stem, sourceDirectories=sourceDirectories) 

459 files = { 

460 fileID: summary.GetOrAddFile(self._sourceFiles[fileID]._path) for fileID in sorted({key[0] for key in lineCounts}) 

461 } 

462 for (fileID, lineNumber) in sorted(lineCounts): 

463 counts = [count for count in lineCounts[(fileID, lineNumber)] if count is not None] 

464 if len(counts) == 0: 

465 cc_Line(lineNumber, LineCoverageStatus.Excluded, parent=files[fileID]) 

466 continue 

467 

468 count = min(counts) 

469 branches = lineBranches.get((fileID, lineNumber), []) 

470 if count == 0: 

471 status = LineCoverageStatus.Uncovered 

472 elif any(branchCount == 0 for branchCount, _ in branches): 

473 status = LineCoverageStatus.PartiallyCovered 

474 else: 

475 status = LineCoverageStatus.Covered 

476 

477 cc_Line(lineNumber, status, count, parent=files[fileID]) 

478 

479 for (fileID, lineNumber), branches in lineBranches.items(): 

480 file = files[fileID] 

481 line = file.GetLine(lineNumber) 

482 for count, target in branches: 

483 status = LineCoverageStatus.Covered if count > 0 else LineCoverageStatus.Uncovered 

484 targetLine = file.GetLine(target) if 0 < target <= file._lastLineNumber else None 

485 cc_Branch(status, count, targetLine, parent=line) 

486 

487 for moduleName, lines in moduleLines.items(): 

488 if len(fileIDs := {fileID for fileID, _ in lines}) == 1: 

489 file = files[fileIDs.pop()] 

490 lineNumbers = [lineNumber for _, lineNumber in lines] 

491 cc_Module( 

492 moduleName, 

493 file=file, 

494 startLine=file.GetLine(min(lineNumbers)), 

495 endLine=file.GetLine(max(lineNumbers)), 

496 parent=summary 

497 ) 

498 

499 summary.Aggregate() 

500 return summary