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

714 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-08-29 03:43 +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 time import perf_counter_ns 

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

98 

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

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

101from pyTooling.Common import getFullyQualifiedName, getResourceFile 

102from pyTooling.Decorators import export, readonly 

103from pyTooling.Exceptions import ToolingException 

104from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod 

105from pyTooling.Tree import Node 

106 

107from pyEDAA.Reports import Resources 

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

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

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

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

112 

113 

114@export 

115class JUnitException: 

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

117 

118 

119@export 

120class UnittestException(UnittestException, JUnitException): 

121 pass 

122 

123 

124@export 

125class AlreadyInHierarchyException(AlreadyInHierarchyException, JUnitException): 

126 """ 

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

128 

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

130 hierarchy should occur only once in the hierarchy. 

131 

132 .. hint:: 

133 

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

135 """ 

136 

137 

138@export 

139class DuplicateTestsuiteException(DuplicateTestsuiteException, JUnitException): 

140 """ 

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

142 

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

144 

145 .. hint:: 

146 

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

148 """ 

149 

150 

151@export 

152class DuplicateTestcaseException(DuplicateTestcaseException, JUnitException): 

153 """ 

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

155 

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

157 

158 .. hint:: 

159 

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

161 """ 

162 

163 

164@export 

165class JUnitReaderMode(Flag): 

166 Default = 0 #: Default behavior 

167 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented 

168 

169 

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

171TestcaseAggregateReturnType = Tuple[int, int, int] 

172TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int] 

173 

174 

175@export 

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

177 """ 

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

179 

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

181 hierarchy. 

182 

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

184 child. |br| 

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

186 """ 

187 

188 _parent: Nullable["Testsuite"] 

189 _name: str 

190 

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

192 """ 

193 Initializes the fields of the base-class. 

194 

195 :param name: Name of the test entity. 

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

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

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

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

200 """ 

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

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

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

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

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

206 raise ex 

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

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

209 

210 self._parent = parent 

211 self._name = name 

212 

213 @readonly 

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

215 """ 

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

217 

218 :returns: Reference to the parent entity. 

219 """ 

220 return self._parent 

221 

222 # QUESTION: allow Parent as setter? 

223 

224 @readonly 

225 def Name(self) -> str: 

226 """ 

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

228 

229 :returns: Name of the test entity. 

230 """ 

231 return self._name 

232 

233 

234@export 

235class BaseWithProperties(Base): 

236 """ 

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

238 

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

240 

241 Every test entity offers an internal dictionary for properties. 

242 """ 

243 

244 _duration: Nullable[timedelta] 

245 _assertionCount: Nullable[int] 

246 _properties: Dict[str, Any] 

247 

248 def __init__( 

249 self, 

250 name: str, 

251 duration: Nullable[timedelta] = None, 

252 assertionCount: Nullable[int] = None, 

253 parent: Nullable["Testsuite"] = None 

254 ) -> None: 

255 """ 

256 Initializes the fields of the base-class. 

257 

258 :param name: Name of the test entity. 

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

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

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

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

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

264 """ 

265 super().__init__(name, parent) 

266 

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

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

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

270 raise ex 

271 

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

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

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

275 raise ex 

276 

277 self._duration = duration 

278 self._assertionCount = assertionCount 

279 

280 self._properties = {} 

281 

282 @readonly 

283 def Duration(self) -> timedelta: 

284 """ 

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

286 

287 .. note:: 

288 

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

290 

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

292 """ 

293 return self._duration 

294 

295 @readonly 

296 @abstractmethod 

297 def AssertionCount(self) -> int: 

298 """ 

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

300 

301 .. note:: 

302 

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

304 

305 :returns: Number of assertions. 

306 """ 

307 

308 def __len__(self) -> int: 

309 """ 

310 Returns the number of annotated properties. 

311 

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

313 

314 :returns: Number of annotated properties. 

315 """ 

316 return len(self._properties) 

317 

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

319 """ 

320 Access a property by name. 

321 

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

323 

324 :param name: Name if the property. 

325 :returns: Value of the accessed property. 

326 """ 

