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

704 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-07 22:48 +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 datetime import datetime, timedelta 

94from enum import Flag 

95from pathlib import Path 

96from sys import version_info 

97from time import perf_counter_ns 

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

99 

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

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

102from pyTooling.Common import getFullyQualifiedName, getResourceFile 

103from pyTooling.Decorators import export, readonly 

104from pyTooling.Exceptions import ToolingException 

105from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod 

106from pyTooling.Tree import Node 

107 

108from pyEDAA.Reports import Resources 

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

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

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

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

113 

114 

115@export 

116class JUnitException: 

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

118 

119 

120@export 

121class UnittestException(UnittestException, JUnitException): 

122 pass 

123 

124 

125@export 

126class AlreadyInHierarchyException(AlreadyInHierarchyException, JUnitException): 

127 """ 

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

129 

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

131 hierarchy should occur only once in the hierarchy. 

132 

133 .. hint:: 

134 

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

136 """ 

137 

138 

139@export 

140class DuplicateTestsuiteException(DuplicateTestsuiteException, JUnitException): 

141 """ 

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

143 

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

145 

146 .. hint:: 

147 

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

149 """ 

150 

151 

152@export 

153class DuplicateTestcaseException(DuplicateTestcaseException, JUnitException): 

154 """ 

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

156 

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

158 

159 .. hint:: 

160 

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

162 """ 

163 

164 

165@export 

166class JUnitReaderMode(Flag): 

167 Default = 0 #: Default behavior 

168 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented 

169 

170 

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

172TestcaseAggregateReturnType = Tuple[int, int, int] 

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

174 

175 

176@export 

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

178 """ 

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

180 

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

182 hierarchy. 

183 

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

185 child. |br| 

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

187 """ 

188 

189 _parent: Nullable["Testsuite"] 

190 _name: str 

191 

192 def __init__(self, name: str, parent: Nullable["Testsuite"] = None) -> 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: When parameter 'name' is None. 

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

200 :raises ValueError: When 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 to access the reference to the parent test entity. 

219 

220 :returns: 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 to access the test entity's name. 

230 

231 :returns: Name of the test entity. 

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 ) -> None: 

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 to access 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 :returns: 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 to access 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 :returns: 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 :returns: 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 :returns: 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 :returns: 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 :returns: 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 ) -> None: 

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 to access the class name of the test case. 

433 

434 :returns: 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 to access the status of the test case. 

450 

451 :returns: The test case's status. 

452 """ 

453 return self._status 

454 

455 @readonly 

456 def AssertionCount(self) -> int: 

457 """ 

458 Read-only property to return 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 :returns: Number of assertions. 

465 """ 

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

467 return 0 

468 return self._assertionCount 

469 

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

471 return self.__class__( 

472 self._name, 

473 self._duration, 

474 self._status, 

475 self._assertionCount 

476 ) 

477 

478 def Aggregate(self) -> None: 

479 if self._status is TestcaseStatus.Unknown: 

480 if self._assertionCount is None: 

481 self._status = TestcaseStatus.Passed 

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

483 self._status = TestcaseStatus.Weak 

484 else: 

485 self._status = TestcaseStatus.Failed 

486 

487 # TODO: check for setup errors 

488 # TODO: check for teardown errors 

489 

490 @classmethod 

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

492 """ 

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

494 

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

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

497 """ 

498 return cls( 

499 testcase._name, 

500 duration=testcase._testDuration, 

501 status= testcase._status, 

502 assertionCount=testcase._assertionCount 

503 ) 

504 

505 def ToTestcase(self) -> ut_Testcase: 

506 return ut_Testcase( 

507 self._name, 

508 testDuration=self._duration, 

509 status=self._status, 

510 assertionCount=self._assertionCount, 

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

512 passedAssertionCount=self._assertionCount 

513 ) 

514 

515 def ToTree(self) -> Node: 

516 node = Node(value=self._name) 

517 node["status"] = self._status 

518 node["assertionCount"] = self._assertionCount 

519 node["duration"] = self._duration 

520 

521 return node 

522 

523 def __str__(self) -> str: 

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

525 className = self.__class__.__name__ 

526 return ( 

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

528 ) 

529 

530 

531@export 

532class TestsuiteBase(BaseWithProperties): 

533 """ 

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

