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

799 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:26 +0000

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

2# _____ ____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2024-2026 Electronic Design Automation Abstraction (EDA²) # 

15# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33The pyEDAA.Reports.Unittesting.JUnit package implements a hierarchy of test entities for the JUnit unit testing summary 

34file format (XML format). This test entity hierarchy is not derived from :class:`pyEDAA.Reports.Unittesting`, because it 

35doesn't match the unified data model. Nonetheless, both data models can be converted to each other. In addition, derived 

36data models are provided for the many dialects of that XML file format. See the list modules in this package for the 

37implemented dialects. 

38 

39The test entity hierarchy consists of test cases, test classes, test suites and a test summary. Test cases are the leaf 

40elements in the hierarchy and represent an individual test run. Next, test classes group test cases, because the 

41original Ant + JUnit format groups test cases (Java methods) in a Java class. Next, test suites are used to group 

42multiple test classes. Finally, the root element is a test summary. When such a summary is stored in a file format like 

43Ant + JUnit4 XML, a file format specific document is derived from a summary class. 

44 

45**Data Model** 

46 

47.. mermaid:: 

48 

49 graph TD; 

50 doc[Document] 

51 sum[Summary] 

52 ts1[Testsuite] 

53 ts11[Testsuite] 

54 ts2[Testsuite] 

55 

56 tc111[Testclass] 

57 tc112[Testclass] 

58 tc23[Testclass] 

59 

60 tc1111[Testcase] 

61 tc1112[Testcase] 

62 tc1113[Testcase] 

63 tc1121[Testcase] 

64 tc1122[Testcase] 

65 tc231[Testcase] 

66 tc232[Testcase] 

67 tc233[Testcase] 

68 

69 doc:::root -.-> sum:::summary 

70 sum --> ts1:::suite 

71 sum ---> ts2:::suite 

72 ts1 --> ts11:::suite 

73 

74 ts11 --> tc111:::cls 

75 ts11 --> tc112:::cls 

76 ts2 --> tc23:::cls 

77 

78 tc111 --> tc1111:::case 

79 tc111 --> tc1112:::case 

80 tc111 --> tc1113:::case 

81 tc112 --> tc1121:::case 

82 tc112 --> tc1122:::case 

83 tc23 --> tc231:::case 

84 tc23 --> tc232:::case 

85 tc23 --> tc233:::case 

86 

87 classDef root fill:#4dc3ff 

88 classDef summary fill:#80d4ff 

89 classDef suite fill:#b3e6ff 

90 classDef cls fill:#ff9966 

91 classDef case fill:#eeccff 

92""" 

93from __future__ import annotations 

94 

95from datetime import datetime, timedelta 

96from enum import Flag 

97from io import BytesIO 

98from pathlib import Path 

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

100 

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

102 XMLSyntaxError, _ElementTree, _Element, _Comment, XMLSchemaParseError 

103from pyTooling.Common import getFullyQualifiedName, getTypeName, getResourceFile 

104from pyTooling.Decorators import export, readonly 

105from pyTooling.Stopwatch import Stopwatch 

106from pyTooling.Exceptions import ToolingException 

107from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod 

108from pyTooling.Tree import Node 

109 

110from pyEDAA.Reports import Resources 

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

112 DuplicateTestcaseError, TestcaseStatus, TestsuiteStatus, TestsuiteKind, \ 

113 IterationScheme, TestcaseOutputMixin, Document as ut_Document, \ 

114 TestsuiteSummary as ut_TestsuiteSummary, Testsuite as ut_Testsuite, \ 

115 Testcase as ut_Testcase 

116 

117 

118@export 

119class JUnitErrorMixin: 

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

121 

122 

123@export 

124class UnittestError(UnittestError, JUnitErrorMixin): 

125 pass 

126 

127 

128@export 

129class AlreadyInTestsuiteError(AlreadyInTestsuiteError, JUnitErrorMixin): 

130 """ 

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

132 

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

134 hierarchy should occur only once in the hierarchy. 

135 

136 .. hint:: 

137 

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

139 """ 

140 

141 

142@export 

143class DuplicateTestsuiteError(DuplicateTestsuiteError, JUnitErrorMixin): 

144 """ 

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

146 

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

148 

149 .. hint:: 

150 

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

152 """ 

153 

154 

155@export 

156class DuplicateTestcaseError(DuplicateTestcaseError, JUnitErrorMixin): 

157 """ 

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

159 

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

161 

162 .. hint:: 

163 

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

165 """ 

166 

167 

168@export 

169class JUnitReaderMode(Flag): 

170 Default = 0 #: Default behavior 

171 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented 

172 

173 

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

175TestcaseAggregateReturnType = Tuple[int, int, int] 

176TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int] 

177 

178 

179@export 

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

181 """ 

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

183 

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

185 hierarchy. 

186 

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

188 child. |br| 

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

190 """ 

191 

192 _parent: Nullable[Testsuite] 

193 _name: str 

194 

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

196 """ 

197 Initializes the fields of the base-class. 

198 

199 :param name: Name of the test entity. 

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

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

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

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

204 """ 

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

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

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

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

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

210 raise ex 

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

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

213 

214 self._parent = parent 

215 self._name = name 

216 

217 @readonly 

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

219 """ 

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

221 

222 :returns: Reference to the parent entity. 

