pyEDAA.Reports.Unittesting.OpenTestReporting.Hierarchy

pyEDAA/Reports/Unittesting/OpenTestReporting/Hierarchy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# ==================================================================================================================== #
#              _____ ____    _        _      ____                       _                                              #
#  _ __  _   _| ____|  _ \  / \      / \    |  _ \ ___ _ __   ___  _ __| |_ ___                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |_) / _ \ '_ \ / _ \| '__| __/ __|                                       #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _|  _ <  __/ |_) | (_) | |  | |_\__ \                                       #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_| \_\___| .__/ \___/|_|   \__|___/                                       #
# |_|    |___/                                        |_|                                                              #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²)                                                  #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
Reader for Open Test Reporting's hierarchical XML format.

Open Test Reporting's command line tool converts the event-based format into this format (``convert`` command). The
root element ``<h:execution>`` holds an ``<infrastructure>`` element and a ``<h:root>`` element per test or container
without parent. A test or container nests its children as ``<h:child>`` elements, and carries a display name
(``name``), a start time (``start``) and a duration (``duration``), besides metadata, sources, attachments and its
result.

.. rubric:: Example

.. code-block:: Python

   from pathlib import Path
   from pyEDAA.Reports.Unittesting.OpenTestReporting.Hierarchy import Document

   document = Document(Path("build/test-results/test/hierarchy.xml"), analyzeAndConvert=True)
   summary = document.ToTestsuiteSummary()
   print(f"{summary.Tests} tests: {summary.Passed} passed, {summary.Failed} failed, {summary.Skipped} skipped")
"""
from __future__                                   import annotations

from datetime                                     import datetime, timedelta
from re                                           import fullmatch

from lxml.etree                                   import _Element
from pyTooling.Decorators                         import export
from pyTooling.Stopwatch                          import Stopwatch

from pyEDAA.Reports.Resources.OpenTestReporting   import Specification
from pyEDAA.Reports.Unittesting                   import UnittestError
from pyEDAA.Reports.Unittesting.OpenTestReporting import Execution, Node, Document as otr_Document


@export
class Document(otr_Document):
	"""
	A document reader for Open Test Reporting's hierarchical XML format.

	A ``<h:root>`` element becomes a root :class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Node`, a ``<h:child>``
	element a node below the node of its parent element. The ``duration`` attribute (an ``xs:duration`` like
	``PT0.193317644S``) becomes the node's duration, truncated to microseconds.
	"""

	_ROOT_NAME = "h:execution"  #: Prefixed name of the root element ``<h:execution>``.

	def _RootTag(self, specification: Specification) -> str:
		"""
		Return the qualified tag of ``<h:execution>`` in a version of Open Test Reporting.

		:param specification: The XML namespaces and schemas of a version.
		:returns:             The tag of the root element, qualified by the namespace URI of the hierarchical format.
		"""
		return f"{{{specification.Hierarchy.URI}}}execution"

	def Convert(self) -> None:
		"""
		Convert the parsed and validated XML data structure into a tree of nodes.

		.. hint::

		   The time spent for model conversion will be made available via property :data:`ModelConversionDuration`.

		:raises UnittestError: If the XML file was not analyzed before. |br|
		                       Call 'Document.Analyze()' or create the document using
		                       'Document(path, analyzeAndConvert=True)'.
		:raises UnittestError: If a duration has years, months or a sign. |br|
		                       Supported are days, hours, minutes and seconds.
		"""
		if self._xmlDocument is None:
			ex = UnittestError(f"Open Test Reporting file '{self._path}' needs to be read and analyzed by an XML parser.")
			ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.")
			raise ex

		core =      self.Specification.Core.URI
		hierarchy = self.Specification.Hierarchy.URI

		with Stopwatch() as sw:
			for element in self._xmlDocument.getroot().iterchildren("*"):  # type: _Element
				if element.tag == f"{{{core}}}infrastructure":
					self._ConvertInfrastructure(element)
				elif element.tag == f"{{{hierarchy}}}root":
					self._ConvertNode(element, self)

		self._modelConversion = sw.Duration

	def _ConvertNode(self, nodeElement: _Element, parent: Node | Execution) -> Node:
		"""
		Convert a ``<h:root>`` or ``<h:child>`` element and its child elements to a node.

		:param nodeElement:    The XML element node describing a test or container.
		:param parent:         The parent node, or the execution for a root node.
		:returns:              The node.
		:raises UnittestError: If the duration has years, months or a sign. |br|
		                       Supported are days, hours, minutes and seconds.
		"""
		if (durationText := nodeElement.attrib.get("duration", None)) is None:
			duration = None
		elif (match := fullmatch(r"P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?)?", durationText)) is None:
			ex = UnittestError(f"Unsupported duration '{durationText}' in '{self._path}'.")
			ex.add_note("Supported are days, hours, minutes and seconds, e.g. 'PT0.193317644S'.")
			raise ex
		else:
			days, hours, minutes, seconds, fraction = match.groups(default="0")
			duration = timedelta(
				days=int(days),
				hours=int(hours),
				minutes=int(minutes),
				seconds=int(seconds),
				microseconds=int(fraction[:6].ljust(6, "0"))
			)

		node = Node(
			nodeElement.attrib["name"],
			datetime.fromisoformat(nodeElement.attrib["start"]),
			duration,
			parent=parent
		)
		self._ConvertTestInfo(nodeElement, node)

		hierarchy = self.Specification.Hierarchy.URI
		for childElement in nodeElement.iterchildren(f"{{{hierarchy}}}child"):  # type: _Element
			self._ConvertNode(childElement, node)

		return node