535 

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

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

538 test summary contains no test classes and test cases. 

539 """ 

540 

541 _startTime: Nullable[datetime] 

542 _status: TestsuiteStatus 

543 

544 _tests: int 

545 _skipped: int 

546 _errored: int 

547 _weak: int 

548 _failed: int 

549 _passed: int 

550 

551 def __init__( 

552 self, 

553 name: str, 

554 startTime: Nullable[datetime] = None, 

555 duration: Nullable[timedelta] = None, 

556 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

557 parent: Nullable["Testsuite"] = None 

558 ) -> None: 

559 """ 

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

561 

562 :param name: Name of the test entity. 

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

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

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

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

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

568 """ 

569 if parent is not None: 

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

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

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

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

574 raise ex 

575 

576 parent._testsuites[name] = self 

577 

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

579 

580 self._startTime = startTime 

581 self._status = status 

582 self._tests = 0 

583 self._skipped = 0 

584 self._errored = 0 

585 self._failed = 0 

586 self._passed = 0 

587 

588 @readonly 

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

590 """ 

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

592 

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

594 """ 

595 return self._startTime 

596 

597 @readonly 

598 def Status(self) -> TestsuiteStatus: 

599 """ 

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

601 

602 :returns: Status of the test entity. 

603 """ 

604 return self._status 

605 

606 @readonly 

607 @mustoverride 

608 def TestcaseCount(self) -> int: 

609 """ 

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

611 

612 :returns: Number of testcases. 

613 """ 

614 pass 

615 

616 @readonly 

617 def Tests(self) -> int: 

618 """ 

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

620 

621 :returns: Number of testcases. 

622 """ 

623 return self.TestcaseCount 

624 

625 @readonly 

626 def Skipped(self) -> int: 

627 """ 

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

629 

630 :returns: Number of skipped testcases. 

631 """ 

632 return self._skipped 

633 

634 @readonly 

635 def Errored(self) -> int: 

636 """ 

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

638 

639 :returns: Number of errored testcases. 

640 """ 

641 return self._errored 

642 

643 @readonly 

644 def Failed(self) -> int: 

645 """ 

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

647 

648 :returns: Number of failed testcases. 

649 """ 

650 return self._failed 

651 

652 @readonly 

653 def Passed(self) -> int: 

654 """ 

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

656 

657 :returns: Number of passed testcases. 

658 """ 

659 return self._passed 

660 

661 def Aggregate(self) -> TestsuiteAggregateReturnType: 

662 tests = 0 

663 skipped = 0 

664 errored = 0 

665 weak = 0 

666 failed = 0 

667 passed = 0 

668 

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

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

671 # tests += t 

672 # skipped += s 

673 # errored += e 

674 # weak += w 

675 # failed += f 

676 # passed += p 

677 

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

679 

680 @mustoverride 

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

682 pass 

683 

684 

685@export 

686class Testclass(Base): 

687 """ 

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

689 

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

691 """ 

692 

693 _testcases: Dict[str, "Testcase"] 

694 

695 def __init__( 

696 self, 

697 classname: str, 

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

699 parent: Nullable["Testsuite"] = None 

700 ) -> None: 

701 """ 

702 Initializes the fields of the test class. 

703 

704 :param classname: Classname of the test entity. 

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

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

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

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

709 """ 

710 if parent is not None: 

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

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

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

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

715 raise ex 

716 

717 parent._testclasses[classname] = self 

718 

719 super().__init__(classname, parent) 

720 

721 self._testcases = {} 

722 if testcases is not None: 

723 for testcase in testcases: 

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

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

726 

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

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

729 

730 testcase._parent = self 

731 self._testcases[testcase._name] = testcase 

732 

733 @readonly 

734 def Classname(self) -> str: 

735 """ 

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

737 

738 :returns: The test class' name. 

