Coverage for pyEDAA/Reports/CodeCoverage/UCIS/Elements.py: 84%

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 elements of the UCIS XML interchange format, which the coverage kinds share: the user-defined attributes, the 

33attributes of an object, the source statement identifiers, and the base-class of an instance's coverage of a kind. 

34""" 

35from __future__ import annotations 

36 

37from typing import TYPE_CHECKING, Iterable, Optional as Nullable, Self 

38 

39from lxml.etree import _Element 

40from pyTooling.Common import getFullyQualifiedName, StringEnum 

41from pyTooling.Decorators import export, readonly 

42from pyTooling.MetaClasses import ExtendedType, abstractclass 

43 

44from pyEDAA.Reports.CodeCoverage import CodeCoverageError 

45 

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

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

48 

49 

50@export 

51class UserAttributeType(StringEnum): 

52 """ 

53 Type of a user-defined attribute's value, as a ``<userAttr>``'s ``type`` states. 

54 """ 

55 

56 Integer = "int" #: A 32-bit integer. 

57 Integer64 = "int64" #: A 64-bit integer. 

58 Float = "float" #: A single precision floating point number. 

59 Double = "double" #: A double precision floating point number. 

60 String = "str" #: A string. 

61 Bits = "bits" #: A bit string, e.g. ``01101100``, its length stated by ``len``. 

62 

63 

64@export 

65class UserAttribute(metaclass=ExtendedType, slots=True): 

66 """ 

67 A ``<userAttr>`` of an element: a key, the type of its value, the value and - for a bit string - its length. 

68 

69 It is a value, as a path is: it names no parent. 

70 """ 

71 

72 _key: str #: Key of the attribute. 

73 _type: UserAttributeType #: Type of the attribute's value. 

74 _value: Nullable[int | float | str] #: The value, of the type :attr:`_type` names. 

75 _length: Nullable[int] #: Length of a bit string. 

76 

77 def __init__( 

78 self, 

79 key: str, 

80 attributeType: UserAttributeType, 

81 value: Nullable[int | float | str] = None, 

82 length: Nullable[int] = None 

83 ) -> None: 

84 """ 

85 Initialize the user-defined attribute. 

86 

87 :param key: Key of the attribute, e.g. ``optimization_level``. 

88 :param attributeType: Type of the attribute's value. 

89 :param value: Optional, the value: :class:`int` for ``int`` and ``int64``, :class:`float` for ``float`` and 

90 ``double``, :class:`str` for ``str`` and ``bits``; ``None``, if the report states none. 

91 Default: ``None``. 

92 :param length: Optional, length of a bit string. Default: ``None``. 

93 :raises ValueError: If parameter ``key`` is ``None``. 

94 :raises TypeError: If parameter ``key`` isn't of type :class:`str`. 

95 :raises ValueError: If parameter ``attributeType`` is ``None``. 

96 :raises TypeError: If parameter ``attributeType`` isn't of type :class:`UserAttributeType`. 

97 :raises TypeError: If parameter ``value`` isn't of the type parameter ``attributeType`` names. 

98 :raises TypeError: If parameter ``length`` isn't of type :class:`int`. 

