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

751 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:49 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2024-2026 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 __future__ import annotations 

94 

95from datetime import datetime, timedelta 

96from enum import Flag 

97from io import BytesIO 

98from pathlib import Path 

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

100 

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

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

103from pyTooling.Common import getFullyQualifiedName, getTypeName, getResourceFile 

104from pyTooling.Decorators import export, readonly 

105from pyTooling.Stopwatch import Stopwatch 

106from pyTooling.Exceptions import ToolingException 

107from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod 

108from pyTooling.Tree import Node 

109 

110from pyEDAA.Reports import Resources 

111from pyEDAA.Reports.Unittesting import UnittestError, AlreadyInTestsuiteError, DuplicateTestsuiteError 

112from pyEDAA.Reports.Unittesting import DuplicateTestcaseError 

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

114from pyEDAA.Reports.Unittesting import TestcaseOutputMixin 

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

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

117 

118 

119@export 

120class JUnitErrorMixin: 

121 """An error-mixin for JUnit format specific errors.""" 

122 

123 

124@export 

125class UnittestError(UnittestError, JUnitErrorMixin): 

126 pass 

127 

128 

129@export 

130class AlreadyInTestsuiteError(AlreadyInTestsuiteError, JUnitErrorMixin): 

131 """ 

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

133 

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

135 hierarchy should occur only once in the hierarchy. 

136 

137 .. hint:: 

138 

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

140 """ 

141 

142 

143@export 

144class DuplicateTestsuiteError(DuplicateTestsuiteError, JUnitErrorMixin): 

145 """ 

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

147 

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

149 

150 .. hint:: 

151 

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

153 """ 

154 

155 

156@export 

157class DuplicateTestcaseError(DuplicateTestcaseError, JUnitErrorMixin): 

158 """ 

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

160 

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

162 

163 .. hint:: 

164 

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

166 """ 

167 

168 

169@export 

170class JUnitReaderMode(Flag): 

171 Default = 0 #: Default behavior 

172 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented 

173 

174 

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

176TestcaseAggregateReturnType = Tuple[int, int, int] 

177TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int] 

178 

179 

180@export 

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

182 """ 

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

184 

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

186 hierarchy. 

187 

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

189 child. |br| 

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

191 """ 

192 

193 _parent: Nullable[Testsuite] 

194 _name: str 

195 

196 def __init__(self, name: str, *, parent: Nullable[Testsuite] = None) -> None: 

197 """ 

198 Initializes the fields of the base-class. 

199 

200 :param name: Name of the test entity. 

201 :param parent: Optional, reference to the parent test entity. 

202 :raises ValueError: When parameter 'name' is None. 

203 :raises TypeError: When parameter 'name' is not a string. 

204 :raises ValueError: When parameter 'name' is empty. 

205 """ 

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

207 raise ValueError("Parameter 'name' is None.") 

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

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

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

211 raise ex 

212 elif name.strip() == "": 

213 raise ValueError("Parameter 'name' is empty.") 

214 

215 self._parent = parent 

216 self._name = name 

217 

218 @readonly 

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

220 """ 

221 Read-only property to access the reference to the parent test entity. 

222 

223 :returns: Reference to the parent entity. 

224 """ 

225 return self._parent 

226 

227 # QUESTION: allow Parent as setter? 

228 

229 @readonly 

230 def Name(self) -> str: 

231 """ 

232 Read-only property to access the test entity's name. 

233 

234 :returns: Name of the test entity. 

235 """ 

236 return self._name 

237 

238 

239@export 

240class BaseWithProperties(Base): 

241 """ 

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

243 

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

245 

246 Every test entity offers an internal dictionary for properties. 

247 """ 

248 

249 _duration: Nullable[timedelta] 

250 _assertionCount: Nullable[int] 

251 _properties: Dict[str, Any] 

252 

253 def __init__( 

254 self, 

255 name: str, 

256 duration: Nullable[timedelta] = None, 

257 assertionCount: Nullable[int] = None, 

258 *, 

259 parent: Nullable[Testsuite] = None 

260 ) -> None: 

261 """ 

262 Initializes the fields of the base-class. 

263 

264 :param name: Name of the test entity. 

265 :param duration: Optional, duration of the entity's execution. 

266 :param assertionCount: Optional, number of assertions within the test. 

267 :param parent: Optional, reference to the parent test entity. 

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

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

270 """ 

271 super().__init__(name, parent=parent) 

272 

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

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

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

276 raise ex 

277 

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

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

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

281 raise ex 

282 

283 self._duration = duration 

284 self._assertionCount = assertionCount 

285 

286 self._properties = {} 

287 

288 @readonly 

289 def Duration(self) -> timedelta: 

290 """ 

291 Read-only property to access the duration of a test entity run. 

292 

293 .. note:: 

294 

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

296 

297 :returns: Duration of the entity's execution. 

298 """ 

299 return self._duration 

300 

301 @readonly 

302 @abstractmethod 

303 def AssertionCount(self) -> int: 

304 """ 

305 Read-only property to access the number of assertions (checks) in a test case. 

306 

307 .. note:: 

308 

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

310 

311 :returns: Number of assertions. 

312 """ 

313 

314 def __len__(self) -> int: 

315 """ 

316 Returns the number of annotated properties. 

317 

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

319 

320 :returns: Number of annotated properties. 

321 """ 

322 return len(self._properties) 

323 

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

325 """ 

326 Access a property by name. 

327 

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

329 

330 :param name: Name if the property. 

331 :returns: Value of the accessed property. 

332 """ 

333 return self._properties[name] 

334 

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

336 """ 

337 Set the value of a property by name. 

338 

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

340 

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

342 

343 :param name: Name of the property. 

344 :param value: Value of the property. 

345 """ 

