Coverage for pyEDAA/Reports/CodeCoverage/UCIS/Branches.py: 79%

148 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 branch coverage of the UCIS XML interchange format: an instance's ``<branchCoverage>``, its branching statements 

33and their branches. 

34""" 

35from __future__ import annotations 

36 

37from typing import TYPE_CHECKING, Generator, Generic, Iterable 

38from typing import Optional as Nullable, Self, TypeVar 

39 

40from lxml.etree import _Element 

41from pyTooling.Common import getFullyQualifiedName 

42from pyTooling.Decorators import export, readonly 

43 

44from pyEDAA.Reports.CodeCoverage.UCIS.Bins import Bin 

45from pyEDAA.Reports.CodeCoverage.UCIS.Elements import Base, MetricCoverage, ObjectAttributesMixin, StatementID 

46from pyEDAA.Reports.CodeCoverage.UCIS.Elements import UserAttribute 

47 

48if TYPE_CHECKING: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true

49 from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage 

50 

51 

52__all__ = ["BranchStatementParentType"] 

53 

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

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

56_Bin = Bin 

57 

58BranchStatementParentType = TypeVar("BranchStatementParentType", bound="BranchCoverage | Branch") 

59"""A type variable for the parent of a :class:`BranchStatement`: a :class:`BranchCoverage` or a :class:`Branch`.""" 

60 

61 

62@export 

63class BranchCoverage(MetricCoverage): 

64 """ 

65 A ``<branchCoverage>`` of an instance: its branching statements. 

66 """ 

67 

68 _statements: list[BranchStatement[BranchCoverage]] #: The branching statements. 

69 

70 def __init__( 

71 self, 

72 metricMode: Nullable[str] = None, 

73 weight: int = 1, 

74 userAttributes: Nullable[Iterable[UserAttribute]] = None, 

75 *, 

76 parent: Nullable[InstanceCoverage] = None 

77 ) -> None: 

78 """ 

79 Initialize the branch coverage, and append it to the branch coverages of its instance. 

80 

81 Its branching statements are added by creating them with this coverage as their parent. 

82 

83 :param metricMode: Optional, the mode in which the coverage was measured. Default: ``None``. 

84 :param weight: Optional, weight of the mode in computing the coverage. Default: ``1``. 

85 :param userAttributes: Optional, the user-defined attributes. Default: ``None``. 

86 :param parent: Optional, the instance the coverage belongs to. Default: ``None``. 

87 :raises TypeError: If parameter ``userAttributes`` isn't iterable. 

88 :raises TypeError: If parameter ``userAttributes`` contains an element not of type 

89 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. 

90 :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`. 

91 :raises ValueError: If parameter ``weight`` is ``None``. 

92 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. 

93 :raises ValueError: If parameter ``weight`` is negative. 

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

95 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage`. 

96 """ 

97 super().__init__(metricMode, weight, userAttributes, parent=parent) 

98 

99 self._statements = [] 

100 

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

102 parent._branchCoverages.append(self) 

103 

104 @classmethod 

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

106 """ 

107 Parse a branch coverage and its branching statements from its ``<branchCoverage>`` element. 

108 

109 :param element: The ``<branchCoverage>`` element. 

110 :param parent: Optional, the instance the coverage belongs to. Default: ``None``. 

111 :returns: The branch coverage. 

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

113 """ 

114 branchCoverage = cls( 

115 element.attrib.get("metricMode"), 

116 int(element.attrib.get("weight", "1")), 

117 cls._ParseUserAttributes(element), 

118 parent=parent 

119 ) 

120 

121 for statementElement in element.iterfind("{*}statement"): 

122 BranchStatement.Parse(statementElement, parent=branchCoverage) 

123 

124 return branchCoverage 

125 

126 def IterateStatements(self) -> Generator[BranchStatement, None, None]: 

127 """ 

128 Iterate the branching statements, each followed by the branching statements nested in its branches. 

129 

130 :returns: A generator of the branching statements. 

131 """ 

132 for statement in self._statements: 

133 yield statement 

134 yield from statement.IterateStatements() 

135 

136 @readonly 

137 def Statements(self) -> list[BranchStatement[BranchCoverage]]: 

138 """ 

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