223 """ 

224 return self._parent 

225 

226 # QUESTION: allow Parent as setter? 

227 

228 @readonly 

229 def Name(self) -> str: 

230 """ 

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

232 

233 :returns: Name of the test entity. 

234 """ 

235 return self._name 

236 

237 

238@export 

239class BaseWithProperties(Base): 

240 """ 

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

242 

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

244 

245 Every test entity offers an internal dictionary for properties. 

246 """ 

247 

248 _duration: Nullable[timedelta] 

249 _assertionCount: Nullable[int] 

250 _properties: Dict[str, Any] 

251 

252 def __init__( 

253 self, 

254 name: str, 

255 duration: Nullable[timedelta] = None, 

256 assertionCount: Nullable[int] = None, 

257 *, 

258 parent: Nullable[Testsuite] = None 

259 ) -> None: 

260 """ 

261 Initializes the fields of the base-class. 

262 

263 :param name: Name of the test entity. 

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

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

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

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

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

269 """ 

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

271 

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

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

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

275 raise ex 

276 

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

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

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

280 raise ex 

281 

282 self._duration = duration 

283 self._assertionCount = assertionCount 

284 

285 self._properties = {} 

286 

287 @readonly 

288 def Duration(self) -> timedelta: 

289 """ 

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

291 

292 .. note:: 

293 

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

295 

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

297 """ 

298 return self._duration 

299 

300 @readonly 

301 @abstractmethod 

302 def AssertionCount(self) -> int: 

303 """ 

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

305 

306 .. note:: 

307 

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

309 

310 :returns: Number of assertions. 

311 """ 

312 

313 def __len__(self) -> int: 

314 """ 

315 Returns the number of annotated properties. 

316 

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

318 

319 :returns: Number of annotated properties. 

320 """ 

321 return len(self._properties) 

322 

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

324 """ 

325 Access a property by name. 

326 

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

328 

329 :param name: Name if the property. 

330 :returns: Value of the accessed property. 

331 """ 

332 return self._properties[name] 

333 

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

335 """ 

336 Set the value of a property by name. 

337 

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

339 

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

341 

342 :param name: Name of the property. 

343 :param value: Value of the property. 

344 """ 

345 self._properties[name] = value 

346 

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

348 """ 

349 Delete a property by name. 

350 

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

352 

353 :param name: Name if the property. 

354 """ 

355 del self._properties[name] 

356 

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

358 """ 

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

360 

361 Syntax: :pycode:`name in obj` 

362 

363 :param name: Name of the property. 

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

365 """ 

366 return name in self._properties 

367 

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

369 """ 

370 Iterate all annotated properties. 

371 

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

373 

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

375 """ 

376 yield from self._properties.items() 

377 

378 

379@export 

380class Testcase(BaseWithProperties, TestcaseOutputMixin): 

381 """ 

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

383 

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

385 of the hierarchy is a test summary. A test case without a class (empty ``classname``) belongs to its test suite 

386 directly, e.g. pytest's entry for a skipped test module. 

387 

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

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

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

391 """ 

392 

393 _status: TestcaseStatus 

394 

395 def __init__( 

396 self, 

397 name: str, 

398 duration: Nullable[timedelta] = None, 

399 status: TestcaseStatus = TestcaseStatus.Unknown, 

400 assertionCount: Nullable[int] = None, 

401 message: Nullable[str] = None, 

402 details: Nullable[str] = None, 

403 standardOutput: Nullable[str] = None, 

404 standardError: Nullable[str] = None, 

405 *, 

406 parent: Nullable[Testclass | Testsuite] = None 

407 ) -> None: 

408 """ 

409 Initializes the fields of a test case. 

410 

411 :param name: Name of the test entity. 

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

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

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

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

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

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

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

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

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

421 :raises TypeError: If parameter 'parent' is not a Testclass or Testsuite. 

422 """ 

423 if parent is not None: 

424 if not isinstance(parent, (Testclass, Testsuite)): 

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

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

427 raise ex 

428 

429 parent._testcases[name] = self 

430 

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

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

433 

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

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

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

437 raise ex 

438 

439 self._status = status 

440 

441 @readonly 

442 def Classname(self) -> str: 

443 """ 

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

445 

446 :returns: The test case's class name, or an empty string for a test case directly in a test suite. 

447 

448 .. note:: 

449 

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

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

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

453 """ 

454 if self._parent is None: 

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

456 elif isinstance(self._parent, Testsuite): 

457 return "" 

458 else: 

459 return self._parent._name 

460 

461 @readonly 

462 def Status(self) -> TestcaseStatus: 

463 """ 

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

465 

466 :returns: The test case's status. 

467 """ 

468 return self._status 

469 

470 @readonly 

471 def AssertionCount(self) -> int: 

472 """ 

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

474 

475 .. note:: 

476 

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

478 

479 :returns: Number of assertions. 

480 """ 

481 if self._assertionCount is None: 

482 return 0 

483 return self._assertionCount 

484 

485 def Copy(self) -> Testcase: 

486 return self.__class__( 

487 self._name, 

488 self._duration, 

489 self._status, 

490 self._assertionCount, 

491 self._message, 

492 self._details, 

493 self._standardOutput, 

494 self._standardError 

495 ) 

496 

497 def Aggregate(self) -> None: 

498 if self._status is TestcaseStatus.Unknown: 

499 if self._assertionCount is None: 

500 self._status = TestcaseStatus.Passed 

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

502 self._status = TestcaseStatus.Weak 

503 else: 

504 self._status = TestcaseStatus.Failed 

505 

506 # TODO: check for setup errors 

507 # TODO: check for teardown errors 

508 

509 @classmethod 

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

511 """ 

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