346 self._properties[name] = value 

347 

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

349 """ 

350 Delete a property by name. 

351 

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

353 

354 :param name: Name if the property. 

355 """ 

356 del self._properties[name] 

357 

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

359 """ 

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

361 

362 Syntax: :pycode:`name in obj` 

363 

364 :param name: Name of the property. 

365 :returns: True, if the property was annotated. 

366 """ 

367 return name in self._properties 

368 

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

370 """ 

371 Iterate all annotated properties. 

372 

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

374 

375 :returns: A generator of property tuples (name, value). 

376 """ 

377 yield from self._properties.items() 

378 

379 

380@export 

381class Testcase(BaseWithProperties, TestcaseOutputMixin): 

382 """ 

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

384 

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

386 of the hierarchy is a test summary. 

387 

388 Every test case has an overall status like unknown, skipped, failed or passed. The message and details of a 

389 ``<failure>``, ``<error>`` or ``<skipped>`` element, as well as the captured output of ``<system-out>`` and 

390 ``<system-err>`` are provided by :class:`~pyEDAA.Reports.Unittesting.TestcaseOutputMixin`. 

391 """ 

392 

393 _status: TestcaseStatus 

394 

395 def __init__( 

396 self, 

397 name: str, 

398 duration: Nullable[timedelta] = None, 

399 status: TestcaseStatus = TestcaseStatus.Unknown, 

400 assertionCount: Nullable[int] = None, 

401 message: Nullable[str] = None, 

402 details: Nullable[str] = None, 

403 standardOutput: Nullable[str] = None, 

404 standardError: Nullable[str] = None, 

405 *, 

406 parent: Nullable[Testclass] = None 

407 ) -> None: 

408 """ 

409 Initializes the fields of a test case. 

410 

411 :param name: Name of the test entity. 

412 :param duration: Optional, duration of the entity's execution. 

413 :param status: Optional, status of the test case. 

414 :param assertionCount: Optional, number of assertions within the test. 

415 :param message: Optional, message explaining the test case's status. 

416 :param details: Optional, details explaining the test case's status (e.g. a traceback). 

417 :param standardOutput: Optional, captured standard output of the test case. 

418 :param standardError: Optional, captured standard error of the test case. 

419 :param parent: Optional, reference to the parent test class. 

420 :raises TypeError: If parameter 'parent' is not a Testclass. 

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

422 """ 

423 if parent is not None: 

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

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

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

427 raise ex 

428 

429 parent._testcases[name] = self 

430 

431 super().__init__(name, duration, assertionCount, parent=parent) 

432 TestcaseOutputMixin.__init__(self, message, details, standardOutput, standardError) 

433 

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

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

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

437 raise ex 

438 

439 self._status = status 

440 

441 @readonly 

442 def Classname(self) -> str: 

443 """ 

444 Read-only property to access the class name of the test case. 

445 

446 :returns: The test case's class name. 

447 

448 .. note:: 

449 

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

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

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

453 """ 

454 if self._parent is None: 

455 raise UnittestError("Standalone Testcase instance is not linked to a Testclass.") 

456 return self._parent._name 

457 

458 @readonly 

459 def Status(self) -> TestcaseStatus: 

460 """ 

461 Read-only property to access the status of the test case. 

462 

463 :returns: The test case's status. 

464 """ 

465 return self._status 

466 

467 @readonly 

468 def AssertionCount(self) -> int: 

469 """ 

470 Read-only property to return the number of assertions (checks) in a test case. 

471 

472 .. note:: 

473 

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

475 

476 :returns: Number of assertions. 

477 """ 

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

479 return 0 

480 return self._assertionCount 

481 

482 def Copy(self) -> Testcase: 

483 return self.__class__( 

484 self._name, 

485 self._duration, 

486 self._status, 

487 self._assertionCount, 

488 self._message, 

489 self._details, 

490 self._standardOutput, 

491 self._standardError 

492 ) 

493 

494 def Aggregate(self) -> None: 

495 if self._status is TestcaseStatus.Unknown: 

496 if self._assertionCount is None: 

497 self._status = TestcaseStatus.Passed 

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

499 self._status = TestcaseStatus.Weak 

500 else: 

501 self._status = TestcaseStatus.Failed 

502 

503 # TODO: check for setup errors 

504 # TODO: check for teardown errors 

505 

506 @classmethod 

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

508 """ 

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

510 

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

512 :returns: Test case from JUnit specific data model. 

513 """ 

514 return cls( 

515 testcase._name, 

516 duration=testcase._testDuration, 

517 status= testcase._status, 

518 assertionCount=testcase._assertionCount, 

519 message=testcase._message, 

520 details=testcase._details, 

521 standardOutput=testcase._standardOutput, 

522 standardError=testcase._standardError 

523 ) 

524 

525 def ToTestcase(self) -> ut_Testcase: 

526 return ut_Testcase( 

527 self._name, 

528 testDuration=self._duration, 

529 status=self._status, 

530 assertionCount=self._assertionCount, 

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

532 passedAssertionCount=self._assertionCount, 

533 message=self._message, 

534 details=self._details, 

535 standardOutput=self._standardOutput, 

536 standardError=self._standardError 

537 ) 

538 

539 def ToTree(self) -> Node: 

540 node = Node(value=self._name) 

541 node["status"] = self._status 

542 node["assertionCount"] = self._assertionCount 

543 node["duration"] = self._duration 

544 

545 return node 

546 

547 def __str__(self) -> str: 

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

549 className = getTypeName(self) 

550 return ( 

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

552 ) 

553 

554 

555@export 

556class TestsuiteBase(BaseWithProperties): 

557 """ 

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

559 

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

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

562 test summary contains no test classes and test cases. 

563 """ 