739 """ 

740 return self._name 

741 

742 @readonly 

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

744 """ 

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

746 

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

748 """ 

749 return self._testcases 

750 

751 @readonly 

752 def TestcaseCount(self) -> int: 

753 """ 

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

755 

756 :returns: Number of test cases. 

757 """ 

758 return len(self._testcases) 

759 

760 @readonly 

761 def AssertionCount(self) -> int: 

762 """ 

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

764 

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

766 """ 

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

768 

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

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

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

772 

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

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

775 

776 testcase._parent = self 

777 self._testcases[testcase._name] = testcase 

778 

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

780 for testcase in testcases: 

781 self.AddTestcase(testcase) 

782 

783 def ToTestsuite(self) -> ut_Testsuite: 

784 return ut_Testsuite( 

785 self._name, 

786 TestsuiteKind.Class, 

787 # startTime=self._startTime, 

788 # totalDuration=self._duration, 

789 # status=self._status, 

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

791 ) 

792 

793 def ToTree(self) -> Node: 

794 node = Node( 

795 value=self._name, 

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

797 ) 

798 

799 return node 

800 

801 def __str__(self) -> str: 

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

803 className = self.__class__.__name__ 

804 return ( 

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

806 ) 

807 

808 

809@export 

810class Testsuite(TestsuiteBase): 

811 """ 

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

813 

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

815 """ 

816 

817 _hostname: str 

818 _testclasses: Dict[str, "Testclass"] 

819 

820 def __init__( 

821 self, 

822 name: str, 

823 hostname: Nullable[str] = None, 

824 startTime: Nullable[datetime] = None, 

825 duration: Nullable[timedelta] = None, 

826 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

828 parent: Nullable["TestsuiteSummary"] = None 

829 ) -> None: 

830 """ 

831 Initializes the fields of a test suite. 

832 

833 :param name: Name of the test suite. 

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

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

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

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

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

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

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

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

842 """ 

843 if parent is not None: 

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

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

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

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

848 raise ex 

849 

850 parent._testsuites[name] = self 

851 

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

853 

854 self._hostname = hostname 

855 

856 self._testclasses = {} 

857 if testclasses is not None: 

858 for testclass in testclasses: 

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

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

861 

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

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

864 

865 testclass._parent = self 

866 self._testclasses[testclass._name] = testclass 

867 

868 @readonly 

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

870 """ 

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

872 

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

874 """ 

875 return self._hostname 

876 

877 @readonly 

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

879 """ 

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

881 

882 :returns: Dictionary of testclass names and testclasses. 

883 """ 

884 return self._testclasses 

885 

886 @readonly 

887 def TestclassCount(self) -> int: 

888 """ 

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

890 

891 :returns: Number of testclasses. 

892 """ 

893 return len(self._testclasses) 

894 

895 # @readonly 

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

897 # return self._classes 

898 

899 @readonly 

900 def TestcaseCount(self) -> int: 

901 """ 

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

903 

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

905 """ 

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

907 

908 @readonly 

909 def AssertionCount(self) -> int: 

910 """ 

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

912 

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

914 """ 

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

916 

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

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

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

920 

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

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

923 

924 testclass._parent = self 

925 self._testclasses[testclass._name] = testclass 

926 

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

928 for testcase in testclasses: 

929 self.AddTestclass(testcase) 

930 

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

932 # return self.Iterate(scheme) 

933 

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

935 return self.Iterate(scheme) 

936 

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

938 return self.__class__( 

939 self._name, 

940 self._hostname, 

941 self._startTime, 

942 self._duration, 

943 self._status 

944 ) 

945 

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

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

948 

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

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

951 _ = testcase.Aggregate() 

952 

953 status = testcase._status 

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

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

956 elif status is TestcaseStatus.Skipped: 

957 skipped += 1 

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

959 errored += 1 

960 elif status is TestcaseStatus.Passed: 

961 passed += 1 

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

963 failed += 1 

964 elif status is TestcaseStatus.Weak: 

965 weak += 1 

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

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

968 else: 

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

970 

971 self._tests = tests 

972 self._skipped = skipped 

973 self._errored = errored 

974 self._weak = weak 

975 self._failed = failed 

976 self._passed = passed 

977 

978 # FIXME: weak? 

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

980 self._status = TestsuiteStatus.Errored 

981 elif failed > 0: 

982 self._status = TestsuiteStatus.Failed 

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

984 self._status = TestsuiteStatus.Empty 

985 elif tests - skipped == passed: 

986 self._status = TestsuiteStatus.Passed 

987 elif tests == skipped: 

988 self._status = TestsuiteStatus.Skipped 

989 else: 

990 self._status = TestsuiteStatus.Unknown 

991 

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

993 

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

995 """ 

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

