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
|
# ==================================================================================================================== #
# _____ ____ _ _ ____ _ #
# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
# |_| |___/ |_| #
# ==================================================================================================================== #
# 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 event-based XML format.
The JUnit Platform writes this format into :file:`open-test-report.xml`, if the configuration parameter
``junit.platform.reporting.open.xml.enabled`` is ``true``. The root element ``<e:events>`` holds an ``<infrastructure>``
element and one event per state change of a test or container:
* ``<e:started>`` starts a test or container, identified by its ``id``, with a display name (``name``) and the ``id`` of
its parent (``parentId``).
* ``<e:reported>`` adds attachments while it runs, e.g. key-value pairs or output on STDOUT.
* ``<e:finished>`` finishes it with a result.
Every event carries a timestamp. A test or container, which was started but didn't finish, e.g. because the test run
crashed, has no result.
.. rubric:: Example
.. code-block:: Python
from pathlib import Path
from pyEDAA.Reports.Unittesting.OpenTestReporting.Events import Document
document = Document(Path("build/test-results/test/open-test-report.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
from typing import Dict
from lxml.etree import QName, _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 Node, Document as otr_Document
@export
class Document(otr_Document):
"""
A document reader for Open Test Reporting's event-based XML format.
A started event creates a :class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Node` below the node named by
``parentId``, or a root node. Every event adds its metadata, sources, attachments and result to the node of its
``id``. A finished event sets the node's duration: the time from its start to its finish.
"""
_ROOT_NAME = "e:events" #: Prefixed name of the root element ``<e:events>``.
def _RootTag(self, specification: Specification) -> str:
"""
Return the qualified tag of ``<e:events>`` 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 event-based format.
"""
return f"{{{specification.Events.URI}}}events"
def Convert(self) -> None:
"""
Convert the parsed and validated XML data structure into a tree of nodes.
The events are processed in the order of the file.
.. 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 started event reuses the ``id`` of a started test or container.
:raises UnittestError: If a started event names a ``parentId``, which wasn't started before. |br|
A parent needs to be started before its children.
:raises UnittestError: If a reported or finished event names an ``id``, which wasn't started before. |br|
A test or container needs to be started before it reports or finishes.
"""
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
events = self.Specification.Events.URI
with Stopwatch() as sw:
nodes: Dict[str, Node] = {}
for element in self._xmlDocument.getroot().iterchildren("*"): # type: _Element
if element.tag == f"{{{core}}}infrastructure":
self._ConvertInfrastructure(element)
continue
nodeID = element.attrib["id"]
if element.tag == f"{{{events}}}started":
if nodeID in nodes:
raise UnittestError(f"Started event in '{self._path}' reuses the ID '{nodeID}'.")
if (parentID := element.attrib.get("parentId", None)) is None:
parent = self
elif (parent := nodes.get(parentID, None)) is None:
ex = UnittestError(f"Started event '{nodeID}' in '{self._path}' names an unknown parent ID '{parentID}'.")
ex.add_note("A parent needs to be started before its children.")
raise ex
node = Node(element.attrib["name"], datetime.fromisoformat(element.attrib["time"]), parent=parent)
nodes[nodeID] = node
elif (node := nodes.get(nodeID, None)) is None:
ex = UnittestError(f"Event '{QName(element).localname}' in '{self._path}' names an unknown ID '{nodeID}'.")
ex.add_note("A test or container needs to be started before it reports or finishes.")
raise ex
elif element.tag == f"{{{events}}}finished":
node._duration = datetime.fromisoformat(element.attrib["time"]) - node._startTime
self._ConvertTestInfo(element, node)
self._modelConversion = sw.Duration
|