564 

565 _startTime: Nullable[datetime] 

566 _status: TestsuiteStatus 

567 

568 _tests: int 

569 _skipped: int 

570 _errored: int 

571 _weak: int 

572 _failed: int 

573 _passed: int 

574 

575 def __init__( 

576 self, 

577 name: str, 

578 startTime: Nullable[datetime] = None, 

579 duration: Nullable[timedelta] = None, 

580 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

581 *, 

582 parent: Nullable[Testsuite] = None 

583 ) -> None: 

584 """ 

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

586 

587 :param name: Name of the test entity. 

588 :param startTime: Optional, time when the test entity was started. 

589 :param duration: Optional, duration of the entity's execution. 

590 :param status: Optional, overall status of the test entity. 

591 :param parent: Optional, reference to the parent test entity. 

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

593 """ 

594 if parent is not None: 

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

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

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

598 raise ex 

599 

600 parent._testsuites[name] = self 

601 

602 super().__init__(name, duration, None, parent=parent) 

603 

604 self._startTime = startTime 

605 self._status = status 

606 self._tests = 0 

607 self._skipped = 0 

608 self._errored = 0 

609 self._failed = 0 

610 self._passed = 0 

611 

612 @readonly 

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

614 """ 

615 Read-only property to access the time the test entity's execution started. 

616 

617 :returns: Start time of the execution, or ``None`` if it wasn't recorded. 

618 """ 

619 return self._startTime 

620 

621 @readonly 

622 def Status(self) -> TestsuiteStatus: 

623 """ 

624 Read-only property to access the test entity's aggregated status. 

625 

626 :returns: Status of the test entity. 

627 """ 

628 return self._status 

629 

630 @readonly 

631 @mustoverride 

632 def TestcaseCount(self) -> int: 

633 """ 

634 Read-only property to access the number of testcases in this entity. 

635 

636 :returns: Number of testcases. 

637 """ 

638 pass 

639 

640 @readonly 

641 def Tests(self) -> int: 

642 """ 

643 Read-only property to access the number of testcases in this entity. 

644 

645 :returns: Number of testcases. 

646 """ 

647 return self.TestcaseCount 

648 

649 @readonly 

650 def Skipped(self) -> int: 

651 """ 

652 Read-only property to access the number of skipped testcases. 

653 

654 :returns: Number of skipped testcases. 

655 """ 

656 return self._skipped 

657 

658 @readonly 

659 def Errored(self) -> int: 

660 """ 

661 Read-only property to access the number of errored testcases. 

662 

663 :returns: Number of errored testcases. 

664 """ 

665 return self._errored 

666 

667 @readonly 

668 def Failed(self) -> int: 

669 """ 

670 Read-only property to access the number of failed testcases. 

671 

672 :returns: Number of failed testcases. 

673 """ 

674 return self._failed 

675 

676 @readonly 

677 def Passed(self) -> int: 

678 """ 

679 Read-only property to access the number of passed testcases. 

680 

681 :returns: Number of passed testcases. 

682 """ 

683 return self._passed 

684 

685 def Aggregate(self) -> TestsuiteAggregateReturnType: 

686 tests = 0 

687 skipped = 0 

688 errored = 0 

689 weak = 0 

690 failed = 0 

691 passed = 0 

692 

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

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

695 # tests += t 

696 # skipped += s 

697 # errored += e 

698 # weak += w 

699 # failed += f 

700 # passed += p 

701 

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

703 

704 @mustoverride 

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

706 pass 

707 

708 

709@export 

710class Testclass(Base): 

711 """ 

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

713 

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

715 """ 

716 

717 _testcases: Dict[str, Testcase] 

718 

719 def __init__( 

720 self, 

721 classname: str, 

722 testcases: Nullable[Iterable[Testcase]] = None, 

723 *, 

724 parent: Nullable[Testsuite] = None 

725 ) -> None: 

726 """ 

727 Initializes the fields of the test class. 

728 

729 :param classname: Classname of the test entity. 

730 :param parent: Optional, reference to the parent test suite. 

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

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

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

734 """ 

735 if parent is not None: 

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

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

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

739 raise ex 

740 

741 parent._testclasses[classname] = self 

742 

743 super().__init__(classname, parent=parent) 

744 

745 self._testcases = {} 

746 if testcases is not None: 

747 for testcase in testcases: 

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

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

750 

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

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

753 

754 testcase._parent = self 

755 self._testcases[testcase._name] = testcase 

756 

757 @readonly 

758 def Classname(self) -> str: 

759 """ 

760 Read-only property to access the name of the test class. 

761 

762 :returns: The test class' name. 

763 """ 

764 return self._name 

765 

766 @readonly 

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

768 """ 

769 Read-only property to access a reference to the internal dictionary of test cases. 

770 

771 :returns: Reference to the dictionary of test cases. 

772 """ 

773 return self._testcases 

774 

775 @readonly 

776 def TestcaseCount(self) -> int: 

777 """ 

778 Read-only property to return the number of all test cases in the test entity hierarchy. 

779 

780 :returns: Number of test cases. 

781 """ 

782 return len(self._testcases) 

783 

784 @readonly 

785 def AssertionCount(self) -> int: 

786 """ 

787 Read-only property to return the number of assertions across all testcases of this testclass. 

788 

789 :returns: Sum of the testcases' assertion counts. 

790 """ 

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

792 

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

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

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

796 

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

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

799 

800 testcase._parent = self 

801 self._testcases[testcase._name] = testcase 

802 

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

804 for testcase in testcases: 

805 self.AddTestcase(testcase) 

806 

807 def ToTestsuite(self) -> ut_Testsuite: 

