Coverage for pyEDAA/Reports/Unittesting/JUnit/__init__.py: 73%
725 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:23 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:23 +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 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme
935 if IterationScheme.PreOrder in scheme:
936 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
937 yield self
939 if IterationScheme.IncludeTestcases in scheme:
940 for testcase in self._testclasses.values():
941 yield testcase
943 for testclass in self._testclasses.values():
944 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf)
946 if IterationScheme.PostOrder in scheme:
947 if IterationScheme.IncludeTestcases in scheme:
948 for testcase in self._testclasses.values():
949 yield testcase
951 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
952 yield self
954 @classmethod
955 def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite":
956 """
957 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object.
959 :param testsuite: Test suite from unified data model.
960 :return: Test suite from JUnit specific data model.
961 """
962 juTestsuite = cls(
963 testsuite._name,
964 startTime=testsuite._startTime,
965 duration=testsuite._totalDuration,
966 status= testsuite._status,
967 )
969 juTestsuite._tests = testsuite._tests
970 juTestsuite._skipped = testsuite._skipped
971 juTestsuite._errored = testsuite._errored
972 juTestsuite._failed = testsuite._failed
973 juTestsuite._passed = testsuite._passed
975 for tc in testsuite.IterateTestcases():
976 ts = tc._parent
977 if ts is None: 977 ↛ 978line 977 didn't jump to line 978 because the condition on line 977 was never true
978 raise UnittestException(f"Testcase '{tc._name}' is not part of a hierarchy.")
980 classname = ts._name
981 ts = ts._parent
982 while ts is not None and ts._kind > TestsuiteKind.Logical:
983 classname = f"{ts._name}.{classname}"
984 ts = ts._parent
986 if classname in juTestsuite._testclasses:
987 juClass = juTestsuite._testclasses[classname]
988 else:
989 juClass = Testclass(classname, parent=juTestsuite)
991 juClass.AddTestcase(Testcase.FromTestcase(tc))
993 return juTestsuite
995 def ToTestsuite(self) -> ut_Testsuite:
996 testsuite = ut_Testsuite(
997 self._name,
998 TestsuiteKind.Logical,
999 startTime=self._startTime,
1000 totalDuration=self._duration,
1001 status=self._status,
1002 )
1004 for testclass in self._testclasses.values():
1005 suite = testsuite
1006 classpath = testclass._name.split(".")
1007 for element in classpath:
1008 if element in suite._testsuites:
1009 suite = suite._testsuites[element]
1010 else:
1011 suite = ut_Testsuite(element, kind=TestsuiteKind.Package, parent=suite)
1013 suite._kind = TestsuiteKind.Class
1014 if suite._parent is not testsuite: 1014 ↛ 1017line 1014 didn't jump to line 1017 because the condition on line 1014 was always true
1015 suite._parent._kind = TestsuiteKind.Module
1017 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values())
1019 return testsuite
1021 def ToTree(self) -> Node:
1022 node = Node(
1023 value=self._name,
1024 children=(cls.ToTree() for cls in self._testclasses.values())
1025 )
1026 node["startTime"] = self._startTime
1027 node["duration"] = self._duration
1029 return node
1031 def __str__(self) -> str:
1032 moduleName = self.__module__.split(".")[-1]
1033 className = self.__class__.__name__
1034 return (
1035 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1036 )
1039@export
1040class TestsuiteSummary(TestsuiteBase):
1041 _testsuites: Dict[str, Testsuite]
1043 def __init__(
1044 self,
1045 name: str,
1046 startTime: Nullable[datetime] = None,
1047 duration: Nullable[timedelta] = None,
1048 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1049 testsuites: Nullable[Iterable[Testsuite]] = None
1050 ):
1051 super().__init__(name, startTime, duration, status, None)
1053 self._testsuites = {}
1054 if testsuites is not None:
1055 for testsuite in testsuites:
1056 if testsuite._parent is not None: 1056 ↛ 1057line 1056 didn't jump to line 1057 because the condition on line 1056 was never true
1057 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1059 if testsuite._name in self._testsuites: 1059 ↛ 1060line 1059 didn't jump to line 1060 because the condition on line 1059 was never true
1060 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1062 testsuite._parent = self
1063 self._testsuites[testsuite._name] = testsuite
1065 @readonly
1066 def Testsuites(self) -> Dict[str, Testsuite]:
1067 return self._testsuites
1069 @readonly
1070 def TestcaseCount(self) -> int:
1071 return sum(ts.TestcaseCount for ts in self._testsuites.values())
1073 @readonly
1074 def TestsuiteCount(self) -> int:
1075 return len(self._testsuites)
1077 @readonly
1078 def AssertionCount(self) -> int:
1079 return sum(ts.AssertionCount for ts in self._testsuites.values())
1081 def AddTestsuite(self, testsuite: Testsuite) -> None:
1082 if testsuite._parent is not None: 1082 ↛ 1083line 1082 didn't jump to line 1083 because the condition on line 1082 was never true
1083 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1085 if testsuite._name in self._testsuites: 1085 ↛ 1086line 1085 didn't jump to line 1086 because the condition on line 1085 was never true
1086 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1088 testsuite._parent = self
1089 self._testsuites[testsuite._name] = testsuite
1091 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None:
1092 for testsuite in testsuites:
1093 self.AddTestsuite(testsuite)
1095 def Aggregate(self) -> TestsuiteAggregateReturnType:
1096 tests, skipped, errored, weak, failed, passed = super().Aggregate()
1098 for testsuite in self._testsuites.values():
1099 t, s, e, w, f, p = testsuite.Aggregate()
1100 tests += t
1101 skipped += s
1102 errored += e
1103 weak += w
1104 failed += f
1105 passed += p
1107 self._tests = tests
1108 self._skipped = skipped
1109 self._errored = errored
1110 self._weak = weak
1111 self._failed = failed
1112 self._passed = passed
1114 # FIXME: weak
1115 if errored > 0: 1115 ↛ 1116line 1115 didn't jump to line 1116 because the condition on line 1115 was never true
1116 self._status = TestsuiteStatus.Errored
1117 elif failed > 0:
1118 self._status = TestsuiteStatus.Failed
1119 elif tests == 0: 1119 ↛ 1121line 1119 didn't jump to line 1121 because the condition on line 1119 was always true
1120 self._status = TestsuiteStatus.Empty
1121 elif tests - skipped == passed:
1122 self._status = TestsuiteStatus.Passed
1123 elif tests == skipped:
1124 self._status = TestsuiteStatus.Skipped
1125 else:
1126 self._status = TestsuiteStatus.Unknown
1128 return tests, skipped, errored, weak, failed, passed
1130 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[Testsuite, Testcase], None, None]:
1131 """
1132 Iterate the test suite summary and its child elements according to the iteration scheme.
1134 If no scheme is given, use the default scheme.
1136 :param scheme: Scheme how to iterate the test suite summary and its child elements.
1137 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
1138 """
1139 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1140 yield self
1142 for testsuite in self._testsuites.values():
1143 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1145 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme:
1146 yield self
1148 @classmethod
1149 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> "TestsuiteSummary":
1150 """
1151 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite.
1153 :param testsuiteSummary: Test suite summary from unified data model.
1154 :return: Test suite summary from JUnit specific data model.
1155 """
1156 return cls(
1157 testsuiteSummary._name,
1158 startTime=testsuiteSummary._startTime,
1159 duration=testsuiteSummary._totalDuration,
1160 status=testsuiteSummary._status,
1161 testsuites=(ut_Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1162 )
1164 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
1165 """
1166 Convert this test suite summary a new test suite summary of the unified data model.
1168 All fields are copied to the new instance. Child elements like test suites are copied recursively.
1170 :return: A test suite summary of the unified test entity data model.
1171 """
1172 return ut_TestsuiteSummary(
1173 self._name,
1174 startTime=self._startTime,
1175 totalDuration=self._duration,
1176 status=self._status,
1177 testsuites=(testsuite.ToTestsuite() for testsuite in self._testsuites.values())
1178 )
1180 def ToTree(self) -> Node:
1181 node = Node(
1182 value=self._name,
1183 children=(ts.ToTree() for ts in self._testsuites.values())
1184 )
1185 node["startTime"] = self._startTime
1186 node["duration"] = self._duration
1188 return node
1190 def __str__(self) -> str:
1191 moduleName = self.__module__.split(".")[-1]
1192 className = self.__class__.__name__
1193 return (
1194 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1195 )
1198@export
1199class Document(TestsuiteSummary, ut_Document):
1200 _TESTCASE: ClassVar[Type[Testcase]] = Testcase
1201 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass
1202 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite
1204 _readerMode: JUnitReaderMode
1205 _xmlDocument: Nullable[_ElementTree]
1207 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default):
1208 super().__init__("Unprocessed JUnit XML file")
1210 self._readerMode = readerMode
1211 self._xmlDocument = None
1213 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert)
1215 @classmethod
1216 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary):
1217 doc = cls(xmlReportFile)
1218 doc._name = testsuiteSummary._name
1219 doc._startTime = testsuiteSummary._startTime
1220 doc._duration = testsuiteSummary._totalDuration
1221 doc._status = testsuiteSummary._status
1222 doc._tests = testsuiteSummary._tests
1223 doc._skipped = testsuiteSummary._skipped
1224 doc._errored = testsuiteSummary._errored
1225 doc._failed = testsuiteSummary._failed
1226 doc._passed = testsuiteSummary._passed
1228 doc.AddTestsuites(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1230 return doc
1232 def Analyze(self) -> None:
1233 """
1234 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML
1235 schema.
1237 .. hint::
1239 The time spend for analysis will be made available via property :data:`AnalysisDuration`.
1241 The used XML schema definition is generic to support "any" dialect.
1242 """
1243 xmlSchemaFile = "Any-JUnit.xsd"
1244 self._Analyze(xmlSchemaFile)
1246 def _Analyze(self, xmlSchemaFile: str) -> None:
1247 if not self._path.exists(): 1247 ↛ 1248line 1247 didn't jump to line 1248 because the condition on line 1247 was never true
1248 raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \
1249 from FileNotFoundError(f"File '{self._path}' not found.")
1251 startAnalysis = perf_counter_ns()
1252 try:
1253 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile)
1254 except ToolingException as ex:
1255 raise UnittestException(f"Couldn't locate XML Schema '{xmlSchemaFile}' in package resources.") from ex
1257 try:
1258 schemaParser = XMLParser(ns_clean=True)
1259 schemaRoot = parse(xmlSchemaResourceFile, schemaParser)
1260 except XMLSyntaxError as ex:
1261 raise UnittestException(f"XML Syntax Error while parsing XML Schema '{xmlSchemaFile}'.") from ex
1263 try:
1264 junitSchema = XMLSchema(schemaRoot)
1265 except XMLSchemaParseError as ex:
1266 raise UnittestException(f"Error while parsing XML Schema '{xmlSchemaFile}'.")
1268 try:
1269 junitParser = XMLParser(schema=junitSchema, ns_clean=True)
1270 junitDocument = parse(self._path, parser=junitParser)
1272 self._xmlDocument = junitDocument
1273 except XMLSyntaxError as ex:
1274 if version_info >= (3, 11): # pragma: no cover
1275 for logEntry in junitParser.error_log:
1276 ex.add_note(str(logEntry))
1277 raise UnittestException(f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'.") from ex
1278 except Exception as ex:
1279 raise UnittestException(f"Couldn't open '{self._path}'.") from ex
1281 endAnalysis = perf_counter_ns()
1282 self._analysisDuration = (endAnalysis - startAnalysis) / 1e9
1284 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None:
1285 """
1286 Write the data model as XML into a file adhering to the Any JUnit dialect.
1288 :param path: Optional path to the XMl file, if internal path shouldn't be used.
1289 :param overwrite: If true, overwrite an existing file.
1290 :param regenerate: If true, regenerate the XML structure from data model.
1291 :raises UnittestException: If the file cannot be overwritten.
1292 :raises UnittestException: If the internal XML data structure wasn't generated.
1293 :raises UnittestException: If the file cannot be opened or written.
1294 """
1295 if path is None:
1296 path = self._path
1298 if not overwrite and path.exists(): 1298 ↛ 1299line 1298 didn't jump to line 1299 because the condition on line 1298 was never true
1299 raise UnittestException(f"JUnit XML file '{path}' can not be overwritten.") \
1300 from FileExistsError(f"File '{path}' already exists.")
1302 if regenerate:
1303 self.Generate(overwrite=True)
1305 if self._xmlDocument is None: 1305 ↛ 1306line 1305 didn't jump to line 1306 because the condition on line 1305 was never true
1306 ex = UnittestException(f"Internal XML document tree is empty and needs to be generated before write is possible.")
1307 ex.add_note(f"Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.")
1308 raise ex
1310 try:
1311 with path.open("wb") as file:
1312 file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True))
1313 except Exception as ex:
1314 raise UnittestException(f"JUnit XML file '{path}' can not be written.") from ex
1316 def Convert(self) -> None:
1317 """
1318 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
1320 This method converts the root element.
1322 .. hint::
1324 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
1326 :raises UnittestException: If XML was not read and parsed before.
1327 """
1328 if self._xmlDocument is None: 1328 ↛ 1329line 1328 didn't jump to line 1329 because the condition on line 1328 was never true
1329 ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.")
1330 ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.")
1331 raise ex
1333 startConversion = perf_counter_ns()
1334 rootElement: _Element = self._xmlDocument.getroot()
1336 self._name = self._ConvertName(rootElement, optional=True)
1337 self._startTime = self._ConvertTimestamp(rootElement, optional=True)
1338 self._duration = self._ConvertTime(rootElement, optional=True)
1340 if False: # self._readerMode is JUnitReaderMode.
1341 self._tests = self._ConvertTests(testsuitesNode)
1342 self._skipped = self._ConvertSkipped(testsuitesNode)
1343 self._errored = self._ConvertErrors(testsuitesNode)
1344 self._failed = self._ConvertFailures(testsuitesNode)
1345 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1347 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element
1348 self._ConvertTestsuite(self, rootNode)
1350 if True: # self._readerMode is JUnitReaderMode.
1351 self.Aggregate()
1353 endConversation = perf_counter_ns()
1354 self._modelConversion = (endConversation - startConversion) / 1e9
1356 def _ConvertName(self, element: _Element, default: str = "root", optional: bool = True) -> str:
1357 """
1358 Convert the ``name`` attribute from an XML element node to a string.
1360 :param element: The XML element node with a ``name`` attribute.
1361 :param default: The default value, if no ``name`` attribute was found.
1362 :param optional: If false, an exception is raised for the missing attribute.
1363 :return: The ``name`` attribute's content if found, otherwise the given default value.
1364 :raises UnittestException: If optional is false and no ``name`` attribute exists on the given element node.
1365 """
1366 if "name" in element.attrib:
1367 return element.attrib["name"]
1368 elif not optional: 1368 ↛ 1369line 1368 didn't jump to line 1369 because the condition on line 1368 was never true
1369 raise UnittestException(f"Required parameter 'name' not found in tag '{element.tag}'.")
1370 else:
1371 return default
1373 def _ConvertTimestamp(self, element: _Element, optional: bool = True) -> Nullable[datetime]:
1374 """
1375 Convert the ``timestamp`` attribute from an XML element node to a datetime.
1377 :param element: The XML element node with a ``timestamp`` attribute.
1378 :param optional: If false, an exception is raised for the missing attribute.
1379 :return: The ``timestamp`` attribute's content if found, otherwise ``None``.
1380 :raises UnittestException: If optional is false and no ``timestamp`` attribute exists on the given element node.
1381 """
1382 if "timestamp" in element.attrib:
1383 timestamp = element.attrib["timestamp"]
1384 return datetime.fromisoformat(timestamp)
1385 elif not optional: 1385 ↛ 1386line 1385 didn't jump to line 1386 because the condition on line 1385 was never true
1386 raise UnittestException(f"Required parameter 'timestamp' not found in tag '{element.tag}'.")
1387 else:
1388 return None
1390 def _ConvertTime(self, element: _Element, optional: bool = True) -> Nullable[timedelta]:
1391 """
1392 Convert the ``time`` attribute from an XML element node to a timedelta.
1394 :param element: The XML element node with a ``time`` attribute.
1395 :param optional: If false, an exception is raised for the missing attribute.
1396 :return: The ``time`` attribute's content if found, otherwise ``None``.
1397 :raises UnittestException: If optional is false and no ``time`` attribute exists on the given element node.
1398 """
1399 if "time" in element.attrib:
1400 time = element.attrib["time"]
1401 return timedelta(seconds=float(time))
1402 elif not optional: 1402 ↛ 1403line 1402 didn't jump to line 1403 because the condition on line 1402 was never true
1403 raise UnittestException(f"Required parameter 'time' not found in tag '{element.tag}'.")
1404 else:
1405 return None
1407 def _ConvertHostname(self, element: _Element, default: str = "localhost", optional: bool = True) -> str:
1408 """
1409 Convert the ``hostname`` attribute from an XML element node to a string.
1411 :param element: The XML element node with a ``hostname`` attribute.
1412 :param default: The default value, if no ``hostname`` attribute was found.
1413 :param optional: If false, an exception is raised for the missing attribute.
1414 :return: The ``hostname`` attribute's content if found, otherwise the given default value.
1415 :raises UnittestException: If optional is false and no ``hostname`` attribute exists on the given element node.
1416 """
1417 if "hostname" in element.attrib:
1418 return element.attrib["hostname"]
1419 elif not optional: 1419 ↛ 1420line 1419 didn't jump to line 1420 because the condition on line 1419 was never true
1420 raise UnittestException(f"Required parameter 'hostname' not found in tag '{element.tag}'.")
1421 else:
1422 return default
1424 def _ConvertClassname(self, element: _Element) -> str:
1425 """
1426 Convert the ``classname`` attribute from an XML element node to a string.
1428 :param element: The XML element node with a ``classname`` attribute.
1429 :return: The ``classname`` attribute's content.
1430 :raises UnittestException: If no ``classname`` attribute exists on the given element node.
1431 """
1432 if "classname" in element.attrib: 1432 ↛ 1435line 1432 didn't jump to line 1435 because the condition on line 1432 was always true
1433 return element.attrib["classname"]
1434 else:
1435 raise UnittestException(f"Required parameter 'classname' not found in tag '{element.tag}'.")
1437 def _ConvertTests(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1438 """
1439 Convert the ``tests`` attribute from an XML element node to an integer.
1441 :param element: The XML element node with a ``tests`` attribute.
1442 :param default: The default value, if no ``tests`` attribute was found.
1443 :param optional: If false, an exception is raised for the missing attribute.
1444 :return: The ``tests`` attribute's content if found, otherwise the given default value.
1445 :raises UnittestException: If optional is false and no ``tests`` attribute exists on the given element node.
1446 """
1447 if "tests" in element.attrib:
1448 return int(element.attrib["tests"])
1449 elif not optional:
1450 raise UnittestException(f"Required parameter 'tests' not found in tag '{element.tag}'.")
1451 else:
1452 return default
1454 def _ConvertSkipped(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1455 """
1456 Convert the ``skipped`` attribute from an XML element node to an integer.
1458 :param element: The XML element node with a ``skipped`` attribute.
1459 :param default: The default value, if no ``skipped`` attribute was found.
1460 :param optional: If false, an exception is raised for the missing attribute.
1461 :return: The ``skipped`` attribute's content if found, otherwise the given default value.
1462 :raises UnittestException: If optional is false and no ``skipped`` attribute exists on the given element node.
1463 """
1464 if "skipped" in element.attrib:
1465 return int(element.attrib["skipped"])
1466 elif not optional:
1467 raise UnittestException(f"Required parameter 'skipped' not found in tag '{element.tag}'.")
1468 else:
1469 return default
1471 def _ConvertErrors(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1472 """
1473 Convert the ``errors`` attribute from an XML element node to an integer.
1475 :param element: The XML element node with a ``errors`` attribute.
1476 :param default: The default value, if no ``errors`` attribute was found.
1477 :param optional: If false, an exception is raised for the missing attribute.
1478 :return: The ``errors`` attribute's content if found, otherwise the given default value.
1479 :raises UnittestException: If optional is false and no ``errors`` attribute exists on the given element node.
1480 """
1481 if "errors" in element.attrib:
1482 return int(element.attrib["errors"])
1483 elif not optional:
1484 raise UnittestException(f"Required parameter 'errors' not found in tag '{element.tag}'.")
1485 else:
1486 return default
1488 def _ConvertFailures(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1489 """
1490 Convert the ``failures`` attribute from an XML element node to an integer.
1492 :param element: The XML element node with a ``failures`` attribute.
1493 :param default: The default value, if no ``failures`` attribute was found.
1494 :param optional: If false, an exception is raised for the missing attribute.
1495 :return: The ``failures`` attribute's content if found, otherwise the given default value.
1496 :raises UnittestException: If optional is false and no ``failures`` attribute exists on the given element node.
1497 """
1498 if "failures" in element.attrib:
1499 return int(element.attrib["failures"])
1500 elif not optional:
1501 raise UnittestException(f"Required parameter 'failures' not found in tag '{element.tag}'.")
1502 else:
1503 return default
1505 def _ConvertAssertions(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1506 """
1507 Convert the ``assertions`` attribute from an XML element node to an integer.
1509 :param element: The XML element node with a ``assertions`` attribute.
1510 :param default: The default value, if no ``assertions`` attribute was found.
1511 :param optional: If false, an exception is raised for the missing attribute.
1512 :return: The ``assertions`` attribute's content if found, otherwise the given default value.
1513 :raises UnittestException: If optional is false and no ``assertions`` attribute exists on the given element node.
1514 """
1515 if "assertions" in element.attrib:
1516 return int(element.attrib["assertions"])
1517 elif not optional: 1517 ↛ 1518line 1517 didn't jump to line 1518 because the condition on line 1517 was never true
1518 raise UnittestException(f"Required parameter 'assertions' not found in tag '{element.tag}'.")
1519 else:
1520 return default
1522 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
1523 """
1524 Convert the XML data structure of a ``<testsuite>`` to a test suite.
1526 This method uses private helper methods provided by the base-class.
1528 :param parent: The test suite summary as a parent element in the test entity hierarchy.
1529 :param testsuitesNode: The current XML element node representing a test suite.
1530 """
1531 newTestsuite = self._TESTSUITE(
1532 self._ConvertName(testsuitesNode, optional=False),
1533 self._ConvertHostname(testsuitesNode, optional=True),
1534 self._ConvertTimestamp(testsuitesNode, optional=True),
1535 self._ConvertTime(testsuitesNode, optional=True),
1536 parent=parent
1537 )
1539 if False: # self._readerMode is JUnitReaderMode.
1540 self._tests = self._ConvertTests(testsuitesNode)
1541 self._skipped = self._ConvertSkipped(testsuitesNode)
1542 self._errored = self._ConvertErrors(testsuitesNode)
1543 self._failed = self._ConvertFailures(testsuitesNode)
1544 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1546 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
1548 def _ConvertTestsuiteChildren(self, testsuitesNode: _Element, newTestsuite: Testsuite) -> None:
1549 for node in testsuitesNode.iterchildren(): # type: _Element
1550 # if node.tag == "testsuite":
1551 # self._ConvertTestsuite(newTestsuite, node)
1552 # el
1553 if node.tag == "testcase":
1554 self._ConvertTestcase(newTestsuite, node)
1556 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None:
1557 """
1558 Convert the XML data structure of a ``<testcase>`` to a test case.
1560 This method uses private helper methods provided by the base-class.
1562 :param parent: The test suite as a parent element in the test entity hierarchy.
1563 :param testcaseNode: The current XML element node representing a test case.
1564 """
1565 className = self._ConvertClassname(testcaseNode)
1566 testclass = self._FindOrCreateTestclass(parent, className)
1568 newTestcase = self._TESTCASE(
1569 self._ConvertName(testcaseNode, optional=False),
1570 self._ConvertTime(testcaseNode, optional=False),
1571 assertionCount=self._ConvertAssertions(testcaseNode),
1572 parent=testclass
1573 )
1575 self._ConvertTestcaseChildren(testcaseNode, newTestcase)
1577 def _FindOrCreateTestclass(self, parent: Testsuite, className: str) -> Testclass:
1578 if className in parent._testclasses:
1579 return parent._testclasses[className]
1580 else:
1581 return self._TESTCLASS(className, parent=parent)
1583 def _ConvertTestcaseChildren(self, testcaseNode: _Element, newTestcase: Testcase) -> None:
1584 for node in testcaseNode.iterchildren(): # type: _Element
1585 if isinstance(node, _Comment): 1585 ↛ 1586line 1585 didn't jump to line 1586 because the condition on line 1585 was never true
1586 pass
1587 elif isinstance(node, _Element): 1587 ↛ 1603line 1587 didn't jump to line 1603 because the condition on line 1587 was always true
1588 if node.tag == "skipped":
1589 newTestcase._status = TestcaseStatus.Skipped
1590 elif node.tag == "failure":
1591 newTestcase._status = TestcaseStatus.Failed
1592 elif node.tag == "error": 1592 ↛ 1593line 1592 didn't jump to line 1593 because the condition on line 1592 was never true
1593 newTestcase._status = TestcaseStatus.Errored
1594 elif node.tag == "system-out":
1595 pass
1596 elif node.tag == "system-err":
1597 pass
1598 elif node.tag == "properties": 1598 ↛ 1601line 1598 didn't jump to line 1601 because the condition on line 1598 was always true
1599 pass
1600 else:
1601 raise UnittestException(f"Unknown element '{node.tag}' in junit file.")
1602 else:
1603 pass
1605 if newTestcase._status is TestcaseStatus.Unknown:
1606 newTestcase._status = TestcaseStatus.Passed
1608 def Generate(self, overwrite: bool = False) -> None:
1609 """
1610 Generate the internal XML data structure from test suites and test cases.
1612 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods.
1614 :param overwrite: Overwrite the internal XML data structure.
1615 :raises UnittestException: If overwrite is false and the internal XML data structure is not empty.
1616 """
1617 if not overwrite and self._xmlDocument is not None: 1617 ↛ 1618line 1617 didn't jump to line 1618 because the condition on line 1617 was never true
1618 raise UnittestException(f"Internal XML document is populated with data.")
1620 rootElement = Element("testsuites")
1621 rootElement.attrib["name"] = self._name
1622 if self._startTime is not None:
1623 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}"
1624 if self._duration is not None:
1625 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}"
1626 rootElement.attrib["tests"] = str(self._tests)
1627 rootElement.attrib["failures"] = str(self._failed)
1628 rootElement.attrib["errors"] = str(self._errored)
1629 rootElement.attrib["skipped"] = str(self._skipped)
1630 # if self._assertionCount is not None:
1631 # rootElement.attrib["assertions"] = f"{self._assertionCount}"
1633 self._xmlDocument = ElementTree(rootElement)
1635 for testsuite in self._testsuites.values():
1636 self._GenerateTestsuite(testsuite, rootElement)
1638 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None:
1639 """
1640 Generate the internal XML data structure for a test suite.
1642 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods.
1644 :param testsuite: The test suite to convert to an XML data structures.
1645 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1646 :return:
1647 """
1648 testsuiteElement = SubElement(parentElement, "testsuite")
1649 testsuiteElement.attrib["name"] = testsuite._name
1650 if testsuite._startTime is not None: 1650 ↛ 1652line 1650 didn't jump to line 1652 because the condition on line 1650 was always true
1651 testsuiteElement.attrib["timestamp"] = f"{testsuite._startTime.isoformat()}"
1652 if testsuite._duration is not None:
1653 testsuiteElement.attrib["time"] = f"{testsuite._duration.total_seconds():.6f}"
1654 testsuiteElement.attrib["tests"] = str(testsuite._tests)
1655 testsuiteElement.attrib["failures"] = str(testsuite._failed)
1656 testsuiteElement.attrib["errors"] = str(testsuite._errored)
1657 testsuiteElement.attrib["skipped"] = str(testsuite._skipped)
1658 # if testsuite._assertionCount is not None:
1659 # testsuiteElement.attrib["assertions"] = f"{testsuite._assertionCount}"
1660 if testsuite._hostname is not None: 1660 ↛ 1661line 1660 didn't jump to line 1661 because the condition on line 1660 was never true
1661 testsuiteElement.attrib["hostname"] = testsuite._hostname
1663 for testclass in testsuite._testclasses.values():
1664 for tc in testclass._testcases.values():
1665 self._GenerateTestcase(tc, testsuiteElement)
1667 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
1668 """
1669 Generate the internal XML data structure for a test case.
1671 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
1673 :param testcase: The test case to convert to an XML data structures.
1674 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1675 :return:
1676 """
1677 testcaseElement = SubElement(parentElement, "testcase")
1678 if testcase.Classname is not None: 1678 ↛ 1680line 1678 didn't jump to line 1680 because the condition on line 1678 was always true
1679 testcaseElement.attrib["classname"] = testcase.Classname
1680 testcaseElement.attrib["name"] = testcase._name
1681 if testcase._duration is not None: 1681 ↛ 1683line 1681 didn't jump to line 1683 because the condition on line 1681 was always true
1682 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}"
1683 if testcase._assertionCount is not None:
1684 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}"
1686 if testcase._status is TestcaseStatus.Passed:
1687 pass
1688 elif testcase._status is TestcaseStatus.Failed:
1689 failureElement = SubElement(testcaseElement, "failure")
1690 elif testcase._status is TestcaseStatus.Skipped: 1690 ↛ 1693line 1690 didn't jump to line 1693 because the condition on line 1690 was always true
1691 skippedElement = SubElement(testcaseElement, "skipped")
1692 else:
1693 errorElement = SubElement(testcaseElement, "error")
1695 def __str__(self) -> str:
1696 moduleName = self.__module__.split(".")[-1]
1697 className = self.__class__.__name__
1698 return (
1699 f"<{moduleName}{className} {self._name} ({self._path}): {self._status.name} - suites/tests:{self.TestsuiteCount}/{self.TestcaseCount}>"
1700 )