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

725 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 22:23 +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 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme 

934 

935 if IterationScheme.PreOrder in scheme: 

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

937 yield self 

938 

939 if IterationScheme.IncludeTestcases in scheme: 

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

941 yield testcase 

942 

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

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

945 

946 if IterationScheme.PostOrder in scheme: 

947 if IterationScheme.IncludeTestcases in scheme: 

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

949 yield testcase 

950 

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

952 yield self 

953 

954 @classmethod 

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

956 """ 

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

958 

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

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

961 """ 

962 juTestsuite = cls( 

963 testsuite._name, 

964 startTime=testsuite._startTime, 

965 duration=testsuite._totalDuration, 

966 status= testsuite._status, 

967 ) 

968 

969 juTestsuite._tests = testsuite._tests 

970 juTestsuite._skipped = testsuite._skipped 

971 juTestsuite._errored = testsuite._errored 

972 juTestsuite._failed = testsuite._failed 

973 juTestsuite._passed = testsuite._passed 

974 

975 for tc in testsuite.IterateTestcases(): 

976 ts = tc._parent 

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

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

979 

980 classname = ts._name 

981 ts = ts._parent 

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

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

984 ts = ts._parent 

985 

986 if classname in juTestsuite._testclasses: 

987 juClass = juTestsuite._testclasses[classname] 

988 else: 

989 juClass = Testclass(classname, parent=juTestsuite) 

990 

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

992 

993 return juTestsuite 

994 

995 def ToTestsuite(self) -> ut_Testsuite: 

996 testsuite = ut_Testsuite( 

997 self._name, 

998 TestsuiteKind.Logical, 

999 startTime=self._startTime, 

1000 totalDuration=self._duration, 

1001 status=self._status, 

1002 ) 

1003 

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

1005 suite = testsuite 

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

1007 for element in classpath: 

1008 if element in suite._testsuites: 

1009 suite = suite._testsuites[element] 

1010 else: 

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

1012 

1013 suite._kind = TestsuiteKind.Class 

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

1015 suite._parent._kind = TestsuiteKind.Module 

1016 

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

1018 

1019 return testsuite 

1020 

1021 def ToTree(self) -> Node: 

1022 node = Node( 

1023 value=self._name, 

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

1025 ) 

1026 node["startTime"] = self._startTime 

1027 node["duration"] = self._duration 

1028 

1029 return node 

1030 

1031 def __str__(self) -> str: 

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

1033 className = self.__class__.__name__ 

1034 return ( 

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

1036 ) 

1037 

1038 

1039@export 

1040class TestsuiteSummary(TestsuiteBase): 

1041 _testsuites: Dict[str, Testsuite] 

1042 

1043 def __init__( 

1044 self, 

1045 name: str, 

1046 startTime: Nullable[datetime] = None, 

1047 duration: Nullable[timedelta] = None, 

1048 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1050 ): 

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

1052 

1053 self._testsuites = {} 

1054 if testsuites is not None: 

1055 for testsuite in testsuites: 

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

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

1058 

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

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

1061 

1062 testsuite._parent = self 

1063 self._testsuites[testsuite._name] = testsuite 

1064 

1065 @readonly 

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

1067 return self._testsuites 

1068 

1069 @readonly 

1070 def TestcaseCount(self) -> int: 

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

1072 

1073 @readonly 

1074 def TestsuiteCount(self) -> int: 

1075 return len(self._testsuites) 

1076 

1077 @readonly 

1078 def AssertionCount(self) -> int: 

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

1080 

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

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

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

1084 

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

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

1087 

1088 testsuite._parent = self 

1089 self._testsuites[testsuite._name] = testsuite 

1090 

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

1092 for testsuite in testsuites: 

1093 self.AddTestsuite(testsuite) 

1094 

1095 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1097 

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

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

1100 tests += t 

1101 skipped += s 

1102 errored += e 

1103 weak += w 

1104 failed += f 

1105 passed += p 

1106 

1107 self._tests = tests 

1108 self._skipped = skipped 

1109 self._errored = errored 

1110 self._weak = weak 

1111 self._failed = failed 

1112 self._passed = passed 

1113 

1114 # FIXME: weak 

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

1116 self._status = TestsuiteStatus.Errored 

1117 elif failed > 0: 

1118 self._status = TestsuiteStatus.Failed 

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

1120 self._status = TestsuiteStatus.Empty 

1121 elif tests - skipped == passed: 

1122 self._status = TestsuiteStatus.Passed 

1123 elif tests == skipped: 

1124 self._status = TestsuiteStatus.Skipped 

1125 else: 

1126 self._status = TestsuiteStatus.Unknown 

1127 

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

1129 

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

1131 """ 

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

1133 

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

1135 

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

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

1138 """ 

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

1140 yield self 

1141 

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

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

1144 

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

1146 yield self 

1147 

1148 @classmethod 

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

1150 """ 

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

1152 

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

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

1155 """ 