99 """ 

100 if key is None: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 raise ValueError("Parameter 'key' is None.") 

102 elif not isinstance(key, str): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 ex = TypeError("Parameter 'key' is not of type 'str'.") 

104 ex.add_note(f"Got type '{getFullyQualifiedName(key)}'.") 

105 raise ex 

106 

107 if attributeType is None: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true

108 raise ValueError("Parameter 'attributeType' is None.") 

109 elif not isinstance(attributeType, UserAttributeType): 

110 ex = TypeError("Parameter 'attributeType' is not of type 'UserAttributeType'.") 

111 ex.add_note(f"Got type '{getFullyQualifiedName(attributeType)}'.") 

112 raise ex 

113 

114 if attributeType in (UserAttributeType.Integer, UserAttributeType.Integer64): 

115 valueType = int 

116 elif attributeType in (UserAttributeType.Float, UserAttributeType.Double): 

117 valueType = float 

118 else: 

119 valueType = str 

120 

121 if value is not None and (not isinstance(value, valueType) or isinstance(value, bool)): 

122 ex = TypeError(f"Parameter 'value' is not of type '{valueType.__name__}'.") 

123 ex.add_note(f"Got type '{getFullyQualifiedName(value)}' for attribute type '{attributeType}'.") 

124 raise ex 

125 

126 if length is not None and not isinstance(length, int): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 ex = TypeError("Parameter 'length' is not of type 'int'.") 

128 ex.add_note(f"Got type '{getFullyQualifiedName(length)}'.") 

129 raise ex 

130 

131 self._key = key 

132 self._type = attributeType 

133 self._value = value 

134 self._length = length 

135 

136 @classmethod 

137 def Parse(cls, element: _Element) -> Self: 

138 """ 

139 Parse a user-defined attribute from its ``<userAttr>`` element. 

140 

141 The value is the element's text, surrounding whitespace removed; an empty text is no value. 

142 

143 :param element: The ``<userAttr>`` element. 

144 :returns: The user-defined attribute. 

145 :raises CodeCoverageError: If the value isn't of the type the attribute states. 

146 """ 

147 key = element.attrib["key"] 

148 attributeType = UserAttributeType(element.attrib["type"]) 

149 text = "" if element.text is None else element.text.strip() 

150 length = element.attrib.get("len") 

151 

152 value: Nullable[int | float | str] = None 

153 if text != "": 

154 try: 

155 if attributeType in (UserAttributeType.Integer, UserAttributeType.Integer64): 

156 value = int(text) 

157 elif attributeType in (UserAttributeType.Float, UserAttributeType.Double): 

158 value = float(text) 

159 else: 

160 value = text 

161 except ValueError as cause: 

162 ex = CodeCoverageError(f"UCIS user attribute '{key}' states a value not of type '{attributeType}'.") 

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

164 raise ex from cause 

165 

166 return cls(key, attributeType, value, None if length is None else int(length)) 

167 

168 @readonly 

169 def Key(self) -> str: 

170 """ 

171 Read-only property to access the key of the attribute (:attr:`_key`). 

172 

173 :returns: The key. 

174 """ 

175 return self._key 

176 

177 @readonly 

178 def Type(self) -> UserAttributeType: 

179 """ 

180 Read-only property to access the type of the attribute's value (:attr:`_type`). 

181 

182 :returns: The type. 

183 """ 

184 return self._type 

185 

186 @readonly 

187 def Value(self) -> Nullable[int | float | str]: 

188 """ 

189 Read-only property to access the value (:attr:`_value`). 

190 

191 :returns: The value, of the type :attr:`Type` names; ``None``, if the report states none. 

192 """ 

193 return self._value 

194 

195 @readonly 

196 def Length(self) -> Nullable[int]: 

197 """ 

198 Read-only property to access the length of a bit string (:attr:`_length`). 

199 

200 :returns: The length; ``None``, if the report states none. 

201 """ 

202 return self._length 

203 

204 

205@export 

206@abstractclass 

207class Base(metaclass=ExtendedType, slots=True): 

208 """ 

209 Base-class of the elements stating user-defined attributes. 

210 """ 

211 

212 _userAttributes: list[UserAttribute] #: The user-defined attributes. 

213 

214 def __init__(self, userAttributes: Nullable[Iterable[UserAttribute]] = None) -> None: 

215 """ 

216 Initialize the user-defined attributes. 

217 

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

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

220 :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`UserAttribute`. 

