Coverage for pyEDAA/Reports/CodeCoverage/UCIS/FSMs.py: 78%
187 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-14 02:26 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-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"""
32The FSM coverage of the UCIS XML interchange format: an instance's ``<fsmCoverage>``, its finite state machines, their
33states and transitions.
34"""
35from __future__ import annotations
37from typing import TYPE_CHECKING, Iterable, Optional as Nullable, Self
39from lxml.etree import _Element
40from pyTooling.Common import getFullyQualifiedName
41from pyTooling.Decorators import export, readonly
43from pyEDAA.Reports.CodeCoverage.UCIS.Bins import Bin
44from pyEDAA.Reports.CodeCoverage.UCIS.Elements import Base, MetricCoverage, ObjectAttributesMixin, UserAttribute
46if TYPE_CHECKING: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 from pyEDAA.Reports.CodeCoverage.UCIS.Instances import InstanceCoverage
50# A class with a property named like a class - ``Bin`` - can't name that class in the annotation of a field: the class
51# body's namespace, where annotations are evaluated, binds the name to the property.
52_Bin = Bin
55@export
56class FSMCoverage(MetricCoverage):
57 """
58 An ``<fsmCoverage>`` of an instance: its finite state machines.
59 """
61 _fsms: list[FSM] #: The finite state machines.
63 def __init__(
64 self,
65 metricMode: Nullable[str] = None,
66 weight: int = 1,
67 userAttributes: Nullable[Iterable[UserAttribute]] = None,
68 *,
69 parent: Nullable[InstanceCoverage] = None
70 ) -> None:
71 """
72 Initialize the FSM coverage, and append it to the FSM coverages of its instance.
74 Its finite state machines are added by creating them with this coverage as their parent.
76 :param metricMode: Optional, the mode in which the coverage was measured. Default: ``None``.
77 :param weight: Optional, weight of the mode in computing the coverage. Default: ``1``.
78 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
79 :param parent: Optional, the instance the coverage belongs to. Default: ``None``.
80 :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`.
81 :raises ValueError: If parameter ``weight`` is ``None``.
82 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
83 :raises ValueError: If parameter ``weight`` is negative.
84 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
85 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
86 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
87 :raises TypeError: If parameter ``parent`` isn't of type
88 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Instances.InstanceCoverage`.
89 """
90 super().__init__(metricMode, weight, userAttributes, parent=parent)
92 self._fsms = []
94 if parent is not None:
95 parent._fsmCoverages.append(self)
97 @classmethod
98 def Parse(cls, element: _Element, *, parent: Nullable[InstanceCoverage] = None) -> Self:
99 """
100 Parse an FSM coverage and its finite state machines from its ``<fsmCoverage>`` element.
102 :param element: The ``<fsmCoverage>`` element.
103 :param parent: Optional, the instance the coverage belongs to. Default: ``None``.
104 :returns: The FSM coverage.
105 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
106 """
107 fsmCoverage = cls(
108 element.attrib.get("metricMode"),
109 int(element.attrib.get("weight", "1")),
110 cls._ParseUserAttributes(element),
111 parent=parent
112 )
114 for fsmElement in element.iterfind("{*}fsm"):
115 FSM.Parse(fsmElement, parent=fsmCoverage)
117 return fsmCoverage
119 @readonly
120 def FSMs(self) -> list[FSM]:
121 """
122 Read-only property to access the finite state machines (:attr:`_fsms`).
124 :returns: The finite state machines, in the order the report lists them.
125 """
126 return self._fsms
129@export
130class FSM(Base, ObjectAttributesMixin):
131 """
132 An ``<fsm>`` of an FSM coverage: a finite state machine, the signal holding its state, its states and transitions.
133 """
135 _parent: Nullable[FSMCoverage] #: The FSM coverage the finite state machine belongs to.
136 _name: Nullable[str] #: Name of the signal holding the state.
137 _type: Nullable[str] #: Declared type of the signal holding the state.
138 _width: Nullable[int] #: Bit width of the signal holding the state.
139 _states: list[FSMState] #: The states.
140 _transitions: list[FSMTransition] #: The transitions.
142 def __init__(
143 self,
144 name: Nullable[str] = None,
145 objectType: Nullable[str] = None,
146 width: Nullable[int] = None,
147 alias: Nullable[str] = None,
148 excluded: bool = False,
149 excludedReason: Nullable[str] = None,
150 weight: int = 1,
151 userAttributes: Nullable[Iterable[UserAttribute]] = None,
152 *,
153 parent: Nullable[FSMCoverage] = None
154 ) -> None:
155 """
156 Initialize the finite state machine, and append it to the finite state machines of its FSM coverage.
158 Its states and transitions are added by creating them with this finite state machine as their parent.
160 :param name: Optional, name of the signal holding the state, e.g. ``ctrl``. Default: ``None``.
161 :param objectType: Optional, declared type of the signal holding the state, e.g. ``reg``. Default: ``None``.
162 :param width: Optional, bit width of the signal holding the state. Default: ``None``.
163 :param alias: Optional, an alias of the finite state machine's name. Default: ``None``.
164 :param excluded: Optional, whether the finite state machine is excluded from the coverage. Default:
165 ``False``.
166 :param excludedReason: Optional, why the finite state machine is excluded. Default: ``None``.
167 :param weight: Optional, weight of the finite state machine in computing the coverage. Default: ``1``.
168 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
169 :param parent: Optional, the FSM coverage the finite state machine belongs to. Default: ``None``.
170 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
171 :raises TypeError: If parameter ``objectType`` isn't of type :class:`str`.
172 :raises TypeError: If parameter ``width`` isn't of type :class:`int`.
173 :raises ValueError: If parameter ``width`` is less than 1.
174 :raises TypeError: If parameter ``alias`` isn't of type :class:`str`.
175 :raises ValueError: If parameter ``excluded`` is ``None``.
176 :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`.
177 :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`.
178 :raises ValueError: If parameter ``weight`` is ``None``.
179 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
180 :raises ValueError: If parameter ``weight`` is negative.
181 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
182 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
183 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
184 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSMCoverage`.
185 """
186 super().__init__(userAttributes)
187 ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight)
189 for parameterName, value in (
190 ("name", name),
191 ("objectType", objectType)
192 ):
193 if value is not None and not isinstance(value, str): 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
195 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
196 raise ex
198 if width is not None: 198 ↛ 208line 198 didn't jump to line 208 because the condition on line 198 was always true
199 if not isinstance(width, int) or isinstance(width, bool): 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 ex = TypeError("Parameter 'width' is not of type 'int'.")
201 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
202 raise ex
203 elif width < 1:
204 ex = ValueError("Parameter 'width' is less than 1.")
205 ex.add_note(f"Got value '{width}'.")
206 raise ex
208 if parent is not None and not isinstance(parent, FSMCoverage): 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 ex = TypeError("Parameter 'parent' is not of type 'FSMCoverage'.")
210 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
211 raise ex
213 self._parent = parent
214 self._name = name
215 self._type = objectType
216 self._width = width
217 self._states = []
218 self._transitions = []
220 if parent is not None: 220 ↛ exitline 220 didn't return from function '__init__' because the condition on line 220 was always true
221 parent._fsms.append(self)
223 @classmethod
224 def Parse(cls, element: _Element, *, parent: Nullable[FSMCoverage] = None) -> Self:
225 """
226 Parse a finite state machine, its states and transitions from its ``<fsm>`` element.
228 :param element: The ``<fsm>`` element.
229 :param parent: Optional, the FSM coverage the finite state machine belongs to. Default: ``None``.
230 :returns: The finite state machine.
231 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
232 """
233 width = element.attrib.get("width")
234 fsm = cls(
235 element.attrib.get("name"),
236 element.attrib.get("type"),
237 None if width is None else int(width),
238 element.attrib.get("alias"),
239 cls._ParseBoolean(element.attrib.get("excluded", "false")),
240 element.attrib.get("excludedReason"),
241 int(element.attrib.get("weight", "1")),
242 cls._ParseUserAttributes(element),
243 parent=parent
244 )
246 for stateElement in element.iterfind("{*}state"):
247 FSMState.Parse(stateElement, parent=fsm)
249 for transitionElement in element.iterfind("{*}stateTransition"):
250 FSMTransition.Parse(transitionElement, parent=fsm)
252 return fsm
254 @readonly
255 def Parent(self) -> Nullable[FSMCoverage]:
256 """
257 Read-only property to access the FSM coverage the finite state machine belongs to (:attr:`_parent`).
259 :returns: The FSM coverage; ``None``, if the finite state machine belongs to none.
260 """
261 return self._parent
263 @readonly
264 def Name(self) -> Nullable[str]:
265 """
266 Read-only property to access the name of the signal holding the state (:attr:`_name`).
268 :returns: The name, e.g. ``ctrl``; ``None``, if the report states none.
269 """
270 return self._name
272 @readonly
273 def Type(self) -> Nullable[str]:
274 """
275 Read-only property to access the declared type of the signal holding the state (:attr:`_type`).
277 :returns: The type, e.g. ``reg``; ``None``, if the report states none.
278 """
279 return self._type
281 @readonly
282 def Width(self) -> Nullable[int]:
283 """
284 Read-only property to access the bit width of the signal holding the state (:attr:`_width`).
286 :returns: The width; ``None``, if the report states none.
287 """
288 return self._width
290 @readonly
291 def States(self) -> list[FSMState]:
292 """
293 Read-only property to access the states (:attr:`_states`).
295 :returns: The states, in the order the report lists them.
296 """
297 return self._states
299 @readonly
300 def Transitions(self) -> list[FSMTransition]:
301 """
302 Read-only property to access the transitions (:attr:`_transitions`).
304 :returns: The transitions, in the order the report lists them.
305 """
306 return self._transitions
309@export
310class FSMState(Base):
311 """
312 A ``<state>`` of a finite state machine: its name, value and bin - how often the machine was in it.
313 """
315 _parent: Nullable[FSM] #: The finite state machine the state belongs to.
316 _name: Nullable[str] #: Name of the state.
317 _value: Nullable[str] #: Value representing the state.
318 _bin: _Bin #: How often the machine was in the state.
320 def __init__(
321 self,
322 coverBin: Bin,
323 name: Nullable[str] = None,
324 value: Nullable[str] = None,
325 userAttributes: Nullable[Iterable[UserAttribute]] = None,
326 *,
327 parent: Nullable[FSM] = None
328 ) -> None:
329 """
330 Initialize the state, become its bin's parent, and append it to the states of its finite state machine.
332 :param coverBin: How often the machine was in the state.
333 :param name: Optional, name of the state, e.g. ``IDLE``. Default: ``None``.
334 :param value: Optional, value representing the state, e.g. ``4``. Default: ``None``.
335 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
336 :param parent: Optional, the finite state machine the state belongs to. Default: ``None``.
337 :raises ValueError: If parameter ``coverBin`` is ``None``.
338 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`.
339 :raises TypeError: If parameter ``name`` isn't of type :class:`str`.
340 :raises TypeError: If parameter ``value`` isn't of type :class:`str`.
341 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
342 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
343 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
344 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSM`.
345 """
346 super().__init__(userAttributes)
348 if coverBin is None: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true
349 raise ValueError("Parameter 'coverBin' is None.")
350 elif not isinstance(coverBin, Bin): 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true
351 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.")
352 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
353 raise ex
355 for parameterName, parameterValue in (
356 ("name", name),
357 ("value", value)
358 ):
359 if parameterValue is not None and not isinstance(parameterValue, str): 359 ↛ 360line 359 didn't jump to line 360 because the condition on line 359 was never true
360 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
361 ex.add_note(f"Got type '{getFullyQualifiedName(parameterValue)}'.")
362 raise ex
364 if parent is not None and not isinstance(parent, FSM): 364 ↛ 365line 364 didn't jump to line 365 because the condition on line 364 was never true
365 ex = TypeError("Parameter 'parent' is not of type 'FSM'.")
366 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
367 raise ex
369 self._parent = parent
370 self._name = name
371 self._value = value
372 self._bin = coverBin
373 coverBin._parent = self
375 if parent is not None: 375 ↛ exitline 375 didn't return from function '__init__' because the condition on line 375 was always true
376 parent._states.append(self)
378 @classmethod
379 def Parse(cls, element: _Element, *, parent: Nullable[FSM] = None) -> Self:
380 """
381 Parse a state and its bin from its ``<state>`` element.
383 :param element: The ``<state>`` element.
384 :param parent: Optional, the finite state machine the state belongs to. Default: ``None``.
385 :returns: The state.
386 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
387 """
388 return cls(
389 Bin.Parse(element.find("{*}stateBin")),
390 element.attrib.get("stateName"),
391 element.attrib.get("stateValue"),
392 cls._ParseUserAttributes(element),
393 parent=parent
394 )
396 @readonly
397 def Parent(self) -> Nullable[FSM]:
398 """
399 Read-only property to access the finite state machine the state belongs to (:attr:`_parent`).
401 :returns: The finite state machine; ``None``, if the state belongs to none.
402 """
403 return self._parent
405 @readonly
406 def Name(self) -> Nullable[str]:
407 """
408 Read-only property to access the name of the state (:attr:`_name`).
410 :returns: The name, e.g. ``IDLE``; ``None``, if the report states none.
411 """
412 return self._name
414 @readonly
415 def Value(self) -> Nullable[str]:
416 """
417 Read-only property to access the value representing the state (:attr:`_value`).
419 :returns: The value, as stated; ``None``, if the report states none.
420 """
421 return self._value
423 @readonly
424 def Bin(self) -> Bin:
425 """
426 Read-only property to access the state's bin (:attr:`_bin`).
428 :returns: The bin, stating how often the machine was in the state.
429 """
430 return self._bin
433@export
434class FSMTransition(Base):
435 """
436 A ``<stateTransition>`` of a finite state machine: the states it passes, and its bin - how often it happened.
437 """
439 _parent: Nullable[FSM] #: The finite state machine the transition belongs to.
440 _states: list[str] #: The states the transition passes, from the first one.
441 _bin: _Bin #: How often the transition happened.
443 def __init__(
444 self,
445 states: Iterable[str],
446 coverBin: Bin,
447 userAttributes: Nullable[Iterable[UserAttribute]] = None,
448 *,
449 parent: Nullable[FSM] = None
450 ) -> None:
451 """
452 Initialize the transition, become its bin's parent, and append it to the transitions of its finite state machine.
454 :param states: The states the transition passes, from the first one, e.g. ``["IDLE", "ACTIVE"]``.
455 :param coverBin: How often the transition happened.
456 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
457 :param parent: Optional, the finite state machine the transition belongs to. Default: ``None``.
458 :raises ValueError: If parameter ``states`` is ``None``.
459 :raises TypeError: If parameter ``states`` isn't iterable.
460 :raises TypeError: If parameter ``states`` contains an element not of type :class:`str`.
461 :raises ValueError: If parameter ``states`` has less than two states.
462 :raises ValueError: If parameter ``coverBin`` is ``None``.
463 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`.
464 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
465 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
466 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
467 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSM`.
468 """
469 super().__init__(userAttributes)
471 if coverBin is None: 471 ↛ 472line 471 didn't jump to line 472 because the condition on line 471 was never true
472 raise ValueError("Parameter 'coverBin' is None.")
473 elif not isinstance(coverBin, Bin): 473 ↛ 474line 473 didn't jump to line 474 because the condition on line 473 was never true
474 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.")
475 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
476 raise ex
478 if parent is not None and not isinstance(parent, FSM): 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true
479 ex = TypeError("Parameter 'parent' is not of type 'FSM'.")
480 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
481 raise ex
483 self._parent = parent
484 self._states = stateList = []
485 self._bin = coverBin
486 coverBin._parent = self
488 if states is None: 488 ↛ 489line 488 didn't jump to line 489 because the condition on line 488 was never true
489 raise ValueError("Parameter 'states' is None.")
490 elif not isinstance(states, Iterable): 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true
491 ex = TypeError("Parameter 'states' is not iterable.")
492 ex.add_note(f"Got type '{getFullyQualifiedName(states)}'.")
493 raise ex
495 for state in states:
496 if not isinstance(state, str): 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true
497 ex = TypeError("Parameter 'states' contains an element not of type 'str'.")
498 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
499 raise ex
501 stateList.append(state)
503 if len(self._states) < 2:
504 ex = ValueError("Parameter 'states' has less than two states.")
505 ex.add_note(f"Got {len(self._states)} state(s).")
506 raise ex
508 if parent is not None: 508 ↛ exitline 508 didn't return from function '__init__' because the condition on line 508 was always true
509 parent._transitions.append(self)
511 @classmethod
512 def Parse(cls, element: _Element, *, parent: Nullable[FSM] = None) -> Self:
513 """
514 Parse a transition and its bin from its ``<stateTransition>`` element.
516 :param element: The ``<stateTransition>`` element.
517 :param parent: Optional, the finite state machine the transition belongs to. Default: ``None``.
518 :returns: The transition.
519 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
520 """
521 return cls(
522 [stateElement.text or "" for stateElement in element.iterfind("{*}state")],
523 Bin.Parse(element.find("{*}transitionBin")),
524 cls._ParseUserAttributes(element),
525 parent=parent
526 )
528 @readonly
529 def Parent(self) -> Nullable[FSM]:
530 """
531 Read-only property to access the finite state machine the transition belongs to (:attr:`_parent`).
533 :returns: The finite state machine; ``None``, if the transition belongs to none.
534 """
535 return self._parent
537 @readonly
538 def States(self) -> list[str]:
539 """
540 Read-only property to access the states the transition passes (:attr:`_states`).
542 :returns: The states, from the first one, as the report states them.
543 """
544 return self._states
546 @readonly
547 def Bin(self) -> Bin:
548 """
549 Read-only property to access the transition's bin (:attr:`_bin`).
551 :returns: The bin, stating how often the transition happened.
552 """
553 return self._bin