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

711 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-16 22:20 +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] 

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: 

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 _failed: int 

548 _passed: int 

549 

550 def __init__( 

551 self, 

552 name: str, 

553 startTime: Nullable[datetime] = None, 

554 duration: Nullable[timedelta] = None, 

555 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

556 parent: Nullable["Testsuite"] = None 

557 ): 

558 """ 

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

560 

561 :param name: Name of the test entity. 

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

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

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

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

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

567 """ 

568 if parent is not None: 

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

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

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

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

573 raise ex 

574 

575 parent._testsuites[name] = self 

576 

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

578 

579 self._startTime = startTime 

580 self._status = status 

581 self._tests = 0 

582 self._skipped = 0 

583 self._errored = 0 

584 self._failed = 0 

585 self._passed = 0 

586 

587 @readonly 

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

589 return self._startTime 

590 

591 @readonly 

592 def Status(self) -> TestsuiteStatus: 

593 return self._status 

594 

595 @readonly 

596 @mustoverride 

597 def TestcaseCount(self) -> int: 

598 pass 

599 

600 @readonly 

601 def Tests(self) -> int: 

602 return self.TestcaseCount 

603 

604 @readonly 

605 def Skipped(self) -> int: 

606 return self._skipped 

607 

608 @readonly 

609 def Errored(self) -> int: 

610 return self._errored 

611 

612 @readonly 

613 def Failed(self) -> int: 

614 return self._failed 

615 

616 @readonly 

617 def Passed(self) -> int: 

618 return self._passed 

619 

620 def Aggregate(self) -> TestsuiteAggregateReturnType: 

621 tests = 0 

622 skipped = 0 

623 errored = 0 

624 failed = 0 

625 passed = 0 

626 

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

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

629 # tests += t 

630 # skipped += s 

631 # errored += e 

632 # weak += w 

633 # failed += f 

634 # passed += p 

635 

636 return tests, skipped, errored, failed, passed 

637 

638 @mustoverride 

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

640 pass 

641 

642 

643@export 

644class Testclass(Base): 

645 """ 

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

647 

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

649 """ 

650 

651 _testcases: Dict[str, "Testcase"] 

652 

653 def __init__( 

654 self, 

655 classname: str, 

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

657 parent: Nullable["Testsuite"] = None 

658 ): 

659 """ 

660 Initializes the fields of the test class. 

661 

662 :param classname: Classname of the test entity. 

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

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

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

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

667 """ 

668 if parent is not None: 

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

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

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

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

673 raise ex 

674 

675 parent._testclasses[classname] = self 

676 

677 super().__init__(classname, parent) 

678 

679 self._testcases = {} 

680 if testcases is not None: 

681 for testcase in testcases: 

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

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

684 

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

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

687 

688 testcase._parent = self 

689 self._testcases[testcase._name] = testcase 

690 

691 @readonly 

692 def Classname(self) -> str: 

693 """ 

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

695 

696 :return: The test class' name. 

697 """ 

698 return self._name 

699 

700 @readonly 

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

702 """ 

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

704 

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

706 """ 

707 return self._testcases 

708 

709 @readonly 

710 def TestcaseCount(self) -> int: 

711 """ 

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

713 

714 :return: Number of test cases. 

715 """ 

716 return len(self._testcases) 

717 

718 @readonly 

719 def AssertionCount(self) -> int: 

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

721 

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

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

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

725 

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

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

728 

729 testcase._parent = self 

730 self._testcases[testcase._name] = testcase 

731 

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

733 for testcase in testcases: 

734 self.AddTestcase(testcase) 

735 

736 def ToTestsuite(self) -> ut_Testsuite: 

737 return ut_Testsuite( 

738 self._name, 

739 TestsuiteKind.Class, 

740 # startTime=self._startTime, 

741 # totalDuration=self._duration, 

742 # status=self._status, 

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

744 ) 

745 

746 def ToTree(self) -> Node: 

747 node = Node( 

748 value=self._name, 

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

750 ) 

751 

752 return node 

753 

754 def __str__(self) -> str: 

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

756 className = self.__class__.__name__ 

757 return ( 

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

759 ) 

760 

761 

762@export 

763class Testsuite(TestsuiteBase): 

764 """ 

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

766 

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

768 """ 

769 

770 _hostname: str 

771 _testclasses: Dict[str, "Testclass"] 

772 

773 def __init__( 

774 self, 

775 name: str, 

776 hostname: Nullable[str] = None, 

777 startTime: Nullable[datetime] = None, 

778 duration: Nullable[timedelta] = None, 

779 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

781 parent: Nullable["TestsuiteSummary"] = None 

782 ): 

