Coverage for pyEDAA / Reports / Unittesting / JUnit / __init__.py: 73%

724 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 22:27 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

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

15# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany # 

16# # 

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

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

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

20# # 

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

22# # 

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

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

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

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

27# limitations under the License. # 

28# # 

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

30# ==================================================================================================================== # 

31# 

32""" 

33The pyEDAA.Reports.Unittesting.JUnit package implements a hierarchy of test entities for the JUnit unit testing summary 

34file format (XML format). This test entity hierarchy is not derived from :class:`pyEDAA.Reports.Unittesting`, because it 

35doesn't match the unified data model. Nonetheless, both data models can be converted to each other. In addition, derived 

36data models are provided for the many dialects of that XML file format. See the list modules in this package for the 

37implemented dialects. 

38 

39The test entity hierarchy consists of test cases, test classes, test suites and a test summary. Test cases are the leaf 

40elements in the hierarchy and represent an individual test run. Next, test classes group test cases, because the 

41original Ant + JUnit format groups test cases (Java methods) in a Java class. Next, test suites are used to group 

42multiple test classes. Finally, the root element is a test summary. When such a summary is stored in a file format like 

43Ant + JUnit4 XML, a file format specific document is derived from a summary class. 

44 

45**Data Model** 

46 

47.. mermaid:: 

48 

49 graph TD; 

50 doc[Document] 

51 sum[Summary] 

52 ts1[Testsuite] 

53 ts11[Testsuite] 

54 ts2[Testsuite] 

55 

56 tc111[Testclass] 

57 tc112[Testclass] 

58 tc23[Testclass] 

59 

60 tc1111[Testcase] 

61 tc1112[Testcase] 

62 tc1113[Testcase] 

63 tc1121[Testcase] 

64 tc1122[Testcase] 

65 tc231[Testcase] 

66 tc232[Testcase] 

67 tc233[Testcase] 

68 

69 doc:::root -.-> sum:::summary 

70 sum --> ts1:::suite 

71 sum ---> ts2:::suite 

72 ts1 --> ts11:::suite 

73 

74 ts11 --> tc111:::cls 

75 ts11 --> tc112:::cls 

76 ts2 --> tc23:::cls 

77 

78 tc111 --> tc1111:::case 

79 tc111 --> tc1112:::case 

80 tc111 --> tc1113:::case 

81 tc112 --> tc1121:::case 

82 tc112 --> tc1122:::case 

83 tc23 --> tc231:::case 

84 tc23 --> tc232:::case 

85 tc23 --> tc233:::case 

86 

87 classDef root fill:#4dc3ff 

88 classDef summary fill:#80d4ff 

89 classDef suite fill:#b3e6ff 

90 classDef cls fill:#ff9966 

91 classDef case fill:#eeccff 

92""" 

93from datetime import datetime, timedelta 

94from enum import Flag 

95from pathlib import Path 

96from sys import version_info 

97from time import perf_counter_ns 

98from typing import Optional as Nullable, Iterable, Dict, Any, Generator, Tuple, Union, TypeVar, Type, ClassVar 

99 

100from lxml.etree import XMLParser, parse, XMLSchema, ElementTree, Element, SubElement, tostring 

101from lxml.etree import XMLSyntaxError, _ElementTree, _Element, _Comment, XMLSchemaParseError 

102from pyTooling.Common import getFullyQualifiedName, getResourceFile 

103from pyTooling.Decorators import export, readonly 

104from pyTooling.Exceptions import ToolingException 

105from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod 

106from pyTooling.Tree import Node 

107 

108from pyEDAA.Reports import Resources 

109from pyEDAA.Reports.Unittesting import UnittestException, AlreadyInHierarchyException, DuplicateTestsuiteException, DuplicateTestcaseException 

110from pyEDAA.Reports.Unittesting import TestcaseStatus, TestsuiteStatus, TestsuiteKind, IterationScheme 

111from pyEDAA.Reports.Unittesting import Document as ut_Document, TestsuiteSummary as ut_TestsuiteSummary 

112from pyEDAA.Reports.Unittesting import Testsuite as ut_Testsuite, Testcase as ut_Testcase 

113 

114 

115@export 

116class JUnitException: 

117 """An exception-mixin for JUnit format specific exceptions.""" 

118 

119 

120@export 

121class UnittestException(UnittestException, JUnitException): 

122 pass 

123 

124 

125@export 

126class AlreadyInHierarchyException(AlreadyInHierarchyException, JUnitException): 

127 """ 

128 A unit test exception raised if the element is already part of a hierarchy. 

129 

130 This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same 

131 hierarchy should occur only once in the hierarchy. 

132 

133 .. hint:: 

134 

135 This is usually caused by a non-None parent reference. 

136 """ 

137 

138 

139@export 

140class DuplicateTestsuiteException(DuplicateTestsuiteException, JUnitException): 

141 """ 

142 A unit test exception raised on duplicate test suites (by name). 

143 

144 This exception is raised, if a child test suite with same name already exist in the test suite. 

145 

146 .. hint:: 

147 

148 Test suite names need to be unique per parent element (test suite or test summary). 

149 """ 

150 

151 

152@export 

153class DuplicateTestcaseException(DuplicateTestcaseException, JUnitException): 

154 """ 

155 A unit test exception raised on duplicate test cases (by name). 

156 

157 This exception is raised, if a child test case with same name already exist in the test suite. 

158 

159 .. hint:: 

160 

161 Test case names need to be unique per parent element (test suite). 

162 """ 

163 

164 

165@export 

166class JUnitReaderMode(Flag): 

167 Default = 0 #: Default behavior 

168 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented 

169 

170 

171TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite") 

172TestcaseAggregateReturnType = Tuple[int, int, int] 

173TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int] 

174 

175 

176@export 

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

178 """ 

179 Base-class for all test entities (test cases, test classes, test suites, ...). 

180 

181 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity 

182 hierarchy. 

183 

184 Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the 

185 child. |br| 

186 E.g. it's used as a test case name in the dictionary of test cases in a test class. 

187 """ 

188 

189 _parent: Nullable["Testsuite"] 

190 _name: str 

191 

192 def __init__(self, name: str, parent: Nullable["Testsuite"] = None): 

193 """ 

194 Initializes the fields of the base-class. 

195 

196 :param name: Name of the test entity. 

197 :param parent: Reference to the parent test entity. 

198 :raises ValueError: If parameter 'name' is None. 

199 :raises TypeError: If parameter 'name' is not a string. 

200 :raises ValueError: If parameter 'name' is empty. 

201 """ 

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

203 raise ValueError(f"Parameter 'name' is None.") 

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

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

206 if version_info >= (3, 11): # pragma: no cover 

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

208 raise ex 

209 elif name.strip() == "": 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true

210 raise ValueError(f"Parameter 'name' is empty.") 

211 

212 self._parent = parent 

213 self._name = name 

214 

215 @readonly 

216 def Parent(self) -> Nullable["Testsuite"]: 

217 """ 

218 Read-only property returning the reference to the parent test entity. 

219 

220 :return: Reference to the parent entity. 

221 """ 

222 return self._parent 

223 

224 # QUESTION: allow Parent as setter? 

225 

226 @readonly 

227 def Name(self) -> str: 

228 """ 

229 Read-only property returning the test entity's name. 

230 

231 :return: 

232 """ 

233 return self._name 

234 

235 

236@export 

237class BaseWithProperties(Base): 

238 """ 

239 Base-class for all test entities supporting properties (test cases, test suites, ...). 

240 

241 Every test entity has fields for the test duration and number of executed assertions. 

242 

243 Every test entity offers an internal dictionary for properties. 

244 """ 

245 

246 _duration: Nullable[timedelta] 

247 _assertionCount: Nullable[int] 

248 _properties: Dict[str, Any] 

249 