513 

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

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

516 """ 

517 return cls( 

518 testcase._name, 

519 duration=testcase._testDuration, 

520 status= testcase._status, 

521 assertionCount=testcase._assertionCount, 

522 message=testcase._message, 

523 details=testcase._details, 

524 standardOutput=testcase._standardOutput, 

525 standardError=testcase._standardError 

526 ) 

527 

528 def ToTestcase(self) -> ut_Testcase: 

529 return ut_Testcase( 

530 self._name, 

531 testDuration=self._duration, 

532 status=self._status, 

533 assertionCount=self._assertionCount, 

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

535 passedAssertionCount=self._assertionCount, 

536 message=self._message, 

537 details=self._details, 

538 standardOutput=self._standardOutput, 

539 standardError=self._standardError 

540 ) 

541 

542 def ToTree(self) -> Node: 

543 node = Node(value=self._name) 

544 node["status"] = self._status 

545 node["assertionCount"] = self._assertionCount 

546 node["duration"] = self._duration 

547 

548 return node 

549 

550 def __str__(self) -> str: 

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

552 className = getTypeName(self) 

553 return ( 

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

555 ) 

556 

557 

558@export 

559class TestsuiteBase(BaseWithProperties): 

560 """ 

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

562 

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

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

565 test summary contains no test classes and test cases. 

566 """ 

567 

568 _startTime: Nullable[datetime] 

569 _status: TestsuiteStatus 

570 

571 _tests: int 

572 _skipped: int 

573 _errored: int 

574 _weak: int 

575 _failed: int 

576 _passed: int 

577 

578 def __init__( 

579 self, 

580 name: str, 

581 startTime: Nullable[datetime] = None, 

582 duration: Nullable[timedelta] = None, 

583 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

584 *, 

585 parent: Nullable[Testsuite] = None 

586 ) -> None: 

587 """ 

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

589 

590 :param name: Name of the test entity. 

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

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

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

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

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

596 """ 

597 if parent is not None: 

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

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

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

601 raise ex 

602 

603 parent._testsuites[name] = self 

604 

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

606 

607 self._startTime = startTime 

608 self._status = status 

609 self._tests = 0 

610 self._skipped = 0 

611 self._errored = 0 

612 self._failed = 0 

613 self._passed = 0 

614 

615 @readonly 

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

617 """ 

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

619 

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

621 """ 

622 return self._startTime 

623 

624 @readonly 

625 def Status(self) -> TestsuiteStatus: 

626 """ 

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

628 

629 :returns: Status of the test entity. 

630 """ 

631 return self._status 

632 

633 @readonly 

634 @mustoverride 

635 def TestcaseCount(self) -> int: 

636 """ 

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

638 

639 :returns: Number of testcases. 

640 """ 

641 pass 

642 

643 @readonly 

644 def Tests(self) -> int: 

645 """ 

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

647 

648 :returns: Number of testcases. 

649 """ 

650 return self.TestcaseCount 

651 

652 @readonly 

653 def Skipped(self) -> int: 

654 """ 

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

656 

657 :returns: Number of skipped testcases. 

658 """ 

659 return self._skipped 

660 

661 @readonly 

662 def Errored(self) -> int: 

663 """ 

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

665 

666 :returns: Number of errored testcases. 

667 """ 

668 return self._errored 

669 

670 @readonly 

671 def Failed(self) -> int: 

672 """ 

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

674 

675 :returns: Number of failed testcases. 

676 """ 

677 return self._failed 

678 

679 @readonly 

680 def Passed(self) -> int: 

681 """ 

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

683 

684 :returns: Number of passed testcases. 

685 """ 

686 return self._passed 

687 

688 def Aggregate(self) -> TestsuiteAggregateReturnType: 

689 tests = 0 

690 skipped = 0 

691 errored = 0 

692 weak = 0 

693 failed = 0 

694 passed = 0 

695 

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

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

698 # tests += t 

699 # skipped += s 

700 # errored += e 

701 # weak += w 

702 # failed += f 

703 # passed += p 

704 

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

706 

707 @mustoverride 

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

709 pass 

710 

711 

712@export 

713class Testclass(Base): 

714 """ 

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

716 

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

718 """ 

719 

720 _testcases: Dict[str, Testcase] 

721 

722 def __init__( 

723 self, 

724 classname: str, 

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

726 *, 

727 parent: Nullable[Testsuite] = None 

728 ) -> None: 

729 """ 

730 Initializes the fields of the test class. 

731 

732 :param classname: Classname of the test entity. 

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

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

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

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

737 """ 

738 if parent is not None: 

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

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

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

742 raise ex 

743 

744 parent._testclasses[classname] = self 

745 

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

747 

748 self._testcases = {} 

749 if testcases is not None: 

750 for testcase in testcases: 

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

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

753 

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

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

756 

757 testcase._parent = self 

758 self._testcases[testcase._name] = testcase 

759 

760 @readonly 

761 def Classname(self) -> str: 

762 """ 

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

764 

765 :returns: The test class' name. 

766 """ 

767 return self._name 

768 

769 @readonly 

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

771 """ 

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

773 

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