808 return ut_Testsuite( 

809 self._name, 

810 kind=TestsuiteKind.Class, 

811 # startTime=self._startTime, 

812 # totalDuration=self._duration, 

813 # status=self._status, 

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

815 ) 

816 

817 def ToTree(self) -> Node: 

818 node = Node( 

819 value=self._name, 

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

821 ) 

822 

823 return node 

824 

825 def __str__(self) -> str: 

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

827 className = getTypeName(self) 

828 return ( 

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

830 ) 

831 

832 

833@export 

834class Testsuite(TestsuiteBase): 

835 """ 

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

837 

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

839 """ 

840 

841 _hostname: str 

842 _testclasses: Dict[str, Testclass] 

843 

844 def __init__( 

845 self, 

846 name: str, 

847 hostname: Nullable[str] = None, 

848 startTime: Nullable[datetime] = None, 

849 duration: Nullable[timedelta] = None, 

850 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

851 testclasses: Nullable[Iterable[Testclass]] = None, 

852 *, 

853 parent: Nullable[TestsuiteSummary] = None 

854 ) -> None: 

855 """ 

856 Initializes the fields of a test suite. 

857 

858 :param name: Name of the test suite. 

859 :param startTime: Optional, time when the test suite was started. 

860 :param duration: Optional, duration of the entity's execution. 

861 :param status: Optional, overall status of the test suite. 

862 :param parent: Optional, reference to the parent test summary. 

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

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

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

866 :raises DuplicateTestcaseError: If a test case in parameter 'testcases' is already listed (by name) in the list of 

867 test cases. 

868 """ 

869 if parent is not None: 

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

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

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

873 raise ex 

874 

875 parent._testsuites[name] = self 

876 

877 super().__init__(name, startTime, duration, status, parent=parent) 

878 

879 self._hostname = hostname 

880 

881 self._testclasses = {} 

882 if testclasses is not None: 

883 for testclass in testclasses: 

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

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

886 

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

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

889 

890 testclass._parent = self 

891 self._testclasses[testclass._name] = testclass 

892 

893 @readonly 

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

895 """ 

896 Read-only property to access the host the testsuite was executed on. 

897 

898 :returns: Hostname, or ``None`` if it wasn't recorded. 

899 """ 

900 return self._hostname 

901 

902 @readonly 

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

904 """ 

905 Read-only property to access the testsuite's testclasses. 

906 

907 :returns: Dictionary of testclass names and testclasses. 

908 """ 

909 return self._testclasses 

910 

911 @readonly 

912 def TestclassCount(self) -> int: 

913 """ 

914 Read-only property to return the number of testclasses in this testsuite. 

915 

916 :returns: Number of testclasses. 

917 """ 

918 return len(self._testclasses) 

919 

920 # @readonly 

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

922 # return self._classes 

923 

924 @readonly 

925 def TestcaseCount(self) -> int: 

926 """ 

927 Read-only property to return the number of testcases across all testclasses. 

928 

929 :returns: Sum of the testclasses' testcase counts. 

930 """ 

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

932 

933 @readonly 

934 def AssertionCount(self) -> int: 

935 """ 

936 Read-only property to return the number of assertions across all testclasses. 

937 

938 :returns: Sum of the testclasses' assertion counts. 

939 """ 

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

941 

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

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

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

945 

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

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

948 

949 testclass._parent = self 

950 self._testclasses[testclass._name] = testclass 

951 

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

953 for testcase in testclasses: 

954 self.AddTestclass(testcase) 

955 

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

957 # return self.Iterate(scheme) 

958 

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

960 return self.Iterate(scheme) 

961 

962 def Copy(self) -> Testsuite: 

963 return self.__class__( 

964 self._name, 

965 self._hostname, 

966 self._startTime, 

967 self._duration, 

968 self._status 

969 ) 

970 

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

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

973 

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

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

976 _ = testcase.Aggregate() 

977 

978 tests += 1 

979 status = testcase._status 

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

981 raise UnittestError(f"Found testcase '{testcase._name}' with state 'Unknown'.") 

982 elif status is TestcaseStatus.Skipped: 

983 skipped += 1 

984 elif status is TestcaseStatus.Errored: 

985 errored += 1 

986 elif status is TestcaseStatus.Passed: 

987 passed += 1 

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

989 failed += 1 

990 elif status is TestcaseStatus.Weak: 

991 weak += 1 

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

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

994 else: 

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

996 

997 self._tests = tests 

998 self._skipped = skipped 

999 self._errored = errored 

1000 self._weak = weak 

1001 self._failed = failed 

1002 self._passed = passed 

1003 

1004 # FIXME: weak? 

1005 if errored > 0: 

1006 self._status = TestsuiteStatus.Errored 

1007 elif failed > 0: 

1008 self._status = TestsuiteStatus.Failed 

1009 elif tests == 0: 

1010 self._status = TestsuiteStatus.Empty 

1011 elif tests - skipped == passed: 1011 ↛ 1013line 1011 didn't jump to line 1013 because the condition on line 1011 was always true

1012 self._status = TestsuiteStatus.Passed 

1013 elif tests == skipped: 

1014 self._status = TestsuiteStatus.Skipped 

1015 else: 

1016 self._status = TestsuiteStatus.Unknown 

1017 

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

1019 

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

1021 """ 

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

1023 

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

1025 

1026 :param scheme: Optional, scheme how to iterate the test suite and its child elements. 

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

1028 """ 

1029 if IterationScheme.PreOrder in scheme: 

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

1031 yield self 

1032 

1033 if IterationScheme.IncludeTestcases in scheme: 

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

1035 yield testcase 

1036 

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

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

1039 

1040 if IterationScheme.PostOrder in scheme: 

1041 if IterationScheme.IncludeTestcases in scheme: 

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