250 def __init__( 

251 self, 

252 name: str, 

253 duration: Nullable[timedelta] = None, 

254 assertionCount: Nullable[int] = None, 

255 parent: Nullable["Testsuite"] = None 

256 ): 

257 """ 

258 Initializes the fields of the base-class. 

259 

260 :param name: Name of the test entity. 

261 :param duration: Duration of the entity's execution. 

262 :param assertionCount: Number of assertions within the test. 

263 :param parent: Reference to the parent test entity. 

264 :raises TypeError: If parameter 'duration' is not a timedelta. 

265 :raises TypeError: If parameter 'assertionCount' is not an integer. 

266 """ 

267 super().__init__(name, parent) 

268 

269 if duration is not None and not isinstance(duration, timedelta): 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 ex = TypeError(f"Parameter 'duration' is not of type 'timedelta'.") 

271 if version_info >= (3, 11): # pragma: no cover 

272 ex.add_note(f"Got type '{getFullyQualifiedName(duration)}'.") 

273 raise ex 

274 

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

276 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.") 

277 if version_info >= (3, 11): # pragma: no cover 

278 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.") 

279 raise ex 

280 

281 self._duration = duration 

282 self._assertionCount = assertionCount 

283 

284 self._properties = {} 

285 

286 @readonly 

287 def Duration(self) -> timedelta: 

288 """ 

289 Read-only property returning the duration of a test entity run. 

290 

291 .. note:: 

292 

293 The JUnit format doesn't distinguish setup, run and teardown durations. 

294 

295 :return: Duration of the entity's execution. 

296 """ 

297 return self._duration 

298 

299 @readonly 

300 @abstractmethod 

301 def AssertionCount(self) -> int: 

302 """ 

303 Read-only property returning the number of assertions (checks) in a test case. 

304 

305 .. note:: 

306 

307 The JUnit format doesn't distinguish passed and failed assertions. 

308 

309 :return: Number of assertions. 

310 """ 

311 

312 def __len__(self) -> int: 

313 """ 

314 Returns the number of annotated properties. 

315 

316 Syntax: :pycode:`length = len(obj)` 

317 

318 :return: Number of annotated properties. 

319 """ 

320 return len(self._properties) 

321 

322 def __getitem__(self, name: str) -> Any: 

323 """ 

324 Access a property by name. 

325 

326 Syntax: :pycode:`value = obj[name]` 

327 

328 :param name: Name if the property. 

329 :return: Value of the accessed property. 

330 """ 

331 return self._properties[name] 

332 

333 def __setitem__(self, name: str, value: Any) -> None: 

334 """ 

335 Set the value of a property by name. 

336 

337 If the property doesn't exist yet, it's created. 

338 

339 Syntax: :pycode:`obj[name] = value` 

340 

341 :param name: Name of the property. 

342 :param value: Value of the property. 

343 """ 

344 self._properties[name] = value 

345 

346 def __delitem__(self, name: str) -> None: 

347 """ 

348 Delete a property by name. 

349 

350 Syntax: :pycode:`del obj[name]` 

351 

352 :param name: Name if the property. 

353 """ 

354 del self._properties[name] 

355 

356 def __contains__(self, name: str) -> bool: 

357 """ 

358 Returns True, if a property was annotated by this name. 

359 

360 Syntax: :pycode:`name in obj` 

361 

362 :param name: Name of the property. 

363 :return: True, if the property was annotated. 

364 """ 

365 return name in self._properties 

366 

367 def __iter__(self) -> Generator[Tuple[str, Any], None, None]: 

368 """ 

369 Iterate all annotated properties. 

370 

371 Syntax: :pycode:`for name, value in obj:` 

372 

373 :return: A generator of property tuples (name, value). 

374 """ 

375 yield from self._properties.items() 

376 

377 

378@export 

379class Testcase(BaseWithProperties): 

380 """ 

381 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run. 

382 

383 Test cases are grouped by test classes in the test entity hierarchy. These are again grouped by test suites. The root 

384 of the hierarchy is a test summary. 

385 

386 Every test case has an overall status like unknown, skipped, failed or passed. 

387 """ 

388 

389 _status: TestcaseStatus 

390 

391 def __init__( 

392 self, 

393 name: str, 

394 duration: Nullable[timedelta] = None, 

395 status: TestcaseStatus = TestcaseStatus.Unknown, 

396 assertionCount: Nullable[int] = None, 

397 parent: Nullable["Testclass"] = None 

398 ): 

399 """ 

400 Initializes the fields of a test case. 

401 

402 :param name: Name of the test entity. 

403 :param duration: Duration of the entity's execution. 

404 :param status: Status of the test case. 

405 :param assertionCount: Number of assertions within the test. 

406 :param parent: Reference to the parent test class. 

407 :raises TypeError: If parameter 'parent' is not a Testsuite. 

408 :raises ValueError: If parameter 'assertionCount' is not consistent. 

409 """ 

410 if parent is not None: 

411 if not isinstance(parent, Testclass): 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true

412 ex = TypeError(f"Parameter 'parent' is not of type 'Testclass'.") 

413 if version_info >= (3, 11): # pragma: no cover 

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

415 raise ex 

416 

417 parent._testcases[name] = self 

418 

419 super().__init__(name, duration, assertionCount, parent) 

420 

421 if not isinstance(status, TestcaseStatus): 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true

422 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.") 

423 if version_info >= (3, 11): # pragma: no cover 

424 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.") 

425 raise ex 

426 

427 self._status = status 

428 

429 @readonly 

430 def Classname(self) -> str: 

431 """ 

432 Read-only property returning the class name of the test case. 

433 

434 :return: The test case's class name. 

435 

436 .. note:: 

437 

438 In the JUnit format, a test case is uniquely identified by a tuple of class name and test case name. This 

439 structure has been decomposed by this data model into 2 leaf-levels in the test entity hierarchy. Thus, the class 

440 name is represented by its own level and instances of test classes. 

441 """ 

442 if self._parent is None: 

443 raise UnittestException("Standalone Testcase instance is not linked to a Testclass.") 

444 return self._parent._name 

445 

446 @readonly 

447 def Status(self) -> TestcaseStatus: 

448 """ 

449 Read-only property returning the status of the test case. 

450 

451 :return: The test case's status. 

452 """ 

453 return self._status 

454 

455 @readonly 

456 def AssertionCount(self) -> int: 

457 """ 

458 Read-only property returning the number of assertions (checks) in a test case. 

459 

460 .. note:: 

461 

462 The JUnit format doesn't distinguish passed and failed assertions. 

463 

464 :return: Number of assertions. 

465 """ 

466 if self._assertionCount is None: 466 ↛ 468line 466 didn't jump to line 468 because the condition on line 466 was always true

467 return 0 

468 return self._assertionCount 

469 

470 def Copy(self) -> "Testcase": 

471 return self.__class__( 

472 self._name, 

473 self._duration, 

474 self._status, 

475 self._assertionCount 

476 ) 

477 

478 def Aggregate(self) -> None: 

479 if self._status is TestcaseStatus.Unknown: 

480 if self._assertionCount is None: 

481 self._status = TestcaseStatus.Passed 

482 elif self._assertionCount == 0: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 self._status = TestcaseStatus.Weak 

484 else: 

485 self._status = TestcaseStatus.Failed 

486 

487 # TODO: check for setup errors 

488 # TODO: check for teardown errors 

489 

490 @classmethod 

491 def FromTestcase(cls, testcase: ut_Testcase) -> "Testcase": 

492 """ 

493 Convert a test case of the unified test entity data model to the JUnit specific data model's test case object. 

494 

495 :param testcase: Test case from unified data model. 

496 :return: Test case from JUnit specific data model. 

497 """ 

498 return cls( 

499 testcase._name, 

500 duration=testcase._testDuration, 

501 status= testcase._status, 

502 assertionCount=testcase._assertionCount 

503 ) 

504 

505 def ToTestcase(self) -> ut_Testcase: 