775 """ 

776 return self._testcases 

777 

778 @readonly 

779 def TestcaseCount(self) -> int: 

780 """ 

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

782 

783 :returns: Number of test cases. 

784 """ 

785 return len(self._testcases) 

786 

787 @readonly 

788 def AssertionCount(self) -> int: 

789 """ 

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

791 

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

793 """ 

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

795 

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

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

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

799 

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

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

802 

803 testcase._parent = self 

804 self._testcases[testcase._name] = testcase 

805 

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

807 for testcase in testcases: 

808 self.AddTestcase(testcase) 

809 

810 def ToTestsuite(self) -> ut_Testsuite: 

811 return ut_Testsuite( 

812 self._name, 

813 kind=TestsuiteKind.Class, 

814 # startTime=self._startTime, 

815 # totalDuration=self._duration, 

816 # status=self._status, 

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

818 ) 

819 

820 def ToTree(self) -> Node: 

821 node = Node( 

822 value=self._name, 

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

824 ) 

825 

826 return node 

827 

828 def __str__(self) -> str: 

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

830 className = getTypeName(self) 

831 return ( 

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

833 ) 

834 

835 

836@export 

837class Testsuite(TestsuiteBase): 

838 """ 

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

840 

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

842 without a class (empty ``classname``) is contained in the test suite directly. 

843 """ 

844 

845 _hostname: str 

846 _testclasses: Dict[str, Testclass] 

847 _testcases: Dict[str, Testcase] #: Test cases without a class, contained in the test suite directly. 

848 

849 def __init__( 

850 self, 

851 name: str, 

852 hostname: Nullable[str] = None, 

853 startTime: Nullable[datetime] = None, 

854 duration: Nullable[timedelta] = None, 

855 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

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

858 *, 

859 parent: Nullable[TestsuiteSummary] = None 

860 ) -> None: 

861 """ 

862 Initializes the fields of a test suite. 

863 

864 :param name: Name of the test suite. 

865 :param hostname: Optional, name of the host the test suite was executed on. 

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

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

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

869 :param testclasses: Optional, test classes of the test suite. 

870 :param testcases: Optional, test cases without a class, contained in the test suite directly. 

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

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

873 :raises TypeError: If an element of parameter 'testcases' is not a Testcase. 

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

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

876 test cases. 

877 :raises TypeError: If parameter 'parent' is not a TestsuiteSummary. 

878 """ 

879 if parent is not None: 

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

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

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

883 raise ex 

884 

885 parent._testsuites[name] = self 

886 

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

888 

889 self._hostname = hostname 

890 

891 self._testclasses = {} 

892 if testclasses is not None: 

893 for testclass in testclasses: 

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

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

896 

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

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

899 

900 testclass._parent = self 

901 self._testclasses[testclass._name] = testclass 

902 

903 self._testcases = {} 

904 if testcases is not None: 

905 if not isinstance(testcases, Iterable): 

906 ex = TypeError("Parameter 'testcases' is not iterable.") 

907 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") 

908 raise ex 

909 

910 for testcase in testcases: 

911 if not isinstance(testcase, Testcase): 

912 ex = TypeError("Element of parameter 'testcases' is not of type 'Testcase'.") 

913 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") 

914 raise ex 

915 elif testcase._parent is not None: 915 ↛ 916line 915 didn't jump to line 916 because the condition on line 915 was never true

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

917 

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

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

920 

921 testcase._parent = self 

922 self._testcases[testcase._name] = testcase 

923 

924 @readonly 

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

926 """ 

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

928 

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

930 """ 

931 return self._hostname 

932 

933 @readonly 

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

935 """ 

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

937 

938 :returns: Dictionary of testclass names and testclasses. 

939 """ 

940 return self._testclasses 

941 

942 @readonly 

943 def TestclassCount(self) -> int: 

944 """ 

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

946 

947 :returns: Number of testclasses. 

948 """ 

949 return len(self._testclasses) 

950 

951 @readonly 

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

953 """ 

954 Read-only property to access the testsuite's testcases without a class (:attr:`_testcases`). 

955 

956 :returns: Dictionary of testcase names and testcases. 

957 """ 

958 return self._testcases 

959 

960 @readonly 

961 def TestcaseCount(self) -> int: 

962 """ 

963 Read-only property to return the number of testcases across all testclasses and of the testcases without a class. 

964 

965 :returns: Sum of the testclasses' testcase counts and the number of testcases without a class. 

966 """ 

967 return sum(cls.TestcaseCount for cls in self._testclasses.values()) + len(self._testcases) 

968 

969 @readonly 

970 def AssertionCount(self) -> int: 

971 """ 

972 Read-only property to return the number of assertions across all testclasses and testcases without a class. 

973 

974 :returns: Sum of the testclasses' and the testcases' assertion counts. 

975 """ 

976 return ( 

977 sum(cls.AssertionCount for cls in self._testclasses.values()) + 

978 sum(tc.AssertionCount for tc in self._testcases.values()) 

979 ) 

980 

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

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

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

984 

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

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

987 

988 testclass._parent = self 

989 self._testclasses[testclass._name] = testclass 

990 

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

992 for testcase in testclasses: 

993 self.AddTestclass(testcase) 

994 

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

996 """ 

997 Add a testcase without a class to the testsuite. 

998 

999 :param testcase: The testcase to add. 

1000 :raises ValueError: If parameter 'testcase' is None. 