1043 yield testcase 

1044 

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

1046 yield self 

1047 

1048 @classmethod 

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

1050 """ 

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

1052 

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

1054 :returns: Test suite from JUnit specific data model. 

1055 """ 

1056 juTestsuite = cls( 

1057 testsuite._name, 

1058 hostname=testsuite._hostname, 

1059 startTime=testsuite._startTime, 

1060 duration=testsuite._totalDuration, 

1061 status= testsuite._status, 

1062 ) 

1063 

1064 for tc in testsuite.IterateTestcases(): 

1065 ts = tc._parent 

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

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

1068 

1069 classname = ts._name 

1070 ts = ts._parent 

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

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

1073 ts = ts._parent 

1074 

1075 if classname in juTestsuite._testclasses: 

1076 juClass = juTestsuite._testclasses[classname] 

1077 else: 

1078 juClass = Testclass(classname, parent=juTestsuite) 

1079 

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

1081 

1082 return juTestsuite 

1083 

1084 def ToTestsuite(self) -> ut_Testsuite: 

1085 testsuite = ut_Testsuite( 

1086 self._name, 

1087 kind=TestsuiteKind.Logical, 

1088 hostname=self._hostname, 

1089 startTime=self._startTime, 

1090 totalDuration=self._duration, 

1091 status=self._status, 

1092 ) 

1093 

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

1095 suite = testsuite 

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

1097 for element in classpath: 

1098 if element in suite._testsuites: 

1099 suite = suite._testsuites[element] 

1100 else: 

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

1102 

1103 suite._kind = TestsuiteKind.Class 

1104 if suite._parent is not testsuite: 

1105 suite._parent._kind = TestsuiteKind.Module 

1106 

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

1108 

1109 return testsuite 

1110 

1111 def ToTree(self) -> Node: 

1112 node = Node( 

1113 value=self._name, 

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

1115 ) 

1116 node["startTime"] = self._startTime 

1117 node["duration"] = self._duration 

1118 

1119 return node 

1120 

1121 def __str__(self) -> str: 

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

1123 className = getTypeName(self) 

1124 return ( 

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

1126 ) 

1127 

1128 

1129@export 

1130class TestsuiteSummary(TestsuiteBase): 

1131 _testsuites: Dict[str, Testsuite] 

1132 

1133 def __init__( 

1134 self, 

1135 name: str, 

1136 startTime: Nullable[datetime] = None, 

1137 duration: Nullable[timedelta] = None, 

1138 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1140 ) -> None: 

1141 super().__init__(name, startTime, duration, status) 

1142 

1143 self._testsuites = {} 

1144 if testsuites is not None: 

1145 for testsuite in testsuites: 

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

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

1148 

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

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

1151 

1152 testsuite._parent = self 

1153 self._testsuites[testsuite._name] = testsuite 

1154 

1155 @readonly 

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

1157 """ 

1158 Read-only property to access the summary's testsuites. 

1159 

1160 :returns: Dictionary of testsuite names and testsuites. 

1161 """ 

1162 return self._testsuites 

1163 

1164 @readonly 

1165 def TestcaseCount(self) -> int: 

1166 """ 

1167 Read-only property to return the number of testcases across all testsuites. 

1168 

1169 :returns: Sum of the testsuites' testcase counts. 

1170 """ 

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

1172 

1173 @readonly 

1174 def TestsuiteCount(self) -> int: 

1175 """ 

1176 Read-only property to return the number of testsuites in this summary. 

1177 

1178 :returns: Number of testsuites. 

1179 """ 

1180 return len(self._testsuites) 

1181 

1182 @readonly 

1183 def AssertionCount(self) -> int: 

1184 """ 

1185 Read-only property to return the number of assertions across all testsuites. 

1186 

1187 :returns: Sum of the testsuites' assertion counts. 

1188 """ 

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

1190 

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

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

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

1194 

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

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

1197 

1198 testsuite._parent = self 

1199 self._testsuites[testsuite._name] = testsuite 

1200 

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

1202 for testsuite in testsuites: 

1203 self.AddTestsuite(testsuite) 

1204 

1205 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1207 

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

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

1210 tests += t 

1211 skipped += s 

1212 errored += e 

1213 weak += w 

1214 failed += f 

1215 passed += p 

1216 

1217 self._tests = tests 

1218 self._skipped = skipped 

1219 self._errored = errored 

1220 self._weak = weak 

1221 self._failed = failed 

1222 self._passed = passed 

1223 

1224 # FIXME: weak 

1225 if errored > 0: 

1226 self._status = TestsuiteStatus.Errored 

1227 elif failed > 0: 

1228 self._status = TestsuiteStatus.Failed 

1229 elif tests == 0: 

1230 self._status = TestsuiteStatus.Empty 

1231 elif tests - skipped == passed: 1231 ↛ 1233line 1231 didn't jump to line 1233 because the condition on line 1231 was always true

1232 self._status = TestsuiteStatus.Passed 

1233 elif tests == skipped: 

1234 self._status = TestsuiteStatus.Skipped 

1235 else: 

1236 self._status = TestsuiteStatus.Unknown 

1237 

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

1239 

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

1241 """ 

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

1243 

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

1245 

1246 :param scheme: Optional, scheme how to iterate the test suite summary and its child elements. 

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

1248 """ 

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

1250 yield self 

1251 

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

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

1254 

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

1256 yield self 

1257 

1258 @classmethod 

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

1260 """ 

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

1262 

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

1264 :returns: Test suite summary from JUnit specific data model. 

1265 """ 

1266 return cls( 

1267 testsuiteSummary._name, 

1268 startTime=testsuiteSummary._startTime, 

1269 duration=testsuiteSummary._totalDuration, 

1270 status=testsuiteSummary._status, 

1271 testsuites=(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values()) 

1272 ) 