783 """ 

784 Initializes the fields of a test suite. 

785 

786 :param name: Name of the test suite. 

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

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

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

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

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

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

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

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

795 """ 

796 if parent is not None: 

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

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

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

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

801 raise ex 

802 

803 parent._testsuites[name] = self 

804 

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

806 

807 self._hostname = hostname 

808 

809 self._testclasses = {} 

810 if testclasses is not None: 

811 for testclass in testclasses: 

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

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

814 

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

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

817 

818 testclass._parent = self 

819 self._testclasses[testclass._name] = testclass 

820 

821 @readonly 

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

823 return self._hostname 

824 

825 @readonly 

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

827 return self._testclasses 

828 

829 @readonly 

830 def TestclassCount(self) -> int: 

831 return len(self._testclasses) 

832 

833 # @readonly 

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

835 # return self._classes 

836 

837 @readonly 

838 def TestcaseCount(self) -> int: 

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

840 

841 @readonly 

842 def AssertionCount(self) -> int: 

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

844 

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

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

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

848 

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

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

851 

852 testclass._parent = self 

853 self._testclasses[testclass._name] = testclass 

854 

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

856 for testcase in testclasses: 

857 self.AddTestclass(testcase) 

858 

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

860 # return self.Iterate(scheme) 

861 

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

863 return self.Iterate(scheme) 

864 

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

866 return self.__class__( 

867 self._name, 

868 self._hostname, 

869 self._startTime, 

870 self._duration, 

871 self._status 

872 ) 

873 

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

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

876 

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

878 _ = testclass.Aggregate(strict) 

879 

880 tests += 1 

881 

882 status = testclass._status 

883 if status is TestcaseStatus.Unknown: 

884 raise UnittestException(f"Found testclass '{testclass._name}' with state 'Unknown'.") 

885 elif status is TestcaseStatus.Skipped: 

886 skipped += 1 

887 elif status is TestcaseStatus.Errored: 

888 errored += 1 

889 elif status is TestcaseStatus.Passed: 

890 passed += 1 

891 elif status is TestcaseStatus.Failed: 

892 failed += 1 

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

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

895 else: 

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

897 

898 self._tests = tests 

899 self._skipped = skipped 

900 self._errored = errored 

901 self._failed = failed 

902 self._passed = passed 

903 

904 if errored > 0: 

905 self._status = TestsuiteStatus.Errored 

906 elif failed > 0: 

907 self._status = TestsuiteStatus.Failed 

908 elif tests == 0: 

909 self._status = TestsuiteStatus.Empty 

910 elif tests - skipped == passed: 

911 self._status = TestsuiteStatus.Passed 

912 elif tests == skipped: 

913 self._status = TestsuiteStatus.Skipped 

914 else: 

915 self._status = TestsuiteStatus.Unknown 

916 

917 return tests, skipped, errored, failed, passed 

918 

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

920 """ 

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

922 

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

924 

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

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

927 """ 

928 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme 

929 

930 if IterationScheme.PreOrder in scheme: 

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

932 yield self 

933 

934 if IterationScheme.IncludeTestcases in scheme: 

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

936 yield testcase 

937 

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

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

940 

941 if IterationScheme.PostOrder in scheme: 

942 if IterationScheme.IncludeTestcases in scheme: 

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

944 yield testcase 

945 

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

947 yield self 

948 

949 @classmethod 

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

951 """ 

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

953 

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

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

956 """ 

957 juTestsuite = cls( 

958 testsuite._name, 

959 startTime=testsuite._startTime, 

960 duration=testsuite._totalDuration, 

961 status= testsuite._status, 

962 ) 

963 

964 juTestsuite._tests = testsuite._tests 

965 juTestsuite._skipped = testsuite._skipped 

966 juTestsuite._errored = testsuite._errored 

967 juTestsuite._failed = testsuite._failed 

968 juTestsuite._passed = testsuite._passed 

969 

970 for tc in testsuite.IterateTestcases(): 

971 ts = tc._parent 

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

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

974 

975 classname = ts._name 

976 ts = ts._parent 

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

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

979 ts = ts._parent 

980 

981 if classname in juTestsuite._testclasses: 

982 juClass = juTestsuite._testclasses[classname] 

983 else: 

984 juClass = Testclass(classname, parent=juTestsuite) 

985 

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

987 

988 return juTestsuite 

989 

990 def ToTestsuite(self) -> ut_Testsuite: 