1001 :raises TypeError: If parameter 'testcase' is not a Testcase. 

1002 :raises AlreadyInTestsuiteError: If the testcase is already part of a test entity hierarchy. 

1003 :raises DuplicateTestcaseError: If the testsuite already contains a testcase without a class by the same name. 

1004 """ 

1005 if testcase is None: 

1006 raise ValueError("Parameter 'testcase' is None.") 

1007 elif not isinstance(testcase, Testcase): 

1008 ex = TypeError("Parameter 'testcase' is not of type 'Testcase'.") 

1009 ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") 

1010 raise ex 

1011 elif testcase._parent is not None: 

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

1013 

1014 if testcase._name in self._testcases: 

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

1016 

1017 testcase._parent = self 

1018 self._testcases[testcase._name] = testcase 

1019 

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

1021 """ 

1022 Add testcases without a class to the testsuite. 

1023 

1024 :param testcases: The testcases to add. 

1025 :raises ValueError: If parameter 'testcases' is None. 

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

1027 """ 

1028 if testcases is None: 

1029 raise ValueError("Parameter 'testcases' is None.") 

1030 elif not isinstance(testcases, Iterable): 

1031 ex = TypeError("Parameter 'testcases' is not iterable.") 

1032 ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") 

1033 raise ex 

1034 

1035 for testcase in testcases: 

1036 self.AddTestcase(testcase) 

1037 

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

1039 # return self.Iterate(scheme) 

1040 

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

1042 return self.Iterate(scheme) 

1043 

1044 def Copy(self) -> Testsuite: 

1045 return self.__class__( 

1046 self._name, 

1047 self._hostname, 

1048 self._startTime, 

1049 self._duration, 

1050 self._status 

1051 ) 

1052 

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

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

1055 

1056 testcases = list(self._testcases.values()) 

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

1058 testcases.extend(testclass._testcases.values()) 

1059 

1060 for testcase in testcases: 

1061 _ = testcase.Aggregate() 

1062 

1063 tests += 1 

1064 status = testcase._status 

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

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

1067 elif status is TestcaseStatus.Skipped: 

1068 skipped += 1 

1069 elif status is TestcaseStatus.Errored: 

1070 errored += 1 

1071 elif status is TestcaseStatus.Passed: 

1072 passed += 1 

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

1074 failed += 1 

1075 elif status is TestcaseStatus.Weak: 

1076 weak += 1 

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

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

1079 else: 

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

1081 

1082 self._tests = tests 

1083 self._skipped = skipped 

1084 self._errored = errored 

1085 self._weak = weak 

1086 self._failed = failed 

1087 self._passed = passed 

1088 

1089 # FIXME: weak? 

1090 if errored > 0: 

1091 self._status = TestsuiteStatus.Errored 

1092 elif failed > 0: 

1093 self._status = TestsuiteStatus.Failed 

1094 elif tests == 0: 

1095 self._status = TestsuiteStatus.Empty 

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

1097 self._status = TestsuiteStatus.Passed 

1098 elif tests == skipped: 

1099 self._status = TestsuiteStatus.Skipped 

1100 else: 

1101 self._status = TestsuiteStatus.Unknown 

1102 

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

1104 

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

1106 """ 

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

1108 

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

1110 

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

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

1113 """ 

1114 if IterationScheme.PreOrder in scheme: 

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

1116 yield self 

1117 

1118 if IterationScheme.IncludeTestcases in scheme: 

1119 for testcase in self._testcases.values(): 

1120 yield testcase 

1121 

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

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

1124 

1125 if IterationScheme.PostOrder in scheme: 

1126 if IterationScheme.IncludeTestcases in scheme: 

1127 for testcase in self._testcases.values(): 

1128 yield testcase 

1129 

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

1131 yield self 

1132 

1133 @classmethod 

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

1135 """ 

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

1137 

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

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

1140 """ 

1141 juTestsuite = cls( 

1142 testsuite._name, 

1143 hostname=testsuite._hostname, 

1144 startTime=testsuite._startTime, 

1145 duration=testsuite._totalDuration, 

1146 status= testsuite._status, 

1147 ) 

1148 

1149 for tc in testsuite.IterateTestcases(): 

1150 ts = tc._parent 

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

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

1153 elif ts is testsuite: 

1154 juTestsuite.AddTestcase(Testcase.FromTestcase(tc)) 

1155 continue 

1156 

1157 classname = ts._name 

1158 ts = ts._parent 

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

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

1161 ts = ts._parent 

1162 

1163 if classname in juTestsuite._testclasses: 

1164 juClass = juTestsuite._testclasses[classname] 

1165 else: 

1166 juClass = Testclass(classname, parent=juTestsuite) 

1167 

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

1169 

1170 return juTestsuite 

1171 

1172 def ToTestsuite(self) -> ut_Testsuite: 

1173 testsuite = ut_Testsuite( 

1174 self._name, 

1175 kind=TestsuiteKind.Logical, 

1176 hostname=self._hostname, 

1177 startTime=self._startTime, 

1178 totalDuration=self._duration, 

1179 status=self._status, 

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

1181 ) 

1182 

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

1184 suite = testsuite 

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

1186 for element in classpath: 

1187 if element in suite._testsuites: 

1188 suite = suite._testsuites[element] 

1189 else: 

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

1191 

1192 suite._kind = TestsuiteKind.Class 

1193 if suite._parent is not testsuite: 

1194 suite._parent._kind = TestsuiteKind.Module 

1195 

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

1197 

1198 return testsuite 

1199 

1200 def ToTree(self) -> Node: 

1201 node = Node( 

1202 value=self._name, 

1203 children=( 

1204 *(tc.ToTree() for tc in self._testcases.values()), 

1205 *(cls.ToTree() for cls in self._testclasses.values()) 

1206 ) 

1207 ) 

1208 node["startTime"] = self._startTime 

1209 node["duration"] = self._duration 

1210 

1211 return node 

1212 

1213 def __str__(self) -> str: 

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

1215 className = getTypeName(self) 

1216 return ( 

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

1218 ) 

1219 

1220 

1221@export 

1222class TestsuiteSummary(TestsuiteBase): 

1223 _testsuites: Dict[str, Testsuite] 

1224 

1225 def __init__( 

1226 self, 

1227 name: str, 

1228 startTime: Nullable[datetime] = None, 

1229 duration: Nullable[timedelta] = None, 

1230 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1232 ) -> None: 

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

1234 

1235 self._testsuites = {} 

1236 if testsuites is not None: 

1237 for testsuite in testsuites: 

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

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

1240 

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

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

1243 

1244 testsuite._parent = self 

1245 self._testsuites[testsuite._name] = testsuite 

1246 

1247 @readonly 

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

1249 """ 

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