997 

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

999 

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

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

1002 """ 

1003 if IterationScheme.PreOrder in scheme: 

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

1005 yield self 

1006 

1007 if IterationScheme.IncludeTestcases in scheme: 

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

1009 yield testcase 

1010 

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

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

1013 

1014 if IterationScheme.PostOrder in scheme: 

1015 if IterationScheme.IncludeTestcases in scheme: 

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

1017 yield testcase 

1018 

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

1020 yield self 

1021 

1022 @classmethod 

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

1024 """ 

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

1026 

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

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

1029 """ 

1030 juTestsuite = cls( 

1031 testsuite._name, 

1032 startTime=testsuite._startTime, 

1033 duration=testsuite._totalDuration, 

1034 status= testsuite._status, 

1035 ) 

1036 

1037 juTestsuite._tests = testsuite._tests 

1038 juTestsuite._skipped = testsuite._skipped 

1039 juTestsuite._errored = testsuite._errored 

1040 juTestsuite._failed = testsuite._failed 

1041 juTestsuite._passed = testsuite._passed 

1042 

1043 for tc in testsuite.IterateTestcases(): 

1044 ts = tc._parent 

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

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

1047 

1048 classname = ts._name 

1049 ts = ts._parent 

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

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

1052 ts = ts._parent 

1053 

1054 if classname in juTestsuite._testclasses: 

1055 juClass = juTestsuite._testclasses[classname] 

1056 else: 

1057 juClass = Testclass(classname, parent=juTestsuite) 

1058 

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

1060 

1061 return juTestsuite 

1062 

1063 def ToTestsuite(self) -> ut_Testsuite: 

1064 testsuite = ut_Testsuite( 

1065 self._name, 

1066 TestsuiteKind.Logical, 

1067 startTime=self._startTime, 

1068 totalDuration=self._duration, 

1069 status=self._status, 

1070 ) 

1071 

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

1073 suite = testsuite 

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

1075 for element in classpath: 

1076 if element in suite._testsuites: 

1077 suite = suite._testsuites[element] 

1078 else: 

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

1080 

1081 suite._kind = TestsuiteKind.Class 

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

1083 suite._parent._kind = TestsuiteKind.Module 

1084 

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

1086 

1087 return testsuite 

1088 

1089 def ToTree(self) -> Node: 

1090 node = Node( 

1091 value=self._name, 

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

1093 ) 

1094 node["startTime"] = self._startTime 

1095 node["duration"] = self._duration 

1096 

1097 return node 

1098 

1099 def __str__(self) -> str: 

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

1101 className = self.__class__.__name__ 

1102 return ( 

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

1104 ) 

1105 

1106 

1107@export 

1108class TestsuiteSummary(TestsuiteBase): 

1109 _testsuites: Dict[str, Testsuite] 

1110 

1111 def __init__( 

1112 self, 

1113 name: str, 

1114 startTime: Nullable[datetime] = None, 

1115 duration: Nullable[timedelta] = None, 

1116 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1118 ) -> None: 

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

1120 

1121 self._testsuites = {} 

1122 if testsuites is not None: 

1123 for testsuite in testsuites: 

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

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

1126 

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

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

1129 

1130 testsuite._parent = self 

1131 self._testsuites[testsuite._name] = testsuite 

1132 

1133 @readonly 

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

1135 """ 

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