1156 return cls( 

1157 testsuiteSummary._name, 

1158 startTime=testsuiteSummary._startTime, 

1159 duration=testsuiteSummary._totalDuration, 

1160 status=testsuiteSummary._status, 

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

1162 ) 

1163 

1164 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1165 """ 

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

1167 

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

1169 

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

1171 """ 

1172 return ut_TestsuiteSummary( 

1173 self._name, 

1174 startTime=self._startTime, 

1175 totalDuration=self._duration, 

1176 status=self._status, 

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

1178 ) 

1179 

1180 def ToTree(self) -> Node: 

1181 node = Node( 

1182 value=self._name, 

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

1184 ) 

1185 node["startTime"] = self._startTime 

1186 node["duration"] = self._duration 

1187 

1188 return node 

1189 

1190 def __str__(self) -> str: 

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

1192 className = self.__class__.__name__ 

1193 return ( 

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

1195 ) 

1196 

1197 

1198@export 

1199class Document(TestsuiteSummary, ut_Document): 

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

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

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

1203 

1204 _readerMode: JUnitReaderMode 

1205 _xmlDocument: Nullable[_ElementTree] 

1206 

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

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

1209 

1210 self._readerMode = readerMode 

1211 self._xmlDocument = None 

1212 

1213 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1214 

1215 @classmethod 

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

1217 doc = cls(xmlReportFile) 

1218 doc._name = testsuiteSummary._name 

1219 doc._startTime = testsuiteSummary._startTime 

1220 doc._duration = testsuiteSummary._totalDuration 

1221 doc._status = testsuiteSummary._status 

1222 doc._tests = testsuiteSummary._tests 

1223 doc._skipped = testsuiteSummary._skipped 

1224 doc._errored = testsuiteSummary._errored 

1225 doc._failed = testsuiteSummary._failed 

1226 doc._passed = testsuiteSummary._passed 

1227 

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

1229 

1230 return doc 

1231 

1232 def Analyze(self) -> None: 

1233 """ 

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

1235 schema. 

1236 

1237 .. hint:: 

1238 

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

1240 

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

1242 """ 

1243 xmlSchemaFile = "Any-JUnit.xsd" 

1244 self._Analyze(xmlSchemaFile) 

1245 

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

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

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

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

1250 

1251 startAnalysis = perf_counter_ns() 

1252 try: 

1253 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1254 except ToolingException as ex: 

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

1256 

1257 try: 

1258 schemaParser = XMLParser(ns_clean=True) 

1259 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1260 except XMLSyntaxError as ex: 

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

1262 

1263 try: 

1264 junitSchema = XMLSchema(schemaRoot) 

1265 except XMLSchemaParseError as ex: 

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

1267 

1268 try: 

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

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

1271 

1272 self._xmlDocument = junitDocument 

1273 except XMLSyntaxError as ex: 

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

1275 for logEntry in junitParser.error_log: 

1276 ex.add_note(str(logEntry)) 

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

1278 except Exception as ex: 

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

1280 

1281 endAnalysis = perf_counter_ns() 

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

1283 

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

1285 """ 

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

1287 

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

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

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

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

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

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

1294 """ 

1295 if path is None: 

1296 path = self._path 

1297 

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

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

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

1301 

1302 if regenerate: 

1303 self.Generate(overwrite=True) 

1304 

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

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

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

1308 raise ex 

1309 

1310 try: 

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

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

1313 except Exception as ex: 

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

1315 

1316 def Convert(self) -> None: 

1317 """ 

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

1319 

1320 This method converts the root element. 

1321 

1322 .. hint:: 

1323 

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

1325 

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

1327 """ 

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

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

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

1331 raise ex 

1332 

1333 startConversion = perf_counter_ns() 

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

1335 

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

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

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

1339 

1340 if False: # self._readerMode is JUnitReaderMode. 

1341 self._tests = self._ConvertTests(testsuitesNode) 

1342 self._skipped = self._ConvertSkipped(testsuitesNode) 

1343 self._errored = self._ConvertErrors(testsuitesNode) 

1344 self._failed = self._ConvertFailures(testsuitesNode) 

1345 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1346 

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

1348 self._ConvertTestsuite(self, rootNode) 

1349 

1350 if True: # self._readerMode is JUnitReaderMode. 

1351 self.Aggregate() 

1352 

1353 endConversation = perf_counter_ns() 

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

1355 

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

1357 """ 

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

1359 

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

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

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

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

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

1365 """ 

1366 if "name" in element.attrib: 

1367 return element.attrib["name"] 

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

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

1370 else: 

1371 return default 

1372 

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

1374 """ 

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

1376 

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

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

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

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

1381 """ 

1382 if "timestamp" in element.attrib: 

1383 timestamp = element.attrib["timestamp"] 

1384 return datetime.fromisoformat(timestamp) 

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

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

1387 else: 

1388 return None 

1389 

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

1391 """ 

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

1393 

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

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

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

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

1398 """ 

1399 if "time" in element.attrib: 

1400 time = element.attrib["time"] 