1251 

1252 :returns: Dictionary of testsuite names and testsuites. 

1253 """ 

1254 return self._testsuites 

1255 

1256 @readonly 

1257 def TestcaseCount(self) -> int: 

1258 """ 

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

1260 

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

1262 """ 

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

1264 

1265 @readonly 

1266 def TestsuiteCount(self) -> int: 

1267 """ 

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

1269 

1270 :returns: Number of testsuites. 

1271 """ 

1272 return len(self._testsuites) 

1273 

1274 @readonly 

1275 def AssertionCount(self) -> int: 

1276 """ 

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

1278 

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

1280 """ 

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

1282 

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

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

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

1286 

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

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

1289 

1290 testsuite._parent = self 

1291 self._testsuites[testsuite._name] = testsuite 

1292 

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

1294 for testsuite in testsuites: 

1295 self.AddTestsuite(testsuite) 

1296 

1297 def Aggregate(self) -> TestsuiteAggregateReturnType: 

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

1299 

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

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

1302 tests += t 

1303 skipped += s 

1304 errored += e 

1305 weak += w 

1306 failed += f 

1307 passed += p 

1308 

1309 self._tests = tests 

1310 self._skipped = skipped 

1311 self._errored = errored 

1312 self._weak = weak 

1313 self._failed = failed 

1314 self._passed = passed 

1315 

1316 # FIXME: weak 

1317 if errored > 0: 

1318 self._status = TestsuiteStatus.Errored 

1319 elif failed > 0: 

1320 self._status = TestsuiteStatus.Failed 

1321 elif tests == 0: 

1322 self._status = TestsuiteStatus.Empty 

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

1324 self._status = TestsuiteStatus.Passed 

1325 elif tests == skipped: 

1326 self._status = TestsuiteStatus.Skipped 

1327 else: 

1328 self._status = TestsuiteStatus.Unknown 

1329 

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

1331 

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

1333 """ 

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

1335 

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

1337 

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

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

1340 """ 

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

1342 yield self 

1343 

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

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

1346 

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

1348 yield self 

1349 

1350 @classmethod 

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

1352 """ 

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

1354 

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

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

1357 """ 

1358 return cls( 

1359 testsuiteSummary._name, 

1360 startTime=testsuiteSummary._startTime, 

1361 duration=testsuiteSummary._totalDuration, 

1362 status=testsuiteSummary._status, 

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

1364 ) 

1365 

1366 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: 

1367 """ 

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

1369 

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

1371 

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

1373 """ 

1374 return ut_TestsuiteSummary( 

1375 self._name, 

1376 startTime=self._startTime, 

1377 totalDuration=self._duration, 

1378 status=self._status, 

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

1380 ) 

1381 

1382 def ToTree(self) -> Node: 

1383 node = Node( 

1384 value=self._name, 

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

1386 ) 

1387 node["startTime"] = self._startTime 

1388 node["duration"] = self._duration 

1389 

1390 return node 

1391 

1392 def __str__(self) -> str: 

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

1394 className = getTypeName(self) 

1395 return ( 

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

1397 ) 

1398 

1399 

1400@export 

1401class Document(TestsuiteSummary, ut_Document): 

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

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

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

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

1406 

1407 _readerMode: JUnitReaderMode 

1408 _xmlDocument: Nullable[_ElementTree] 

1409 

1410 def __init__( 

1411 self, 

1412 xmlReportFile: Path, 

1413 analyzeAndConvert: bool = False, 

1414 readerMode: JUnitReaderMode = JUnitReaderMode.Default, 

1415 *, 

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

1417 startTime: Nullable[datetime] = None, 

1418 duration: Nullable[timedelta] = None, 

1419 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

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

1421 ) -> None: 

1422 """ 

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

1424 

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

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

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

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

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

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

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

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

1433 """ 

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

1435 

1436 self._readerMode = readerMode 

1437 self._xmlDocument = None 

1438 

1439 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert) 

1440 

1441 @classmethod 

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

1443 """ 

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