1273 

1274 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1275 """ 

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

1277 

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

1279 

1280 :returns: A test suite summary of the unified test entity data model. 

1281 """ 

1282 return ut_TestsuiteSummary( 

1283 self._name, 

1284 startTime=self._startTime, 

1285 totalDuration=self._duration, 

1286 status=self._status, 

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

1288 ) 

1289 

1290 def ToTree(self) -> Node: 

1291 node = Node( 

1292 value=self._name, 

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

1294 ) 

1295 node["startTime"] = self._startTime 

1296 node["duration"] = self._duration 

1297 

1298 return node 

1299 

1300 def __str__(self) -> str: 

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

1302 className = getTypeName(self) 

1303 return ( 

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

1305 ) 

1306 

1307 

1308@export 

1309class Document(TestsuiteSummary, ut_Document): 

1310 _DIALECT: ClassVar[str] = "Any-JUnit" 

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

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

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

1314 

1315 _readerMode: JUnitReaderMode 

1316 _xmlDocument: Nullable[_ElementTree] 

1317 

1318 def __init__( 

1319 self, 

1320 xmlReportFile: Path, 

1321 analyzeAndConvert: bool = False, 

1322 readerMode: JUnitReaderMode = JUnitReaderMode.Default, 

1323 *, 

1324 name: str = "Unprocessed JUnit XML file", 

1325 startTime: Nullable[datetime] = None, 

1326 duration: Nullable[timedelta] = None, 

1327 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1329 ) -> None: 

1330 """ 

1331 Initializes a JUnit XML document, read from or written to a file. 

1332 

1333 :param xmlReportFile: Path to the JUnit XML file. 

1334 :param analyzeAndConvert: Optional, if true, analyze (parse and validate) the file and convert its content. 

1335 :param readerMode: Optional, how strictly the file is read. 

1336 :param name: Optional, name of the test suite summary. 

1337 :param startTime: Optional, time when the test run was started. 

1338 :param duration: Optional, duration of the test run. 

1339 :param status: Optional, overall status of the test run. 

1340 :param testsuites: Optional, test suites of the summary. 

1341 """ 

1342 super().__init__(name, startTime, duration, status, testsuites) 

1343 

1344 self._readerMode = readerMode 

1345 self._xmlDocument = None 

1346 

1347 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1348 

1349 @classmethod 

1350 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary) -> Document: 

1351 """ 

1352 Convert a test suite summary of the unified test entity data model to a JUnit XML document of this dialect. 

1353 

1354 The test suites become test suites of the dialect (:attr:`_TESTSUITE`). The counters are computed by 

1355 :meth:`Aggregate`. 

1356 

1357 :param xmlReportFile: Path to the JUnit XML file to write. 

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

1359 :returns: JUnit XML document of this dialect. 

1360 """ 

1361 doc = cls( 

1362 xmlReportFile, 

1363 name=testsuiteSummary._name, 

1364 startTime=testsuiteSummary._startTime, 

1365 duration=testsuiteSummary._totalDuration, 

1366 status=testsuiteSummary._status, 

1367 testsuites=(cls._TESTSUITE.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values()) 

1368 ) 

1369 doc.Aggregate() 

1370 

1371 return doc 

1372 

1373 def Analyze(self) -> None: 

1374 """ 

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

1376 schema. 

1377 

1378 .. hint:: 

1379 

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

1381 

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

1383 """ 

1384 xmlSchemaFile = "Any-JUnit.xsd" 

1385 self._Analyze(xmlSchemaFile) 

1386 

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

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

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

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

1391 

1392 with Stopwatch() as sw: 

1393 try: 

1394 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1395 except ToolingException as ex: 

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

1397 

1398 try: 

1399 schemaParser = XMLParser(ns_clean=True) 

1400 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1401 except XMLSyntaxError as ex: 

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

1403 

1404 try: 

1405 junitSchema = XMLSchema(schemaRoot) 

1406 except XMLSchemaParseError as ex: 

1407 raise UnittestError(f"Error while parsing XML Schema '{xmlSchemaFile}'.") from ex 

1408 

1409 try: 

1410 content = self._path.read_bytes() 

1411 except OSError as ex: 

1412 raise UnittestError(f"Couldn't read JUnit XML file '{self._path}'.") from ex 

1413 

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

1415 try: 

1416 self._xmlDocument = parse(BytesIO(content), parser=junitParser, base_url=str(self._path)) 

1417 except XMLSyntaxError as ex: 

1418 for logEntry in junitParser.error_log: 

1419 ex.add_note(str(logEntry)) 

1420 raise UnittestError( 

1421 f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'." 

1422 ) from ex 

1423 

1424 self._analysisDuration = sw.Duration 

1425 

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

1427 """ 

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

1429 

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

1431 :param overwrite: Optional, if true, overwrite an existing file. 

1432 :param regenerate: Optional, if true, regenerate the XML structure from data model. 

1433 :raises UnittestError: If the file cannot be overwritten. 

1434 :raises UnittestError: If the internal XML data structure wasn't generated. 

1435 :raises UnittestError: If the file cannot be opened or written. 

1436 """ 

1437 if path is None: 

1438 path = self._path 

1439 

1440 if not overwrite and path.exists(): 

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

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

1443 

1444 if regenerate: 

1445 self.Generate(overwrite=True) 

1446 

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

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

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

1450 raise ex 

1451 

1452 content = tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True) 

1453 try: 

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

1455 file.write(content) 

1456 except OSError as ex: 

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

1458 

1459 def Convert(self) -> None: 