140 

141 :returns: The branching statements, which aren't nested in a branch, in the order the report lists them. 

142 """ 

143 return self._statements 

144 

145 

146@export 

147class BranchStatement(Base, ObjectAttributesMixin, Generic[BranchStatementParentType]): 

148 """ 

149 A branching statement - a ``<statement>`` of a branch coverage, or a ``<nestedBranch>`` of a branch -: where it is, 

150 its kind, its expression and its branches. 

151 """ 

152 

153 _parent: Nullable[BranchStatementParentType] #: The branch coverage or branch the statement belongs to. 

154 _id: StatementID #: Where the branching statement is. 

155 _statementType: str #: Kind of the branching statement, e.g. ``if``. 

156 _branchExpression: Nullable[str] #: The expression the statement branches on. 

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

158 

159 def __init__( 

160 self, 

161 statementID: StatementID, 

162 statementType: str, 

163 branchExpression: Nullable[str] = None, 

164 alias: Nullable[str] = None, 

165 excluded: bool = False, 

166 excludedReason: Nullable[str] = None, 

167 weight: int = 1, 

168 userAttributes: Nullable[Iterable[UserAttribute]] = None, 

169 *, 

170 parent: Nullable[BranchStatementParentType] = None 

171 ) -> None: 

172 """ 

173 Initialize the branching statement, and append it to the branching statements of its parent. 

174 

175 Its branches are added by creating them with this statement as their parent. 

176 

177 :param statementID: Where the branching statement is. 

178 :param statementType: Kind of the branching statement, e.g. ``if`` or ``case``. 

179 :param branchExpression: Optional, the expression the statement branches on, e.g. ``(x)``. Default: ``None``. 

180 :param alias: Optional, an alias of the statement's name. Default: ``None``. 

181 :param excluded: Optional, whether the statement is excluded from the coverage. Default: ``False``. 

182 :param excludedReason: Optional, why the statement is excluded. Default: ``None``. 

183 :param weight: Optional, weight of the statement in computing the coverage. Default: ``1``. 

184 :param userAttributes: Optional, the user-defined attributes. Default: ``None``. 

185 :param parent: Optional, the branch coverage or branch the statement belongs to. Default: ``None``. 

186 :raises TypeError: If parameter ``userAttributes`` isn't iterable. 

187 :raises TypeError: If parameter ``userAttributes`` contains an element not of type 

188 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. 

189 :raises TypeError: If parameter ``alias`` isn't of type :class:`str`. 

190 :raises ValueError: If parameter ``excluded`` is ``None``. 

191 :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`. 

192 :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`. 

193 :raises ValueError: If parameter ``weight`` is ``None``. 

194 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`. 

195 :raises ValueError: If parameter ``weight`` is negative. 

196 :raises ValueError: If parameter ``statementID`` is ``None``. 

197 :raises TypeError: If parameter ``statementID`` isn't of type 

198 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`. 

199 :raises ValueError: If parameter ``statementType`` is ``None``. 

200 :raises TypeError: If parameter ``statementType`` isn't of type :class:`str`. 

201 :raises TypeError: If parameter ``branchExpression`` isn't of type :class:`str`. 

202 :raises TypeError: If parameter ``parent`` isn't of type :class:`BranchCoverage` or :class:`Branch`. 