221 """ 

222 self._userAttributes = [] 

223 

224 if userAttributes is not None: 

225 if not isinstance(userAttributes, Iterable): 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true

226 ex = TypeError("Parameter 'userAttributes' is not iterable.") 

227 ex.add_note(f"Got type '{getFullyQualifiedName(userAttributes)}'.") 

228 raise ex 

229 

230 for userAttribute in userAttributes: 

231 if not isinstance(userAttribute, UserAttribute): 

232 ex = TypeError("Parameter 'userAttributes' contains an element not of type 'UserAttribute'.") 

233 ex.add_note(f"Got type '{getFullyQualifiedName(userAttribute)}'.") 

234 raise ex 

235 

236 self._userAttributes.append(userAttribute) 

237 

238 @staticmethod 

239 def _ParseUserAttributes(element: _Element) -> list[UserAttribute]: 

240 """ 

241 Parse the user-defined attributes of an element, its ``<userAttr>`` elements. 

242 

243 :param element: The element. 

244 :returns: The user-defined attributes, in the order the report lists them. 

245 :raises CodeCoverageError: If a value isn't of the type its attribute states. 

246 """ 

247 return [UserAttribute.Parse(attributeElement) for attributeElement in element.iterfind("{*}userAttr")] 

248 

249 @staticmethod 

250 def _ParseBoolean(value: str) -> bool: 

251 """ 

252 Parse an attribute's value of XML Schema type ``boolean``. 

253 

254 :param value: The value: ``true``, ``1``, ``false`` or ``0``. 

255 :returns: The boolean. 

256 """ 

257 return value in ("true", "1") 

258 

259 @readonly 

260 def UserAttributes(self) -> list[UserAttribute]: 

261 """ 

262 Read-only property to access the user-defined attributes (:attr:`_userAttributes`). 

263 

264 :returns: The user-defined attributes, in the order the report lists them. 

265 """ 

266 return self._userAttributes 

267 

268 

269@export 

270class ObjectAttributesMixin(metaclass=ExtendedType, mixin=True): 

271 """ 

272 A mixin-class adding the attributes of an object: its alias, whether it is excluded and why, and its weight. 

273 """ 

274 

275 _alias: Nullable[str] #: An alias of the object's name. 

276 _isExcluded: bool #: Whether the object is excluded from the coverage. 

277 _excludedReason: Nullable[str] #: Why the object is excluded. 

278 _weight: int #: Weight of the object in computing the coverage. 

279 

280 def __init__( 

281 self, 

282 alias: Nullable[str] = None, 

283 excluded: bool = False, 

284 excludedReason: Nullable[str] = None, 

285 weight: int = 1 

286 ) -> None: 

287 """ 

288 Initialize the attributes of the object. 

289 

290 :param alias: Optional, an alias of the object's name. Default: ``None``. 

291 :param excluded: Optional, whether the object is excluded from the coverage. Default: ``False``. 

292 :param excludedReason: Optional, why the object is excluded. Default: ``None``. 

293 :param weight: Optional, weight of the object in computing the coverage. Default: ``1``. 

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

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

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

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

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

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

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

301 """ 

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

303 ex = TypeError("Parameter 'alias' is not of type 'str'.") 

304 ex.add_note(f"Got type '{getFullyQualifiedName(alias)}'.") 

305 raise ex 

306 

307 if excluded is None: 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true

308 raise ValueError("Parameter 'excluded' is None.") 

309 elif not isinstance(excluded, bool): 

310 ex = TypeError("Parameter 'excluded' is not of type 'bool'.") 

311 ex.add_note(f"Got type '{getFullyQualifiedName(excluded)}'.") 

312 raise ex 

313 

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

315 ex = TypeError("Parameter 'excludedReason' is not of type 'str'.") 

316 ex.add_note(f"Got type '{getFullyQualifiedName(excludedReason)}'.") 

317 raise ex 

318 

319 if weight is None: 319 ↛ 320line 319 didn't jump to line 320 because the condition on line 319 was never true

320 raise ValueError("Parameter 'weight' is None.") 

321 elif not isinstance(weight, int) or isinstance(weight, bool): 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true

322 ex = TypeError("Parameter 'weight' is not of type 'int'.") 

323 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") 

324 raise ex 

325 elif weight < 0: 

326 ex = ValueError("Parameter 'weight' is negative.") 

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

328 raise ex 

329 

330 self._alias = alias 

331 self._isExcluded = excluded 

332 self._excludedReason = excludedReason 

333 self._weight = weight 

334 

335 @readonly 

336 def Alias(self) -> Nullable[str]: 

337 """ 