327 return self._properties[name] 

328 

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

330 """ 

331 Set the value of a property by name. 

332 

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

334 

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

336 

337 :param name: Name of the property. 

338 :param value: Value of the property. 

339 """ 

340 self._properties[name] = value 

341 

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

343 """ 

344 Delete a property by name. 

345 

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

347 

348 :param name: Name if the property. 

349 """ 

350 del self._properties[name] 

351 

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

353 """ 

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

355 

356 Syntax: :pycode:`name in obj` 

357 

358 :param name: Name of the property. 

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

360 """ 

361 return name in self._properties 

362 

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

364 """ 

365 Iterate all annotated properties. 

366 

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

368 

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

370 """ 

371 yield from self._properties.items() 

372 

373 

374@export 

375class Testcase(BaseWithProperties): 

376 """ 

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

378 

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

380 of the hierarchy is a test summary. 

381 

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

383 """ 

384 

385 _status: TestcaseStatus 

386 

387 def __init__( 

388 self, 

389 name: str, 

390 duration: Nullable[timedelta] = None, 

391 status: TestcaseStatus = TestcaseStatus.Unknown, 

392 assertionCount: Nullable[int] = None, 

393 parent: Nullable["Testclass"] = None 

394 ) -> None: 

395 """ 

396 Initializes the fields of a test case. 

397 

398 :param name: Name of the test entity. 

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

400 :param status: Status of the test case. 

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

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

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

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

405 """ 

406 if parent is not None: 

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

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

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

410 raise ex 

411 

412 parent._testcases[name] = self 

413 

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

415 

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

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

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

419 raise ex 

420 

421 self._status = status 

422 

423 @readonly 

424 def Classname(self) -> str: 

425 """ 

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

427 

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

429 

430 .. note:: 

431 

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

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

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

435 """ 

436 if self._parent is None: 

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

438 return self._parent._name 

439 

440 @readonly 

441 def Status(self) -> TestcaseStatus: 

442 """ 

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

444 

445 :returns: The test case's status. 

446 """ 

447 return self._status 

448 

449 @readonly 

450 def AssertionCount(self) -> int: 

451 """ 

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

453 

454 .. note:: 

455 

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

457 

458 :returns: Number of assertions. 

459 """ 

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

461 return 0 

462 return self._assertionCount 

463 

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

465 return self.__class__( 

466 self._name, 

467 self._duration, 

468 self._status, 

469 self._assertionCount 

470 ) 

471 

472 def Aggregate(self) -> None: 

473 if self._status is TestcaseStatus.Unknown: 

474 if self._assertionCount is None: 

475 self._status = TestcaseStatus.Passed 

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

477 self._status = TestcaseStatus.Weak 

478 else: 

479 self._status = TestcaseStatus.Failed 

480 

481 # TODO: check for setup errors 

482 # TODO: check for teardown errors 

483 

484 @classmethod 

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

486 """ 

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

488 

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

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

491 """ 

492 return cls( 

493 testcase._name, 

494 duration=testcase._testDuration, 

495 status= testcase._status, 

496 assertionCount=testcase._assertionCount 

497 ) 

498 

499 def ToTestcase(self) -> ut_Testcase: 

500 return ut_Testcase( 

501 self._name, 

502 testDuration=self._duration, 

503 status=self._status, 

504 assertionCount=self._assertionCount, 

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

506 passedAssertionCount=self._assertionCount 

507 ) 

508 

509 def ToTree(self) -> Node: 

510 node = Node(value=self._name) 

511 node["status"] = self._status 

512 node["assertionCount"] = self._assertionCount 

513 node["duration"] = self._duration 

514 

515 return node 

516 

517 def __str__(self) -> str: 

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

519 className = self.__class__.__name__ 

520 return ( 

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

522 ) 

523 

524 

525@export 

526class TestsuiteBase(BaseWithProperties): 

527 """ 

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

529 

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

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

532 test summary contains no test classes and test cases. 

533 """ 