991 testsuite = ut_Testsuite( 

992 self._name, 

993 TestsuiteKind.Logical, 

994 startTime=self._startTime, 

995 totalDuration=self._duration, 

996 status=self._status, 

997 ) 

998 

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

1000 suite = testsuite 

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

1002 for element in classpath: 

1003 if element in suite._testsuites: 

1004 suite = suite._testsuites[element] 

1005 else: 

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

1007 

1008 suite._kind = TestsuiteKind.Class 

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

1010 suite._parent._kind = TestsuiteKind.Module 

1011 

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

1013 

1014 return testsuite 

1015 

1016 def ToTree(self) -> Node: 

1017 node = Node( 

1018 value=self._name, 

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

1020 ) 

1021 node["startTime"] = self._startTime 

1022 node["duration"] = self._duration 

1023 

1024 return node 

1025 

1026 def __str__(self) -> str: 

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

1028 className = self.__class__.__name__ 

1029 return ( 

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

1031 ) 

1032 

1033 

1034@export 

1035class TestsuiteSummary(TestsuiteBase): 

1036 _testsuites: Dict[str, Testsuite] 

1037 

1038 def __init__( 

1039 self, 

1040 name: str, 

1041 startTime: Nullable[datetime] = None, 

1042 duration: Nullable[timedelta] = None, 

1043 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1045 ): 

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

1047 

1048 self._testsuites = {} 

1049 if testsuites is not None: 

1050 for testsuite in testsuites: 

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

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

1053 

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

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

1056 

1057 testsuite._parent = self 

1058 self._testsuites[testsuite._name] = testsuite 

1059 

1060 @readonly 

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

1062 return self._testsuites 

1063 

1064 @readonly 

1065 def TestcaseCount(self) -> int: 

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

1067 

1068 @readonly 

1069 def TestsuiteCount(self) -> int: 

1070 return len(self._testsuites) 

1071 

1072 @readonly 

1073 def AssertionCount(self) -> int: 

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

1075 

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

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

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

1079 

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

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

1082 

1083 testsuite._parent = self 

1084 self._testsuites[testsuite._name] = testsuite 

1085 

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

1087 for testsuite in testsuites: 

1088 self.AddTestsuite(testsuite) 

1089 

1090 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1092 

1093 self._tests = tests 

1094 self._skipped = skipped 

1095 self._errored = errored 

1096 self._failed = failed 

1097 self._passed = passed 

1098 

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

1100 self._status = TestsuiteStatus.Errored 

1101 elif failed > 0: 1101 ↛ 1102line 1101 didn't jump to line 1102 because the condition on line 1101 was never true

1102 self._status = TestsuiteStatus.Failed 

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

1104 self._status = TestsuiteStatus.Empty 

1105 elif tests - skipped == passed: 

1106 self._status = TestsuiteStatus.Passed 

1107 elif tests == skipped: 

1108 self._status = TestsuiteStatus.Skipped 

1109 else: 

1110 self._status = TestsuiteStatus.Unknown 

1111 

1112 return tests, skipped, errored, failed, passed 

1113 

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

1115 """ 

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

1117 

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

1119 

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

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

1122 """ 

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

1124 yield self 

1125 

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

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

1128 

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

1130 yield self 

1131 

1132 @classmethod 

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

1134 """ 

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

1136 

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

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

1139 """ 

1140 return cls( 

1141 testsuiteSummary._name, 

1142 startTime=testsuiteSummary._startTime, 

1143 duration=testsuiteSummary._totalDuration, 

1144 status=testsuiteSummary._status, 

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

1146 ) 

1147 

1148 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1149 """ 

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

1151 

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

1153 

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

1155 """ 

1156 return ut_TestsuiteSummary( 

1157 self._name, 

1158 startTime=self._startTime, 

1159 totalDuration=self._duration, 

1160 status=self._status, 

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

1162 ) 

1163 

1164 def ToTree(self) -> Node: 

1165 node = Node( 

1166 value=self._name, 

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

1168 ) 

1169 node["startTime"] = self._startTime 

1170 node["duration"] = self._duration 

1171 

1172 return node 

1173 

1174 def __str__(self) -> str: 

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

1176 className = self.__class__.__name__ 

1177 return ( 

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

1179 ) 

1180 

1181 

1182@export 

1183class Document(TestsuiteSummary, ut_Document): 

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

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

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

1187 

1188 _readerMode: JUnitReaderMode 

1189 _xmlDocument: Nullable[_ElementTree] 

1190 

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

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

