pyEDAA.Reports.Unittesting.OpenTestReporting.Events

pyEDAA/Reports/Unittesting/OpenTestReporting/Events.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
# ==================================================================================================================== #
#              _____ ____    _        _      ____                       _                                              #
#  _ __  _   _| ____|  _ \  / \      / \    |  _ \ ___ _ __   ___  _ __| |_ ___                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |_) / _ \ '_ \ / _ \| '__| __/ __|                                       #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _|  _ <  __/ |_) | (_) | |  | |_\__ \                                       #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_| \_\___| .__/ \___/|_|   \__|___/                                       #
# |_|    |___/                                        |_|                                                              #
# ==================================================================================================================== #
# 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