534 

535 _startTime: Nullable[datetime] 

536 _status: TestsuiteStatus 

537 

538 _tests: int 

539 _skipped: int 

540 _errored: int 

541 _weak: int 

542 _failed: int 

543 _passed: int 

544 

545 def __init__( 

546 self, 

547 name: str, 

548 startTime: Nullable[datetime] = None, 

549 duration: Nullable[timedelta] = None, 

550 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

551 parent: Nullable["Testsuite"] = None 

552 ) -> None: 

553 """ 

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

555 

556 :param name: Name of the test entity. 

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

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

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

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

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

562 """ 

563 if parent is not None: 

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

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

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

567 raise ex 

568 

569 parent._testsuites[name] = self 

570 

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

572 

573 self._startTime = startTime 

574 self._status = status 

575 self._tests = 0 

576 self._skipped = 0 

577 self._errored = 0 

578 self._failed = 0 

579 self._passed = 0 

580 

581 @readonly 

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

583 """ 

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

585 

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

587 """ 

588 return self._startTime 

589 

590 @readonly 

591 def Status(self) -> TestsuiteStatus: 

592 """ 

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

594 

595 :returns: Status of the test entity. 

596 """ 

597 return self._status 

598 

599 @readonly 

600 @mustoverride 

601 def TestcaseCount(self) -> int: 

602 """ 

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

604 

605 :returns: Number of testcases. 

606 """ 

607 pass 

608 

609 @readonly 

610 def Tests(self) -> int: 

611 """ 

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

613 

614 :returns: Number of testcases. 

615 """ 

616 return self.TestcaseCount 

617 

618 @readonly 

619 def Skipped(self) -> int: 

620 """ 

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

622 

623 :returns: Number of skipped testcases. 

624 """ 

625 return self._skipped 

626 

627 @readonly 

628 def Errored(self) -> int: 

629 """ 

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

631 

632 :returns: Number of errored testcases. 

633 """ 

634 return self._errored 

635 

636 @readonly 

637 def Failed(self) -> int: 

638 """ 

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

640 

641 :returns: Number of failed testcases. 

642 """ 

643 return self._failed 

644 

645 @readonly 

646 def Passed(self) -> int: 

647 """ 

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

649 

650 :returns: Number of passed testcases. 

651 """ 

652 return self._passed 

653 

654 def Aggregate(self) -> TestsuiteAggregateReturnType: 

655 tests = 0 

656 skipped = 0 

657 errored = 0 

658 weak = 0 

659 failed = 0 

660 passed = 0 

661 

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

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

664 # tests += t 

665 # skipped += s 

666 # errored += e 

667 # weak += w 

668 # failed += f 

669 # passed += p 

670 

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

672 

673 @mustoverride 

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

675 pass 

676 

677 

678@export 

679class Testclass(Base): 

680 """ 

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

682 

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

684 """ 

685 

686 _testcases: Dict[str, "Testcase"] 

687 

688 def __init__( 

689 self, 

690 classname: str, 

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

692 parent: Nullable["Testsuite"] = None 

693 ) -> None: 

694 """ 

695 Initializes the fields of the test class. 

696 

697 :param classname: Classname of the test entity. 

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

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

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

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

702 """ 

703 if parent is not None: 

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

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

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

707 raise ex 

708 

709 parent._testclasses[classname] = self 

710 

711 super().__init__(classname, parent) 

712 

713 self._testcases = {} 

714 if testcases is not None: 

715 for testcase in testcases: 

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

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

718 

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

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

721 

722 testcase._parent = self 

723 self._testcases[testcase._name] = testcase 

724 

725 @readonly 

726 def Classname(self) -> str: 

727 """ 

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

729 

730 :returns: The test class' name. 

731 """ 

732 return self._name 

733 

734 @readonly 

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

736 """ 

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

738 

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

740 """ 

741 return self._testcases 

742 

743 @readonly 

744 def TestcaseCount(self) -> int: 

745 """ 

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

747 

748 :returns: Number of test cases. 

749 """ 