1193 

1194 self._readerMode = readerMode 

1195 self._xmlDocument = None 

1196 

1197 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1198 

1199 @classmethod 

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

1201 doc = cls(xmlReportFile) 

1202 doc._name = testsuiteSummary._name 

1203 doc._startTime = testsuiteSummary._startTime 

1204 doc._duration = testsuiteSummary._totalDuration 

1205 doc._status = testsuiteSummary._status 

1206 doc._tests = testsuiteSummary._tests 

1207 doc._skipped = testsuiteSummary._skipped 

1208 doc._errored = testsuiteSummary._errored 

1209 doc._failed = testsuiteSummary._failed 

1210 doc._passed = testsuiteSummary._passed 

1211 

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

1213 

1214 return doc 

1215 

1216 def Analyze(self) -> None: 

1217 """ 

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

1219 schema. 

1220 

1221 .. hint:: 

1222 

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

1224 

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

1226 """ 

1227 xmlSchemaFile = "Any-JUnit.xsd" 

1228 self._Analyze(xmlSchemaFile) 

1229 

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

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

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

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

1234 

1235 startAnalysis = perf_counter_ns() 

1236 try: 

1237 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1238 except ToolingException as ex: 

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

1240 

1241 try: 

1242 schemaParser = XMLParser(ns_clean=True) 

1243 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1244 except XMLSyntaxError as ex: 

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

1246 

1247 try: 

1248 junitSchema = XMLSchema(schemaRoot) 

1249 except XMLSchemaParseError as ex: 

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

1251 

1252 try: 

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

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

1255 

1256 self._xmlDocument = junitDocument 

1257 except XMLSyntaxError as ex: 

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

1259 for logEntry in junitParser.error_log: 

1260 ex.add_note(str(logEntry)) 

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

1262 except Exception as ex: 

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

1264 

1265 endAnalysis = perf_counter_ns() 

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

1267 

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

1269 """ 

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

1271 

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

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

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

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

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

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

1278 """ 

1279 if path is None: 

1280 path = self._path 

1281 

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

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

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

1285 

1286 if regenerate: 

1287 self.Generate(overwrite=True) 

1288 

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

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

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

1292 raise ex 

1293 

1294 try: 

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

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

1297 except Exception as ex: 

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

1299 

1300 def Convert(self) -> None: 

1301 """ 

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

1303 

1304 This method converts the root element. 

1305 

1306 .. hint:: 

1307 

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

1309 

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

1311 """ 

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

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

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

1315 raise ex 

1316 

1317 startConversion = perf_counter_ns() 

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

1319 

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

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

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

1323 

1324 if False: # self._readerMode is JUnitReaderMode. 

1325 self._tests = self._ConvertTests(testsuitesNode) 

1326 self._skipped = self._ConvertSkipped(testsuitesNode) 

1327 self._errored = self._ConvertErrors(testsuitesNode) 

1328 self._failed = self._ConvertFailures(testsuitesNode) 

1329 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1330 

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

1332 self._ConvertTestsuite(self, rootNode) 

1333 

1334 if True: # self._readerMode is JUnitReaderMode. 

1335 self.Aggregate() 

1336 

1337 endConversation = perf_counter_ns() 

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

1339 

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

1341 """ 

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

1343 

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

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

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

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

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

1349 """ 

1350 if "name" in element.attrib: 

1351 return element.attrib["name"] 

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

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

1354 else: 

1355 return default 

1356 

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

1358 """ 

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

1360 

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

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

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

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

1365 """ 

1366 if "timestamp" in element.attrib: 

1367 timestamp = element.attrib["timestamp"] 

1368 return datetime.fromisoformat(timestamp) 

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

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

1371 else: 

1372 return None 

1373 

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

1375 """ 

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

1377 

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

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

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

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

1382 """ 

1383 if "time" in element.attrib: 

1384 time = element.attrib["time"] 

1385 return timedelta(seconds=float(time)) 

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

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

1388 else: 

1389 return None 

1390 

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

1392 """ 

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

1394 

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

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

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

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

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

1400 """ 

1401 if "hostname" in element.attrib: 

1402 return element.attrib["hostname"] 

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

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

1405 else: 

1406 return default 

1407 

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

1409 """ 

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

1411 

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

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

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

1415 """ 

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

1417 return element.attrib["classname"] 

1418 else: 

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

1420 

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

1422 """ 

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

1424 

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

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

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

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

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

1430 """ 

1431 if "tests" in element.attrib: 

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

1433 elif not optional: 

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

1435 else: 

