Coverage for pyEDAA/OSVVM/TestsuiteSummary.py: 66%

212 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 22:24 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ___ ______ ____ ____ __ # 

3# _ __ _ _| ____| _ \ / \ / \ / _ \/ ___\ \ / /\ \ / / \/ | # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | \___ \\ \ / / \ \ / /| |\/| | # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| |___) |\ V / \ V / | | | | # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/|____/ \_/ \_/ |_| |_| # 

7# |_| |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2021-2025 Electronic Design Automation Abstraction (EDA²) # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31"""Reader for OSVVM test report summary files in YAML format.""" 

32from datetime import timedelta, datetime 

33from pathlib import Path 

34from typing import Optional as Nullable, Iterator, Iterable, Mapping, Any, List 

35 

36from ruamel.yaml import YAML, CommentedMap, CommentedSeq 

37from pyTooling.Decorators import export, InheritDocString, notimplemented 

38from pyTooling.MetaClasses import ExtendedType 

39from pyTooling.Stopwatch import Stopwatch 

40from pyTooling.Versioning import CalendarVersion, SemanticVersion 

41 

42from pyEDAA.Reports.Unittesting import UnittestException, Document, TestcaseStatus, TestsuiteStatus, TestsuiteType, TestsuiteKind 

43from pyEDAA.Reports.Unittesting import TestsuiteSummary as ut_TestsuiteSummary, Testsuite as ut_Testsuite 

44from pyEDAA.Reports.Unittesting import Testcase as ut_Testcase 

45 

46 

47@export 

48class OsvvmException: 

49 pass 

50 

51 

52@export 

53@InheritDocString(UnittestException) 

54class UnittestException(UnittestException, OsvvmException): 

55 """@InheritDocString(UnittestException)""" 

56 

57 

58@export 

59@InheritDocString(ut_Testcase) 

60class Testcase(ut_Testcase): 

61 """@InheritDocString(ut_Testcase)""" 

62 

63 

64@export 

65@InheritDocString(ut_Testsuite) 

66class Testsuite(ut_Testsuite): 

67 """@InheritDocString(ut_Testsuite)""" 

68 

69 

70@export 

71class BuildInformation(metaclass=ExtendedType, slots=True): 

72 _startTime: datetime 

73 _finishTime: datetime 

74 _elapsed: timedelta 

75 _simulator: str 

76 _simulatorVersion: SemanticVersion 

77 _osvvmVersion: CalendarVersion 

78 _buildErrorCode: int 

79 _analyzeErrorCount: int 

80 _simulateErrorCount: int 

81 

82 def __init__(self) -> None: 

83 pass 

84 

85 

86@export 

87class Settings(metaclass=ExtendedType, slots=True): 

88 _baseDirectory: Path 

89 _reportsSubdirectory: Path 

90 _simulationLogFile: Path 

91 _simulationHtmlLogFile: Path 

92 _requirementsSubdirectory: Path 

93 _coverageSubdirectory: Path 

94 _report2CssFiles: List[Path] 

95 _report2PngFile: List[Path] 

96 

97 def __init__(self) -> None: 

98 pass 

99 

100 

101@export 

102@InheritDocString(ut_TestsuiteSummary) 

103class TestsuiteSummary(ut_TestsuiteSummary): 

104 """@InheritDocString(ut_TestsuiteSummary)""" 

105 

106 _datetime: datetime 

107 

108 def __init__( 

109 self, 

110 name: str, 

111 startTime: Nullable[datetime] = None, 

112 setupDuration: Nullable[timedelta] = None, 

113 testDuration: Nullable[timedelta] = None, 

114 teardownDuration: Nullable[timedelta] = None, 

115 totalDuration: Nullable[timedelta] = None, 

116 status: TestsuiteStatus = TestsuiteStatus.Unknown, 

117 warningCount: int = 0, 

118 errorCount: int = 0, 

119 fatalCount: int = 0, 

120 testsuites: Nullable[Iterable[TestsuiteType]] = None, 

121 keyValuePairs: Nullable[Mapping[str, Any]] = None, 

122 parent: Nullable[TestsuiteType] = None 

123 ) -> None: 