1137 

1138 :returns: Dictionary of testsuite names and testsuites. 

1139 """ 

1140 return self._testsuites 

1141 

1142 @readonly 

1143 def TestcaseCount(self) -> int: 

1144 """ 

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

1146 

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

1148 """ 

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

1150 

1151 @readonly 

1152 def TestsuiteCount(self) -> int: 

1153 """ 

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

1155 

1156 :returns: Number of testsuites. 

1157 """ 

1158 return len(self._testsuites) 

1159 

1160 @readonly 

1161 def AssertionCount(self) -> int: 

1162 """ 

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

1164 

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

1166 """ 

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

1168 

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

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

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

1172 

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

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

1175 

1176 testsuite._parent = self 

1177 self._testsuites[testsuite._name] = testsuite 

1178 

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

1180 for testsuite in testsuites: 

1181 self.AddTestsuite(testsuite) 

1182 

1183 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1185 

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

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

1188 tests += t 

1189 skipped += s 

1190 errored += e 

1191 weak += w 

1192 failed += f 

1193 passed += p 

1194 

1195 self._tests = tests 

1196 self._skipped = skipped 

1197 self._errored = errored 

1198 self._weak = weak 

1199 self._failed = failed 

1200 self._passed = passed 

1201 

1202 # FIXME: weak 

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

1204 self._status = TestsuiteStatus.Errored 

1205 elif failed > 0: 

1206 self._status = TestsuiteStatus.Failed 

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

1208 self._status = TestsuiteStatus.Empty 

1209 elif tests - skipped == passed: 

1210 self._status = TestsuiteStatus.Passed 

1211 elif tests == skipped: 

1212 self._status = TestsuiteStatus.Skipped 

1213 else: 

1214 self._status = TestsuiteStatus.Unknown 

1215 

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

1217 

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

1219 """ 

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

1221 

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

1223 

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

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

1226 """ 

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

1228 yield self 

1229 

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

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

1232 

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

1234 yield self 

1235 

1236 @classmethod 

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

1238 """ 

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

1240 

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

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

1243 """ 

1244 return cls( 

1245 testsuiteSummary._name, 

1246 startTime=testsuiteSummary._startTime, 

1247 duration=testsuiteSummary._totalDuration, 

1248 status=testsuiteSummary._status, 

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

1250 ) 

1251 

1252 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1253 """ 

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

1255 

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

1257 

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

1259 """ 

1260 return ut_TestsuiteSummary( 

1261 self._name, 

1262 startTime=self._startTime, 

1263 totalDuration=self._duration, 

1264 status=self._status, 

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

1266 ) 

1267 

1268 def ToTree(self) -> Node: 

1269 node = Node( 

1270 value=self._name, 

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

1272 ) 

1273 node["startTime"] = self._startTime 

1274 node["duration"] = self._duration 

1275 

1276 return node 

1277 

1278 def __str__(self) -> str: 

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

1280 className = self.__class__.__name__ 

1281 return ( 

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

1283 ) 

1284 

1285 

1286@export 

1287class Document(TestsuiteSummary, ut_Document): 

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

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

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

1291 

1292 _readerMode: JUnitReaderMode 

1293 _xmlDocument: Nullable[_ElementTree] 

1294 

1295 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default) -> None: 

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

1297 

1298 self._readerMode = readerMode 

1299 self._xmlDocument = None 

1300 

1301 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1302 

1303 @classmethod 

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

1305 doc = cls(xmlReportFile) 

1306 doc._name = testsuiteSummary._name 

1307 doc._startTime = testsuiteSummary._startTime 

1308 doc._duration = testsuiteSummary._totalDuration 

1309 doc._status = testsuiteSummary._status 

1310 doc._tests = testsuiteSummary._tests 

1311 doc._skipped = testsuiteSummary._skipped 

1312 doc._errored = testsuiteSummary._errored 

1313 doc._failed = testsuiteSummary._failed 

1314 doc._passed = testsuiteSummary._passed 

1315 

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

1317 

1318 return doc 

1319 

1320 def Analyze(self) -> None: 

1321 """ 

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