338 Read-only property to access the alias of the object's name (:attr:`_alias`). 

339 

340 :returns: The alias; ``None``, if the report states none. 

341 """ 

342 return self._alias 

343 

344 @readonly 

345 def IsExcluded(self) -> bool: 

346 """ 

347 Read-only property to access whether the object is excluded from the coverage (:attr:`_isExcluded`). 

348 

349 :returns: ``True``, if excluded. 

350 """ 

351 return self._isExcluded 

352 

353 @readonly 

354 def ExcludedReason(self) -> Nullable[str]: 

355 """ 

356 Read-only property to access why the object is excluded (:attr:`_excludedReason`). 

357 

358 :returns: The reason; ``None``, if the report states none. 

359 """ 

360 return self._excludedReason 

361 

362 @readonly 

363 def Weight(self) -> int: 

364 """ 

365 Read-only property to access the weight of the object in computing the coverage (:attr:`_weight`). 

366 

367 :returns: The weight; ``1``, if the report states none. 

368 """ 

369 return self._weight 

370 

371 

372@export 

373class StatementID(metaclass=ExtendedType, slots=True): 

374 """ 

375 A source statement identifier: the source file, the line and the index of the statement in the line. 

376 

377 It is stated e.g. by an ``<id>`` element. It is a value, as a path is: it names no parent. 

378 """ 

379 

380 _fileID: int #: ID of the source file. 

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

382 _inlineCount: int #: Index of the statement in its line, counted from 1. 

383 

384 def __init__(self, fileID: int, lineNumber: int, inlineCount: int) -> None: 

385 """ 

386 Initialize the source statement identifier. 

387 

388 :param fileID: ID of the source file. 

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

390 :param inlineCount: Index of the statement in its line, counted from 1. 

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

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

393 :raises ValueError: If parameter ``fileID`` is less than 1. 

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

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

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

397 :raises ValueError: If parameter ``inlineCount`` is ``None``. 

398 :raises TypeError: If parameter ``inlineCount`` isn't of type :class:`int`. 

399 :raises ValueError: If parameter ``inlineCount`` is less than 1. 

400 """ 

401 for name, value in ( 

402 ("fileID", fileID), 

403 ("lineNumber", lineNumber), 

404 ("inlineCount", inlineCount) 

405 ): 

406 if value is None: 

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

408 elif not isinstance(value, int) or isinstance(value, bool): 

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

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

411 raise ex 

412 elif value < 1: 

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

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

415 raise ex 

416 

417 self._fileID = fileID 

418 self._lineNumber = lineNumber 

419 self._inlineCount = inlineCount 

420 

421 @classmethod 

422 def Parse(cls, element: _Element) -> Self: 

423 """ 

424 Parse a source statement identifier from its element. 

425 

426 The element is e.g. an ``<id>``. 

427 

428 :param element: The identifier's element. 

429 :returns: The source statement identifier. 

430 """ 

431 return cls(int(element.attrib["file"]), int(element.attrib["line"]), int(element.attrib["inlineCount"])) 

432 

433 @readonly 

434 def FileID(self) -> int: 

435 """ 

436 Read-only property to access the ID of the source file (:attr:`_fileID`). 

437 

438 :returns: The ID, as the report's source file states it. 

439 """ 

440 return self._fileID 

441 

442 @readonly 

443 def LineNumber(self) -> int: 

444 """ 

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