203 """ 

204 super().__init__(userAttributes) 

205 ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight) 

206 

207 if statementID is None: 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true

208 raise ValueError("Parameter 'statementID' is None.") 

209 elif not isinstance(statementID, StatementID): 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true

210 ex = TypeError("Parameter 'statementID' is not of type 'StatementID'.") 

211 ex.add_note(f"Got type '{getFullyQualifiedName(statementID)}'.") 

212 raise ex 

213 

214 if statementType is None: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true

215 raise ValueError("Parameter 'statementType' is None.") 

216 elif not isinstance(statementType, str): 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 ex = TypeError("Parameter 'statementType' is not of type 'str'.") 

218 ex.add_note(f"Got type '{getFullyQualifiedName(statementType)}'.") 

219 raise ex 

220 

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

222 ex = TypeError("Parameter 'branchExpression' is not of type 'str'.") 

223 ex.add_note(f"Got type '{getFullyQualifiedName(branchExpression)}'.") 

224 raise ex 

225 

226 if parent is not None and not isinstance(parent, (BranchCoverage, Branch)): 

227 ex = TypeError("Parameter 'parent' is not of type 'BranchCoverage' or 'Branch'.") 

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

229 raise ex 

230 

231 self._parent = parent 

232 self._id = statementID 

233 self._statementType = statementType 

234 self._branchExpression = branchExpression 

235 self._branches = [] 

236 

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

238 parent._statements.append(self) 

239 

240 @classmethod 

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

242 """ 

243 Parse a branching statement and its branches from its ``<statement>`` or ``<nestedBranch>`` element. 

244 

245 :param element: The ``<statement>`` or ``<nestedBranch>`` element. 

246 :param parent: Optional, the branch coverage or branch the statement belongs to. Default: ``None``. 

247 :returns: The branching statement. 

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

249 """ 

250 statement = cls( 

251 StatementID.Parse(element.find("{*}id")), 

252 element.attrib["statementType"], 

253 element.attrib.get("branchExpr"), 

254 element.attrib.get("alias"), 

255 cls._ParseBoolean(element.attrib.get("excluded", "false")), 

256 element.attrib.get("excludedReason"), 

257 int(element.attrib.get("weight", "1")), 

258 cls._ParseUserAttributes(element), 

259 parent=parent 

260 ) 

261 

262 for branchElement in element.iterfind("{*}branch"): 

263 Branch.Parse(branchElement, parent=statement) 

264 

265 return statement 

266 

267 def IterateStatements(self) -> Generator[BranchStatement[Branch], None, None]: 

268 """ 

269 Iterate the branching statements nested in this statement's branches, each followed by those nested in it. 

270 

271 :returns: A generator of the branching statements. 

272 """ 

273 for branch in self._branches: 

274 for statement in branch._statements: 

275 yield statement 

276 yield from statement.IterateStatements() 

277 

278 @readonly 

279 def Parent(self) -> Nullable[BranchStatementParentType]: 

280 """ 

281 Read-only property to access the branch coverage or branch the statement belongs to (:attr:`_parent`). 

282 

283 :returns: The :class:`BranchCoverage` or :class:`Branch`; ``None``, if the statement belongs to none. 

284 """ 

285 return self._parent 

286 

287 @readonly 

288 def ID(self) -> StatementID: 

289 """ 

290 Read-only property to access where the branching statement is (:attr:`_id`). 

291 

292 :returns: The source statement identifier. 

293 """ 

294 return self._id 

295 

296 @readonly 

297 def StatementType(self) -> str: 

298 """ 

299 Read-only property to access the kind of the branching statement (:attr:`_statementType`). 

300 

301 :returns: The kind, e.g. ``if`` or ``case``. 

302 """ 

303 return self._statementType 

304 

305 @readonly 

306 def BranchExpression(self) -> Nullable[str]: 

307 """ 

308 Read-only property to access the expression the statement branches on (:attr:`_branchExpression`). 

309 

310 :returns: The expression; ``None``, if the report states none. 

311 """ 

312 return self._branchExpression 

313 

314 @readonly 

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

316 """ 

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

318 

319 :returns: The branches, in the order the report lists them. 

320 """ 

321 return self._branches 

322 

323 

324@export 

325class Branch(Base): 

326 """ 

327 A ``<branch>`` of a branching statement: where it is, its bin - how often it was taken - and the branching 

328 statements nested in it. 

329 """ 

330 

331 _parent: Nullable[BranchStatement] #: The branching statement the branch belongs to. 

332 _id: StatementID #: Where the branch is. 

333 _bin: _Bin #: How often the branch was taken. 

334 _statements: list[BranchStatement[Branch]] #: The branching statements nested in the branch. 

335 

336 def __init__( 

337 self, 

338 branchID: StatementID, 

339 coverBin: Bin, 

340 userAttributes: Nullable[Iterable[UserAttribute]] = None, 

341 *, 

342 parent: Nullable[BranchStatement] = None 

343 ) -> None: 

344 """ 

