Coverage for pyEDAA/Reports/Unittesting/JUnit/__init__.py: 68%
711 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +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]
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:
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 _failed: int
548 _passed: int
550 def __init__(
551 self,
552 name: str,
553 startTime: Nullable[datetime] = None,
554 duration: Nullable[timedelta] = None,
555 status: TestsuiteStatus = TestsuiteStatus.Unknown,
556 parent: Nullable["Testsuite"] = None
557 ):
558 """
559 Initializes the based-class fields of a test suite or test summary.
561 :param name: Name of the test entity.
562 :param startTime: Time when the test entity was started.
563 :param duration: Duration of the entity's execution.
564 :param status: Overall status of the test entity.
565 :param parent: Reference to the parent test entity.
566 :raises TypeError: If parameter 'parent' is not a TestsuiteBase.
567 """
568 if parent is not None:
569 if not isinstance(parent, TestsuiteBase): 569 ↛ 570line 569 didn't jump to line 570 because the condition on line 569 was never true
570 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.")
571 if version_info >= (3, 11): # pragma: no cover
572 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
573 raise ex
575 parent._testsuites[name] = self
577 super().__init__(name, duration, None, parent)
579 self._startTime = startTime
580 self._status = status
581 self._tests = 0
582 self._skipped = 0
583 self._errored = 0
584 self._failed = 0
585 self._passed = 0
587 @readonly
588 def StartTime(self) -> Nullable[datetime]:
589 return self._startTime
591 @readonly
592 def Status(self) -> TestsuiteStatus:
593 return self._status
595 @readonly
596 @mustoverride
597 def TestcaseCount(self) -> int:
598 pass
600 @readonly
601 def Tests(self) -> int:
602 return self.TestcaseCount
604 @readonly
605 def Skipped(self) -> int:
606 return self._skipped
608 @readonly
609 def Errored(self) -> int:
610 return self._errored
612 @readonly
613 def Failed(self) -> int:
614 return self._failed
616 @readonly
617 def Passed(self) -> int:
618 return self._passed
620 def Aggregate(self) -> TestsuiteAggregateReturnType:
621 tests = 0
622 skipped = 0
623 errored = 0
624 failed = 0
625 passed = 0
627 # for testsuite in self._testsuites.values():
628 # t, s, e, w, f, p = testsuite.Aggregate()
629 # tests += t
630 # skipped += s
631 # errored += e
632 # weak += w
633 # failed += f
634 # passed += p
636 return tests, skipped, errored, failed, passed
638 @mustoverride
639 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
640 pass
643@export
644class Testclass(Base):
645 """
646 A test class is a low-level element in the test entity hierarchy representing a group of tests.
648 Test classes contain test cases and are grouped by a test suites.
649 """
651 _testcases: Dict[str, "Testcase"]
653 def __init__(
654 self,
655 classname: str,
656 testcases: Nullable[Iterable["Testcase"]] = None,
657 parent: Nullable["Testsuite"] = None
658 ):
659 """
660 Initializes the fields of the test class.
662 :param classname: Classname of the test entity.
663 :param parent: Reference to the parent test suite.
664 :raises ValueError: If parameter 'classname' is None.
665 :raises TypeError: If parameter 'classname' is not a string.
666 :raises ValueError: If parameter 'classname' is empty.
667 """
668 if parent is not None:
669 if not isinstance(parent, Testsuite): 669 ↛ 670line 669 didn't jump to line 670 because the condition on line 669 was never true
670 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
671 if version_info >= (3, 11): # pragma: no cover
672 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
673 raise ex
675 parent._testclasses[classname] = self
677 super().__init__(classname, parent)
679 self._testcases = {}
680 if testcases is not None:
681 for testcase in testcases:
682 if testcase._parent is not None: 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
685 if testcase._name in self._testcases: 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true
686 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
688 testcase._parent = self
689 self._testcases[testcase._name] = testcase
691 @readonly
692 def Classname(self) -> str:
693 """
694 Read-only property returning the name of the test class.
696 :return: The test class' name.
697 """
698 return self._name
700 @readonly
701 def Testcases(self) -> Dict[str, "Testcase"]:
702 """
703 Read-only property returning a reference to the internal dictionary of test cases.
705 :return: Reference to the dictionary of test cases.
706 """
707 return self._testcases
709 @readonly
710 def TestcaseCount(self) -> int:
711 """
712 Read-only property returning the number of all test cases in the test entity hierarchy.
714 :return: Number of test cases.
715 """
716 return len(self._testcases)
718 @readonly
719 def AssertionCount(self) -> int:
720 return sum(tc.AssertionCount for tc in self._testcases.values())
722 def AddTestcase(self, testcase: "Testcase") -> None:
723 if testcase._parent is not None: 723 ↛ 724line 723 didn't jump to line 724 because the condition on line 723 was never true
724 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
726 if testcase._name in self._testcases: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true
727 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
729 testcase._parent = self
730 self._testcases[testcase._name] = testcase
732 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
733 for testcase in testcases:
734 self.AddTestcase(testcase)
736 def ToTestsuite(self) -> ut_Testsuite:
737 return ut_Testsuite(
738 self._name,
739 TestsuiteKind.Class,
740 # startTime=self._startTime,
741 # totalDuration=self._duration,
742 # status=self._status,
743 testcases=(tc.ToTestcase() for tc in self._testcases.values())
744 )
746 def ToTree(self) -> Node:
747 node = Node(
748 value=self._name,
749 children=(tc.ToTree() for tc in self._testcases.values())
750 )
752 return node
754 def __str__(self) -> str:
755 moduleName = self.__module__.split(".")[-1]
756 className = self.__class__.__name__
757 return (
758 f"<{moduleName}{className} {self._name}: {len(self._testcases)}>"
759 )
762@export
763class Testsuite(TestsuiteBase):
764 """
765 A testsuite is a mid-level element in the test entity hierarchy representing a logical group of tests.
767 Test suites contain test classes and are grouped by a test summary, which is the root of the hierarchy.
768 """
770 _hostname: str
771 _testclasses: Dict[str, "Testclass"]
773 def __init__(
774 self,
775 name: str,
776 hostname: Nullable[str] = None,
777 startTime: Nullable[datetime] = None,
778 duration: Nullable[timedelta] = None,
779 status: TestsuiteStatus = TestsuiteStatus.Unknown,
780 testclasses: Nullable[Iterable["Testclass"]] = None,
781 parent: Nullable["TestsuiteSummary"] = None
782 ):
783 """
784 Initializes the fields of a test suite.
786 :param name: Name of the test suite.
787 :param startTime: Time when the test suite was started.
788 :param duration: duration of the entity's execution.
789 :param status: Overall status of the test suite.
790 :param parent: Reference to the parent test summary.
791 :raises TypeError: If parameter 'testcases' is not iterable.
792 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
793 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
794 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
795 """
796 if parent is not None:
797 if not isinstance(parent, TestsuiteSummary): 797 ↛ 798line 797 didn't jump to line 798 because the condition on line 797 was never true
798 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteSummary'.")
799 if version_info >= (3, 11): # pragma: no cover
800 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
801 raise ex
803 parent._testsuites[name] = self
805 super().__init__(name, startTime, duration, status, parent)
807 self._hostname = hostname
809 self._testclasses = {}
810 if testclasses is not None:
811 for testclass in testclasses:
812 if testclass._parent is not None: 812 ↛ 813line 812 didn't jump to line 813 because the condition on line 812 was never true
813 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
815 if testclass._name in self._testclasses: 815 ↛ 816line 815 didn't jump to line 816 because the condition on line 815 was never true
816 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
818 testclass._parent = self
819 self._testclasses[testclass._name] = testclass
821 @readonly
822 def Hostname(self) -> Nullable[str]:
823 return self._hostname
825 @readonly
826 def Testclasses(self) -> Dict[str, "Testclass"]:
827 return self._testclasses
829 @readonly
830 def TestclassCount(self) -> int:
831 return len(self._testclasses)
833 # @readonly
834 # def Testcases(self) -> Dict[str, "Testcase"]:
835 # return self._classes
837 @readonly
838 def TestcaseCount(self) -> int:
839 return sum(cls.TestcaseCount for cls in self._testclasses.values())
841 @readonly
842 def AssertionCount(self) -> int:
843 return sum(cls.AssertionCount for cls in self._testclasses.values())
845 def AddTestclass(self, testclass: "Testclass") -> None:
846 if testclass._parent is not None: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true
847 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
849 if testclass._name in self._testclasses: 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true
850 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
852 testclass._parent = self
853 self._testclasses[testclass._name] = testclass
855 def AddTestclasses(self, testclasses: Iterable["Testclass"]) -> None:
856 for testcase in testclasses:
857 self.AddTestclass(testcase)
859 # def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
860 # return self.Iterate(scheme)
862 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
863 return self.Iterate(scheme)
865 def Copy(self) -> "Testsuite":
866 return self.__class__(
867 self._name,
868 self._hostname,
869 self._startTime,
870 self._duration,
871 self._status
872 )
874 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
875 tests, skipped, errored, failed, passed = super().Aggregate()
877 for testclass in self._testclasses.values():
878 _ = testclass.Aggregate(strict)
880 tests += 1
882 status = testclass._status
883 if status is TestcaseStatus.Unknown:
884 raise UnittestException(f"Found testclass '{testclass._name}' with state 'Unknown'.")
885 elif status is TestcaseStatus.Skipped:
886 skipped += 1
887 elif status is TestcaseStatus.Errored:
888 errored += 1
889 elif status is TestcaseStatus.Passed:
890 passed += 1
891 elif status is TestcaseStatus.Failed:
892 failed += 1
893 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
894 raise UnittestException(f"Found testclass '{testclass._name}' with unsupported state '{status}'.")
895 else:
896 raise UnittestException(f"Internal error for testclass '{testclass._name}', field '_status' is '{status}'.")
898 self._tests = tests
899 self._skipped = skipped
900 self._errored = errored
901 self._failed = failed
902 self._passed = passed
904 if errored > 0:
905 self._status = TestsuiteStatus.Errored
906 elif failed > 0:
907 self._status = TestsuiteStatus.Failed
908 elif tests == 0:
909 self._status = TestsuiteStatus.Empty
910 elif tests - skipped == passed:
911 self._status = TestsuiteStatus.Passed
912 elif tests == skipped:
913 self._status = TestsuiteStatus.Skipped
914 else:
915 self._status = TestsuiteStatus.Unknown
917 return tests, skipped, errored, failed, passed
919 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
920 """
921 Iterate the test suite and its child elements according to the iteration scheme.
923 If no scheme is given, use the default scheme.
925 :param scheme: Scheme how to iterate the test suite and its child elements.
926 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
927 """
928 assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme
930 if IterationScheme.PreOrder in scheme:
931 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
932 yield self
934 if IterationScheme.IncludeTestcases in scheme:
935 for testcase in self._testclasses.values():
936 yield testcase
938 for testclass in self._testclasses.values():
939 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf)
941 if IterationScheme.PostOrder in scheme:
942 if IterationScheme.IncludeTestcases in scheme:
943 for testcase in self._testclasses.values():
944 yield testcase
946 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
947 yield self
949 @classmethod
950 def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite":
951 """
952 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object.
954 :param testsuite: Test suite from unified data model.
955 :return: Test suite from JUnit specific data model.
956 """
957 juTestsuite = cls(
958 testsuite._name,
959 startTime=testsuite._startTime,
960 duration=testsuite._totalDuration,
961 status= testsuite._status,
962 )
964 juTestsuite._tests = testsuite._tests
965 juTestsuite._skipped = testsuite._skipped
966 juTestsuite._errored = testsuite._errored
967 juTestsuite._failed = testsuite._failed
968 juTestsuite._passed = testsuite._passed
970 for tc in testsuite.IterateTestcases():
971 ts = tc._parent
972 if ts is None: 972 ↛ 973line 972 didn't jump to line 973 because the condition on line 972 was never true
973 raise UnittestException(f"Testcase '{tc._name}' is not part of a hierarchy.")
975 classname = ts._name
976 ts = ts._parent
977 while ts is not None and ts._kind > TestsuiteKind.Logical:
978 classname = f"{ts._name}.{classname}"
979 ts = ts._parent
981 if classname in juTestsuite._testclasses:
982 juClass = juTestsuite._testclasses[classname]
983 else:
984 juClass = Testclass(classname, parent=juTestsuite)
986 juClass.AddTestcase(Testcase.FromTestcase(tc))
988 return juTestsuite
990 def ToTestsuite(self) -> ut_Testsuite:
991 testsuite = ut_Testsuite(
992 self._name,
993 TestsuiteKind.Logical,
994 startTime=self._startTime,
995 totalDuration=self._duration,
996 status=self._status,
997 )
999 for testclass in self._testclasses.values():
1000 suite = testsuite
1001 classpath = testclass._name.split(".")
1002 for element in classpath:
1003 if element in suite._testsuites:
1004 suite = suite._testsuites[element]
1005 else:
1006 suite = ut_Testsuite(element, kind=TestsuiteKind.Package, parent=suite)
1008 suite._kind = TestsuiteKind.Class
1009 if suite._parent is not testsuite: 1009 ↛ 1012line 1009 didn't jump to line 1012 because the condition on line 1009 was always true
1010 suite._parent._kind = TestsuiteKind.Module
1012 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values())
1014 return testsuite
1016 def ToTree(self) -> Node:
1017 node = Node(
1018 value=self._name,
1019 children=(cls.ToTree() for cls in self._testclasses.values())
1020 )
1021 node["startTime"] = self._startTime
1022 node["duration"] = self._duration
1024 return node
1026 def __str__(self) -> str:
1027 moduleName = self.__module__.split(".")[-1]
1028 className = self.__class__.__name__
1029 return (
1030 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1031 )
1034@export
1035class TestsuiteSummary(TestsuiteBase):
1036 _testsuites: Dict[str, Testsuite]
1038 def __init__(
1039 self,
1040 name: str,
1041 startTime: Nullable[datetime] = None,
1042 duration: Nullable[timedelta] = None,
1043 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1044 testsuites: Nullable[Iterable[Testsuite]] = None
1045 ):
1046 super().__init__(name, startTime, duration, status, None)
1048 self._testsuites = {}
1049 if testsuites is not None:
1050 for testsuite in testsuites:
1051 if testsuite._parent is not None: 1051 ↛ 1052line 1051 didn't jump to line 1052 because the condition on line 1051 was never true
1052 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1054 if testsuite._name in self._testsuites: 1054 ↛ 1055line 1054 didn't jump to line 1055 because the condition on line 1054 was never true
1055 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1057 testsuite._parent = self
1058 self._testsuites[testsuite._name] = testsuite
1060 @readonly
1061 def Testsuites(self) -> Dict[str, Testsuite]:
1062 return self._testsuites
1064 @readonly
1065 def TestcaseCount(self) -> int:
1066 return sum(ts.TestcaseCount for ts in self._testsuites.values())
1068 @readonly
1069 def TestsuiteCount(self) -> int:
1070 return len(self._testsuites)
1072 @readonly
1073 def AssertionCount(self) -> int:
1074 return sum(ts.AssertionCount for ts in self._testsuites.values())
1076 def AddTestsuite(self, testsuite: Testsuite) -> None:
1077 if testsuite._parent is not None: 1077 ↛ 1078line 1077 didn't jump to line 1078 because the condition on line 1077 was never true
1078 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1080 if testsuite._name in self._testsuites: 1080 ↛ 1081line 1080 didn't jump to line 1081 because the condition on line 1080 was never true
1081 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1083 testsuite._parent = self
1084 self._testsuites[testsuite._name] = testsuite
1086 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None:
1087 for testsuite in testsuites:
1088 self.AddTestsuite(testsuite)
1090 def Aggregate(self) -> TestsuiteAggregateReturnType:
1091 tests, skipped, errored, failed, passed = super().Aggregate()
1093 self._tests = tests
1094 self._skipped = skipped
1095 self._errored = errored
1096 self._failed = failed
1097 self._passed = passed
1099 if errored > 0: 1099 ↛ 1100line 1099 didn't jump to line 1100 because the condition on line 1099 was never true
1100 self._status = TestsuiteStatus.Errored
1101 elif failed > 0: 1101 ↛ 1102line 1101 didn't jump to line 1102 because the condition on line 1101 was never true
1102 self._status = TestsuiteStatus.Failed
1103 elif tests == 0: 1103 ↛ 1105line 1103 didn't jump to line 1105 because the condition on line 1103 was always true
1104 self._status = TestsuiteStatus.Empty
1105 elif tests - skipped == passed:
1106 self._status = TestsuiteStatus.Passed
1107 elif tests == skipped:
1108 self._status = TestsuiteStatus.Skipped
1109 else:
1110 self._status = TestsuiteStatus.Unknown
1112 return tests, skipped, errored, failed, passed
1114 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[Testsuite, Testcase], None, None]:
1115 """
1116 Iterate the test suite summary and its child elements according to the iteration scheme.
1118 If no scheme is given, use the default scheme.
1120 :param scheme: Scheme how to iterate the test suite summary and its child elements.
1121 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
1122 """
1123 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1124 yield self
1126 for testsuite in self._testsuites.values():
1127 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1129 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme:
1130 yield self
1132 @classmethod
1133 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> "TestsuiteSummary":
1134 """
1135 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite.
1137 :param testsuiteSummary: Test suite summary from unified data model.
1138 :return: Test suite summary from JUnit specific data model.
1139 """
1140 return cls(
1141 testsuiteSummary._name,
1142 startTime=testsuiteSummary._startTime,
1143 duration=testsuiteSummary._totalDuration,
1144 status=testsuiteSummary._status,
1145 testsuites=(ut_Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1146 )
1148 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
1149 """
1150 Convert this test suite summary a new test suite summary of the unified data model.
1152 All fields are copied to the new instance. Child elements like test suites are copied recursively.
1154 :return: A test suite summary of the unified test entity data model.
1155 """
1156 return ut_TestsuiteSummary(
1157 self._name,
1158 startTime=self._startTime,
1159 totalDuration=self._duration,
1160 status=self._status,
1161 testsuites=(testsuite.ToTestsuite() for testsuite in self._testsuites.values())
1162 )
1164 def ToTree(self) -> Node:
1165 node = Node(
1166 value=self._name,
1167 children=(ts.ToTree() for ts in self._testsuites.values())
1168 )
1169 node["startTime"] = self._startTime
1170 node["duration"] = self._duration
1172 return node
1174 def __str__(self) -> str:
1175 moduleName = self.__module__.split(".")[-1]
1176 className = self.__class__.__name__
1177 return (
1178 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1179 )
1182@export
1183class Document(TestsuiteSummary, ut_Document):
1184 _TESTCASE: ClassVar[Type[Testcase]] = Testcase
1185 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass
1186 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite
1188 _readerMode: JUnitReaderMode
1189 _xmlDocument: Nullable[_ElementTree]
1191 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default):
1192 super().__init__("Unprocessed JUnit XML file")
1194 self._readerMode = readerMode
1195 self._xmlDocument = None
1197 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert)
1199 @classmethod
1200 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary):
1201 doc = cls(xmlReportFile)
1202 doc._name = testsuiteSummary._name
1203 doc._startTime = testsuiteSummary._startTime
1204 doc._duration = testsuiteSummary._totalDuration
1205 doc._status = testsuiteSummary._status
1206 doc._tests = testsuiteSummary._tests
1207 doc._skipped = testsuiteSummary._skipped
1208 doc._errored = testsuiteSummary._errored
1209 doc._failed = testsuiteSummary._failed
1210 doc._passed = testsuiteSummary._passed
1212 doc.AddTestsuites(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1214 return doc
1216 def Analyze(self) -> None:
1217 """
1218 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML
1219 schema.
1221 .. hint::
1223 The time spend for analysis will be made available via property :data:`AnalysisDuration`.
1225 The used XML schema definition is generic to support "any" dialect.
1226 """
1227 xmlSchemaFile = "Any-JUnit.xsd"
1228 self._Analyze(xmlSchemaFile)
1230 def _Analyze(self, xmlSchemaFile: str) -> None:
1231 if not self._path.exists(): 1231 ↛ 1232line 1231 didn't jump to line 1232 because the condition on line 1231 was never true
1232 raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \
1233 from FileNotFoundError(f"File '{self._path}' not found.")
1235 startAnalysis = perf_counter_ns()
1236 try:
1237 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile)
1238 except ToolingException as ex:
1239 raise UnittestException(f"Couldn't locate XML Schema '{xmlSchemaFile}' in package resources.") from ex
1241 try:
1242 schemaParser = XMLParser(ns_clean=True)
1243 schemaRoot = parse(xmlSchemaResourceFile, schemaParser)
1244 except XMLSyntaxError as ex:
1245 raise UnittestException(f"XML Syntax Error while parsing XML Schema '{xmlSchemaFile}'.") from ex
1247 try:
1248 junitSchema = XMLSchema(schemaRoot)
1249 except XMLSchemaParseError as ex:
1250 raise UnittestException(f"Error while parsing XML Schema '{xmlSchemaFile}'.")
1252 try:
1253 junitParser = XMLParser(schema=junitSchema, ns_clean=True)
1254 junitDocument = parse(self._path, parser=junitParser)
1256 self._xmlDocument = junitDocument
1257 except XMLSyntaxError as ex:
1258 if version_info >= (3, 11): # pragma: no cover
1259 for logEntry in junitParser.error_log:
1260 ex.add_note(str(logEntry))
1261 raise UnittestException(f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'.") from ex
1262 except Exception as ex:
1263 raise UnittestException(f"Couldn't open '{self._path}'.") from ex
1265 endAnalysis = perf_counter_ns()
1266 self._analysisDuration = (endAnalysis - startAnalysis) / 1e9
1268 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None:
1269 """
1270 Write the data model as XML into a file adhering to the Any JUnit dialect.
1272 :param path: Optional path to the XMl file, if internal path shouldn't be used.
1273 :param overwrite: If true, overwrite an existing file.
1274 :param regenerate: If true, regenerate the XML structure from data model.
1275 :raises UnittestException: If the file cannot be overwritten.
1276 :raises UnittestException: If the internal XML data structure wasn't generated.
1277 :raises UnittestException: If the file cannot be opened or written.
1278 """
1279 if path is None:
1280 path = self._path
1282 if not overwrite and path.exists(): 1282 ↛ 1283line 1282 didn't jump to line 1283 because the condition on line 1282 was never true
1283 raise UnittestException(f"JUnit XML file '{path}' can not be overwritten.") \
1284 from FileExistsError(f"File '{path}' already exists.")
1286 if regenerate:
1287 self.Generate(overwrite=True)
1289 if self._xmlDocument is None: 1289 ↛ 1290line 1289 didn't jump to line 1290 because the condition on line 1289 was never true
1290 ex = UnittestException(f"Internal XML document tree is empty and needs to be generated before write is possible.")
1291 ex.add_note(f"Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.")
1292 raise ex
1294 try:
1295 with path.open("wb") as file:
1296 file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True))
1297 except Exception as ex:
1298 raise UnittestException(f"JUnit XML file '{path}' can not be written.") from ex
1300 def Convert(self) -> None:
1301 """
1302 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
1304 This method converts the root element.
1306 .. hint::
1308 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
1310 :raises UnittestException: If XML was not read and parsed before.
1311 """
1312 if self._xmlDocument is None: 1312 ↛ 1313line 1312 didn't jump to line 1313 because the condition on line 1312 was never true
1313 ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.")
1314 ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.")
1315 raise ex
1317 startConversion = perf_counter_ns()
1318 rootElement: _Element = self._xmlDocument.getroot()
1320 self._name = self._ConvertName(rootElement, optional=True)
1321 self._startTime = self._ConvertTimestamp(rootElement, optional=True)
1322 self._duration = self._ConvertTime(rootElement, optional=True)
1324 if False: # self._readerMode is JUnitReaderMode.
1325 self._tests = self._ConvertTests(testsuitesNode)
1326 self._skipped = self._ConvertSkipped(testsuitesNode)
1327 self._errored = self._ConvertErrors(testsuitesNode)
1328 self._failed = self._ConvertFailures(testsuitesNode)
1329 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1331 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element
1332 self._ConvertTestsuite(self, rootNode)
1334 if True: # self._readerMode is JUnitReaderMode.
1335 self.Aggregate()
1337 endConversation = perf_counter_ns()
1338 self._modelConversion = (endConversation - startConversion) / 1e9
1340 def _ConvertName(self, element: _Element, default: str = "root", optional: bool = True) -> str:
1341 """
1342 Convert the ``name`` attribute from an XML element node to a string.
1344 :param element: The XML element node with a ``name`` attribute.
1345 :param default: The default value, if no ``name`` attribute was found.
1346 :param optional: If false, an exception is raised for the missing attribute.
1347 :return: The ``name`` attribute's content if found, otherwise the given default value.
1348 :raises UnittestException: If optional is false and no ``name`` attribute exists on the given element node.
1349 """
1350 if "name" in element.attrib:
1351 return element.attrib["name"]
1352 elif not optional: 1352 ↛ 1353line 1352 didn't jump to line 1353 because the condition on line 1352 was never true
1353 raise UnittestException(f"Required parameter 'name' not found in tag '{element.tag}'.")
1354 else:
1355 return default
1357 def _ConvertTimestamp(self, element: _Element, optional: bool = True) -> Nullable[datetime]:
1358 """
1359 Convert the ``timestamp`` attribute from an XML element node to a datetime.
1361 :param element: The XML element node with a ``timestamp`` attribute.
1362 :param optional: If false, an exception is raised for the missing attribute.
1363 :return: The ``timestamp`` attribute's content if found, otherwise ``None``.
1364 :raises UnittestException: If optional is false and no ``timestamp`` attribute exists on the given element node.
1365 """
1366 if "timestamp" in element.attrib:
1367 timestamp = element.attrib["timestamp"]
1368 return datetime.fromisoformat(timestamp)
1369 elif not optional: 1369 ↛ 1370line 1369 didn't jump to line 1370 because the condition on line 1369 was never true
1370 raise UnittestException(f"Required parameter 'timestamp' not found in tag '{element.tag}'.")
1371 else:
1372 return None
1374 def _ConvertTime(self, element: _Element, optional: bool = True) -> Nullable[timedelta]:
1375 """
1376 Convert the ``time`` attribute from an XML element node to a timedelta.
1378 :param element: The XML element node with a ``time`` attribute.
1379 :param optional: If false, an exception is raised for the missing attribute.
1380 :return: The ``time`` attribute's content if found, otherwise ``None``.
1381 :raises UnittestException: If optional is false and no ``time`` attribute exists on the given element node.
1382 """
1383 if "time" in element.attrib:
1384 time = element.attrib["time"]
1385 return timedelta(seconds=float(time))
1386 elif not optional: 1386 ↛ 1387line 1386 didn't jump to line 1387 because the condition on line 1386 was never true
1387 raise UnittestException(f"Required parameter 'time' not found in tag '{element.tag}'.")
1388 else:
1389 return None
1391 def _ConvertHostname(self, element: _Element, default: str = "localhost", optional: bool = True) -> str:
1392 """
1393 Convert the ``hostname`` attribute from an XML element node to a string.
1395 :param element: The XML element node with a ``hostname`` attribute.
1396 :param default: The default value, if no ``hostname`` attribute was found.
1397 :param optional: If false, an exception is raised for the missing attribute.
1398 :return: The ``hostname`` attribute's content if found, otherwise the given default value.
1399 :raises UnittestException: If optional is false and no ``hostname`` attribute exists on the given element node.
1400 """
1401 if "hostname" in element.attrib:
1402 return element.attrib["hostname"]
1403 elif not optional: 1403 ↛ 1404line 1403 didn't jump to line 1404 because the condition on line 1403 was never true
1404 raise UnittestException(f"Required parameter 'hostname' not found in tag '{element.tag}'.")
1405 else:
1406 return default
1408 def _ConvertClassname(self, element: _Element) -> str:
1409 """
1410 Convert the ``classname`` attribute from an XML element node to a string.
1412 :param element: The XML element node with a ``classname`` attribute.
1413 :return: The ``classname`` attribute's content.
1414 :raises UnittestException: If no ``classname`` attribute exists on the given element node.
1415 """
1416 if "classname" in element.attrib: 1416 ↛ 1419line 1416 didn't jump to line 1419 because the condition on line 1416 was always true
1417 return element.attrib["classname"]
1418 else:
1419 raise UnittestException(f"Required parameter 'classname' not found in tag '{element.tag}'.")
1421 def _ConvertTests(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1422 """
1423 Convert the ``tests`` attribute from an XML element node to an integer.
1425 :param element: The XML element node with a ``tests`` attribute.
1426 :param default: The default value, if no ``tests`` attribute was found.
1427 :param optional: If false, an exception is raised for the missing attribute.
1428 :return: The ``tests`` attribute's content if found, otherwise the given default value.
1429 :raises UnittestException: If optional is false and no ``tests`` attribute exists on the given element node.
1430 """
1431 if "tests" in element.attrib:
1432 return int(element.attrib["tests"])
1433 elif not optional:
1434 raise UnittestException(f"Required parameter 'tests' not found in tag '{element.tag}'.")
1435 else:
1436 return default
1438 def _ConvertSkipped(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1439 """
1440 Convert the ``skipped`` attribute from an XML element node to an integer.
1442 :param element: The XML element node with a ``skipped`` attribute.
1443 :param default: The default value, if no ``skipped`` attribute was found.
1444 :param optional: If false, an exception is raised for the missing attribute.
1445 :return: The ``skipped`` attribute's content if found, otherwise the given default value.
1446 :raises UnittestException: If optional is false and no ``skipped`` attribute exists on the given element node.
1447 """
1448 if "skipped" in element.attrib:
1449 return int(element.attrib["skipped"])
1450 elif not optional:
1451 raise UnittestException(f"Required parameter 'skipped' not found in tag '{element.tag}'.")
1452 else:
1453 return default
1455 def _ConvertErrors(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1456 """
1457 Convert the ``errors`` attribute from an XML element node to an integer.
1459 :param element: The XML element node with a ``errors`` attribute.
1460 :param default: The default value, if no ``errors`` attribute was found.
1461 :param optional: If false, an exception is raised for the missing attribute.
1462 :return: The ``errors`` attribute's content if found, otherwise the given default value.
1463 :raises UnittestException: If optional is false and no ``errors`` attribute exists on the given element node.
1464 """
1465 if "errors" in element.attrib:
1466 return int(element.attrib["errors"])
1467 elif not optional:
1468 raise UnittestException(f"Required parameter 'errors' not found in tag '{element.tag}'.")
1469 else:
1470 return default
1472 def _ConvertFailures(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1473 """
1474 Convert the ``failures`` attribute from an XML element node to an integer.
1476 :param element: The XML element node with a ``failures`` attribute.
1477 :param default: The default value, if no ``failures`` attribute was found.
1478 :param optional: If false, an exception is raised for the missing attribute.
1479 :return: The ``failures`` attribute's content if found, otherwise the given default value.
1480 :raises UnittestException: If optional is false and no ``failures`` attribute exists on the given element node.
1481 """
1482 if "failures" in element.attrib:
1483 return int(element.attrib["failures"])
1484 elif not optional:
1485 raise UnittestException(f"Required parameter 'failures' not found in tag '{element.tag}'.")
1486 else:
1487 return default
1489 def _ConvertAssertions(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1490 """
1491 Convert the ``assertions`` attribute from an XML element node to an integer.
1493 :param element: The XML element node with a ``assertions`` attribute.
1494 :param default: The default value, if no ``assertions`` attribute was found.
1495 :param optional: If false, an exception is raised for the missing attribute.
1496 :return: The ``assertions`` attribute's content if found, otherwise the given default value.
1497 :raises UnittestException: If optional is false and no ``assertions`` attribute exists on the given element node.
1498 """
1499 if "assertions" in element.attrib:
1500 return int(element.attrib["assertions"])
1501 elif not optional: 1501 ↛ 1502line 1501 didn't jump to line 1502 because the condition on line 1501 was never true
1502 raise UnittestException(f"Required parameter 'assertions' not found in tag '{element.tag}'.")
1503 else:
1504 return default
1506 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
1507 """
1508 Convert the XML data structure of a ``<testsuite>`` to a test suite.
1510 This method uses private helper methods provided by the base-class.
1512 :param parent: The test suite summary as a parent element in the test entity hierarchy.
1513 :param testsuitesNode: The current XML element node representing a test suite.
1514 """
1515 newTestsuite = self._TESTSUITE(
1516 self._ConvertName(testsuitesNode, optional=False),
1517 self._ConvertHostname(testsuitesNode, optional=True),
1518 self._ConvertTimestamp(testsuitesNode, optional=True),
1519 self._ConvertTime(testsuitesNode, optional=True),
1520 parent=parent
1521 )
1523 if False: # self._readerMode is JUnitReaderMode.
1524 self._tests = self._ConvertTests(testsuitesNode)
1525 self._skipped = self._ConvertSkipped(testsuitesNode)
1526 self._errored = self._ConvertErrors(testsuitesNode)
1527 self._failed = self._ConvertFailures(testsuitesNode)
1528 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1530 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
1532 def _ConvertTestsuiteChildren(self, testsuitesNode: _Element, newTestsuite: Testsuite) -> None:
1533 for node in testsuitesNode.iterchildren(): # type: _Element
1534 # if node.tag == "testsuite":
1535 # self._ConvertTestsuite(newTestsuite, node)
1536 # el
1537 if node.tag == "testcase":
1538 self._ConvertTestcase(newTestsuite, node)
1540 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None:
1541 """
1542 Convert the XML data structure of a ``<testcase>`` to a test case.
1544 This method uses private helper methods provided by the base-class.
1546 :param parent: The test suite as a parent element in the test entity hierarchy.
1547 :param testcaseNode: The current XML element node representing a test case.
1548 """
1549 className = self._ConvertClassname(testcaseNode)
1550 testclass = self._FindOrCreateTestclass(parent, className)
1552 newTestcase = self._TESTCASE(
1553 self._ConvertName(testcaseNode, optional=False),
1554 self._ConvertTime(testcaseNode, optional=False),
1555 assertionCount=self._ConvertAssertions(testcaseNode),
1556 parent=testclass
1557 )
1559 self._ConvertTestcaseChildren(testcaseNode, newTestcase)
1561 def _FindOrCreateTestclass(self, parent: Testsuite, className: str) -> Testclass:
1562 if className in parent._testclasses:
1563 return parent._testclasses[className]
1564 else:
1565 return self._TESTCLASS(className, parent=parent)
1567 def _ConvertTestcaseChildren(self, testcaseNode: _Element, newTestcase: Testcase) -> None:
1568 for node in testcaseNode.iterchildren(): # type: _Element
1569 if isinstance(node, _Comment): 1569 ↛ 1570line 1569 didn't jump to line 1570 because the condition on line 1569 was never true
1570 pass
1571 elif isinstance(node, _Element): 1571 ↛ 1587line 1571 didn't jump to line 1587 because the condition on line 1571 was always true
1572 if node.tag == "skipped":
1573 newTestcase._status = TestcaseStatus.Skipped
1574 elif node.tag == "failure":
1575 newTestcase._status = TestcaseStatus.Failed
1576 elif node.tag == "error": 1576 ↛ 1577line 1576 didn't jump to line 1577 because the condition on line 1576 was never true
1577 newTestcase._status = TestcaseStatus.Errored
1578 elif node.tag == "system-out":
1579 pass
1580 elif node.tag == "system-err":
1581 pass
1582 elif node.tag == "properties": 1582 ↛ 1585line 1582 didn't jump to line 1585 because the condition on line 1582 was always true
1583 pass
1584 else:
1585 raise UnittestException(f"Unknown element '{node.tag}' in junit file.")
1586 else:
1587 pass
1589 if newTestcase._status is TestcaseStatus.Unknown:
1590 newTestcase._status = TestcaseStatus.Passed
1592 def Generate(self, overwrite: bool = False) -> None:
1593 """
1594 Generate the internal XML data structure from test suites and test cases.
1596 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods.
1598 :param overwrite: Overwrite the internal XML data structure.
1599 :raises UnittestException: If overwrite is false and the internal XML data structure is not empty.
1600 """
1601 if not overwrite and self._xmlDocument is not None: 1601 ↛ 1602line 1601 didn't jump to line 1602 because the condition on line 1601 was never true
1602 raise UnittestException(f"Internal XML document is populated with data.")
1604 rootElement = Element("testsuites")
1605 rootElement.attrib["name"] = self._name
1606 if self._startTime is not None:
1607 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}"
1608 if self._duration is not None:
1609 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}"
1610 rootElement.attrib["tests"] = str(self._tests)
1611 rootElement.attrib["failures"] = str(self._failed)
1612 rootElement.attrib["errors"] = str(self._errored)
1613 rootElement.attrib["skipped"] = str(self._skipped)
1614 # if self._assertionCount is not None:
1615 # rootElement.attrib["assertions"] = f"{self._assertionCount}"
1617 self._xmlDocument = ElementTree(rootElement)
1619 for testsuite in self._testsuites.values():
1620 self._GenerateTestsuite(testsuite, rootElement)
1622 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None:
1623 """
1624 Generate the internal XML data structure for a test suite.
1626 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods.
1628 :param testsuite: The test suite to convert to an XML data structures.
1629 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1630 :return:
1631 """
1632 testsuiteElement = SubElement(parentElement, "testsuite")
1633 testsuiteElement.attrib["name"] = testsuite._name
1634 if testsuite._startTime is not None: 1634 ↛ 1636line 1634 didn't jump to line 1636 because the condition on line 1634 was always true
1635 testsuiteElement.attrib["timestamp"] = f"{testsuite._startTime.isoformat()}"
1636 if testsuite._duration is not None:
1637 testsuiteElement.attrib["time"] = f"{testsuite._duration.total_seconds():.6f}"
1638 testsuiteElement.attrib["tests"] = str(testsuite._tests)
1639 testsuiteElement.attrib["failures"] = str(testsuite._failed)
1640 testsuiteElement.attrib["errors"] = str(testsuite._errored)
1641 testsuiteElement.attrib["skipped"] = str(testsuite._skipped)
1642 # if testsuite._assertionCount is not None:
1643 # testsuiteElement.attrib["assertions"] = f"{testsuite._assertionCount}"
1644 if testsuite._hostname is not None: 1644 ↛ 1645line 1644 didn't jump to line 1645 because the condition on line 1644 was never true
1645 testsuiteElement.attrib["hostname"] = testsuite._hostname
1647 for testclass in testsuite._testclasses.values():
1648 for tc in testclass._testcases.values():
1649 self._GenerateTestcase(tc, testsuiteElement)
1651 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
1652 """
1653 Generate the internal XML data structure for a test case.
1655 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
1657 :param testcase: The test case to convert to an XML data structures.
1658 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1659 :return:
1660 """
1661 testcaseElement = SubElement(parentElement, "testcase")
1662 if testcase.Classname is not None: 1662 ↛ 1664line 1662 didn't jump to line 1664 because the condition on line 1662 was always true
1663 testcaseElement.attrib["classname"] = testcase.Classname
1664 testcaseElement.attrib["name"] = testcase._name
1665 if testcase._duration is not None: 1665 ↛ 1667line 1665 didn't jump to line 1667 because the condition on line 1665 was always true
1666 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}"
1667 if testcase._assertionCount is not None:
1668 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}"
1670 if testcase._status is TestcaseStatus.Passed:
1671 pass
1672 elif testcase._status is TestcaseStatus.Failed: 1672 ↛ 1673line 1672 didn't jump to line 1673 because the condition on line 1672 was never true
1673 failureElement = SubElement(testcaseElement, "failure")
1674 elif testcase._status is TestcaseStatus.Skipped:
1675 skippedElement = SubElement(testcaseElement, "skipped")
1676 else:
1677 errorElement = SubElement(testcaseElement, "error")
1679 def __str__(self) -> str:
1680 moduleName = self.__module__.split(".")[-1]
1681 className = self.__class__.__name__
1682 return (
1683 f"<{moduleName}{className} {self._name} ({self._path}): {self._status.name} - suites/tests:{self.TestsuiteCount}/{self.TestcaseCount}>"
1684 )