Coverage for pyEDAA / Reports / Unittesting / JUnit / __init__.py: 73%
724 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:27 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:27 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2024-2025 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 datetime import datetime, timedelta
94from enum import Flag
95from pathlib import Path
96from sys import version_info
97from time import perf_counter_ns
98from typing import Optional as Nullable, Iterable, Dict, Any, Generator, Tuple, Union, TypeVar, Type, ClassVar
100from lxml.etree import XMLParser, parse, XMLSchema, ElementTree, Element, SubElement, tostring
101from lxml.etree import XMLSyntaxError, _ElementTree, _Element, _Comment, XMLSchemaParseError
102from pyTooling.Common import getFullyQualifiedName, getResourceFile
103from pyTooling.Decorators import export, readonly
104from pyTooling.Exceptions import ToolingException
105from pyTooling.MetaClasses import ExtendedType, mustoverride, abstractmethod
106from pyTooling.Tree import Node
108from pyEDAA.Reports import Resources
109from pyEDAA.Reports.Unittesting import UnittestException, AlreadyInHierarchyException, DuplicateTestsuiteException, DuplicateTestcaseException
110from pyEDAA.Reports.Unittesting import TestcaseStatus, TestsuiteStatus, TestsuiteKind, IterationScheme
111from pyEDAA.Reports.Unittesting import Document as ut_Document, TestsuiteSummary as ut_TestsuiteSummary
112from pyEDAA.Reports.Unittesting import Testsuite as ut_Testsuite, Testcase as ut_Testcase
115@export
116class JUnitException:
117 """An exception-mixin for JUnit format specific exceptions."""
120@export
121class UnittestException(UnittestException, JUnitException):
122 pass
125@export
126class AlreadyInHierarchyException(AlreadyInHierarchyException, JUnitException):
127 """
128 A unit test exception raised if the element is already part of a hierarchy.
130 This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same
131 hierarchy should occur only once in the hierarchy.
133 .. hint::
135 This is usually caused by a non-None parent reference.
136 """
139@export
140class DuplicateTestsuiteException(DuplicateTestsuiteException, JUnitException):
141 """
142 A unit test exception raised on duplicate test suites (by name).
144 This exception is raised, if a child test suite with same name already exist in the test suite.
146 .. hint::
148 Test suite names need to be unique per parent element (test suite or test summary).
149 """
152@export
153class DuplicateTestcaseException(DuplicateTestcaseException, JUnitException):
154 """
155 A unit test exception raised on duplicate test cases (by name).
157 This exception is raised, if a child test case with same name already exist in the test suite.
159 .. hint::
161 Test case names need to be unique per parent element (test suite).
162 """
165@export
166class JUnitReaderMode(Flag):
167 Default = 0 #: Default behavior
168 DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented
171TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite")
172TestcaseAggregateReturnType = Tuple[int, int, int]
173TestsuiteAggregateReturnType = Tuple[int, int, int, int, int, int]
176@export
177class Base(metaclass=ExtendedType, slots=True):
178 """
179 Base-class for all test entities (test cases, test classes, test suites, ...).
181 It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity
182 hierarchy.
184 Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the
185 child. |br|
186 E.g. it's used as a test case name in the dictionary of test cases in a test class.
187 """
189 _parent: Nullable["Testsuite"]
190 _name: str
192 def __init__(self, name: str, parent: Nullable["Testsuite"] = None):
193 """
194 Initializes the fields of the base-class.
196 :param name: Name of the test entity.
197 :param parent: Reference to the parent test entity.
198 :raises ValueError: If parameter 'name' is None.
199 :raises TypeError: If parameter 'name' is not a string.
200 :raises ValueError: If parameter 'name' is empty.
201 """
202 if name is None: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true
203 raise ValueError(f"Parameter 'name' is None.")
204 elif not isinstance(name, str): 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 ex = TypeError(f"Parameter 'name' is not of type 'str'.")
206 if version_info >= (3, 11): # pragma: no cover
207 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
208 raise ex
209 elif name.strip() == "": 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true
210 raise ValueError(f"Parameter 'name' is empty.")
212 self._parent = parent
213 self._name = name
215 @readonly
216 def Parent(self) -> Nullable["Testsuite"]:
217 """
218 Read-only property returning the reference to the parent test entity.
220 :return: Reference to the parent entity.
221 """
222 return self._parent
224 # QUESTION: allow Parent as setter?
226 @readonly
227 def Name(self) -> str:
228 """
229 Read-only property returning the test entity's name.
231 :return:
232 """
233 return self._name
236@export
237class BaseWithProperties(Base):
238 """
239 Base-class for all test entities supporting properties (test cases, test suites, ...).
241 Every test entity has fields for the test duration and number of executed assertions.
243 Every test entity offers an internal dictionary for properties.
244 """
246 _duration: Nullable[timedelta]
247 _assertionCount: Nullable[int]
248 _properties: Dict[str, Any]
250 def __init__(
251 self,
252 name: str,
253 duration: Nullable[timedelta] = None,
254 assertionCount: Nullable[int] = None,
255 parent: Nullable["Testsuite"] = None
256 ):
257 """
258 Initializes the fields of the base-class.
260 :param name: Name of the test entity.
261 :param duration: Duration of the entity's execution.
262 :param assertionCount: Number of assertions within the test.
263 :param parent: Reference to the parent test entity.
264 :raises TypeError: If parameter 'duration' is not a timedelta.
265 :raises TypeError: If parameter 'assertionCount' is not an integer.
266 """
267 super().__init__(name, parent)
269 if duration is not None and not isinstance(duration, timedelta): 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true
270 ex = TypeError(f"Parameter 'duration' is not of type 'timedelta'.")
271 if version_info >= (3, 11): # pragma: no cover
272 ex.add_note(f"Got type '{getFullyQualifiedName(duration)}'.")
273 raise ex
275 if assertionCount is not None and not isinstance(assertionCount, int): 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.")
277 if version_info >= (3, 11): # pragma: no cover
278 ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.")
279 raise ex
281 self._duration = duration
282 self._assertionCount = assertionCount
284 self._properties = {}
286 @readonly
287 def Duration(self) -> timedelta:
288 """
289 Read-only property returning the duration of a test entity run.
291 .. note::
293 The JUnit format doesn't distinguish setup, run and teardown durations.
295 :return: Duration of the entity's execution.
296 """
297 return self._duration
299 @readonly
300 @abstractmethod
301 def AssertionCount(self) -> int:
302 """
303 Read-only property returning the number of assertions (checks) in a test case.
305 .. note::
307 The JUnit format doesn't distinguish passed and failed assertions.
309 :return: Number of assertions.
310 """
312 def __len__(self) -> int:
313 """
314 Returns the number of annotated properties.
316 Syntax: :pycode:`length = len(obj)`
318 :return: Number of annotated properties.
319 """
320 return len(self._properties)
322 def __getitem__(self, name: str) -> Any:
323 """
324 Access a property by name.
326 Syntax: :pycode:`value = obj[name]`
328 :param name: Name if the property.
329 :return: Value of the accessed property.
330 """
331 return self._properties[name]
333 def __setitem__(self, name: str, value: Any) -> None:
334 """
335 Set the value of a property by name.
337 If the property doesn't exist yet, it's created.
339 Syntax: :pycode:`obj[name] = value`
341 :param name: Name of the property.
342 :param value: Value of the property.
343 """
344 self._properties[name] = value
346 def __delitem__(self, name: str) -> None:
347 """
348 Delete a property by name.
350 Syntax: :pycode:`del obj[name]`
352 :param name: Name if the property.
353 """
354 del self._properties[name]
356 def __contains__(self, name: str) -> bool:
357 """
358 Returns True, if a property was annotated by this name.
360 Syntax: :pycode:`name in obj`
362 :param name: Name of the property.
363 :return: True, if the property was annotated.
364 """
365 return name in self._properties
367 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
368 """
369 Iterate all annotated properties.
371 Syntax: :pycode:`for name, value in obj:`
373 :return: A generator of property tuples (name, value).
374 """
375 yield from self._properties.items()
378@export
379class Testcase(BaseWithProperties):
380 """
381 A testcase is the leaf-entity in the test entity hierarchy representing an individual test run.
383 Test cases are grouped by test classes in the test entity hierarchy. These are again grouped by test suites. The root
384 of the hierarchy is a test summary.
386 Every test case has an overall status like unknown, skipped, failed or passed.
387 """
389 _status: TestcaseStatus
391 def __init__(
392 self,
393 name: str,
394 duration: Nullable[timedelta] = None,
395 status: TestcaseStatus = TestcaseStatus.Unknown,
396 assertionCount: Nullable[int] = None,
397 parent: Nullable["Testclass"] = None
398 ):
399 """
400 Initializes the fields of a test case.
402 :param name: Name of the test entity.
403 :param duration: Duration of the entity's execution.
404 :param status: Status of the test case.
405 :param assertionCount: Number of assertions within the test.
406 :param parent: Reference to the parent test class.
407 :raises TypeError: If parameter 'parent' is not a Testsuite.
408 :raises ValueError: If parameter 'assertionCount' is not consistent.
409 """
410 if parent is not None:
411 if not isinstance(parent, Testclass): 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 ex = TypeError(f"Parameter 'parent' is not of type 'Testclass'.")
413 if version_info >= (3, 11): # pragma: no cover
414 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
415 raise ex
417 parent._testcases[name] = self
419 super().__init__(name, duration, assertionCount, parent)
421 if not isinstance(status, TestcaseStatus): 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true
422 ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.")
423 if version_info >= (3, 11): # pragma: no cover
424 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.")
425 raise ex
427 self._status = status
429 @readonly
430 def Classname(self) -> str:
431 """
432 Read-only property returning the class name of the test case.
434 :return: The test case's class name.
436 .. note::
438 In the JUnit format, a test case is uniquely identified by a tuple of class name and test case name. This
439 structure has been decomposed by this data model into 2 leaf-levels in the test entity hierarchy. Thus, the class
440 name is represented by its own level and instances of test classes.
441 """
442 if self._parent is None:
443 raise UnittestException("Standalone Testcase instance is not linked to a Testclass.")
444 return self._parent._name
446 @readonly
447 def Status(self) -> TestcaseStatus:
448 """
449 Read-only property returning the status of the test case.
451 :return: The test case's status.
452 """
453 return self._status
455 @readonly
456 def AssertionCount(self) -> int:
457 """
458 Read-only property returning the number of assertions (checks) in a test case.
460 .. note::
462 The JUnit format doesn't distinguish passed and failed assertions.
464 :return: Number of assertions.
465 """
466 if self._assertionCount is None: 466 ↛ 468line 466 didn't jump to line 468 because the condition on line 466 was always true
467 return 0
468 return self._assertionCount
470 def Copy(self) -> "Testcase":
471 return self.__class__(
472 self._name,
473 self._duration,
474 self._status,
475 self._assertionCount
476 )
478 def Aggregate(self) -> None:
479 if self._status is TestcaseStatus.Unknown:
480 if self._assertionCount is None:
481 self._status = TestcaseStatus.Passed
482 elif self._assertionCount == 0: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true
483 self._status = TestcaseStatus.Weak
484 else:
485 self._status = TestcaseStatus.Failed
487 # TODO: check for setup errors
488 # TODO: check for teardown errors
490 @classmethod
491 def FromTestcase(cls, testcase: ut_Testcase) -> "Testcase":
492 """
493 Convert a test case of the unified test entity data model to the JUnit specific data model's test case object.
495 :param testcase: Test case from unified data model.
496 :return: Test case from JUnit specific data model.
497 """
498 return cls(
499 testcase._name,
500 duration=testcase._testDuration,
501 status= testcase._status,
502 assertionCount=testcase._assertionCount
503 )
505 def ToTestcase(self) -> ut_Testcase:
506 return ut_Testcase(
507 self._name,
508 testDuration=self._duration,
509 status=self._status,
510 assertionCount=self._assertionCount,
511 # TODO: as only assertions are recorded by JUnit files, all are marked as passed
512 passedAssertionCount=self._assertionCount
513 )
515 def ToTree(self) -> Node:
516 node = Node(value=self._name)
517 node["status"] = self._status
518 node["assertionCount"] = self._assertionCount
519 node["duration"] = self._duration
521 return node
523 def __str__(self) -> str:
524 moduleName = self.__module__.split(".")[-1]
525 className = self.__class__.__name__
526 return (
527 f"<{moduleName}{className} {self._name}: {self._status.name} - asserts:{self._assertionCount}>"
528 )
531@export
532class TestsuiteBase(BaseWithProperties):
533 """
534 Base-class for all test suites and for test summaries.
536 A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root
537 element in that hierarchy. While a test suite groups test classes, a test summary can only group test suites. Thus, a
538 test summary contains no test classes and test cases.
539 """
541 _startTime: Nullable[datetime]
542 _status: TestsuiteStatus
544 _tests: int
545 _skipped: int
546 _errored: int
547 _weak: int
548 _failed: int
549 _passed: int
551 def __init__(
552 self,
553 name: str,
554 startTime: Nullable[datetime] = None,
555 duration: Nullable[timedelta] = None,
556 status: TestsuiteStatus = TestsuiteStatus.Unknown,
557 parent: Nullable["Testsuite"] = None
558 ):
559 """
560 Initializes the based-class fields of a test suite or test summary.
562 :param name: Name of the test entity.
563 :param startTime: Time when the test entity was started.
564 :param duration: Duration of the entity's execution.
565 :param status: Overall status of the test entity.
566 :param parent: Reference to the parent test entity.
567 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
568 """
569 if parent is not None:
570 if not isinstance(parent, TestsuiteBase): 570 ↛ 571line 570 didn't jump to line 571 because the condition on line 570 was never true
571 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
572 if version_info >= (3, 11): # pragma: no cover
573 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
574 raise ex
576 parent._testsuites[name] = self
578 super().__init__(name, duration, None, parent)
580 self._startTime = startTime
581 self._status = status
582 self._tests = 0
583 self._skipped = 0
584 self._errored = 0
585 self._failed = 0
586 self._passed = 0
588 @readonly
589 def StartTime(self) -> Nullable[datetime]:
590 return self._startTime
592 @readonly
593 def Status(self) -> TestsuiteStatus:
594 return self._status
596 @readonly
597 @mustoverride
598 def TestcaseCount(self) -> int:
599 pass
601 @readonly
602 def Tests(self) -> int:
603 return self.TestcaseCount
605 @readonly
606 def Skipped(self) -> int:
607 return self._skipped
609 @readonly
610 def Errored(self) -> int:
611 return self._errored
613 @readonly
614 def Failed(self) -> int:
615 return self._failed
617 @readonly
618 def Passed(self) -> int:
619 return self._passed
621 def Aggregate(self) -> TestsuiteAggregateReturnType:
622 tests = 0
623 skipped = 0
624 errored = 0
625 weak = 0
626 failed = 0
627 passed = 0
629 # for testsuite in self._testsuites.values():
630 # t, s, e, w, f, p = testsuite.Aggregate()
631 # tests += t
632 # skipped += s
633 # errored += e
634 # weak += w
635 # failed += f
636 # passed += p
638 return tests, skipped, errored, weak, failed, passed
640 @mustoverride
641 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
642 pass
645@export
646class Testclass(Base):
647 """
648 A test class is a low-level element in the test entity hierarchy representing a group of tests.
650 Test classes contain test cases and are grouped by a test suites.
651 """
653 _testcases: Dict[str, "Testcase"]
655 def __init__(
656 self,
657 classname: str,
658 testcases: Nullable[Iterable["Testcase"]] = None,
659 parent: Nullable["Testsuite"] = None
660 ):
661 """
662 Initializes the fields of the test class.
664 :param classname: Classname of the test entity.
665 :param parent: Reference to the parent test suite.
666 :raises ValueError: If parameter 'classname' is None.
667 :raises TypeError: If parameter 'classname' is not a string.
668 :raises ValueError: If parameter 'classname' is empty.
669 """
670 if parent is not None:
671 if not isinstance(parent, Testsuite): 671 ↛ 672line 671 didn't jump to line 672 because the condition on line 671 was never true
672 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
673 if version_info >= (3, 11): # pragma: no cover
674 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
675 raise ex
677 parent._testclasses[classname] = self
679 super().__init__(classname, parent)
681 self._testcases = {}
682 if testcases is not None:
683 for testcase in testcases:
684 if testcase._parent is not None: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true
685 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
687 if testcase._name in self._testcases: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
690 testcase._parent = self
691 self._testcases[testcase._name] = testcase
693 @readonly
694 def Classname(self) -> str:
695 """
696 Read-only property returning the name of the test class.
698 :return: The test class' name.
699 """
700 return self._name
702 @readonly
703 def Testcases(self) -> Dict[str, "Testcase"]:
704 """
705 Read-only property returning a reference to the internal dictionary of test cases.
707 :return: Reference to the dictionary of test cases.
708 """
709 return self._testcases
711 @readonly
712 def TestcaseCount(self) -> int:
713 """
714 Read-only property returning the number of all test cases in the test entity hierarchy.
716 :return: Number of test cases.
717 """
718 return len(self._testcases)
720 @readonly
721 def AssertionCount(self) -> int:
722 return sum(tc.AssertionCount for tc in self._testcases.values())
724 def AddTestcase(self, testcase: "Testcase") -> None:
725 if testcase._parent is not None: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true
726 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
728 if testcase._name in self._testcases: 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true
729 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
731 testcase._parent = self
732 self._testcases[testcase._name] = testcase
734 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
735 for testcase in testcases:
736 self.AddTestcase(testcase)
738 def ToTestsuite(self) -> ut_Testsuite:
739 return ut_Testsuite(
740 self._name,
741 TestsuiteKind.Class,
742 # startTime=self._startTime,
743 # totalDuration=self._duration,
744 # status=self._status,
745 testcases=(tc.ToTestcase() for tc in self._testcases.values())
746 )
748 def ToTree(self) -> Node:
749 node = Node(
750 value=self._name,
751 children=(tc.ToTree() for tc in self._testcases.values())
752 )
754 return node
756 def __str__(self) -> str:
757 moduleName = self.__module__.split(".")[-1]
758 className = self.__class__.__name__
759 return (
760 f"<{moduleName}{className} {self._name}: {len(self._testcases)}>"
761 )
764@export
765class Testsuite(TestsuiteBase):
766 """
767 A testsuite is a mid-level element in the test entity hierarchy representing a logical group of tests.
769 Test suites contain test classes and are grouped by a test summary, which is the root of the hierarchy.
770 """
772 _hostname: str
773 _testclasses: Dict[str, "Testclass"]
775 def __init__(
776 self,
777 name: str,
778 hostname: Nullable[str] = None,
779 startTime: Nullable[datetime] = None,
780 duration: Nullable[timedelta] = None,
781 status: TestsuiteStatus = TestsuiteStatus.Unknown,
782 testclasses: Nullable[Iterable["Testclass"]] = None,
783 parent: Nullable["TestsuiteSummary"] = None
784 ):
785 """
786 Initializes the fields of a test suite.
788 :param name: Name of the test suite.
789 :param startTime: Time when the test suite was started.
790 :param duration: duration of the entity's execution.
791 :param status: Overall status of the test suite.
792 :param parent: Reference to the parent test summary.
793 :raises TypeError: If parameter 'testcases' is not iterable.
794 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
795 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
796 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
797 """
798 if parent is not None:
799 if not isinstance(parent, TestsuiteSummary): 799 ↛ 800line 799 didn't jump to line 800 because the condition on line 799 was never true
800 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteSummary'.")
801 if version_info >= (3, 11): # pragma: no cover
802 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
803 raise ex
805 parent._testsuites[name] = self
807 super().__init__(name, startTime, duration, status, parent)
809 self._hostname = hostname
811 self._testclasses = {}
812 if testclasses is not None:
813 for testclass in testclasses:
814 if testclass._parent is not None: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true
815 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
817 if testclass._name in self._testclasses: 817 ↛ 818line 817 didn't jump to line 818 because the condition on line 817 was never true
818 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
820 testclass._parent = self
821 self._testclasses[testclass._name] = testclass
823 @readonly
824 def Hostname(self) -> Nullable[str]:
825 return self._hostname
827 @readonly
828 def Testclasses(self) -> Dict[str, "Testclass"]:
829 return self._testclasses
831 @readonly
832 def TestclassCount(self) -> int:
833 return len(self._testclasses)
835 # @readonly
836 # def Testcases(self) -> Dict[str, "Testcase"]:
837 # return self._classes
839 @readonly
840 def TestcaseCount(self) -> int:
841 return sum(cls.TestcaseCount for cls in self._testclasses.values())
843 @readonly
844 def AssertionCount(self) -> int:
845 return sum(cls.AssertionCount for cls in self._testclasses.values())
847 def AddTestclass(self, testclass: "Testclass") -> None:
848 if testclass._parent is not None: 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true
849 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
851 if testclass._name in self._testclasses: 851 ↛ 852line 851 didn't jump to line 852 because the condition on line 851 was never true
852 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
854 testclass._parent = self
855 self._testclasses[testclass._name] = testclass
857 def AddTestclasses(self, testclasses: Iterable["Testclass"]) -> None:
858 for testcase in testclasses:
859 self.AddTestclass(testcase)
861 # def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
862 # return self.Iterate(scheme)
864 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
865 return self.Iterate(scheme)
867 def Copy(self) -> "Testsuite":
868 return self.__class__(
869 self._name,
870 self._hostname,
871 self._startTime,
872 self._duration,
873 self._status
874 )
876 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
877 tests, skipped, errored, weak, failed, passed = super().Aggregate()
879 for testclass in self._testclasses.values():
880 for testcase in testclass._testcases.values():
881 _ = testcase.Aggregate()
883 status = testcase._status
884 if status is TestcaseStatus.Unknown: 884 ↛ 885line 884 didn't jump to line 885 because the condition on line 884 was never true
885 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.")
886 elif status is TestcaseStatus.Skipped:
887 skipped += 1
888 elif status is TestcaseStatus.Errored: 888 ↛ 889line 888 didn't jump to line 889 because the condition on line 888 was never true
889 errored += 1
890 elif status is TestcaseStatus.Passed:
891 passed += 1
892 elif status is TestcaseStatus.Failed: 892 ↛ 894line 892 didn't jump to line 894 because the condition on line 892 was always true
893 failed += 1
894 elif status is TestcaseStatus.Weak:
895 weak += 1
896 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
897 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.")
898 else:
899 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.")
901 self._tests = tests
902 self._skipped = skipped
903 self._errored = errored
904 self._weak = weak
905 self._failed = failed
906 self._passed = passed
908 # FIXME: weak?
909 if errored > 0: 909 ↛ 910line 909 didn't jump to line 910 because the condition on line 909 was never true
910 self._status = TestsuiteStatus.Errored
911 elif failed > 0:
912 self._status = TestsuiteStatus.Failed
913 elif tests == 0: 913 ↛ 915line 913 didn't jump to line 915 because the condition on line 913 was always true
914 self._status = TestsuiteStatus.Empty
915 elif tests - skipped == passed:
916 self._status = TestsuiteStatus.Passed
917 elif tests == skipped:
918 self._status = TestsuiteStatus.Skipped
919 else:
920 self._status = TestsuiteStatus.Unknown
922 return tests, skipped, errored, weak, failed, passed
924 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
925 """
926 Iterate the test suite and its child elements according to the iteration scheme.
928 If no scheme is given, use the default scheme.
930 :param scheme: Scheme how to iterate the test suite and its child elements.
931 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
932 """
933 if IterationScheme.PreOrder in scheme:
934 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
935 yield self
937 if IterationScheme.IncludeTestcases in scheme:
938 for testcase in self._testclasses.values():
939 yield testcase
941 for testclass in self._testclasses.values():
942 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf)
944 if IterationScheme.PostOrder in scheme:
945 if IterationScheme.IncludeTestcases in scheme:
946 for testcase in self._testclasses.values():
947 yield testcase
949 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
950 yield self
952 @classmethod
953 def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite":
954 """
955 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object.
957 :param testsuite: Test suite from unified data model.
958 :return: Test suite from JUnit specific data model.
959 """
960 juTestsuite = cls(
961 testsuite._name,
962 startTime=testsuite._startTime,
963 duration=testsuite._totalDuration,
964 status= testsuite._status,
965 )
967 juTestsuite._tests = testsuite._tests
968 juTestsuite._skipped = testsuite._skipped
969 juTestsuite._errored = testsuite._errored
970 juTestsuite._failed = testsuite._failed
971 juTestsuite._passed = testsuite._passed
973 for tc in testsuite.IterateTestcases():
974 ts = tc._parent
975 if ts is None: 975 ↛ 976line 975 didn't jump to line 976 because the condition on line 975 was never true
976 raise UnittestException(f"Testcase '{tc._name}' is not part of a hierarchy.")
978 classname = ts._name
979 ts = ts._parent
980 while ts is not None and ts._kind > TestsuiteKind.Logical:
981 classname = f"{ts._name}.{classname}"
982 ts = ts._parent
984 if classname in juTestsuite._testclasses:
985 juClass = juTestsuite._testclasses[classname]
986 else:
987 juClass = Testclass(classname, parent=juTestsuite)
989 juClass.AddTestcase(Testcase.FromTestcase(tc))
991 return juTestsuite
993 def ToTestsuite(self) -> ut_Testsuite:
994 testsuite = ut_Testsuite(
995 self._name,
996 TestsuiteKind.Logical,
997 startTime=self._startTime,
998 totalDuration=self._duration,
999 status=self._status,
1000 )
1002 for testclass in self._testclasses.values():
1003 suite = testsuite
1004 classpath = testclass._name.split(".")
1005 for element in classpath:
1006 if element in suite._testsuites:
1007 suite = suite._testsuites[element]
1008 else:
1009 suite = ut_Testsuite(element, kind=TestsuiteKind.Package, parent=suite)
1011 suite._kind = TestsuiteKind.Class
1012 if suite._parent is not testsuite: 1012 ↛ 1015line 1012 didn't jump to line 1015 because the condition on line 1012 was always true
1013 suite._parent._kind = TestsuiteKind.Module
1015 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values())
1017 return testsuite
1019 def ToTree(self) -> Node:
1020 node = Node(
1021 value=self._name,
1022 children=(cls.ToTree() for cls in self._testclasses.values())
1023 )
1024 node["startTime"] = self._startTime
1025 node["duration"] = self._duration
1027 return node
1029 def __str__(self) -> str:
1030 moduleName = self.__module__.split(".")[-1]
1031 className = self.__class__.__name__
1032 return (
1033 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1034 )
1037@export
1038class TestsuiteSummary(TestsuiteBase):
1039 _testsuites: Dict[str, Testsuite]
1041 def __init__(
1042 self,
1043 name: str,
1044 startTime: Nullable[datetime] = None,
1045 duration: Nullable[timedelta] = None,
1046 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1047 testsuites: Nullable[Iterable[Testsuite]] = None
1048 ):
1049 super().__init__(name, startTime, duration, status, None)
1051 self._testsuites = {}
1052 if testsuites is not None:
1053 for testsuite in testsuites:
1054 if testsuite._parent is not None: 1054 ↛ 1055line 1054 didn't jump to line 1055 because the condition on line 1054 was never true
1055 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1057 if testsuite._name in self._testsuites: 1057 ↛ 1058line 1057 didn't jump to line 1058 because the condition on line 1057 was never true
1058 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1060 testsuite._parent = self
1061 self._testsuites[testsuite._name] = testsuite
1063 @readonly
1064 def Testsuites(self) -> Dict[str, Testsuite]:
1065 return self._testsuites
1067 @readonly
1068 def TestcaseCount(self) -> int:
1069 return sum(ts.TestcaseCount for ts in self._testsuites.values())
1071 @readonly
1072 def TestsuiteCount(self) -> int:
1073 return len(self._testsuites)
1075 @readonly
1076 def AssertionCount(self) -> int:
1077 return sum(ts.AssertionCount for ts in self._testsuites.values())
1079 def AddTestsuite(self, testsuite: Testsuite) -> None:
1080 if testsuite._parent is not None: 1080 ↛ 1081line 1080 didn't jump to line 1081 because the condition on line 1080 was never true
1081 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1083 if testsuite._name in self._testsuites: 1083 ↛ 1084line 1083 didn't jump to line 1084 because the condition on line 1083 was never true
1084 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1086 testsuite._parent = self
1087 self._testsuites[testsuite._name] = testsuite
1089 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None:
1090 for testsuite in testsuites:
1091 self.AddTestsuite(testsuite)
1093 def Aggregate(self) -> TestsuiteAggregateReturnType:
1094 tests, skipped, errored, weak, failed, passed = super().Aggregate()
1096 for testsuite in self._testsuites.values():
1097 t, s, e, w, f, p = testsuite.Aggregate()
1098 tests += t
1099 skipped += s
1100 errored += e
1101 weak += w
1102 failed += f
1103 passed += p
1105 self._tests = tests
1106 self._skipped = skipped
1107 self._errored = errored
1108 self._weak = weak
1109 self._failed = failed
1110 self._passed = passed
1112 # FIXME: weak
1113 if errored > 0: 1113 ↛ 1114line 1113 didn't jump to line 1114 because the condition on line 1113 was never true
1114 self._status = TestsuiteStatus.Errored
1115 elif failed > 0:
1116 self._status = TestsuiteStatus.Failed
1117 elif tests == 0: 1117 ↛ 1119line 1117 didn't jump to line 1119 because the condition on line 1117 was always true
1118 self._status = TestsuiteStatus.Empty
1119 elif tests - skipped == passed:
1120 self._status = TestsuiteStatus.Passed
1121 elif tests == skipped:
1122 self._status = TestsuiteStatus.Skipped
1123 else:
1124 self._status = TestsuiteStatus.Unknown
1126 return tests, skipped, errored, weak, failed, passed
1128 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[Testsuite, Testcase], None, None]:
1129 """
1130 Iterate the test suite summary and its child elements according to the iteration scheme.
1132 If no scheme is given, use the default scheme.
1134 :param scheme: Scheme how to iterate the test suite summary and its child elements.
1135 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
1136 """
1137 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1138 yield self
1140 for testsuite in self._testsuites.values():
1141 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1143 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme:
1144 yield self
1146 @classmethod
1147 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> "TestsuiteSummary":
1148 """
1149 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite.
1151 :param testsuiteSummary: Test suite summary from unified data model.
1152 :return: Test suite summary from JUnit specific data model.
1153 """
1154 return cls(
1155 testsuiteSummary._name,
1156 startTime=testsuiteSummary._startTime,
1157 duration=testsuiteSummary._totalDuration,
1158 status=testsuiteSummary._status,
1159 testsuites=(ut_Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1160 )
1162 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
1163 """
1164 Convert this test suite summary a new test suite summary of the unified data model.
1166 All fields are copied to the new instance. Child elements like test suites are copied recursively.
1168 :return: A test suite summary of the unified test entity data model.
1169 """
1170 return ut_TestsuiteSummary(
1171 self._name,
1172 startTime=self._startTime,
1173 totalDuration=self._duration,
1174 status=self._status,
1175 testsuites=(testsuite.ToTestsuite() for testsuite in self._testsuites.values())
1176 )
1178 def ToTree(self) -> Node:
1179 node = Node(
1180 value=self._name,
1181 children=(ts.ToTree() for ts in self._testsuites.values())
1182 )
1183 node["startTime"] = self._startTime
1184 node["duration"] = self._duration
1186 return node
1188 def __str__(self) -> str:
1189 moduleName = self.__module__.split(".")[-1]
1190 className = self.__class__.__name__
1191 return (
1192 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1193 )
1196@export
1197class Document(TestsuiteSummary, ut_Document):
1198 _TESTCASE: ClassVar[Type[Testcase]] = Testcase
1199 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass
1200 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite
1202 _readerMode: JUnitReaderMode
1203 _xmlDocument: Nullable[_ElementTree]
1205 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default):
1206 super().__init__("Unprocessed JUnit XML file")
1208 self._readerMode = readerMode
1209 self._xmlDocument = None
1211 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert)
1213 @classmethod
1214 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary):
1215 doc = cls(xmlReportFile)
1216 doc._name = testsuiteSummary._name
1217 doc._startTime = testsuiteSummary._startTime
1218 doc._duration = testsuiteSummary._totalDuration
1219 doc._status = testsuiteSummary._status
1220 doc._tests = testsuiteSummary._tests
1221 doc._skipped = testsuiteSummary._skipped
1222 doc._errored = testsuiteSummary._errored
1223 doc._failed = testsuiteSummary._failed
1224 doc._passed = testsuiteSummary._passed
1226 doc.AddTestsuites(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1228 return doc
1230 def Analyze(self) -> None:
1231 """
1232 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML
1233 schema.
1235 .. hint::
1237 The time spend for analysis will be made available via property :data:`AnalysisDuration`.
1239 The used XML schema definition is generic to support "any" dialect.
1240 """
1241 xmlSchemaFile = "Any-JUnit.xsd"
1242 self._Analyze(xmlSchemaFile)
1244 def _Analyze(self, xmlSchemaFile: str) -> None:
1245 if not self._path.exists(): 1245 ↛ 1246line 1245 didn't jump to line 1246 because the condition on line 1245 was never true
1246 raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \
1247 from FileNotFoundError(f"File '{self._path}' not found.")
1249 startAnalysis = perf_counter_ns()
1250 try:
1251 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile)
1252 except ToolingException as ex:
1253 raise UnittestException(f"Couldn't locate XML Schema '{xmlSchemaFile}' in package resources.") from ex
1255 try:
1256 schemaParser = XMLParser(ns_clean=True)
1257 schemaRoot = parse(xmlSchemaResourceFile, schemaParser)
1258 except XMLSyntaxError as ex:
1259 raise UnittestException(f"XML Syntax Error while parsing XML Schema '{xmlSchemaFile}'.") from ex
1261 try:
1262 junitSchema = XMLSchema(schemaRoot)
1263 except XMLSchemaParseError as ex:
1264 raise UnittestException(f"Error while parsing XML Schema '{xmlSchemaFile}'.")
1266 try:
1267 junitParser = XMLParser(schema=junitSchema, ns_clean=True)
1268 junitDocument = parse(self._path, parser=junitParser)
1270 self._xmlDocument = junitDocument
1271 except XMLSyntaxError as ex:
1272 if version_info >= (3, 11): # pragma: no cover
1273 for logEntry in junitParser.error_log:
1274 ex.add_note(str(logEntry))
1275 raise UnittestException(f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'.") from ex
1276 except Exception as ex:
1277 raise UnittestException(f"Couldn't open '{self._path}'.") from ex
1279 endAnalysis = perf_counter_ns()
1280 self._analysisDuration = (endAnalysis - startAnalysis) / 1e9
1282 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None:
1283 """
1284 Write the data model as XML into a file adhering to the Any JUnit dialect.
1286 :param path: Optional path to the XMl file, if internal path shouldn't be used.
1287 :param overwrite: If true, overwrite an existing file.
1288 :param regenerate: If true, regenerate the XML structure from data model.
1289 :raises UnittestException: If the file cannot be overwritten.
1290 :raises UnittestException: If the internal XML data structure wasn't generated.
1291 :raises UnittestException: If the file cannot be opened or written.
1292 """
1293 if path is None:
1294 path = self._path
1296 if not overwrite and path.exists(): 1296 ↛ 1297line 1296 didn't jump to line 1297 because the condition on line 1296 was never true
1297 raise UnittestException(f"JUnit XML file '{path}' can not be overwritten.") \
1298 from FileExistsError(f"File '{path}' already exists.")
1300 if regenerate:
1301 self.Generate(overwrite=True)
1303 if self._xmlDocument is None: 1303 ↛ 1304line 1303 didn't jump to line 1304 because the condition on line 1303 was never true
1304 ex = UnittestException(f"Internal XML document tree is empty and needs to be generated before write is possible.")
1305 ex.add_note(f"Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.")
1306 raise ex
1308 try:
1309 with path.open("wb") as file:
1310 file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True))
1311 except Exception as ex:
1312 raise UnittestException(f"JUnit XML file '{path}' can not be written.") from ex
1314 def Convert(self) -> None:
1315 """
1316 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
1318 This method converts the root element.
1320 .. hint::
1322 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
1324 :raises UnittestException: If XML was not read and parsed before.
1325 """
1326 if self._xmlDocument is None: 1326 ↛ 1327line 1326 didn't jump to line 1327 because the condition on line 1326 was never true
1327 ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.")
1328 ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.")
1329 raise ex
1331 startConversion = perf_counter_ns()
1332 rootElement: _Element = self._xmlDocument.getroot()
1334 self._name = self._ConvertName(rootElement, optional=True)
1335 self._startTime = self._ConvertTimestamp(rootElement, optional=True)
1336 self._duration = self._ConvertTime(rootElement, optional=True)
1338 if False: # self._readerMode is JUnitReaderMode.
1339 self._tests = self._ConvertTests(testsuitesNode)
1340 self._skipped = self._ConvertSkipped(testsuitesNode)
1341 self._errored = self._ConvertErrors(testsuitesNode)
1342 self._failed = self._ConvertFailures(testsuitesNode)
1343 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1345 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element
1346 self._ConvertTestsuite(self, rootNode)
1348 if True: # self._readerMode is JUnitReaderMode.
1349 self.Aggregate()
1351 endConversation = perf_counter_ns()
1352 self._modelConversion = (endConversation - startConversion) / 1e9
1354 def _ConvertName(self, element: _Element, default: str = "root", optional: bool = True) -> str:
1355 """
1356 Convert the ``name`` attribute from an XML element node to a string.
1358 :param element: The XML element node with a ``name`` attribute.
1359 :param default: The default value, if no ``name`` attribute was found.
1360 :param optional: If false, an exception is raised for the missing attribute.
1361 :return: The ``name`` attribute's content if found, otherwise the given default value.
1362 :raises UnittestException: If optional is false and no ``name`` attribute exists on the given element node.
1363 """
1364 if "name" in element.attrib:
1365 return element.attrib["name"]
1366 elif not optional: 1366 ↛ 1367line 1366 didn't jump to line 1367 because the condition on line 1366 was never true
1367 raise UnittestException(f"Required parameter 'name' not found in tag '{element.tag}'.")
1368 else:
1369 return default
1371 def _ConvertTimestamp(self, element: _Element, optional: bool = True) -> Nullable[datetime]:
1372 """
1373 Convert the ``timestamp`` attribute from an XML element node to a datetime.
1375 :param element: The XML element node with a ``timestamp`` attribute.
1376 :param optional: If false, an exception is raised for the missing attribute.
1377 :return: The ``timestamp`` attribute's content if found, otherwise ``None``.
1378 :raises UnittestException: If optional is false and no ``timestamp`` attribute exists on the given element node.
1379 """
1380 if "timestamp" in element.attrib:
1381 timestamp = element.attrib["timestamp"]
1382 return datetime.fromisoformat(timestamp)
1383 elif not optional: 1383 ↛ 1384line 1383 didn't jump to line 1384 because the condition on line 1383 was never true
1384 raise UnittestException(f"Required parameter 'timestamp' not found in tag '{element.tag}'.")
1385 else:
1386 return None
1388 def _ConvertTime(self, element: _Element, optional: bool = True) -> Nullable[timedelta]:
1389 """
1390 Convert the ``time`` attribute from an XML element node to a timedelta.
1392 :param element: The XML element node with a ``time`` attribute.
1393 :param optional: If false, an exception is raised for the missing attribute.
1394 :return: The ``time`` attribute's content if found, otherwise ``None``.
1395 :raises UnittestException: If optional is false and no ``time`` attribute exists on the given element node.
1396 """
1397 if "time" in element.attrib:
1398 time = element.attrib["time"]
1399 return timedelta(seconds=float(time))
1400 elif not optional: 1400 ↛ 1401line 1400 didn't jump to line 1401 because the condition on line 1400 was never true
1401 raise UnittestException(f"Required parameter 'time' not found in tag '{element.tag}'.")
1402 else:
1403 return None
1405 def _ConvertHostname(self, element: _Element, default: str = "localhost", optional: bool = True) -> str:
1406 """
1407 Convert the ``hostname`` attribute from an XML element node to a string.
1409 :param element: The XML element node with a ``hostname`` attribute.
1410 :param default: The default value, if no ``hostname`` attribute was found.
1411 :param optional: If false, an exception is raised for the missing attribute.
1412 :return: The ``hostname`` attribute's content if found, otherwise the given default value.
1413 :raises UnittestException: If optional is false and no ``hostname`` attribute exists on the given element node.
1414 """
1415 if "hostname" in element.attrib:
1416 return element.attrib["hostname"]
1417 elif not optional: 1417 ↛ 1418line 1417 didn't jump to line 1418 because the condition on line 1417 was never true
1418 raise UnittestException(f"Required parameter 'hostname' not found in tag '{element.tag}'.")
1419 else:
1420 return default
1422 def _ConvertClassname(self, element: _Element) -> str:
1423 """
1424 Convert the ``classname`` attribute from an XML element node to a string.
1426 :param element: The XML element node with a ``classname`` attribute.
1427 :return: The ``classname`` attribute's content.
1428 :raises UnittestException: If no ``classname`` attribute exists on the given element node.
1429 """
1430 if "classname" in element.attrib: 1430 ↛ 1433line 1430 didn't jump to line 1433 because the condition on line 1430 was always true
1431 return element.attrib["classname"]
1432 else:
1433 raise UnittestException(f"Required parameter 'classname' not found in tag '{element.tag}'.")
1435 def _ConvertTests(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1436 """
1437 Convert the ``tests`` attribute from an XML element node to an integer.
1439 :param element: The XML element node with a ``tests`` attribute.
1440 :param default: The default value, if no ``tests`` attribute was found.
1441 :param optional: If false, an exception is raised for the missing attribute.
1442 :return: The ``tests`` attribute's content if found, otherwise the given default value.
1443 :raises UnittestException: If optional is false and no ``tests`` attribute exists on the given element node.
1444 """
1445 if "tests" in element.attrib:
1446 return int(element.attrib["tests"])
1447 elif not optional:
1448 raise UnittestException(f"Required parameter 'tests' not found in tag '{element.tag}'.")
1449 else:
1450 return default
1452 def _ConvertSkipped(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1453 """
1454 Convert the ``skipped`` attribute from an XML element node to an integer.
1456 :param element: The XML element node with a ``skipped`` attribute.
1457 :param default: The default value, if no ``skipped`` attribute was found.
1458 :param optional: If false, an exception is raised for the missing attribute.
1459 :return: The ``skipped`` attribute's content if found, otherwise the given default value.
1460 :raises UnittestException: If optional is false and no ``skipped`` attribute exists on the given element node.
1461 """
1462 if "skipped" in element.attrib:
1463 return int(element.attrib["skipped"])
1464 elif not optional:
1465 raise UnittestException(f"Required parameter 'skipped' not found in tag '{element.tag}'.")
1466 else:
1467 return default
1469 def _ConvertErrors(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1470 """
1471 Convert the ``errors`` attribute from an XML element node to an integer.
1473 :param element: The XML element node with a ``errors`` attribute.
1474 :param default: The default value, if no ``errors`` attribute was found.
1475 :param optional: If false, an exception is raised for the missing attribute.
1476 :return: The ``errors`` attribute's content if found, otherwise the given default value.
1477 :raises UnittestException: If optional is false and no ``errors`` attribute exists on the given element node.
1478 """
1479 if "errors" in element.attrib:
1480 return int(element.attrib["errors"])
1481 elif not optional:
1482 raise UnittestException(f"Required parameter 'errors' not found in tag '{element.tag}'.")
1483 else:
1484 return default
1486 def _ConvertFailures(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1487 """
1488 Convert the ``failures`` attribute from an XML element node to an integer.
1490 :param element: The XML element node with a ``failures`` attribute.
1491 :param default: The default value, if no ``failures`` attribute was found.
1492 :param optional: If false, an exception is raised for the missing attribute.
1493 :return: The ``failures`` attribute's content if found, otherwise the given default value.
1494 :raises UnittestException: If optional is false and no ``failures`` attribute exists on the given element node.
1495 """
1496 if "failures" in element.attrib:
1497 return int(element.attrib["failures"])
1498 elif not optional:
1499 raise UnittestException(f"Required parameter 'failures' not found in tag '{element.tag}'.")
1500 else:
1501 return default
1503 def _ConvertAssertions(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1504 """
1505 Convert the ``assertions`` attribute from an XML element node to an integer.
1507 :param element: The XML element node with a ``assertions`` attribute.
1508 :param default: The default value, if no ``assertions`` attribute was found.
1509 :param optional: If false, an exception is raised for the missing attribute.
1510 :return: The ``assertions`` attribute's content if found, otherwise the given default value.
1511 :raises UnittestException: If optional is false and no ``assertions`` attribute exists on the given element node.
1512 """
1513 if "assertions" in element.attrib:
1514 return int(element.attrib["assertions"])
1515 elif not optional: 1515 ↛ 1516line 1515 didn't jump to line 1516 because the condition on line 1515 was never true
1516 raise UnittestException(f"Required parameter 'assertions' not found in tag '{element.tag}'.")
1517 else:
1518 return default
1520 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
1521 """
1522 Convert the XML data structure of a ``<testsuite>`` to a test suite.
1524 This method uses private helper methods provided by the base-class.
1526 :param parent: The test suite summary as a parent element in the test entity hierarchy.
1527 :param testsuitesNode: The current XML element node representing a test suite.
1528 """
1529 newTestsuite = self._TESTSUITE(
1530 self._ConvertName(testsuitesNode, optional=False),
1531 self._ConvertHostname(testsuitesNode, optional=True),
1532 self._ConvertTimestamp(testsuitesNode, optional=True),
1533 self._ConvertTime(testsuitesNode, optional=True),
1534 parent=parent
1535 )
1537 if False: # self._readerMode is JUnitReaderMode.
1538 self._tests = self._ConvertTests(testsuitesNode)
1539 self._skipped = self._ConvertSkipped(testsuitesNode)
1540 self._errored = self._ConvertErrors(testsuitesNode)
1541 self._failed = self._ConvertFailures(testsuitesNode)
1542 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1544 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
1546 def _ConvertTestsuiteChildren(self, testsuitesNode: _Element, newTestsuite: Testsuite) -> None:
1547 for node in testsuitesNode.iterchildren(): # type: _Element
1548 # if node.tag == "testsuite":
1549 # self._ConvertTestsuite(newTestsuite, node)
1550 # el
1551 if node.tag == "testcase":
1552 self._ConvertTestcase(newTestsuite, node)
1554 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None:
1555 """
1556 Convert the XML data structure of a ``<testcase>`` to a test case.
1558 This method uses private helper methods provided by the base-class.
1560 :param parent: The test suite as a parent element in the test entity hierarchy.
1561 :param testcaseNode: The current XML element node representing a test case.
1562 """
1563 className = self._ConvertClassname(testcaseNode)
1564 testclass = self._FindOrCreateTestclass(parent, className)
1566 newTestcase = self._TESTCASE(
1567 self._ConvertName(testcaseNode, optional=False),
1568 self._ConvertTime(testcaseNode, optional=False),
1569 assertionCount=self._ConvertAssertions(testcaseNode),
1570 parent=testclass
1571 )
1573 self._ConvertTestcaseChildren(testcaseNode, newTestcase)
1575 def _FindOrCreateTestclass(self, parent: Testsuite, className: str) -> Testclass:
1576 if className in parent._testclasses:
1577 return parent._testclasses[className]
1578 else:
1579 return self._TESTCLASS(className, parent=parent)
1581 def _ConvertTestcaseChildren(self, testcaseNode: _Element, newTestcase: Testcase) -> None:
1582 for node in testcaseNode.iterchildren(): # type: _Element
1583 if isinstance(node, _Comment): 1583 ↛ 1584line 1583 didn't jump to line 1584 because the condition on line 1583 was never true
1584 pass
1585 elif isinstance(node, _Element): 1585 ↛ 1601line 1585 didn't jump to line 1601 because the condition on line 1585 was always true
1586 if node.tag == "skipped":
1587 newTestcase._status = TestcaseStatus.Skipped
1588 elif node.tag == "failure":
1589 newTestcase._status = TestcaseStatus.Failed
1590 elif node.tag == "error": 1590 ↛ 1591line 1590 didn't jump to line 1591 because the condition on line 1590 was never true
1591 newTestcase._status = TestcaseStatus.Errored
1592 elif node.tag == "system-out":
1593 pass
1594 elif node.tag == "system-err":
1595 pass
1596 elif node.tag == "properties": 1596 ↛ 1599line 1596 didn't jump to line 1599 because the condition on line 1596 was always true
1597 pass
1598 else:
1599 raise UnittestException(f"Unknown element '{node.tag}' in junit file.")
1600 else:
1601 pass
1603 if newTestcase._status is TestcaseStatus.Unknown:
1604 newTestcase._status = TestcaseStatus.Passed
1606 def Generate(self, overwrite: bool = False) -> None:
1607 """
1608 Generate the internal XML data structure from test suites and test cases.
1610 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods.
1612 :param overwrite: Overwrite the internal XML data structure.
1613 :raises UnittestException: If overwrite is false and the internal XML data structure is not empty.
1614 """
1615 if not overwrite and self._xmlDocument is not None: 1615 ↛ 1616line 1615 didn't jump to line 1616 because the condition on line 1615 was never true
1616 raise UnittestException(f"Internal XML document is populated with data.")
1618 rootElement = Element("testsuites")
1619 rootElement.attrib["name"] = self._name
1620 if self._startTime is not None:
1621 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}"
1622 if self._duration is not None:
1623 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}"
1624 rootElement.attrib["tests"] = str(self._tests)
1625 rootElement.attrib["failures"] = str(self._failed)
1626 rootElement.attrib["errors"] = str(self._errored)
1627 rootElement.attrib["skipped"] = str(self._skipped)
1628 # if self._assertionCount is not None:
1629 # rootElement.attrib["assertions"] = f"{self._assertionCount}"
1631 self._xmlDocument = ElementTree(rootElement)
1633 for testsuite in self._testsuites.values():
1634 self._GenerateTestsuite(testsuite, rootElement)
1636 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None:
1637 """
1638 Generate the internal XML data structure for a test suite.
1640 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods.
1642 :param testsuite: The test suite to convert to an XML data structures.
1643 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1644 :return:
1645 """
1646 testsuiteElement = SubElement(parentElement, "testsuite")
1647 testsuiteElement.attrib["name"] = testsuite._name
1648 if testsuite._startTime is not None: 1648 ↛ 1650line 1648 didn't jump to line 1650 because the condition on line 1648 was always true
1649 testsuiteElement.attrib["timestamp"] = f"{testsuite._startTime.isoformat()}"
1650 if testsuite._duration is not None:
1651 testsuiteElement.attrib["time"] = f"{testsuite._duration.total_seconds():.6f}"
1652 testsuiteElement.attrib["tests"] = str(testsuite._tests)
1653 testsuiteElement.attrib["failures"] = str(testsuite._failed)
1654 testsuiteElement.attrib["errors"] = str(testsuite._errored)
1655 testsuiteElement.attrib["skipped"] = str(testsuite._skipped)
1656 # if testsuite._assertionCount is not None:
1657 # testsuiteElement.attrib["assertions"] = f"{testsuite._assertionCount}"
1658 if testsuite._hostname is not None: 1658 ↛ 1659line 1658 didn't jump to line 1659 because the condition on line 1658 was never true
1659 testsuiteElement.attrib["hostname"] = testsuite._hostname
1661 for testclass in testsuite._testclasses.values():
1662 for tc in testclass._testcases.values():
1663 self._GenerateTestcase(tc, testsuiteElement)
1665 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
1666 """
1667 Generate the internal XML data structure for a test case.
1669 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
1671 :param testcase: The test case to convert to an XML data structures.
1672 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1673 :return:
1674 """
1675 testcaseElement = SubElement(parentElement, "testcase")
1676 if testcase.Classname is not None: 1676 ↛ 1678line 1676 didn't jump to line 1678 because the condition on line 1676 was always true
1677 testcaseElement.attrib["classname"] = testcase.Classname
1678 testcaseElement.attrib["name"] = testcase._name
1679 if testcase._duration is not None: 1679 ↛ 1681line 1679 didn't jump to line 1681 because the condition on line 1679 was always true
1680 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}"
1681 if testcase._assertionCount is not None:
1682 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}"
1684 if testcase._status is TestcaseStatus.Passed:
1685 pass
1686 elif testcase._status is TestcaseStatus.Failed:
1687 failureElement = SubElement(testcaseElement, "failure")
1688 elif testcase._status is TestcaseStatus.Skipped: 1688 ↛ 1691line 1688 didn't jump to line 1691 because the condition on line 1688 was always true
1689 skippedElement = SubElement(testcaseElement, "skipped")
1690 else:
1691 errorElement = SubElement(testcaseElement, "error")
1693 def __str__(self) -> str:
1694 moduleName = self.__module__.split(".")[-1]
1695 className = self.__class__.__name__
1696 return (
1697 f"<{moduleName}{className} {self._name} ({self._path}): {self._status.name} - suites/tests:{self.TestsuiteCount}/{self.TestcaseCount}>"
1698 )