750 return len(self._testcases) 

751 

752 @readonly 

753 def AssertionCount(self) -> int: 

754 """ 

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

756 

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

758 """ 

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

760 

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

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

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

764 

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

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

767 

768 testcase._parent = self 

769 self._testcases[testcase._name] = testcase 

770 

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

772 for testcase in testcases: 

773 self.AddTestcase(testcase) 

774 

775 def ToTestsuite(self) -> ut_Testsuite: 

776 return ut_Testsuite( 

777 self._name, 

778 TestsuiteKind.Class, 

779 # startTime=self._startTime, 

780 # totalDuration=self._duration, 

781 # status=self._status, 

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

783 ) 

784 

785 def ToTree(self) -> Node: 

786 node = Node( 

787 value=self._name, 

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

789 ) 

790 

791 return node 

792 

793 def __str__(self) -> str: 

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

795 className = self.__class__.__name__ 

796 return ( 

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

798 ) 

799 

800 

801@export 

802class Testsuite(TestsuiteBase): 

803 """ 

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

805 

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

807 """ 

808 

809 _hostname: str 

810 _testclasses: Dict[str, "Testclass"] 

811 

812 def __init__( 

813 self, 

814 name: str, 

815 hostname: Nullable[str] = None, 

816 startTime: Nullable[datetime] = None, 

817 duration: Nullable[timedelta] = None, 

818 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

820 parent: Nullable["TestsuiteSummary"] = None 

821 ) -> None: 

822 """ 

823 Initializes the fields of a test suite. 

824 

825 :param name: Name of the test suite. 

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

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

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

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

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

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

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

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

834 """ 

835 if parent is not None: 

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

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

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

839 raise ex 

840 

841 parent._testsuites[name] = self 

842 

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

844 

845 self._hostname = hostname 

846 

847 self._testclasses = {} 

848 if testclasses is not None: 

849 for testclass in testclasses: 

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

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

852 

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

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

855 

856 testclass._parent = self 

857 self._testclasses[testclass._name] = testclass 

858 

859 @readonly 

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

861 """ 

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

863 

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

865 """ 

866 return self._hostname 

867 

868 @readonly 

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

870 """ 

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

872 

873 :returns: Dictionary of testclass names and testclasses. 

874 """ 

875 return self._testclasses 

876 

877 @readonly 

878 def TestclassCount(self) -> int: 

879 """ 

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

881 

882 :returns: Number of testclasses. 

883 """ 

884 return len(self._testclasses) 

885 

886 # @readonly 

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

888 # return self._classes 

889 

890 @readonly 

891 def TestcaseCount(self) -> int: 

892 """ 

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

894 

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

896 """ 

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

898 

899 @readonly 

900 def AssertionCount(self) -> int: 

901 """ 

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

903 

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

905 """ 

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

907 

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

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

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

911 

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

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

914 

915 testclass._parent = self 

916 self._testclasses[testclass._name] = testclass 

917 

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

919 for testcase in testclasses: 

920 self.AddTestclass(testcase) 

921 

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

923 # return self.Iterate(scheme) 

924 

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

926 return self.Iterate(scheme) 

927 

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

929 return self.__class__( 

930 self._name, 

931 self._hostname, 

932 self._startTime, 

933 self._duration, 

934 self._status 

935 ) 

936 

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

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

939 

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

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

942 _ = testcase.Aggregate() 

943 

944 status = testcase._status 

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

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

947 elif status is TestcaseStatus.Skipped: 

948 skipped += 1 

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

950 errored += 1 

951 elif status is TestcaseStatus.Passed: 

952 passed += 1 

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

954 failed += 1 

955 elif status is TestcaseStatus.Weak: 

956 weak += 1 

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

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

959 else: 

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

961 

962 self._tests = tests 

963 self._skipped = skipped 

964 self._errored = errored 

965 self._weak = weak 

966 self._failed = failed 

967 self._passed = passed 

968 

969 # FIXME: weak? 

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

971 self._status = TestsuiteStatus.Errored 

