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

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 

36 

37from pathlib import Path 

38from typing import Optional as Nullable, Generator, Tuple, Union, TypeVar, Type, ClassVar 

39 

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 

44 

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 

50 

51TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite") 

52TestcaseAggregateReturnType = Tuple[int, int, int] 

53TestsuiteAggregateReturnType = Tuple[int, int, int, int, int] 

54 

55 

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 """ 

62 

63 

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 """ 

70 

71 

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 """ 

78 

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. 

84 

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 ) 

95 

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 

101 

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.") 

106 

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 

112 

113 if classname in juTestsuite._testclasses: 

114 juClass = juTestsuite._testclasses[classname] 

115 else: 

116 juClass = Testclass(classname, parent=juTestsuite) 

117 

118 juClass.AddTestcase(Testcase.FromTestcase(tc)) 

119 

120 return juTestsuite 

121 

122 

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 """ 

129 

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. 

135 

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 ) 

146 

147 

148@export 

149class Document(ju_Document): 

150 """ 

151 A document reader and writer for the Ant + JUnit4 XML file format. 

152 

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. 

155 

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 """ 

159 

160 _TESTCASE: ClassVar[Type[Testcase]] = Testcase 

161 _TESTCLASS: ClassVar[Type[Testclass]] = Testclass 

162 _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite 

163 

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. 

168 

169 .. hint:: 

170 

171 The time spend for analysis will be made available via property :data:`AnalysisDuration`.. 

172 

173 The used XML schema definition is specific to the Ant JUnit4 dialect. 

174 """ 

175 xmlSchemaFile = "Ant-JUnit4.xsd" 

176 self._Analyze(xmlSchemaFile) 

177 

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. 

181 

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 

191 

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.") 

195 

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) 

198 

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 

203 

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 

210 

211 def Convert(self) -> None: 

212 """ 

213 Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. 

214 

215 This method converts the root element. 

216 

217 .. hint:: 

218 

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

220 

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 

227 

228 with Stopwatch() as sw: 

229 rootElement: _Element = self._xmlDocument.getroot() 

230 

231 self._name = self._ConvertName(rootElement, optional=True) 

232 self._startTime =self._ConvertTimestamp(rootElement, optional=True) 

233 self._duration = self._ConvertTime(rootElement, optional=True) 

234 

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") 

240 

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) 

244 

245 self.Aggregate() 

246 

247 self._modelConversion = sw.Duration 

248 

249 def _ConvertTestsuite(self, parent: TestsuiteSummary, testsuitesNode: _Element) -> None: 

250 """ 

251 Convert the XML data structure of a ``<testsuite>`` to a test suite. 

252 

253 This method uses private helper methods provided by the base-class. 

254 

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 ) 

265 

266 self._ConvertTestsuiteChildren(testsuitesNode, newTestsuite) 

267 

268 def Generate(self, overwrite: bool = False) -> None: 

269 """ 

270 Generate the internal XML data structure from test suites and test cases. 

271 

272 This method generates the XML root element (``<testsuite>``) and recursively calls other generated methods. 

273 

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.") 

279 

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 

284 

285 testsuite = firstValue(self._testsuites) 

286 

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 

301 

302 self._xmlDocument = ElementTree(rootElement) 

303 

304 for testclass in testsuite._testclasses.values(): 

305 for tc in testclass._testcases.values(): 

306 self._GenerateTestcase(tc, rootElement) 

307 

308 def _GenerateTestcase(self, testcase: Testcase, parentElement: _Element) -> None: 

309 """ 

310 Generate the internal XML data structure for a test case. 

311 

312 This method generates the XML element (``<testcase>``) and recursively calls other generated methods. 

313 

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}" 

325 

326 self._GenerateTestcaseChildren(testcase, testcaseElement)