345 Initialize the branch, become its bin's parent, and append it to the branches of its branching statement. 

346 

347 The branching statements nested in it are added by creating them with this branch as their parent. 

348 

349 :param branchID: Where the branch is. 

350 :param coverBin: How often the branch was taken. 

351 :param userAttributes: Optional, the user-defined attributes. Default: ``None``. 

352 :param parent: Optional, the branching statement the branch belongs to. Default: ``None``. 

353 :raises TypeError: If parameter ``userAttributes`` isn't iterable. 

354 :raises TypeError: If parameter ``userAttributes`` contains an element not of type 

355 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`. 

356 :raises ValueError: If parameter ``branchID`` is ``None``. 

357 :raises TypeError: If parameter ``branchID`` isn't of type 

358 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.StatementID`. 

359 :raises ValueError: If parameter ``coverBin`` is ``None``. 

360 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`. 

361 :raises TypeError: If parameter ``parent`` isn't of type :class:`BranchStatement`. 

362 """ 

363 super().__init__(userAttributes) 

364 

365 if branchID is None: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true

366 raise ValueError("Parameter 'branchID' is None.") 

367 elif not isinstance(branchID, StatementID): 367 ↛ 368line 367 didn't jump to line 368 because the condition on line 367 was never true

368 ex = TypeError("Parameter 'branchID' is not of type 'StatementID'.") 

369 ex.add_note(f"Got type '{getFullyQualifiedName(branchID)}'.") 

370 raise ex 

371 

372 if coverBin is None: 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true

373 raise ValueError("Parameter 'coverBin' is None.") 

374 elif not isinstance(coverBin, Bin): 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true

375 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.") 

376 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.") 

377 raise ex 

378 

379 if parent is not None and not isinstance(parent, BranchStatement): 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true

380 ex = TypeError("Parameter 'parent' is not of type 'BranchStatement'.") 

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

382 raise ex 

383 

384 self._parent = parent 

385 self._id = branchID 

386 self._bin = coverBin 

387 self._statements = [] 

388 coverBin._parent = self 

389 

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

391 parent._branches.append(self) 

392 

393 @classmethod 

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

395 """ 

396 Parse a branch, its bin and the branching statements nested in it from its ``<branch>`` element. 

397 

398 :param element: The ``<branch>`` element. 

399 :param parent: Optional, the branching statement the branch belongs to. Default: ``None``. 

400 :returns: The branch. 

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

402 """ 

403 branch = cls( 

404 StatementID.Parse(element.find("{*}id")), 

405 Bin.Parse(element.find("{*}branchBin")), 

406 cls._ParseUserAttributes(element), 

407 parent=parent 

408 ) 

409 

410 for statementElement in element.iterfind("{*}nestedBranch"): 410 ↛ 411line 410 didn't jump to line 411 because the loop on line 410 never started

411 BranchStatement.Parse(statementElement, parent=branch) 

412 

413 return branch 

414 

415 @readonly 

416 def Parent(self) -> Nullable[BranchStatement]: 

417 """ 

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

419 

420 :returns: The branching statement; ``None``, if the branch belongs to none. 

421 """ 

422 return self._parent 

423 

424 @readonly 

425 def ID(self) -> StatementID: 

426 """ 

427 Read-only property to access where the branch is (:attr:`_id`). 

428 

429 :returns: The source statement identifier. 

430 """ 

431 return self._id 

432 

433 @readonly 

434 def Bin(self) -> Bin: 

435 """ 

436 Read-only property to access the branch's bin (:attr:`_bin`). 

437 

438 :returns: The bin, stating how often the branch was taken. 

439 """ 

440 return self._bin 

441 

442 @readonly 

443 def Statements(self) -> list[BranchStatement[Branch]]: 

444 """ 

445 Read-only property to access the branching statements nested in the branch (:attr:`_statements`). 

446 

447 :returns: The branching statements, in the order the report lists them. 

448 """ 

449 return self._statements