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
« 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.
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.
45**Data Model**
47.. mermaid::
49 graph TD;
50 doc[Document]
51 sum[Summary]
52 ts1[Testsuite]
53 ts11[Testsuite]
54 ts2[Testsuite]
56 tc111[Testclass]
57 tc112[Testclass]
58 tc23[Testclass]
60 tc1111[Testcase]
61 tc1112[Testcase]
62 tc1113[Testcase]
63 tc1121[Testcase]
64 tc1122[Testcase]
65 tc231[Testcase]
66 tc232[Testcase]
67 tc233[Testcase]
69 doc:::root -.-> sum:::summary
70 sum --> ts1:::suite
71 sum ---> ts2:::suite
72 ts1 --> ts11:::suite
74 ts11 --> tc111:::cls
75 ts11 --> tc112:::cls
76 ts2 --> tc23:::cls
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
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
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
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
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
118@export
119class JUnitErrorMixin:
120 """An error-mixin for JUnit format specific errors."""
123@export
124class UnittestError(UnittestError, JUnitErrorMixin):
125 pass
128@export
129class AlreadyInTestsuiteError(AlreadyInTestsuiteError, JUnitErrorMixin):
130 """
131 A unit test exception raised if the element is already part of a hierarchy.
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.
136 .. hint::
138 This is usually caused by a non-None parent reference.
139 """
142@export
143class DuplicateTestsuiteError(DuplicateTestsuiteError, JUnitErrorMixin):
144 """
145 A unit test exception raised on duplicate test suites (by name).
147 This exception is raised, if a child test suite with same name already exist in the test suite.
149 .. hint::
151 Test suite names need to be unique per parent element (test suite or test summary).
152 """
155@export
156class DuplicateTestcaseError(DuplicateTestcaseError, JUnitErrorMixin):
157 """
158 A unit test exception raised on duplicate test cases (by name).
160 This exception is raised, if a child test case with same name already exist in the test suite.
162 .. hint::
164 Test case names need to be unique per parent element (test suite).
165 """
168@export
169class JUnitReaderMode(Flag):
170 Default = 0 #: Default behavior
171 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented
174TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite")
175TestcaseAggregateReturnType = Tuple[int, int, int]
176TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int]
179@export
180class Base(metaclass=ExtendedType, slots=True):
181 """
182 Base-class for all test entities (test cases, test classes, test suites, ...).
184 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity
185 hierarchy.
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 """
192 _parent: Nullable[Testsuite]
193 _name: str
195 def __init__(self, name: str, *, parent: Nullable[Testsuite] = None) -> None:
196 """
197 Initializes the fields of the base-class.
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.")
214 self._parent = parent
215 self._name = name
217 @readonly
218 def Parent(self) -> Nullable[Testsuite]:
219 """
220 Read-only property to access the reference to the parent test entity.
222 :returns: Reference to the parent entity.
223 """
224 return self._parent
226 # QUESTION: allow Parent as setter?
228 @readonly
229 def Name(self) -> str:
230 """
231 Read-only property to access the test entity's name.
233 :returns: Name of the test entity.
234 """
235 return self._name
238@export
239class BaseWithProperties(Base):
240 """
241 Base-class for all test entities supporting properties (test cases, test suites, ...).
243 Every test entity has fields for the test duration and number of executed assertions.
245 Every test entity offers an internal dictionary for properties.
246 """
248 _duration: Nullable[timedelta]
249 _assertionCount: Nullable[int]
250 _properties: Dict[str, Any]
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.
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)
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
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
282 self._duration = duration
283 self._assertionCount = assertionCount
285 self._properties = {}
287 @readonly
288 def Duration(self) -> timedelta:
289 """
290 Read-only property to access the duration of a test entity run.
292 .. note::
294 The JUnit format doesn't distinguish setup, run and teardown durations.
296 :returns: Duration of the entity's execution.
297 """
298 return self._duration
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.
306 .. note::
308 The JUnit format doesn't distinguish passed and failed assertions.
310 :returns: Number of assertions.
311 """
313 def __len__(self) -> int:
314 """
315 Returns the number of annotated properties.
317 Syntax: :pycode:`length = len(obj)`
319 :returns: Number of annotated properties.
320 """
321 return len(self._properties)
323 def __getitem__(self, name: str) -> Any:
324 """
325 Access a property by name.
327 Syntax: :pycode:`value = obj[name]`
329 :param name: Name if the property.
330 :returns: Value of the accessed property.
331 """
332 return self._properties[name]
334 def __setitem__(self, name: str, value: Any) -> None:
335 """
336 Set the value of a property by name.
338 If the property doesn't exist yet, it's created.
340 Syntax: :pycode:`obj[name] = value`
342 :param name: Name of the property.
343 :param value: Value of the property.
344 """
345 self._properties[name] = value
347 def __delitem__(self, name: str) -> None:
348 """
349 Delete a property by name.
351 Syntax: :pycode:`del obj[name]`
353 :param name: Name if the property.
354 """
355 del self._properties[name]
357 def __contains__(self, name: str) -> bool:
358 """
359 Returns True, if a property was annotated by this name.
361 Syntax: :pycode:`name in obj`
363 :param name: Name of the property.
364 :returns: True, if the property was annotated.
365 """
366 return name in self._properties
368 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
369 """
370 Iterate all annotated properties.
372 Syntax: :pycode:`for name, value in obj:`
374 :returns: A generator of property tuples (name, value).
375 """
376 yield from self._properties.items()
379@export
380class Testcase(BaseWithProperties, TestcaseOutputMixin):
381 """
382 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run.
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.
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 """
393 _status: TestcaseStatus
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.
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
429 parent._testcases[name] = self
431 super().__init__(name, duration, assertionCount, parent=parent)
432 TestcaseOutputMixin.__init__(self, message, details, standardOutput, standardError)
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
439 self._status = status
441 @readonly
442 def Classname(self) -> str:
443 """
444 Read-only property to return the class name of the test case.
446 :returns: The test case's class name, or an empty string for a test case directly in a test suite.
448 .. note::
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
461 @readonly
462 def Status(self) -> TestcaseStatus:
463 """
464 Read-only property to access the status of the test case.
466 :returns: The test case's status.
467 """
468 return self._status
470 @readonly
471 def AssertionCount(self) -> int:
472 """
473 Read-only property to return the number of assertions (checks) in a test case.
475 .. note::
477 The JUnit format doesn't distinguish passed and failed assertions.
479 :returns: Number of assertions.
480 """
481 if self._assertionCount is None:
482 return 0
483 return self._assertionCount
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 )
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
506 # TODO: check for setup errors
507 # TODO: check for teardown errors
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.
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 )
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 )
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
548 return node
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 )
558@export
559class TestsuiteBase(BaseWithProperties):
560 """
561 Base-class for all test suites and for test summaries.
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 """
568 _startTime: Nullable[datetime]
569 _status: TestsuiteStatus
571 _tests: int
572 _skipped: int
573 _errored: int
574 _weak: int
575 _failed: int
576 _passed: int
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.
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
603 parent._testsuites[name] = self
605 super().__init__(name, duration, None, parent=parent)
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
615 @readonly
616 def StartTime(self) -> Nullable[datetime]:
617 """
618 Read-only property to access the time the test entity's execution started.
620 :returns: Start time of the execution, or ``None`` if it wasn't recorded.
621 """
622 return self._startTime
624 @readonly
625 def Status(self) -> TestsuiteStatus:
626 """
627 Read-only property to access the test entity's aggregated status.
629 :returns: Status of the test entity.
630 """
631 return self._status
633 @readonly
634 @mustoverride
635 def TestcaseCount(self) -> int:
636 """
637 Read-only property to access the number of testcases in this entity.
639 :returns: Number of testcases.
640 """
641 pass
643 @readonly
644 def Tests(self) -> int:
645 """
646 Read-only property to access the number of testcases in this entity.
648 :returns: Number of testcases.
649 """
650 return self.TestcaseCount
652 @readonly
653 def Skipped(self) -> int:
654 """
655 Read-only property to access the number of skipped testcases.
657 :returns: Number of skipped testcases.
658 """
659 return self._skipped
661 @readonly
662 def Errored(self) -> int:
663 """
664 Read-only property to access the number of errored testcases.
666 :returns: Number of errored testcases.
667 """
668 return self._errored
670 @readonly
671 def Failed(self) -> int:
672 """
673 Read-only property to access the number of failed testcases.
675 :returns: Number of failed testcases.
676 """
677 return self._failed
679 @readonly
680 def Passed(self) -> int:
681 """
682 Read-only property to access the number of passed testcases.
684 :returns: Number of passed testcases.
685 """
686 return self._passed
688 def Aggregate(self) -> TestsuiteAggregateReturnType:
689 tests = 0
690 skipped = 0
691 errored = 0
692 weak = 0
693 failed = 0
694 passed = 0
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
705 return tests, skipped, errored, weak, failed, passed
707 @mustoverride
708 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
709 pass
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.
717 Test classes contain test cases and are grouped by a test suites.
718 """
720 _testcases: Dict[str, Testcase]
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.
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
744 parent._testclasses[classname] = self
746 super().__init__(classname, parent=parent)
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.")
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}'.")
757 testcase._parent = self
758 self._testcases[testcase._name] = testcase
760 @readonly
761 def Classname(self) -> str:
762 """
763 Read-only property to access the name of the test class.
765 :returns: The test class' name.
766 """
767 return self._name
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.
774 :returns: Reference to the dictionary of test cases.
775 """
776 return self._testcases
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.
783 :returns: Number of test cases.
784 """
785 return len(self._testcases)
787 @readonly
788 def AssertionCount(self) -> int:
789 """
790 Read-only property to return the number of assertions across all testcases of this testclass.
792 :returns: Sum of the testcases' assertion counts.
793 """
794 return sum(tc.AssertionCount for tc in self._testcases.values())
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.")
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}'.")
803 testcase._parent = self
804 self._testcases[testcase._name] = testcase
806 def AddTestcases(self, testcases: Iterable[Testcase]) -> None:
807 for testcase in testcases:
808 self.AddTestcase(testcase)
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 )
820 def ToTree(self) -> Node:
821 node = Node(
822 value=self._name,
823 children=(tc.ToTree() for tc in self._testcases.values())
824 )
826 return node
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 )
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.
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 """
845 _hostname: str
846 _testclasses: Dict[str, Testclass]
847 _testcases: Dict[str, Testcase] #: Test cases without a class, contained in the test suite directly.
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.
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
885 parent._testsuites[name] = self
887 super().__init__(name, startTime, duration, status, parent=parent)
889 self._hostname = hostname
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.")
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}'.")
900 testclass._parent = self
901 self._testclasses[testclass._name] = testclass
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
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.")
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}'.")
921 testcase._parent = self
922 self._testcases[testcase._name] = testcase
924 @readonly
925 def Hostname(self) -> Nullable[str]:
926 """
927 Read-only property to access the host the testsuite was executed on.
929 :returns: Hostname, or ``None`` if it wasn't recorded.
930 """
931 return self._hostname
933 @readonly
934 def Testclasses(self) -> Dict[str, Testclass]:
935 """
936 Read-only property to access the testsuite's testclasses.
938 :returns: Dictionary of testclass names and testclasses.
939 """
940 return self._testclasses
942 @readonly
943 def TestclassCount(self) -> int:
944 """
945 Read-only property to return the number of testclasses in this testsuite.
947 :returns: Number of testclasses.
948 """
949 return len(self._testclasses)
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`).
956 :returns: Dictionary of testcase names and testcases.
957 """
958 return self._testcases
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.
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)
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.
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 )
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.")
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}'.")
988 testclass._parent = self
989 self._testclasses[testclass._name] = testclass
991 def AddTestclasses(self, testclasses: Iterable[Testclass]) -> None:
992 for testcase in testclasses:
993 self.AddTestclass(testcase)
995 def AddTestcase(self, testcase: Testcase) -> None:
996 """
997 Add a testcase without a class to the testsuite.
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.")
1014 if testcase._name in self._testcases:
1015 raise DuplicateTestcaseError(f"Testsuite already contains a testcase with same name '{testcase._name}'.")
1017 testcase._parent = self
1018 self._testcases[testcase._name] = testcase
1020 def AddTestcases(self, testcases: Iterable[Testcase]) -> None:
1021 """
1022 Add testcases without a class to the testsuite.
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
1035 for testcase in testcases:
1036 self.AddTestcase(testcase)
1038 # def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
1039 # return self.Iterate(scheme)
1041 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
1042 return self.Iterate(scheme)
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 )
1053 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
1054 tests, skipped, errored, weak, failed, passed = super().Aggregate()
1056 testcases = list(self._testcases.values())
1057 for testclass in self._testclasses.values():
1058 testcases.extend(testclass._testcases.values())
1060 for testcase in testcases:
1061 _ = testcase.Aggregate()
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}'.")
1082 self._tests = tests
1083 self._skipped = skipped
1084 self._errored = errored
1085 self._weak = weak
1086 self._failed = failed
1087 self._passed = passed
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
1103 return tests, skipped, errored, weak, failed, passed
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.
1109 If no scheme is given, use the default scheme.
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
1118 if IterationScheme.IncludeTestcases in scheme:
1119 for testcase in self._testcases.values():
1120 yield testcase
1122 for testclass in self._testclasses.values():
1123 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf)
1125 if IterationScheme.PostOrder in scheme:
1126 if IterationScheme.IncludeTestcases in scheme:
1127 for testcase in self._testcases.values():
1128 yield testcase
1130 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1131 yield self
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.
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 )
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
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
1163 if classname in juTestsuite._testclasses:
1164 juClass = juTestsuite._testclasses[classname]
1165 else:
1166 juClass = Testclass(classname, parent=juTestsuite)
1168 juClass.AddTestcase(Testcase.FromTestcase(tc))
1170 return juTestsuite
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 )
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)
1192 suite._kind = TestsuiteKind.Class
1193 if suite._parent is not testsuite:
1194 suite._parent._kind = TestsuiteKind.Module
1196 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values())
1198 return testsuite
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
1211 return node
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 )
1221@export
1222class TestsuiteSummary(TestsuiteBase):
1223 _testsuites: Dict[str, Testsuite]
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)
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.")
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}'.")
1244 testsuite._parent = self
1245 self._testsuites[testsuite._name] = testsuite
1247 @readonly
1248 def Testsuites(self) -> Dict[str, Testsuite]:
1249 """
1250 Read-only property to access the summary's testsuites.
1252 :returns: Dictionary of testsuite names and testsuites.
1253 """
1254 return self._testsuites
1256 @readonly
1257 def TestcaseCount(self) -> int:
1258 """
1259 Read-only property to return the number of testcases across all testsuites.
1261 :returns: Sum of the testsuites' testcase counts.
1262 """
1263 return sum(ts.TestcaseCount for ts in self._testsuites.values())
1265 @readonly
1266 def TestsuiteCount(self) -> int:
1267 """
1268 Read-only property to return the number of testsuites in this summary.
1270 :returns: Number of testsuites.
1271 """
1272 return len(self._testsuites)
1274 @readonly
1275 def AssertionCount(self) -> int:
1276 """
1277 Read-only property to return the number of assertions across all testsuites.
1279 :returns: Sum of the testsuites' assertion counts.
1280 """
1281 return sum(ts.AssertionCount for ts in self._testsuites.values())
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.")
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}'.")
1290 testsuite._parent = self
1291 self._testsuites[testsuite._name] = testsuite
1293 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None:
1294 for testsuite in testsuites:
1295 self.AddTestsuite(testsuite)
1297 def Aggregate(self) -> TestsuiteAggregateReturnType:
1298 tests, skipped, errored, weak, failed, passed = super().Aggregate()
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
1309 self._tests = tests
1310 self._skipped = skipped
1311 self._errored = errored
1312 self._weak = weak
1313 self._failed = failed
1314 self._passed = passed
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
1330 return tests, skipped, errored, weak, failed, passed
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.
1336 If no scheme is given, use the default scheme.
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
1344 for testsuite in self._testsuites.values():
1345 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1347 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme:
1348 yield self
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.
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 )
1366 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
1367 """
1368 Convert this test suite summary a new test suite summary of the unified data model.
1370 All fields are copied to the new instance. Child elements like test suites are copied recursively.
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 )
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
1390 return node
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 )
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
1407 _readerMode: JUnitReaderMode
1408 _xmlDocument: Nullable[_ElementTree]
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.
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)
1436 self._readerMode = readerMode
1437 self._xmlDocument = None
1439 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert)
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.
1446 The test suites become test suites of the dialect (:attr:`_TESTSUITE`). The counters are computed by
1447 :meth:`Aggregate`.
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()
1463 return doc
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.
1470 .. hint::
1472 The time spend for analysis will be made available via property :data:`AnalysisDuration`.
1474 The used XML schema definition is generic to support "any" dialect.
1475 """
1476 xmlSchemaFile = "Any-JUnit.xsd"
1477 self._Analyze(xmlSchemaFile)
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.")
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
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
1496 try:
1497 junitSchema = XMLSchema(schemaRoot)
1498 except XMLSchemaParseError as ex:
1499 raise UnittestError(f"Error while parsing XML Schema '{xmlSchemaFile}'.") from ex
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
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
1516 self._analysisDuration = sw.Duration
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.
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
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.")
1536 if regenerate:
1537 self.Generate(overwrite=True)
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
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
1551 def Convert(self) -> None:
1552 """
1553 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
1555 This method converts the root element.
1557 .. hint::
1559 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
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
1568 with Stopwatch() as sw:
1569 rootElement: _Element = self._xmlDocument.getroot()
1571 self._name = self._ConvertName(rootElement, optional=True)
1572 self._startTime = self._ConvertTimestamp(rootElement, optional=True)
1573 self._duration = self._ConvertTime(rootElement, optional=True)
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)
1582 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element
1583 self._ConvertTestsuite(self, rootNode)
1585 if True: # self._readerMode is JUnitReaderMode.
1586 self.Aggregate()
1588 self._modelConversion = sw.Duration
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.
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
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.
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
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.
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
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.
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
1658 def _ConvertClassname(self, element: _Element) -> str:
1659 """
1660 Convert the ``classname`` attribute from an XML element node to a string.
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}'.")
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.
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
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.
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
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.
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
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.
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
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.
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
1756 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
1757 """
1758 Convert the XML data structure of a ``<testsuite>`` to a test suite.
1760 This method uses private helper methods provided by the base-class.
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 )
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)
1780 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
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)
1790 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None:
1791 """
1792 Convert the XML data structure of a ``<testcase>`` to a test case.
1794 This method uses private helper methods provided by the base-class.
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)
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 )
1812 self._ConvertTestcaseChildren(testcaseNode, newTestcase)
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)
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.
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.
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
1867 if newTestcase._status is TestcaseStatus.Unknown:
1868 newTestcase._status = TestcaseStatus.Passed
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.
1874 An empty text (only whitespace) is not recorded.
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
1882 def Generate(self, overwrite: bool = False) -> None:
1883 """
1884 Generate the internal XML data structure from test suites and test cases.
1886 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods.
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.")
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}"
1907 self._xmlDocument = ElementTree(rootElement)
1909 for testsuite in self._testsuites.values():
1910 self._GenerateTestsuite(testsuite, rootElement)
1912 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None:
1913 """
1914 Generate the internal XML data structure for a test suite.
1916 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods.
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
1936 for tc in testsuite._testcases.values():
1937 self._GenerateTestcase(tc, testsuiteElement)
1939 for testclass in testsuite._testclasses.values():
1940 for tc in testclass._testcases.values():
1941 self._GenerateTestcase(tc, testsuiteElement)
1943 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
1944 """
1945 Generate the internal XML data structure for a test case.
1947 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
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}"
1961 self._GenerateTestcaseChildren(testcase, testcaseElement)
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.
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.
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")
1983 if statusElement is not None:
1984 if testcase._message is not None:
1985 statusElement.attrib["message"] = testcase._message
1987 if testcase._details is not None:
1988 statusElement.text = testcase._details
1990 if testcase._standardOutput is not None:
1991 SubElement(testcaseElement, "system-out").text = testcase._standardOutput
1993 if testcase._standardError is not None:
1994 SubElement(testcaseElement, "system-err").text = testcase._standardError
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 )