124 """ 

125 Initializes the fields of a test summary. 

126 

127 :param name: Name of the test summary. 

128 :param startTime: Time when the test summary was started. 

129 :param setupDuration: Duration it took to set up the test summary. 

130 :param testDuration: Duration of all tests listed in the test summary. 

131 :param teardownDuration: Duration it took to tear down the test summary. 

132 :param totalDuration: Total duration of the entity's execution (setup + test + teardown) 

133 :param status: Overall status of the test summary. 

134 :param warningCount: Count of encountered warnings incl. warnings from sub-elements. 

135 :param errorCount: Count of encountered errors incl. errors from sub-elements. 

136 :param fatalCount: Count of encountered fatal errors incl. fatal errors from sub-elements. 

137 :param testsuites: List of test suites to initialize the test summary with. 

138 :param keyValuePairs: Mapping of key-value pairs to initialize the test summary with. 

139 :param parent: Reference to the parent test summary. 

140 """ 

141 super().__init__( 

142 name, 

143 startTime, 

144 setupDuration, 

145 testDuration, 

146 teardownDuration, 

147 totalDuration, 

148 status, 

149 warningCount, 

150 errorCount, 

151 fatalCount, 

152 testsuites, 

153 keyValuePairs, 

154 parent 

155 ) 

156 

157 

158@export 

159class BuildSummaryDocument(TestsuiteSummary, Document): 

160 _yamlDocument: Nullable[YAML] 

161 

162 def __init__(self, yamlReportFile: Path, analyzeAndConvert: bool = False) -> None: 

163 super().__init__("Unprocessed OSVVM YAML file") 

164 

165 self._yamlDocument = None 

166 

167 Document.__init__(self, yamlReportFile, analyzeAndConvert) 

168 

169 def Analyze(self) -> None: 

170 """ 

171 Analyze the YAML file, parse the content into an YAML data structure. 

172 

173 .. hint:: 

174 

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

176 """ 

177 if not self._path.exists(): 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true

178 raise UnittestException(f"OSVVM YAML file '{self._path}' does not exist.") \ 

179 from FileNotFoundError(f"File '{self._path}' not found.") 

180 

181 with Stopwatch() as sw: 

182 try: 

183 yamlReader = YAML() 

184 self._yamlDocument = yamlReader.load(self._path) 

185 except Exception as ex: 

186 raise UnittestException(f"Couldn't open '{self._path}'.") from ex 

187 

188 self._analysisDuration = sw.Duration 

189 

190 @notimplemented 

191 def Write(self, path: Nullable[Path] = None, overwrite: bool = False) -> None: 

192 """ 

193 Write the data model as XML into a file adhering to the Any JUnit dialect. 

194 

195 :param path: Optional path to the YAML file, if internal path shouldn't be used. 

196 :param overwrite: If true, overwrite an existing file. 

197 :raises UnittestException: If the file cannot be overwritten. 

198 :raises UnittestException: If the internal YAML data structure wasn't generated. 

199 :raises UnittestException: If the file cannot be opened or written. 

200 """ 

201 if path is None: 

202 path = self._path 

203 

204 if not overwrite and path.exists(): 

205 raise UnittestException(f"OSVVM YAML file '{path}' can not be overwritten.") \ 

206 from FileExistsError(f"File '{path}' already exists.") 

207 

208 # if regenerate: 

209 # self.Generate(overwrite=True) 

210 

211 if self._yamlDocument is None: 

212 ex = UnittestException(f"Internal YAML document tree is empty and needs to be generated before write is possible.") 

213 # ex.add_note(f"Call 'BuildSummaryDocument.Generate()' or 'BuildSummaryDocument.Write(..., regenerate=True)'.") 

214 raise ex 

215 

216 # with path.open("w", encoding="utf-8") as file: 

217 # self._yamlDocument.writexml(file, addindent="\t", encoding="utf-8", newl="\n") 

218 

219 @staticmethod 

220 def _ParseSequenceFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedSeq]: 

221 try: 

222 value = node[fieldName] 

223 except KeyError as ex: 

