Coverage for pyEDAA/OSVVM/AlertLog.py: 80%
282 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-12 23:17 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-12 23:17 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ ______ ____ ____ __ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \/ ___\ \ / /\ \ / / \/ | #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | \___ \\ \ / / \ \ / /| |\/| | #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| |___) |\ V / \ V / | | | | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/|____/ \_/ \_/ |_| |_| #
7# |_| |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2021-2026 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
43from pyTooling.Versioning import SemanticVersion
45from pyEDAA.OSVVM import OSVVMException
48@export
49class AlertLogException(OSVVMException):
50 """Base-class for all pyEDAA.OSVVM.AlertLog specific exceptions."""
53@export
54class DuplicateItemException(AlertLogException):
55 """Raised if a duplicate item is detected in the AlertLog hierarchy."""
58@export
59class AlertLogStatus(Enum):
60 """Status of an :class:`AlertLogItem`."""
61 Unknown = auto()
62 Passed = auto()
63 Failed = auto()
65 __MAPPINGS__ = {
66 "passed": Passed,
67 "failed": Failed
68 }
70 @classmethod
71 def Parse(self, name: str) -> "AlertLogStatus":
72 try:
73 return self.__MAPPINGS__[name.lower()]
74 except KeyError as ex:
75 raise AlertLogException(f"Unknown AlertLog status '{name}'.") from ex
77 def __bool__(self) -> bool:
78 """
79 Convert an *AlertLogStatus* to a boolean.
81 :returns: Return true, if the status is ``Passed``.
82 """
83 return self is self.Passed
86@export
87def _format(node: Node) -> str:
88 """
89 User-defined :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>` formatting function for nodes referencing :class:`AlertLogItems <AlertLogItem>`.
91 :param node: Node to format.
92 :returns: String representation (one-liner) describing an AlertLogItem.
93 """
94 return f"{node['Name']}: {node['TotalErrors']}={node['AlertCountFailures']}/{node['AlertCountErrors']}/{node['AlertCountWarnings']} {node['PassedCount']}/{node['AffirmCount']}"
97@export
98class AlertLogItem(metaclass=ExtendedType, slots=True):
99 """
100 An *AlertLogItem* represents an AlertLog hierarchy item.
102 An item has a reference to its parent item in the AlertLog hierarchy. If the item is the top-most element (root
103 element), the parent reference is ``None``.
105 An item can contain further child items.
106 """
107 _parent: "AlertLogItem" #: Reference to the parent item.
108 _name: str #: Name of the AlertLog item.
109 _children: Dict[str, "AlertLogItem"] #: Dictionary of child items.
111 _status: AlertLogStatus #: AlertLog item's status
112 _totalErrors: int #: Total number of warnings, errors and failures.
113 _alertCountWarnings: int #: Warning count.
114 _alertCountErrors: int #: Error count.
115 _alertCountFailures: int #: Failure count.
116 _passedCount: int #: Passed affirmation count.
117 _affirmCount: int #: Overall affirmation count (incl. failed affirmations).
118 _requirementsPassed: int #: Count of passed requirements.
119 _requirementsGoal: int #: Overall expected requirements.
120 _disabledAlertCountWarnings: int #: Count of disabled warnings.
121 _disabledAlertCountErrors: int #: Count of disabled errors.
122 _disabledAlertCountFailures: int #: Count of disabled failures.
124 def __init__(
125 self,
126 name: str,
127 status: AlertLogStatus = AlertLogStatus.Unknown,
128 totalErrors: int = 0,
129 alertCountWarnings: int = 0,
130 alertCountErrors: int = 0,
131 alertCountFailures: int = 0,
132 passedCount: int = 0,
133 affirmCount: int = 0,
134 requirementsPassed: int = 0,
135 requirementsGoal: int = 0,
136 disabledAlertCountWarnings: int = 0,
137 disabledAlertCountErrors: int = 0,
138 disabledAlertCountFailures: int = 0,
139 children: Iterable["AlertLogItem"] = None,
140 parent: Nullable["AlertLogItem"] = None
141 ) -> None:
142 self._name = name
143 self._parent = parent
144 if parent is not None:
145 if not isinstance(parent, AlertLogItem): 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 ex = TypeError(f"Parameter 'parent' is not an AlertLogItem.")
147 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
148 raise ex
149 elif name in parent._children: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 raise DuplicateItemException(f"AlertLogItem '{name}' already exists in '{parent._name}'.")
152 parent._children[name] = self
154 self._children = {}
155 if children is not None:
156 for child in children:
157 if not isinstance(child, AlertLogItem): 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 ex = TypeError(f"Item in parameter 'children' is not an AlertLogItem.")
159 ex.add_note(f"Got type '{getFullyQualifiedName(child)}'.")
160 raise ex
161 elif child._name in self._children: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 raise DuplicateItemException(f"AlertLogItem '{child._name}' already exists in '{self._name}'.")
163 elif child._parent is not None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 raise AlertLogException(f"AlertLogItem '{child._name}' is already part of another AlertLog hierarchy ({child._parent._name}).")
166 self._children[child._name] = child
167 child._parent = self
169 self._status = status
170 self._totalErrors = totalErrors
171 self._alertCountWarnings = alertCountWarnings
172 self._alertCountErrors = alertCountErrors
173 self._alertCountFailures = alertCountFailures
174 self._passedCount = passedCount
175 self._affirmCount = affirmCount
176 self._requirementsPassed = requirementsPassed
177 self._requirementsGoal = requirementsGoal
178 self._disabledAlertCountWarnings = disabledAlertCountWarnings
179 self._disabledAlertCountErrors = disabledAlertCountErrors
180 self._disabledAlertCountFailures = disabledAlertCountFailures
182 @property
183 def Parent(self) -> Nullable["AlertLogItem"]:
184 """
185 Property to access the parent item of this item (:attr:`_parent`).
187 :returns: The item's parent item. ``None``, if it's the top-most item (root).
188 """
189 return self._parent
191 @Parent.setter
192 def Parent(self, value: Nullable["AlertLogItem"]) -> None:
193 if value is None: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 del self._parent._children[self._name]
195 else:
196 if not isinstance(value, AlertLogItem): 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 ex = TypeError(f"Parameter 'value' is not an AlertLogItem.")
198 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
199 raise ex
200 elif self._name in value._children: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 raise DuplicateItemException(f"AlertLogItem '{self._name}' already exists in '{value._name}'.")
203 value._children[self._name] = self
205 self._parent = value
207 @readonly
208 def Name(self) -> str:
209 """
210 Read-only property to access the AlertLog item's name (:attr:`_name`).
212 :returns: AlertLog item's name.
213 """
214 return self._name
216 @readonly
217 def Status(self) -> AlertLogStatus:
218 """
219 Read-only property to access the AlertLog item's status (:attr:`_status`).
221 :returns: AlertLog item's status.
222 """
223 return self._status
225 @readonly
226 def TotalErrors(self) -> int:
227 """
228 Read-only property to access the AlertLog item's total error count (:attr:`_totalErrors`).
230 :returns: AlertLog item's total errors.
231 """
232 return self._totalErrors
234 @readonly
235 def AlertCountWarnings(self) -> int:
236 """
237 Read-only property to access the AlertLog item's warning count (:attr:`_alertCountWarnings`).
239 :returns: AlertLog item's warning count.
240 """
241 return self._alertCountWarnings
243 @readonly
244 def AlertCountErrors(self) -> int:
245 """
246 Read-only property to access the AlertLog item's error count (:attr:`_alertCountErrors`).
248 :returns: AlertLog item's error count.
249 """
250 return self._alertCountErrors
252 @readonly
253 def AlertCountFailures(self) -> int:
254 """
255 Read-only property to access the AlertLog item's failure count (:attr:`_alertCountFailures`).
257 :returns: AlertLog item's failure count.
258 """
259 return self._alertCountFailures
261 @readonly
262 def PassedCount(self) -> int:
263 """
264 Read-only property to access the AlertLog item's passed affirmation count (:attr:`_alertCountFailures`).
266 :returns: AlertLog item's passed affirmations.
267 """
268 return self._passedCount
270 @readonly
271 def AffirmCount(self) -> int:
272 """
273 Read-only property to access the AlertLog item's overall affirmation count (:attr:`_affirmCount`).
275 :returns: AlertLog item's overall affirmations.
276 """
277 return self._affirmCount
279 @readonly
280 def RequirementsPassed(self) -> int:
281 return self._requirementsPassed
283 @readonly
284 def RequirementsGoal(self) -> int:
285 return self._requirementsGoal
287 @readonly
288 def DisabledAlertCountWarnings(self) -> int:
289 """
290 Read-only property to access the AlertLog item's count of disabled warnings (:attr:`_disabledAlertCountWarnings`).
292 :returns: AlertLog item's count of disabled warnings.
293 """
294 return self._disabledAlertCountWarnings
296 @readonly
297 def DisabledAlertCountErrors(self) -> int:
298 """
299 Read-only property to access the AlertLog item's count of disabled errors (:attr:`_disabledAlertCountErrors`).
301 :returns: AlertLog item's count of disabled errors.
302 """
303 return self._disabledAlertCountErrors
305 @readonly
306 def DisabledAlertCountFailures(self) -> int:
307 """
308 Read-only property to access the AlertLog item's count of disabled failures (:attr:`_disabledAlertCountFailures`).
310 :returns: AlertLog item's count of disabled failures.
311 """
312 return self._disabledAlertCountFailures
314 @readonly
315 def Children(self) -> Dict[str, "AlertLogItem"]:
316 return self._children
318 def __iter__(self) -> Iterator["AlertLogItem"]:
319 """
320 Iterate all child AlertLog items.
322 :returns: An iterator of child items.
323 """
324 return iter(self._children.values())
326 def __len__(self) -> int:
327 """
328 Returns number of child AlertLog items.
330 :returns: The number of nested AlertLog items.
331 """
332 return len(self._children)
334 def __getitem__(self, name: str) -> "AlertLogItem":
335 """Index access for returning child AlertLog items.
337 :param name: The child's name.
338 :returns: The referenced child.
339 :raises KeyError: When the child referenced by parameter 'name' doesn't exist.
340 """
341 return self._children[name]
343 def ToTree(self, format: Callable[[Node], str] = _format) -> Node:
344 """
345 Convert the AlertLog hierarchy starting from this AlertLog item to a :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>`.
347 :params format: A user-defined :external+pyTool:ref:`pyTooling Tree <STRUCT/Tree>` formatting function.
348 :returns: A tree of nodes referencing an AlertLog item.
349 """
350 node = Node(
351 value=self,
352 keyValuePairs={
353 "Name": self._name,
354 "TotalErrors": self._totalErrors,
355 "AlertCountFailures": self._alertCountFailures,
356 "AlertCountErrors": self._alertCountErrors,
357 "AlertCountWarnings": self._alertCountWarnings,
358 "PassedCount": self._passedCount,
359 "AffirmCount": self._affirmCount
360 },
361 children=(child.ToTree() for child in self._children.values()),
362 format=format
363 )
365 return node
368@export
369class Settings(metaclass=ExtendedType, mixin=True):
370 _externalWarningCount: int
371 _externalErrorCount: int
372 _externalFailureCount: int
373 _failOnDisabledErrors: bool
374 _failOnRequirementErrors: bool
375 _failOnWarning: bool
377 def __init__(self) -> None:
378 self._externalWarningCount = 0
379 self._externalErrorCount = 0
380 self._externalFailureCount = 0
381 self._failOnDisabledErrors = False
382 self._failOnRequirementErrors = True
383 self._failOnWarning = False
386# TODO: Derive from common Document class?
387@export
388class Document(AlertLogItem, Settings):
389 """
390 An *AlertLog Document* represents an OSVVM AlertLog report document (YAML file).
392 The document inherits :class:`AlertLogItem` and represents the AlertLog hierarchy's root element.
394 When analyzing and converting the document, the YAML analysis duration as well as the model conversion duration gets
395 captured.
396 """
398 _path: Path #: Path to the YAML file.
399 _yamlDocument: Nullable[YAML] #: Internal YAML document instance.
400 _version: Nullable[SemanticVersion] #: YAML data structure version.
402 _analysisDuration: Nullable[timedelta] #: YAML file analysis duration in seconds.
403 _modelConversionDuration: Nullable[timedelta] #: Data structure conversion duration in seconds.
405 def __init__(self, filename: Path, analyzeAndConvert: bool = False) -> None:
406 """
407 Initializes an AlertLog YAML document.
409 :param filename: Path to the YAML file.
410 :param analyzeAndConvert: If true, analyze the YAML document and convert the content to an AlertLog data model instance.
411 """
412 super().__init__("", parent=None)
413 Settings.__init__(self)
415 self._path = filename
416 self._yamlDocument = None
417 self._version = None
419 self._analysisDuration = None
420 self._modelConversionDuration = None
422 if analyzeAndConvert:
423 self.Analyze()
424 self.Parse()
426 @property
427 def Path(self) -> Path:
428 """
429 Read-only property to access the path to the YAML file of this document (:attr:`_path`).
431 :returns: The document's path to the YAML file.
432 """
433 return self._path
435 @readonly
436 def AnalysisDuration(self) -> timedelta:
437 """
438 Read-only property to access the time spent for YAML file analysis (:attr:`_analysisDuration`).
440 :returns: The YAML file analysis duration.
441 """
442 if self._analysisDuration is None:
443 raise AlertLogException(f"Document '{self._path}' was not analyzed.")
445 return self._analysisDuration
447 @readonly
448 def ModelConversionDuration(self) -> timedelta:
449 """
450 Read-only property to access the time spent for data structure to AlertLog hierarchy conversion (:attr:`_modelConversionDuration`).
452 :returns: The data structure conversion duration.
453 """
454 if self._modelConversionDuration is None:
455 raise AlertLogException(f"Document '{self._path}' was not converted.")
457 return self._modelConversionDuration
459 def Analyze(self) -> None:
460 """
461 Analyze the YAML file (specified by :attr:`_path`) and store the YAML document in :attr:`_yamlDocument`.
463 :raises AlertLogException: If YAML file doesn't exist.
464 :raises AlertLogException: If YAML file can't be opened.
465 """
466 if not self._path.exists(): 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true
467 raise AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' does not exist.") \
468 from FileNotFoundError(f"File '{self._path}' not found.")
470 with Stopwatch() as sw:
471 try:
472 yamlReader = YAML()
473 self._yamlDocument = yamlReader.load(self._path)
474 except Exception as ex:
475 raise AlertLogException(f"Couldn't open '{self._path}'.") from ex
477 self._analysisDuration = timedelta(seconds=sw.Duration)
479 def Parse(self) -> None:
480 """
481 Convert the YAML data structure to a hierarchy of :class:`AlertLogItem` instances.
483 :raises AlertLogException: If YAML file was not analyzed.
484 """
485 if self._yamlDocument is None: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 ex = AlertLogException(f"OSVVM AlertLog YAML file '{self._path}' needs to be read and analyzed by a YAML parser.")
487 ex.add_note(f"Call 'Document.Analyze()' or create the document using 'Document(path, parse=True)'.")
488 raise ex
490 with Stopwatch() as sw:
491 self._version = SemanticVersion.Parse(self._yamlDocument["Version"])
492 if not (self._version >> "0.1"): 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true
493 ex = AlertLogException(f"Unsupported YAML data structure version {self._version} for file '{self._path}'.")
494 ex.add_note("Supported versions are: 1.0")
495 raise ex
497 self._name = self._ParseStrFieldFromYAML(self._yamlDocument, "Name")
498 self._status = AlertLogStatus.Parse(self._ParseStrFieldFromYAML(self._yamlDocument, "Status"))
499 for child in self._ParseSequenceFromYAML(self._yamlDocument, "Children"):
500 _ = self._ParseAlertLogItem(child, self)
502 self._modelConversionDuration = timedelta(seconds=sw.Duration)
504 @staticmethod
505 def _ParseSequenceFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedSeq]:
506 try:
507 value = node[fieldName]
508 except KeyError as ex:
509 newEx = OSVVMException(f"Sequence field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
510 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
511 raise newEx from ex
513 if value is None: 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true
514 return ()
515 elif not isinstance(value, CommentedSeq): 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true
516 ex = AlertLogException(f"Field '{fieldName}' is not a sequence.") # TODO: from TypeError??
517 ex.add_note(f"Found type {value.__class__.__name__} at line {node._yaml_line_col.data[fieldName][0] + 1}.")
518 raise ex
520 return value
522 @staticmethod
523 def _ParseMapFromYAML(node: CommentedMap, fieldName: str) -> Nullable[CommentedMap]:
524 try:
525 value = node[fieldName]
526 except KeyError as ex:
527 newEx = OSVVMException(f"Dictionary field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
528 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
529 raise newEx from ex
531 if value is None: 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true
532 return {}
533 elif not isinstance(value, CommentedMap): 533 ↛ 534line 533 didn't jump to line 534 because the condition on line 533 was never true
534 ex = AlertLogException(f"Field '{fieldName}' is not a list.") # TODO: from TypeError??
535 ex.add_note(f"Type mismatch found for line {node._yaml_line_col.data[fieldName][0] + 1}.")
536 raise ex
537 return value
539 @staticmethod
540 def _ParseStrFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[str]:
541 try:
542 value = node[fieldName]
543 except KeyError as ex:
544 newEx = OSVVMException(f"String field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
545 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
546 raise newEx from ex
548 if not isinstance(value, str): 548 ↛ 549line 548 didn't jump to line 549 because the condition on line 548 was never true
549 raise AlertLogException(f"Field '{fieldName}' is not of type str.") # TODO: from TypeError??
551 return value
553 @staticmethod
554 def _ParseIntFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[int]:
555 try:
556 value = node[fieldName]
557 except KeyError as ex:
558 newEx = OSVVMException(f"Integer field '{fieldName}' not found in node starting at line {node.lc.line + 1}.")
559 newEx.add_note(f"Available fields: {', '.join(key for key in node)}")
560 raise newEx from ex
562 if not isinstance(value, int): 562 ↛ 563line 562 didn't jump to line 563 because the condition on line 562 was never true
563 raise AlertLogException(f"Field '{fieldName}' is not of type int.") # TODO: from TypeError??
565 return value
567 def _ParseAlertLogItem(self, child: CommentedMap, parent: Nullable[AlertLogItem] = None) -> AlertLogItem:
568 results = self._ParseMapFromYAML(child, "Results")
569 yamlAlertCount = self._ParseMapFromYAML(results, "AlertCount")
570 yamlDisabledAlertCount = self._ParseMapFromYAML(results, "DisabledAlertCount")
571 alertLogItem = AlertLogItem(
572 self._ParseStrFieldFromYAML(child, "Name"),
573 AlertLogStatus.Parse(self._ParseStrFieldFromYAML(child, "Status")),
574 self._ParseIntFieldFromYAML(results, "TotalErrors"),
575 self._ParseIntFieldFromYAML(yamlAlertCount, "Warning"),
576 self._ParseIntFieldFromYAML(yamlAlertCount, "Error"),
577 self._ParseIntFieldFromYAML(yamlAlertCount, "Failure"),
578 self._ParseIntFieldFromYAML(results, "PassedCount"),
579 self._ParseIntFieldFromYAML(results, "AffirmCount"),
580 self._ParseIntFieldFromYAML(results, "RequirementsPassed"),
581 self._ParseIntFieldFromYAML(results, "RequirementsGoal"),
582 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Warning"),
583 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Error"),
584 self._ParseIntFieldFromYAML(yamlDisabledAlertCount, "Failure"),
585 children=(self._ParseAlertLogItem(ch) for ch in self._ParseSequenceFromYAML(child, "Children")),
586 parent=parent
587 )
589 return alertLogItem