Coverage for pyEDAA/Reports/CodeCoverage/UCIS/FSMs.py: 78%
187 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-21 23:49 +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 ``userAttributes`` isn't iterable.
81 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
82 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
83 :raises TypeError: If parameter ``metricMode`` isn't of type :class:`str`.
84 :raises ValueError: If parameter ``weight`` is ``None``.
85 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
86 :raises ValueError: If parameter ``weight`` is negative.
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 ``userAttributes`` isn't iterable.
171 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
172 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
173 :raises TypeError: If parameter ``alias`` isn't of type :class:`str`.
174 :raises ValueError: If parameter ``excluded`` is ``None``.
175 :raises TypeError: If parameter ``excluded`` isn't of type :class:`bool`.
176 :raises TypeError: If parameter ``excludedReason`` isn't of type :class:`str`.
177 :raises ValueError: If parameter ``weight`` is ``None``.
178 :raises TypeError: If parameter ``weight`` isn't of type :class:`int`.
179 :raises ValueError: If parameter ``weight`` is negative.
180 :raises TypeError: If parameter ``name`` or ``objectType`` isn't of type :class:`str`.
181 :raises TypeError: If parameter ``width`` isn't of type :class:`int`.
182 :raises ValueError: If parameter ``width`` is less than 1.
183 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSMCoverage`.
184 """
185 super().__init__(userAttributes)
186 ObjectAttributesMixin.__init__(self, alias, excluded, excludedReason, weight)
188 for parameterName, value in (
189 ("name", name),
190 ("objectType", objectType)
191 ):
192 if value is not None and not isinstance(value, str): 192 ↛ 193line 192 didn't jump to line 193 because the condition on line 192 was never true
193 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
194 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
195 raise ex
197 if width is not None: 197 ↛ 207line 197 didn't jump to line 207 because the condition on line 197 was always true
198 if not isinstance(width, int) or isinstance(width, bool): 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 ex = TypeError("Parameter 'width' is not of type 'int'.")
200 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
201 raise ex
202 elif width < 1:
203 ex = ValueError("Parameter 'width' is less than 1.")
204 ex.add_note(f"Got value '{width}'.")
205 raise ex
207 if parent is not None and not isinstance(parent, FSMCoverage): 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true
208 ex = TypeError("Parameter 'parent' is not of type 'FSMCoverage'.")
209 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
210 raise ex
212 self._parent = parent
213 self._name = name
214 self._type = objectType
215 self._width = width
216 self._states = []
217 self._transitions = []
219 if parent is not None: 219 ↛ exitline 219 didn't return from function '__init__' because the condition on line 219 was always true
220 parent._fsms.append(self)
222 @classmethod
223 def Parse(cls, element: _Element, *, parent: Nullable[FSMCoverage] = None) -> Self:
224 """
225 Parse a finite state machine, its states and transitions from its ``<fsm>`` element.
227 :param element: The ``<fsm>`` element.
228 :param parent: Optional, the FSM coverage the finite state machine belongs to. Default: ``None``.
229 :returns: The finite state machine.
230 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
231 """
232 width = element.attrib.get("width")
233 fsm = cls(
234 element.attrib.get("name"),
235 element.attrib.get("type"),
236 None if width is None else int(width),
237 element.attrib.get("alias"),
238 cls._ParseBoolean(element.attrib.get("excluded", "false")),
239 element.attrib.get("excludedReason"),
240 int(element.attrib.get("weight", "1")),
241 cls._ParseUserAttributes(element),
242 parent=parent
243 )
245 for stateElement in element.iterfind("{*}state"):
246 FSMState.Parse(stateElement, parent=fsm)
248 for transitionElement in element.iterfind("{*}stateTransition"):
249 FSMTransition.Parse(transitionElement, parent=fsm)
251 return fsm
253 @readonly
254 def Parent(self) -> Nullable[FSMCoverage]:
255 """
256 Read-only property to access the FSM coverage the finite state machine belongs to (:attr:`_parent`).
258 :returns: The FSM coverage; ``None``, if the finite state machine belongs to none.
259 """
260 return self._parent
262 @readonly
263 def Name(self) -> Nullable[str]:
264 """
265 Read-only property to access the name of the signal holding the state (:attr:`_name`).
267 :returns: The name, e.g. ``ctrl``; ``None``, if the report states none.
268 """
269 return self._name
271 @readonly
272 def Type(self) -> Nullable[str]:
273 """
274 Read-only property to access the declared type of the signal holding the state (:attr:`_type`).
276 :returns: The type, e.g. ``reg``; ``None``, if the report states none.
277 """
278 return self._type
280 @readonly
281 def Width(self) -> Nullable[int]:
282 """
283 Read-only property to access the bit width of the signal holding the state (:attr:`_width`).
285 :returns: The width; ``None``, if the report states none.
286 """
287 return self._width
289 @readonly
290 def States(self) -> list[FSMState]:
291 """
292 Read-only property to access the states (:attr:`_states`).
294 :returns: The states, in the order the report lists them.
295 """
296 return self._states
298 @readonly
299 def Transitions(self) -> list[FSMTransition]:
300 """
301 Read-only property to access the transitions (:attr:`_transitions`).
303 :returns: The transitions, in the order the report lists them.
304 """
305 return self._transitions
308@export
309class FSMState(Base):
310 """
311 A ``<state>`` of a finite state machine: its name, value and bin - how often the machine was in it.
312 """
314 _parent: Nullable[FSM] #: The finite state machine the state belongs to.
315 _name: Nullable[str] #: Name of the state.
316 _value: Nullable[str] #: Value representing the state.
317 _bin: _Bin #: How often the machine was in the state.
319 def __init__(
320 self,
321 coverBin: Bin,
322 name: Nullable[str] = None,
323 value: Nullable[str] = None,
324 userAttributes: Nullable[Iterable[UserAttribute]] = None,
325 *,
326 parent: Nullable[FSM] = None
327 ) -> None:
328 """
329 Initialize the state, become its bin's parent, and append it to the states of its finite state machine.
331 :param coverBin: How often the machine was in the state.
332 :param name: Optional, name of the state, e.g. ``IDLE``. Default: ``None``.
333 :param value: Optional, value representing the state, e.g. ``4``. Default: ``None``.
334 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
335 :param parent: Optional, the finite state machine the state belongs to. Default: ``None``.
336 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
337 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
338 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
339 :raises ValueError: If parameter ``coverBin`` is ``None``.
340 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`.
341 :raises TypeError: If parameter ``name`` or ``value`` isn't of type :class:`str`.
342 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSM`.
343 """
344 super().__init__(userAttributes)
346 if coverBin is None: 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true
347 raise ValueError("Parameter 'coverBin' is None.")
348 elif not isinstance(coverBin, Bin): 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true
349 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.")
350 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
351 raise ex
353 for parameterName, parameterValue in (
354 ("name", name),
355 ("value", value)
356 ):
357 if parameterValue is not None and not isinstance(parameterValue, str): 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true
358 ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
359 ex.add_note(f"Got type '{getFullyQualifiedName(parameterValue)}'.")
360 raise ex
362 if parent is not None and not isinstance(parent, FSM): 362 ↛ 363line 362 didn't jump to line 363 because the condition on line 362 was never true
363 ex = TypeError("Parameter 'parent' is not of type 'FSM'.")
364 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
365 raise ex
367 self._parent = parent
368 self._name = name
369 self._value = value
370 self._bin = coverBin
371 coverBin._parent = self
373 if parent is not None: 373 ↛ exitline 373 didn't return from function '__init__' because the condition on line 373 was always true
374 parent._states.append(self)
376 @classmethod
377 def Parse(cls, element: _Element, *, parent: Nullable[FSM] = None) -> Self:
378 """
379 Parse a state and its bin from its ``<state>`` element.
381 :param element: The ``<state>`` element.
382 :param parent: Optional, the finite state machine the state belongs to. Default: ``None``.
383 :returns: The state.
384 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
385 """
386 return cls(
387 Bin.Parse(element.find("{*}stateBin")),
388 element.attrib.get("stateName"),
389 element.attrib.get("stateValue"),
390 cls._ParseUserAttributes(element),
391 parent=parent
392 )
394 @readonly
395 def Parent(self) -> Nullable[FSM]:
396 """
397 Read-only property to access the finite state machine the state belongs to (:attr:`_parent`).
399 :returns: The finite state machine; ``None``, if the state belongs to none.
400 """
401 return self._parent
403 @readonly
404 def Name(self) -> Nullable[str]:
405 """
406 Read-only property to access the name of the state (:attr:`_name`).
408 :returns: The name, e.g. ``IDLE``; ``None``, if the report states none.
409 """
410 return self._name
412 @readonly
413 def Value(self) -> Nullable[str]:
414 """
415 Read-only property to access the value representing the state (:attr:`_value`).
417 :returns: The value, as stated; ``None``, if the report states none.
418 """
419 return self._value
421 @readonly
422 def Bin(self) -> Bin:
423 """
424 Read-only property to access the state's bin (:attr:`_bin`).
426 :returns: The bin, stating how often the machine was in the state.
427 """
428 return self._bin
431@export
432class FSMTransition(Base):
433 """
434 A ``<stateTransition>`` of a finite state machine: the states it passes, and its bin - how often it happened.
435 """
437 _parent: Nullable[FSM] #: The finite state machine the transition belongs to.
438 _states: list[str] #: The states the transition passes, from the first one.
439 _bin: _Bin #: How often the transition happened.
441 def __init__(
442 self,
443 states: Iterable[str],
444 coverBin: Bin,
445 userAttributes: Nullable[Iterable[UserAttribute]] = None,
446 *,
447 parent: Nullable[FSM] = None
448 ) -> None:
449 """
450 Initialize the transition, become its bin's parent, and append it to the transitions of its finite state machine.
452 :param states: The states the transition passes, from the first one, e.g. ``["IDLE", "ACTIVE"]``.
453 :param coverBin: How often the transition happened.
454 :param userAttributes: Optional, the user-defined attributes. Default: ``None``.
455 :param parent: Optional, the finite state machine the transition belongs to. Default: ``None``.
456 :raises TypeError: If parameter ``userAttributes`` isn't iterable.
457 :raises TypeError: If parameter ``userAttributes`` contains an element not of type
458 :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Elements.UserAttribute`.
459 :raises ValueError: If parameter ``coverBin`` is ``None``.
460 :raises TypeError: If parameter ``coverBin`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.UCIS.Bins.Bin`.
461 :raises TypeError: If parameter ``parent`` isn't of type :class:`FSM`.
462 :raises ValueError: If parameter ``states`` is ``None``.
463 :raises TypeError: If parameter ``states`` isn't iterable.
464 :raises TypeError: If parameter ``states`` contains an element not of type :class:`str`.
465 :raises ValueError: If parameter ``states`` has less than two states.
466 """
467 super().__init__(userAttributes)
469 if coverBin is None: 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true
470 raise ValueError("Parameter 'coverBin' is None.")
471 elif not isinstance(coverBin, Bin): 471 ↛ 472line 471 didn't jump to line 472 because the condition on line 471 was never true
472 ex = TypeError("Parameter 'coverBin' is not of type 'Bin'.")
473 ex.add_note(f"Got type '{getFullyQualifiedName(coverBin)}'.")
474 raise ex
476 if parent is not None and not isinstance(parent, FSM): 476 ↛ 477line 476 didn't jump to line 477 because the condition on line 476 was never true
477 ex = TypeError("Parameter 'parent' is not of type 'FSM'.")
478 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
479 raise ex
481 self._parent = parent
482 self._states = []
483 self._bin = coverBin
484 coverBin._parent = self
486 if states is None: 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true
487 raise ValueError("Parameter 'states' is None.")
488 elif not isinstance(states, Iterable): 488 ↛ 489line 488 didn't jump to line 489 because the condition on line 488 was never true
489 ex = TypeError("Parameter 'states' is not iterable.")
490 ex.add_note(f"Got type '{getFullyQualifiedName(states)}'.")
491 raise ex
493 for state in states:
494 if not isinstance(state, str): 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true
495 ex = TypeError("Parameter 'states' contains an element not of type 'str'.")
496 ex.add_note(f"Got type '{getFullyQualifiedName(state)}'.")
497 raise ex
499 self._states.append(state)
501 if len(self._states) < 2:
502 ex = ValueError("Parameter 'states' has less than two states.")
503 ex.add_note(f"Got {len(self._states)} state(s).")
504 raise ex
506 if parent is not None: 506 ↛ exitline 506 didn't return from function '__init__' because the condition on line 506 was always true
507 parent._transitions.append(self)
509 @classmethod
510 def Parse(cls, element: _Element, *, parent: Nullable[FSM] = None) -> Self:
511 """
512 Parse a transition and its bin from its ``<stateTransition>`` element.
514 :param element: The ``<stateTransition>`` element.
515 :param parent: Optional, the finite state machine the transition belongs to. Default: ``None``.
516 :returns: The transition.
517 :raises CodeCoverageError: If a user-defined attribute's value isn't of the type the attribute states.
518 """
519 return cls(
520 [stateElement.text or "" for stateElement in element.iterfind("{*}state")],
521 Bin.Parse(element.find("{*}transitionBin")),
522 cls._ParseUserAttributes(element),
523 parent=parent
524 )
526 @readonly
527 def Parent(self) -> Nullable[FSM]:
528 """
529 Read-only property to access the finite state machine the transition belongs to (:attr:`_parent`).
531 :returns: The finite state machine; ``None``, if the transition belongs to none.
532 """
533 return self._parent
535 @readonly
536 def States(self) -> list[str]:
537 """
538 Read-only property to access the states the transition passes (:attr:`_states`).
540 :returns: The states, from the first one, as the report states them.
541 """
542 return self._states
544 @readonly
545 def Bin(self) -> Bin:
546 """
547 Read-only property to access the transition's bin (:attr:`_bin`).
549 :returns: The bin, stating how often the transition happened.
550 """
551 return self._bin