1445 

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

1447 :meth:`Aggregate`. 

1448 

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

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

1451 :returns: JUnit XML document of this dialect. 

1452 """ 

1453 doc = cls( 

1454 xmlReportFile, 

1455 name=testsuiteSummary._name, 

1456 startTime=testsuiteSummary._startTime, 

1457 duration=testsuiteSummary._totalDuration, 

1458 status=testsuiteSummary._status, 

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

1460 ) 

1461 doc.Aggregate() 

1462 

1463 return doc 

1464 

1465 def Analyze(self) -> None: 

1466 """ 

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

1468 schema. 

1469 

1470 .. hint:: 

1471 

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

1473 

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

1475 """ 

1476 xmlSchemaFile = "Any-JUnit.xsd" 

1477 self._Analyze(xmlSchemaFile) 

1478 

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

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

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

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

1483 

1484 with Stopwatch() as sw: 

1485 try: 

1486 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile) 

1487 except ToolingException as ex: 

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

1489 

1490 try: 

1491 schemaParser = XMLParser(ns_clean=True) 

1492 schemaRoot = parse(xmlSchemaResourceFile, schemaParser) 

1493 except XMLSyntaxError as ex: 

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

1495 

1496 try: 

1497 junitSchema = XMLSchema(schemaRoot) 

1498 except XMLSchemaParseError as ex: 

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

1500 

1501 try: 

1502 content = self._path.read_bytes() 

1503 except OSError as ex: 

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

1505 

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

1507 try: 

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

1509 except XMLSyntaxError as ex: 

1510 for logEntry in junitParser.error_log: 

1511 ex.add_note(str(logEntry)) 

1512 raise UnittestError( 

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

1514 ) from ex 

1515 

1516 self._analysisDuration = sw.Duration 

1517 

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

1519 """ 

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

1521 

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

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

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

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

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

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

1528 """ 

1529 if path is None: 

1530 path = self._path 

1531 

1532 if not overwrite and path.exists(): 

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

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

1535 

1536 if regenerate: 

1537 self.Generate(overwrite=True) 

1538 

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

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

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

1542 raise ex 

1543 

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

1545 try: 

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

1547 file.write(content) 

1548 except OSError as ex: 

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

1550 

1551 def Convert(self) -> None: 

1552 """ 

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

1554 

1555 This method converts the root element. 

1556 

1557 .. hint:: 

1558 

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

1560 

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

1562 """ 

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

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

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

1566 raise ex 

1567 

1568 with Stopwatch() as sw: 

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

1570 

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

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

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

1574 

1575 if False: # self._readerMode is JUnitReaderMode. 

1576 self._tests = self._ConvertTests(testsuitesNode) 

1577 self._skipped = self._ConvertSkipped(testsuitesNode) 

1578 self._errored = self._ConvertErrors(testsuitesNode) 

1579 self._failed = self._ConvertFailures(testsuitesNode) 

1580 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1581 

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

1583 self._ConvertTestsuite(self, rootNode) 

1584 

1585 if True: # self._readerMode is JUnitReaderMode. 

1586 self.Aggregate() 

1587 

1588 self._modelConversion = sw.Duration 

1589 

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

1591 """ 

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

1593 

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

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

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

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

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

1599 """ 

1600 if "name" in element.attrib: 

1601 return element.attrib["name"] 

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

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

1604 else: 

1605 return default 

1606 

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

1608 """ 

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

1610 

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

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

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

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

1615 """ 

1616 if "timestamp" in element.attrib: 

1617 timestamp = element.attrib["timestamp"] 

1618 return datetime.fromisoformat(timestamp) 

1619 elif not optional: 

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

1621 else: 

1622 return None 

1623 

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

1625 """ 

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

1627 

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

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

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

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

1632 """ 

1633 if "time" in element.attrib: 

1634 time = element.attrib["time"] 

1635 return timedelta(seconds=float(time)) 

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

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

1638 else: 

1639 return None 

1640 

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

1642 """ 

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

1644 

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

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

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

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

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

1650 """ 

1651 if "hostname" in element.attrib: 

1652 return element.attrib["hostname"] 

1653 elif not optional: 

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

1655 else: 

1656 return default 

1657 

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

1659 """ 

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

1661 

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

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

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

1665 """ 

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

1667 return element.attrib["classname"] 

1668 else: 

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

1670 

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

1672 """ 

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

1674 

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

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

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

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

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

1680 """ 

1681 if "tests" in element.attrib: 

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

1683 elif not optional: 

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

1685 else: 

1686 return default 

1687 

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

1689 """ 

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

1691 

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

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

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

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

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

1697 """ 

1698 if "skipped" in element.attrib: 

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

1700 elif not optional: 

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

1702 else: 

1703 return default 

1704 

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

1706 """ 

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

1708 

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

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

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

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

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

1714 """ 

1715 if "errors" in element.attrib: 

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

1717 elif not optional: 

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

1719 else: 

1720 return default 

1721 

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

1723 """ 

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

1725 

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

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

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

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

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

1731 """ 

1732 if "failures" in element.attrib: 

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

1734 elif not optional: 

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

1736 else: 

1737 return default 

1738 

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

1740 """ 

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

1742 

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

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

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

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

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

1748 """ 

1749 if "assertions" in element.attrib: 

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

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

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

1753 else: 

1754 return default 

1755 

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

1757 """ 

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

1759 

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

