Coverage for pyEDAA/Reports/Unittesting/JUnit/AntJUnit4.py: 87%
129 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 2024-2026 Electronic Design Automation Abstraction (EDA²) #
15# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""
33Reader for JUnit unit testing summary files in XML format.
34"""
35from __future__ import annotations
37from pathlib import Path
38from typing import Optional as Nullable, Generator, Tuple, Union, TypeVar, Type, ClassVar
40from lxml.etree import ElementTree, Element, SubElement, tostring, _Element
41from pyTooling.Common import firstValue
42from pyTooling.Decorators import export, InheritDocString, DocStringMergeStrategy
43from pyTooling.Stopwatch import Stopwatch
45from pyEDAA.Reports.Unittesting import UnittestError, TestsuiteKind
46from pyEDAA.Reports.Unittesting import TestcaseStatus, TestsuiteStatus, IterationScheme
47from pyEDAA.Reports.Unittesting import TestsuiteSummary as ut_TestsuiteSummary, Testsuite as ut_Testsuite
48from pyEDAA.Reports.Unittesting.JUnit import Testcase as ju_Testcase, Testclass as ju_Testclass, Testsuite as ju_Testsuite
49from pyEDAA.Reports.Unittesting.JUnit import TestsuiteSummary as ju_TestsuiteSummary, Document as ju_Document
51TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite")
52TestcaseAggregateReturnType = Tuple[int, int, int]
53TestsuiteAggregateReturnType = Tuple[int, int, int, int, int]
56@export
57@InheritDocString(ju_Testcase, DocStringMergeStrategy.BaseLast)
58class Testcase(ju_Testcase):
59 """
60 This is a derived implementation for the Ant + JUnit4 dialect.
61 """
64@export
65@InheritDocString(ju_Testclass, DocStringMergeStrategy.BaseLast)
66class Testclass(ju_Testclass):
67 """
68 This is a derived implementation for the Ant + JUnit4 dialect.
69 """
72@export
73@InheritDocString(ju_Testsuite, DocStringMergeStrategy.BaseLast)
74class Testsuite(ju_Testsuite):
75 """
76 This is a derived implementation for the Ant + JUnit4 dialect.
77 """
79 @classmethod
80 def FromTestsuite(cls, testsuite: ut_Testsuite) -> Testsuite:
81 """
82 Convert a test suite of the unified test entity data model to the JUnit specific data model's test suite object
83 adhering to the Ant + JUnit4 dialect.
85 :param testsuite: Test suite from unified data model.
86 :returns: Test suite from JUnit specific data model (Ant + JUnit4 dialect).
87 """
88 juTestsuite = cls(
89 testsuite._name,
90 hostname=testsuite._hostname,
91 startTime=testsuite._startTime,
92 duration=testsuite._totalDuration,
93 status= testsuite._status,
94 )
96 juTestsuite._tests = testsuite._tests
97 juTestsuite._skipped = testsuite._skipped
98 juTestsuite._errored = testsuite._errored
99 juTestsuite._failed = testsuite._failed
100 juTestsuite._passed = testsuite._passed
102 for tc in testsuite.IterateTestcases():
103 ts = tc._parent
104 if ts is None: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 raise UnittestError(f"Testcase '{tc._name}' is not part of a hierarchy.")
107 classname = ts._name
108 ts = ts._parent
109 while ts is not None and ts._kind > TestsuiteKind.Logical:
110 classname = f"{ts._name}.{classname}"
111 ts = ts._parent
113 if classname in juTestsuite._testclasses:
114 juClass = juTestsuite._testclasses[classname]
115 else:
116 juClass = Testclass(classname, parent=juTestsuite)
118 juClass.AddTestcase(Testcase.FromTestcase(tc))
120 return juTestsuite
123@export
124@InheritDocString(ju_TestsuiteSummary, DocStringMergeStrategy.BaseLast)
125class TestsuiteSummary(ju_TestsuiteSummary):
126 """
127 This is a derived implementation for the Ant + JUnit4 dialect.
128 """
130 @classmethod
131 def FromTestsuiteSummary(cls, testsuiteSummary: ut_TestsuiteSummary) -> TestsuiteSummary:
132 """
133 Convert a test suite summary of the unified test entity data model to the JUnit specific data model's test suite
134 summary object adhering to the Ant + JUnit4 dialect.
136 :param testsuiteSummary: Test suite summary from unified data model.
137 :returns: Test suite summary from JUnit specific data model (Ant + JUnit4 dialect).
138 """
139 return cls(
140 testsuiteSummary._name,
141 startTime=testsuiteSummary._startTime,
142 duration=testsuiteSummary._totalDuration,
143 status=testsuiteSummary._status,
144 testsuites=(Testsuite.FromTestsuite(testsuite) for testsuite in testsuiteSummary._testsuites.values())
145 )
148@export
149class Document(ju_Document):
150 """
151 A document reader and writer for the Ant + JUnit4 XML file format.
153 This class reads, validates and transforms an XML file in the Ant + JUnit4 format into a JUnit data model. It can then
154 be converted into a unified test entity data model.
156 In reverse, a JUnit data model instance with the specific Ant + JUnit4 file format can be created from a unified test
157 entity data model. This data model can be written as XML into a file.
158 """
160 _TESTCASE: ClassVar[Type[Testcase]] = Testcase
161 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass
162 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite
164 def Analyze(self) -> None:
165 """
166 Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML
167 schema.
169 .. hint::
171 The time spend for analysis will be made available via property :data:`AnalysisDuration`..
173 The used XML schema definition is specific to the Ant JUnit4 dialect.
174 """
175 xmlSchemaFile = "Ant-JUnit4.xsd"
176 self._Analyze(xmlSchemaFile)
178 def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None:
179 """
180 Write the data model as XML into a file adhering to the Ant + JUnit4 dialect.
182 :param path: Optional, path to the XML file, if internal path shouldn't be used.
183 :param overwrite: Optional, if true, overwrite an existing file.
184 :param regenerate: Optional, if true, regenerate the XML structure from data model.
185 :raises UnittestError: If the file cannot be overwritten.
186 :raises UnittestError: If the internal XML data structure wasn't generated.
187 :raises UnittestError: If the file cannot be opened or written.
188 """
189 if path is None:
190 path = self._path
192 if not overwrite and path.exists():
193 raise UnittestError(f"JUnit XML file '{path}' can not be overwritten.") \
194 from FileExistsError(f"File '{path}' already exists.")
196 if regenerate: 196 ↛ 199line 196 didn't jump to line 199 because the condition on line 196 was always true
197 self.Generate(overwrite=True)
199 if self._xmlDocument is None: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 ex = UnittestError("Internal XML document tree is empty and needs to be generated before write is possible.")
201 ex.add_note("Call 'JUnitDocument.Generate()' or 'JUnitDocument.Write(..., regenerate=True)'.")
202 raise ex
204 content = tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)
205 try:
206 with path.open("wb") as file:
207 file.write(content)
208 except OSError as ex:
209 raise UnittestError(f"JUnit XML file '{path}' can not be written.") from ex
211 def Convert(self) -> None:
212 """
213 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy.
215 This method converts the root element.
217 .. hint::
219 The time spend for model conversion will be made available via property :data:`ModelConversionDuration`.
221 :raises UnittestError: If XML was not read and parsed before.
222 """
223 if self._xmlDocument is None: 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true
224 ex = UnittestError(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.")
225 ex.add_note("Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.")
226 raise ex
228 with Stopwatch() as sw:
229 rootElement: _Element = self._xmlDocument.getroot()
231 self._name = self._ConvertName(rootElement, optional=True)
232 self._startTime =self._ConvertTimestamp(rootElement, optional=True)
233 self._duration = self._ConvertTime(rootElement, optional=True)
235 # tests = rootElement.getAttribute("tests")
236 # skipped = rootElement.getAttribute("skipped")
237 # errors = rootElement.getAttribute("errors")
238 # failures = rootElement.getAttribute("failures")
239 # assertions = rootElement.getAttribute("assertions")
241 hostname = self._ConvertHostname(rootElement, optional=True, default=None)
242 ts = Testsuite(self._name, hostname, startTime=self._startTime, duration=self._duration, parent=self)
243 self._ConvertTestsuiteChildren(rootElement, ts)
245 self.Aggregate()
247 self._modelConversion = sw.Duration
249 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None:
250 """
251 Convert the XML data structure of a ``<testsuite>`` to a test suite.
253 This method uses private helper methods provided by the base-class.
255 :param parent: The test suite summary as a parent element in the test entity hierarchy.
256 :param testsuitesNode: The current XML element node representing a test suite.
257 """
258 newTestsuite = Testsuite(
259 self._ConvertName(testsuitesNode, optional=False),
260 self._ConvertHostname(testsuitesNode, optional=False),
261 self._ConvertTimestamp(testsuitesNode, optional=False),
262 self._ConvertTime(testsuitesNode, optional=False),
263 parent=parent
264 )
266 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite)
268 def Generate(self, overwrite: bool = False) -> None:
269 """
270 Generate the internal XML data structure from test suites and test cases.
272 This method generates the XML root element (``<testsuite>``) and recursively calls other generated methods.
274 :param overwrite: Optional, overwrite the internal XML data structure.
275 :raises UnittestError: If overwrite is false and the internal XML data structure is not empty.
276 """
277 if not overwrite and self._xmlDocument is not None: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 raise UnittestError("Internal XML document is populated with data.")
280 if self.TestsuiteCount != 1:
281 ex = UnittestError("The Ant + JUnit4 format requires exactly one test suite.")
282 ex.add_note(f"Found {self.TestsuiteCount} test suites.")
283 raise ex
285 testsuite = firstValue(self._testsuites)
287 rootElement = Element("testsuite")
288 rootElement.attrib["name"] = self._name
289 if self._startTime is not None:
290 rootElement.attrib["timestamp"] = f"{self._startTime.isoformat()}"
291 if self._duration is not None: 291 ↛ 293line 291 didn't jump to line 293 because the condition on line 291 was always true
292 rootElement.attrib["time"] = f"{self._duration.total_seconds():.6f}"
293 rootElement.attrib["tests"] = str(self._tests)
294 rootElement.attrib["failures"] = str(self._failed)
295 rootElement.attrib["errors"] = str(self._errored)
296 rootElement.attrib["skipped"] = str(self._skipped)
297 # if self._assertionCount is not None:
298 # rootElement.attrib["assertions"] = f"{self._assertionCount}"
299 if testsuite._hostname is not None: 299 ↛ 302line 299 didn't jump to line 302 because the condition on line 299 was always true
300 rootElement.attrib["hostname"] = testsuite._hostname
302 self._xmlDocument = ElementTree(rootElement)
304 for testclass in testsuite._testclasses.values():
305 for tc in testclass._testcases.values():
306 self._GenerateTestcase(tc, rootElement)
308 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None:
309 """
310 Generate the internal XML data structure for a test case.
312 This method generates the XML element (``<testcase>``) and recursively calls other generated methods.
314 :param testcase: The test case to convert to an XML data structures.
315 :param parentElement: The parent XML data structure element, this data structure part will be added to.
316 """
317 testcaseElement = SubElement(parentElement, "testcase")
318 if testcase.Classname is not None: 318 ↛ 320line 318 didn't jump to line 320 because the condition on line 318 was always true
319 testcaseElement.attrib["classname"] = testcase.Classname
320 testcaseElement.attrib["name"] = testcase._name
321 if testcase._duration is not None: 321 ↛ 323line 321 didn't jump to line 323 because the condition on line 321 was always true
322 testcaseElement.attrib["time"] = f"{testcase._duration.total_seconds():.6f}"
323 if testcase._assertionCount is not None: 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true
324 testcaseElement.attrib["assertions"] = f"{testcase._assertionCount}"
326 self._GenerateTestcaseChildren(testcase, testcaseElement)