diff --git a/.release b/.release index eb100b2b0e6722fd4bc3289a31bd3903af80d890..02f219ab46a84e7ed5f50ed35b5e6536a431fc21 100644 --- a/.release +++ b/.release @@ -1,2 +1,2 @@ -release=0.6.2 -tag=lmcbaseclasses-0.6.2 +release=0.6.3 +tag=lmcbaseclasses-0.6.3 diff --git a/README.md b/README.md index a95d8a3fedda66df4e51a57283cfbb2fccf534b8..55520e446b963fa62a4fc956ae363662ac7cfb0b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ The lmc-base-classe repository contains set of eight classes as mentioned in SKA ## Version History +#### 0.6.3 +- Fix omission of fatal_error transition from base device state machine. + #### 0.6.2 - Fix issue with incorrect updates to transitions dict from inherited devices. Only noticeable if running multiple devices of different types in the same diff --git a/docs/source/SKABaseDevice.rst b/docs/source/SKABaseDevice.rst index 9d41032725d5449356f3100f4035213ed5a81c07..4e6bcb44ad1ec4d5feb0cb2a8ff2353eb78d963b 100644 --- a/docs/source/SKABaseDevice.rst +++ b/docs/source/SKABaseDevice.rst @@ -6,6 +6,14 @@ SKA BaseDevice ============================================ +The SKABaseDevice implements the basic device state machine, as illustrated +below, but without, at present, a Standby state. + +.. image:: images/device_state_diagram.png + :width: 400 + :alt: Diagram of the device state machine showing states and transitions + + .. toctree:: :maxdepth: 2 diff --git a/docs/source/images/device_state_diagram.png b/docs/source/images/device_state_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..839caca46bad985fb1d6b59e5e6728e2d1ca5a35 Binary files /dev/null and b/docs/source/images/device_state_diagram.png differ diff --git a/src/ska/base/base_device.py b/src/ska/base/base_device.py index e44dddfd72f12e236c3fd52b2f1a77b031fed9f5..30852c8c0c30dff6bf93101c09c0eb58d1dd70a7 100644 --- a/src/ska/base/base_device.py +++ b/src/ska/base/base_device.py @@ -477,7 +477,7 @@ class SKABaseDeviceStateModel(DeviceStateModel): lambda self: self._set_dev_state(DevState.ON) ), ('OFF', 'on_failed'): ( - "FAULT", + "FAULT (ENABLED)", lambda self: self._set_dev_state(DevState.FAULT) ), ('ON', 'off_succeeded'): ( @@ -485,7 +485,11 @@ class SKABaseDeviceStateModel(DeviceStateModel): lambda self: self._set_dev_state(DevState.OFF) ), ('ON', 'off_failed'): ( - "FAULT", + "FAULT (ENABLED)", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('ON', 'fatal_error'): ( + "FAULT (ENABLED)", lambda self: self._set_dev_state(DevState.FAULT) ), diff --git a/src/ska/base/release.py b/src/ska/base/release.py index c5877ac7d47e1f83bcea9acb75bfcefda3047c4a..1afbbc52106869070e5e007bea9f57606139e4eb 100644 --- a/src/ska/base/release.py +++ b/src/ska/base/release.py @@ -7,7 +7,7 @@ """Release information for lmc-base-classes Python Package""" name = """lmcbaseclasses""" -version = "0.6.2" +version = "0.6.3" version_info = version.split(".") description = """A set of generic base devices for SKA Telescope.""" author = "SKA India and SARAO and CSIRO" diff --git a/tests/conftest.py b/tests/conftest.py index ebf675836329536662629c16660fdd35f3a4a292..081bfde7aa24b99c8ba7d5d5827d75552ed536fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,8 +8,6 @@ from queue import Empty, Queue from tango import EventType from tango.test_context import DeviceTestContext -from ska.base import SKASubarrayStateModel - @pytest.fixture(scope="class") def tango_context(request): @@ -67,14 +65,6 @@ def initialize_device(tango_context): yield tango_context.device.Init() -@pytest.fixture(scope="function") -def state_model(): - """ - Yields an SKASubarrayStateModel. - """ - yield SKASubarrayStateModel() - - @pytest.fixture(scope="function") def tango_change_event_helper(tango_context): """ diff --git a/tests/test_base_device.py b/tests/test_base_device.py index b5c47cfe73c5000d7520bfd6860de8e39e44f002..dfe59795963417f50af18cfea014b32fffdd837d 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -8,6 +8,7 @@ ######################################################################################### """Contain the tests for the SKABASE.""" +import itertools import re import pytest @@ -18,6 +19,7 @@ import tango from unittest import mock from tango import DevFailed, DevState +from ska.base import SKABaseDeviceStateModel from ska.base.control_model import ( AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode ) @@ -28,6 +30,8 @@ from ska.base.base_device import ( LoggingTargetError, TangoLoggingServiceHandler, ) +from ska.base.faults import StateModelError + # PROTECTED REGION END # // SKABaseDevice.test_additional_imports # Device test case # PROTECTED REGION ID(SKABaseDevice.test_SKABaseDevice_decorators) ENABLED START # @@ -583,3 +587,145 @@ class TestSKABaseDevice(object): # PROTECTED REGION ID(SKABaseDevice.test_testMode) ENABLED START # assert tango_context.device.testMode == TestMode.NONE # PROTECTED REGION END # // SKABaseDevice.test_testMode + + +@pytest.fixture +def state_model(): + yield SKABaseDeviceStateModel() + + +class TestSKABaseDeviceStateModel(): + """ + Test cases for SKABaseDeviceStateModel. + """ + + @pytest.mark.parametrize( + 'state_under_test, action_under_test', + itertools.product( + ["UNINITIALISED", "INIT_ENABLED", "INIT_DISABLED", "FAULT_ENABLED", + "FAULT_DISABLED", "DISABLED", "OFF", "ON"], + ["init_started", "init_succeeded", "init_failed", "fatal_error", + "reset_succeeded", "reset_failed", "to_notfitted", + "to_offline", "to_online", "to_maintenance", "on_succeeded", + "on_failed", "off_succeeded", "off_failed"] + ) + ) + def test_state_machine( + self, state_model, state_under_test, action_under_test + ): + """ + Test the subarray state machine: for a given initial state and + an action, does execution of that action, from that initial + state, yield the expected results? If the action was not allowed + from that initial state, does the device raise a DevFailed + exception? If the action was allowed, does it result in the + correct state transition? + + :todo: support starting in different memorised adminModes + """ + states = { + "UNINITIALISED": + (None, None), + "FAULT_ENABLED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.FAULT), + "FAULT_DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.FAULT), + "INIT_ENABLED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.INIT), + "INIT_DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.INIT), + "DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.DISABLE), + "OFF": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.OFF), + "ON": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON), + } + + def assert_state(state): + (admin_modes, state) = states[state] + if admin_modes is not None: + assert state_model.admin_mode in admin_modes + if state is not None: + assert state_model.dev_state == state + + transitions = { + ('UNINITIALISED', 'init_started'): "INIT_ENABLED", + ('INIT_ENABLED', 'to_notfitted'): "INIT_DISABLED", + ('INIT_ENABLED', 'to_offline'): "INIT_DISABLED", + ('INIT_ENABLED', 'to_online'): "INIT_ENABLED", + ('INIT_ENABLED', 'to_maintenance'): "INIT_ENABLED", + ('INIT_ENABLED', 'init_succeeded'): 'OFF', + ('INIT_ENABLED', 'init_failed'): 'FAULT_ENABLED', + ('INIT_ENABLED', 'fatal_error'): "FAULT_ENABLED", + ('INIT_DISABLED', 'to_notfitted'): "INIT_DISABLED", + ('INIT_DISABLED', 'to_offline'): "INIT_DISABLED", + ('INIT_DISABLED', 'to_online'): "INIT_ENABLED", + ('INIT_DISABLED', 'to_maintenance'): "INIT_ENABLED", + ('INIT_DISABLED', 'init_succeeded'): 'DISABLED', + ('INIT_DISABLED', 'init_failed'): 'FAULT_DISABLED', + ('INIT_DISABLED', 'fatal_error'): "FAULT_DISABLED", + ('FAULT_DISABLED', 'to_notfitted'): "FAULT_DISABLED", + ('FAULT_DISABLED', 'to_offline'): "FAULT_DISABLED", + ('FAULT_DISABLED', 'to_online'): "FAULT_ENABLED", + ('FAULT_DISABLED', 'to_maintenance'): "FAULT_ENABLED", + ('FAULT_DISABLED', 'reset_succeeded'): "DISABLED", + ('FAULT_DISABLED', 'reset_failed'): "FAULT_DISABLED", + ('FAULT_DISABLED', 'fatal_error'): "FAULT_DISABLED", + ('FAULT_ENABLED', 'to_notfitted'): "FAULT_DISABLED", + ('FAULT_ENABLED', 'to_offline'): "FAULT_DISABLED", + ('FAULT_ENABLED', 'to_online'): "FAULT_ENABLED", + ('FAULT_ENABLED', 'to_maintenance'): "FAULT_ENABLED", + ('FAULT_ENABLED', 'reset_succeeded'): "OFF", + ('FAULT_ENABLED', 'reset_failed'): "FAULT_ENABLED", + ('FAULT_ENABLED', 'fatal_error'): "FAULT_ENABLED", + ('DISABLED', 'to_notfitted'): "DISABLED", + ('DISABLED', 'to_offline'): "DISABLED", + ('DISABLED', 'to_online'): "OFF", + ('DISABLED', 'to_maintenance'): "OFF", + ('DISABLED', 'fatal_error'): "FAULT_DISABLED", + ('OFF', 'to_notfitted'): "DISABLED", + ('OFF', 'to_offline'): "DISABLED", + ('OFF', 'to_online'): "OFF", + ('OFF', 'to_maintenance'): "OFF", + ('OFF', 'on_succeeded'): "ON", + ('OFF', 'on_failed'): "FAULT_ENABLED", + ('OFF', 'fatal_error'): "FAULT_ENABLED", + ('ON', 'off_succeeded'): "OFF", + ('ON', 'off_failed'): "FAULT_ENABLED", + ('ON', 'fatal_error'): "FAULT_ENABLED", + } + + setups = { + "UNINITIALISED": [], + "INIT_ENABLED": ['init_started'], + "INIT_DISABLED": ['init_started', 'to_offline'], + "FAULT_ENABLED": ['init_started', 'init_failed'], + "FAULT_DISABLED": ['init_started', 'to_offline', 'init_failed'], + "OFF": ['init_started', 'init_succeeded'], + "DISABLED": ['init_started', 'init_succeeded', 'to_offline'], + "ON": ['init_started', 'init_succeeded', 'on_succeeded'], + } + + # state = "UNINITIALISED" # for test debugging only + # assert_state(state) # for test debugging only + + # Put the device into the state under test + for action in setups[state_under_test]: + state_model.perform_action(action) + # state = transitions[state, action] # for test debugging only + # assert_state(state) # for test debugging only + + # Check that we are in the state under test + assert_state(state_under_test) + + # Test that the action under test does what we expect it to + if (state_under_test, action_under_test) in transitions: + # Action should succeed + state_model.perform_action(action_under_test) + assert_state(transitions[(state_under_test, action_under_test)]) + else: + # Action should fail and the state should not change + with pytest.raises(StateModelError): + state_model.perform_action(action_under_test) + assert_state(state_under_test) diff --git a/tests/test_subarray_device.py b/tests/test_subarray_device.py index 8abc50cdfcdc62e63c7d0b673dc31ea24cf2e804..ae4a8cade5bc4f254d7ef459aa40ab95d93c1e4b 100644 --- a/tests/test_subarray_device.py +++ b/tests/test_subarray_device.py @@ -15,7 +15,7 @@ import pytest from tango import DevState, DevFailed # PROTECTED REGION ID(SKASubarray.test_additional_imports) ENABLED START # -from ska.base import SKASubarray, SKASubarrayResourceManager +from ska.base import SKASubarray, SKASubarrayResourceManager, SKASubarrayStateModel from ska.base.commands import ResultCode from ska.base.control_model import ( AdminMode, ControlMode, HealthState, ObsMode, ObsState, SimulationMode, TestMode @@ -579,6 +579,11 @@ def resource_manager(): yield SKASubarrayResourceManager() +@pytest.fixture +def state_model(): + yield SKASubarrayStateModel() + + class TestSKASubarrayResourceManager: def test_ResourceManager_assign(self, resource_manager): # create a resource manager and check that it is empty diff --git a/tests/test_subarray_state_model.py b/tests/test_subarray_state_model.py index 8c8d2f16ccc4e0fd7833ec380510968afbc0a44b..804b8cf98f39e2252297856b24ed29f645395248 100644 --- a/tests/test_subarray_state_model.py +++ b/tests/test_subarray_state_model.py @@ -13,11 +13,17 @@ import pytest from tango import DevState +from ska.base import SKASubarrayStateModel from ska.base.control_model import AdminMode, ObsState from ska.base.faults import StateModelError -class TestSKASubarrayStateModel(): +@pytest.fixture +def state_model(): + yield SKASubarrayStateModel() + + +class TestSKASubarrayStateModel: """ Test cases for SKASubarrayStateModel. """ @@ -43,8 +49,9 @@ class TestSKASubarrayStateModel(): "restart_failed"] ) ) - def test_state_machine(self, state_model, - state_under_test, action_under_test): + def test_state_machine( + self, state_model, state_under_test, action_under_test + ): """ Test the subarray state machine: for a given initial state and an action, does execution of that action, from that initial