506 return ut_Testcase( 

507 self._name, 

508 testDuration=self._duration, 

509 status=self._status, 

510 assertionCount=self._assertionCount, 

511 # TODO: as only assertions are recorded by JUnit files, all are marked as passed 

512 passedAssertionCount=self._assertionCount 

513 ) 

514 

515 def ToTree(self) -> Node: 

516 node = Node(value=self._name) 

517 node["status"] = self._status 

518 node["assertionCount"] = self._assertionCount 

519 node["duration"] = self._duration 

520 

521 return node 

522 

523 def __str__(self) -> str: 

524 moduleName = self.__module__.split(".")[-1] 

525 className = self.__class__.__name__ 

526 return ( 

527 f"<{moduleName}{className} {self._name}: {self._status.name} - asserts:{self._assertionCount}>" 

528 ) 

529 

530 

531@export 

532class TestsuiteBase(BaseWithProperties): 

533 """ 

534 Base-class for all test suites and for test summaries. 

535 

536 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root 

537 element in that hierarchy. While a test suite groups test classes, a test summary can only group test suites. Thus, a 

538 test summary contains no test classes and test cases. 

539 """ 

540 

541 _startTime: Nullable[datetime] 

542 _status: TestsuiteStatus 

543 

544 _tests: int 

545 _skipped: int 

546 _errored: int 

547 _weak: int 

548 _failed: int 

549 _passed: int 

550 

551 def __init__( 

552 self, 

553 name: str, 

554 startTime: Nullable[datetime] = None, 

555 duration: Nullable[timedelta] = None, 

556 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

557 parent: Nullable["Testsuite"] = None 

558 ): 

559 """ 

560 Initializes the based-class fields of a test suite or test summary. 

561 

562 :param name: Name of the test entity. 

563 :param startTime: Time when the test entity was started. 

564 :param duration: Duration of the entity's execution. 

565 :param status: Overall status of the test entity. 

566 :param parent: Reference to the parent test entity. 

567 :raises TypeError: If parameter 'parent' is not a TestsuiteBase. 

568 """ 

569 if parent is not None: 

570 if not isinstance(parent, TestsuiteBase): 570 ↛ 571line 570 didn't jump to line 571 because the condition on line 570 was never true

571 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.") 

572 if version_info >= (3, 11): # pragma: no cover 

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

574 raise ex 

575 

576 parent._testsuites[name] = self 

577 

578 super().__init__(name, duration, None, parent) 

579 

580 self._startTime = startTime 

581 self._status = status 

582 self._tests = 0 

583 self._skipped = 0 

584 self._errored = 0 

585 self._failed = 0 

586 self._passed = 0 

587 

588 @readonly 

589 def StartTime(self) -> Nullable[datetime]: 

590 return self._startTime 

591 

592 @readonly 

593 def Status(self) -> TestsuiteStatus: 

594 return self._status 

595 

596 @readonly 

597 @mustoverride 

598 def TestcaseCount(self) -> int: 

599 pass 

600 

601 @readonly 

602 def Tests(self) -> int: 

603 return self.TestcaseCount 

604 

605 @readonly 

606 def Skipped(self) -> int: 

607 return self._skipped 

608 

609 @readonly 

610 def Errored(self) -> int: 

611 return self._errored 

612 

613 @readonly 

614 def Failed(self) -> int: 

615 return self._failed 

616 

617 @readonly 

618 def Passed(self) -> int: 

619 return self._passed 

620 

621 def Aggregate(self) -> TestsuiteAggregateReturnType: 

622 tests = 0 

623 skipped = 0 

624 errored = 0 

625 weak = 0 

626 failed = 0 

627 passed = 0 

628 

629 # for testsuite in self._testsuites.values(): 

630 # t, s, e, w, f, p = testsuite.Aggregate() 

631 # tests += t 

632 # skipped += s 

633 # errored += e 

634 # weak += w 

635 # failed += f 

636 # passed += p 

637 

638 return tests, skipped, errored, weak, failed, passed 

639 

640 @mustoverride 

641 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: 

642 pass 

643 

644 

645@export 

646class Testclass(Base): 

647 """ 

648 A test class is a low-level element in the test entity hierarchy representing a group of tests. 

649 

650 Test classes contain test cases and are grouped by a test suites. 

651 """ 

652 

653 _testcases: Dict[str, "Testcase"] 

654 

655 def __init__( 

656 self, 

657 classname: str, 

658 testcases: Nullable[Iterable["Testcase"]] = None, 

659 parent: Nullable["Testsuite"] = None 

660 ): 

661 """ 

662 Initializes the fields of the test class. 

663 

664 :param classname: Classname of the test entity. 

665 :param parent: Reference to the parent test suite. 

666 :raises ValueError: If parameter 'classname' is None. 

667 :raises TypeError: If parameter 'classname' is not a string. 

668 :raises ValueError: If parameter 'classname' is empty. 

669 """ 

670 if parent is not None: 

671 if not isinstance(parent, Testsuite): 671 ↛ 672line 671 didn't jump to line 672 because the condition on line 671 was never true

672 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.") 

673 if version_info >= (3, 11): # pragma: no cover 

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

675 raise ex 

676 

677 parent._testclasses[classname] = self 

678 

679 super().__init__(classname, parent) 

680 

681 self._testcases = {} 

682 if testcases is not None: 

683 for testcase in testcases: 

684 if testcase._parent is not None: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true

685 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") 

686 

687 if testcase._name in self._testcases: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true

688 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.") 

689 

690 testcase._parent = self 

691 self._testcases[testcase._name] = testcase 

692 

693 @readonly 

694 def Classname(self) -> str: 

695 """ 

696 Read-only property returning the name of the test class. 

697 

698 :return: The test class' name. 

699 """ 

700 return self._name 

701 

702 @readonly 

703 def Testcases(self) -> Dict[str, "Testcase"]: 

704 """ 

705 Read-only property returning a reference to the internal dictionary of test cases. 

706 

707 :return: Reference to the dictionary of test cases. 

708 """ 

709 return self._testcases 

710 

711 @readonly 

712 def TestcaseCount(self) -> int: 

713 """ 

714 Read-only property returning the number of all test cases in the test entity hierarchy. 

715 

716 :return: Number of test cases. 

717 """ 

718 return len(self._testcases) 

719 

720 @readonly 

721 def AssertionCount(self) -> int: 

722 return sum(tc.AssertionCount for tc in self._testcases.values()) 

723 

724 def AddTestcase(self, testcase: "Testcase") -> None: 

725 if testcase._parent is not None: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true

726 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") 

727 

728 if testcase._name in self._testcases: 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true

729 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.") 

730 

731 testcase._parent = self 

732 self._testcases[testcase._name] = testcase 

733 

734 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None: 

735 for testcase in testcases: 

736 self.AddTestcase(testcase) 

737 

738 def ToTestsuite(self) -> ut_Testsuite: 

739 return ut_Testsuite( 

740 self._name, 

741 TestsuiteKind.Class, 

742 # startTime=self._startTime, 

743 # totalDuration=self._duration, 

744 # status=self._status, 

745 testcases=(tc.ToTestcase() for tc in self._testcases.values()) 

746 ) 

747 

748 def ToTree(self) -> Node: 

749 node = Node( 

750 value=self._name, 

751 children=(tc.ToTree() for tc in self._testcases.values()) 

752 ) 

753 

754 return node 

755 

756 def __str__(self) -> str: 

757 moduleName = self.__module__.split(".")[-1] 

758 className = self.__class__.__name__ 

759 return ( 

760 f"<{moduleName}{className} {self._name}: {len(self._testcases)}>" 

761 ) 

762 

763 

764@export 

765class Testsuite(TestsuiteBase): 

766 """ 

767 A testsuite is a mid-level element in the test entity hierarchy representing a logical group of tests. 

768 

769 Test suites contain test classes and are grouped by a test summary, which is the root of the hierarchy. 

770 """ 