1460 """ 

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

1462 

1463 This method converts the root element. 

1464 

1465 .. hint:: 

1466 

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

1468 

1469 :raises UnittestError: If XML was not read and parsed before. 

1470 """ 

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

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

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

1474 raise ex 

1475 

1476 with Stopwatch() as sw: 

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

1478 

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

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

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

1482 

1483 if False: # self._readerMode is JUnitReaderMode. 

1484 self._tests = self._ConvertTests(testsuitesNode) 

1485 self._skipped = self._ConvertSkipped(testsuitesNode) 

1486 self._errored = self._ConvertErrors(testsuitesNode) 

1487 self._failed = self._ConvertFailures(testsuitesNode) 

1488 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1489 

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

1491 self._ConvertTestsuite(self, rootNode) 

1492 

1493 if True: # self._readerMode is JUnitReaderMode. 

1494 self.Aggregate() 

1495 

1496 self._modelConversion = sw.Duration 

1497 

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

1499 """ 

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

1501 

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

1503 :param default: Optional, the default value, if no ``name`` attribute was found. 

1504 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1507 """ 

1508 if "name" in element.attrib: 

1509 return element.attrib["name"] 

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

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

1512 else: 

1513 return default 

1514 

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

1516 """ 

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

1518 

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

1520 :param optional: Optional, if false, an exception is raised for the missing attribute. 

1521 :returns: The ``timestamp`` attribute's content if found, otherwise ``None``. 

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

1523 """ 

1524 if "timestamp" in element.attrib: 

1525 timestamp = element.attrib["timestamp"] 

1526 return datetime.fromisoformat(timestamp) 

1527 elif not optional: 

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

1529 else: 

1530 return None 

1531 

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

1533 """ 

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

1535 

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

1537 :param optional: Optional, if false, an exception is raised for the missing attribute. 

1538 :returns: The ``time`` attribute's content if found, otherwise ``None``. 

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

1540 """ 

1541 if "time" in element.attrib: 

1542 time = element.attrib["time"] 

1543 return timedelta(seconds=float(time)) 

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

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

1546 else: 

1547 return None 

1548 

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

1550 """ 

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

1552 

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

1554 :param default: Optional, the default value, if no ``hostname`` attribute was found. 

1555 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1558 """ 

1559 if "hostname" in element.attrib: 

1560 return element.attrib["hostname"] 

1561 elif not optional: 

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

1563 else: 

1564 return default 

1565 

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

1567 """ 

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

1569 

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

1571 :returns: The ``classname`` attribute's content. 

1572 :raises UnittestError: If no ``classname`` attribute exists on the given element node. 

1573 """ 

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

1575 return element.attrib["classname"] 

1576 else: 

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

1578 

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

1580 """ 

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

1582 

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

1584 :param default: Optional, the default value, if no ``tests`` attribute was found. 

1585 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1588 """ 

1589 if "tests" in element.attrib: 

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

1591 elif not optional: 

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

1593 else: 

1594 return default 

1595 

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

1597 """ 

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

1599 

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

1601 :param default: Optional, the default value, if no ``skipped`` attribute was found. 

1602 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1605 """ 

1606 if "skipped" in element.attrib: 

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

1608 elif not optional: 

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

1610 else: 

1611 return default 

1612 

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

1614 """ 

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

1616 

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

1618 :param default: Optional, the default value, if no ``errors`` attribute was found. 

1619 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1622 """ 

1623 if "errors" in element.attrib: 

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

1625 elif not optional: 

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

1627 else: 

1628 return default 

1629 

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

1631 """ 

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

1633 

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

1635 :param default: Optional, the default value, if no ``failures`` attribute was found. 

1636 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1639 """ 

1640 if "failures" in element.attrib: 

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

1642 elif not optional: 

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

1644 else: 

1645 return default 

1646 

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

1648 """ 

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

1650 

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

1652 :param default: Optional, the default value, if no ``assertions`` attribute was found. 

1653 :param optional: Optional, if false, an exception is raised for the missing attribute. 

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

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

1656 """ 

1657 if "assertions" in element.attrib: 

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

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

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

1661 else: 

1662 return default 

1663 

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

1665 """ 

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

1667 

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

1669 

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

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

1672 """ 

1673 newTestsuite = self._TESTSUITE( 

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

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

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

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

1678 parent=parent 

1679 ) 

1680 

1681 if False: # self._readerMode is JUnitReaderMode. 

1682 self._tests = self._ConvertTests(testsuitesNode) 

1683 self._skipped = self._ConvertSkipped(testsuitesNode) 

1684 self._errored = self._ConvertErrors(testsuitesNode) 

1685 self._failed = self._ConvertFailures(testsuitesNode) 

1686 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1687 

1688 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1689 

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

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

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

1693 # self._ConvertTestsuite(newTestsuite, node) 

1694 # el 

1695 if node.tag == "testcase": 

1696 self._ConvertTestcase(newTestsuite, node) 

1697 

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

1699 """ 

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

1701 

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

1703 

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

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

1706 """ 

1707 className = self._ConvertClassname(testcaseNode) 

1708 testclass = self._FindOrCreateTestclass(parent, className) 

1709 

1710 newTestcase = self._TESTCASE( 

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

1712 self._ConvertTime(testcaseNode, optional=True), 

1713 assertionCount=self._ConvertAssertions(testcaseNode), 

1714 parent=testclass 

1715 ) 

1716 

1717 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1718 

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

1720 if className in parent._testclasses: 

1721 return parent._testclasses[className] 

1722 else: 

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

1724 

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