972 elif failed > 0: 

973 self._status = TestsuiteStatus.Failed 

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

975 self._status = TestsuiteStatus.Empty 

976 elif tests - skipped == passed: 

977 self._status = TestsuiteStatus.Passed 

978 elif tests == skipped: 

979 self._status = TestsuiteStatus.Skipped 

980 else: 

981 self._status = TestsuiteStatus.Unknown 

982 

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

984 

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

986 """ 

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

988 

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

990 

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

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

993 """ 

994 if IterationScheme.PreOrder in scheme: 

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

996 yield self 

997 

998 if IterationScheme.IncludeTestcases in scheme: 

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

1000 yield testcase 

1001 

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

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

1004 

1005 if IterationScheme.PostOrder in scheme: 

1006 if IterationScheme.IncludeTestcases in scheme: 

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

1008 yield testcase 

1009 

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

1011 yield self 

1012 

1013 @classmethod 

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

1015 """ 

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

1017 

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

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

1020 """ 

1021 juTestsuite = cls( 

1022 testsuite._name, 

1023 hostname=testsuite._hostname, 

1024 startTime=testsuite._startTime, 

1025 duration=testsuite._totalDuration, 

1026 status= testsuite._status, 

1027 ) 

1028 

1029 juTestsuite._tests = testsuite._tests 

1030 juTestsuite._skipped = testsuite._skipped 

1031 juTestsuite._errored = testsuite._errored 

1032 juTestsuite._failed = testsuite._failed 

1033 juTestsuite._passed = testsuite._passed 

1034 

1035 for tc in testsuite.IterateTestcases(): 

1036 ts = tc._parent 

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

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

1039 

1040 classname = ts._name 

1041 ts = ts._parent 

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

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

1044 ts = ts._parent 

1045 

1046 if classname in juTestsuite._testclasses: 

1047 juClass = juTestsuite._testclasses[classname] 

1048 else: 

1049 juClass = Testclass(classname, parent=juTestsuite) 

1050 

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

1052 

1053 return juTestsuite 

1054 

1055 def ToTestsuite(self) -> ut_Testsuite: 

1056 testsuite = ut_Testsuite( 

1057 self._name, 

1058 TestsuiteKind.Logical, 

1059 self._hostname, 

1060 startTime=self._startTime, 

1061 totalDuration=self._duration, 

1062 status=self._status, 

1063 ) 

1064 

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

1066 suite = testsuite 

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

1068 for element in classpath: 

1069 if element in suite._testsuites: 

1070 suite = suite._testsuites[element] 

1071 else: 

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

1073 

1074 suite._kind = TestsuiteKind.Class 

1075 if suite._parent is not testsuite: 

1076 suite._parent._kind = TestsuiteKind.Module 

1077 

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

1079 

1080 return testsuite 

1081 

1082 def ToTree(self) -> Node: 

1083 node = Node( 

1084 value=self._name, 

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

1086 ) 

1087 node["startTime"] = self._startTime 

1088 node["duration"] = self._duration 

1089 

1090 return node 

1091 

1092 def __str__(self) -> str: 

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

1094 className = self.__class__.__name__ 

1095 return ( 

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

1097 ) 

1098 

1099 

1100@export 

1101class TestsuiteSummary(TestsuiteBase): 

1102 _testsuites: Dict[str, Testsuite] 

1103 

1104 def __init__( 

1105 self, 

1106 name: str, 

1107 startTime: Nullable[datetime] = None, 

1108 duration: Nullable[timedelta] = None, 

1109 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1111 ) -> None: 

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

1113 

1114 self._testsuites = {} 

1115 if testsuites is not None: 

1116 for testsuite in testsuites: 

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

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

1119 

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

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

1122 

1123 testsuite._parent = self 

1124 self._testsuites[testsuite._name] = testsuite 

1125 

1126 @readonly 

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

1128 """ 

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

1130 

1131 :returns: Dictionary of testsuite names and testsuites. 

1132 """ 

1133 return self._testsuites 

1134 

1135 @readonly 