771 

772 _hostname: str 

773 _testclasses: Dict[str, "Testclass"] 

774 

775 def __init__( 

776 self, 

777 name: str, 

778 hostname: Nullable[str] = None, 

779 startTime: Nullable[datetime] = None, 

780 duration: Nullable[timedelta] = None, 

781 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

782 testclasses: Nullable[Iterable["Testclass"]] = None, 

783 parent: Nullable["TestsuiteSummary"] = None 

784 ): 

785 """ 

786 Initializes the fields of a test suite. 

787 

788 :param name: Name of the test suite. 

789 :param startTime: Time when the test suite was started. 

790 :param duration: duration of the entity's execution. 

791 :param status: Overall status of the test suite. 

792 :param parent: Reference to the parent test summary. 

793 :raises TypeError: If parameter 'testcases' is not iterable. 

794 :raises TypeError: If element in parameter 'testcases' is not a Testcase. 

795 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy. 

796 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases. 

797 """ 

798 if parent is not None: 

799 if not isinstance(parent, TestsuiteSummary): 799 ↛ 800line 799 didn't jump to line 800 because the condition on line 799 was never true

800 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteSummary'.") 

801 if version_info >= (3, 11): # pragma: no cover 

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

803 raise ex 

804 

805 parent._testsuites[name] = self 

806 

807 super().__init__(name, startTime, duration, status, parent) 

808 

809 self._hostname = hostname 

810 

811 self._testclasses = {} 

812 if testclasses is not None: 

813 for testclass in testclasses: 

814 if testclass._parent is not None: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true

815 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.") 

816 

817 if testclass._name in self._testclasses: 817 ↛ 818line 817 didn't jump to line 818 because the condition on line 817 was never true

818 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.") 

819 

820 testclass._parent = self 

821 self._testclasses[testclass._name] = testclass 

822 

823 @readonly 

824 def Hostname(self) -> Nullable[str]: 

825 return self._hostname 

826 

827 @readonly 

828 def Testclasses(self) -> Dict[str, "Testclass"]: 

829 return self._testclasses 

830 

831 @readonly 

832 def TestclassCount(self) -> int: 

833 return len(self._testclasses) 

834 

835 # @readonly 

836 # def Testcases(self) -> Dict[str, "Testcase"]: 

837 # return self._classes 

838 

839 @readonly 

840 def TestcaseCount(self) -> int: 

841 return sum(cls.TestcaseCount for cls in self._testclasses.values()) 

842 

843 @readonly 

844 def AssertionCount(self) -> int: 

845 return sum(cls.AssertionCount for cls in self._testclasses.values()) 

846 

847 def AddTestclass(self, testclass: "Testclass") -> None: 

848 if testclass._parent is not None: 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true

849 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.") 

850 

851 if testclass._name in self._testclasses: 851 ↛ 852line 851 didn't jump to line 852 because the condition on line 851 was never true

852 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.") 

853 

854 testclass._parent = self 

855 self._testclasses[testclass._name] = testclass 

856 

857 def AddTestclasses(self, testclasses: Iterable["Testclass"]) -> None: 

858 for testcase in testclasses: 

859 self.AddTestclass(testcase) 

860 

861 # def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]: 

862 # return self.Iterate(scheme) 

863 

864 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]: 

865 return self.Iterate(scheme) 

866 

867 def Copy(self) -> "Testsuite": 

868 return self.__class__( 

869 self._name, 

870 self._hostname, 

871 self._startTime, 

872 self._duration, 

873 self._status 

874 ) 

875 

876 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: 

877 tests, skipped, errored, weak, failed, passed = super().Aggregate() 

878 

879 for testclass in self._testclasses.values(): 

880 for testcase in testclass._testcases.values(): 

881 _ = testcase.Aggregate() 

882 

883 status = testcase._status 

884 if status is TestcaseStatus.Unknown: 884 ↛ 885line 884 didn't jump to line 885 because the condition on line 884 was never true

885 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.") 

886 elif status is TestcaseStatus.Skipped: 

887 skipped += 1 

888 elif status is TestcaseStatus.Errored: 888 ↛ 889line 888 didn't jump to line 889 because the condition on line 888 was never true

889 errored += 1 

890 elif status is TestcaseStatus.Passed: 

891 passed += 1 

892 elif status is TestcaseStatus.Failed: 892 ↛ 894line 892 didn't jump to line 894 because the condition on line 892 was always true

893 failed += 1 

894 elif status is TestcaseStatus.Weak: 

895 weak += 1 

896 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown: 

897 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.") 

898 else: 

899 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.") 

900 

901 self._tests = tests 

902 self._skipped = skipped 

903 self._errored = errored 

904 self._weak = weak 

905 self._failed = failed 

906 self._passed = passed 

907 

908 # FIXME: weak? 

909 if errored > 0: 909 ↛ 910line 909 didn't jump to line 910 because the condition on line 909 was never true

910 self._status = TestsuiteStatus.Errored 

911 elif failed > 0: 

912 self._status = TestsuiteStatus.Failed 

913 elif tests == 0: 913 ↛ 915line 913 didn't jump to line 915 because the condition on line 913 was always true

914 self._status = TestsuiteStatus.Empty 

915 elif tests - skipped == passed: 

916 self._status = TestsuiteStatus.Passed 

917 elif tests == skipped: 

918 self._status = TestsuiteStatus.Skipped 

919 else: 

920 self._status = TestsuiteStatus.Unknown 

921 

922 return tests, skipped, errored, weak, failed, passed 

923 

924 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: 

925 """ 

926 Iterate the test suite and its child elements according to the iteration scheme. 

927 

928 If no scheme is given, use the default scheme. 

929 

930 :param scheme: Scheme how to iterate the test suite and its child elements. 

931 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme. 

932 """ 

933 if IterationScheme.PreOrder in scheme: 

934 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: 

935 yield self 

936 

937 if IterationScheme.IncludeTestcases in scheme: 

938 for testcase in self._testclasses.values(): 

939 yield testcase 

940 

941 for testclass in self._testclasses.values(): 

942 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf) 

943 

944 if IterationScheme.PostOrder in scheme: 

945 if IterationScheme.IncludeTestcases in scheme: 

946 for testcase in self._testclasses.values(): 

947 yield testcase 

948 

949 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: 

950 yield self 

951 

952 @classmethod 

953 def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite": 

954 """ 

955 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object. 

956 

957 :param testsuite: Test suite from unified data model. 

958 :return: Test suite from JUnit specific data model. 

959 """ 

960 juTestsuite = cls( 

961 testsuite._name, 

962 startTime=testsuite._startTime, 

963 duration=testsuite._totalDuration, 

964 status= testsuite._status, 

965 ) 

966 

967 juTestsuite._tests = testsuite._tests 

968 juTestsuite._skipped = testsuite._skipped 

969 juTestsuite._errored = testsuite._errored 

970 juTestsuite._failed = testsuite._failed 

971 juTestsuite._passed = testsuite._passed 

972 

973 for tc in testsuite.IterateTestcases(): 

974 ts = tc._parent 

975 if ts is None: 975 ↛ 976line 975 didn't jump to line 976 because the condition on line 975 was never true

976 raise UnittestException(f"Testcase '{tc._name}' is not part of a hierarchy.") 

977 

978 classname = ts._name 

979 ts = ts._parent 

980 while ts is not None and ts._kind > TestsuiteKind.Logical: 

981 classname = f"{ts._name}.{classname}" 

982 ts = ts._parent 

983 

984 if classname in juTestsuite._testclasses: 

985 juClass = juTestsuite._testclasses[classname] 

986 else: 

987 juClass = Testclass(classname, parent=juTestsuite) 

988 

989 juClass.AddTestcase(Testcase.FromTestcase(tc)) 

990 

991 return juTestsuite 

992 

993 def ToTestsuite(self) -> ut_Testsuite: 