1726 """ 

1727 Convert the child elements of a ``<testcase>`` to the test case's status, message, details and captured output. 

1728 

1729 A ``<skipped>``, ``<failure>`` or ``<error>`` element sets the status. Its ``message`` attribute becomes the test 

1730 case's message, its text becomes the test case's details. The texts of ``<system-out>`` and ``<system-err>`` 

1731 become the captured standard output and standard error; multiple such elements are concatenated. The reruns of a 

1732 test case (``<flakyFailure>``, ``<flakyError>``, ``<rerunFailure>`` and ``<rerunError>``) don't change its status. 

1733 

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

1735 :param newTestcase: The test case to update. 

1736 :raises UnittestError: If an unknown element is found. 

1737 """ 

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

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

1740 pass 

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

1742 if node.tag == "skipped": 

1743 newTestcase._status = TestcaseStatus.Skipped 

1744 self._ConvertStatusMessage(node, newTestcase) 

1745 elif node.tag == "failure": 

1746 newTestcase._status = TestcaseStatus.Failed 

1747 self._ConvertStatusMessage(node, newTestcase) 

1748 elif node.tag == "error": 

1749 newTestcase._status = TestcaseStatus.Errored 

1750 self._ConvertStatusMessage(node, newTestcase) 

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

1752 if node.text is None: 1752 ↛ 1753line 1752 didn't jump to line 1753 because the condition on line 1752 was never true

1753 pass 

1754 elif newTestcase._standardOutput is None: 

1755 newTestcase._standardOutput = node.text 

1756 else: 

1757 newTestcase._standardOutput += node.text 

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

1759 if node.text is None: 

1760 pass 

1761 elif newTestcase._standardError is None: 

1762 newTestcase._standardError = node.text 

1763 else: 

1764 newTestcase._standardError += node.text 

1765 elif node.tag in ("properties", "flakyFailure", "flakyError", "rerunFailure", "rerunError"): 1765 ↛ 1768line 1765 didn't jump to line 1768 because the condition on line 1765 was always true

1766 pass 

1767 else: 

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

1769 else: 

1770 pass 

1771 

1772 if newTestcase._status is TestcaseStatus.Unknown: 

1773 newTestcase._status = TestcaseStatus.Passed 

1774 

1775 def _ConvertStatusMessage(self, element: _Element, testcase: Testcase) -> None: 

1776 """ 

1777 Convert the ``message`` attribute and the text of a ``<skipped>``, ``<failure>`` or ``<error>`` element. 

1778 

1779 An empty text (only whitespace) is not recorded. 

1780 

1781 :param element: The XML element node explaining the test case's status. 

1782 :param testcase: The test case to update. 

1783 """ 

1784 testcase._message = element.attrib.get("message", None) 

1785 testcase._details = None if element.text is None or element.text.strip() == "" else element.text 

1786 

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

1788 """ 

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

1790 

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

1792 

1793 :param overwrite: Optional, overwrite the internal XML data structure. 

1794 :raises UnittestError: If overwrite is false and the internal XML data structure is not empty. 

1795 """ 

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

1797 raise UnittestError("Internal XML document is populated with data.") 

1798 

1799 rootElement = Element("testsuites") 

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

1801 if self._startTime is not None: 

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

1803 if self._duration is not None: 

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

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

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

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

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

1809 # if self._assertionCount is not None: 

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

1811 

1812 self._xmlDocument = ElementTree(rootElement) 

1813 

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

1815 self._GenerateTestsuite(testsuite, rootElement) 

1816 

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

1818 """ 

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

1820 

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

1822 

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

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

1825 """ 

1826 testsuiteElement = SubElement(parentElement, "testsuite") 

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

1828 if testsuite._startTime is not None: 

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

1830 if testsuite._duration is not None: 

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

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

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

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

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

1836 # if testsuite._assertionCount is not None: 

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

1838 if testsuite._hostname is not None: 

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

1840 

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

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

1843 self._GenerateTestcase(tc, testsuiteElement) 

1844 

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

1846 """ 

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

1848 

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

1850 

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

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

1853 """ 

1854 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1860 if testcase._assertionCount is not None: 

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

1862 

1863 self._GenerateTestcaseChildren(testcase, testcaseElement) 

1864 

1865 def _GenerateTestcaseChildren(self, testcase: Testcase, testcaseElement: _Element) -> None: 

1866 """ 

1867 Generate the child elements of a ``<testcase>`` from the test case's status, message, details and captured output. 

1868 

1869 A failed, skipped or errored test case gets a ``<failure>``, ``<skipped>`` or ``<error>`` element carrying the 

1870 message (``message`` attribute) and details (text). Captured standard output and standard error are written as 

1871 ``<system-out>`` and ``<system-err>`` elements. 

1872 

1873 :param testcase: The test case to convert to XML child elements. 

1874 :param testcaseElement: The ``<testcase>`` element, the child elements will be added to. 

1875 """ 

1876 if testcase._status is TestcaseStatus.Passed: 

1877 statusElement = None 

1878 elif testcase._status is TestcaseStatus.Failed: 

1879 statusElement = SubElement(testcaseElement, "failure") 

1880 elif testcase._status is TestcaseStatus.Skipped: 

1881 statusElement = SubElement(testcaseElement, "skipped") 

1882 else: 

1883 statusElement = SubElement(testcaseElement, "error") 

1884 

1885 if statusElement is not None: 

1886 if testcase._message is not None: 

1887 statusElement.attrib["message"] = testcase._message 

1888 

1889 if testcase._details is not None: 

1890 statusElement.text = testcase._details 

1891 

1892 if testcase._standardOutput is not None: 

1893 SubElement(testcaseElement, "system-out").text = testcase._standardOutput 

1894 

1895 if testcase._standardError is not None: 

1896 SubElement(testcaseElement, "system-err").text = testcase._standardError 

1897 

1898 def __str__(self) -> str: 

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

1900 className = getTypeName(self) 

1901 return ( 

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

1903 )