446 

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

448 """ 

449 return self._lineNumber 

450 

451 @readonly 

452 def InlineCount(self) -> int: 

453 """ 

454 Read-only property to access the index of the statement in its line (:attr:`_inlineCount`). 

455 

456 :returns: The index, counted from 1. 

457 """ 

458 return self._inlineCount 

459 

460 def __repr__(self) -> str: 

461 """ 

462 Return a representation of the identifier for debugging. 

463 

464 The first statement in line 12 of file 1 reads ``<StatementID 1:12:1>``. 

465 

466 :returns: The file ID, the line number and the index in the line. 

467 """ 

468 return f"<StatementID {self._fileID}:{self._lineNumber}:{self._inlineCount}>" 

469 

470 

471@export 

472@abstractclass 

473class MetricCoverage(Base): 

474 """ 

475 Base-class of the coverage of one kind of an instance: its metric mode and weight. 

476 

477 A ``<blockCoverage>`` is such a coverage. An instance states a kind's coverage once per metric mode. 

478 """ 

479 

480 _parent: Nullable[InstanceCoverage] #: The instance the coverage belongs to. 

481 _metricMode: Nullable[str] #: The mode in which the coverage was measured. 

482 _weight: int #: Weight of the mode in computing the coverage. 

483 

484 def __init__( 

485 self, 

486 metricMode: Nullable[str] = None, 

487 weight: int = 1, 

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

489 *, 

490 parent: Nullable[InstanceCoverage] = None 

491 ) -> None: 

492 """ 

493 Initialize the metric mode, the weight and the parent of the coverage. 

494 

495 A derived class adds the coverage to its instance. 

496 

497 :param metricMode: Optional, the mode in which the coverage was measured, e.g. ``UCIS:BITWISE_FLAT``. 

498 Default: ``None``. 

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

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

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

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

503 :raises TypeError: If parameter ``userAttributes`` contains an element not of type :class:`UserAttribute`. 

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

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

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

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

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

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

510 """ 

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

512 

513 super().__init__(userAttributes) 

514 

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

516 ex = TypeError("Parameter 'metricMode' is not of type 'str'.") 

517 ex.add_note(f"Got type '{getFullyQualifiedName(metricMode)}'.") 

518 raise ex 

519 

520 if weight is None: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 raise ValueError("Parameter 'weight' is None.") 

522 elif not isinstance(weight, int) or isinstance(weight, bool): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true

523 ex = TypeError("Parameter 'weight' is not of type 'int'.") 

524 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") 

525 raise ex 

526 elif weight < 0: 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true

527 ex = ValueError("Parameter 'weight' is negative.") 

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

529 raise ex 

530 

531 if parent is not None and not isinstance(parent, InstanceCoverage): 

532 ex = TypeError("Parameter 'parent' is not of type 'InstanceCoverage'.") 

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

534 raise ex 

535 

536 self._parent = parent 

537 self._metricMode = metricMode 

538 self._weight = weight 

539 

540 @readonly 

541 def Parent(self) -> Nullable[InstanceCoverage]: 

542 """ 

543 Read-only property to access the instance the coverage belongs to (:attr:`_parent`). 

544 

545 :returns: The instance; ``None``, if the coverage belongs to none. 

546 """ 

547 return self._parent 

548 

549 @readonly 

550 def MetricMode(self) -> Nullable[str]: 

551 """ 

552 Read-only property to access the mode in which the coverage was measured (:attr:`_metricMode`). 

553 

554 :returns: The mode; ``None``, if the report states none. 

555 """ 

556 return self._metricMode 

557 

558 @readonly 

559 def Weight(self) -> int: 

560 """ 

561 Read-only property to access the weight of the mode in computing the coverage (:attr:`_weight`). 

562 

563 :returns: The weight; ``1``, if the report states none. 

564 """ 

565 return self._weight