1323 schema. 

1324 

1325 .. hint:: 

1326 

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

1328 

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

1330 """ 

1331 xmlSchemaFile = "Any-JUnit.xsd" 

1332 self._Analyze(xmlSchemaFile) 

1333 

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

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

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

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

1338 

1339 startAnalysis = perf_counter_ns() 

1340 try: 

1341 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1342 except ToolingException as ex: 

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

1344 

1345 try: 

1346 schemaParser = XMLParser(ns_clean=True) 

1347 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1348 except XMLSyntaxError as ex: 

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

1350 

1351 try: 

1352 junitSchema = XMLSchema(schemaRoot) 

1353 except XMLSchemaParseError as ex: 

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

1355 

1356 try: 

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

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

1359 

1360 self._xmlDocument = junitDocument 

1361 except XMLSyntaxError as ex: 

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

1363 for logEntry in junitParser.error_log: 

1364 ex.add_note(str(logEntry)) 

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

1366 except Exception as ex: 

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

1368 

1369 endAnalysis = perf_counter_ns() 

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

1371 

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

1373 """ 

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

1375 

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

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

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

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

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

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

1382 """ 

1383 if path is None: 

1384 path = self._path 

1385 

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

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

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

1389 

1390 if regenerate: 

1391 self.Generate(overwrite=True) 

1392 

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

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

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

1396 raise ex 

1397 

1398 try: 

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

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

1401 except Exception as ex: 

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

1403 

1404 def Convert(self) -> None: 

1405 """ 

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

1407 

1408 This method converts the root element. 

1409 

1410 .. hint:: 

1411 

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

1413 

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

1415 """ 

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

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

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

1419 raise ex 

1420 

1421 startConversion = perf_counter_ns() 

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

1423 

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

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

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

1427 

1428 if False: # self._readerMode is JUnitReaderMode. 

1429 self._tests = self._ConvertTests(testsuitesNode) 

1430 self._skipped = self._ConvertSkipped(testsuitesNode) 

1431 self._errored = self._ConvertErrors(testsuitesNode) 

1432 self._failed = self._ConvertFailures(testsuitesNode) 

1433 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1434 

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

1436 self._ConvertTestsuite(self, rootNode) 

1437 

1438 if True: # self._readerMode is JUnitReaderMode. 

1439 self.Aggregate() 

1440 

1441 endConversation = perf_counter_ns() 

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

1443 

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

1445 """ 

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

1447 

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

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

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

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

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

1453 """ 

1454 if "name" in element.attrib: 

1455 return element.attrib["name"] 

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

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

1458 else: 

1459 return default 

1460 

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

1462 """ 

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

1464 

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

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

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

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

1469 """ 

1470 if "timestamp" in element.attrib: 

1471 timestamp = element.attrib["timestamp"] 

1472 return datetime.fromisoformat(timestamp) 

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

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

1475 else: 

1476 return None 

1477 

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

1479 """ 

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

1481 

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

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

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

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

1486 """ 

1487 if "time" in element.attrib: 

1488 time = element.attrib["time"] 

1489 return timedelta(seconds=float(time)) 

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

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

1492 else: 

1493 return None 

1494 

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

1496 """ 

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

1498 

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

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

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

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

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

1504 """ 

1505 if "hostname" in element.attrib: 

1506 return element.attrib["hostname"] 

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

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

1509 else: 

1510 return default 

1511 

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

1513 """ 

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

1515 

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

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

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

1519 """ 

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

1521 return element.attrib["classname"] 

1522 else: 

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

1524 

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

1526 """ 

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

1528 

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

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

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

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

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

1534 """ 

1535 if "tests" in element.attrib: 

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

1537 elif not optional: 

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

1539 else: 

1540 return default 

1541 

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

1543 """ 

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

1545 

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

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

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

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

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

1551 """ 

1552 if "skipped" in element.attrib: 

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

1554 elif not optional: 

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

1556 else: 

1557 return default 

1558 

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

1560 """ 

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

1562 

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

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

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

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

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