1136 def TestcaseCount(self) -> int: 

1137 """ 

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

1139 

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

1141 """ 

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

1143 

1144 @readonly 

1145 def TestsuiteCount(self) -> int: 

1146 """ 

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

1148 

1149 :returns: Number of testsuites. 

1150 """ 

1151 return len(self._testsuites) 

1152 

1153 @readonly 

1154 def AssertionCount(self) -> int: 

1155 """ 

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

1157 

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

1159 """ 

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

1161 

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

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

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

1165 

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

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

1168 

1169 testsuite._parent = self 

1170 self._testsuites[testsuite._name] = testsuite 

1171 

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

1173 for testsuite in testsuites: 

1174 self.AddTestsuite(testsuite) 

1175 

1176 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1178 

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

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

1181 tests += t 

1182 skipped += s 

1183 errored += e 

1184 weak += w 

1185 failed += f 

1186 passed += p 

1187 

1188 self._tests = tests 

1189 self._skipped = skipped 

1190 self._errored = errored 

1191 self._weak = weak 

1192 self._failed = failed 

1193 self._passed = passed 

1194 

1195 # FIXME: weak 

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

1197 self._status = TestsuiteStatus.Errored 

1198 elif failed > 0: 

1199 self._status = TestsuiteStatus.Failed 

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

1201 self._status = TestsuiteStatus.Empty 

1202 elif tests - skipped == passed: 

1203 self._status = TestsuiteStatus.Passed 

1204 elif tests == skipped: 

1205 self._status = TestsuiteStatus.Skipped 

1206 else: 

1207 self._status = TestsuiteStatus.Unknown 

1208 

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

1210 

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

1212 """ 

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

1214 

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

1216 

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

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

1219 """ 

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

1221 yield self 

1222 

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

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

1225 

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

1227 yield self 

1228 

1229 @classmethod 

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

1231 """ 

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

1233 

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

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

1236 """ 

1237 return cls( 

1238 testsuiteSummary._name, 

1239 startTime=testsuiteSummary._startTime, 

1240 duration=testsuiteSummary._totalDuration, 

1241 status=testsuiteSummary._status, 

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

1243 ) 

1244 

1245 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1246 """ 

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

1248 

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

1250 

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

1252 """ 

1253 return ut_TestsuiteSummary( 

1254 self._name, 

1255 startTime=self._startTime, 

1256 totalDuration=self._duration, 

1257 status=self._status, 

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

1259 ) 

1260 

1261 def ToTree(self) -> Node: 

1262 node = Node( 

1263 value=self._name, 

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

1265 ) 

1266 node["startTime"] = self._startTime 

1267 node["duration"] = self._duration 

1268 

1269 return node 

1270 

1271 def __str__(self) -> str: 

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

1273 className = self.__class__.__name__ 

1274 return ( 

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

1276 ) 

1277 

1278 

1279@export 

1280class Document(TestsuiteSummary, ut_Document): 

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

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

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

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

1285 

1286 _readerMode: JUnitReaderMode 

1287 _xmlDocument: Nullable[_ElementTree] 

1288 

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

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

1291 

1292 self._readerMode = readerMode 

1293 self._xmlDocument = None 

1294 

1295 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1296 

1297 @classmethod 

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

1299 doc = cls(xmlReportFile) 

1300 doc._name = testsuiteSummary._name 

1301 doc._startTime = testsuiteSummary._startTime 

1302 doc._duration = testsuiteSummary._totalDuration 

1303 doc._status = testsuiteSummary._status 

1304 doc._tests = testsuiteSummary._tests 

1305 doc._skipped = testsuiteSummary._skipped 

1306 doc._errored = testsuiteSummary._errored 

1307 doc._failed = testsuiteSummary._failed 

1308 doc._passed = testsuiteSummary._passed 

1309 

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

1311 

1312 return doc 

1313 

1314 def Analyze(self) -> None: 

1315 """ 

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

1317 schema. 

1318 

1319 .. hint:: 

1320 

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

1322 

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

1324 """ 