1436 return default 

1437 

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

1439 """ 

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

1441 

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

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

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

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

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

1447 """ 

1448 if "skipped" in element.attrib: 

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

1450 elif not optional: 

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

1452 else: 

1453 return default 

1454 

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

1456 """ 

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

1458 

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

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

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

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

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

1464 """ 

1465 if "errors" in element.attrib: 

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

1467 elif not optional: 

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

1469 else: 

1470 return default 

1471 

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

1473 """ 

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

1475 

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

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

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

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

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

1481 """ 

1482 if "failures" in element.attrib: 

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

1484 elif not optional: 

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

1486 else: 

1487 return default 

1488 

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

1490 """ 

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

1492 

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

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

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

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

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

1498 """ 

1499 if "assertions" in element.attrib: 

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

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

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

1503 else: 

1504 return default 

1505 

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

1507 """ 

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

1509 

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

1511 

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

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

1514 """ 

1515 newTestsuite = self._TESTSUITE( 

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

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

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

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

1520 parent=parent 

1521 ) 

1522 

1523 if False: # self._readerMode is JUnitReaderMode. 

1524 self._tests = self._ConvertTests(testsuitesNode) 

1525 self._skipped = self._ConvertSkipped(testsuitesNode) 

1526 self._errored = self._ConvertErrors(testsuitesNode) 

1527 self._failed = self._ConvertFailures(testsuitesNode) 

1528 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1529 

1530 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1531 

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

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

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

1535 # self._ConvertTestsuite(newTestsuite, node) 

1536 # el 

1537 if node.tag == "testcase": 

1538 self._ConvertTestcase(newTestsuite, node) 

1539 

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

1541 """ 

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

1543 

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

1545 

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

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

1548 """ 

1549 className = self._ConvertClassname(testcaseNode) 

1550 testclass = self._FindOrCreateTestclass(parent, className) 

1551 

1552 newTestcase = self._TESTCASE( 

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

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

1555 assertionCount=self._ConvertAssertions(testcaseNode), 

1556 parent=testclass 

1557 ) 

1558 

1559 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1560 

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

1562 if className in parent._testclasses: 

1563 return parent._testclasses[className] 

1564 else: 

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

1566 

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

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

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

1570 pass 

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

1572 if node.tag == "skipped": 

1573 newTestcase._status = TestcaseStatus.Skipped 

1574 elif node.tag == "failure": 

1575 newTestcase._status = TestcaseStatus.Failed 

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

1577 newTestcase._status = TestcaseStatus.Errored 

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

1579 pass 

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

1581 pass 

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

1583 pass 

1584 else: 

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

1586 else: 

1587 pass 

1588 

1589 if newTestcase._status is TestcaseStatus.Unknown: 

1590 newTestcase._status = TestcaseStatus.Passed 

1591 

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

1593 """ 

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

1595 

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

1597 

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

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

1600 """ 

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

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

1603 

1604 rootElement = Element("testsuites") 

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

1606 if self._startTime is not None: 

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

1608 if self._duration is not None: 

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

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

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

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

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

1614 # if self._assertionCount is not None: 

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

1616 

1617 self._xmlDocument = ElementTree(rootElement) 

1618 

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

1620 self._GenerateTestsuite(testsuite, rootElement) 

1621 

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

1623 """ 

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

1625 

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

1627 

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

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

1630 :return: 

1631 """ 

1632 testsuiteElement = SubElement(parentElement, "testsuite") 

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

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

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

1636 if testsuite._duration is not None: 

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

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

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

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

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

1642 # if testsuite._assertionCount is not None: 

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

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

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

1646 

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

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

1649 self._GenerateTestcase(tc, testsuiteElement) 

1650 

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

1652 """ 

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

1654 

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

1656 

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

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

1659 :return: 

1660 """ 

1661 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1667 if testcase._assertionCount is not None: 

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

1669 

1670 if testcase._status is TestcaseStatus.Passed: 

1671 pass 

1672 elif testcase._status is TestcaseStatus.Failed: 1672 ↛ 1673line 1672 didn't jump to line 1673 because the condition on line 1672 was never true

1673 failureElement = SubElement(testcaseElement, "failure") 

1674 elif testcase._status is TestcaseStatus.Skipped: 

1675 skippedElement = SubElement(testcaseElement, "skipped") 

1676 else: 

1677 errorElement = SubElement(testcaseElement, "error") 

1678 

1679 def __str__(self) -> str: 

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

1681 className = self.__class__.__name__ 

1682 return ( 

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

1684 )