1568 """ 

1569 if "errors" in element.attrib: 

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

1571 elif not optional: 

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

1573 else: 

1574 return default 

1575 

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

1577 """ 

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

1579 

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

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

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

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

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

1585 """ 

1586 if "failures" in element.attrib: 

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

1588 elif not optional: 

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

1590 else: 

1591 return default 

1592 

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

1594 """ 

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

1596 

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

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

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

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

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

1602 """ 

1603 if "assertions" in element.attrib: 

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

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

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

1607 else: 

1608 return default 

1609 

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

1611 """ 

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

1613 

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

1615 

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

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

1618 """ 

1619 newTestsuite = self._TESTSUITE( 

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

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

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

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

1624 parent=parent 

1625 ) 

1626 

1627 if False: # self._readerMode is JUnitReaderMode. 

1628 self._tests = self._ConvertTests(testsuitesNode) 

1629 self._skipped = self._ConvertSkipped(testsuitesNode) 

1630 self._errored = self._ConvertErrors(testsuitesNode) 

1631 self._failed = self._ConvertFailures(testsuitesNode) 

1632 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1633 

1634 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1635 

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

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

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

1639 # self._ConvertTestsuite(newTestsuite, node) 

1640 # el 

1641 if node.tag == "testcase": 

1642 self._ConvertTestcase(newTestsuite, node) 

1643 

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

1645 """ 

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

1647 

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

1649 

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

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

1652 """ 

1653 className = self._ConvertClassname(testcaseNode) 

1654 testclass = self._FindOrCreateTestclass(parent, className) 

1655 

1656 newTestcase = self._TESTCASE( 

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

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

1659 assertionCount=self._ConvertAssertions(testcaseNode), 

1660 parent=testclass 

1661 ) 

1662 

1663 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1664 

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

1666 if className in parent._testclasses: 

1667 return parent._testclasses[className] 

1668 else: 

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

1670 

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

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

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

1674 pass 

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

1676 if node.tag == "skipped": 

1677 newTestcase._status = TestcaseStatus.Skipped 

1678 elif node.tag == "failure": 

1679 newTestcase._status = TestcaseStatus.Failed 

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

1681 newTestcase._status = TestcaseStatus.Errored 

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

1683 pass 

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

1685 pass 

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

1687 pass 

1688 else: 

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

1690 else: 

1691 pass 

1692 

1693 if newTestcase._status is TestcaseStatus.Unknown: 

1694 newTestcase._status = TestcaseStatus.Passed 

1695 

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

1697 """ 

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

1699 

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

1701 

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

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

1704 """ 

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

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

1707 

1708 rootElement = Element("testsuites") 

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

1710 if self._startTime is not None: 

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

1712 if self._duration is not None: 

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

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

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

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

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

1718 # if self._assertionCount is not None: 

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

1720 

1721 self._xmlDocument = ElementTree(rootElement) 

1722 

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

1724 self._GenerateTestsuite(testsuite, rootElement) 

1725 

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

1727 """ 

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

1729 

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

1731 

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

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

1734 """ 

1735 testsuiteElement = SubElement(parentElement, "testsuite") 

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

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

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

1739 if testsuite._duration is not None: 

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

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

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

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

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

1745 # if testsuite._assertionCount is not None: 

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

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

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

1749 

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

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

1752 self._GenerateTestcase(tc, testsuiteElement) 

1753 

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

1755 """ 

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

1757 

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

1759 

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

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

1762 """ 

1763 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1769 if testcase._assertionCount is not None: 

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

1771 

1772 if testcase._status is TestcaseStatus.Passed: 

1773 pass 

1774 elif testcase._status is TestcaseStatus.Failed: 

1775 failureElement = SubElement(testcaseElement, "failure") 

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

1777 skippedElement = SubElement(testcaseElement, "skipped") 

1778 else: 

1779 errorElement = SubElement(testcaseElement, "error") 

1780 

1781 def __str__(self) -> str: 

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

1783 className = self.__class__.__name__ 

1784 return ( 

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

1786 )