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
|