1401 return timedelta(seconds=float(time)) 

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

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

1404 else: 

1405 return None 

1406 

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

1408 """ 

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

1410 

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

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

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

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

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

1416 """ 

1417 if "hostname" in element.attrib: 

1418 return element.attrib["hostname"] 

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

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

1421 else: 

1422 return default 

1423 

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

1425 """ 

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

1427 

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

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

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

1431 """ 

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

1433 return element.attrib["classname"] 

1434 else: 

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

1436 

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

1438 """ 

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

1440 

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

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

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

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

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

1446 """ 

1447 if "tests" in element.attrib: 

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

1449 elif not optional: 

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

1451 else: 

1452 return default 

1453 

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

1455 """ 

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

1457 

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

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

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

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

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

1463 """ 

1464 if "skipped" in element.attrib: 

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

1466 elif not optional: 

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

1468 else: 

1469 return default 

1470 

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

1472 """ 

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

1474 

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

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

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

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

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

1480 """ 

1481 if "errors" in element.attrib: 

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

1483 elif not optional: 

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

1485 else: 

1486 return default 

1487 

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

1489 """ 

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

1491 

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

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

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

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

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

1497 """ 

1498 if "failures" in element.attrib: 

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

1500 elif not optional: 

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

1502 else: 

1503 return default 

1504 

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

1506 """ 

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

1508 

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

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

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

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

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

1514 """ 

1515 if "assertions" in element.attrib: 

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

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

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

1519 else: 

1520 return default 

1521 

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

1523 """ 

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

1525 

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

1527 

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

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

1530 """ 

1531 newTestsuite = self._TESTSUITE( 

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

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

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

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

1536 parent=parent 

1537 ) 

1538 

1539 if False: # self._readerMode is JUnitReaderMode. 

1540 self._tests = self._ConvertTests(testsuitesNode) 

1541 self._skipped = self._ConvertSkipped(testsuitesNode) 

1542 self._errored = self._ConvertErrors(testsuitesNode) 

1543 self._failed = self._ConvertFailures(testsuitesNode) 

1544 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1545 

1546 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1547 

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

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

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

1551 # self._ConvertTestsuite(newTestsuite, node) 

1552 # el 

1553 if node.tag == "testcase": 

1554 self._ConvertTestcase(newTestsuite, node) 

1555 

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

1557 """ 

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

1559 

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

1561 

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

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

1564 """ 

1565 className = self._ConvertClassname(testcaseNode) 

1566 testclass = self._FindOrCreateTestclass(parent, className) 

1567 

1568 newTestcase = self._TESTCASE( 

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

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

1571 assertionCount=self._ConvertAssertions(testcaseNode), 

1572 parent=testclass 

1573 ) 

1574 

1575 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1576 

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

1578 if className in parent._testclasses: 

1579 return parent._testclasses[className] 

1580 else: 

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

1582 

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

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

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

1586 pass 

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

1588 if node.tag == "skipped": 

1589 newTestcase._status = TestcaseStatus.Skipped 

1590 elif node.tag == "failure": 

1591 newTestcase._status = TestcaseStatus.Failed 

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

1593 newTestcase._status = TestcaseStatus.Errored 

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

1595 pass 

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

1597 pass 

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

1599 pass 

1600 else: 

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

1602 else: 

1603 pass 

1604 

1605 if newTestcase._status is TestcaseStatus.Unknown: 

1606 newTestcase._status = TestcaseStatus.Passed 

1607 

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

1609 """ 

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

1611 

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

1613 

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

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

1616 """ 

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

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

1619 

1620 rootElement = Element("testsuites") 

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

1622 if self._startTime is not None: 

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

1624 if self._duration is not None: 

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

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

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

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

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

1630 # if self._assertionCount is not None: 

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

1632 

1633 self._xmlDocument = ElementTree(rootElement) 

1634 

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

1636 self._GenerateTestsuite(testsuite, rootElement) 

1637 

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

1639 """ 

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

1641 

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

1643 

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

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

1646 :return: 

1647 """ 

1648 testsuiteElement = SubElement(parentElement, "testsuite") 

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

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

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

1652 if testsuite._duration is not None: 

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

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

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

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

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

1658 # if testsuite._assertionCount is not None: 

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

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

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

1662 

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

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

1665 self._GenerateTestcase(tc, testsuiteElement) 

1666 

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

1668 """ 

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

1670 

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

1672 

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

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

1675 :return: 

1676 """ 

1677 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1683 if testcase._assertionCount is not None: 

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

1685 

1686 if testcase._status is TestcaseStatus.Passed: 

1687 pass 

1688 elif testcase._status is TestcaseStatus.Failed: 

1689 failureElement = SubElement(testcaseElement, "failure") 

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

1691 skippedElement = SubElement(testcaseElement, "skipped") 

1692 else: 

1693 errorElement = SubElement(testcaseElement, "error") 

1694 

1695 def __str__(self) -> str: 

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

1697 className = self.__class__.__name__ 

1698 return ( 

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

1700 )