994 testsuite = ut_Testsuite( 

995 self._name, 

996 TestsuiteKind.Logical, 

997 startTime=self._startTime, 

998 totalDuration=self._duration, 

999 status=self._status, 

1000 ) 

1001 

1002 for testclass in self._testclasses.values(): 

1003 suite = testsuite 

1004 classpath = testclass._name.split(".") 

1005 for element in classpath: 

1006 if element in suite._testsuites: 

1007 suite = suite._testsuites[element] 

1008 else: 

1009 suite = ut_Testsuite(element, kind=TestsuiteKind.Package, parent=suite) 

1010 

1011 suite._kind = TestsuiteKind.Class 

1012 if suite._parent is not testsuite: 1012 ↛ 1015line 1012 didn't jump to line 1015 because the condition on line 1012 was always true

1013 suite._parent._kind = TestsuiteKind.Module 

1014 

1015 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values()) 

1016 

1017 return testsuite 

1018 

1019 def ToTree(self) -> Node: 

1020 node = Node( 

1021 value=self._name, 

1022 children=(cls.ToTree() for cls in self._testclasses.values()) 

1023 ) 

1024 node["startTime"] = self._startTime 

1025 node["duration"] = self._duration 

1026 

1027 return node 

1028 

1029 def __str__(self) -> str: 

1030 moduleName = self.__module__.split(".")[-1] 

1031 className = self.__class__.__name__ 

1032 return ( 

1033 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>" 

1034 ) 

1035 

1036 

1037@export 

1038class TestsuiteSummary(TestsuiteBase): 

1039 _testsuites: Dict[str, Testsuite] 

1040 

1041 def __init__( 

1042 self, 

1043 name: str, 

1044 startTime: Nullable[datetime] = None, 

1045 duration: Nullable[timedelta] = None, 

1046 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

1047 testsuites: Nullable[Iterable[Testsuite]] = None 

1048 ): 

1049 super().__init__(name, startTime, duration, status, None) 

1050 

1051 self._testsuites = {} 

1052 if testsuites is not None: 

1053 for testsuite in testsuites: 

1054 if testsuite._parent is not None: 1054 ↛ 1055line 1054 didn't jump to line 1055 because the condition on line 1054 was never true

1055 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") 

1056 

1057 if testsuite._name in self._testsuites: 1057 ↛ 1058line 1057 didn't jump to line 1058 because the condition on line 1057 was never true

1058 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") 

1059 

1060 testsuite._parent = self 

1061 self._testsuites[testsuite._name] = testsuite 

1062 

1063 @readonly 

1064 def Testsuites(self) -> Dict[str, Testsuite]: 

1065 return self._testsuites 

1066 

1067 @readonly 

1068 def TestcaseCount(self) -> int: 

1069 return sum(ts.TestcaseCount for ts in self._testsuites.values()) 

1070 

1071 @readonly 

1072 def TestsuiteCount(self) -> int: 

1073 return len(self._testsuites) 

1074 

1075 @readonly 

1076 def AssertionCount(self) -> int: 

1077 return sum(ts.AssertionCount for ts in self._testsuites.values()) 

1078 

1079 def AddTestsuite(self, testsuite: Testsuite) -> None: 

1080 if testsuite._parent is not None: 1080 ↛ 1081line 1080 didn't jump to line 1081 because the condition on line 1080 was never true

1081 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") 

1082 

1083 if testsuite._name in self._testsuites: 1083 ↛ 1084line 1083 didn't jump to line 1084 because the condition on line 1083 was never true

1084 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") 

1085 

1086 testsuite._parent = self 

1087 self._testsuites[testsuite._name] = testsuite 

1088 

1089 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None: 

1090 for testsuite in testsuites: 

1091 self.AddTestsuite(testsuite) 

1092 

1093 def Aggregate(self) -> TestsuiteAggregateReturnType: 

1094 tests, skipped, errored, weak, failed, passed = super().Aggregate() 

1095 

1096 for testsuite in self._testsuites.values(): 

1097 t, s, e, w, f, p = testsuite.Aggregate() 

1098 tests += t 

1099 skipped += s 

1100 errored += e 

1101 weak += w 

1102 failed += f 

1103 passed += p 

1104 

1105 self._tests = tests 

1106 self._skipped = skipped 

1107 self._errored = errored 

1108 self._weak = weak 

1109 self._failed = failed 

1110 self._passed = passed 

1111 

1112 # FIXME: weak 

1113 if errored > 0: 1113 ↛ 1114line 1113 didn't jump to line 1114 because the condition on line 1113 was never true

1114 self._status = TestsuiteStatus.Errored 

1115 elif failed > 0: 

1116 self._status = TestsuiteStatus.Failed 

1117 elif tests == 0: 1117 ↛ 1119line 1117 didn't jump to line 1119 because the condition on line 1117 was always true

1118 self._status = TestsuiteStatus.Empty 

1119 elif tests - skipped == passed: 

1120 self._status = TestsuiteStatus.Passed 

1121 elif tests == skipped: 

1122 self._status = TestsuiteStatus.Skipped 

1123 else: 

1124 self._status = TestsuiteStatus.Unknown 

1125 

1126 return tests, skipped, errored, weak, failed, passed 

1127 

1128 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[Testsuite, Testcase], None, None]: 

1129 """ 

1130 Iterate the test suite summary and its child elements according to the iteration scheme. 

1131 

1132 If no scheme is given, use the default scheme. 

1133 

1134 :param scheme: Scheme how to iterate the test suite summary and its child elements. 

1135 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme. 

1136 """ 

1137 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme: 

1138 yield self 

1139 

1140 for testsuite in self._testsuites.values(): 

1141 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf) 

1142 

1143 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme: 

1144 yield self 

1145 

1146 @classmethod 

1147 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> "TestsuiteSummary": 

1148 """ 

1149 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite. 

1150 

1151 :param testsuiteSummary: Test suite summary from unified data model. 

1152 :return: Test suite summary from JUnit specific data model. 

1153 """ 

1154 return cls( 

1155 testsuiteSummary._name, 

1156 startTime=testsuiteSummary._startTime, 

1157 duration=testsuiteSummary._totalDuration, 

1158 status=testsuiteSummary._status, 

1159 testsuites=(ut_Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values()) 

1160 ) 

1161 

1162 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1163 """ 

1164 Convert this test suite summary a new test suite summary of the unified data model. 

1165 

1166 All fields are copied to the new instance. Child elements like test suites are copied recursively. 

1167 

1168 :return: A test suite summary of the unified test entity data model. 

1169 """ 

1170 return ut_TestsuiteSummary( 

1171 self._name, 

1172 startTime=self._startTime, 

1173 totalDuration=self._duration, 

1174 status=self._status, 

1175 testsuites=(testsuite.ToTestsuite() for testsuite in self._testsuites.values()) 

1176 ) 

1177 

1178 def ToTree(self) -> Node: 

1179 node = Node( 

1180 value=self._name, 

1181 children=(ts.ToTree() for ts in self._testsuites.values()) 

1182 ) 

1183 node["startTime"] = self._startTime 

1184 node["duration"] = self._duration 

1185 

1186 return node 

1187 

1188 def __str__(self) -> str: 

1189 moduleName = self.__module__.split(".")[-1] 

1190 className = self.__class__.__name__ 

1191 return ( 

1192 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>" 

1193 ) 

1194 

1195 

1196@export 

1197class Document(TestsuiteSummary, ut_Document): 

1198 _TESTCASE: ClassVar[Type[Testcase]] = Testcase 

1199 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass 

1200 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite 

1201 

1202 _readerMode: JUnitReaderMode 

1203 _xmlDocument: Nullable[_ElementTree] 

1204 

1205 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default): 

1206 super().__init__("Unprocessed JUnit XML file") 

1207 

1208 self._readerMode = readerMode 

1209 self._xmlDocument = None 

1210 