224 newEx = UnittestException(f"Sequence field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

225 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

226 raise newEx from ex 

227 

228 if value is None: 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true

229 return () 

230 elif not isinstance(value, CommentedSeq): 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true

231 line = node._yaml_line_col.data[fieldName][0] + 1 

232 ex = UnittestException(f"Field '{fieldName}' is not a sequence.") # TODO: from TypeError?? 

233 ex.add_note(f"Found type {value.__class__.__name__} at line {line}.") 

234 raise ex 

235 

236 return value 

237 

238 @staticmethod 

239 def _ParseMapFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedMap]: 

240 try: 

241 value = node[fieldName] 

242 except KeyError as ex: 

243 newEx = UnittestException(f"Dictionary field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

244 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

245 raise newEx from ex 

246 

247 if value is None: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 return {} 

249 elif not isinstance(value, CommentedMap): 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true

250 line = node._yaml_line_col.data[fieldName][0] + 1 

251 ex = UnittestException(f"Field '{fieldName}' is not a list.") # TODO: from TypeError?? 

252 ex.add_note(f"Type mismatch found for line {line}.") 

253 raise ex 

254 return value 

255 

256 @staticmethod 

257 def _ParseStrFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[str]: 

258 try: 

259 value = node[fieldName] 

260 except KeyError as ex: 

261 newEx = UnittestException(f"String field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

262 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

263 raise newEx from ex 

264 

265 if not isinstance(value, str): 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true

266 raise UnittestException(f"Field '{fieldName}' is not of type str.") # TODO: from TypeError?? 

267 

268 return value 

269 

270 @staticmethod 

271 def _ParseIntFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[int]: 

272 try: 

273 value = node[fieldName] 

274 except KeyError as ex: 

275 newEx = UnittestException(f"Integer field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

276 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

277 raise newEx from ex 

278 

279 if not isinstance(value, int): 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true

280 raise UnittestException(f"Field '{fieldName}' is not of type int.") # TODO: from TypeError?? 

281 

282 return value 

283 

284 @staticmethod 

285 def _ParseDateFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[datetime]: 

286 try: 

287 value = node[fieldName] 

288 except KeyError as ex: 

289 newEx = UnittestException(f"Date field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

290 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

291 raise newEx from ex 

292 

293 if not isinstance(value, datetime): 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true

294 raise UnittestException(f"Field '{fieldName}' is not of type datetime.") # TODO: from TypeError?? 

295 

296 return value 

297 

298 @staticmethod 

299 def _ParseDurationFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[timedelta]: 

300 try: 

301 value = node[fieldName] 

302 except KeyError as ex: 

303 newEx = UnittestException(f"Duration field '{fieldName}' not found in node starting at line {node.lc.line + 1}.") 

304 newEx.add_note(f"Available fields: {', '.join(key for key in node)}") 

305 raise newEx from ex 

306 

307 if not isinstance(value, float): 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true

308 raise UnittestException(f"Field '{fieldName}' is not of type float.") # TODO: from TypeError?? 

309 

310 return timedelta(seconds=value) 

311 

312 def Convert(self) -> None: 

313 """ 

314 Convert the parsed YAML data structure into a test entity hierarchy. 

315 

316 This method converts the root element. 

317 

318 .. hint:: 

319 

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

321 

322 :raises UnittestException: If XML was not read and parsed before. 

323 """ 

324 if self._yamlDocument is None: 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true

325 ex = UnittestException(f"OSVVM YAML file '{self._path}' needs to be read and analyzed by a YAML parser.") 

326 ex.add_note(f"Call 'Document.Analyze()' or create document using 'Document(path, parse=True)'.") 

327 raise ex 

328 

329 with Stopwatch() as sw: 

330 self._name = self._yamlDocument["Name"] 

331 buildInfo = self._ParseMapFromYAML(self._yamlDocument, "BuildInfo") 

332 self._startTime = self._ParseDateFieldFromYAML(buildInfo, "StartTime") 

333 self._totalDuration = self._ParseDurationFieldFromYAML(buildInfo, "Elapsed") 

334 

335 if "TestSuites" in self._yamlDocument: 

336 for yamlTestsuite in self._ParseSequenceFromYAML(self._yamlDocument, "TestSuites"): 

337 self._ConvertTestsuite(self, yamlTestsuite) 

338 

339 self.Aggregate() 

340 

341 self._modelConversion = sw.Duration 

342 

343 def _ConvertTestsuite(self, parentTestsuite: Testsuite, yamlTestsuite: CommentedMap) -> None: 

344 testsuiteName = self._ParseStrFieldFromYAML(yamlTestsuite, "Name") 

345 totalDuration = self._ParseDurationFieldFromYAML(yamlTestsuite, "ElapsedTime") 

346 

347 testsuite = Testsuite( 

348 testsuiteName, 

349 totalDuration=totalDuration, 

350 parent=parentTestsuite 

351 ) 

352 

353 # if yamlTestsuite['TestCases'] is not None: 

354 for yamlTestcase in self._ParseSequenceFromYAML(yamlTestsuite, 'TestCases'): 

355 self._ConvertTestcase(testsuite, yamlTestcase) 

356 

357 def _ConvertTestcase(self, parentTestsuite: Testsuite, yamlTestcase: CommentedMap) -> None: 

358 testcaseName = self._ParseStrFieldFromYAML(yamlTestcase, "TestCaseName") 

359 totalDuration = self._ParseDurationFieldFromYAML(yamlTestcase, "ElapsedTime") 

360 yamlStatus = self._ParseStrFieldFromYAML(yamlTestcase, "Status").lower() 

361 yamlResults = self._ParseMapFromYAML(yamlTestcase, "Results") 

362 assertionCount = self._ParseIntFieldFromYAML(yamlResults, "AffirmCount") 

363 passedAssertionCount = self._ParseIntFieldFromYAML(yamlResults, "PassedCount") 

364 totalErrors = self._ParseIntFieldFromYAML(yamlResults, "TotalErrors") 

365 yamlAlertCount = self._ParseMapFromYAML(yamlResults, "AlertCount") 

366 warningCount = self._ParseIntFieldFromYAML(yamlAlertCount, "Warning") 

367 errorCount = self._ParseIntFieldFromYAML(yamlAlertCount, "Error") 

368 fatalCount = self._ParseIntFieldFromYAML(yamlAlertCount, "Failure") 

369 

370 # FIXME: write a Parse classmethod in enum 

371 if yamlStatus == "passed": 371 ↛ 373line 371 didn't jump to line 373 because the condition on line 371 was always true

372 status = TestcaseStatus.Passed 

373 elif yamlStatus == "skipped": 

374 status = TestcaseStatus.Skipped 

375 elif yamlStatus == "failed": 

376 status = TestcaseStatus.Failed 

377 else: 

378 status = TestcaseStatus.Unknown 

379 

380 if totalErrors == warningCount + errorCount + fatalCount: 

381 if warningCount > 0: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true

382 status |= TestcaseStatus.Warned 

383 if errorCount > 0: 383 ↛ 384line 383 didn't jump to line 384 because the condition on line 383 was never true

384 status |= TestcaseStatus.Errored 

385 if fatalCount > 0: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true

386 status |= TestcaseStatus.Aborted 

387 # else: 

388 # status |= TestcaseStatus.Inconsistent 

389 

390 _ = Testcase( 

391 testcaseName, 

392 totalDuration=totalDuration, 

393 assertionCount=assertionCount, 

394 passedAssertionCount=passedAssertionCount, 

395 warningCount=warningCount, 

396 status=status, 

397 errorCount=errorCount, 

398 fatalCount=fatalCount, 

399 parent=parentTestsuite 

400 ) 

401 

402 def __contains__(self, key: str) -> bool: 

403 return key in self._testsuites 

404 

405 def __iter__(self) -> Iterator[Testsuite]: 

406 return iter(self._testsuites.values()) 

407 

408 def __getitem__(self, key: str) -> Testsuite: 

409 return self._testsuites[key] 

410 

411 def __len__(self) -> int: 

412 return self._testsuites.__len__()