Coverage for pyEDAA/OSVVM/AlertLog.py: 80%
274 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:24 +0000
« 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"""A data model for OSVVM's AlertLog YAML file format."""
32from datetime import timedelta
33from enum import Enum, auto
34from pathlib import Path
35from typing import Optional as Nullable, Dict, Iterator, Iterable, Callable
37from ruamel.yaml import YAML, CommentedSeq, CommentedMap
38from pyTooling.Decorators import readonly, export
39from pyTooling.MetaClasses import ExtendedType
40from pyTooling.Common import getFullyQualifiedName
41from pyTooling.Stopwatch import Stopwatch
42from pyTooling.Tree import Node
44from pyEDAA.OSVVM import OSVVMException
47@export
48class AlertLogException(OSVVMException):
49 """Base-class for all pyEDAA.OSVVM.AlertLog specific exceptions."""
52@export
53class DuplicateItemException(AlertLogException):
54 """Raised if a duplicate item is detected in the AlertLog hierarchy."""
57@export
58class AlertLogStatus(Enum):
59 """Status of an :class:`AlertLogItem`."""
60 Unknown = auto()
61 Passed = auto()
62 Failed = auto()
64 __MAPPINGS__ = {
65 "passed": Passed,
66 "failed": Failed
67 }
69 @classmethod
70 def Parse(self, name: str) -> "AlertLogStatus":
71 try:
72 return self.__MAPPINGS__[name.lower()]
73 except KeyError as ex:
74 raise AlertLogException(f"Unknown AlertLog status '{name}'.") from ex
76 def __bool__(self) -> bool:
77 """
78 Convert an *AlertLogStatus* to a boolean.
80 :returns: Return true, if the status is ``Passed``.
81 """
82 return self is self.Passed
85@export
86def _format(node: Node) -> str:
87 """
88 User-defined :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>` formatting function for nodes referencing :class:`AlertLogItems <AlertLogItem>`.
90 :param node: Node to format.
91 :returns: String representation (one-liner) describing an AlertLogItem.
92 """
93 return f"{node["Name"]}: {node["TotalErrors"]}={node["AlertCountFailures"]}/{node["AlertCountErrors"]}/{node["AlertCountWarnings"]} {node["PassedCount"]}/{node["AffirmCount"]}"
96@export
97class AlertLogItem(metaclass=ExtendedType, slots=True):
98 """
99 An *AlertLogItem* represents an AlertLog hierarchy item.
101 An item has a reference to its parent item in the AlertLog hierarchy. If the item is the top-most element (root
102 element), the parent reference is ``None``.
104 An item can contain further child items.
105 """
106 _parent: "AlertLogItem" #: Reference to the parent item.
107 _name: str #: Name of the AlertLog item.
108 _children: Dict[str, "AlertLogItem"] #: Dictionary of child items.
110 _status: AlertLogStatus #: AlertLog item's status
111 _totalErrors: int #: Total number of warnings, errors and failures.
112 _alertCountWarnings: int #: Warning count.
113 _alertCountErrors: int #: Error count.
114 _alertCountFailures: int #: Failure count.
115 _passedCount: int #: Passed affirmation count.
116 _affirmCount: int #: Overall affirmation count (incl. failed affirmations).
117 _requirementsPassed: int #: Count of passed requirements.
118 _requirementsGoal: int #: Overall expected requirements.
119 _disabledAlertCountWarnings: int #: Count of disabled warnings.
120 _disabledAlertCountErrors: int #: Count of disabled errors.
121 _disabledAlertCountFailures: int #: Count of disabled failures.
123 def __init__(
124 self,
125 name: str,
126 status: AlertLogStatus = AlertLogStatus.Unknown,
127 totalErrors: int = 0,
128 alertCountWarnings: int = 0,
129 alertCountErrors: int = 0,
130 alertCountFailures: int = 0,
131 passedCount: int = 0,
132 affirmCount: int = 0,
133 requirementsPassed: int = 0,
134 requirementsGoal: int = 0,
135 disabledAlertCountWarnings: int = 0,
136 disabledAlertCountErrors: int = 0,
137 disabledAlertCountFailures: int = 0,
138 children: Iterable["AlertLogItem"] = None,
139 parent: Nullable["AlertLogItem"] = None
140 ) -> None:
141 self._name = name
142 self._parent = parent
143 if parent is not None:
144 if not isinstance(parent, AlertLogItem): 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 ex = TypeError(f"Parameter 'parent' is not an AlertLogItem.")
146 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
147 raise ex
148 elif name in parent._children: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true
149 raise DuplicateItemException(f"AlertLogItem '{name}' already exists in '{parent._name}'.")
151 parent._children[name] = self
153 self._children = {}
154 if children is not None:
155 for child in children:
156 if not isinstance(child, AlertLogItem): 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true
157 ex = TypeError(f"Item in parameter 'children' is not an AlertLogItem.")
158 ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.")
159 raise ex
160 elif child._name in self._children: 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true
161 raise DuplicateItemException(f"AlertLogItem '{child._name}' already exists in '{self._name}'.")
162 elif child._parent is not None: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 raise AlertLogException(f"AlertLogItem '{child._name}' is already part of another AlertLog hierarchy ({child._parent._name}).")
165 self._children[child._name] = child
166 child._parent = self
168 self._status = status
169 self._totalErrors = totalErrors
170 self._alertCountWarnings = alertCountWarnings
171 self._alertCountErrors = alertCountErrors
172 self._alertCountFailures = alertCountFailures
173 self._passedCount = passedCount
174 self._affirmCount = affirmCount
175 self._requirementsPassed = requirementsPassed
176 self._requirementsGoal = requirementsGoal
177 self._disabledAlertCountWarnings = disabledAlertCountWarnings
178 self._disabledAlertCountErrors = disabledAlertCountErrors
179 self._disabledAlertCountFailures = disabledAlertCountFailures
181 @property
182 def Parent(self) -> Nullable["AlertLogItem"]:
183 """
184 Property to access the parent item of this item (:attr:`_parent`).
186 :returns: The item's parent item. ``None``, if it's the top-most item (root).
187 """
188 return self._parent
190 @Parent.setter
191 def Parent(self, value: Nullable["AlertLogItem"]) -> None:
192 if value is None: 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true
193 del self._parent._children[self._name]
194 else:
195 if not isinstance(value, AlertLogItem): 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 ex = TypeError(f"Parameter 'value' is not an AlertLogItem.")
197 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
198 raise ex
199 elif self._name in value._children: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 raise DuplicateItemException(f"AlertLogItem '{self._name}' already exists in '{value._name}'.")
202 value._children[self._name] = self
204 self._parent = value
206 @readonly
207 def Name(self) -> str:
208 """
209 Read-only property to access the AlertLog item's name (:attr:`_name`).
211 :returns: AlertLog item's name.
212 """
213 return self._name
215 @readonly
216 def Status(self) -> AlertLogStatus:
217 """
218 Read-only property to access the AlertLog item's status (:attr:`_status`).
220 :returns: AlertLog item's status.
221 """
222 return self._status
224 @readonly
225 def TotalErrors(self) -> int:
226 """
227 Read-only property to access the AlertLog item's total error count (:attr:`_totalErrors`).
229 :returns: AlertLog item's total errors.
230 """
231 return self._totalErrors
233 @readonly
234 def AlertCountWarnings(self) -> int:
235 """
236 Read-only property to access the AlertLog item's warning count (:attr:`_alertCountWarnings`).
238 :returns: AlertLog item's warning count.
239 """
240 return self._alertCountWarnings
242 @readonly
243 def AlertCountErrors(self) -> int:
244 """
245 Read-only property to access the AlertLog item's error count (:attr:`_alertCountErrors`).
247 :returns: AlertLog item's error count.
248 """
249 return self._alertCountErrors
251 @readonly
252 def AlertCountFailures(self) -> int:
253 """
254 Read-only property to access the AlertLog item's failure count (:attr:`_alertCountFailures`).
256 :returns: AlertLog item's failure count.
257 """
258 return self._alertCountFailures
260 @readonly
261 def PassedCount(self) -> int:
262 """
263 Read-only property to access the AlertLog item's passed affirmation count (:attr:`_alertCountFailures`).
265 :returns: AlertLog item's passed affirmations.
266 """
267 return self._passedCount
269 @readonly
270 def AffirmCount(self) -> int:
271 """
272 Read-only property to access the AlertLog item's overall affirmation count (:attr:`_affirmCount`).
274 :returns: AlertLog item's overall affirmations.
275 """
276 return self._affirmCount
278 @readonly
279 def RequirementsPassed(self) -> int:
280 return self._requirementsPassed
282 @readonly
283 def RequirementsGoal(self) -> int:
284 return self._requirementsGoal
286 @readonly
287 def DisabledAlertCountWarnings(self) -> int:
288 """
289 Read-only property to access the AlertLog item's count of disabled warnings (:attr:`_disabledAlertCountWarnings`).
291 :returns: AlertLog item's count of disabled warnings.
292 """
293 return self._disabledAlertCountWarnings
295 @readonly
296 def DisabledAlertCountErrors(self) -> int:
297 """
298 Read-only property to access the AlertLog item's count of disabled errors (:attr:`_disabledAlertCountErrors`).
300 :returns: AlertLog item's count of disabled errors.
301 """
302 return self._disabledAlertCountErrors
304 @readonly
305 def DisabledAlertCountFailures(self) -> int:
306 """
307 Read-only property to access the AlertLog item's count of disabled failures (:attr:`_disabledAlertCountFailures`).
309 :returns: AlertLog item's count of disabled failures.
310 """
311 return self._disabledAlertCountFailures
313 @readonly
314 def Children(self) -> Dict[str, "AlertLogItem"]:
315 return self._children
317 def __iter__(self) -> Iterator["AlertLogItem"]:
318 """
319 Iterate all child AlertLog items.
321 :return: An iterator of child items.
322 """
323 return iter(self._children.values())
325 def __len__(self) -> int:
326 """
327 Returns number of child AlertLog items.
329 :returns: The number of nested AlertLog items.
330 """
331 return len(self._children)
333 def __getitem__(self, name: str) -> "AlertLogItem":
334 """Index access for returning child AlertLog items.
336 :param name: The child's name.
337 :returns: The referenced child.
338 :raises KeyError: When the child referenced by parameter 'name' doesn't exist.
339 """
340 return self._children[name]
342 def ToTree(self, format: Callable[[Node], str] = _format) -> Node:
343 """
344 Convert the AlertLog hierarchy starting from this AlertLog item to a :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>`.
346 :params format: A user-defined :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>` formatting function.
347 :returns: A tree of nodes referencing an AlertLog item.
348 """
349 node = Node(
350 value=self,
351 keyValuePairs={
352 "Name": self._name,
353 "TotalErrors": self._totalErrors,
354 "AlertCountFailures": self._alertCountFailures,
355 "AlertCountErrors": self._alertCountErrors,
356 "AlertCountWarnings": self._alertCountWarnings,
357 "PassedCount": self._passedCount,
358 "AffirmCount": self._affirmCount
359 },
360 children=(child.ToTree() for child in self._children.values()),
361 format=format
362 )
364 return node
367@export
368class Settings(metaclass=ExtendedType, mixin=True):
369 _externalWarningCount: int
370 _externalErrorCount: int
371 _externalFailureCount: int
372 _failOnDisabledErrors: bool
373 _failOnRequirementErrors: bool
374 _failOnWarning: bool
376 def __init__(self) -> None:
377 self._externalWarningCount = 0
378 self._externalErrorCount = 0
379 self._externalFailureCount = 0
380 self._failOnDisabledErrors = False
381 self._failOnRequirementErrors = True
382 self._failOnWarning = False
385@export
386class Document(AlertLogItem, Settings):
387 """
388 An *AlertLog Document* represents an OSVVM AlertLog report document (YAML file).
390 The document inherits :class:`AlertLogItem` and represents the AlertLog hierarchy's root element.
392 When analyzing and converting the document, the YAML analysis duration as well as the model conversion duration gets
393 captured.
394 """
396 _path: Path #: Path to the YAML file.
397 _yamlDocument: Nullable[YAML] #: Internal YAML document instance.
399 _analysisDuration: Nullable[timedelta] #: YAML file analysis duration in seconds.
400 _modelConversionDuration: Nullable[timedelta] #: Data structure conversion duration in seconds.
402 def __init__(self, filename: Path, analyzeAndConvert: bool = False) -> None:
403 """
404 Initializes an AlertLog YAML document.
406 :param filename: Path to the YAML file.
407 :param analyzeAndConvert: If true, analyze the YAML document and convert the content to an AlertLog data model instance.
408 """
409 super().__init__("", parent=None)
410 Settings.__init__(self)
412 self._path = filename
413 self._yamlDocument = None
415 self._analysisDuration = None
416 self._modelConversionDuration = None
418 if analyzeAndConvert:
419 self.Analyze()
420 self.Parse()
422 @property
423 def Path(self) -> Path:
424 """
425 Read-only property to access the path to the YAML file of this document (:attr:`_path`).
427 :returns: The document's path to the YAML file.
428 """
429 return self._path
431 @readonly
432 def AnalysisDuration(self) -> timedelta:
433 """
434 Read-only property to access the time spent for YAML file analysis (:attr:`_analysisDuration`).
436 :returns: The YAML file analysis duration.
437 """
438 if self._analysisDuration is None:
439 raise AlertLogException(f"Document '{self._path}' was not analyzed.")
441 return self._analysisDuration
443 @readonly
444 def ModelConversionDuration(self) -> timedelta:
445 """
446 Read-only property to access the time spent for data structure to AlertLog hierarchy conversion (:attr:`_modelConversionDuration`).
448 :returns: The data structure conversion duration.
449 """
450 if self._modelConversionDuration is None:
451 raise AlertLogException(f"Document '{self._path}' was not converted.")
453 return self._modelConversionDuration
455 def Analyze(self) -> None:
456 """
457 Analyze the YAML file (specified by :attr:`_path`) and store the YAML document in :attr:`_yamlDocument`.
459 :raises AlertLogException: If YAML file doesn't exist.
460 :raises AlertLogException: If YAML file can't be opened.
461 """
462 if not self._path.exists(): 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true
463 raise AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' does not exist.") \
464 from FileNotFoundError(f"File '{self._path}' not found.")
466 with Stopwatch() as sw:
467 try:
468 yamlReader = YAML()
469 self._yamlDocument = yamlReader.load(self._path)
470 except Exception as ex:
471 raise AlertLogException(f"Couldn't open '{self._path}'.") from ex
473 self._analysisDuration = timedelta(seconds=sw.Duration)
475 def Parse(self) -> None:
476 """
477 Convert the YAML data structure to a hierarchy of :class:`AlertLogItem` instances.
479 :raises AlertLogException: If YAML file was not analyzed.
480 """
481 if self._yamlDocument is None: 481 ↛ 482line 481 didn't jump to line 482 because the condition on line 481 was never true
482 ex = AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' needs to be read and analyzed by a YAML parser.")
483 ex.add_note(f"Call 'Document.Analyze()' or create the document using 'Document(path, parse=True)'.")
484 raise ex
486 with Stopwatch() as sw:
487 self._name = self._ParseStrFieldFromYAML(self._yamlDocument, "Name")
488 self._status = AlertLogStatus.Parse(self._ParseStrFieldFromYAML(self._yamlDocument, "Status"))
489 for child in self._ParseSequenceFromYAML(self._yamlDocument, "Children"):
490 _ = self._ParseAlertLogItem(child, self)
492 self._modelConversionDuration = timedelta(seconds=sw.Duration)
494 @staticmethod
495 def _ParseSequenceFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedSeq]:
496 try:
497 value = node[fieldName]
498 except KeyError as ex:
499 newEx = OSVVMException(f"Sequence field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
500 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
501 raise newEx from ex
503 if value is None: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true
504 return ()
505 elif not isinstance(value, CommentedSeq): 505 ↛ 506line 505 didn't jump to line 506 because the condition on line 505 was never true
506 ex = AlertLogException(f"Field '{fieldName}' is not a sequence.") # TODO: from TypeError??
507 ex.add_note(f"Found type {value.__class__.__name__} at line {node._yaml_line_col.data[fieldName][0] + 1}.")
508 raise ex
510 return value
512 @staticmethod
513 def _ParseMapFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedMap]:
514 try:
515 value = node[fieldName]
516 except KeyError as ex:
517 newEx = OSVVMException(f"Dictionary field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
518 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
519 raise newEx from ex
521 if value is None: 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true
522 return {}
523 elif not isinstance(value, CommentedMap): 523 ↛ 524line 523 didn't jump to line 524 because the condition on line 523 was never true
524 ex = AlertLogException(f"Field '{fieldName}' is not a list.") # TODO: from TypeError??
525 ex.add_note(f"Type mismatch found for line {node._yaml_line_col.data[fieldName][0] + 1}.")
526 raise ex
527 return value
529 @staticmethod
530 def _ParseStrFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[str]:
531 try:
532 value = node[fieldName]
533 except KeyError as ex:
534 newEx = OSVVMException(f"String field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
535 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
536 raise newEx from ex
538 if not isinstance(value, str): 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true
539 raise AlertLogException(f"Field '{fieldName}' is not of type str.") # TODO: from TypeError??
541 return value
543 @staticmethod
544 def _ParseIntFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[int]:
545 try:
546 value = node[fieldName]
547 except KeyError as ex:
548 newEx = OSVVMException(f"Integer field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
549 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
550 raise newEx from ex
552 if not isinstance(value, int): 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true
553 raise AlertLogException(f"Field '{fieldName}' is not of type int.") # TODO: from TypeError??
555 return value
557 def _ParseAlertLogItem(self, child: CommentedMap, parent: Nullable[AlertLogItem] = None) -> AlertLogItem:
558 results = self._ParseMapFromYAML(child, "Results")
559 yamlAlertCount = self._ParseMapFromYAML(results, "AlertCount")
560 yamlDisabledAlertCount = self._ParseMapFromYAML(results, "DisabledAlertCount")
561 alertLogItem = AlertLogItem(
562 self._ParseStrFieldFromYAML(child, "Name"),
563 AlertLogStatus.Parse(self._ParseStrFieldFromYAML(child, "Status")),
564 self._ParseIntFieldFromYAML(results, "TotalErrors"),
565 self._ParseIntFieldFromYAML(yamlAlertCount, "Warning"),
566 self._ParseIntFieldFromYAML(yamlAlertCount, "Error"),
567 self._ParseIntFieldFromYAML(yamlAlertCount, "Failure"),
568 self._ParseIntFieldFromYAML(results, "PassedCount"),
569 self._ParseIntFieldFromYAML(results, "AffirmCount"),
570 self._ParseIntFieldFromYAML(results, "RequirementsPassed"),
571 self._ParseIntFieldFromYAML(results, "RequirementsGoal"),
572 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Warning"),
573 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Error"),
574 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Failure"),
575 children=(self._ParseAlertLogItem(ch) for ch in self._ParseSequenceFromYAML(child, "Children")),
576 parent=parent
577 )
579 return alertLogItem