1211 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1212 

1213 @classmethod 

1214 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): 

1215 doc = cls(xmlReportFile) 

1216 doc._name = testsuiteSummary._name 

1217 doc._startTime = testsuiteSummary._startTime 

1218 doc._duration = testsuiteSummary._totalDuration 

1219 doc._status = testsuiteSummary._status 

1220 doc._tests = testsuiteSummary._tests 

1221 doc._skipped = testsuiteSummary._skipped 

1222 doc._errored = testsuiteSummary._errored 

1223 doc._failed = testsuiteSummary._failed 

1224 doc._passed = testsuiteSummary._passed 

1225 

1226 doc.AddTestsuites(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values()) 

1227 

1228 return doc 

1229 

1230 def Analyze(self) -> None: 

1231 """ 

1232 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML 

1233 schema. 

1234 

1235 .. hint:: 

1236 

1237 The time spend for analysis will be made available via property :data:`AnalysisDuration`. 

1238 

1239 The used XML schema definition is generic to support "any" dialect. 

1240 """ 

1241 xmlSchemaFile = "Any-JUnit.xsd" 

1242 self._Analyze(xmlSchemaFile) 

1243 

1244 def _Analyze(self, xmlSchemaFile: str) -> None: 

1245 if not self._path.exists(): 1245 ↛ 1246line 1245 didn't jump to line 1246 because the condition on line 1245 was never true

1246 raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \ 

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

1248 

1249 startAnalysis = perf_counter_ns() 

1250 try: 

1251 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1252 except ToolingException as ex: 

1253 raise UnittestException(f"Couldn't locate XML Schema '{xmlSchemaFile}' in package resources.") from ex 

1254 

1255 try: 

1256 schemaParser = XMLParser(ns_clean=True) 

1257 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1258 except XMLSyntaxError as ex: 

1259 raise UnittestException(f"XML Syntax Error while parsing XML Schema '{xmlSchemaFile}'.") from ex 

1260 

1261 try: 

1262 junitSchema = XMLSchema(schemaRoot) 

1263 except XMLSchemaParseError as ex: 

1264 raise UnittestException(f"Error while parsing XML Schema '{xmlSchemaFile}'.") 

1265 

1266 try: 

1267 junitParser = XMLParser(schema=junitSchema, ns_clean=True) 

1268 junitDocument = parse(self._path, parser=junitParser) 

1269 

1270 self._xmlDocument = junitDocument 

1271 except XMLSyntaxError as ex: 

1272 if version_info >= (3, 11): # pragma: no cover 

1273 for logEntry in junitParser.error_log: 

1274 ex.add_note(str(logEntry)) 

1275 raise UnittestException(f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'.") from ex 

1276 except Exception as ex: 

1277 raise UnittestException(f"Couldn't open '{self._path}'.") from ex 

1278 

1279 endAnalysis = perf_counter_ns() 

1280 self._analysisDuration = (endAnalysis - startAnalysis) / 1e9 

1281 

1282 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: 

1283 """ 

1284 Write the data model as XML into a file adhering to the Any JUnit dialect. 

1285 

1286 :param path: Optional path to the XMl file, if internal path shouldn't be used. 

1287 :param overwrite: If true, overwrite an existing file. 

1288 :param regenerate: If true, regenerate the XML structure from data model. 

1289 :raises UnittestException: If the file cannot be overwritten. 

1290 :raises UnittestException: If the internal XML data structure wasn't generated. 

1291 :raises UnittestException: If the file cannot be opened or written. 

1292 """ 

1293 if path is None: 

1294 path = self._path 

1295 

1296 if not overwrite and path.exists(): 1296 ↛ 1297line 1296 didn't jump to line 1297 because the condition on line 1296 was never true

1297 raise UnittestException(f"JUnit XML file '{path}' can not be overwritten.") \ 

1298 from FileExistsError(f"File '{path}' already exists.") 

1299 

1300 if regenerate: 

1301 self.Generate(overwrite=True) 

1302 

1303 if self._xmlDocument is None: 1303 ↛ 1304line 1303 didn't jump to line 1304 because the condition on line 1303 was never true

1304 ex = UnittestException(f"Internal XML document tree is empty and needs to be generated before write is possible.") 

1305 ex.add_note(f"Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.") 

1306 raise ex 

1307 

1308 try: 

1309 with path.open("wb") as file: 

1310 file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) 

1311 except Exception as ex: 

1312 raise UnittestException(f"JUnit XML file '{path}' can not be written.") from ex 

1313 

1314 def Convert(self) -> None: 

1315 """ 

1316 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. 

1317 

1318 This method converts the root element. 

1319 

1320 .. hint:: 

1321 

1322 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. 

1323 

1324 :raises UnittestException: If XML was not read and parsed before. 

1325 """ 

1326 if self._xmlDocument is None: 1326 ↛ 1327line 1326 didn't jump to line 1327 because the condition on line 1326 was never true

1327 ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") 

1328 ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") 

1329 raise ex 

1330 

1331 startConversion = perf_counter_ns() 

1332 rootElement: _Element = self._xmlDocument.getroot() 

1333 

1334 self._name = self._ConvertName(rootElement, optional=True) 

1335 self._startTime = self._ConvertTimestamp(rootElement, optional=True) 

1336 self._duration = self._ConvertTime(rootElement, optional=True) 

1337 

1338 if False: # self._readerMode is JUnitReaderMode. 

1339 self._tests = self._ConvertTests(testsuitesNode) 

1340 self._skipped = self._ConvertSkipped(testsuitesNode) 

1341 self._errored = self._ConvertErrors(testsuitesNode) 

1342 self._failed = self._ConvertFailures(testsuitesNode) 

1343 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1344 

1345 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element 

1346 self._ConvertTestsuite(self, rootNode) 

1347 

1348 if True: # self._readerMode is JUnitReaderMode. 

1349 self.Aggregate() 

1350 

1351 endConversation = perf_counter_ns() 

1352 self._modelConversion = (endConversation - startConversion) / 1e9 

1353 

1354 def _ConvertName(self, element: _Element, default: str = "root", optional: bool = True) -> str: 

1355 """ 

1356 Convert the ``name`` attribute from an XML element node to a string. 

1357 

1358 :param element: The XML element node with a ``name`` attribute. 

1359 :param default: The default value, if no ``name`` attribute was found. 

1360 :param optional: If false, an exception is raised for the missing attribute. 

1361 :return: The ``name`` attribute's content if found, otherwise the given default value. 

1362 :raises UnittestException: If optional is false and no ``name`` attribute exists on the given element node. 

1363 """ 

1364 if "name" in element.attrib: 

1365 return element.attrib["name"] 

1366 elif not optional: 1366 ↛ 1367line 1366 didn't jump to line 1367 because the condition on line 1366 was never true

1367 raise UnittestException(f"Required parameter 'name' not found in tag '{element.tag}'.") 

1368 else: 

1369 return default 

1370 

1371 def _ConvertTimestamp(self, element: _Element, optional: bool = True) -> Nullable[datetime]: 

1372 """ 

1373 Convert the ``timestamp`` attribute from an XML element node to a datetime. 

1374 

1375 :param element: The XML element node with a ``timestamp`` attribute. 

1376 :param optional: If false, an exception is raised for the missing attribute. 

1377 :return: The ``timestamp`` attribute's content if found, otherwise ``None``. 

1378 :raises UnittestException: If optional is false and no ``timestamp`` attribute exists on the given element node. 

1379 """ 

1380 if "timestamp" in element.attrib: 

1381 timestamp = element.attrib["timestamp"] 

1382 return datetime.fromisoformat(timestamp) 

1383 elif not optional: 1383 ↛ 1384line 1383 didn't jump to line 1384 because the condition on line 1383 was never true

1384 raise UnittestException(f"Required parameter 'timestamp' not found in tag '{element.tag}'.") 

1385 else: 

1386 return None 

1387 

