Coverage for pyEDAA/Reports/Unittesting/JUnit/__init__.py: 72%
704 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-07 22:48 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-07 22:48 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2024-2026 Electronic Design Automation Abstraction (EDA²) #
15# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""
33The pyEDAA.Reports.Unittesting.JUnit package implements a hierarchy of test entities for the JUnit unit testing summary
34file format (XML format). This test entity hierarchy is not derived from :class:`pyEDAA.Reports.Unittesting`, because it
35doesn't match the unified data model. Nonetheless, both data models can be converted to each other. In addition, derived
36data models are provided for the many dialects of that XML file format. See the list modules in this package for the
37implemented dialects.
39The test entity hierarchy consists of test cases, test classes, test suites and a test summary. Test cases are the leaf
40elements in the hierarchy and represent an individual test run. Next, test classes group test cases, because the
41original Ant + JUnit format groups test cases (Java methods) in a Java class. Next, test suites are used to group
42multiple test classes. Finally, the root element is a test summary. When such a summary is stored in a file format like
43Ant + JUnit4 XML, a file format specific document is derived from a summary class.
45**Data Model**
47.. mermaid::
49 graph TD;
50 doc[Document]
51 sum[Summary]
52 ts1[Testsuite]
53 ts11[Testsuite]
54 ts2[Testsuite]
56 tc111[Testclass]
57 tc112[Testclass]
58 tc23[Testclass]
60 tc1111[Testcase]
61 tc1112[Testcase]
62 tc1113[Testcase]
63 tc1121[Testcase]
64 tc1122[Testcase]
65 tc231[Testcase]
66 tc232[Testcase]
67 tc233[Testcase]
69 doc:::root -.-> sum:::summary
70 sum --> ts1:::suite
71 sum ---> ts2:::suite
72 ts1 --> ts11:::suite
74 ts11 --> tc111:::cls
75 ts11 --> tc112:::cls
76 ts2 --> tc23:::cls
78 tc111 --> tc1111:::case
79 tc111 --> tc1112:::case
80 tc111 --> tc1113:::case
81 tc112 --> tc1121:::case
82 tc112 --> tc1122:::case
83 tc23 --> tc231:::case
84 tc23 --> tc232:::case
85 tc23 --> tc233:::case
87 classDef root fill:#4dc3ff
88 classDef summary fill:#80d4ff
89 classDef suite fill:#b3e6ff
90 classDef cls fill:#ff9966
91 classDef case fill:#eeccff
92"""
93from 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) -> 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: When parameter 'name' is None.
199 :raises TypeError: When parameter 'name' is not a string.
200 :raises ValueError: When 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 to access the reference to the parent test entity.
220 :returns: 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 to access the test entity's name.
231 :returns: Name of the test entity.
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 ) -> None:
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 to access the duration of a test entity run.
291 .. note::
293 The JUnit format doesn't distinguish setup, run and teardown durations.
295 :returns: 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 to access the number of assertions (checks) in a test case.
305 .. note::
307 The JUnit format doesn't distinguish passed and failed assertions.
309 :returns: Number of assertions.
310 """
312 def __len__(self) -> int:
313 """
314 Returns the number of annotated properties.
316 Syntax: :pycode:`length = len(obj)`
318 :returns: 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 :returns: 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 :returns: 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 :returns: 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 ) -> None:
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 to access the class name of the test case.
434 :returns: 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 to access the status of the test case.
451 :returns: The test case's status.
452 """
453 return self._status
455 @readonly
456 def AssertionCount(self) -> int:
457 """
458 Read-only property to return the number of assertions (checks) in a test case.
460 .. note::
462 The JUnit format doesn't distinguish passed and failed assertions.
464 :returns: 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 :returns: 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 ) -> None:
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 """
591 Read-only property to access the time the test entity's execution started.
593 :returns: Start time of the execution, or ``None`` if it wasn't recorded.
594 """
595 return self._startTime
597 @readonly
598 def Status(self) -> TestsuiteStatus:
599 """
600 Read-only property to access the test entity's aggregated status.
602 :returns: Status of the test entity.
603 """
604 return self._status
606 @readonly
607 @mustoverride
608 def TestcaseCount(self) -> int:
609 """
610 Read-only property to access the number of testcases in this entity.
612 :returns: Number of testcases.
613 """
614 pass
616 @readonly
617 def Tests(self) -> int:
618 """
619 Read-only property to access the number of testcases in this entity.
621 :returns: Number of testcases.
622 """
623 return self.TestcaseCount
625 @readonly
626 def Skipped(self) -> int:
627 """
628 Read-only property to access the number of skipped testcases.
630 :returns: Number of skipped testcases.
631 """
632 return self._skipped
634 @readonly
635 def Errored(self) -> int:
636 """
637 Read-only property to access the number of errored testcases.
639 :returns: Number of errored testcases.
640 """
641 return self._errored
643 @readonly
644 def Failed(self) -> int:
645 """
646 Read-only property to access the number of failed testcases.
648 :returns: Number of failed testcases.
649 """
650 return self._failed
652 @readonly
653 def Passed(self) -> int:
654 """
655 Read-only property to access the number of passed testcases.
657 :returns: Number of passed testcases.
658 """
659 return self._passed
661 def Aggregate(self) -> TestsuiteAggregateReturnType:
662 tests = 0
663 skipped = 0
664 errored = 0
665 weak = 0
666 failed = 0
667 passed = 0
669 # for testsuite in self._testsuites.values():
670 # t, s, e, w, f, p = testsuite.Aggregate()
671 # tests += t
672 # skipped += s
673 # errored += e
674 # weak += w
675 # failed += f
676 # passed += p
678 return tests, skipped, errored, weak, failed, passed
680 @mustoverride
681 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
682 pass
685@export
686class Testclass(Base):
687 """
688 A test class is a low-level element in the test entity hierarchy representing a group of tests.
690 Test classes contain test cases and are grouped by a test suites.
691 """
693 _testcases: Dict[str, "Testcase"]
695 def __init__(
696 self,
697 classname: str,
698 testcases: Nullable[Iterable["Testcase"]] = None,
699 parent: Nullable["Testsuite"] = None
700 ) -> None:
701 """
702 Initializes the fields of the test class.
704 :param classname: Classname of the test entity.
705 :param parent: Reference to the parent test suite.
706 :raises ValueError: If parameter 'classname' is None.
707 :raises TypeError: If parameter 'classname' is not a string.
708 :raises ValueError: If parameter 'classname' is empty.
709 """
710 if parent is not None:
711 if not isinstance(parent, Testsuite): 711 ↛ 712line 711 didn't jump to line 712 because the condition on line 711 was never true
712 ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.")
713 if version_info >= (3, 11): # pragma: no cover
714 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
715 raise ex
717 parent._testclasses[classname] = self
719 super().__init__(classname, parent)
721 self._testcases = {}
722 if testcases is not None:
723 for testcase in testcases:
724 if testcase._parent is not None: 724 ↛ 725line 724 didn't jump to line 725 because the condition on line 724 was never true
725 raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
727 if testcase._name in self._testcases: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true
728 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
730 testcase._parent = self
731 self._testcases[testcase._name] = testcase
733 @readonly
734 def Classname(self) -> str:
735 """
736 Read-only property to access the name of the test class.
738 :returns: The test class' name.
739 """
740 return self._name
742 @readonly
743 def Testcases(self) -> Dict[str, "Testcase"]:
744 """
745 Read-only property to access a reference to the internal dictionary of test cases.
747 :returns: Reference to the dictionary of test cases.
748 """
749 return self._testcases
751 @readonly
752 def TestcaseCount(self) -> int:
753 """
754 Read-only property to return the number of all test cases in the test entity hierarchy.
756 :returns: Number of test cases.
757 """
758 return len(self._testcases)
760 @readonly
761 def AssertionCount(self) -> int:
762 """
763 Read-only property to return the number of assertions across all testcases of this testclass.
765 :returns: Sum of the testcases' assertion counts.
766 """
767 return sum(tc.AssertionCount for tc in self._testcases.values())
769 def AddTestcase(self, testcase: "Testcase") -> None:
770 if testcase._parent is not None: 770 ↛ 771line 770 didn't jump to line 771 because the condition on line 770 was never true
771 raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.")
773 if testcase._name in self._testcases: 773 ↛ 774line 773 didn't jump to line 774 because the condition on line 773 was never true
774 raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.")
776 testcase._parent = self
777 self._testcases[testcase._name] = testcase
779 def AddTestcases(self, testcases: Iterable["Testcase"]) -> None:
780 for testcase in testcases:
781 self.AddTestcase(testcase)
783 def ToTestsuite(self) -> ut_Testsuite:
784 return ut_Testsuite(
785 self._name,
786 TestsuiteKind.Class,
787 # startTime=self._startTime,
788 # totalDuration=self._duration,
789 # status=self._status,
790 testcases=(tc.ToTestcase() for tc in self._testcases.values())
791 )
793 def ToTree(self) -> Node:
794 node = Node(
795 value=self._name,
796 children=(tc.ToTree() for tc in self._testcases.values())
797 )
799 return node
801 def __str__(self) -> str:
802 moduleName = self.__module__.split(".")[-1]
803 className = self.__class__.__name__
804 return (
805 f"<{moduleName}{className} {self._name}: {len(self._testcases)}>"
806 )
809@export
810class Testsuite(TestsuiteBase):
811 """
812 A testsuite is a mid-level element in the test entity hierarchy representing a logical group of tests.
814 Test suites contain test classes and are grouped by a test summary, which is the root of the hierarchy.
815 """
817 _hostname: str
818 _testclasses: Dict[str, "Testclass"]
820 def __init__(
821 self,
822 name: str,
823 hostname: Nullable[str] = None,
824 startTime: Nullable[datetime] = None,
825 duration: Nullable[timedelta] = None,
826 status: TestsuiteStatus = TestsuiteStatus.Unknown,
827 testclasses: Nullable[Iterable["Testclass"]] = None,
828 parent: Nullable["TestsuiteSummary"] = None
829 ) -> None:
830 """
831 Initializes the fields of a test suite.
833 :param name: Name of the test suite.
834 :param startTime: Time when the test suite was started.
835 :param duration: duration of the entity's execution.
836 :param status: Overall status of the test suite.
837 :param parent: Reference to the parent test summary.
838 :raises TypeError: If parameter 'testcases' is not iterable.
839 :raises TypeError: If element in parameter 'testcases' is not a Testcase.
840 :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy.
841 :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases.
842 """
843 if parent is not None:
844 if not isinstance(parent, TestsuiteSummary): 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true
845 ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteSummary'.")
846 if version_info >= (3, 11): # pragma: no cover
847 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
848 raise ex
850 parent._testsuites[name] = self
852 super().__init__(name, startTime, duration, status, parent)
854 self._hostname = hostname
856 self._testclasses = {}
857 if testclasses is not None:
858 for testclass in testclasses:
859 if testclass._parent is not None: 859 ↛ 860line 859 didn't jump to line 860 because the condition on line 859 was never true
860 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
862 if testclass._name in self._testclasses: 862 ↛ 863line 862 didn't jump to line 863 because the condition on line 862 was never true
863 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
865 testclass._parent = self
866 self._testclasses[testclass._name] = testclass
868 @readonly
869 def Hostname(self) -> Nullable[str]:
870 """
871 Read-only property to access the host the testsuite was executed on.
873 :returns: Hostname, or ``None`` if it wasn't recorded.
874 """
875 return self._hostname
877 @readonly
878 def Testclasses(self) -> Dict[str, "Testclass"]:
879 """
880 Read-only property to access the testsuite's testclasses.
882 :returns: Dictionary of testclass names and testclasses.
883 """
884 return self._testclasses
886 @readonly
887 def TestclassCount(self) -> int:
888 """
889 Read-only property to return the number of testclasses in this testsuite.
891 :returns: Number of testclasses.
892 """
893 return len(self._testclasses)
895 # @readonly
896 # def Testcases(self) -> Dict[str, "Testcase"]:
897 # return self._classes
899 @readonly
900 def TestcaseCount(self) -> int:
901 """
902 Read-only property to return the number of testcases across all testclasses.
904 :returns: Sum of the testclasses' testcase counts.
905 """
906 return sum(cls.TestcaseCount for cls in self._testclasses.values())
908 @readonly
909 def AssertionCount(self) -> int:
910 """
911 Read-only property to return the number of assertions across all testclasses.
913 :returns: Sum of the testclasses' assertion counts.
914 """
915 return sum(cls.AssertionCount for cls in self._testclasses.values())
917 def AddTestclass(self, testclass: "Testclass") -> None:
918 if testclass._parent is not None: 918 ↛ 919line 918 didn't jump to line 919 because the condition on line 918 was never true
919 raise ValueError(f"Class '{testclass._name}' is already part of a testsuite hierarchy.")
921 if testclass._name in self._testclasses: 921 ↛ 922line 921 didn't jump to line 922 because the condition on line 921 was never true
922 raise DuplicateTestcaseException(f"Testsuite already contains a class with same name '{testclass._name}'.")
924 testclass._parent = self
925 self._testclasses[testclass._name] = testclass
927 def AddTestclasses(self, testclasses: Iterable["Testclass"]) -> None:
928 for testcase in testclasses:
929 self.AddTestclass(testcase)
931 # def IterateTestsuites(self, scheme: IterationScheme = IterationScheme.TestsuiteDefault) -> Generator[TestsuiteType, None, None]:
932 # return self.Iterate(scheme)
934 def IterateTestcases(self, scheme: IterationScheme = IterationScheme.TestcaseDefault) -> Generator[Testcase, None, None]:
935 return self.Iterate(scheme)
937 def Copy(self) -> "Testsuite":
938 return self.__class__(
939 self._name,
940 self._hostname,
941 self._startTime,
942 self._duration,
943 self._status
944 )
946 def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType:
947 tests, skipped, errored, weak, failed, passed = super().Aggregate()
949 for testclass in self._testclasses.values():
950 for testcase in testclass._testcases.values():
951 _ = testcase.Aggregate()
953 status = testcase._status
954 if status is TestcaseStatus.Unknown: 954 ↛ 955line 954 didn't jump to line 955 because the condition on line 954 was never true
955 raise UnittestException(f"Found testcase '{testcase._name}' with state 'Unknown'.")
956 elif status is TestcaseStatus.Skipped:
957 skipped += 1
958 elif status is TestcaseStatus.Errored: 958 ↛ 959line 958 didn't jump to line 959 because the condition on line 958 was never true
959 errored += 1
960 elif status is TestcaseStatus.Passed:
961 passed += 1
962 elif status is TestcaseStatus.Failed: 962 ↛ 964line 962 didn't jump to line 964 because the condition on line 962 was always true
963 failed += 1
964 elif status is TestcaseStatus.Weak:
965 weak += 1
966 elif status & TestcaseStatus.Mask is not TestcaseStatus.Unknown:
967 raise UnittestException(f"Found testcase '{testcase._name}' with unsupported state '{status}'.")
968 else:
969 raise UnittestException(f"Internal error for testcase '{testcase._name}', field '_status' is '{status}'.")
971 self._tests = tests
972 self._skipped = skipped
973 self._errored = errored
974 self._weak = weak
975 self._failed = failed
976 self._passed = passed
978 # FIXME: weak?
979 if errored > 0: 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true
980 self._status = TestsuiteStatus.Errored
981 elif failed > 0:
982 self._status = TestsuiteStatus.Failed
983 elif tests == 0: 983 ↛ 985line 983 didn't jump to line 985 because the condition on line 983 was always true
984 self._status = TestsuiteStatus.Empty
985 elif tests - skipped == passed:
986 self._status = TestsuiteStatus.Passed
987 elif tests == skipped:
988 self._status = TestsuiteStatus.Skipped
989 else:
990 self._status = TestsuiteStatus.Unknown
992 return tests, skipped, errored, weak, failed, passed
994 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]:
995 """
996 Iterate the test suite and its child elements according to the iteration scheme.
998 If no scheme is given, use the default scheme.
1000 :param scheme: Scheme how to iterate the test suite and its child elements.
1001 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
1002 """
1003 if IterationScheme.PreOrder in scheme:
1004 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1005 yield self
1007 if IterationScheme.IncludeTestcases in scheme:
1008 for testcase in self._testclasses.values():
1009 yield testcase
1011 for testclass in self._testclasses.values():
1012 yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf)
1014 if IterationScheme.PostOrder in scheme:
1015 if IterationScheme.IncludeTestcases in scheme:
1016 for testcase in self._testclasses.values():
1017 yield testcase
1019 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme:
1020 yield self
1022 @classmethod
1023 def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite":
1024 """
1025 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object.
1027 :param testsuite: Test suite from unified data model.
1028 :returns: Test suite from JUnit specific data model.
1029 """
1030 juTestsuite = cls(
1031 testsuite._name,
1032 startTime=testsuite._startTime,
1033 duration=testsuite._totalDuration,
1034 status= testsuite._status,
1035 )
1037 juTestsuite._tests = testsuite._tests
1038 juTestsuite._skipped = testsuite._skipped
1039 juTestsuite._errored = testsuite._errored
1040 juTestsuite._failed = testsuite._failed
1041 juTestsuite._passed = testsuite._passed
1043 for tc in testsuite.IterateTestcases():
1044 ts = tc._parent
1045 if ts is None: 1045 ↛ 1046line 1045 didn't jump to line 1046 because the condition on line 1045 was never true
1046 raise UnittestException(f"Testcase '{tc._name}' is not part of a hierarchy.")
1048 classname = ts._name
1049 ts = ts._parent
1050 while ts is not None and ts._kind > TestsuiteKind.Logical:
1051 classname = f"{ts._name}.{classname}"
1052 ts = ts._parent
1054 if classname in juTestsuite._testclasses:
1055 juClass = juTestsuite._testclasses[classname]
1056 else:
1057 juClass = Testclass(classname, parent=juTestsuite)
1059 juClass.AddTestcase(Testcase.FromTestcase(tc))
1061 return juTestsuite
1063 def ToTestsuite(self) -> ut_Testsuite:
1064 testsuite = ut_Testsuite(
1065 self._name,
1066 TestsuiteKind.Logical,
1067 startTime=self._startTime,
1068 totalDuration=self._duration,
1069 status=self._status,
1070 )
1072 for testclass in self._testclasses.values():
1073 suite = testsuite
1074 classpath = testclass._name.split(".")
1075 for element in classpath:
1076 if element in suite._testsuites:
1077 suite = suite._testsuites[element]
1078 else:
1079 suite = ut_Testsuite(element, kind=TestsuiteKind.Package, parent=suite)
1081 suite._kind = TestsuiteKind.Class
1082 if suite._parent is not testsuite: 1082 ↛ 1085line 1082 didn't jump to line 1085 because the condition on line 1082 was always true
1083 suite._parent._kind = TestsuiteKind.Module
1085 suite.AddTestcases(tc.ToTestcase() for tc in testclass._testcases.values())
1087 return testsuite
1089 def ToTree(self) -> Node:
1090 node = Node(
1091 value=self._name,
1092 children=(cls.ToTree() for cls in self._testclasses.values())
1093 )
1094 node["startTime"] = self._startTime
1095 node["duration"] = self._duration
1097 return node
1099 def __str__(self) -> str:
1100 moduleName = self.__module__.split(".")[-1]
1101 className = self.__class__.__name__
1102 return (
1103 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1104 )
1107@export
1108class TestsuiteSummary(TestsuiteBase):
1109 _testsuites: Dict[str, Testsuite]
1111 def __init__(
1112 self,
1113 name: str,
1114 startTime: Nullable[datetime] = None,
1115 duration: Nullable[timedelta] = None,
1116 status: TestsuiteStatus = TestsuiteStatus.Unknown,
1117 testsuites: Nullable[Iterable[Testsuite]] = None
1118 ) -> None:
1119 super().__init__(name, startTime, duration, status, None)
1121 self._testsuites = {}
1122 if testsuites is not None:
1123 for testsuite in testsuites:
1124 if testsuite._parent is not None: 1124 ↛ 1125line 1124 didn't jump to line 1125 because the condition on line 1124 was never true
1125 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1127 if testsuite._name in self._testsuites: 1127 ↛ 1128line 1127 didn't jump to line 1128 because the condition on line 1127 was never true
1128 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1130 testsuite._parent = self
1131 self._testsuites[testsuite._name] = testsuite
1133 @readonly
1134 def Testsuites(self) -> Dict[str, Testsuite]:
1135 """
1136 Read-only property to access the summary's testsuites.
1138 :returns: Dictionary of testsuite names and testsuites.
1139 """
1140 return self._testsuites
1142 @readonly
1143 def TestcaseCount(self) -> int:
1144 """
1145 Read-only property to return the number of testcases across all testsuites.
1147 :returns: Sum of the testsuites' testcase counts.
1148 """
1149 return sum(ts.TestcaseCount for ts in self._testsuites.values())
1151 @readonly
1152 def TestsuiteCount(self) -> int:
1153 """
1154 Read-only property to return the number of testsuites in this summary.
1156 :returns: Number of testsuites.
1157 """
1158 return len(self._testsuites)
1160 @readonly
1161 def AssertionCount(self) -> int:
1162 """
1163 Read-only property to return the number of assertions across all testsuites.
1165 :returns: Sum of the testsuites' assertion counts.
1166 """
1167 return sum(ts.AssertionCount for ts in self._testsuites.values())
1169 def AddTestsuite(self, testsuite: Testsuite) -> None:
1170 if testsuite._parent is not None: 1170 ↛ 1171line 1170 didn't jump to line 1171 because the condition on line 1170 was never true
1171 raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.")
1173 if testsuite._name in self._testsuites: 1173 ↛ 1174line 1173 didn't jump to line 1174 because the condition on line 1173 was never true
1174 raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.")
1176 testsuite._parent = self
1177 self._testsuites[testsuite._name] = testsuite
1179 def AddTestsuites(self, testsuites: Iterable[Testsuite]) -> None:
1180 for testsuite in testsuites:
1181 self.AddTestsuite(testsuite)
1183 def Aggregate(self) -> TestsuiteAggregateReturnType:
1184 tests, skipped, errored, weak, failed, passed = super().Aggregate()
1186 for testsuite in self._testsuites.values():
1187 t, s, e, w, f, p = testsuite.Aggregate()
1188 tests += t
1189 skipped += s
1190 errored += e
1191 weak += w
1192 failed += f
1193 passed += p
1195 self._tests = tests
1196 self._skipped = skipped
1197 self._errored = errored
1198 self._weak = weak
1199 self._failed = failed
1200 self._passed = passed
1202 # FIXME: weak
1203 if errored > 0: 1203 ↛ 1204line 1203 didn't jump to line 1204 because the condition on line 1203 was never true
1204 self._status = TestsuiteStatus.Errored
1205 elif failed > 0:
1206 self._status = TestsuiteStatus.Failed
1207 elif tests == 0: 1207 ↛ 1209line 1207 didn't jump to line 1209 because the condition on line 1207 was always true
1208 self._status = TestsuiteStatus.Empty
1209 elif tests - skipped == passed:
1210 self._status = TestsuiteStatus.Passed
1211 elif tests == skipped:
1212 self._status = TestsuiteStatus.Skipped
1213 else:
1214 self._status = TestsuiteStatus.Unknown
1216 return tests, skipped, errored, weak, failed, passed
1218 def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[Testsuite, Testcase], None, None]:
1219 """
1220 Iterate the test suite summary and its child elements according to the iteration scheme.
1222 If no scheme is given, use the default scheme.
1224 :param scheme: Scheme how to iterate the test suite summary and its child elements.
1225 :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme.
1226 """
1227 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PreOrder in scheme:
1228 yield self
1230 for testsuite in self._testsuites.values():
1231 yield from testsuite.IterateTestsuites(scheme | IterationScheme.IncludeSelf)
1233 if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites | IterationScheme.PostOrder in scheme:
1234 yield self
1236 @classmethod
1237 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> "TestsuiteSummary":
1238 """
1239 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite.
1241 :param testsuiteSummary: Test suite summary from unified data model.
1242 :returns: Test suite summary from JUnit specific data model.
1243 """
1244 return cls(
1245 testsuiteSummary._name,
1246 startTime=testsuiteSummary._startTime,
1247 duration=testsuiteSummary._totalDuration,
1248 status=testsuiteSummary._status,
1249 testsuites=(ut_Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1250 )
1252 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
1253 """
1254 Convert this test suite summary a new test suite summary of the unified data model.
1256 All fields are copied to the new instance. Child elements like test suites are copied recursively.
1258 :returns: A test suite summary of the unified test entity data model.
1259 """
1260 return ut_TestsuiteSummary(
1261 self._name,
1262 startTime=self._startTime,
1263 totalDuration=self._duration,
1264 status=self._status,
1265 testsuites=(testsuite.ToTestsuite() for testsuite in self._testsuites.values())
1266 )
1268 def ToTree(self) -> Node:
1269 node = Node(
1270 value=self._name,
1271 children=(ts.ToTree() for ts in self._testsuites.values())
1272 )
1273 node["startTime"] = self._startTime
1274 node["duration"] = self._duration
1276 return node
1278 def __str__(self) -> str:
1279 moduleName = self.__module__.split(".")[-1]
1280 className = self.__class__.__name__
1281 return (
1282 f"<{moduleName}{className} {self._name}: {self._status.name} - tests:{self._tests}>"
1283 )
1286@export
1287class Document(TestsuiteSummary, ut_Document):
1288 _TESTCASE: ClassVar[Type[Testcase]] = Testcase
1289 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass
1290 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite
1292 _readerMode: JUnitReaderMode
1293 _xmlDocument: Nullable[_ElementTree]
1295 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False, readerMode: JUnitReaderMode = JUnitReaderMode.Default) -> None:
1296 super().__init__("Unprocessed JUnit XML file")
1298 self._readerMode = readerMode
1299 self._xmlDocument = None
1301 ut_Document.__init__(self, xmlReportFile, analyzeAndConvert)
1303 @classmethod
1304 def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary):
1305 doc = cls(xmlReportFile)
1306 doc._name = testsuiteSummary._name
1307 doc._startTime = testsuiteSummary._startTime
1308 doc._duration = testsuiteSummary._totalDuration
1309 doc._status = testsuiteSummary._status
1310 doc._tests = testsuiteSummary._tests
1311 doc._skipped = testsuiteSummary._skipped
1312 doc._errored = testsuiteSummary._errored
1313 doc._failed = testsuiteSummary._failed
1314 doc._passed = testsuiteSummary._passed
1316 doc.AddTestsuites(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
1318 return doc
1320 def Analyze(self) -> None:
1321 """
1322 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML
1323 schema.
1325 .. hint::
1327 The time spend for analysis will be made available via property :data:`AnalysisDuration`.
1329 The used XML schema definition is generic to support "any" dialect.
1330 """
1331 xmlSchemaFile = "Any-JUnit.xsd"
1332 self._Analyze(xmlSchemaFile)
1334 def _Analyze(self, xmlSchemaFile: str) -> None:
1335 if not self._path.exists(): 1335 ↛ 1336line 1335 didn't jump to line 1336 because the condition on line 1335 was never true
1336 raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \
1337 from FileNotFoundError(f"File '{self._path}' not found.")
1339 startAnalysis = perf_counter_ns()
1340 try:
1341 xmlSchemaResourceFile = getResourceFile(Resources, xmlSchemaFile)
1342 except ToolingException as ex:
1343 raise UnittestException(f"Couldn't locate XML Schema '{xmlSchemaFile}' in package resources.") from ex
1345 try:
1346 schemaParser = XMLParser(ns_clean=True)
1347 schemaRoot = parse(xmlSchemaResourceFile, schemaParser)
1348 except XMLSyntaxError as ex:
1349 raise UnittestException(f"XML Syntax Error while parsing XML Schema '{xmlSchemaFile}'.") from ex
1351 try:
1352 junitSchema = XMLSchema(schemaRoot)
1353 except XMLSchemaParseError as ex:
1354 raise UnittestException(f"Error while parsing XML Schema '{xmlSchemaFile}'.")
1356 try:
1357 junitParser = XMLParser(schema=junitSchema, ns_clean=True)
1358 junitDocument = parse(self._path, parser=junitParser)
1360 self._xmlDocument = junitDocument
1361 except XMLSyntaxError as ex:
1362 if version_info >= (3, 11): # pragma: no cover
1363 for logEntry in junitParser.error_log:
1364 ex.add_note(str(logEntry))
1365 raise UnittestException(f"XML syntax or validation error for '{self._path}' using XSD schema '{xmlSchemaResourceFile}'.") from ex
1366 except Exception as ex:
1367 raise UnittestException(f"Couldn't open '{self._path}'.") from ex
1369 endAnalysis = perf_counter_ns()
1370 self._analysisDuration = (endAnalysis - startAnalysis) / 1e9
1372 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None:
1373 """
1374 Write the data model as XML into a file adhering to the Any JUnit dialect.
1376 :param path: Optional path to the XMl file, if internal path shouldn't be used.
1377 :param overwrite: If true, overwrite an existing file.
1378 :param regenerate: If true, regenerate the XML structure from data model.
1379 :raises UnittestException: If the file cannot be overwritten.
1380 :raises UnittestException: If the internal XML data structure wasn't generated.
1381 :raises UnittestException: If the file cannot be opened or written.
1382 """
1383 if path is None:
1384 path = self._path
1386 if not overwrite and path.exists(): 1386 ↛ 1387line 1386 didn't jump to line 1387 because the condition on line 1386 was never true
1387 raise UnittestException(f"JUnit XML file '{path}' can not be overwritten.") \
1388 from FileExistsError(f"File '{path}' already exists.")
1390 if regenerate:
1391 self.Generate(overwrite=True)
1393 if self._xmlDocument is None: 1393 ↛ 1394line 1393 didn't jump to line 1394 because the condition on line 1393 was never true
1394 ex = UnittestException(f"Internal XML document tree is empty and needs to be generated before write is possible.")
1395 ex.add_note(f"Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.")
1396 raise ex
1398 try:
1399 with path.open("wb") as file:
1400 file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True))
1401 except Exception as ex:
1402 raise UnittestException(f"JUnit XML file '{path}' can not be written.") from ex
1404 def Convert(self) -> None:
1405 """
1406 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
1408 This method converts the root element.
1410 .. hint::
1412 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
1414 :raises UnittestException: If XML was not read and parsed before.
1415 """
1416 if self._xmlDocument is None: 1416 ↛ 1417line 1416 didn't jump to line 1417 because the condition on line 1416 was never true
1417 ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.")
1418 ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.")
1419 raise ex
1421 startConversion = perf_counter_ns()
1422 rootElement: _Element = self._xmlDocument.getroot()
1424 self._name = self._ConvertName(rootElement, optional=True)
1425 self._startTime = self._ConvertTimestamp(rootElement, optional=True)
1426 self._duration = self._ConvertTime(rootElement, optional=True)
1428 if False: # self._readerMode is JUnitReaderMode.
1429 self._tests = self._ConvertTests(testsuitesNode)
1430 self._skipped = self._ConvertSkipped(testsuitesNode)
1431 self._errored = self._ConvertErrors(testsuitesNode)
1432 self._failed = self._ConvertFailures(testsuitesNode)
1433 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1435 for rootNode in rootElement.iterchildren(tag="testsuite"): # type: _Element
1436 self._ConvertTestsuite(self, rootNode)
1438 if True: # self._readerMode is JUnitReaderMode.
1439 self.Aggregate()
1441 endConversation = perf_counter_ns()
1442 self._modelConversion = (endConversation - startConversion) / 1e9
1444 def _ConvertName(self, element: _Element, default: str = "root", optional: bool = True) -> str:
1445 """
1446 Convert the ``name`` attribute from an XML element node to a string.
1448 :param element: The XML element node with a ``name`` attribute.
1449 :param default: The default value, if no ``name`` attribute was found.
1450 :param optional: If false, an exception is raised for the missing attribute.
1451 :returns: The ``name`` attribute's content if found, otherwise the given default value.
1452 :raises UnittestException: If optional is false and no ``name`` attribute exists on the given element node.
1453 """
1454 if "name" in element.attrib:
1455 return element.attrib["name"]
1456 elif not optional: 1456 ↛ 1457line 1456 didn't jump to line 1457 because the condition on line 1456 was never true
1457 raise UnittestException(f"Required parameter 'name' not found in tag '{element.tag}'.")
1458 else:
1459 return default
1461 def _ConvertTimestamp(self, element: _Element, optional: bool = True) -> Nullable[datetime]:
1462 """
1463 Convert the ``timestamp`` attribute from an XML element node to a datetime.
1465 :param element: The XML element node with a ``timestamp`` attribute.
1466 :param optional: If false, an exception is raised for the missing attribute.
1467 :returns: The ``timestamp`` attribute's content if found, otherwise ``None``.
1468 :raises UnittestException: If optional is false and no ``timestamp`` attribute exists on the given element node.
1469 """
1470 if "timestamp" in element.attrib:
1471 timestamp = element.attrib["timestamp"]
1472 return datetime.fromisoformat(timestamp)
1473 elif not optional: 1473 ↛ 1474line 1473 didn't jump to line 1474 because the condition on line 1473 was never true
1474 raise UnittestException(f"Required parameter 'timestamp' not found in tag '{element.tag}'.")
1475 else:
1476 return None
1478 def _ConvertTime(self, element: _Element, optional: bool = True) -> Nullable[timedelta]:
1479 """
1480 Convert the ``time`` attribute from an XML element node to a timedelta.
1482 :param element: The XML element node with a ``time`` attribute.
1483 :param optional: If false, an exception is raised for the missing attribute.
1484 :returns: The ``time`` attribute's content if found, otherwise ``None``.
1485 :raises UnittestException: If optional is false and no ``time`` attribute exists on the given element node.
1486 """
1487 if "time" in element.attrib:
1488 time = element.attrib["time"]
1489 return timedelta(seconds=float(time))
1490 elif not optional: 1490 ↛ 1491line 1490 didn't jump to line 1491 because the condition on line 1490 was never true
1491 raise UnittestException(f"Required parameter 'time' not found in tag '{element.tag}'.")
1492 else:
1493 return None
1495 def _ConvertHostname(self, element: _Element, default: str = "localhost", optional: bool = True) -> str:
1496 """
1497 Convert the ``hostname`` attribute from an XML element node to a string.
1499 :param element: The XML element node with a ``hostname`` attribute.
1500 :param default: The default value, if no ``hostname`` attribute was found.
1501 :param optional: If false, an exception is raised for the missing attribute.
1502 :returns: The ``hostname`` attribute's content if found, otherwise the given default value.
1503 :raises UnittestException: If optional is false and no ``hostname`` attribute exists on the given element node.
1504 """
1505 if "hostname" in element.attrib:
1506 return element.attrib["hostname"]
1507 elif not optional: 1507 ↛ 1508line 1507 didn't jump to line 1508 because the condition on line 1507 was never true
1508 raise UnittestException(f"Required parameter 'hostname' not found in tag '{element.tag}'.")
1509 else:
1510 return default
1512 def _ConvertClassname(self, element: _Element) -> str:
1513 """
1514 Convert the ``classname`` attribute from an XML element node to a string.
1516 :param element: The XML element node with a ``classname`` attribute.
1517 :returns: The ``classname`` attribute's content.
1518 :raises UnittestException: If no ``classname`` attribute exists on the given element node.
1519 """
1520 if "classname" in element.attrib: 1520 ↛ 1523line 1520 didn't jump to line 1523 because the condition on line 1520 was always true
1521 return element.attrib["classname"]
1522 else:
1523 raise UnittestException(f"Required parameter 'classname' not found in tag '{element.tag}'.")
1525 def _ConvertTests(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1526 """
1527 Convert the ``tests`` attribute from an XML element node to an integer.
1529 :param element: The XML element node with a ``tests`` attribute.
1530 :param default: The default value, if no ``tests`` attribute was found.
1531 :param optional: If false, an exception is raised for the missing attribute.
1532 :returns: The ``tests`` attribute's content if found, otherwise the given default value.
1533 :raises UnittestException: If optional is false and no ``tests`` attribute exists on the given element node.
1534 """
1535 if "tests" in element.attrib:
1536 return int(element.attrib["tests"])
1537 elif not optional:
1538 raise UnittestException(f"Required parameter 'tests' not found in tag '{element.tag}'.")
1539 else:
1540 return default
1542 def _ConvertSkipped(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1543 """
1544 Convert the ``skipped`` attribute from an XML element node to an integer.
1546 :param element: The XML element node with a ``skipped`` attribute.
1547 :param default: The default value, if no ``skipped`` attribute was found.
1548 :param optional: If false, an exception is raised for the missing attribute.
1549 :returns: The ``skipped`` attribute's content if found, otherwise the given default value.
1550 :raises UnittestException: If optional is false and no ``skipped`` attribute exists on the given element node.
1551 """
1552 if "skipped" in element.attrib:
1553 return int(element.attrib["skipped"])
1554 elif not optional:
1555 raise UnittestException(f"Required parameter 'skipped' not found in tag '{element.tag}'.")
1556 else:
1557 return default
1559 def _ConvertErrors(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1560 """
1561 Convert the ``errors`` attribute from an XML element node to an integer.
1563 :param element: The XML element node with a ``errors`` attribute.
1564 :param default: The default value, if no ``errors`` attribute was found.
1565 :param optional: If false, an exception is raised for the missing attribute.
1566 :returns: The ``errors`` attribute's content if found, otherwise the given default value.
1567 :raises UnittestException: If optional is false and no ``errors`` attribute exists on the given element node.
1568 """
1569 if "errors" in element.attrib:
1570 return int(element.attrib["errors"])
1571 elif not optional:
1572 raise UnittestException(f"Required parameter 'errors' not found in tag '{element.tag}'.")
1573 else:
1574 return default
1576 def _ConvertFailures(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1577 """
1578 Convert the ``failures`` attribute from an XML element node to an integer.
1580 :param element: The XML element node with a ``failures`` attribute.
1581 :param default: The default value, if no ``failures`` attribute was found.
1582 :param optional: If false, an exception is raised for the missing attribute.
1583 :returns: The ``failures`` attribute's content if found, otherwise the given default value.
1584 :raises UnittestException: If optional is false and no ``failures`` attribute exists on the given element node.
1585 """
1586 if "failures" in element.attrib:
1587 return int(element.attrib["failures"])
1588 elif not optional:
1589 raise UnittestException(f"Required parameter 'failures' not found in tag '{element.tag}'.")
1590 else:
1591 return default
1593 def _ConvertAssertions(self, element: _Element, default: Nullable[int] = None, optional: bool = True) -> Nullable[int]:
1594 """
1595 Convert the ``assertions`` attribute from an XML element node to an integer.
1597 :param element: The XML element node with a ``assertions`` attribute.
1598 :param default: The default value, if no ``assertions`` attribute was found.
1599 :param optional: If false, an exception is raised for the missing attribute.
1600 :returns: The ``assertions`` attribute's content if found, otherwise the given default value.
1601 :raises UnittestException: If optional is false and no ``assertions`` attribute exists on the given element node.
1602 """
1603 if "assertions" in element.attrib:
1604 return int(element.attrib["assertions"])
1605 elif not optional: 1605 ↛ 1606line 1605 didn't jump to line 1606 because the condition on line 1605 was never true
1606 raise UnittestException(f"Required parameter 'assertions' not found in tag '{element.tag}'.")
1607 else:
1608 return default
1610 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
1611 """
1612 Convert the XML data structure of a ``<testsuite>`` to a test suite.
1614 This method uses private helper methods provided by the base-class.
1616 :param parent: The test suite summary as a parent element in the test entity hierarchy.
1617 :param testsuitesNode: The current XML element node representing a test suite.
1618 """
1619 newTestsuite = self._TESTSUITE(
1620 self._ConvertName(testsuitesNode, optional=False),
1621 self._ConvertHostname(testsuitesNode, optional=True),
1622 self._ConvertTimestamp(testsuitesNode, optional=True),
1623 self._ConvertTime(testsuitesNode, optional=True),
1624 parent=parent
1625 )
1627 if False: # self._readerMode is JUnitReaderMode.
1628 self._tests = self._ConvertTests(testsuitesNode)
1629 self._skipped = self._ConvertSkipped(testsuitesNode)
1630 self._errored = self._ConvertErrors(testsuitesNode)
1631 self._failed = self._ConvertFailures(testsuitesNode)
1632 self._assertionCount = self._ConvertAssertions(testsuitesNode)
1634 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
1636 def _ConvertTestsuiteChildren(self, testsuitesNode: _Element, newTestsuite: Testsuite) -> None:
1637 for node in testsuitesNode.iterchildren(): # type: _Element
1638 # if node.tag == "testsuite":
1639 # self._ConvertTestsuite(newTestsuite, node)
1640 # el
1641 if node.tag == "testcase":
1642 self._ConvertTestcase(newTestsuite, node)
1644 def _ConvertTestcase(self, parent: Testsuite, testcaseNode: _Element) -> None:
1645 """
1646 Convert the XML data structure of a ``<testcase>`` to a test case.
1648 This method uses private helper methods provided by the base-class.
1650 :param parent: The test suite as a parent element in the test entity hierarchy.
1651 :param testcaseNode: The current XML element node representing a test case.
1652 """
1653 className = self._ConvertClassname(testcaseNode)
1654 testclass = self._FindOrCreateTestclass(parent, className)
1656 newTestcase = self._TESTCASE(
1657 self._ConvertName(testcaseNode, optional=False),
1658 self._ConvertTime(testcaseNode, optional=False),
1659 assertionCount=self._ConvertAssertions(testcaseNode),
1660 parent=testclass
1661 )
1663 self._ConvertTestcaseChildren(testcaseNode, newTestcase)
1665 def _FindOrCreateTestclass(self, parent: Testsuite, className: str) -> Testclass:
1666 if className in parent._testclasses:
1667 return parent._testclasses[className]
1668 else:
1669 return self._TESTCLASS(className, parent=parent)
1671 def _ConvertTestcaseChildren(self, testcaseNode: _Element, newTestcase: Testcase) -> None:
1672 for node in testcaseNode.iterchildren(): # type: _Element
1673 if isinstance(node, _Comment): 1673 ↛ 1674line 1673 didn't jump to line 1674 because the condition on line 1673 was never true
1674 pass
1675 elif isinstance(node, _Element): 1675 ↛ 1691line 1675 didn't jump to line 1691 because the condition on line 1675 was always true
1676 if node.tag == "skipped":
1677 newTestcase._status = TestcaseStatus.Skipped
1678 elif node.tag == "failure":
1679 newTestcase._status = TestcaseStatus.Failed
1680 elif node.tag == "error": 1680 ↛ 1681line 1680 didn't jump to line 1681 because the condition on line 1680 was never true
1681 newTestcase._status = TestcaseStatus.Errored
1682 elif node.tag == "system-out":
1683 pass
1684 elif node.tag == "system-err":
1685 pass
1686 elif node.tag == "properties": 1686 ↛ 1689line 1686 didn't jump to line 1689 because the condition on line 1686 was always true
1687 pass
1688 else:
1689 raise UnittestException(f"Unknown element '{node.tag}' in junit file.")
1690 else:
1691 pass
1693 if newTestcase._status is TestcaseStatus.Unknown:
1694 newTestcase._status = TestcaseStatus.Passed
1696 def Generate(self, overwrite: bool = False) -> None:
1697 """
1698 Generate the internal XML data structure from test suites and test cases.
1700 This method generates the XML root element (``<testsuites>``) and recursively calls other generated methods.
1702 :param overwrite: Overwrite the internal XML data structure.
1703 :raises UnittestException: If overwrite is false and the internal XML data structure is not empty.
1704 """
1705 if not overwrite and self._xmlDocument is not None: 1705 ↛ 1706line 1705 didn't jump to line 1706 because the condition on line 1705 was never true
1706 raise UnittestException(f"Internal XML document is populated with data.")
1708 rootElement = Element("testsuites")
1709 rootElement.attrib["name"] = self._name
1710 if self._startTime is not None:
1711 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}"
1712 if self._duration is not None:
1713 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}"
1714 rootElement.attrib["tests"] = str(self._tests)
1715 rootElement.attrib["failures"] = str(self._failed)
1716 rootElement.attrib["errors"] = str(self._errored)
1717 rootElement.attrib["skipped"] = str(self._skipped)
1718 # if self._assertionCount is not None:
1719 # rootElement.attrib["assertions"] = f"{self._assertionCount}"
1721 self._xmlDocument = ElementTree(rootElement)
1723 for testsuite in self._testsuites.values():
1724 self._GenerateTestsuite(testsuite, rootElement)
1726 def _GenerateTestsuite(self, testsuite: Testsuite, parentElement: _Element) -> None:
1727 """
1728 Generate the internal XML data structure for a test suite.
1730 This method generates the XML element (``<testsuite>``) and recursively calls other generated methods.
1732 :param testsuite: The test suite to convert to an XML data structures.
1733 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1734 """
1735 testsuiteElement = SubElement(parentElement, "testsuite")
1736 testsuiteElement.attrib["name"] = testsuite._name
1737 if testsuite._startTime is not None: 1737 ↛ 1739line 1737 didn't jump to line 1739 because the condition on line 1737 was always true
1738 testsuiteElement.attrib["timestamp"] = f"{testsuite._startTime.isoformat()}"
1739 if testsuite._duration is not None:
1740 testsuiteElement.attrib["time"] = f"{testsuite._duration.total_seconds():.6f}"
1741 testsuiteElement.attrib["tests"] = str(testsuite._tests)
1742 testsuiteElement.attrib["failures"] = str(testsuite._failed)
1743 testsuiteElement.attrib["errors"] = str(testsuite._errored)
1744 testsuiteElement.attrib["skipped"] = str(testsuite._skipped)
1745 # if testsuite._assertionCount is not None:
1746 # testsuiteElement.attrib["assertions"] = f"{testsuite._assertionCount}"
1747 if testsuite._hostname is not None: 1747 ↛ 1748line 1747 didn't jump to line 1748 because the condition on line 1747 was never true
1748 testsuiteElement.attrib["hostname"] = testsuite._hostname
1750 for testclass in testsuite._testclasses.values():
1751 for tc in testclass._testcases.values():
1752 self._GenerateTestcase(tc, testsuiteElement)
1754 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
1755 """
1756 Generate the internal XML data structure for a test case.
1758 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
1760 :param testcase: The test case to convert to an XML data structures.
1761 :param parentElement: The parent XML data structure element, this data structure part will be added to.
1762 """
1763 testcaseElement = SubElement(parentElement, "testcase")
1764 if testcase.Classname is not None: 1764 ↛ 1766line 1764 didn't jump to line 1766 because the condition on line 1764 was always true
1765 testcaseElement.attrib["classname"] = testcase.Classname
1766 testcaseElement.attrib["name"] = testcase._name
1767 if testcase._duration is not None: 1767 ↛ 1769line 1767 didn't jump to line 1769 because the condition on line 1767 was always true
1768 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}"
1769 if testcase._assertionCount is not None:
1770 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}"
1772 if testcase._status is TestcaseStatus.Passed:
1773 pass
1774 elif testcase._status is TestcaseStatus.Failed:
1775 failureElement = SubElement(testcaseElement, "failure")
1776 elif testcase._status is TestcaseStatus.Skipped: 1776 ↛ 1779line 1776 didn't jump to line 1779 because the condition on line 1776 was always true
1777 skippedElement = SubElement(testcaseElement, "skipped")
1778 else:
1779 errorElement = SubElement(testcaseElement, "error")
1781 def __str__(self) -> str:
1782 moduleName = self.__module__.split(".")[-1]
1783 className = self.__class__.__name__
1784 return (
1785 f"<{moduleName}{className} {self._name} ({self._path}): {self._status.name} - suites/tests:{self.TestsuiteCount}/{self.TestcaseCount}>"
1786 )