1325 xmlSchemaFile = "Any-JUnit.xsd" 

1326 self._Analyze(xmlSchemaFile) 

1327 

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

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

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

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

1332 

1333 startAnalysis = perf_counter_ns() 

1334 try: 

1335 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1336 except ToolingException as ex: 

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

1338 

1339 try: 

1340 schemaParser = XMLParser(ns_clean=True) 

1341 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1342 except XMLSyntaxError as ex: 

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

1344 

1345 try: 

1346 junitSchema = XMLSchema(schemaRoot) 

1347 except XMLSchemaParseError as ex: 

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

1349 

1350 try: 

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

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

1353 

1354 self._xmlDocument = junitDocument 

1355 except XMLSyntaxError as ex: 

1356 for logEntry in junitParser.error_log: 

1357 ex.add_note(str(logEntry)) 

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

1359 except Exception as ex: 

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

1361 

1362 endAnalysis = perf_counter_ns() 

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

1364 

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

1366 """ 

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

1368 

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

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

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

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

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

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

1375 """ 

1376 if path is None: 

1377 path = self._path 

1378 

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

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

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

1382 

1383 if regenerate: 

1384 self.Generate(overwrite=True) 

1385 

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

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

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

1389 raise ex 

1390 

1391 try: 

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

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

1394 except Exception as ex: 

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

1396 

1397 def Convert(self) -> None: 

1398 """ 

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

1400 

1401 This method converts the root element. 

1402 

1403 .. hint:: 

1404 

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

1406 

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

1408 """ 

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

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

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

1412 raise ex 

1413 

1414 startConversion = perf_counter_ns() 

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

1416 

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

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

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

1420 

1421 if False: # self._readerMode is JUnitReaderMode. 

1422 self._tests = self._ConvertTests(testsuitesNode) 

1423 self._skipped = self._ConvertSkipped(testsuitesNode) 

1424 self._errored = self._ConvertErrors(testsuitesNode) 

1425 self._failed = self._ConvertFailures(testsuitesNode) 

1426 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1427 

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

1429 self._ConvertTestsuite(self, rootNode) 

1430 

1431 if True: # self._readerMode is JUnitReaderMode. 

1432 self.Aggregate() 

1433 

1434 endConversation = perf_counter_ns() 

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

1436 

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

1438 """ 

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

1440 

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

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

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

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

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

1446 """ 

1447 if "name" in element.attrib: 

1448 return element.attrib["name"] 

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

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

1451 else: 

1452 return default 

1453 

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

1455 """ 

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

1457 

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

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

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

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

1462 """ 

1463 if "timestamp" in element.attrib: 

1464 timestamp = element.attrib["timestamp"] 

1465 return datetime.fromisoformat(timestamp) 

1466 elif not optional: 

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

1468 else: 

1469 return None 

1470 

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

1472 """ 

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

1474 

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

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

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

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

1479 """ 

1480 if "time" in element.attrib: 

1481 time = element.attrib["time"] 

1482 return timedelta(seconds=float(time)) 

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

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

1485 else: 

1486 return None 

1487 

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

1489 """ 

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

1491 

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

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

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

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

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

1497 """ 

1498 if "hostname" in element.attrib: 

1499 return element.attrib["hostname"] 

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

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

1502 else: 

1503 return default 

1504 

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

1506 """ 

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

1508 

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

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

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

1512 """ 

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

1514 return element.attrib["classname"] 

1515 else: 

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

1517 

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

1519 """ 

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

1521 

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

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

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

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

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

1527 """ 

1528 if "tests" in element.attrib: 

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

1530 elif not optional: 

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

1532 else: 

1533 return default 

1534 

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

1536 """ 

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

1538 

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

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

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

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

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

1544 """ 

1545 if "skipped" in element.attrib: 

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

1547 elif not optional: 

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

1549 else: 

1550 return default 

1551 

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

1553 """ 

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

1555 

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

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

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

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

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

1561 """ 

1562 if "errors" in element.attrib: 

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

1564 elif not optional: 

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