1388 def _ConvertTime(self, element: _Element, optional: bool = True) -> Nullable[timedelta]: 

1389 """ 

1390 Convert the ``time`` attribute from an XML element node to a timedelta. 

1391 

1392 :param element: The XML element node with a ``time`` attribute. 

1393 :param optional: If false, an exception is raised for the missing attribute. 

1394 :return: The ``time`` attribute's content if found, otherwise ``None``. 

1395 :raises UnittestException: If optional is false and no ``time`` attribute exists on the given element node. 

1396 """ 

1397 if "time" in element.attrib: 

1398 time = element.attrib["time"] 

1399 return timedelta(seconds=float(time)) 

1400 elif not optional: 1400 ↛ 1401line 1400 didn't jump to line 1401 because the condition on line 1400 was never true

1401 raise UnittestException(f"Required parameter 'time' not found in tag '{element.tag}'.") 

1402 else: 

1403 return None 

1404 

1405 def _ConvertHostname(self, element: _Element, default: str = "localhost", optional: bool = True) -> str: 

1406 """ 

1407 Convert the ``hostname`` attribute from an XML element node to a string. 

1408 

1409 :param element: The XML element node with a ``hostname`` attribute. 

1410 :param default: The default value, if no ``hostname`` attribute was found. 

1411 :param optional: If false, an exception is raised for the missing attribute. 

1412 :return: The ``hostname`` attribute's content if found, otherwise the given default value. 

1413 :raises UnittestException: If optional is false and no ``hostname`` attribute exists on the given element node. 

1414 """ 

1415 if "hostname" in element.attrib: 

1416 return element.attrib["hostname"] 

1417 elif not optional: 1417 ↛ 1418line 1417 didn't jump to line 1418 because the condition on line 1417 was never true

1418 raise UnittestException(f"Required parameter 'hostname' not found in tag '{element.tag}'.") 

1419 else: 

1420 return default 

1421 

1422 def _ConvertClassname(self, element: _Element) -> str: 

1423 """ 

1424 Convert the ``classname`` attribute from an XML element node to a string. 

1425 

1426 :param element: The XML element node with a ``classname`` attribute. 

1427 :return: The ``classname`` attribute's content. 

1428 :raises UnittestException: If no ``classname`` attribute exists on the given element node. 

1429 """ 

1430 if "classname" in element.attrib: 1430 ↛ 1433line 1430 didn't jump to line 1433 because the condition on line 1430 was always true

1431 return element.attrib["classname"] 

1432 else: 

1433 raise UnittestException(f"Required parameter 'classname' not found in tag '{element.tag}'.") 

1434 

1435 def _ConvertTests(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]: 

1436 """ 

1437 Convert the ``tests`` attribute from an XML element node to an integer. 

1438 

1439 :param element: The XML element node with a ``tests`` attribute. 

1440 :param default: The default value, if no ``tests`` attribute was found. 

1441 :param optional: If false, an exception is raised for the missing attribute. 

1442 :return: The ``tests`` attribute's content if found, otherwise the given default value. 

1443 :raises UnittestException: If optional is false and no ``tests`` attribute exists on the given element node. 

1444 """ 

1445 if "tests" in element.attrib: 

1446 return int(element.attrib["tests"]) 

1447 elif not optional: 

1448 raise UnittestException(f"Required parameter 'tests' not found in tag '{element.tag}'.") 

1449 else: 

1450 return default 

1451 

1452 def _ConvertSkipped(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]: 

1453 """ 

1454 Convert the ``skipped`` attribute from an XML element node to an integer. 

1455 

1456 :param element: The XML element node with a ``skipped`` attribute. 

1457 :param default: The default value, if no ``skipped`` attribute was found. 

1458 :param optional: If false, an exception is raised for the missing attribute. 

1459 :return: The ``skipped`` attribute's content if found, otherwise the given default value. 

1460 :raises UnittestException: If optional is false and no ``skipped`` attribute exists on the given element node. 

1461 """ 

1462 if "skipped" in element.attrib: 

1463 return int(element.attrib["skipped"]) 

1464 elif not optional: 

1465 raise UnittestException(f"Required parameter 'skipped' not found in tag '{element.tag}'.") 

1466 else: 

1467 return default 

1468 

1469 def _ConvertErrors(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]: 

1470 """ 

1471 Convert the ``errors`` attribute from an XML element node to an integer. 

1472 

1473 :param element: The XML element node with a ``errors`` attribute. 

1474 :param default: The default value, if no ``errors`` attribute was found. 

1475 :param optional: If false, an exception is raised for the missing attribute. 

1476 :return: The ``errors`` attribute's content if found, otherwise the given default value. 

1477 :raises UnittestException: If optional is false and no ``errors`` attribute exists on the given element node. 

1478 """ 

1479 if "errors" in element.attrib: 

1480 return int(element.attrib["errors"]) 

1481 elif not optional: 

1482 raise UnittestException(f"Required parameter 'errors' not found in tag '{element.tag}'.") 

1483 else: 

1484 return default 

1485 

1486 def _ConvertFailures(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]: 

1487 """ 

1488 Convert the ``failures`` attribute from an XML element node to an integer. 

1489 

1490 :param element: The XML element node with a ``failures`` attribute. 

1491 :param default: The default value, if no ``failures`` attribute was found. 

1492 :param optional: If false, an exception is raised for the missing attribute. 

1493 :return: The ``failures`` attribute's content if found, otherwise the given default value. 

1494 :raises UnittestException: If optional is false and no ``failures`` attribute exists on the given element node. 

1495 """ 

1496 if "failures" in element.attrib: 

1497 return int(element.attrib["failures"]) 

1498 elif not optional: 

1499 raise UnittestException(f"Required parameter 'failures' not found in tag '{element.tag}'.") 

1500 else: 

1501 return default 

1502 

1503 def _ConvertAssertions(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]: 

1504 """ 

1505 Convert the ``assertions`` attribute from an XML element node to an integer. 

1506 

1507 :param element: The XML element node with a ``assertions`` attribute. 

1508 :param default: The default value, if no ``assertions`` attribute was found. 

1509 :param optional: If false, an exception is raised for the missing attribute. 

1510 :return: The ``assertions`` attribute's content if found, otherwise the given default value. 

1511 :raises UnittestException: If optional is false and no ``assertions`` attribute exists on the given element node. 

1512 """ 

1513 if "assertions" in element.attrib: 

1514 return int(element.attrib["assertions"]) 

1515 elif not optional: 1515 ↛ 1516line 1515 didn't jump to line 1516 because the condition on line 1515 was never true

1516 raise UnittestException(f"Required parameter 'assertions' not found in tag '{element.tag}'.") 

1517 else: 

1518 return default 

1519 

1520 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None: 

1521 """ 

1522 Convert the XML data structure of a ``<testsuite>`` to a test suite. 

1523 

1524 This method uses private helper methods provided by the base-class. 

1525 

1526 :param parent: The test suite summary as a parent element in the test entity hierarchy. 

1527 :param testsuitesNode: The current XML element node representing a test suite. 

1528 """ 

1529 newTestsuite = self._TESTSUITE( 

1530 self._ConvertName(testsuitesNode, optional=False), 

1531 self._ConvertHostname(testsuitesNode, optional=True), 

1532 self._ConvertTimestamp(testsuitesNode, optional=True), 

1533 self._ConvertTime(testsuitesNode, optional=True), 

1534 parent=parent 

1535 ) 

1536 

1537 if False: # self._readerMode is JUnitReaderMode. 

1538 self._tests = self._ConvertTests(testsuitesNode) 

1539 self._skipped = self._ConvertSkipped(testsuitesNode) 

1540 self._errored = self._ConvertErrors(testsuitesNode) 

1541 self._failed = self._ConvertFailures(testsuitesNode) 

1542 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1543 

1544 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1545 

