# ==================================================================================================================== #
# _____ ____ _ _ ___ ______ ____ ____ __ #
# _ __ _ _| ____| _ \ / \ / \ / _ \/ ___\ \ / /\ \ / / \/ | #
# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | \___ \\ \ / / \ \ / /| |\/| | #
# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| |___) |\ V / \ V / | | | | #
# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/|____/ \_/ \_/ |_| |_| #
# |_| |___/ #
# ==================================================================================================================== #
# Authors: #
# Patrick Lehmann #
# #
# License: #
# ==================================================================================================================== #
# Copyright 2021-2025 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 #
# ==================================================================================================================== #
#
"""A data model for OSVVM's AlertLog YAML file format."""
from datetime import timedelta
from enum import Enum, auto
from pathlib import Path
from typing import Optional as Nullable, Dict, Iterator, Iterable, Callable
from ruamel.yaml import YAML, CommentedSeq, CommentedMap
from pyTooling.Decorators import readonly, export
from pyTooling.MetaClasses import ExtendedType
from pyTooling.Common import getFullyQualifiedName
from pyTooling.Stopwatch import Stopwatch
from pyTooling.Tree import Node
from pyEDAA.OSVVM import OSVVMException
[docs]
@export
class AlertLogException(OSVVMException):
"""Base-class for all pyEDAA.OSVVM.AlertLog specific exceptions."""
[docs]
@export
class DuplicateItemException(AlertLogException):
"""Raised if a duplicate item is detected in the AlertLog hierarchy."""
[docs]
@export
class AlertLogStatus(Enum):
"""Status of an :class:`AlertLogItem`."""
Unknown = auto()
Passed = auto()
Failed = auto()
__MAPPINGS__ = {
"passed": Passed,
"failed": Failed
}
@classmethod
def Parse(self, name: str) -> "AlertLogStatus":
try:
return self.__MAPPINGS__[name.lower()]
except KeyError as ex:
raise AlertLogException(f"Unknown AlertLog status '{name}'.") from ex
[docs]
def __bool__(self) -> bool:
"""
Convert an *AlertLogStatus* to a boolean.
:returns: Return true, if the status is ``Passed``.
"""
return self is self.Passed
[docs]
@export
class AlertLogItem(metaclass=ExtendedType, slots=True):
"""
An *AlertLogItem* represents an AlertLog hierarchy item.
An item has a reference to its parent item in the AlertLog hierarchy. If the item is the top-most element (root
element), the parent reference is ``None``.
An item can contain further child items.
"""
_parent: "AlertLogItem" #: Reference to the parent item.
_name: str #: Name of the AlertLog item.
_children: Dict[str, "AlertLogItem"] #: Dictionary of child items.
_status: AlertLogStatus #: AlertLog item's status
_totalErrors: int #: Total number of warnings, errors and failures.
_alertCountWarnings: int #: Warning count.
_alertCountErrors: int #: Error count.
_alertCountFailures: int #: Failure count.
_passedCount: int #: Passed affirmation count.
_affirmCount: int #: Overall affirmation count (incl. failed affirmations).
_requirementsPassed: int #: Count of passed requirements.
_requirementsGoal: int #: Overall expected requirements.
_disabledAlertCountWarnings: int #: Count of disabled warnings.
_disabledAlertCountErrors: int #: Count of disabled errors.
_disabledAlertCountFailures: int #: Count of disabled failures.
[docs]
def __init__(
self,
name: str,
status: AlertLogStatus = AlertLogStatus.Unknown,
totalErrors: int = 0,
alertCountWarnings: int = 0,
alertCountErrors: int = 0,
alertCountFailures: int = 0,
passedCount: int = 0,
affirmCount: int = 0,
requirementsPassed: int = 0,
requirementsGoal: int = 0,
disabledAlertCountWarnings: int = 0,
disabledAlertCountErrors: int = 0,
disabledAlertCountFailures: int = 0,
children: Iterable["AlertLogItem"] = None,
parent: Nullable["AlertLogItem"] = None
) -> None:
self._name = name
self._parent = parent
if parent is not None:
if not isinstance(parent, AlertLogItem):
ex = TypeError(f"Parameter 'parent' is not an AlertLogItem.")
ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
raise ex
elif name in parent._children:
raise DuplicateItemException(f"AlertLogItem '{name}' already exists in '{parent._name}'.")
parent._children[name] = self
self._children = {}
if children is not None:
for child in children:
if not isinstance(child, AlertLogItem):
ex = TypeError(f"Item in parameter 'children' is not an AlertLogItem.")
ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.")
raise ex
elif child._name in self._children:
raise DuplicateItemException(f"AlertLogItem '{child._name}' already exists in '{self._name}'.")
elif child._parent is not None:
raise AlertLogException(f"AlertLogItem '{child._name}' is already part of another AlertLog hierarchy ({child._parent._name}).")
self._children[child._name] = child
child._parent = self
self._status = status
self._totalErrors = totalErrors
self._alertCountWarnings = alertCountWarnings
self._alertCountErrors = alertCountErrors
self._alertCountFailures = alertCountFailures
self._passedCount = passedCount
self._affirmCount = affirmCount
self._requirementsPassed = requirementsPassed
self._requirementsGoal = requirementsGoal
self._disabledAlertCountWarnings = disabledAlertCountWarnings
self._disabledAlertCountErrors = disabledAlertCountErrors
self._disabledAlertCountFailures = disabledAlertCountFailures
@property
def Parent(self) -> Nullable["AlertLogItem"]:
"""
Property to access the parent item of this item (:attr:`_parent`).
:returns: The item's parent item. ``None``, if it's the top-most item (root).
"""
return self._parent
@Parent.setter
def Parent(self, value: Nullable["AlertLogItem"]) -> None:
if value is None:
del self._parent._children[self._name]
else:
if not isinstance(value, AlertLogItem):
ex = TypeError(f"Parameter 'value' is not an AlertLogItem.")
ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
raise ex
elif self._name in value._children:
raise DuplicateItemException(f"AlertLogItem '{self._name}' already exists in '{value._name}'.")
value._children[self._name] = self
self._parent = value
@readonly
def Name(self) -> str:
"""
Read-only property to access the AlertLog item's name (:attr:`_name`).
:returns: AlertLog item's name.
"""
return self._name
@readonly
def Status(self) -> AlertLogStatus:
"""
Read-only property to access the AlertLog item's status (:attr:`_status`).
:returns: AlertLog item's status.
"""
return self._status
@readonly
def TotalErrors(self) -> int:
"""
Read-only property to access the AlertLog item's total error count (:attr:`_totalErrors`).
:returns: AlertLog item's total errors.
"""
return self._totalErrors
@readonly
def AlertCountWarnings(self) -> int:
"""
Read-only property to access the AlertLog item's warning count (:attr:`_alertCountWarnings`).
:returns: AlertLog item's warning count.
"""
return self._alertCountWarnings
@readonly
def AlertCountErrors(self) -> int:
"""
Read-only property to access the AlertLog item's error count (:attr:`_alertCountErrors`).
:returns: AlertLog item's error count.
"""
return self._alertCountErrors
@readonly
def AlertCountFailures(self) -> int:
"""
Read-only property to access the AlertLog item's failure count (:attr:`_alertCountFailures`).
:returns: AlertLog item's failure count.
"""
return self._alertCountFailures
@readonly
def PassedCount(self) -> int:
"""
Read-only property to access the AlertLog item's passed affirmation count (:attr:`_alertCountFailures`).
:returns: AlertLog item's passed affirmations.
"""
return self._passedCount
@readonly
def AffirmCount(self) -> int:
"""
Read-only property to access the AlertLog item's overall affirmation count (:attr:`_affirmCount`).
:returns: AlertLog item's overall affirmations.
"""
return self._affirmCount
@readonly
def RequirementsPassed(self) -> int:
return self._requirementsPassed
@readonly
def RequirementsGoal(self) -> int:
return self._requirementsGoal
@readonly
def DisabledAlertCountWarnings(self) -> int:
"""
Read-only property to access the AlertLog item's count of disabled warnings (:attr:`_disabledAlertCountWarnings`).
:returns: AlertLog item's count of disabled warnings.
"""
return self._disabledAlertCountWarnings
@readonly
def DisabledAlertCountErrors(self) -> int:
"""
Read-only property to access the AlertLog item's count of disabled errors (:attr:`_disabledAlertCountErrors`).
:returns: AlertLog item's count of disabled errors.
"""
return self._disabledAlertCountErrors
@readonly
def DisabledAlertCountFailures(self) -> int:
"""
Read-only property to access the AlertLog item's count of disabled failures (:attr:`_disabledAlertCountFailures`).
:returns: AlertLog item's count of disabled failures.
"""
return self._disabledAlertCountFailures
@readonly
def Children(self) -> Dict[str, "AlertLogItem"]:
return self._children
[docs]
def __iter__(self) -> Iterator["AlertLogItem"]:
"""
Iterate all child AlertLog items.
:return: An iterator of child items.
"""
return iter(self._children.values())
[docs]
def __len__(self) -> int:
"""
Returns number of child AlertLog items.
:returns: The number of nested AlertLog items.
"""
return len(self._children)
[docs]
def __getitem__(self, name: str) -> "AlertLogItem":
"""Index access for returning child AlertLog items.
:param name: The child's name.
:returns: The referenced child.
:raises KeyError: When the child referenced by parameter 'name' doesn't exist.
"""
return self._children[name]
[docs]
def ToTree(self, format: Callable[[Node], str] = _format) -> Node:
"""
Convert the AlertLog hierarchy starting from this AlertLog item to a :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>`.
:params format: A user-defined :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>` formatting function.
:returns: A tree of nodes referencing an AlertLog item.
"""
node = Node(
value=self,
keyValuePairs={
"Name": self._name,
"TotalErrors": self._totalErrors,
"AlertCountFailures": self._alertCountFailures,
"AlertCountErrors": self._alertCountErrors,
"AlertCountWarnings": self._alertCountWarnings,
"PassedCount": self._passedCount,
"AffirmCount": self._affirmCount
},
children=(child.ToTree() for child in self._children.values()),
format=format
)
return node
[docs]
@export
class Settings(metaclass=ExtendedType, mixin=True):
_externalWarningCount: int
_externalErrorCount: int
_externalFailureCount: int
_failOnDisabledErrors: bool
_failOnRequirementErrors: bool
_failOnWarning: bool
[docs]
def __init__(self) -> None:
self._externalWarningCount = 0
self._externalErrorCount = 0
self._externalFailureCount = 0
self._failOnDisabledErrors = False
self._failOnRequirementErrors = True
self._failOnWarning = False
[docs]
@export
class Document(AlertLogItem, Settings):
"""
An *AlertLog Document* represents an OSVVM AlertLog report document (YAML file).
The document inherits :class:`AlertLogItem` and represents the AlertLog hierarchy's root element.
When analyzing and converting the document, the YAML analysis duration as well as the model conversion duration gets
captured.
"""
_path: Path #: Path to the YAML file.
_yamlDocument: Nullable[YAML] #: Internal YAML document instance.
_analysisDuration: Nullable[timedelta] #: YAML file analysis duration in seconds.
_modelConversionDuration: Nullable[timedelta] #: Data structure conversion duration in seconds.
[docs]
def __init__(self, filename: Path, analyzeAndConvert: bool = False) -> None:
"""
Initializes an AlertLog YAML document.
:param filename: Path to the YAML file.
:param analyzeAndConvert: If true, analyze the YAML document and convert the content to an AlertLog data model instance.
"""
super().__init__("", parent=None)
Settings.__init__(self)
self._path = filename
self._yamlDocument = None
self._analysisDuration = None
self._modelConversionDuration = None
if analyzeAndConvert:
self.Analyze()
self.Parse()
@property
def Path(self) -> Path:
"""
Read-only property to access the path to the YAML file of this document (:attr:`_path`).
:returns: The document's path to the YAML file.
"""
return self._path
@readonly
def AnalysisDuration(self) -> timedelta:
"""
Read-only property to access the time spent for YAML file analysis (:attr:`_analysisDuration`).
:returns: The YAML file analysis duration.
"""
if self._analysisDuration is None:
raise AlertLogException(f"Document '{self._path}' was not analyzed.")
return self._analysisDuration
@readonly
def ModelConversionDuration(self) -> timedelta:
"""
Read-only property to access the time spent for data structure to AlertLog hierarchy conversion (:attr:`_modelConversionDuration`).
:returns: The data structure conversion duration.
"""
if self._modelConversionDuration is None:
raise AlertLogException(f"Document '{self._path}' was not converted.")
return self._modelConversionDuration
[docs]
def Analyze(self) -> None:
"""
Analyze the YAML file (specified by :attr:`_path`) and store the YAML document in :attr:`_yamlDocument`.
:raises AlertLogException: If YAML file doesn't exist.
:raises AlertLogException: If YAML file can't be opened.
"""
if not self._path.exists():
raise AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' does not exist.") \
from FileNotFoundError(f"File '{self._path}' not found.")
with Stopwatch() as sw:
try:
yamlReader = YAML()
self._yamlDocument = yamlReader.load(self._path)
except Exception as ex:
raise AlertLogException(f"Couldn't open '{self._path}'.") from ex
self._analysisDuration = timedelta(seconds=sw.Duration)
[docs]
def Parse(self) -> None:
"""
Convert the YAML data structure to a hierarchy of :class:`AlertLogItem` instances.
:raises AlertLogException: If YAML file was not analyzed.
"""
if self._yamlDocument is None:
ex = AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' needs to be read and analyzed by a YAML parser.")
ex.add_note(f"Call 'Document.Analyze()' or create the document using 'Document(path, parse=True)'.")
raise ex
with Stopwatch() as sw:
self._name = self._ParseStrFieldFromYAML(self._yamlDocument, "Name")
self._status = AlertLogStatus.Parse(self._ParseStrFieldFromYAML(self._yamlDocument, "Status"))
for child in self._ParseSequenceFromYAML(self._yamlDocument, "Children"):
_ = self._ParseAlertLogItem(child, self)
self._modelConversionDuration = timedelta(seconds=sw.Duration)
@staticmethod
def _ParseSequenceFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedSeq]:
try:
value = node[fieldName]
except KeyError as ex:
newEx = OSVVMException(f"Sequence field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
raise newEx from ex
if value is None:
return ()
elif not isinstance(value, CommentedSeq):
ex = AlertLogException(f"Field '{fieldName}' is not a sequence.") # TODO: from TypeError??
ex.add_note(f"Found type {value.__class__.__name__} at line {node._yaml_line_col.data[fieldName][0] + 1}.")
raise ex
return value
@staticmethod
def _ParseMapFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedMap]:
try:
value = node[fieldName]
except KeyError as ex:
newEx = OSVVMException(f"Dictionary field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
raise newEx from ex
if value is None:
return {}
elif not isinstance(value, CommentedMap):
ex = AlertLogException(f"Field '{fieldName}' is not a list.") # TODO: from TypeError??
ex.add_note(f"Type mismatch found for line {node._yaml_line_col.data[fieldName][0] + 1}.")
raise ex
return value
@staticmethod
def _ParseStrFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[str]:
try:
value = node[fieldName]
except KeyError as ex:
newEx = OSVVMException(f"String field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
raise newEx from ex
if not isinstance(value, str):
raise AlertLogException(f"Field '{fieldName}' is not of type str.") # TODO: from TypeError??
return value
@staticmethod
def _ParseIntFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[int]:
try:
value = node[fieldName]
except KeyError as ex:
newEx = OSVVMException(f"Integer field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
raise newEx from ex
if not isinstance(value, int):
raise AlertLogException(f"Field '{fieldName}' is not of type int.") # TODO: from TypeError??
return value
def _ParseAlertLogItem(self, child: CommentedMap, parent: Nullable[AlertLogItem] = None) -> AlertLogItem:
results = self._ParseMapFromYAML(child, "Results")
yamlAlertCount = self._ParseMapFromYAML(results, "AlertCount")
yamlDisabledAlertCount = self._ParseMapFromYAML(results, "DisabledAlertCount")
alertLogItem = AlertLogItem(
self._ParseStrFieldFromYAML(child, "Name"),
AlertLogStatus.Parse(self._ParseStrFieldFromYAML(child, "Status")),
self._ParseIntFieldFromYAML(results, "TotalErrors"),
self._ParseIntFieldFromYAML(yamlAlertCount, "Warning"),
self._ParseIntFieldFromYAML(yamlAlertCount, "Error"),
self._ParseIntFieldFromYAML(yamlAlertCount, "Failure"),
self._ParseIntFieldFromYAML(results, "PassedCount"),
self._ParseIntFieldFromYAML(results, "AffirmCount"),
self._ParseIntFieldFromYAML(results, "RequirementsPassed"),
self._ParseIntFieldFromYAML(results, "RequirementsGoal"),
self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Warning"),
self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Error"),
self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Failure"),
children=(self._ParseAlertLogItem(ch) for ch in self._ParseSequenceFromYAML(child, "Children")),
parent=parent
)
return alertLogItem