1566 else: 

1567 return default 

1568 

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

1570 """ 

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

1572 

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

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

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

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

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

1578 """ 

1579 if "failures" in element.attrib: 

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

1581 elif not optional: 

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

1583 else: 

1584 return default 

1585 

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

1587 """ 

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

1589 

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

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

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

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

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

1595 """ 

1596 if "assertions" in element.attrib: 

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

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

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

1600 else: 

1601 return default 

1602 

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

1604 """ 

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

1606 

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

1608 

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

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

1611 """ 

1612 newTestsuite = self._TESTSUITE( 

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

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

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

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

1617 parent=parent 

1618 ) 

1619 

1620 if False: # self._readerMode is JUnitReaderMode. 

1621 self._tests = self._ConvertTests(testsuitesNode) 

1622 self._skipped = self._ConvertSkipped(testsuitesNode) 

1623 self._errored = self._ConvertErrors(testsuitesNode) 

1624 self._failed = self._ConvertFailures(testsuitesNode) 

1625 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1626 

1627 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1628 

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

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

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

1632 # self._ConvertTestsuite(newTestsuite, node) 

1633 # el 

1634 if node.tag == "testcase": 

1635 self._ConvertTestcase(newTestsuite, node) 

1636 

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

1638 """ 

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

1640 

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

1642 

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

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

1645 """ 

1646 className = self._ConvertClassname(testcaseNode) 

1647 testclass = self._FindOrCreateTestclass(parent, className) 

1648 

1649 newTestcase = self._TESTCASE( 

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

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

1652 assertionCount=self._ConvertAssertions(testcaseNode), 

1653 parent=testclass 

1654 ) 

1655 

1656 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1657 

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

1659 if className in parent._testclasses: 

1660 return parent._testclasses[className] 

1661 else: 

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

1663 

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

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

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

1667 pass 

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

1669 if node.tag == "skipped": 

1670 newTestcase._status = TestcaseStatus.Skipped 

1671 elif node.tag == "failure": 

1672 newTestcase._status = TestcaseStatus.Failed 

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

1674 newTestcase._status = TestcaseStatus.Errored 

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

1676 pass 

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

1678 pass 

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

1680 pass 

1681 else: 

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

1683 else: 

1684 pass 

1685 

1686 if newTestcase._status is TestcaseStatus.Unknown: 

1687 newTestcase._status = TestcaseStatus.Passed 

1688 

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

1690 """ 

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

1692 

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

1694 

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

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

1697 """ 

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

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

1700 

1701 rootElement = Element("testsuites") 

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

1703 if self._startTime is not None: 

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

1705 if self._duration is not None: 

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

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

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

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

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

1711 # if self._assertionCount is not None: 

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

1713 

1714 self._xmlDocument = ElementTree(rootElement) 

1715 

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

1717 self._GenerateTestsuite(testsuite, rootElement) 

1718 

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

1720 """ 

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

1722 

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

1724 

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

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

1727 """ 

1728 testsuiteElement = SubElement(parentElement, "testsuite") 

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

1730 if testsuite._startTime is not None: 

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

1732 if testsuite._duration is not None: 

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

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

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

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

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

1738 # if testsuite._assertionCount is not None: 

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

1740 if testsuite._hostname is not None: 

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

1742 

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

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

1745 self._GenerateTestcase(tc, testsuiteElement) 

1746 

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

1748 """ 

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

1750 

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

1752 

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

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

1755 """ 

1756 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1762 if testcase._assertionCount is not None: 

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

1764 

1765 if testcase._status is TestcaseStatus.Passed: 

1766 pass 

1767 elif testcase._status is TestcaseStatus.Failed: 

1768 failureElement = SubElement(testcaseElement, "failure") 

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

1770 skippedElement = SubElement(testcaseElement, "skipped") 

1771 else: 

1772 errorElement = SubElement(testcaseElement, "error") 

1773 

1774 def __str__(self) -> str: 

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

1776 className = self.__class__.__name__ 

1777 return ( 

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

1779 )