Coverage for pyEDAA/Reports/Unittesting/OpenTestReporting/__init__.py: 94%
308 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32A data model of Open Test Reporting's XML formats, which converts to the unified test entity hierarchy.
34`Open Test Reporting <https://github.com/ota4j-team/open-test-reporting>`__ (OTR) defines XML formats for test results,
35independent of a test framework or programming language. The *event-based* format is written while the tests run, one
36event per state change of a test or container. The *hierarchical* format nests tests in their containers. The JUnit
37Platform writes the event-based format, OTR's command line tool converts it into the hierarchical format.
39Both formats describe a test or container by the same elements: metadata, sources, attachments and a result. This
40package models them as a tree of :class:`Node` objects within an :class:`Execution`. A document reader per format,
41derived from :class:`Document`, fills the tree and converts it into the unified test entity hierarchy of
42:mod:`pyEDAA.Reports.Unittesting`.
44The records of the tree's elements - the infrastructure and a result - are in
45:mod:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Records`.
47.. rubric:: Example
49.. code-block:: Python
51 from pathlib import Path
52 from pyEDAA.Reports.Unittesting.OpenTestReporting.Events import Document
54 document = Document(Path("build/test-results/test/open-test-report.xml"), analyzeAndConvert=True)
55 summary = document.ToTestsuiteSummary()
56 for testcase in summary.IterateTestcases():
57 print(f"{testcase.Name}: {testcase.Status.name}")
58"""
59from __future__ import annotations
61from datetime import datetime, timedelta
62from enum import Enum
63from io import BytesIO
64from pathlib import Path
65from typing import ClassVar, Dict, Generic, List, Optional as Nullable
66from typing import Tuple, TypeVar
68from lxml.etree import QName, XMLParser, XMLSchema, XMLSchemaParseError
69from lxml.etree import XMLSyntaxError, _Element, _ElementTree, parse
70from pyTooling.Common import getFullyQualifiedName, getResourceFile, StringEnum
71from pyTooling.Decorators import export, readonly
72from pyTooling.Exceptions import ToolingException
73from pyTooling.MetaClasses import ExtendedType, abstractmethod
74from pyTooling.Stopwatch import Stopwatch
76from pyEDAA.Reports.Resources import OpenTestReporting as OpenTestReportingResources
77from pyEDAA.Reports.Resources.OpenTestReporting import SPECIFICATIONS, Specification
78from pyEDAA.Reports.Unittesting import UnittestError, TestcaseStatus, TestsuiteKind
79from pyEDAA.Reports.Unittesting import Document as ut_Document
80from pyEDAA.Reports.Unittesting import TestsuiteSummary as ut_TestsuiteSummary
81from pyEDAA.Reports.Unittesting import Testsuite as ut_Testsuite, Testcase as ut_Testcase
82from pyEDAA.Reports.Unittesting.OpenTestReporting.Records import Infrastructure, Result, ResultStatus
85__all__ = ["STATUS_MAP"]
88@export
89class FormatVersion(StringEnum):
90 """
91 Version of Open Test Reporting, as the URIs of its XML namespaces state it.
93 The XML namespaces and schemas of each version are in
94 :data:`~pyEDAA.Reports.Resources.OpenTestReporting.SPECIFICATIONS`, keyed by the version.
95 """
97 Version0_2_0 = "0.2.0" #: Open Test Reporting 0.2.0.
100ParentType = TypeVar("ParentType", bound="Node | Execution")
101"""A type variable for the parent of a :class:`Node`: a :class:`Node`, or the :class:`Execution` of a root node."""
104@export
105class NodeType(Enum):
106 """An enumeration of the kinds of a node, as the JUnit Platform states them (``<junit:type>``)."""
108 Container = "CONTAINER" #: A container of tests or containers, e.g. a test class.
109 Test = "TEST" #: A test, e.g. a test method or an invocation of a parameterized test.
110 ContainerAndTest = "CONTAINER_AND_TEST" #: A test, which contains tests or containers too.
113STATUS_MAP: Dict[ResultStatus, TestcaseStatus] = {
114 ResultStatus.Successful: TestcaseStatus.Passed,
115 ResultStatus.Skipped: TestcaseStatus.Skipped,
116 ResultStatus.Aborted: TestcaseStatus.Skipped,
117 ResultStatus.Failed: TestcaseStatus.Failed,
118 ResultStatus.Errored: TestcaseStatus.Errored,
119} #: Mapping of a result's status to a test case status.
122@export
123class Node(Generic[ParentType], metaclass=ExtendedType, slots=True):
124 """
125 A test or a container of tests.
127 Test engines, test classes and parameterized tests are containers.
129 A node is named by its display name. Its metadata, sources, attachments and result are filled by a document reader.
130 The JUnit Platform states additional metadata: a unique ID, a legacy reporting name and the node's type.
131 """
133 _parent: Nullable[ParentType] #: The parent node, or the execution of a root node.
134 _children: List[Node[Node]] #: Child nodes in the order they were started.
135 _name: str #: Display name.
136 _startTime: datetime #: Time when the node was started.
137 _duration: Nullable[timedelta] #: Duration from start to finish.
138 _tags: List[str] #: Tags.
139 _uniqueID: Nullable[str] #: JUnit Platform's unique ID.
140 _legacyReportingName: Nullable[str] #: JUnit Platform's name for tools not showing display names.
141 _type: Nullable[NodeType] #: JUnit Platform's kind of node.
142 _className: Nullable[str] #: Name of the Java class (Java extension).
143 _methodName: Nullable[str] #: Name of the Java method (Java extension).
144 _methodParameterTypes: Nullable[str] #: Parameter types of the Java method (Java extension).
145 _data: List[Tuple[str, str]] #: Key-value pairs of the data attachments in reported order.
146 _output: Dict[str, str] #: Output attachments by source, e.g. ``stdout``.
147 _files: List[Path] #: Paths of the file attachments.
148 _result: Nullable[Result] #: Result, or ``None`` if the node didn't finish.
150 def __init__(
151 self,
152 name: str,
153 startTime: datetime,
154 duration: Nullable[timedelta] = None,
155 *,
156 parent: Nullable[ParentType] = None
157 ) -> None:
158 """
159 Initializes a node, and adds it to the children of its parent node or the root nodes of its execution.
161 :param name: Display name of the node.
162 :param startTime: Time when the node was started.
163 :param duration: Optional, duration from start to finish.
164 :param parent: Optional, the parent node, or the execution of a root node. Default: ``None``.
165 :raises TypeError: If parameter 'parent' is not of type :class:`Node` or :class:`Execution`.
166 :raises ValueError: If parameter 'name' is None.
167 :raises TypeError: If parameter 'name' is not of type :class:`str`.
168 :raises ValueError: If parameter 'startTime' is None.
169 :raises TypeError: If parameter 'startTime' is not of type :class:`~datetime.datetime`.
170 :raises TypeError: If parameter 'duration' is not of type :class:`~datetime.timedelta`.
171 """
172 if parent is not None and not isinstance(parent, (Node, Execution)):
173 ex = TypeError("Parameter 'parent' is not of type 'Node' or 'Execution'.")
174 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
175 raise ex
177 if name is None:
178 raise ValueError("Parameter 'name' is None.")
179 elif not isinstance(name, str): 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 ex = TypeError("Parameter 'name' is not of type 'str'.")
181 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
182 raise ex
184 if startTime is None: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 raise ValueError("Parameter 'startTime' is None.")
186 elif not isinstance(startTime, datetime):
187 ex = TypeError("Parameter 'startTime' is not of type 'datetime'.")
188 ex.add_note(f"Got type '{getFullyQualifiedName(startTime)}'.")
189 raise ex
191 if duration is not None and not isinstance(duration, timedelta): 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true
192 ex = TypeError("Parameter 'duration' is not of type 'timedelta'.")
193 ex.add_note(f"Got type '{getFullyQualifiedName(duration)}'.")
194 raise ex
196 self._parent = parent
197 self._children = []
198 self._name = name
199 self._startTime = startTime
200 self._duration = duration
201 self._tags = []
202 self._uniqueID = None
203 self._legacyReportingName = None
204 self._type = None
205 self._className = None
206 self._methodName = None
207 self._methodParameterTypes = None
208 self._data = []
209 self._output = {}
210 self._files = []
211 self._result = None
213 if isinstance(parent, Execution):
214 parent._roots.append(self)
215 elif parent is not None:
216 parent._children.append(self)
218 @readonly
219 def Parent(self) -> Nullable[ParentType]:
220 """
221 Read-only property to access the parent node, or the execution of a root node (:attr:`_parent`).
223 :returns: The parent :class:`Node`, or the :class:`Execution` of a root node; ``None`` if the node belongs to
224 neither.
225 """
226 return self._parent
228 @readonly
229 def Children(self) -> List[Node[Node]]:
230 """
231 Read-only property to access the child nodes (:attr:`_children`).
233 :returns: The list of child nodes in the order they were started.
234 """
235 return self._children
237 @readonly
238 def Name(self) -> str:
239 """
240 Read-only property to access the display name (:attr:`_name`).
242 The display name is given by the test, e.g. ``Tests of MyClass``, or generated, e.g. ``[1] "5", "5"`` for a
243 parameterized test's invocation.
245 :returns: The display name.
246 """
247 return self._name
249 @readonly
250 def StartTime(self) -> datetime:
251 """
252 Read-only property to access the time when the node was started (:attr:`_startTime`).
254 :returns: The start time.
255 """
256 return self._startTime
258 @readonly
259 def Duration(self) -> Nullable[timedelta]:
260 """
261 Read-only property to access the duration from start to finish (:attr:`_duration`).
263 :returns: The duration, or ``None`` if the node didn't finish.
264 """
265 return self._duration
267 @readonly
268 def Tags(self) -> List[str]:
269 """
270 Read-only property to access the tags (:attr:`_tags`).
272 :returns: The list of tags.
273 """
274 return self._tags
276 @readonly
277 def UniqueID(self) -> Nullable[str]:
278 """
279 Read-only property to access the JUnit Platform's unique ID (:attr:`_uniqueID`).
281 A test class run by JUnit Jupiter has the unique ID ``[engine:junit-jupiter]/[class:my.pack.MyClassTest]``.
283 :returns: The unique ID, or ``None`` if it wasn't reported.
284 """
285 return self._uniqueID
287 @readonly
288 def LegacyReportingName(self) -> Nullable[str]:
289 """
290 Read-only property to access the JUnit Platform's legacy reporting name (:attr:`_legacyReportingName`).
292 A test class has its qualified name, e.g. ``my.pack.MyClassTest``, a test method its name and parameters, e.g.
293 ``testReturnTrue()``.
295 :returns: The legacy reporting name, or ``None`` if it wasn't reported.
296 """
297 return self._legacyReportingName
299 @readonly
300 def Type(self) -> Nullable[NodeType]:
301 """
302 Read-only property to access the JUnit Platform's kind of node (:attr:`_type`).
304 :returns: The kind of node, or ``None`` if it wasn't reported.
305 """
306 return self._type
308 @readonly
309 def ClassName(self) -> Nullable[str]:
310 """
311 Read-only property to access the name of the Java class (:attr:`_className`).
313 :returns: The fully qualified class name, or ``None`` if no Java class or method source was reported.
314 """
315 return self._className
317 @readonly
318 def MethodName(self) -> Nullable[str]:
319 """
320 Read-only property to access the name of the Java method (:attr:`_methodName`).
322 :returns: The method name, or ``None`` if no Java method source was reported.
323 """
324 return self._methodName
326 @readonly
327 def MethodParameterTypes(self) -> Nullable[str]:
328 """
329 Read-only property to access the parameter types of the Java method (:attr:`_methodParameterTypes`).
331 :returns: The comma-separated parameter types, or ``None`` if they weren't reported.
332 """
333 return self._methodParameterTypes
335 @readonly
336 def Data(self) -> List[Tuple[str, str]]:
337 """
338 Read-only property to access the key-value pairs of the data attachments (:attr:`_data`).
340 :returns: The list of key-value pairs in the order they were reported.
341 """
342 return self._data
344 @readonly
345 def Output(self) -> Dict[str, str]:
346 """
347 Read-only property to access the output attachments (:attr:`_output`).
349 An output's source is e.g. ``stdout`` or ``stderr``. Output of the same source is concatenated in the order it was
350 reported.
352 :returns: The dictionary of output by source.
353 """
354 return self._output
356 @readonly
357 def Files(self) -> List[Path]:
358 """
359 Read-only property to access the paths of the file attachments (:attr:`_files`).
361 :returns: The list of paths.
362 """
363 return self._files
365 @readonly
366 def Result(self) -> Nullable[Result]:
367 """
368 Read-only property to access the result (:attr:`_result`).
370 :returns: The result, or ``None`` if the node didn't finish.
371 """
372 return self._result
374 @readonly
375 def IsTest(self) -> bool:
376 """
377 Read-only property to return whether the node is a test.
379 A node is a test, if the JUnit Platform states it as ``TEST``. A node of another or no stated type is a test, if it
380 has no child nodes.
382 :returns: True, if the node is a test.
383 """
384 if self._type is NodeType.Test:
385 return True
386 elif self._type is NodeType.Container:
387 return False
389 return len(self._children) == 0
391 def ToTestsuite(self, hostname: Nullable[str] = None) -> ut_Testsuite:
392 """
393 Convert this container to a test suite of the unified data model.
395 The test suite is named by the legacy reporting name, if the JUnit Platform reported one, else by the display name.
396 Then, the display name becomes the test suite's title. A container of a Java class becomes a test suite of kind
397 ``Class``. Child containers become test suites, child tests become test cases.
399 :param hostname: Optional, name of the host the test suite was executed on.
400 :returns: A test suite of the unified test entity data model.
401 :raises DuplicateTestsuiteError: If two child containers have the same name.
402 :raises DuplicateTestcaseError: If two child tests have the same name.
403 """
404 return ut_Testsuite(
405 self._name if self._legacyReportingName is None else self._legacyReportingName,
406 title=None if self._legacyReportingName is None else self._name,
407 kind=TestsuiteKind.Class if self._className is not None and self._methodName is None else TestsuiteKind.Logical,
408 hostname=hostname,
409 startTime=self._startTime,
410 totalDuration=self._duration,
411 testsuites=[child.ToTestsuite() for child in self._children if not child.IsTest],
412 testcases=[child.ToTestcase() for child in self._children if child.IsTest]
413 )
415 def ToTestcase(self) -> ut_Testcase:
416 """
417 Convert this test to a test case of the unified data model.
419 The test case is named by the legacy reporting name, if the JUnit Platform reported one, else by the display name.
420 Then, the display name becomes the test case's title. The status is mapped by :data:`STATUS_MAP`; a test without a
421 result status is ``Inconsistent``. The result's message and stack trace become the test case's message and details,
422 the output on ``stdout`` and ``stderr`` its captured output streams, and the data attachments its key-value pairs.
424 :returns: A test case of the unified test entity data model.
425 """
426 if self._result is None:
427 status = TestcaseStatus.Inconsistent
428 message = None
429 details = None
430 else:
431 status = TestcaseStatus.Inconsistent if self._result._status is None else STATUS_MAP[self._result._status]
432 message = self._result.Message
433 details = self._result._stackTrace
435 testcase = ut_Testcase(
436 self._name if self._legacyReportingName is None else self._legacyReportingName,
437 title=None if self._legacyReportingName is None else self._name,
438 startTime=self._startTime,
439 totalDuration=self._duration,
440 status=status,
441 message=message,
442 details=details,
443 standardOutput=self._output.get("stdout", None),
444 standardError=self._output.get("stderr", None)
445 )
447 for key, value in self._data:
448 testcase[key] = value
450 return testcase
453@export
454class Execution(metaclass=ExtendedType, mixin=True):
455 """
456 A mixin-class representing a test run: the infrastructure the tests were run on and the tree of tests and containers.
458 A document reader fills the infrastructure and adds a root node per node without parent, e.g. per test engine. Both
459 are added by creating them with the execution as their parent.
460 """
462 _infrastructure: Nullable[Infrastructure] #: Infrastructure the tests were run on.
463 _roots: List[Node[Execution]] #: Root nodes in the order they were started.
465 def __init__(self) -> None:
466 """Initializes an execution without infrastructure and nodes."""
467 self._infrastructure = None
468 self._roots = []
470 @readonly
471 def Infrastructure(self) -> Nullable[Infrastructure]:
472 """
473 Read-only property to access the infrastructure the tests were run on (:attr:`_infrastructure`).
475 :returns: The infrastructure, or ``None`` if it wasn't reported.
476 """
477 return self._infrastructure
479 @readonly
480 def Roots(self) -> List[Node[Execution]]:
481 """
482 Read-only property to access the root nodes (:attr:`_roots`).
484 A JUnit Platform report's root nodes are its test engines.
486 :returns: The list of root nodes in the order they were started.
487 """
488 return self._roots
491# 'Document' has properties named 'FormatVersion' and 'Specification', which a field's or a property's annotation in the
492# class body would name instead of the classes.
493_FormatVersion = FormatVersion
494_Specification = Specification
497@export
498class Document(ut_Document, Execution, slots=True):
499 """
500 Base-class of the document readers for Open Test Reporting's XML formats.
502 The format version is read from the root element's namespace. The file is validated against the XML schema of
503 that version's :class:`~pyEDAA.Reports.Resources.OpenTestReporting.Specification`, which imports the schemas of all
504 namespaces: the core namespace, both formats, and the Java, Git and JUnit Platform extensions. Both formats describe a
505 test or container by the same elements of the core namespace, which this class converts. A derived class converts
506 the format's structure into the tree of :class:`Node` objects.
507 """
509 _ROOT_NAME: ClassVar[str] #: Prefixed name of the format's root element, e.g. ``e:events``.
511 _xmlDocument: Nullable[_ElementTree] #: Parsed and validated XML document.
512 _formatVersion: Nullable[_FormatVersion] #: Version of Open Test Reporting the file states, after :meth:`Analyze`.
514 def __init__(self, xmlReportFile: Path, analyzeAndConvert: bool = False) -> None:
515 """
516 Initializes the document reader.
518 :param xmlReportFile: Path to the XML report file.
519 :param analyzeAndConvert: Optional, if true, analyze (parse and validate) the file and convert its content.
520 """
521 super().__init__(xmlReportFile)
522 Execution.__init__(self)
524 self._xmlDocument = None
525 self._formatVersion = None
527 if analyzeAndConvert:
528 self.Analyze()
529 self.Convert()
531 @readonly
532 def FormatVersion(self) -> Nullable[_FormatVersion]:
533 """
534 Read-only property to access the version of Open Test Reporting the file states (:attr:`_formatVersion`).
536 :returns: The format version, or ``None`` before :meth:`Analyze`.
537 """
538 return self._formatVersion
540 @readonly
541 def Specification(self) -> _Specification:
542 """
543 Read-only property to return the XML namespaces and schemas of the file's format version.
545 :returns: The specification of :attr:`FormatVersion`.
546 :raises UnittestError: If the file wasn't analyzed yet.
547 """
548 if self._formatVersion is None:
549 raise UnittestError(f"Format version of Open Test Reporting file '{self._path}' is known after 'Analyze()'.")
551 return SPECIFICATIONS[self._formatVersion]
553 @abstractmethod
554 def _RootTag(self, specification: _Specification) -> str:
555 """
556 Return the qualified tag of the format's root element in a version of Open Test Reporting.
558 :param specification: The XML namespaces and schemas of a version.
559 :returns: The tag of the root element, qualified by the format's namespace URI.
560 """
562 def Analyze(self) -> None:
563 """
564 Analyze the XML file: parse it, check its root element and validate it against the XML schema.
566 .. hint::
568 The time spent for analysis will be made available via property :data:`AnalysisDuration`.
570 :raises UnittestError: If the file doesn't exist.
571 :raises UnittestError: If the file can't be read.
572 :raises UnittestError: If the file isn't well-formed XML.
573 :raises UnittestError: If the root element isn't the format's root element of a supported version of Open Test
574 Reporting. |br|
575 The exception notes the supported versions.
576 :raises UnittestError: If the XML schema can't be located or parsed.
577 :raises UnittestError: If the file isn't valid according to the XML schema.
578 """
579 if not self._path.exists():
580 raise UnittestError(f"Open Test Reporting file '{self._path}' does not exist.") \
581 from FileNotFoundError(f"File '{self._path}' not found.")
583 with Stopwatch() as sw:
584 try:
585 content = self._path.read_bytes()
586 except OSError as ex:
587 raise UnittestError(f"Couldn't read Open Test Reporting file '{self._path}'.") from ex
589 try:
590 xmlDocument = parse(BytesIO(content), XMLParser(ns_clean=True), base_url=str(self._path))
591 except XMLSyntaxError as ex:
592 raise UnittestError(f"XML syntax error in Open Test Reporting file '{self._path}'.") from ex
594 rootElement: _Element = xmlDocument.getroot()
595 for formatVersion in FormatVersion:
596 if rootElement.tag == self._RootTag(SPECIFICATIONS[formatVersion]):
597 break
598 else:
599 ex = UnittestError(f"Root element of '{self._path}' is not '<{self._ROOT_NAME}>' of Open Test Reporting.")
600 ex.add_note(f"Got root element '{rootElement.tag}'.")
601 ex.add_note(f"Supported versions: {', '.join(FormatVersion)}.")
602 raise ex
604 schemaFile = SPECIFICATIONS[formatVersion].SchemaFile
605 try:
606 schemaResourceFile = getResourceFile(OpenTestReportingResources, schemaFile)
607 except ToolingException as ex:
608 raise UnittestError(f"Couldn't locate XML Schema '{schemaFile}' in package resources.") from ex
610 try:
611 xmlSchema = XMLSchema(parse(schemaResourceFile, XMLParser(ns_clean=True)))
612 except (XMLSyntaxError, XMLSchemaParseError) as ex:
613 raise UnittestError(f"Error while parsing XML Schema '{schemaFile}'.") from ex
615 if not xmlSchema.validate(xmlDocument):
616 ex = UnittestError(f"Validation error for '{self._path}' using XSD schema '{schemaFile}'.")
617 for logEntry in xmlSchema.error_log:
618 ex.add_note(str(logEntry))
619 raise ex
621 self._xmlDocument = xmlDocument
622 self._formatVersion = formatVersion
624 self._analysisDuration = sw.Duration
626 @abstractmethod
627 def Convert(self) -> None:
628 """Convert the parsed and validated XML data structure into a tree of nodes."""
630 def _ConvertInfrastructure(self, infrastructureElement: _Element) -> None:
631 """
632 Convert the ``<infrastructure>`` element.
634 :param infrastructureElement: The XML element node describing the infrastructure.
635 """
636 core = self.Specification.Core.URI
638 infrastructure = Infrastructure(parent=self)
639 for element in infrastructureElement.iterchildren("*"): # type: _Element
640 if element.tag == f"{{{core}}}hostName":
641 infrastructure._hostName = element.text
642 elif element.tag == f"{{{core}}}userName":
643 infrastructure._userName = element.text
644 elif element.tag == f"{{{core}}}operatingSystem":
645 infrastructure._operatingSystem = element.text
646 elif element.tag == f"{{{core}}}cpuCores":
647 infrastructure._cpuCoreCount = int(element.text)
648 else:
649 localName = QName(element).localname
650 if element.text is not None:
651 infrastructure._extensions[localName] = element.text
653 for attributeName, value in element.attrib.items():
654 infrastructure._extensions[f"{localName}.{attributeName}"] = value
656 def _ConvertTestInfo(self, element: _Element, node: Node) -> None:
657 """
658 Convert the metadata, sources, attachments and result of an event.
660 :param element: The XML element node of an event.
661 :param node: The node to update.
662 """
663 core = self.Specification.Core.URI
664 java = self.Specification.Java.URI
665 junit = self.Specification.JUnit.URI
667 for child in element.iterchildren("*"): # type: _Element
668 if child.tag == f"{{{core}}}metadata":
669 for metadataElement in child.iterchildren("*"): # type: _Element
670 if metadataElement.tag == f"{{{core}}}tags":
671 for tagElement in metadataElement.iterchildren(f"{{{core}}}tag"): # type: _Element
672 node._tags.append("" if tagElement.text is None else tagElement.text)
673 elif metadataElement.tag == f"{{{junit}}}uniqueId":
674 node._uniqueID = metadataElement.text
675 elif metadataElement.tag == f"{{{junit}}}legacyReportingName":
676 node._legacyReportingName = metadataElement.text
677 elif metadataElement.tag == f"{{{junit}}}type": 677 ↛ 669line 677 didn't jump to line 669 because the condition on line 677 was always true
678 node._type = NodeType(metadataElement.text)
679 elif child.tag == f"{{{core}}}sources":
680 sourceTags = (f"{{{java}}}classSource", f"{{{java}}}methodSource")
681 for sourceElement in child.iterchildren(*sourceTags): # type: _Element
682 node._className = sourceElement.attrib["className"]
683 node._methodName = sourceElement.attrib.get("methodName", None)
684 node._methodParameterTypes = sourceElement.attrib.get("methodParameterTypes", None)
685 elif child.tag == f"{{{core}}}attachments":
686 for attachmentElement in child.iterchildren("*"): # type: _Element
687 if attachmentElement.tag == f"{{{core}}}data":
688 for entryElement in attachmentElement.iterchildren(f"{{{core}}}entry"): # type: _Element
689 value = "" if entryElement.text is None else entryElement.text
690 node._data.append((entryElement.attrib["key"], value))
691 elif attachmentElement.tag == f"{{{core}}}output": 691 ↛ 695line 691 didn't jump to line 695 because the condition on line 691 was always true
692 source = attachmentElement.attrib["source"]
693 text = "" if attachmentElement.text is None else attachmentElement.text
694 node._output[source] = node._output.get(source, "") + text
695 elif attachmentElement.tag == f"{{{core}}}file":
696 node._files.append(Path(attachmentElement.attrib["path"]))
697 elif child.tag == f"{{{core}}}result":
698 status = child.attrib.get("status", None)
699 reasonElement = child.find(f"{{{core}}}reason")
700 throwableElement = child.find(f"{{{java}}}throwable")
702 Result(
703 None if status is None else ResultStatus(status),
704 None if reasonElement is None else reasonElement.text,
705 None if throwableElement is None else throwableElement.attrib["type"],
706 None if throwableElement is None else throwableElement.attrib["assertionError"] in ("true", "1"),
707 None if throwableElement is None else throwableElement.text,
708 parent=node
709 )
711 def ToTestsuiteSummary(self) -> ut_TestsuiteSummary:
712 """
713 Convert the tree of nodes to a test suite summary of the unified data model.
715 The test suite summary is named after the report file (without extension). Every root node becomes a test suite,
716 executed on the reported host. The summary starts with the first root node and lasts until the last one finished.
717 Afterwards, the hierarchy is aggregated, so all counters and states are computed.
719 :returns: A test suite summary of the unified test entity data model.
720 :raises UnittestError: If a root node is a test.
721 """
722 hostname = None if self._infrastructure is None else self._infrastructure._hostName
724 testsuites: List[ut_Testsuite] = []
725 for root in self._roots:
726 if root.IsTest:
727 ex = UnittestError(f"Test '{root._name}' in '{self._path}' has no container.")
728 ex.add_note("A test case of the unified data model needs a test suite as parent.")
729 raise ex
731 testsuites.append(root.ToTestsuite(hostname))
733 startTime = min((root._startTime for root in self._roots), default=None)
734 finishTime = max(
735 (root._startTime + root._duration for root in self._roots if root._duration is not None),
736 default=None
737 )
739 summary = ut_TestsuiteSummary(
740 self._path.stem,
741 startTime=startTime,
742 totalDuration=None if finishTime is None else finishTime - startTime,
743 testsuites=testsuites
744 )
745 summary.Aggregate()
747 return summary