1761 

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

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

1764 """ 

1765 newTestsuite = self._TESTSUITE( 

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

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

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

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

1770 parent=parent 

1771 ) 

1772 

1773 if False: # self._readerMode is JUnitReaderMode. 

1774 self._tests = self._ConvertTests(testsuitesNode) 

1775 self._skipped = self._ConvertSkipped(testsuitesNode) 

1776 self._errored = self._ConvertErrors(testsuitesNode) 

1777 self._failed = self._ConvertFailures(testsuitesNode) 

1778 self._assertionCount = self._ConvertAssertions(testsuitesNode) 

1779 

1780 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

1781 

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

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

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

1785 # self._ConvertTestsuite(newTestsuite, node) 

1786 # el 

1787 if node.tag == "testcase": 

1788 self._ConvertTestcase(newTestsuite, node) 

1789 

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

1791 """ 

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

1793 

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

1795 

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

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

1798 """ 

1799 className = self._ConvertClassname(testcaseNode) 

1800 if className == "": 

1801 testcaseParent = parent 

1802 else: 

1803 testcaseParent = self._FindOrCreateTestclass(parent, className) 

1804 

1805 newTestcase = self._TESTCASE( 

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

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

1808 assertionCount=self._ConvertAssertions(testcaseNode), 

1809 parent=testcaseParent 

1810 ) 

1811 

1812 self._ConvertTestcaseChildren(testcaseNode, newTestcase) 

1813 

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

1815 if className in parent._testclasses: 

1816 return parent._testclasses[className] 

1817 else: 

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

1819 

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

1821 """ 

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

1823 

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

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

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

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

1828 

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

1830 :param newTestcase: The test case to update. 

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

1832 """ 

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

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

1835 pass 

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

1837 if node.tag == "skipped": 

1838 newTestcase._status = TestcaseStatus.Skipped 

1839 self._ConvertStatusMessage(node, newTestcase) 

1840 elif node.tag == "failure": 

1841 newTestcase._status = TestcaseStatus.Failed 

1842 self._ConvertStatusMessage(node, newTestcase) 

1843 elif node.tag == "error": 

1844 newTestcase._status = TestcaseStatus.Errored 

1845 self._ConvertStatusMessage(node, newTestcase) 

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

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

1848 pass 

1849 elif newTestcase._standardOutput is None: 

1850 newTestcase._standardOutput = node.text 

1851 else: 

1852 newTestcase._standardOutput += node.text 

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

1854 if node.text is None: 

1855 pass 

1856 elif newTestcase._standardError is None: 

1857 newTestcase._standardError = node.text 

1858 else: 

1859 newTestcase._standardError += node.text 

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

1861 pass 

1862 else: 

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

1864 else: 

1865 pass 

1866 

1867 if newTestcase._status is TestcaseStatus.Unknown: 

1868 newTestcase._status = TestcaseStatus.Passed 

1869 

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

1871 """ 

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

1873 

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

1875 

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

1877 :param testcase: The test case to update. 

1878 """ 

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

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

1881 

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

1883 """ 

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

1885 

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

1887 

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

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

1890 """ 

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

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

1893 

1894 rootElement = Element("testsuites") 

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

1896 if self._startTime is not None: 

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

1898 if self._duration is not None: 

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

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

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

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

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

1904 # if self._assertionCount is not None: 

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

1906 

1907 self._xmlDocument = ElementTree(rootElement) 

1908 

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

1910 self._GenerateTestsuite(testsuite, rootElement) 

1911 

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

1913 """ 

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

1915 

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

1917 

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

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

1920 """ 

1921 testsuiteElement = SubElement(parentElement, "testsuite") 

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

1923 if testsuite._startTime is not None: 

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

1925 if testsuite._duration is not None: 

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

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

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

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

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

1931 # if testsuite._assertionCount is not None: 

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

1933 if testsuite._hostname is not None: 

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

1935 

1936 for tc in testsuite._testcases.values(): 

1937 self._GenerateTestcase(tc, testsuiteElement) 

1938 

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

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

1941 self._GenerateTestcase(tc, testsuiteElement) 

1942 

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

1944 """ 

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

1946 

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

1948 

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

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

1951 """ 

1952 testcaseElement = SubElement(parentElement, "testcase") 

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

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

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

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

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

1958 if testcase._assertionCount is not None: 

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

1960 

1961 self._GenerateTestcaseChildren(testcase, testcaseElement) 

1962 

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

1964 """ 

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

1966 

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

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

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

1970 

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

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

1973 """ 

1974 if testcase._status is TestcaseStatus.Passed: 

1975 statusElement = None 

1976 elif testcase._status is TestcaseStatus.Failed: 

1977 statusElement = SubElement(testcaseElement, "failure") 

1978 elif testcase._status is TestcaseStatus.Skipped: 

1979 statusElement = SubElement(testcaseElement, "skipped") 

1980 else: 

1981 statusElement = SubElement(testcaseElement, "error") 

1982 

1983 if statusElement is not None: 

1984 if testcase._message is not None: 

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

1986 

1987 if testcase._details is not None: 

1988 statusElement.text = testcase._details 

1989 

1990 if testcase._standardOutput is not None: 

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

1992 

1993 if testcase._standardError is not None: 

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

1995 

1996 def __str__(self) -> str: 

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

1998 className = getTypeName(self) 

1999 return ( 

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

2001 )