1546 def _ConvertTestsuiteChildren(self, testsuitesNode: _Element, newTestsuite: Testsuite) -> None: 

1547 for node in testsuitesNode.iterchildren(): # type: _Element 

1548 # if node.tag == "testsuite": 

1549 # self._ConvertTestsuite(newTestsuite, node) 

1550 # el 

1551 if node.tag == "testcase": 

1552 self._ConvertTestcase(newTestsuite, node) 

1553 

1554 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None: 

1555 """ 

1556 Convert the XML data structure of a ``<testcase>`` to a test case. 

1557 

1558 This method uses private helper methods provided by the base-class. 

1559 

1560 :param parent: The test suite as a parent element in the test entity hierarchy. 

1561 :param testcaseNode: The current XML element node representing a test case. 

1562 """ 

1563 className = self._ConvertClassname(testcaseNode) 

1564 testclass = self._FindOrCreateTestclass(parent, className) 

1565 

1566 newTestcase = self._TESTCASE( 

1567 self._ConvertName(testcaseNode, optional=False), 

1568 self._ConvertTime(testcaseNode, optional=False), 

1569 assertionCount=self._ConvertAssertions(testcaseNode), 

1570 parent=testclass 

1571 ) 

1572 

1573 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1574 

1575 def _FindOrCreateTestclass(self, parent: Testsuite, className: str) -> Testclass: 

1576 if className in parent._testclasses: 

1577 return parent._testclasses[className] 

1578 else: 

1579 return self._TESTCLASS(className, parent=parent) 

1580 

1581 def _ConvertTestcaseChildren(self, testcaseNode: _Element, newTestcase: Testcase) -> None: 

1582 for node in testcaseNode.iterchildren(): # type: _Element 

1583 if isinstance(node, _Comment): 1583 ↛ 1584line 1583 didn't jump to line 1584 because the condition on line 1583 was never true

1584 pass 

1585 elif isinstance(node, _Element): 1585 ↛ 1601line 1585 didn't jump to line 1601 because the condition on line 1585 was always true

1586 if node.tag == "skipped": 

1587 newTestcase._status = TestcaseStatus.Skipped 

1588 elif node.tag == "failure": 

1589 newTestcase._status = TestcaseStatus.Failed 

1590 elif node.tag == "error": 1590 ↛ 1591line 1590 didn't jump to line 1591 because the condition on line 1590 was never true

1591 newTestcase._status = TestcaseStatus.Errored 

1592 elif node.tag == "system-out": 

1593 pass 

1594 elif node.tag == "system-err": 

1595 pass 

1596 elif node.tag == "properties": 1596 ↛ 1599line 1596 didn't jump to line 1599 because the condition on line 1596 was always true

1597 pass 

1598 else: 

1599 raise UnittestException(f"Unknown element '{node.tag}' in junit file.") 

1600 else: 

1601 pass 

1602 

1603 if newTestcase._status is TestcaseStatus.Unknown: 

1604 newTestcase._status = TestcaseStatus.Passed 

1605 

1606 def Generate(self, overwrite: bool = False) -> None: 

1607 """ 

1608 Generate the internal XML data structure from test suites and test cases. 

1609 

1610 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods. 

1611 

1612 :param overwrite: Overwrite the internal XML data structure. 

1613 :raises UnittestException: If overwrite is false and the internal XML data structure is not empty. 

1614 """ 

1615 if not overwrite and self._xmlDocument is not None: 1615 ↛ 1616line 1615 didn't jump to line 1616 because the condition on line 1615 was never true

1616 raise UnittestException(f"Internal XML document is populated with data.") 

1617 

1618 rootElement = Element("testsuites") 

1619 rootElement.attrib["name"] = self._name 

1620 if self._startTime is not None: 

1621 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}" 

1622 if self._duration is not None: 

1623 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}" 

1624 rootElement.attrib["tests"] = str(self._tests) 

1625 rootElement.attrib["failures"] = str(self._failed) 

1626 rootElement.attrib["errors"] = str(self._errored) 

1627 rootElement.attrib["skipped"] = str(self._skipped) 

1628 # if self._assertionCount is not None: 

1629 # rootElement.attrib["assertions"] = f"{self._assertionCount}" 

1630 

1631 self._xmlDocument = ElementTree(rootElement) 

1632 

1633 for testsuite in self._testsuites.values(): 

1634 self._GenerateTestsuite(testsuite, rootElement) 

1635 

1636 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None: 

1637 """ 

1638 Generate the internal XML data structure for a test suite. 

1639 

1640 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods. 

1641 

1642 :param testsuite: The test suite to convert to an XML data structures. 

1643 :param parentElement: The parent XML data structure element, this data structure part will be added to. 

1644 :return: 

1645 """ 

1646 testsuiteElement = SubElement(parentElement, "testsuite") 

1647 testsuiteElement.attrib["name"] = testsuite._name 

1648 if testsuite._startTime is not None: 1648 ↛ 1650line 1648 didn't jump to line 1650 because the condition on line 1648 was always true

1649 testsuiteElement.attrib["timestamp"] = f"{testsuite._startTime.isoformat()}" 

1650 if testsuite._duration is not None: 

1651 testsuiteElement.attrib["time"] = f"{testsuite._duration.total_seconds():.6f}" 

1652 testsuiteElement.attrib["tests"] = str(testsuite._tests) 

1653 testsuiteElement.attrib["failures"] = str(testsuite._failed) 

1654 testsuiteElement.attrib["errors"] = str(testsuite._errored) 

1655 testsuiteElement.attrib["skipped"] = str(testsuite._skipped) 

1656 # if testsuite._assertionCount is not None: 

1657 # testsuiteElement.attrib["assertions"] = f"{testsuite._assertionCount}" 

1658 if testsuite._hostname is not None: 1658 ↛ 1659line 1658 didn't jump to line 1659 because the condition on line 1658 was never true

1659 testsuiteElement.attrib["hostname"] = testsuite._hostname 

1660 

1661 for testclass in testsuite._testclasses.values(): 

1662 for tc in testclass._testcases.values(): 

1663 self._GenerateTestcase(tc, testsuiteElement) 

1664 

1665 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None: 

1666 """ 

1667 Generate the internal XML data structure for a test case. 

1668 

1669 This method generates the XML element (``<testcase>``) and recursively calls other generated methods. 

1670 

1671 :param testcase: The test case to convert to an XML data structures. 

1672 :param parentElement: The parent XML data structure element, this data structure part will be added to. 

1673 :return: 

1674 """ 

1675 testcaseElement = SubElement(parentElement, "testcase") 

1676 if testcase.Classname is not None: 1676 ↛ 1678line 1676 didn't jump to line 1678 because the condition on line 1676 was always true

1677 testcaseElement.attrib["classname"] = testcase.Classname 

1678 testcaseElement.attrib["name"] = testcase._name 

1679 if testcase._duration is not None: 1679 ↛ 1681line 1679 didn't jump to line 1681 because the condition on line 1679 was always true

1680 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}" 

1681 if testcase._assertionCount is not None: 

1682 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}" 

1683 

1684 if testcase._status is TestcaseStatus.Passed: 

1685 pass 

1686 elif testcase._status is TestcaseStatus.Failed: 

1687 failureElement = SubElement(testcaseElement, "failure") 

1688 elif testcase._status is TestcaseStatus.Skipped: 1688 ↛ 1691line 1688 didn't jump to line 1691 because the condition on line 1688 was always true

1689 skippedElement = SubElement(testcaseElement, "skipped") 

1690 else: 

1691 errorElement = SubElement(testcaseElement, "error") 

1692 

1693 def __str__(self) -> str: 

1694 moduleName = self.__module__.split(".")[-1] 

1695 className = self.__class__.__name__ 

1696 return ( 

1697 f"<{moduleName}{className} {self._name} ({self._path}): {self._status.name} - suites/tests:{self.TestsuiteCount}/{self.TestcaseCount}>" 

1698 )