From ecf3a5f9cdd4d634958fa839fdc7cff26c810cf3 Mon Sep 17 00:00:00 2001 From: Drew Devereux <drew.devereux@csiro.au> Date: Tue, 23 Jun 2020 06:28:58 +0000 Subject: [PATCH] Improve error handling messages For the SKABaseDevice, the logger may have failed to initialise so we just print an error to stdout. We don't want to raise another exception in `init_device`. Log exceptions on command failure to ensure we don't miss these. The `!s` formatting for enums gives the enum name rather than the integer value. The name is easier to interpret. --- .pylintrc | 2 +- .release | 4 +- README.md | 30 + docs/source/Commands.rst | 11 + docs/source/SKASubarray.rst | 7 + docs/source/images/ADR-8.png | Bin 0 -> 99300 bytes docs/source/index.rst | 1 + pogo/SKAAlarmHandler.xmi | 2 +- pogo/SKABaseDevice.xmi | 2 +- pogo/SKACapability.xmi | 4 +- pogo/SKALogger.xmi | 6 +- pogo/SKAMaster.xmi | 2 +- pogo/SKAObsDevice.xmi | 8 +- pogo/SKASubarray.xmi | 68 +- pogo/SKATelState.xmi | 2 +- src/ska/base/__init__.py | 17 +- src/ska/base/alarm_handler_device.py | 153 ++- src/ska/base/base_device.py | 536 ++++++++- src/ska/base/capability_device.py | 90 +- src/ska/base/commands.py | 336 ++++++ src/ska/base/control_model.py | 249 +++-- src/ska/base/faults.py | 16 + src/ska/base/logger_device.py | 122 ++- src/ska/base/master_device.py | 151 ++- src/ska/base/obs_device.py | 113 +- src/ska/base/release.py | 2 +- src/ska/base/subarray_device.py | 1520 +++++++++++++++++++++----- src/ska/base/tel_state_device.py | 22 +- src/ska/base/utils.py | 5 +- tests/conftest.py | 12 +- tests/test_alarm_handler_device.py | 6 +- tests/test_base_device.py | 23 +- tests/test_capability_device.py | 7 +- tests/test_logger_device.py | 23 +- tests/test_master_device.py | 33 +- tests/test_obs_device.py | 25 +- tests/test_subarray_device.py | 434 ++++++-- tests/test_subarray_state_model.py | 273 +++++ tests/test_tel_state_device.py | 23 +- tests/test_utils.py | 1 + 40 files changed, 3523 insertions(+), 818 deletions(-) create mode 100644 docs/source/Commands.rst create mode 100644 docs/source/images/ADR-8.png create mode 100644 src/ska/base/commands.py create mode 100644 tests/test_subarray_state_model.py diff --git a/.pylintrc b/.pylintrc index b525115d..ddbbd9ec 100644 --- a/.pylintrc +++ b/.pylintrc @@ -258,7 +258,7 @@ contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members= +generated-members=target|logger # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). diff --git a/.release b/.release index 159363b5..05e9522e 100644 --- a/.release +++ b/.release @@ -1,2 +1,2 @@ -release=0.5.4 -tag=lmcbaseclasses-0.5.4 +release=0.6.0 +tag=lmcbaseclasses-0.6.0 diff --git a/README.md b/README.md index cd8bc131..aab1c108 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,36 @@ The lmc-base-classe repository contains set of eight classes as mentioned in SKA ## Version History +#### 0.6.0 +- Breaking change: State management + - SKABaseDevice implements a simple state machine with states + `DISABLED`, `OFF`, `ON`, `INIT` and `FAULT`, along with transitions + between them. + - SKASubarray implements full subarray state machine in accordance + with ADR-8 (the underlying state model supports all states and + transitions, including transitions through transient states; the + subarray device uses this state model but currently provide a + simple, purely synchronous implementation) + - Base classes provide subclassing code hooks that separate management + of device state from other device functionality. Thus, subclasses + are encouraged to leave state management in the care of the base + classes by: + - leaving `init_device()` alone and placing their (stateless) + initialisation code in the `do()` method of the `InitCommand` object instead. The base `init_device()` implementation will ensure that + the `do()` method is called, whilst ensuring state is managed e.g. + the device is put into state `IDLE` beforehand, and put into the + right state afterwards. + - leaving commands like `Configure()` alone and placing their + (stateless) implementation code in `ConfigureCommand.do()` + instead. This applies to all commands that affect device state: + `Off()`, `On()`, `AssignResources()`, `ReleaseResources()`, + `ReleaseAllResources()`, `Configure()`, `Scan()`, `EndScan()`, + `End()`, `Abort()`, `Reset()`, `Restart()`. + - leaving the base device to handle reads from and writes to the + state attributes `adminMode`, `obsState` and device `state`. For + example, do not call `Device.set_state()` directly; and do not + override methods like `write_adminMode()`. + #### 0.5.4 - Remove `ObsState` command from SKACapability, SKAObsDevice and SKASubarray Pogo XMI files. It should not have been included - the `obsState` attribute provides this information. The command was not in the Python diff --git a/docs/source/Commands.rst b/docs/source/Commands.rst new file mode 100644 index 00000000..63c5d710 --- /dev/null +++ b/docs/source/Commands.rst @@ -0,0 +1,11 @@ + +SKA Commands +============================================ + +.. toctree:: + :maxdepth: 2 + +.. automodule:: ska.base.commands + :members: + :undoc-members: + diff --git a/docs/source/SKASubarray.rst b/docs/source/SKASubarray.rst index 4db79893..93f32045 100644 --- a/docs/source/SKASubarray.rst +++ b/docs/source/SKASubarray.rst @@ -6,6 +6,13 @@ SKA Subarray ============================================ +The SKA Subarray device implements the observation state machine as defined +in ADR-8: + +.. image:: images/ADR-8.png + :width: 400 + :alt: Diagram of the observation state machine showing states and transitions + .. toctree:: :maxdepth: 2 diff --git a/docs/source/images/ADR-8.png b/docs/source/images/ADR-8.png new file mode 100644 index 0000000000000000000000000000000000000000..620ef1226e29f34cb596eace4f6e4e0bcd5b2fa1 GIT binary patch literal 99300 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ĽYs+AZc>oL2GisCe%6*z&Dy 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 R85kHCJYD@<);T3K0RS*Ea-9GG literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst index 68e1c1ff..adec6359 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,6 +21,7 @@ Welcome to LMC Base Classes documentation! Device: SKA Subarray<SKASubarray> SKA Control Model<Control_Model> + SKA Commands<Commands> Indices and tables ================== diff --git a/pogo/SKAAlarmHandler.xmi b/pogo/SKAAlarmHandler.xmi index dd03cbdc..a6813c57 100644 --- a/pogo/SKAAlarmHandler.xmi +++ b/pogo/SKAAlarmHandler.xmi @@ -100,7 +100,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true" concreteHere="false"/> </commands> diff --git a/pogo/SKABaseDevice.xmi b/pogo/SKABaseDevice.xmi index ef623530..a04e519c 100644 --- a/pogo/SKABaseDevice.xmi +++ b/pogo/SKABaseDevice.xmi @@ -55,7 +55,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> diff --git a/pogo/SKACapability.xmi b/pogo/SKACapability.xmi index 22dcbeae..7ffc2979 100644 --- a/pogo/SKACapability.xmi +++ b/pogo/SKACapability.xmi @@ -68,7 +68,7 @@ <type xsi:type="pogoDsl:UShortType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -77,7 +77,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true"/> </commands> diff --git a/pogo/SKALogger.xmi b/pogo/SKALogger.xmi index d7c3d9d0..f5b5b10f 100644 --- a/pogo/SKALogger.xmi +++ b/pogo/SKALogger.xmi @@ -46,8 +46,8 @@ <argin description="Logging level for selected devices:
(0=OFF, 1=FATAL, 2=ERROR, 3=WARNING, 4=INFO, 5=DEBUG).
Example: [[4, 5], ['my/dev/1', 'my/dev/2']]."> <type xsi:type="pogoDsl:LongStringArrayType"/> </argin> - <argout description="No output arguments"> - <type xsi:type="pogoDsl:VoidType"/> + <argout description=""> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -65,7 +65,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true"/> </commands> diff --git a/pogo/SKAMaster.xmi b/pogo/SKAMaster.xmi index 4849959f..6b37e806 100644 --- a/pogo/SKAMaster.xmi +++ b/pogo/SKAMaster.xmi @@ -78,7 +78,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true"/> </commands> diff --git a/pogo/SKAObsDevice.xmi b/pogo/SKAObsDevice.xmi index a583296b..86660007 100644 --- a/pogo/SKAObsDevice.xmi +++ b/pogo/SKAObsDevice.xmi @@ -56,7 +56,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true"/> </commands> @@ -65,13 +65,17 @@ <dataReadyEvent fire="false" libCheckCriteria="true"/> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> <properties description="Observing State" label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/> + <enumLabels>EMPTY</enumLabels> + <enumLabels>RESOURCING</enumLabels> <enumLabels>IDLE</enumLabels> <enumLabels>CONFIGURING</enumLabels> <enumLabels>READY</enumLabels> <enumLabels>SCANNING</enumLabels> - <enumLabels>PAUSED</enumLabels> + <enumLabels>ABORTING</enumLabels> <enumLabels>ABORTED</enumLabels> + <enumLabels>RESETTING</enumLabels> <enumLabels>FAULT</enumLabels> + <enumLabels>RESTARTING</enumLabels> </attributes> <attributes name="obsMode" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="1000" maxX="" maxY="" allocReadMember="true" isDynamic="false"> <dataType xsi:type="pogoDsl:EnumType"/> diff --git a/pogo/SKASubarray.xmi b/pogo/SKASubarray.xmi index fee6b291..36221277 100644 --- a/pogo/SKASubarray.xmi +++ b/pogo/SKASubarray.xmi @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="ASCII"?> <pogoDsl:PogoSystem xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pogoDsl="http://www.esrf.fr/tango/pogo/PogoDsl"> <classes name="SKASubarray" pogoRevision="9.6"> - <description description="SubArray handling device" title="SKASubarray" sourcePath="/home/tango/src/lmc-base-classes/pogo" language="PythonHL" filestogenerate="XMI file,Code files,Python Package,Protected Regions" license="none" copyright="" hasMandatoryProperty="false" hasConcreteProperty="true" hasAbstractCommand="false" hasAbstractAttribute="false"> + <description description="SubArray handling device" title="SKASubarray" sourcePath="/home/tango/src/lmc-base-classes/pogo" language="PythonHL" filestogenerate="XMI file" license="none" copyright="" hasMandatoryProperty="false" hasConcreteProperty="true" hasAbstractCommand="false" hasAbstractAttribute="false"> <inheritances classname="Device_Impl" sourcePath=""/> <inheritances classname="SKAObsDevice" sourcePath="./"/> <identification contact="at ska.ac.za - cam" author="cam" emailDomain="ska.ac.za" classFamily="SkaBase" siteSpecific="" platform="All Platforms" bus="Not Applicable" manufacturer="none" reference=""/> @@ -37,34 +37,16 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> - </argout> - <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> - </commands> - <commands name="ConfigureCapability" description="Configures number of instances for each capability. If the capability exists, 
it increments the configured instances by the number of instances requested, 
otherwise an exception will be raised.
Note: The two lists arguments must be of equal length or an exception will be raised." execMethod="configure_capability" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> - <argin description="[Number of instances to add][Capability types]"> <type xsi:type="pogoDsl:LongStringArrayType"/> - </argin> - <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> - <commands name="DeconfigureAllCapabilities" description="Deconfigure all instances of the given Capability type. If the capability type does not exist an exception will be raised, 
otherwise it sets the configured instances for that capability type to zero." execMethod="deconfigure_all_capabilities" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> - <argin description="Capability type"> + <commands name="Configure" description="Configures the scan for this subarray" execMethod="configure" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> + <argin description="The scan configuration, formatted as a JSON string"> <type xsi:type="pogoDsl:StringType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> - </argout> - <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> - </commands> - <commands name="DeconfigureCapability" description="Deconfigures a given number of instances for each capability. If the capability exists, 
it decrements the configured instances by the number of instances requested,
otherwise an exceptioin will be raised.
Note: The two lists arguments must be of equal length or an exception will be raised" execMethod="deconfigure_capability" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> - <argin description="[Number of instances to remove][Capability types]"> <type xsi:type="pogoDsl:LongStringArrayType"/> - </argin> - <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -99,35 +81,26 @@ <argin description="List of Resources to add to subarray."> <type xsi:type="pogoDsl:StringArrayType"/> </argin> - <argout description="A list of Resources added to the subarray."> - <type xsi:type="pogoDsl:StringArrayType"/> - </argout> - <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> - </commands> - <commands name="EndSB" description="Change obsState to IDLE." execMethod="end_sb" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> - <argin description=""> - <type xsi:type="pogoDsl:VoidType"/> - </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> - <commands name="EndScan" description="Terminate scan." execMethod="end_scan" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> + <commands name="End" description="Change obsState to IDLE." execMethod="end" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> <argin description=""> <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> - <commands name="Pause" description="Pause scan." execMethod="pause" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> + <commands name="EndScan" description="Terminate scan." execMethod="end_scan" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> <argin description=""> <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -135,8 +108,8 @@ <argin description=""> <type xsi:type="pogoDsl:VoidType"/> </argin> - <argout description="List of resources removed from the subarray."> - <type xsi:type="pogoDsl:StringArrayType"/> + <argout description=""> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -144,8 +117,8 @@ <argin description="List of resources to remove from the subarray."> <type xsi:type="pogoDsl:StringArrayType"/> </argin> - <argout description="List of resources removed from the subarray."> - <type xsi:type="pogoDsl:StringArrayType"/> + <argout description=""> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -154,16 +127,25 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true" concreteHere="false"/> </commands> - <commands name="Resume" description="Resume scan." execMethod="resume" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false"> + <commands name="ObsReset" description="Reset observation state machine to its default state" execMethod="obsreset" displayLevel="OPERATOR" polledPeriod="0"> <argin description=""> <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> + <type xsi:type="pogoDsl:LongStringArrayType"/> + </argout> + <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> + </commands> + <commands name="Restart" description="Restart the observation state machine" execMethod="restart" displayLevel="OPERATOR" polledPeriod="0"> + <argin description=""> <type xsi:type="pogoDsl:VoidType"/> + </argin> + <argout description=""> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -172,7 +154,7 @@ <type xsi:type="pogoDsl:StringArrayType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="false" concrete="true" concreteHere="true"/> </commands> @@ -293,7 +275,7 @@ <states name="DISABLE" description="The device cannot be switched ON for an external reason. E.g. the power supply has its door open, the safety conditions are not satisfactory to allow the device to operate."> <status abstract="false" inherited="true" concrete="true"/> </states> - <preferences docHome="./doc_html" makefileHome="/usr/local/share/pogo/preferences"/> + <preferences docHome="./doc_html" makefileHome="$(TANGO_HOME)"/> <overlodedPollPeriodObject name="adminMode" type="attribute" pollPeriod="0"/> <overlodedPollPeriodObject name="configurationDelayExpected" type="attribute" pollPeriod="0"/> <overlodedPollPeriodObject name="configurationProgress" type="attribute" pollPeriod="0"/> diff --git a/pogo/SKATelState.xmi b/pogo/SKATelState.xmi index 90b25401..0aebc36f 100644 --- a/pogo/SKATelState.xmi +++ b/pogo/SKATelState.xmi @@ -60,7 +60,7 @@ <type xsi:type="pogoDsl:VoidType"/> </argin> <argout description=""> - <type xsi:type="pogoDsl:VoidType"/> + <type xsi:type="pogoDsl:LongStringArrayType"/> </argout> <status abstract="false" inherited="true" concrete="true"/> </commands> diff --git a/src/ska/base/__init__.py b/src/ska/base/__init__.py index 4478f8e6..cebd3438 100644 --- a/src/ska/base/__init__.py +++ b/src/ska/base/__init__.py @@ -1,25 +1,28 @@ __all__ = ( + "commands", "control_model", "SKAAlarmHandler", - "SKABaseDevice", + "SKABaseDevice", "SKABaseDeviceStateModel", "SKACapability", "SKALogger", "SKAMaster", - "SKAObsDevice", - "SKASubarray", - "SKATelState" + "SKAObsDevice", "SKAObsDeviceStateModel", + "SKASubarray", "SKASubarrayStateModel", "SKASubarrayResourceManager", + "SKATelState", ) # Note: order of imports is important - start with lowest in the hierarchy # SKABaseDevice, and then classes that inherit from it -from .base_device import SKABaseDevice +from .base_device import SKABaseDevice, SKABaseDeviceStateModel from .alarm_handler_device import SKAAlarmHandler from .logger_device import SKALogger from .master_device import SKAMaster from .tel_state_device import SKATelState # SKAObsDevice, and then classes that inherit from it -from .obs_device import SKAObsDevice +from .obs_device import SKAObsDevice, SKAObsDeviceStateModel from .capability_device import SKACapability -from .subarray_device import SKASubarray +from .subarray_device import ( + SKASubarray, SKASubarrayStateModel, SKASubarrayResourceManager +) diff --git a/src/ska/base/alarm_handler_device.py b/src/ska/base/alarm_handler_device.py index 2a05b700..848c4793 100644 --- a/src/ska/base/alarm_handler_device.py +++ b/src/ska/base/alarm_handler_device.py @@ -4,23 +4,21 @@ # # # -""" SKAAlarmHandler - -A generic base device for Alarms for SKA. It exposes SKA alarms and SKA alerts as TANGO attributes. -SKA Alarms and SKA/Element Alerts are rules-based configurable conditions that can be defined over multiple -attribute values and quality factors, and are separate from the "built-in" TANGO attribute alarms. +""" +This module implements SKAAlarmHandler, a generic base device for Alarms +for SKA. It exposes SKA alarms and SKA alerts as TANGO attributes. SKA +Alarms and SKA/Element Alerts are rules-based configurable conditions +that can be defined over multiple attribute values and quality factors, +and are separate from the "built-in" TANGO attribute alarms. """ # PROTECTED REGION ID(SKAAlarmHandler.additionnal_import) ENABLED START # -# Standard imports -import os -import sys - # Tango imports from tango import DebugIt from tango.server import run, attribute, command, device_property # SKA specific imports -from . import SKABaseDevice, release +from ska.base import SKABaseDevice +from ska.base.commands import BaseCommand # PROTECTED REGION END # // SKAAlarmHandler.additionnal_import @@ -91,13 +89,31 @@ class SKAAlarmHandler(SKABaseDevice): # General methods # --------------- - def init_device(self): - SKABaseDevice.init_device(self) - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # PROTECTED REGION ID(SKAAlarmHandler.init_device) ENABLED START # - # PROTECTED REGION END # // SKAAlarmHandler.init_device + def init_command_objects(self): + """ + Sets up the command objects + """ + super().init_command_objects() + self.register_command_object( + "GetAlarmRule", + self.GetAlarmRuleCommand(self, self.state_model, self.logger) + ) + self.register_command_object( + "GetAlarmData", + self.GetAlarmDataCommand(self, self.state_model, self.logger) + ) + self.register_command_object( + "GetAlarmAdditionalInfo", + self.GetAlarmAdditionalInfoCommand(self, self.state_model, self.logger) + ) + self.register_command_object( + "GetAlarmStats", + self.GetAlarmStatsCommand(self, self.state_model, self.logger) + ) + self.register_command_object( + "GetAlertStats", + self.GetAlertStatsCommand(self, self.state_model, self.logger) + ) def always_executed_hook(self): # PROTECTED REGION ID(SKAAlarmHandler.always_executed_hook) ENABLED START # @@ -180,16 +196,88 @@ class SKAAlarmHandler(SKABaseDevice): # Commands # -------- + class GetAlarmRuleCommand(BaseCommand): + """ + A class for the SKAAlarmHandler's GetAlarmRule() command. + """ + def do(self, argin): + """ + Stateless hook for SKAAlarmHandler GetAlarmRule() command. + + :return: Alarm configuration info: rule, actions, etc. + :rtype: JSON string + """ + return "" + + class GetAlarmDataCommand(BaseCommand): + """ + A class for the SKAAlarmHandler's GetAlarmData() command. + """ + def do(self, argin): + """ + Stateless hook for SKAAlarmHandler GetAlarmData() command. + + :return: Alarm data + :rtype: JSON string + """ + return "" + + class GetAlarmAdditionalInfoCommand(BaseCommand): + """ + A class for the SKAAlarmHandler's GetAlarmAdditionalInfo() + command. + """ + def do(self, argin): + """ + Stateless hook for SKAAlarmHandler GetAlarmAdditionalInfo() + command. + + :return: Alarm additional info + :rtype: JSON string + """ + return "" + + class GetAlarmStatsCommand(BaseCommand): + """ + A class for the SKAAlarmHandler's GetAlarmStats() command. + """ + def do(self): + """ + Stateless hook for SKAAlarmHandler GetAlarmStats() command. + + :return: Alarm stats + :rtype: JSON string + """ + return "" + + class GetAlertStatsCommand(BaseCommand): + """ + A class for the SKAAlarmHandler's GetAlertStats() command. + """ + def do(self): + """ + Stateless hook for SKAAlarmHandler GetAlertStats() command. + + :return: Alert stats + :rtype: JSON string + """ + return "" + @command(dtype_in='str', doc_in="Alarm name", dtype_out='str', doc_out="JSON string",) @DebugIt() def GetAlarmRule(self, argin): # PROTECTED REGION ID(SKAAlarmHandler.GetAlarmRule) ENABLED START # """ Get all configuration info of the alarm, e.g. rule, defined action, etc. + + To modify behaviour for this command, modify the do() method of + the command class. + :param argin: Name of the alarm :return: JSON string containing configuration information of the alarm """ - return "" + command = self.get_command_object("GetAlarmRule") + return command(argin) # PROTECTED REGION END # // SKAAlarmHandler.GetAlarmRule @command(dtype_in='str', doc_in="Alarm name", dtype_out='str', doc_out="JSON string",) @@ -199,10 +287,15 @@ class SKAAlarmHandler(SKABaseDevice): """ Get list of current value, quality factor and status of all attributes participating in the alarm rule. + + To modify behaviour for this command, modify the do() method of + the command class. + :param argin: Name of the alarm :return: JSON string containing alarm data """ - return "" + command = self.get_command_object("GetAlarmData") + return command(argin) # PROTECTED REGION END # // SKAAlarmHandler.GetAlarmData @command(dtype_in='str', doc_in="Alarm name", dtype_out='str', doc_out="JSON string", ) @@ -211,10 +304,15 @@ class SKAAlarmHandler(SKABaseDevice): # PROTECTED REGION ID(SKAAlarmHandler.GetAlarmAdditionalInfo) ENABLED START # """ Get additional alarm information. + + To modify behaviour for this command, modify the do() method of + the command class. + :param argin: Name of the alarm :return: JSON string containing additional alarm information """ - return "" + command = self.get_command_object("GetAlarmAdditionalInfo") + return command(argin) # PROTECTED REGION END # // SKAAlarmHandler.GetAlarmAdditionalInfo @command(dtype_out='str', doc_out="JSON string",) @@ -223,9 +321,14 @@ class SKAAlarmHandler(SKABaseDevice): # PROTECTED REGION ID(SKAAlarmHandler.GetAlarmStats) ENABLED START # """ Get current alarm stats. + + To modify behaviour for this command, modify the do() method of + the command class. + :return: JSON string containing alarm statistics """ - return "" + command = self.get_command_object("GetAlarmStats") + return command() # PROTECTED REGION END # // SKAAlarmHandler.GetAlarmStats @command(dtype_out='str', doc_out="JSON string",) @@ -234,9 +337,14 @@ class SKAAlarmHandler(SKABaseDevice): # PROTECTED REGION ID(SKAAlarmHandler.GetAlertStats) ENABLED START # """ Get current alert stats. + + To modify behaviour for this command, modify the do() method of + the command class. + :return: JSON string containing alert statistics """ - return "" + command = self.get_command_object("GetAlertStats") + return command() # PROTECTED REGION END # // SKAAlarmHandler.GetAlertStats # ---------- @@ -255,5 +363,6 @@ def main(args=None, **kwargs): return run((SKAAlarmHandler,), args=args, **kwargs) # PROTECTED REGION END # // SKAAlarmHandler.main + if __name__ == '__main__': main() diff --git a/src/ska/base/base_device.py b/src/ska/base/base_device.py index effa1936..63f9c63b 100644 --- a/src/ska/base/base_device.py +++ b/src/ska/base/base_device.py @@ -5,16 +5,16 @@ # # -"""A generic base device for SKA. It exposes the generic attributes, -properties and commands of an SKA device. +""" +This module implements a generic base model and device for SKA. It +exposes the generic attributes, properties and commands of an SKA +device. """ # PROTECTED REGION ID(SKABaseDevice.additionnal_import) ENABLED START # # Standard imports import enum -import json import logging import logging.handlers -import os import socket import sys import threading @@ -24,26 +24,24 @@ from urllib.parse import urlparse from urllib.request import url2pathname # Tango imports -import tango -from tango import DebugIt +from tango import AttrWriteType, DebugIt, DevState from tango.server import run, Device, attribute, command, device_property -from tango import AttrQuality, AttrWriteType -from tango import DeviceProxy, DevFailed # SKA specific imports import ska.logging as ska_logging -from . import release -from .control_model import ( - AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode +from ska.base import release +from ska.base.commands import ( + ActionCommand, BaseCommand, ResultCode +) +from ska.base.control_model import ( + AdminMode, ControlMode, SimulationMode, TestMode, HealthState, + LoggingLevel, DeviceStateModel ) -from .utils import (get_dp_command, - coerce_value, - get_groups_from_json, - get_tango_device_type_id) -from .faults import (GroupDefinitionsError, - LoggingTargetError, - LoggingLevelError) +from ska.base.utils import get_groups_from_json +from ska.base.faults import (GroupDefinitionsError, + LoggingTargetError, + LoggingLevelError) LOG_FILE_SIZE = 1024 * 1024 # Log file size 1MB. @@ -309,25 +307,314 @@ class LoggingUtils: # PROTECTED REGION END # // SKABaseDevice.additionnal_import -__all__ = ["SKABaseDevice", "main"] +__all__ = ["SKABaseDevice", "SKABaseDeviceStateModel", "main"] + + +class SKABaseDeviceStateModel(DeviceStateModel): + """ + Implements the state model for the SKABaseDevice + """ + + __transitions = { + ('UNINITIALISED', 'init_started'): ( + "INIT (ENABLED)", + lambda self: ( + self._set_admin_mode(AdminMode.MAINTENANCE), + self._set_dev_state(DevState.INIT), + ) + ), + ('INIT (ENABLED)', 'init_succeeded'): ( + 'OFF', + lambda self: self._set_dev_state(DevState.OFF) + ), + ('INIT (ENABLED)', 'init_failed'): ( + 'FAULT (ENABLED)', + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('INIT (ENABLED)', 'fatal_error'): ( + "FAULT (ENABLED)", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('INIT (ENABLED)', 'to_notfitted'): ( + "INIT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.NOT_FITTED) + ), + ('INIT (ENABLED)', 'to_offline'): ( + "INIT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.OFFLINE) + ), + ('INIT (ENABLED)', 'to_maintenance'): ( + "INIT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.MAINTENANCE) + ), + ('INIT (ENABLED)', 'to_online'): ( + "INIT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.ONLINE) + ), + ('INIT (DISABLED)', 'init_succeeded'): ( + 'DISABLED', + lambda self: self._set_dev_state(DevState.DISABLE) + ), + ('INIT (DISABLED)', 'init_failed'): ( + 'FAULT (DISABLED)', + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('INIT (DISABLED)', 'fatal_error'): ( + "FAULT (DISABLED)", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('INIT (DISABLED)', 'to_notfitted'): ( + "INIT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.NOT_FITTED) + ), + ('INIT (DISABLED)', 'to_offline'): ( + "INIT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.OFFLINE) + ), + ('INIT (DISABLED)', 'to_maintenance'): ( + "INIT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.MAINTENANCE) + ), + ('INIT (DISABLED)', 'to_online'): ( + "INIT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.ONLINE) + ), + ('FAULT (DISABLED)', 'reset_succeeded'): ( + "DISABLED", + lambda self: self._set_dev_state(DevState.DISABLE) + ), + ('FAULT (DISABLED)', 'reset_failed'): ("FAULT (DISABLED)", None), + ('FAULT (DISABLED)', 'fatal_error'): ("FAULT (DISABLED)", None), + ('FAULT (DISABLED)', 'to_notfitted'): ( + "FAULT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.NOT_FITTED) + ), + ('FAULT (DISABLED)', 'to_offline'): ( + "FAULT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.OFFLINE) + ), + ('FAULT (DISABLED)', 'to_maintenance'): ( + "FAULT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.MAINTENANCE) + ), + ('FAULT (DISABLED)', 'to_online'): ( + "FAULT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.ONLINE) + ), + ('FAULT (ENABLED)', 'reset_succeeded'): ( + "OFF", + lambda self: self._set_dev_state(DevState.OFF) + ), + ('FAULT (ENABLED)', 'reset_failed'): ("FAULT (ENABLED)", None), + ('FAULT (ENABLED)', 'fatal_error'): ("FAULT (ENABLED)", None), + ('FAULT (ENABLED)', 'to_notfitted'): ( + "FAULT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.NOT_FITTED)), + ('FAULT (ENABLED)', 'to_offline'): ( + "FAULT (DISABLED)", + lambda self: self._set_admin_mode(AdminMode.OFFLINE)), + ('FAULT (ENABLED)', 'to_maintenance'): ( + "FAULT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.MAINTENANCE) + ), + ('FAULT (ENABLED)', 'to_online'): ( + "FAULT (ENABLED)", + lambda self: self._set_admin_mode(AdminMode.ONLINE) + ), + ('DISABLED', 'to_offline'): ( + "DISABLED", + lambda self: self._set_admin_mode(AdminMode.OFFLINE) + ), + ('DISABLED', 'to_online'): ( + "OFF", + lambda self: ( + self._set_admin_mode(AdminMode.ONLINE), + self._set_dev_state(DevState.OFF) + ) + ), + ('DISABLED', 'to_maintenance'): ( + "OFF", + lambda self: ( + self._set_admin_mode(AdminMode.MAINTENANCE), + self._set_dev_state(DevState.OFF) + ) + ), + ('DISABLED', 'to_notfitted'): ( + "DISABLED", + lambda self: self._set_admin_mode(AdminMode.NOT_FITTED) + ), + ('DISABLED', 'fatal_error'): ( + "FAULT (DISABLED)", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('OFF', 'to_notfitted'): ( + "DISABLED", + lambda self: ( + self._set_admin_mode(AdminMode.NOT_FITTED), + self._set_dev_state(DevState.DISABLE) + ) + ), + ('OFF', 'to_offline'): ( + "DISABLED", lambda self: ( + self._set_admin_mode(AdminMode.OFFLINE), + self._set_dev_state(DevState.DISABLE) + ) + ), + ('OFF', 'to_online'): ( + "OFF", + lambda self: self._set_admin_mode(AdminMode.ONLINE) + ), + ('OFF', 'to_maintenance'): ( + "OFF", + lambda self: self._set_admin_mode(AdminMode.MAINTENANCE) + ), + ('OFF', 'fatal_error'): ( + "FAULT (ENABLED)", + lambda self: self._set_dev_state(DevState.FAULT) + ), + } + + def __init__(self, dev_state_callback=None): + """ + Initialises the state model. + + :param dev_state_callback: A callback to be called when a + transition implies a change to device state + :type dev_state_callback: tango.DevState + """ + super().__init__(self.__transitions, "UNINITIALISED") + + self._admin_mode = None + self._dev_state = None + self._dev_state_callback = dev_state_callback + + @property + def admin_mode(self): + """ + Returns the admin_mode + + :returns: admin_mode of this state model + :rtype: AdminMode + """ + return self._admin_mode + + def _set_admin_mode(self, admin_mode): + """ + Helper method: calls the admin_mode callback if one exists + + :param admin_mode: the new admin_mode value + :type admin_mode: AdminMode + """ + self._admin_mode = admin_mode + + @property + def dev_state(self): + """ + Returns the dev_state + + :returns: dev_state of this state model + :rtype: tango.DevState + """ + return self._dev_state + + def _set_dev_state(self, dev_state): + """ + Helper method: sets this state models dev_state, and calls the + dev_state callback if one exists + + :param dev_state: the new state value + :type admin_mode: DevState + """ + if self._dev_state != dev_state: + self._dev_state = dev_state + if self._dev_state_callback is not None: + self._dev_state_callback(self._dev_state) class SKABaseDevice(Device): """ A generic base device for SKA. """ - # PROTECTED REGION ID(SKABaseDevice.class_variable) ENABLED START # + class InitCommand(ActionCommand): + """ + A class for the SKABaseDevice's init_device() "command". + """ + def __init__(self, target, state_model, logger=None): + """ + Create a new InitCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "init", start_action=True, logger=logger + ) + + def do(self): + """ + Stateless hook for device initialisation. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + + device._health_state = HealthState.OK + device._control_mode = ControlMode.REMOTE + device._simulation_mode = SimulationMode.FALSE + device._test_mode = TestMode.NONE + + device._build_state = '{}, {}, {}'.format(release.name, + release.version, + release.description) + device._version_id = release.version + + try: + # create TANGO Groups dict, according to property + self.logger.debug( + "Groups definitions: {}".format( + device.GroupDefinitions + ) + ) + device.groups = get_groups_from_json( + device.GroupDefinitions + ) + self.logger.info( + "Groups loaded: {}".format( + sorted(device.groups.keys()) + ) + ) + except GroupDefinitionsError: + self.logger.debug( + "No Groups loaded for device: {}".format( + device.get_name() + ) + ) + + message = "SKABaseDevice Init command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) _logging_config_lock = threading.Lock() _logging_configured = False def _init_logging(self): """ - This method initializes the logging mechanism, based on default properties. - - :param: None. - - :return: None. + This method initializes the logging mechanism, based on default + properties. """ class EnsureTagsFilter(logging.Filter): @@ -481,36 +768,73 @@ class SKABaseDevice(Device): # General methods # --------------- + def _update_state(self, state): + """ + Helper method for changing state; passed to the state model as a + callback + + :param state: the new state value + :type state: DevState + """ + if state != self.get_state(): + self.logger.info( + f"Device state changed from {self.get_state()} to {state}" + ) + self.set_state(state) + self.set_status(f"The device is in {state} state.") + def init_device(self): """ - Method that initializes the tango device after startup. + Initializes the tango device after startup. + + Subclasses that have no need to override the default + default implementation of state management may leave + ``init_device()`` alone. Override the ``do()`` method + on the nested class ``InitCommand`` instead. + :return: None """ - Device.init_device(self) - # PROTECTED REGION ID(SKABaseDevice.init_device) ENABLED START # + try: + super().init_device() - self._init_logging() + self._init_logging() + self._init_state_model() - # Initialize attribute values. - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - self._health_state = HealthState.OK - self._admin_mode = AdminMode.ONLINE - self._control_mode = ControlMode.REMOTE - self._simulation_mode = SimulationMode.FALSE - self._test_mode = TestMode.NONE + self._command_objects = {} - try: - # create TANGO Groups objects dict, according to property - self.logger.debug("Groups definitions: {}".format(self.GroupDefinitions)) - self.groups = get_groups_from_json(self.GroupDefinitions) - self.logger.info("Groups loaded: {}".format(sorted(self.groups.keys()))) - except GroupDefinitionsError: - self.logger.info("No Groups loaded for device: {}".format(self.get_name())) + self.InitCommand(self, self.state_model, self.logger)() + + self.init_command_objects() + except Exception as exc: + self.set_state(DevState.FAULT) + self.set_status("The device is in FAULT state - init_device failed.") + if hasattr(self, "logger"): + self.logger.exception("init_device() failed.") + else: + print(f"ERROR: init_device failed, and no logger: {exc}.") - self.logger.info("Completed SKABaseDevice.init_device") - # PROTECTED REGION END # // SKABaseDevice.init_device + def _init_state_model(self): + """ + Creates the state model for the device + """ + self.state_model = SKABaseDeviceStateModel( + dev_state_callback=self._update_state + ) + + def register_command_object(self, command_name, command_object): + self._command_objects[command_name] = command_object + + def get_command_object(self, command_name): + return self._command_objects[command_name] + + def init_command_objects(self): + self.register_command_object( + "Reset", self.ResetCommand(self, self.state_model, self.logger) + ) + self.register_command_object( + "GetVersionInfo", + self.GetVersionInfoCommand(self, self.state_model, self.logger) + ) def always_executed_hook(self): # PROTECTED REGION ID(SKABaseDevice.always_executed_hook) ENABLED START # @@ -583,8 +907,11 @@ class SKABaseDevice(Device): self._logging_level = lmc_logging_level self.logger.setLevel(_LMC_TO_PYTHON_LOGGING_LEVEL[lmc_logging_level]) - self.logger.tango_logger.set_level(_LMC_TO_TANGO_LOGGING_LEVEL[lmc_logging_level]) - self.logger.info('Logging level set to %s on Python and Tango loggers', lmc_logging_level) + self.logger.tango_logger.set_level( + _LMC_TO_TANGO_LOGGING_LEVEL[lmc_logging_level] + ) + self.logger.info('Logging level set to %s on Python and Tango loggers', + lmc_logging_level) # PROTECTED REGION END # // SKABaseDevice.loggingLevel_write def read_loggingTargets(self): @@ -613,7 +940,8 @@ class SKABaseDevice(Device): :return: None. """ device_name = self.get_name() - valid_targets = LoggingUtils.sanitise_logging_targets(value, device_name) + valid_targets = LoggingUtils.sanitise_logging_targets(value, + device_name) LoggingUtils.update_logging_handlers(valid_targets, self.logger) # PROTECTED REGION END # // SKABaseDevice.loggingTargets_write @@ -633,8 +961,9 @@ class SKABaseDevice(Device): Reads Admin Mode of the device. :return: Admin Mode of the device + :rtype: AdminMode """ - return self._admin_mode + return self.state_model.admin_mode # PROTECTED REGION END # // SKABaseDevice.adminMode_read def write_adminMode(self, value): @@ -643,10 +972,20 @@ class SKABaseDevice(Device): Sets Admin Mode of the device. :param value: Admin Mode of the device. + :type value: AdminMode :return: None """ - self._admin_mode = value + if value == AdminMode.NOT_FITTED: + self.state_model.perform_action("to_notfitted") + elif value == AdminMode.OFFLINE: + self.state_model.perform_action("to_offline") + elif value == AdminMode.MAINTENANCE: + self.state_model.perform_action("to_maintenance") + elif value == AdminMode.ONLINE: + self.state_model.perform_action("to_online") + else: + raise ValueError(f"Unknown adminMode {value}") # PROTECTED REGION END # // SKABaseDevice.adminMode_write def read_controlMode(self): @@ -719,6 +1058,22 @@ class SKABaseDevice(Device): # Commands # -------- + class GetVersionInfoCommand(BaseCommand): + """ + A class for the SKABaseDevice's Reset() command. + """ + def do(self): + """ + Stateless hook for device GetVersionInfo() command. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + return [f"{device.__class__.__name__}, {device.read_buildState()}"] + @command(dtype_out=('str',), doc_out="Version strings",) @DebugIt() def GetVersionInfo(self): @@ -726,22 +1081,88 @@ class SKABaseDevice(Device): """ Returns the version information of the device. + To modify behaviour for this command, modify the do() method of + the command class. + :return: Version details of the device. """ - return ['{}, {}'.format(self.__class__.__name__, self.read_buildState())] + command = self.get_command_object("GetVersionInfo") + return command() # PROTECTED REGION END # // SKABaseDevice.GetVersionInfo + class ResetCommand(ActionCommand): + """ + A class for the SKABaseDevice's Reset() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Create a new ResetCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "reset", logger=logger) + + def do(self): + """ + Stateless hook for device reset. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + device._health_state = HealthState.OK + device._control_mode = ControlMode.REMOTE + device._simulation_mode = SimulationMode.FALSE + device._test_mode = TestMode.NONE + + message = "Reset command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + def is_Reset_allowed(self): + """ + Whether the ``Reset()`` command is allowed to be run in the + current state + + :returns: whether the ``Reset()`` command is allowed to be run in the + current state + :rtype: boolean + """ + command = self.get_command_object("Reset") + return command.is_allowed() + @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() def Reset(self): - # PROTECTED REGION ID(SKABaseDevice.Reset) ENABLED START # """ - Reset device to its default state. + Reset the device from the FAULT state. + + To modify behaviour for this command, modify the do() method of + the command class. :return: None """ - # PROTECTED REGION END # // SKABaseDevice.Reset + command = self.get_command_object("Reset") + (return_code, message) = command() + return [[return_code], [message]] + # ---------- # Run server @@ -754,10 +1175,7 @@ def main(args=None, **kwargs): Main function of the SKABaseDevice module. :param args: None - :param kwargs: - - :return: """ return run((SKABaseDevice,), args=args, **kwargs) # PROTECTED REGION END # // SKABaseDevice.main diff --git a/src/ska/base/capability_device.py b/src/ska/base/capability_device.py index 930905aa..dedbb54f 100644 --- a/src/ska/base/capability_device.py +++ b/src/ska/base/capability_device.py @@ -9,16 +9,13 @@ Capability handling device """ # PROTECTED REGION ID(SKACapability.additionnal_import) ENABLED START # -# Standard import -import os -import sys - # Tango imports from tango import DebugIt from tango.server import run, attribute, command, device_property # SKA specific imports -from . import SKAObsDevice, release +from ska.base import SKAObsDevice +from ska.base.commands import ResponseCommand, ResultCode # PROTECTED REGION END # // SKACapability.additionnal_imports __all__ = ["SKACapability", "main"] @@ -28,6 +25,38 @@ class SKACapability(SKAObsDevice): """ A Subarray handling device. It exposes the instances of configured capabilities. """ + def init_command_objects(self): + """ + Sets up the command objects + """ + super().init_command_objects() + self.register_command_object( + "ConfigureInstances", self.ConfigureInstancesCommand( + self, self.state_model, self.logger + ) + ) + + class InitCommand(SKAObsDevice.InitCommand): + def do(self): + """ + Stateless hook for device initialisation. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + super().do() + + device = self.target + device._activation_time = 0.0 + device._configured_instances = 0 + device._used_components = [""] + + message = "SKACapability Init command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + # PROTECTED REGION ID(SKACapability.class_variable) ENABLED START # # PROTECTED REGION END # // SKACapability.class_variable @@ -74,17 +103,6 @@ class SKACapability(SKAObsDevice): # General methods # --------------- - def init_device(self): - SKAObsDevice.init_device(self) - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # PROTECTED REGION ID(SKACapability.init_device) ENABLED START # - self._activation_time = 0.0 - self._configured_instances = 0 - self._used_components = [""] - # PROTECTED REGION END # // SKACapability.init_device - def always_executed_hook(self): # PROTECTED REGION ID(SKACapability.always_executed_hook) ENABLED START # pass @@ -126,34 +144,66 @@ class SKACapability(SKAObsDevice): return self._used_components # PROTECTED REGION END # // SKACapability.usedComponents_read - # -------- # Commands # -------- - @command(dtype_in='uint16', doc_in="The number of instances to configure for this Capability.",) + class ConfigureInstancesCommand(ResponseCommand): + """ + A class for the SKALoggerDevice's SetLoggingLevel() command. + """ + def do(self, argin): + """ + Stateless hook for ConfigureInstances()) command + functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + device._configured_instances = argin + + message = "ConfigureInstances command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + @command( + dtype_in='uint16', + doc_in="The number of instances to configure for this Capability.", + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() def ConfigureInstances(self, argin): # PROTECTED REGION ID(SKACapability.ConfigureInstances) ENABLED START # """ This function indicates how many number of instances of the current capacity should to be configured. + + To modify behaviour for this command, modify the do() method of + the command class. + :param argin: Number of instances to configure :return: None. """ - self._configured_instances = argin + command = self.get_command_object("ConfigureInstances") + (return_code, message) = command(argin) + return [[return_code], [message]] # PROTECTED REGION END # // SKACapability.ConfigureInstances + # ---------- # Run server # ---------- - def main(args=None, **kwargs): # PROTECTED REGION ID(SKACapability.main) ENABLED START # """Main function of the SKACapability module.""" return run((SKACapability,), args=args, **kwargs) # PROTECTED REGION END # // SKACapability.main + if __name__ == '__main__': main() diff --git a/src/ska/base/commands.py b/src/ska/base/commands.py new file mode 100644 index 00000000..1d6fc20d --- /dev/null +++ b/src/ska/base/commands.py @@ -0,0 +1,336 @@ +""" +This module provides abstract base classes for device commands, and a +ResultCode enum. +""" +import enum +import logging +from ska.base.faults import CommandError, ResultCodeError, StateModelError + +module_logger = logging.getLogger(__name__) + + +class ResultCode(enum.IntEnum): + """ + Python enumerated type for command return codes. + """ + + OK = 0 + """ + The command was executed successfully. + """ + + STARTED = 1 + """ + The command has been accepted and will start immediately. + """ + + QUEUED = 2 + """ + The command has been accepted and will be executed at a future time + """ + + FAILED = 3 + """ + The command could not be executed. + """ + + UNKNOWN = 4 + """ + The status of the command is not known. + """ + + +class BaseCommand: + """ + Abstract base class for Tango device server commands. Ensures the + command is run, and that if the command errors, the "fatal_error" + action will be called on the state model. + """ + + def __init__(self, target, state_model, logger=None): + """ + Creates a new BaseCommand object for a device. + + :param state_model: the state model that this command uses, for + example to raise a fatal error if the command errors out. + :type state_model: SKABaseClassStateModel or a subclass of same + :param target: the object that this base command acts upon. For + example, the device that this BaseCommand implements the + command for. + :type target: object + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + self.name = self.__class__.__name__ + self.target = target + self.state_model = state_model + self.logger = logger or module_logger + + def __call__(self, argin=None): + """ + What to do when the command is called. This base class simply + calls ``do()`` or ``do(argin)``, depending on whether the + ``argin`` argument is provided. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + try: + return self._call_do(argin) + except Exception: + self.logger.exception( + f"Error executing command {self.name} with argin '{argin}'" + ) + self.fatal_error() + raise + + def _call_do(self, argin=None): + """ + Helper method that ensures the ``do`` method is called with the + right arguments, and that the call is logged. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + if argin is None: + returned = self.do() + else: + returned = self.do(argin=argin) + + self.logger.info( + f"Exiting command {self.name}" + ) + return returned + + def do(self, argin=None): + """ + Hook for the functionality that the command implements. This + class provides stub functionality; subclasses should subclass + this method with their command functionality. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + raise NotImplementedError( + "BaseCommand is abstract; do() must be subclassed not called." + ) + + def fatal_error(self): + """ + Callback for a fatal error in the command, such as an unhandled + exception. + """ + self._perform_action("fatal_error") + + def _is_action_allowed(self, action): + """ + Helper method; whether a given action is permitted in the + current state of the state model. + + :param action: the action on the state model that is being + scrutinised + :type action: string + :returns: whether the action is allowed + :rtype: boolean + """ + return self.state_model.is_action_allowed(action) + + def _try_action(self, action): + """ + Helper method; "tries" an action on the state model. + + :param action: the action to perform on the state model + :type action: string + :raises: CommandError if the action is not allowed in current state + :returns: True is the action is allowed + """ + try: + return self.state_model.try_action(action) + except StateModelError as exc: + raise CommandError( + f"Error executing command {self.name}") from exc + + def _perform_action(self, action): + """ + Helper method; performs an action on the state model, thus + driving state + + :param action: the action to perform on the state model + :type action: string + """ + self.state_model.perform_action(action) + + +class ResponseCommand(BaseCommand): + def __call__(self, argin=None): + """ + What to do when the command is called. This base class simply + calls ``do()`` or ``do(argin)``, depending on whether the + ``argin`` argument is provided. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + try: + (return_code, message) = self._call_do(argin) + except Exception: + self.logger.exception( + f"Error executing command {self.name} with argin '{argin}'" + ) + self.fatal_error() + raise + return (return_code, message) + + def _call_do(self, argin=None): + """ + Helper method that ensures the ``do`` method is called with the + right arguments, and that the call is logged. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + if argin is None: + (return_code, message) = self.do() + else: + (return_code, message) = self.do(argin=argin) + + self.logger.info( + f"Exiting command {self.name} with return_code {return_code!s}, " + f"message: '{message}'" + ) + return (return_code, message) + + +class ActionCommand(ResponseCommand): + """ + Abstract base class for a tango command, which checks a state model + to find out whether the command is allowed to be run, and after + running, sends an action to that state model, thus driving device + state. + """ + def __init__( + self, target, state_model, action_hook, start_action=False, logger=None + ): + """ + Create a new ActionCommand for a device. + + :param target: the object that this base command acts upon. For + example, the device that this ActionCommand implements the + command for. + :type target: object + :param action_hook: a hook for the command, used to build + actions that will be sent to the state model; for example, + if the hook is "scan", then success of the command will + result in action "scan_succeeded" being sent to the state + model. + :type action_hook: string + :param start_action: whether the state model supports a start + action (i.e. to put the state model into an transient state + while the command is running); default False + :type start_action: boolean + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, logger=logger) + self._succeeded_hook = f"{action_hook}_succeeded" + self._failed_hook = f"{action_hook}_failed" + + self._started_hook = None + if start_action: + self._started_hook = f"{action_hook}_started" + + def __call__(self, argin=None): + """ + What to do when the command is called. This is implemented to + check that the command is allowed to run, then run the command, + then send an action to the state model advising whether the + command succeeded or failed. + + :param argin: the argument passed to the Tango command, if + present + :type argin: ANY + """ + self.check_allowed() + try: + self.started() + (return_code, message) = self._call_do(argin) + self._returned(return_code) + except Exception: + self.logger.exception( + f"Error executing command {self.name} with argin '{argin}'" + ) + self.fatal_error() + raise + return (return_code, message) + + def _returned(self, return_code): + """ + Helper method that handles the return of the ``do()`` method. + If the return code is OK or FAILED, then it performs an + appropriate action on the state model. Otherwise it raises an + error. + + :param return_code: The return_code returned by the ``do()`` + method + :type return_code: ResultCode + """ + if return_code == ResultCode.OK: + self.succeeded() + elif return_code == ResultCode.FAILED: + self.failed() + else: + if self._started_hook is None: + raise ResultCodeError( + f"ActionCommands that do not have a started action may" + f"only return with code OK or FAILED, not {return_code!s}." + ) + + def check_allowed(self): + """ + Checks whether the command is allowed to be run in the current + state of the state model. + + :returns: True if the command is allowed to be run + :raises StateModelError: if the command is not allowed to be run + """ + return self._try_action(self._started_hook or self._succeeded_hook) + + def is_allowed(self): + """ + Whether this command is allowed to run in the current state of + the state model. + + :returns: whether this command is allowed to run + :rtype: boolean + """ + return self._is_action_allowed( + self._started_hook or self._succeeded_hook + ) + + def started(self): + """ + Action to perform upon starting the comand. + """ + if self._started_hook is not None: + self._perform_action(self._started_hook) + + def succeeded(self): + """ + Callback for the successful completion of the command. + """ + self._perform_action(self._succeeded_hook) + + def failed(self): + """ + Callback for the failed completion of the command. + """ + self._perform_action(self._failed_hook) diff --git a/src/ska/base/control_model.py b/src/ska/base/control_model.py index 36482485..52328e8a 100644 --- a/src/ska/base/control_model.py +++ b/src/ska/base/control_model.py @@ -4,6 +4,9 @@ Module for SKA Control Model (SCM) related code. For further details see the SKA1 CONTROL SYSTEM GUIDELINES (CS_GUIDELINES MAIN VOLUME) Document number: 000-000000-010 GDL +And architectural updates: +https://jira.skatelescope.org/browse/ADR-8 +https://confluence.skatelescope.org/pages/viewpage.action?pageId=105416556 The enumerated types mapping to the states and modes are included here, as well as other useful enumerations. @@ -11,6 +14,7 @@ other useful enumerations. """ import enum +from ska.base.faults import StateModelError # --------------------------------- # Core SKA Control Model attributes @@ -40,9 +44,9 @@ class HealthState(enum.IntEnum): that belongs to a subarray is unresponsive, or may report healthState as ``FAILED``. Difference between ``DEGRADED`` and ``FAILED`` health shall be clearly identified - (quantified) and documented. For example, the difference between ``DEGRADED`` and ``FAILED`` - subarray can be defined as the number or percent of the dishes available, the number or - percent of the baselines available, sensitivity, or some other criterion. More than one + (quantified) and documented. For example, the difference between ``DEGRADED`` and ``FAILED`` + subarray can be defined as the number or percent of the dishes available, the number or + percent of the baselines available, sensitivity, or some other criterion. More than one criteria may be defined for a TANGO Device. """ @@ -71,8 +75,8 @@ class AdminMode(enum.IntEnum): """ OFFLINE = 1 - """SKA operations declared that the entity is not used for observing or other function it - provides. A subset of the monitor and control functionality may be supported in this mode. + """SKA operations declared that the entity is not used for observing or other function it + provides. A subset of the monitor and control functionality may be supported in this mode. ``adminMode=OFFLINE`` is also used to indicate unused Subarrays and unused Capabilities. TANGO devices report ``state=DISABLED`` when ``adminMode=OFFLINE``. """ @@ -80,13 +84,13 @@ class AdminMode(enum.IntEnum): MAINTENANCE = 2 """ SKA operations declared that the entity is reserved for maintenance and cannot - be part of scientific observations, but can be used for observing in a ‘Maintenance Subarray’. - + be part of scientific observations, but can be used for observing in a ‘Maintenance Subarray’. + ``MAINTENANCE`` mode has different meaning for different entities, depending on the context and functionality. Some entities may implement different behaviour when in ``MAINTENANCE`` mode. - - For each TANGO Device, the difference in behaviour and functionality in ``MAINTENANCE`` mode + + For each TANGO Device, the difference in behaviour and functionality in ``MAINTENANCE`` mode shall be documented. ``MAINTENANCE`` is the factory default for ``adminMode``. Transition out of ``adminMode=NOT_FITTED`` is always via ``MAINTENANCE``; an engineer/operator has to verify that the entity is operational as expected before it is set to ``ONLINE`` @@ -95,28 +99,28 @@ class AdminMode(enum.IntEnum): NOT_FITTED = 3 """ - SKA operations declared the entity as ``NOT_FITTED`` (and therefore cannot be used for - observing or other function it provides). TM shall not send commands or queries to the + SKA operations declared the entity as ``NOT_FITTED`` (and therefore cannot be used for + observing or other function it provides). TM shall not send commands or queries to the Element (entity) while in this mode. - - TANGO devices shall report ``state=DISABLE`` when ``adminMode=NOT_FITTED``; higher level - entities (Element, Sub-element, component, Subarray and/or Capability) which ‘use’ - ``NOT_FITTED`` equipment shall report operational ``state`` as ``DISABLE``. If only a subset - of higher-level functionality is affected, overall ``state`` of the higher-level entity that - uses ``NOT_FITTED`` equipment may be reported as ``ON``, but with ``healthState=DEGRADED``. - Additional queries may be necessary to identify which functionality and capabilities are - available. - - Higher-level entities shall intelligently exclude ``NOT_FITTED`` items from ``healthState`` and - Element Alerts/Telescope Alarms; e.g. if a receiver band in DSH is ``NOT_FITTED`` and there - is no communication to that receiver band, then DSH shall not raise Element Alerts for that - entity and it should not report ``healthState=FAILED`` because of an entity that is + + TANGO devices shall report ``state=DISABLE`` when ``adminMode=NOT_FITTED``; higher level + entities (Element, Sub-element, component, Subarray and/or Capability) which ‘use’ + ``NOT_FITTED`` equipment shall report operational ``state`` as ``DISABLE``. If only a subset + of higher-level functionality is affected, overall ``state`` of the higher-level entity that + uses ``NOT_FITTED`` equipment may be reported as ``ON``, but with ``healthState=DEGRADED``. + Additional queries may be necessary to identify which functionality and capabilities are + available. + + Higher-level entities shall intelligently exclude ``NOT_FITTED`` items from ``healthState`` and + Element Alerts/Telescope Alarms; e.g. if a receiver band in DSH is ``NOT_FITTED`` and there + is no communication to that receiver band, then DSH shall not raise Element Alerts for that + entity and it should not report ``healthState=FAILED`` because of an entity that is ``NOT_FITTED``. """ RESERVED = 4 - """This mode is used to identify additional equipment that is ready to take over when the - operational equipment fails. This equipment does not take part in the operations at this + """This mode is used to identify additional equipment that is ready to take over when the + operational equipment fails. This equipment does not take part in the operations at this point in time. TANGO devices report ``state=DISABLED`` when ``adminMode=RESERVED``. """ @@ -124,64 +128,79 @@ class AdminMode(enum.IntEnum): class ObsState(enum.IntEnum): """Python enumerated type for ``obsState`` attribute - the observing state.""" - IDLE = 0 + EMPTY = 0 + """ + The sub-array is ready to observe, but is in an undefined + configuration and has no resources allocated. """ - Subarray, resource, Capability is not used for observing, it does not produce output - products. The exact implementation is [TBD4] for each Element/Sub-element that - implements subarrays. + + RESOURCING = 1 + """ + The system is allocating resources to, or deallocating resources + from, the subarray. This may be a complete de/allocation, or it may + be incremental. In both cases it is a transient state and will + automatically transition to IDLE when complete. For some subsystems + this may be a very brief state if resourcing is a quick activity. """ - CONFIGURING = 1 + IDLE = 2 """ - Subarray is being prepared for a specific scan. On entry to the state no assumptions - can be made about the previous conditions. This is a transient state. Subarray/Capability - are supposed to automatically transitions to ``obsState=READY`` when configuration is - successfully completed. If an error is encountered, TANGO Device may: - - * report failure and abort the configuration, waiting for additional input; - * proceed with reconfiguration, transition to ``obsState=READY`` and set - ``healthState=DEGRADED`` (if possible notify the originator of the request that - configuration is not 100% successful); - * if serious failure is encountered, transition to ``obsState=FAULT``, - ``healthState=FAILED``. + The subarray has resources allocated and is ready to be used for + observing. In normal science operations these will be the resources + required for the upcoming SBI execution. """ - READY = 2 + CONFIGURING = 3 """ - Subarray is fully prepared for the next scan, but not actually taking data or moving - in the observed coordinate system (i.e. it may be tracking, but not moving relative - to the coordinate system). + The subarray is being configured ready to scan. On entry to the + state no assumptions can be made about the previous conditions. It + is a transient state and will automatically transition to READY when + it completes normally. """ - SCANNING = 3 + READY = 4 """ - Subarray is taking data and, if needed, all components are synchronously moving in the - observed coordinate system. All the M&C flows to the sub-systems are happening - automatically (e.g. DISHes are receiving pointing updates, CSP is receiving updates for - delay tracking). + The subarray is fully prepared to scan, but is not actually taking + data or moving in the observed coordinate system (it may be + tracking, but not moving relative to the coordinate system). """ - PAUSED = 4 + SCANNING = 5 """ - [TBC11 by SKAO SW Architects] Subarray is fully prepared for the next observation, but - not actually taking data or moving in the observed system. Similar to ``READY`` state. - If required, then functionality required by DISHes, LFAA, CSP is [TBD5] - (do they keep signal processing and stop transmitting output data? What happens to - observations that are time/position sensitive and cannot resume after a pause?) + The subarray is taking data and, if needed, all components are + synchronously moving in the observed coordinate system. Any changes + to the sub-systems are happening automatically (this allows for a + scan to cover the case where the phase centre is moved in a + pre-defined pattern). """ - ABORTED = 5 + ABORTING = 6 """ - The subarray has had its previous state interrupted by the controller. Exit from - the ``ABORTED`` state requires the ``Reset`` command. + The subarray is trying to abort what it was doing due to having been + interrupted by the controller. """ - FAULT = 6 + ABORTED = 7 """ - Subarray has detected an internal error making it impossible to remain in the previous - state. - - **Note:** This shall trigger a ``healthState`` update of the Subarray/Capability. + The subarray has had its previous state interrupted by the + controller, and is now in an aborted state. + """ + + RESETTING = 8 + """ + The subarray device is resetting to the IDLE state. + """ + + FAULT = 9 + """ + The subarray has detected an error in its observing state making it + impossible to remain in the previous state. + """ + + RESTARTING = 10 + """ + The subarray device is restarting, as the last known stable state is + where no resources were allocated and the configuration undefined. """ @@ -246,13 +265,13 @@ class ControlMode(enum.IntEnum): LOCAL = 1 """ - TANGO Device accepts only from a ‘local’ client and ignores commands and queries received - from TM or any other ‘remote’ clients. This is typically activated by a switch, + TANGO Device accepts only from a ‘local’ client and ignores commands and queries received + from TM or any other ‘remote’ clients. This is typically activated by a switch, or a connection on the local control interface. The intention is to support early integration of DISHes and stations. The equipment has to be put back in ``REMOTE`` before clients can take control again. ``controlMode`` may be removed from the SCM if unused/not needed. - + **Note:** Setting `controlMode` to `LOCAL` **is not a safety feature**, but rather a usability feature. Safety has to be implemented separately to the control paths. """ @@ -289,8 +308,8 @@ class TestMode(enum.IntEnum): TEST = 1 """ - Element (entity) behaviour and/or set of commands differ for the normal operating mode. To - be implemented only by devices that implement one or more test modes. The Element + Element (entity) behaviour and/or set of commands differ for the normal operating mode. To + be implemented only by devices that implement one or more test modes. The Element documentation shall provide detailed description. """ @@ -309,3 +328,93 @@ class LoggingLevel(enum.IntEnum): WARNING = 3 INFO = 4 DEBUG = 5 + + +class DeviceStateModel: + """ + Base class for the state model used by SKA devices. + """ + + def __init__(self, transitions, initial_state): + """ + Create a new device state model. + + :param transitions: a dictionary for which each key is a (state, + event) tuple, and each value is a (state, side-effect) + tuple. When the device is in state `IN-STATE`, and action + `ACTION` is attempted, the transitions table will be checked + for an entry under key `(IN-STATE, EVENT)`. If no such key + exists, the action will be denied and a model will raise a + `StateModelError`. If the key does exist, then its value + `(OUT-STATE, SIDE-EFFECT)` will result in the model + transitioning to state `OUT-STATE`, and executing + `SIDE-EFFECT`, which must be a function or lambda that + takes a single parameter - a model instance. + :type transitions: dict + :param initial_state: the starting state of the model + :type initial_state: a state with an entry in the transitions + table + """ + self._transitions = transitions + self._state = initial_state + + @property + def state(self): + """Return current state as a string.""" + return self._state + + def update_transitions(self, transitions): + """ + Update the transitions table with new transitions. + + :param transitions: new transitions to be included in the + transitions table. Transitions with pre-existing keys will + replace the transitions for that key. Transitions with novel + keys will be added. There is no provision for removing + transitions + :type transitions: dict + """ + self._transitions.update(transitions) + + def is_action_allowed(self, action): + """ + Whether a given action is allowed in the current state. + + :param action: an action, as given in the transitions table + :type action: ANY + """ + return (self._state, action) in self._transitions + + def try_action(self, action): + """ + Checks whether a given action is allowed in the current state, + and raises a StateModelError if it is not. + + :param action: an action, as given in the transitions table + :type action: ANY + :raises StateModelError: if the action is not allowed in the + current state + :returns: True if the action is allowed + :rtype: boolean + """ + if not self.is_action_allowed(action): + raise StateModelError( + f"Action '{action}' not allowed in current state ({self._state})." + ) + return True + + def perform_action(self, action): + """ + Performs an action on the state model + + :param action: an action, as given in the transitions table + :type action: ANY + :raises StateModelError: if the action is not allowed in the + current state + + """ + self.try_action(action) + + (self._state, side_effect) = self._transitions[(self._state, action)] + if side_effect is not None: + side_effect(self) diff --git a/src/ska/base/faults.py b/src/ska/base/faults.py index 0fb8cf06..b25218b6 100644 --- a/src/ska/base/faults.py +++ b/src/ska/base/faults.py @@ -15,3 +15,19 @@ class LoggingLevelError(SKABaseError): class LoggingTargetError(SKABaseError): """Error parsing logging target string.""" + + +class ResultCodeError(ValueError): + """A method has returned an invalid return code.""" + + +class StateModelError(ValueError): + """Error in state machine model related to transitions or state.""" + + +class CommandError(RuntimeError): + """Error executing a BaseCommand or similar.""" + + +class CapabilityValidationError(ValueError): + """Error in validating capability input against capability types.""" diff --git a/src/ska/base/logger_device.py b/src/ska/base/logger_device.py index 6f1113f8..72f03d2c 100644 --- a/src/ska/base/logger_device.py +++ b/src/ska/base/logger_device.py @@ -4,23 +4,21 @@ # # # -""" SKALogger - -A generic base device for Logging for SKA. It enables to view on-line logs through the TANGO Logging Services -and to store logs using Python logging. It configures the log levels of remote logging for selected devices. +""" +This module implements SKALogger device, a generic base device for +logging for SKA. It enables to view on-line logs through the TANGO +Logging Services and to store logs using Python logging. It configures +the log levels of remote logging for selected devices. """ # PROTECTED REGION ID(SKALogger.additionnal_import) ENABLED START # -# Standard imports -import os -import sys - # Tango imports from tango import DebugIt, DeviceProxy, DevFailed from tango.server import run, command # SKA specific imports -from . import SKABaseDevice, release -from .control_model import LoggingLevel +from ska.base import SKABaseDevice +from ska.base.commands import ResponseCommand, ResultCode +from ska.base.control_model import LoggingLevel # PROTECTED REGION END # // SKALogger.additionnal_import __all__ = ["SKALogger", "main"] @@ -44,14 +42,15 @@ class SKALogger(SKABaseDevice): # --------------- # General methods # --------------- - - def init_device(self): - SKABaseDevice.init_device(self) - # PROTECTED REGION ID(SKALogger.init_device) ENABLED START # - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # PROTECTED REGION END # // SKALogger.init_device + def init_command_objects(self): + """ + Sets up the command objects + """ + super().init_command_objects() + self.register_command_object( + "SetLoggingLevel", + self.SetLoggingLevelCommand(self, self.state_model, self.logger) + ) def always_executed_hook(self): # PROTECTED REGION ID(SKALogger.always_executed_hook) ENABLED START # @@ -70,36 +69,84 @@ class SKALogger(SKABaseDevice): # -------- # Commands # -------- - - @command(dtype_in='DevVarLongStringArray', - doc_in="Logging level for selected devices:" - "(0=OFF, 1=FATAL, 2=ERROR, 3=WARNING, 4=INFO, 5=DEBUG)." - "Example: [[4, 5], ['my/dev/1', 'my/dev/2']].") + class SetLoggingLevelCommand(ResponseCommand): + """ + A class for the SKALoggerDevice's SetLoggingLevel() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for SetLoggingLevelCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, logger=logger) + + def do(self, argin): + """ + Stateless hook for SetLoggingLevel() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + logging_levels = argin[0][:] + logging_devices = argin[1][:] + for level, device in zip(logging_levels, logging_devices): + try: + new_level = LoggingLevel(level) + self.logger.info("Setting logging level %s for %s", new_level, device) + dev_proxy = DeviceProxy(device) + dev_proxy.loggingLevel = new_level + except DevFailed: + self.logger.exception("Failed to set logging level %s for %s", level, device) + + message = "SetLoggingLevel command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + @command( + dtype_in='DevVarLongStringArray', + doc_in="Logging level for selected devices:" + "(0=OFF, 1=FATAL, 2=ERROR, 3=WARNING, 4=INFO, 5=DEBUG)." + "Example: [[4, 5], ['my/dev/1', 'my/dev/2']].", + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() def SetLoggingLevel(self, argin): # PROTECTED REGION ID(SKALogger.SetLoggingLevel) ENABLED START # """ Sets logging level of the specified devices. - :parameter: argin: DevVarLongStringArray - Array consisting of + To modify behaviour for this command, modify the do() method of + the command class. - argin[0]: list of DevLong. Desired logging level. + :param argin: Array consisting of - argin[1]: list of DevString. Desired tango device. + * argin[0]: list of DevLong. Desired logging level. + * argin[1]: list of DevString. Desired tango device. + + :type argin: DevVarLongStringArray :returns: None. """ - logging_levels = argin[0][:] - logging_devices = argin[1][:] - for level, device in zip(logging_levels, logging_devices): - try: - new_level = LoggingLevel(level) - self.logger.info("Setting logging level %s for %s", new_level, device) - dev_proxy = DeviceProxy(device) - dev_proxy.loggingLevel = new_level - except DevFailed: - self.logger.exception("Failed to set logging level %s for %s", level, device) + command = self.get_command_object("SetLoggingLevel") + (return_code, message) = command(argin) + return [[return_code], [message]] + # PROTECTED REGION END # // SKALogger.SetLoggingLevel # ---------- @@ -115,5 +162,6 @@ def main(args=None, **kwargs): return run((SKALogger,), args=args, **kwargs) # PROTECTED REGION END # // SKALogger.main + if __name__ == '__main__': main() diff --git a/src/ska/base/master_device.py b/src/ska/base/master_device.py index 1b227c8a..bc503819 100644 --- a/src/ska/base/master_device.py +++ b/src/ska/base/master_device.py @@ -7,20 +7,17 @@ """ SKAMaster -A master test +Master device """ # PROTECTED REGION ID(SKAMaster.additionnal_import) ENABLED START # -# Standard imports -import os -import sys - # Tango imports from tango import DebugIt from tango.server import run, attribute, command, device_property # SKA specific imports -from . import SKABaseDevice, release -from .utils import validate_capability_types, validate_input_sizes, convert_dict_to_list +from ska.base import SKABaseDevice +from ska.base.commands import BaseCommand, ResultCode +from ska.base.utils import validate_capability_types, validate_input_sizes, convert_dict_to_list # PROTECTED REGION END # // SKAMaster.additionnal_imports @@ -30,8 +27,54 @@ __all__ = ["SKAMaster", "main"] class SKAMaster(SKABaseDevice): """ - A master test + Master device """ + def init_command_objects(self): + """ + Sets up the command objects + """ + super().init_command_objects() + self.register_command_object( + "IsCapabilityAchievable", + self.IsCapabilityAchievableCommand( + self, self.state_model, self.logger + ) + ) + + class InitCommand(SKABaseDevice.InitCommand): + """ + A class for the SKAMaster's init_device() "command". + """ + def do(self): + """ + Stateless hook for device initialisation. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + super().do() + + device = self.target + device._element_logger_address = "" + device._element_alarm_address = "" + device._element_tel_state_address = "" + device._element_database_address = "" + device._element_alarm_device = "" + device._element_tel_state_device = "" + device._element_database_device = "" + device._max_capabilities = {} + if device.MaxCapabilities: + for max_capability in device.MaxCapabilities: + capability_type, max_capability_instances = max_capability.split(":") + device._max_capabilities[capability_type] = int(max_capability_instances) + device._available_capabilities = device._max_capabilities.copy() + + message = "SKAMaster Init command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + # PROTECTED REGION ID(SKAMaster.class_variable) ENABLED START # # PROTECTED REGION END # // SKAMaster.class_variable @@ -73,7 +116,8 @@ class SKAMaster(SKABaseDevice): maxCapabilities = attribute( dtype=('str',), max_dim_x=20, - doc="Maximum number of instances of each capability type, e.g. 'CORRELATOR:512', 'PSS-BEAMS:4'.", + doc=("Maximum number of instances of each capability type," + " e.g. 'CORRELATOR:512', 'PSS-BEAMS:4'."), ) availableCapabilities = attribute( @@ -87,28 +131,6 @@ class SKAMaster(SKABaseDevice): # General methods # --------------- - def init_device(self): - SKABaseDevice.init_device(self) - # PROTECTED REGION ID(SKAMaster.init_device) ENABLED START # - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # Initialize attribute values. - self._element_logger_address = "" - self._element_alarm_address = "" - self._element_tel_state_address = "" - self._element_database_address = "" - self._element_alarm_device = "" - self._element_tel_state_device = "" - self._element_database_device = "" - self._max_capabilities = {} - if self.MaxCapabilities: - for max_capability in self.MaxCapabilities: - capability_type, max_capability_instances = max_capability.split(":") - self._max_capabilities[capability_type] = int(max_capability_instances) - self._available_capabilities = self._max_capabilities.copy() - # PROTECTED REGION END # // SKAMaster.init_device - def always_executed_hook(self): # PROTECTED REGION ID(SKAMaster.always_executed_hook) ENABLED START # pass @@ -163,49 +185,70 @@ class SKAMaster(SKABaseDevice): # Commands # -------- - @command(dtype_in='DevVarLongStringArray', doc_in="[nrInstances][Capability types]", dtype_out='bool',) + class IsCapabilityAchievableCommand(BaseCommand): + """ + A class for the SKAMaster's IsCapabilityAchievable() command. + """ + def do(self, argin): + """ + Stateless hook for device IsCapabilityAchievable() command. + + :return: Whether the capability is achievable + :rtype: bool + """ + device = self.target + command_name = 'isCapabilityAchievable' + capabilities_instances, capability_types = argin + validate_input_sizes(command_name, argin) + validate_capability_types(command_name, capability_types, + list(device._max_capabilities.keys())) + + for capability_type, capability_instances in zip( + capability_types, capabilities_instances + ): + if not device._available_capabilities[ + capability_type + ] >= capability_instances: + return False + return True + + @command( + dtype_in='DevVarLongStringArray', + doc_in="[nrInstances][Capability types]", + dtype_out='bool', + ) @DebugIt() def isCapabilityAchievable(self, argin): # PROTECTED REGION ID(SKAMaster.isCapabilityAchievable) ENABLED START # """ Checks of provided capabilities can be achieved by the resource(s). - :param argin: DevVarLongStringArray. + To modify behaviour for this command, modify the do() method of + the command class. - An array consisting pair of - [nrInstances]: DevLong. Number of instances of the capability. + :param argin: An array consisting pair of - [Capability types]: DevString. Type of capability. + * [nrInstances]: DevLong. Number of instances of the capability. + * [Capability types]: DevString. Type of capability. - :return: DevBoolean + :type argin: DevVarLongStringArray. - True if capability can be achieved. - - False if cannot. + :return: True if capability can be achieved, False if cannot + :rtype: DevBoolean """ - command_name = 'isCapabilityAchievable' - capabilities_instances, capability_types = argin - validate_input_sizes(command_name, argin) - validate_capability_types(command_name, capability_types, - list(self._max_capabilities.keys())) - - for capability_type, capability_instances in zip( - capability_types, capabilities_instances): - if not self._available_capabilities[capability_type] >= capability_instances: - return False - - return True + command = self.get_command_object("IsCapabilityAchievable") + return command(argin) # PROTECTED REGION END # // SKAMaster.isCapabilityAchievable + # ---------- # Run server # ---------- - - def main(args=None, **kwargs): # PROTECTED REGION ID(SKAMaster.main) ENABLED START # return run((SKAMaster,), args=args, **kwargs) # PROTECTED REGION END # // SKAMaster.main + if __name__ == '__main__': main() diff --git a/src/ska/base/obs_device.py b/src/ska/base/obs_device.py index 18f63d89..90e5d983 100644 --- a/src/ska/base/obs_device.py +++ b/src/ska/base/obs_device.py @@ -6,32 +6,96 @@ # """ SKAObsDevice -A generic base device for Observations for SKA. It inherits SKABaseDevice class. Any device implementing -an obsMode will inherit from SKAObsDevice instead of just SKABaseDevice. +A generic base device for Observations for SKA. It inherits SKABaseDevice +class. Any device implementing an obsMode will inherit from SKAObsDevice +instead of just SKABaseDevice. """ # Additional import # PROTECTED REGION ID(SKAObsDevice.additionnal_import) ENABLED START # -# Standard imports -import os -import sys - # Tango imports +from tango import DevState from tango.server import run, attribute # SKA specific imports -from . import SKABaseDevice, release -from .control_model import ObsMode, ObsState +from ska.base import SKABaseDevice, SKABaseDeviceStateModel +from ska.base.commands import ResultCode +from ska.base.control_model import AdminMode, ObsMode, ObsState # PROTECTED REGION END # // SKAObsDevice.additionnal_imports -__all__ = ["SKAObsDevice", "main"] +__all__ = ["SKAObsDevice", "SKAObsDeviceStateModel", "main"] + + +class SKAObsDeviceStateModel(SKABaseDeviceStateModel): + """ + Implements the state model for the SKABaseDevice + """ + def __init__(self, dev_state_callback=None): + """ + Initialises the model. Note that this does not imply moving to + INIT state. The INIT state is managed by the model itself. + """ + super().__init__( + dev_state_callback=dev_state_callback + ) + self.update_transitions( + { + ('UNINITIALISED', 'init_started'): ( + "INIT (ENABLED)", + lambda self: ( + self._set_admin_mode(AdminMode.MAINTENANCE), + self._set_dev_state(DevState.INIT), + self._set_obs_state(ObsState.EMPTY) + ) + ) + } + ) + self._obs_state = None + + def _set_obs_state(self, obs_state): + """ + Helper method: set the value of obs_state value + + :param obs_state: the new obs_state value + :type obs_state: ObsState + """ + self._obs_state = obs_state + + @property + def obs_state(self): + return self._obs_state class SKAObsDevice(SKABaseDevice): """ A generic base device for Observations for SKA. """ + class InitCommand(SKABaseDevice.InitCommand): + """ + A class for the SKAObsDevice's init_device() "command". + """ + def do(self): + """ + Stateless hook for device initialisation. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + super().do() + + device = self.target + device._obs_mode = ObsMode.IDLE + device._config_progress = 0 + device._config_delay_expected = 0 + + message = "SKAObsDevice Init command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + # PROTECTED REGION ID(SKAObsDevice.class_variable) ENABLED START # + # PROTECTED REGION END # // SKAObsDevice.class_variable # ----------------- @@ -71,19 +135,13 @@ class SKAObsDevice(SKABaseDevice): # --------------- # General methods # --------------- - - def init_device(self): - SKABaseDevice.init_device(self) - # PROTECTED REGION ID(SKAObsDevice.init_device) ENABLED START # - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # Initialize attribute values. - self._obs_state = ObsState.IDLE - self._obs_mode = ObsMode.IDLE - self._config_progress = 0 - self._config_delay_expected = 0 - # PROTECTED REGION END # // SKAObsDevice.init_device + def _init_state_model(self): + """ + Sets up the state model for the device + """ + self.state_model = SKAObsDeviceStateModel( + dev_state_callback=self._update_state, + ) def always_executed_hook(self): # PROTECTED REGION ID(SKAObsDevice.always_executed_hook) ENABLED START # @@ -102,7 +160,7 @@ class SKAObsDevice(SKABaseDevice): def read_obsState(self): # PROTECTED REGION ID(SKAObsDevice.obsState_read) ENABLED START # """Reads Observation State of the device""" - return self._obs_state + return self.state_model.obs_state # PROTECTED REGION END # // SKAObsDevice.obsState_read def read_obsMode(self): @@ -123,7 +181,6 @@ class SKAObsDevice(SKABaseDevice): return self._config_delay_expected # PROTECTED REGION END # // SKAObsDevice.configurationDelayExpected_read - # -------- # Commands # -------- @@ -132,10 +189,18 @@ class SKAObsDevice(SKABaseDevice): # Run server # ---------- + def main(args=None, **kwargs): # PROTECTED REGION ID(SKAObsDevice.main) ENABLED START # + """ + Main function of the SKAObsDevice module. + + :param args: None + :param kwargs: + """ return run((SKAObsDevice,), args=args, **kwargs) # PROTECTED REGION END # // SKAObsDevice.main + if __name__ == '__main__': main() diff --git a/src/ska/base/release.py b/src/ska/base/release.py index 7ec0d525..f6b57c23 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.5.4" +version = "0.6.0" version_info = version.split(".") description = """A set of generic base devices for SKA Telescope.""" author = "SKA India and SARAO" diff --git a/src/ska/base/subarray_device.py b/src/ska/base/subarray_device.py index 1b785c58..db105c86 100644 --- a/src/ska/base/subarray_device.py +++ b/src/ska/base/subarray_device.py @@ -6,158 +6,964 @@ # """ SKASubarray -A SubArray handling device. It allows the assigning/releasing of resources into/from Subarray, configuring -capabilities, and exposes the related information like assigned resources, configured capabilities, etc. +A SubArray handling device. It allows the assigning/releasing of resources +into/from Subarray, configuring capabilities, and exposes the related +information like assigned resources, configured capabilities, etc. """ # PROTECTED REGION ID(SKASubarray.additionnal_import) ENABLED START # -# Standard imports -import os -import sys +import json -# Tango imports from tango import DebugIt +from tango import DevState from tango.server import run, attribute, command from tango.server import device_property -from tango import Except, ErrSeverity, DevState # SKA specific imports -from . import SKAObsDevice, release -from .control_model import AdminMode, ObsState +from ska.base import SKAObsDevice, SKAObsDeviceStateModel +from ska.base.commands import ActionCommand, ResultCode +from ska.base.control_model import ObsState +from ska.base.faults import CapabilityValidationError # PROTECTED REGION END # // SKASubarray.additionnal_imports -__all__ = ["SKASubarray", "main"] +__all__ = ["SKASubarray", "SKASubarrayResourceManager", "SKASubarrayStateModel", "main"] -class SKASubarray(SKAObsDevice): +class SKASubarrayStateModel(SKAObsDeviceStateModel): """ - SubArray handling device + Implements the state model for the SKASubarray """ - # PROTECTED REGION ID(SKASubarray.class_variable) ENABLED START # - def _is_command_allowed(self, command_name): - """Determine whether the command specified by the command_name parameter should - be allowed to execute or not. - - Parameters - ---------- - command_name: str - The name of the command which is to be executed. - - Returns - ------- - True or False: boolean - A True is returned when the device is in the allowed states and modes to - execute the command. Returns False if the command name is not in the list of - commands with rules specified for them. - - Raises - ------ - tango.DevFailed: If the device is not in the allowed states and/modes to - execute the command. - """ - admin_mode = self.read_adminMode() - obs_state = self.read_obsState() - if command_name in ["ReleaseResources", "AssignResources"]: - if admin_mode in [AdminMode.OFFLINE, AdminMode.NOT_FITTED]: - Except.throw_exception("Command failed!", "Subarray adminMode is" - " 'OFFLINE' or 'NOT_FITTED'.", - command_name, ErrSeverity.ERR) - - if obs_state == ObsState.IDLE: - if admin_mode in [AdminMode.ONLINE, AdminMode.MAINTENANCE]: - return True - else: - Except.throw_exception("Command failed!", "Subarray adminMode not" - "'ONLINE' or not in 'MAINTENANCE'.", - command_name, ErrSeverity.ERR) - else: - Except.throw_exception("Command failed!", "Subarray obsState not 'IDLE'.", - command_name, ErrSeverity.ERR) - - elif command_name in ['ConfigureCapability', 'DeconfigureCapability', - 'DeconfigureAllCapabilities']: - if self.get_state() == DevState.ON and admin_mode == AdminMode.ONLINE: - if obs_state in [ObsState.IDLE, ObsState.READY]: - return True - else: - Except.throw_exception( - "Command failed!", "Subarray obsState not 'IDLE' or 'READY'.", - command_name, ErrSeverity.ERR) - else: - Except.throw_exception( - "Command failed!", "Subarray State not 'ON' and/or adminMode not" - " 'ONLINE'.", command_name, ErrSeverity.ERR) + __transitions = { + ('OFF', 'on_succeeded'): ( + "EMPTY", + lambda self: self._set_dev_state(DevState.ON) + ), + ('OFF', 'on_failed'): ( + "FAULT", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('EMPTY', 'off_succeeded'): ( + "OFF", + lambda self: self._set_dev_state(DevState.OFF) + ), + ('EMPTY', 'off_failed'): ( + "FAULT", + lambda self: self._set_dev_state(DevState.FAULT) + ), + ('EMPTY', 'assign_started'): ( + "RESOURCING", + lambda self: self._set_obs_state(ObsState.RESOURCING) + ), + ('EMPTY', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESOURCING', 'resourcing_succeeded_some_resources'): ( + "IDLE", + lambda self: self._set_obs_state(ObsState.IDLE) + ), + ('RESOURCING', 'resourcing_succeeded_no_resources'): ( + "EMPTY", + lambda self: self._set_obs_state(ObsState.EMPTY) + ), + ('RESOURCING', 'resourcing_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESOURCING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('IDLE', 'assign_started'): ( + "RESOURCING", + lambda self: self._set_obs_state(ObsState.RESOURCING) + ), + ('IDLE', 'release_started'): ( + "RESOURCING", + lambda self: self._set_obs_state(ObsState.RESOURCING) + ), + ('IDLE', 'configure_started'): ( + "CONFIGURING", + lambda self: self._set_obs_state(ObsState.CONFIGURING) + ), + ('IDLE', 'abort_started'): ( + "ABORTING", + lambda self: self._set_obs_state(ObsState.ABORTING) + ), + ('IDLE', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('CONFIGURING', 'configure_succeeded'): ( + "READY", + lambda self: self._set_obs_state(ObsState.READY) + ), + ('CONFIGURING', 'configure_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('CONFIGURING', 'abort_started'): ( + "ABORTING", + lambda self: self._set_obs_state(ObsState.ABORTING) + ), + ('CONFIGURING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('READY', 'end_succeeded'): ( + "IDLE", + lambda self: self._set_obs_state(ObsState.IDLE) + ), + ('READY', 'end_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('READY', 'configure_started'): ( + "CONFIGURING", + lambda self: self._set_obs_state(ObsState.CONFIGURING) + ), + ('READY', 'abort_started'): ( + "ABORTING", + lambda self: self._set_obs_state(ObsState.ABORTING) + ), + ('READY', 'scan_started'): ( + "SCANNING", + lambda self: self._set_obs_state(ObsState.SCANNING) + ), + ('READY', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('SCANNING', 'scan_succeeded'): ( + "READY", + lambda self: self._set_obs_state(ObsState.READY) + ), + ('SCANNING', 'scan_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('SCANNING', 'end_scan_succeeded'): ( + "READY", + lambda self: self._set_obs_state(ObsState.READY) + ), + ('SCANNING', 'end_scan_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('SCANNING', 'abort_started'): ( + "ABORTING", + lambda self: self._set_obs_state(ObsState.ABORTING) + ), + ('SCANNING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('ABORTING', 'abort_succeeded'): ( + "ABORTED", + lambda self: self._set_obs_state(ObsState.ABORTED) + ), + ('ABORTING', 'abort_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('ABORTING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('ABORTED', 'obs_reset_started'): ( + "RESETTING", + lambda self: self._set_obs_state(ObsState.RESETTING) + ), + ('ABORTED', 'restart_started'): ( + "RESTARTING", + lambda self: self._set_obs_state(ObsState.RESTARTING) + ), + ('ABORTED', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('OBSFAULT', 'obs_reset_started'): ( + "RESETTING", + lambda self: self._set_obs_state(ObsState.RESETTING) + ), + ('OBSFAULT', 'restart_started'): ( + "RESTARTING", + lambda self: self._set_obs_state(ObsState.RESTARTING) + ), + ('OBSFAULT', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESETTING', 'obs_reset_succeeded'): ( + "IDLE", + lambda self: self._set_obs_state(ObsState.IDLE) + ), + ('RESETTING', 'obs_reset_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESETTING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESTARTING', 'restart_succeeded'): ( + "EMPTY", + lambda self: self._set_obs_state(ObsState.EMPTY) + ), + ('RESTARTING', 'restart_failed'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + ('RESTARTING', 'fatal_error'): ( + "OBSFAULT", + lambda self: self._set_obs_state(ObsState.FAULT) + ), + } + + def __init__(self, dev_state_callback=None): + """ + Initialises the model. Note that this does not imply moving to + INIT state. The INIT state is managed by the model itself. + """ + super().__init__( + dev_state_callback=dev_state_callback, + ) + self.update_transitions(self.__transitions) - return False +class SKASubarrayResourceManager: + """ + A simple class for managing subarray resources + """ + def __init__(self): + """ + Constructor for SKASubarrayResourceManager + """ + self._resources = set() - def _validate_capability_types(self, command_name, capability_types): - """Check the validity of the input parameter passed on to the command specified - by the command_name parameter. + def __len__(self): + """ + Returns the number of resources currently assigned. Note that + this also functions as a boolean method for whether there are + any assigned resources: ``if len()``. - Parameters - ---------- - command_name: str - The name of the command which is to be executed. - capability_types: list - A list strings representing capability types. + :return: number of resources assigned + :rtype: int + """ + return len(self._resources) - Raises - ------ - tango.DevFailed: If any of the capabilities requested are not valid. + def assign(self, resources): """ - invalid_capabilities = list( - set(capability_types) - set(self._configured_capabilities)) + Assign some resources + + :todo: Currently implemented for testing purposes to take a JSON + string encoding a dictionary with key 'example'. In future this + will take a collection of resources. + :param resources: JSON-encoding of a dictionary, with resources to + assign under key 'example' + :type resources: JSON string + """ + resources_dict = json.loads(resources) + add_resources = resources_dict['example'] + self._resources |= set(add_resources) - if invalid_capabilities: - Except.throw_exception( - "Command failed!", "Invalid capability types requested {}".format( - invalid_capabilities), command_name, ErrSeverity.ERR) + def release(self, resources): + """ + Release some resources + + :todo: Currently implemented for testing purposes to take a JSON + string encoding a dictionary with key 'example'. In future this + will take a collection of resources. + :param resources: JSON-encoding of a dictionary, with resources to + assign under key 'example' + :type resources: JSON string + """ + resources_dict = json.loads(resources) + drop_resources = resources_dict['example'] + self._resources -= set(drop_resources) + def release_all(self): + """ + Release all resources + """ + self._resources.clear() - def _validate_input_sizes(self, command_name, argin): - """Check the validity of the input parameters passed on to the command specified - by the command_name parameter. + def get(self): + """ + Get current resources + + :return: a set of current resources. + :rtype: set of string + """ + return set(self._resources) - Parameters - ---------- - command_name: str - The name of the command which is to be executed. - argin: tango.DevVarLongStringArray - A tuple of two lists representing [number of instances][capability types] - Raises - ------ - tango.DevFailed: If the two lists are not equal in length. +class SKASubarray(SKAObsDevice): + """ + Implements the SKA SubArray device + """ + class InitCommand(SKAObsDevice.InitCommand): """ - capabilities_instances, capability_types = argin - if len(capabilities_instances) != len(capability_types): - Except.throw_exception("Command failed!", "Argin value lists size mismatch.", - command_name, ErrSeverity.ERR) + A class for the SKASubarray's init_device() "command". + """ + def do(self): + """ + Stateless hook for device initialisation. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + super().do() + + device = self.target + device.resource_manager = SKASubarrayResourceManager() + device._activation_time = 0.0 + + # device._configured_capabilities is kept as a + # dictionary internally. The keys and values will represent + # the capability type name and the number of instances, + # respectively. + try: + device._configured_capabilities = dict.fromkeys( + device.CapabilityTypes, + 0 + ) + except TypeError: + # Might need to have the device property be mandatory in the database. + device._configured_capabilities = {} + + message = "SKASubarray Init command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class OnCommand(ActionCommand): + """ + A class for the SKASubarray's On() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for OnCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "on", logger=logger) + + def do(self): + """ + Stateless hook for On() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + message = "On command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class OffCommand(ActionCommand): + """ + A class for the SKASubarray's Off() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for OffCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "off", logger=logger) + + def do(self): + """ + Stateless hook for Off() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + message = "Off command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class _ResourcingCommand(ActionCommand): + """ + An abstract base class for SKASubarray's resourcing commands. + """ + def __init__(self, target, state_model, action_hook, logger=None): + """ + Constructor for _ResourcingCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param action_hook: a hook for the command, used to build + actions that will be sent to the state model; for example, + if the hook is "scan", then success of the command will + result in action "scan_succeeded" being sent to the state + model. + :type action_hook: string + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, action_hook, start_action=True, logger=logger + ) + + def succeeded(self): + """ + Action to take on successful completion of a resourcing + command. + """ + if len(self.target): + action = "resourcing_succeeded_some_resources" + else: + action = "resourcing_succeeded_no_resources" + self.state_model.perform_action(action) + def failed(self): + """ + Action to take on failed completion of a resourcing command. + """ + self.state_model.perform_action("resourcing_failed") - def is_AssignResources_allowed(self): - return self._is_command_allowed("AssignResources") + class AssignResourcesCommand(_ResourcingCommand): + """ + A class for SKASubarray's AssignResources() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for AssignResourcesCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "assign", logger=logger) + + def do(self, argin): + """ + Stateless hook for AssignResources() command functionality. + + :param argin: The resources to be assigned + :type argin: list of str + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + resource_manager = self.target + resource_manager.assign(argin) + + message = "AssignResources command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class ReleaseResourcesCommand(_ResourcingCommand): + """ + A class for SKASubarray's ReleaseResources() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for OnCommand() + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "release", logger=logger) + + def do(self, argin): + """ + Stateless hook for ReleaseResources() command functionality. + + :param argin: The resources to be released + :type argin: list of str + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + resource_manager = self.target + resource_manager.release(argin) + + message = "ReleaseResources command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class ReleaseAllResourcesCommand(ReleaseResourcesCommand): + """ + A class for SKASubarray's ReleaseAllResources() command. + """ + def do(self): + """ + Stateless hook for ReleaseAllResources() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + resource_manager = self.target + resource_manager.release_all() + + if len(resource_manager): + message = "ReleaseAllResources command failed to release all." + self.logger.info(message) + return (ResultCode.FAILED, message) + else: + message = "ReleaseAllResources command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) - def is_ReleaseResources_allowed(self): - return self._is_command_allowed("ReleaseResources") + class ConfigureCommand(ActionCommand): + """ + A class for SKASubarray's Configure() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for ConfigureCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "configure", start_action=True, logger=logger + ) + + def do(self, argin): + """ + Stateless hook for Configure() command functionality. + + :param argin: The configuration as JSON + :type argin: str + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + + # In this example implementation, the keys of the dict + # are the capability types, and the values are the + # integer number of instances required. + # E.g., config = {"BAND1": 5, "BAND2": 3} + config = json.loads(argin) + capability_types = list(config.keys()) + device._validate_capability_types(capability_types) + + # Perform the configuration. + for capability_type, capability_instances in config.items(): + device._configured_capabilities[capability_type] += capability_instances + + message = "Configure command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class ScanCommand(ActionCommand): + """ + A class for SKASubarray's Scan() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for ScanCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "scan", start_action=True, logger=logger + ) + + def do(self, argin): + """ + Stateless hook for Scan() command functionality. + + :param argin: Scan info + :type argin: str + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + # we do a json.loads just for basic string validation + message = f"Scan command STARTED - config {json.loads(argin)}" + self.logger.info(message) + return (ResultCode.STARTED, message) + + class EndScanCommand(ActionCommand): + """ + A class for SKASubarray's EndScan() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for EndScanCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "end_scan", logger=logger) + + def do(self): + """ + Stateless hook for EndScan() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + message = "EndScan command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class EndCommand(ActionCommand): + """ + A class for SKASubarray's End() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for EndCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__(target, state_model, "end", logger=logger) + + def do(self): + """ + Stateless hook for End() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + device._deconfigure() + + message = "End command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class AbortCommand(ActionCommand): + """ + A class for SKASubarray's Abort() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for AbortCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "abort", start_action=True, logger=logger + ) + + def do(self): + """ + Stateless hook for Abort() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + message = "Abort command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class ObsResetCommand(ActionCommand): + """ + A class for SKASubarray's ObsReset() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for ObsResetCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "obs_reset", start_action=True, logger=logger + ) + + def do(self): + """ + Stateless hook for ObsReset() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + + # We might have interrupted a long-running command such as a Configure + # or a Scan, so we need to clean up from that. + + # Now totally deconfigure + device._deconfigure() + + message = "ObsReset command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) + + class RestartCommand(ActionCommand): + """ + A class for SKASubarray's Restart() command. + """ + def __init__(self, target, state_model, logger=None): + """ + Constructor for RestartCommand + + :param target: the object that this command acts upon; for + example, the SKASubarray device for which this class + implements the command + :type target: object + :param state_model: the state model that this command uses + to check that it is allowed to run, and that it drives + with actions. + :type state_model: SKABaseClassStateModel or a subclass of + same + :param logger: the logger to be used by this Command. If not + provided, then a default module logger will be used. + :type logger: a logger that implements the standard library + logger interface + """ + super().__init__( + target, state_model, "restart", start_action=True, logger=logger + ) + + def do(self): + """ + Stateless hook for Restart() command functionality. + + :return: A tuple containing a return code and a string + message indicating status. The message is for + information purpose only. + :rtype: (ResultCode, str) + """ + device = self.target + + # We might have interrupted a long-running command such as a Configure + # or a Scan, so we need to clean up from that. + + # Now totally deconfigure + device._deconfigure() + + # and release all resources + device.resource_manager.release_all() + + message = "Restart command completed OK" + self.logger.info(message) + return (ResultCode.OK, message) - def is_ReleaseAllResources_allowed(self): - return self._is_command_allowed("ReleaseResources") + # PROTECTED REGION ID(SKASubarray.class_variable) ENABLED START # + def _init_state_model(self): + """ + Sets up the state model for the device + """ + self.state_model = SKASubarrayStateModel( + dev_state_callback=self._update_state, + ) - def is_ConfigureCapability_allowed(self): - return self._is_command_allowed('ConfigureCapability') + def init_command_objects(self): + """ + Sets up the command objects + """ + super().init_command_objects() + + device_args = (self, self.state_model, self.logger) + resource_args = (self.resource_manager, self.state_model, self.logger) + + self.register_command_object("On", self.OnCommand(*device_args)) + self.register_command_object("Off", self.OffCommand(*device_args)) + self.register_command_object( + "AssignResources", + self.AssignResourcesCommand(*resource_args) + ) + self.register_command_object( + "ReleaseResources", + self.ReleaseResourcesCommand(*resource_args) + ) + self.register_command_object( + "ReleaseAllResources", + self.ReleaseAllResourcesCommand(*resource_args) + ) + self.register_command_object( + "Configure", + self.ConfigureCommand(*device_args) + ) + self.register_command_object("Scan", self.ScanCommand(*device_args)) + self.register_command_object( + "EndScan", + self.EndScanCommand(*device_args) + ) + self.register_command_object("End", self.EndCommand(*device_args)) + self.register_command_object("Abort", self.AbortCommand(*device_args)) + self.register_command_object( + "ObsReset", + self.ObsResetCommand(*device_args) + ) + self.register_command_object( + "Restart", + self.RestartCommand(*device_args) + ) + + def _validate_capability_types(self, capability_types): + """ + Check the validity of the input parameter passed to the + Configure command. + + :param device: the device for which this class implements + the configure command + :type device: SKASubarray + :param capability_types: a list strings representing + capability types. + :type capability_types: list + :raises ValueError: If any of the capabilities requested are + not valid. + """ + invalid_capabilities = list( + set(capability_types) - set(self._configured_capabilities)) - def is_DeconfigureCapability_allowed(self): - return self._is_command_allowed('DeconfigureCapability') + if invalid_capabilities: + raise CapabilityValidationError( + "Invalid capability types requested {}".format( + invalid_capabilities + ) + ) - def is_DeconfigureAllCapabilities_allowed(self): - return self._is_command_allowed('DeconfigureAllCapabilities') - # PROTECTED REGION END # // SKASubarray.class_variable + def _deconfigure(self): + """ + Completely deconfigure the subarray + """ + self._configured_capabilities = {k: 0 for k in self._configured_capabilities} # ----------------- # Device Properties # ----------------- - CapabilityTypes = device_property( dtype=('str',), ) @@ -169,7 +975,6 @@ class SKASubarray(SKAObsDevice): # ---------- # Attributes # ---------- - activationTime = attribute( dtype='double', unit="s", @@ -196,32 +1001,6 @@ class SKASubarray(SKAObsDevice): # --------------- # General methods # --------------- - - def init_device(self): - SKAObsDevice.init_device(self) - # PROTECTED REGION ID(SKASubarray.init_device) ENABLED START # - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - - # Initialize attribute values. - self._activation_time = 0.0 - self._assigned_resources = [""] - self._assigned_resources.clear() - # self._configured_capabilities is gonna be kept as a dictionary internally. The - # keys and value will represent the capability type name and the number of - # instances, respectively. - try: - self._configured_capabilities = dict.fromkeys(self.CapabilityTypes, 0) - except TypeError: - # Might need to have the device property be mandatory in the database. - self._configured_capabilities = {} - - # When Subarray in not in use it reports: - self.set_state(DevState.DISABLE) - - # PROTECTED REGION END # // SKASubarray.init_device - def always_executed_hook(self): # PROTECTED REGION ID(SKASubarray.always_executed_hook) ENABLED START # pass @@ -235,11 +1014,11 @@ class SKASubarray(SKAObsDevice): # ------------------ # Attributes methods # ------------------ - def read_activationTime(self): # PROTECTED REGION ID(SKASubarray.activationTime_read) ENABLED START # """ Reads the time since device is activated. + :return: Time of activation in seconds since Unix epoch. """ return self._activation_time @@ -249,17 +1028,19 @@ class SKASubarray(SKAObsDevice): # PROTECTED REGION ID(SKASubarray.assignedResources_read) ENABLED START # """ Reads the resources assigned to the device. + :return: Resources assigned to the device. """ - return self._assigned_resources + return sorted(self.resource_manager.get()) # PROTECTED REGION END # // SKASubarray.assignedResources_read def read_configuredCapabilities(self): # PROTECTED REGION ID(SKASubarray.configuredCapabilities_read) ENABLED START # """ Reads capabilities configured in the Subarray. - :return: A list of capability types with no. of instances - used in the Subarray + + :return: A list of capability types with no. of instances used + in the Subarray """ configured_capabilities = [] for capability_type, capability_instances in ( @@ -272,164 +1053,366 @@ class SKASubarray(SKAObsDevice): # -------- # Commands # -------- + def is_On_allowed(self): + """ + Check if command `On` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("On") + return command.check_allowed() @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() - def Abort(self): - # PROTECTED REGION ID(SKASubarray.Abort) ENABLED START # - """Change obsState to ABORTED.""" - # PROTECTED REGION END # // SKASubarray.Abort + def On(self): + """ + Turn subarray on - @command(dtype_in='DevVarLongStringArray', doc_in="[Number of instances to add][Capability types]",) - @DebugIt() - def ConfigureCapability(self, argin): - # PROTECTED REGION ID(SKASubarray.ConfigureCapability) ENABLED START # - """Configures number of instances for each capability. If the capability exists, - it increments the configured instances by the number of instances requested, - otherwise an exception will be raised. - Note: The two lists arguments must be of equal length or an exception will be raised.""" - command_name = 'ConfigureCapability' - - capabilities_instances, capability_types = argin - self._validate_capability_types(command_name, capability_types) - self._validate_input_sizes(command_name, argin) - - # Set obsState to 'CONFIGURING'. - self._obs_state = ObsState.CONFIGURING - - # Perform the configuration. - for capability_instances, capability_type in zip( - capabilities_instances, capability_types): - self._configured_capabilities[capability_type] += capability_instances - - # Change the obsState to 'READY'. - self._obs_state = ObsState.READY - # PROTECTED REGION END # // SKASubarray.ConfigureCapability - - @command(dtype_in='str', doc_in="Capability type",) - @DebugIt() - def DeconfigureAllCapabilities(self, argin): - # PROTECTED REGION ID(SKASubarray.DeconfigureAllCapabilities) ENABLED START #i - """Deconfigure all instances of the given Capability type. If the capability - type does not exist an exception will be raised, otherwise it sets the - configured instances for that capability type to zero.""" - self._validate_capability_types('DeconfigureAllCapabilities', [argin]) - self._configured_capabilities[argin] = 0 - # PROTECTED REGION END # // SKASubarray.DeconfigureAllCapabilities - - @command(dtype_in='DevVarLongStringArray', doc_in="[Number of instances to remove][Capability types]",) + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("On") + (return_code, message) = command() + return [[return_code], [message]] + + def is_Off_allowed(self): + """ + Check if command `Off` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("Off") + return command.check_allowed() + + @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() - def DeconfigureCapability(self, argin): - # PROTECTED REGION ID(SKASubarray.DeconfigureCapability) ENABLED START # - """Deconfigures a given number of instances for each capability. - If the capability exists, it decrements the configured instances by the - number of instances requested, otherwise an exceptioin will be raised. - Note: The two lists arguments must be of equal length or an exception - will be raised""" - command_name = 'DeconfigureCapability' - capabilities_instances, capability_types = argin - - self._validate_capability_types(command_name, capability_types) - self._validate_input_sizes(command_name, argin) - - - # Perform the deconfiguration - for capability_instances, capability_type in zip( - capabilities_instances, capability_types): - if self._configured_capabilities[capability_type] < int(capability_instances): - self._configured_capabilities[capability_type] = 0 - else: - self._configured_capabilities[capability_type] -= ( - int(capability_instances)) - # PROTECTED REGION END # // SKASubarray.DeconfigureCapability + def Off(self): + """ + Turn the subarray off + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("Off") + (return_code, message) = command() + return [[return_code], [message]] + + def is_AssignResources_allowed(self): + """ + Check if command `AssignResources` is allowed in the current + device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("AssignResources") + return command.check_allowed() - @command(dtype_in=('str',), doc_in="List of Resources to add to subarray.", dtype_out=('str',), - doc_out="A list of Resources added to the subarray.",) + @command( + dtype_in="DevString", + doc_in="JSON-encoded string with the resources to add to subarray", + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() def AssignResources(self, argin): - # PROTECTED REGION ID(SKASubarray.AssignResources) ENABLED START # - """Assign resources to a Subarray""" - argout = [] - resources = self._assigned_resources[:] - for resource in argin: - if resource not in resources: - self._assigned_resources.append(resource) - argout.append(resource) - - self.set_state(DevState.ON) - return argout - - @command(dtype_in=('str',), doc_in="List of resources to remove from the subarray.", dtype_out=('str',), - doc_out="List of resources removed from the subarray.",) + """ + Assign resources to this subarray + + To modify behaviour for this command, modify the do() method of + the command class. + + :param argin: the resources to be assigned + :type argin: list of str + """ + command = self.get_command_object("AssignResources") + (return_code, message) = command(argin) + return [[return_code], [message]] + + def is_ReleaseResources_allowed(self): + """ + Check if command `ReleaseResources` is allowed in the current + device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("ReleaseResources") + return command.check_allowed() + + @command( + dtype_in="DevString", + doc_in="JSON-encoded string with the resources to remove from the subarray", + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() def ReleaseResources(self, argin): - # PROTECTED REGION ID(SKASubarray.ReleaseResources) ENABLED START # - """Delta removal of assigned resources.""" - argout = [] - # Release resources... - resources = self._assigned_resources[:] - for resource in argin: - if resource in resources: - self._assigned_resources.remove(resource) - argout.append(resource) - return argout - # PROTECTED REGION END # // SKASubarray.ReleaseResources + """ + Delta removal of assigned resources. + + To modify behaviour for this command, modify the do() method of + the command class. + + :param argin: the resources to be released + :type argin: list of str + """ + command = self.get_command_object("ReleaseResources") + (return_code, message) = command(argin) + return [[return_code], [message]] + + def is_ReleaseAllResources_allowed(self): + """ + Check if command `ReleaseAllResources` is allowed in the current + device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("ReleaseAllResources") + return command.check_allowed() @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() - def EndSB(self): - # PROTECTED REGION ID(SKASubarray.EndSB) ENABLED START # - """Change obsState to IDLE.""" - # PROTECTED REGION END # // SKASubarray.EndSB + def ReleaseAllResources(self): + """ + Remove all resources to tear down to an empty subarray. + + To modify behaviour for this command, modify the do() method of + the command class. + + :return: list of resources removed + :rtype: list of string + """ + command = self.get_command_object("ReleaseAllResources") + (return_code, message) = command() + return [[return_code], [message]] + + def is_Configure_allowed(self): + """ + Check if command `Configure` is allowed in the current + device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("Configure") + return command.check_allowed() @command( + dtype_in="DevString", + doc_in="JSON-encoded string with the scan configuration", + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) + @DebugIt() + def Configure(self, argin): + """ + Configures the capabilities of this subarray + + To modify behaviour for this command, modify the do() method of + the command class. + + :param argin: configuration specification + :type argin: string + """ + command = self.get_command_object("Configure") + (return_code, message) = command(argin) + return [[return_code], [message]] + + def is_Scan_allowed(self): + """ + Check if command `Scan` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("Scan") + return command.check_allowed() + + @command( + dtype_in="DevString", + doc_in="JSON-encoded string with the per-scan configuration", + dtype_out="DevVarLongStringArray", + doc_out="(ReturnType, 'informational message')", + ) + @DebugIt() + def Scan(self, argin): + """ + Start scanning + + To modify behaviour for this command, modify the do() method of + the command class. + + :param argin: Information about the scan + :type argin: Array of str + """ + command = self.get_command_object("Scan") + (return_code, message) = command(argin) + return [[return_code], [message]] + + def is_EndScan_allowed(self): + """ + Check if command `EndScan` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("EndScan") + return command.check_allowed() + + @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() def EndScan(self): - # PROTECTED REGION ID(SKASubarray.EndScan) ENABLED START # - """Ends the scan""" - # PROTECTED REGION END # // SKASubarray.EndScan + """ + End the scan + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("EndScan") + (return_code, message) = command() + return [[return_code], [message]] + + def is_End_allowed(self): + """ + Check if command `End` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("End") + return command.check_allowed() @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() - def Pause(self): - # PROTECTED REGION ID(SKASubarray.Pause) ENABLED START # - """Pauses the scan""" - # PROTECTED REGION END # // SKASubarray.Pause + def End(self): + # PROTECTED REGION ID(SKASubarray.EndSB) ENABLED START # + """ + End the scan block. + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("End") + (return_code, message) = command() + return [[return_code], [message]] + + def is_Abort_allowed(self): + """ + Check if command `Abort` is allowed in the current device state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("Abort") + return command.check_allowed() - @command(dtype_out=('str',), doc_out="List of resources removed from the subarray.",) + @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() - def ReleaseAllResources(self): - # PROTECTED REGION ID(SKASubarray.ReleaseAllResources) ENABLED START # - """Remove all resources to tear down to an empty subarray.""" - resources = self._assigned_resources[:] - released_resources = self.ReleaseResources(resources) - return released_resources - # PROTECTED REGION END # // SKASubarray.ReleaseAllResources + def Abort(self): + """ + Abort any long-running command such as ``Configure()`` or + ``Scan()``. + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("Abort") + (return_code, message) = command() + return [[return_code], [message]] + + def is_ObsReset_allowed(self): + """ + Check if command `ObsReset` is allowed in the current device + state. + + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("ObsReset") + return command.check_allowed() @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", ) @DebugIt() - def Resume(self): - # PROTECTED REGION ID(SKASubarray.Resume) ENABLED START # - """Resumes the scan""" - # PROTECTED REGION END # // SKASubarray.Resume + def ObsReset(self): + """ + Reset the current observation process. + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("ObsReset") + (return_code, message) = command() + return [[return_code], [message]] + + def is_Restart_allowed(self): + """ + Check if command `Restart` is allowed in the current device + state. - @command(dtype_in=('str',),) + :raises ``tango.DevFailed``: if the command is not allowed + :return: ``True`` if the command is allowed + :rtype: boolean + """ + command = self.get_command_object("Restart") + return command.check_allowed() + + @command( + dtype_out='DevVarLongStringArray', + doc_out="(ReturnType, 'informational message')", + ) @DebugIt() - def Scan(self, argin): - # PROTECTED REGION ID(SKASubarray.Scan) ENABLED START # - """Starts the scan""" - # PROTECTED REGION END # // SKASubarray.Scan + def Restart(self): + """ + Restart the subarray. That is, deconfigure and release + all resources. + + To modify behaviour for this command, modify the do() method of + the command class. + """ + command = self.get_command_object("Restart") + (return_code, message) = command() + return [[return_code], [message]] + # ---------- # Run server # ---------- - - def main(args=None, **kwargs): # PROTECTED REGION ID(SKASubarray.main) ENABLED START # """ @@ -438,5 +1421,6 @@ def main(args=None, **kwargs): return run((SKASubarray,), args=args, **kwargs) # PROTECTED REGION END # // SKASubarray.main + if __name__ == '__main__': main() diff --git a/src/ska/base/tel_state_device.py b/src/ska/base/tel_state_device.py index d9b3b0fd..c040e37e 100644 --- a/src/ska/base/tel_state_device.py +++ b/src/ska/base/tel_state_device.py @@ -9,15 +9,12 @@ A generic base device for Telescope State for SKA. """ # PROTECTED REGION ID(SKATelState.additionnal_import) ENABLED START # -# Standard import -import os -import sys - # Tango imports +# from tango import DebugIt from tango.server import run, device_property # SKA specific imports -from . import SKABaseDevice, release +from ska.base import SKABaseDevice # PROTECTED REGION END # // SKATelState.additionnal_imports __all__ = ["SKATelState", "main"] @@ -38,7 +35,6 @@ class SKATelState(SKABaseDevice): dtype='str', ) - # ---------- # Attributes # ---------- @@ -46,18 +42,6 @@ class SKATelState(SKABaseDevice): # --------------- # General methods # --------------- - - def init_device(self): - """init_device - Init device method of SKATelStateDevice - """ - SKABaseDevice.init_device(self) - self._build_state = '{}, {}, {}'.format(release.name, release.version, - release.description) - self._version_id = release.version - # PROTECTED REGION ID(SKATelState.init_device) ENABLED START # - # PROTECTED REGION END # // SKATelState.init_device - def always_executed_hook(self): # PROTECTED REGION ID(SKATelState.always_executed_hook) ENABLED START # pass @@ -72,7 +56,6 @@ class SKATelState(SKABaseDevice): # Attributes methods # ------------------ - # -------- # Commands # -------- @@ -96,5 +79,6 @@ def main(args=None, **kwargs): return run((SKATelState,), args=args, **kwargs) # PROTECTED REGION END # // SKATelState.main + if __name__ == '__main__': main() diff --git a/src/ska/base/utils.py b/src/ska/base/utils.py index 7a5b93d1..a1fe8ca0 100644 --- a/src/ska/base/utils.py +++ b/src/ska/base/utils.py @@ -14,7 +14,7 @@ from tango import (DeviceProxy, DbDatum, DbDevInfo, AttrQuality, AttrWriteType, Except, ErrSeverity) from tango import DevState from contextlib import contextmanager -from .faults import GroupDefinitionsError, SKABaseError +from ska.base.faults import GroupDefinitionsError, SKABaseError int_types = {tango._tango.CmdArgType.DevUShort, tango._tango.CmdArgType.DevLong, @@ -222,7 +222,7 @@ def get_dp_attribute(device_proxy, attribute, with_value=False, with_context=Fal ts = datetime.fromtimestamp(attr_value.time.tv_sec) ts.replace(microsecond=attr_value.time.tv_usec) attr_dict['timestamp'] = ts.isoformat() - except: + except Exception: # TBD - decide what to do - add log? pass @@ -358,7 +358,6 @@ def get_groups_from_json(json_definitions): # the exc_info is included for detailed traceback ska_error = SKABaseError(exc) raise GroupDefinitionsError(ska_error).with_traceback(sys.exc_info()[2]) - #raise GroupDefinitionsError(exc), None, sys.exc_info()[2] def _validate_group(definition): diff --git a/tests/conftest.py b/tests/conftest.py index ce97603d..da3a018d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,8 @@ import pytest from tango.test_context import DeviceTestContext +from ska.base import SKASubarrayStateModel + @pytest.fixture(scope="class") def tango_context(request): @@ -27,7 +29,7 @@ def tango_context(request): }, 'SKASubarray': { - 'CapabilityTypes': 'BAND1', + "CapabilityTypes": ["BAND1", "BAND2"], 'LoggingTargetsDefault': '', 'GroupDefinitions': '', 'SkaLevel': '4', @@ -61,3 +63,11 @@ def initialize_device(tango_context): Context to run a device without a database. """ yield tango_context.device.Init() + + +@pytest.fixture(scope="function") +def state_model(): + """ + Yields an SKASubarrayStateModel. + """ + yield SKASubarrayStateModel() diff --git a/tests/test_alarm_handler_device.py b/tests/test_alarm_handler_device.py index e82e28c9..b97224d7 100644 --- a/tests/test_alarm_handler_device.py +++ b/tests/test_alarm_handler_device.py @@ -8,14 +8,11 @@ ######################################################################################### """Contain the tests for the SKAAlarmHandler.""" -# Standard imports -import sys -import os - # Imports import re import pytest + # PROTECTED REGION ID(SKAAlarmHandler.test_additional_imports) ENABLED START # # PROTECTED REGION END # // SKAAlarmHandler.test_additional_imports # Device test case @@ -47,7 +44,6 @@ class TestSKAAlarmHandler(object): # PROTECTED REGION ID(SKAAlarmHandler.test_properties) ENABLED START # # PROTECTED REGION END # // SKAAlarmHandler.test_properties - # PROTECTED REGION ID(SKAAlarmHandler.test_GetAlarmRule_decorators) ENABLED START # # PROTECTED REGION END # // SKAAlarmHandler.test_GetAlarmRule_decorators def test_GetAlarmRule(self, tango_context): diff --git a/tests/test_base_device.py b/tests/test_base_device.py index d220092b..37d37633 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -8,9 +8,6 @@ ######################################################################################### """Contain the tests for the SKABASE.""" -# Standard imports -import sys -import os import re import pytest @@ -323,7 +320,9 @@ class TestLoggingUtils: @pytest.mark.usefixtures("tango_context", "initialize_device") # PROTECTED REGION END # // SKABaseDevice.test_SKABaseDevice_decorators class TestSKABaseDevice(object): - """Test case for packet generation.""" + """ + Test cases for SKABaseDevice. + """ properties = { 'SkaLevel': '4', @@ -358,7 +357,7 @@ class TestSKABaseDevice(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKABaseDevice.test_State) ENABLED START # - assert tango_context.device.State() == DevState.UNKNOWN + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKABaseDevice.test_State # PROTECTED REGION ID(SKABaseDevice.test_Status_decorators) ENABLED START # @@ -366,7 +365,7 @@ class TestSKABaseDevice(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKABaseDevice.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in UNKNOWN state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKABaseDevice.test_Status # PROTECTED REGION ID(SKABaseDevice.test_GetVersionInfo_decorators) ENABLED START # @@ -386,7 +385,11 @@ class TestSKABaseDevice(object): def test_Reset(self, tango_context): """Test for Reset""" # PROTECTED REGION ID(SKABaseDevice.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None + # This is a pretty weak test, but Reset() is only allowed from + # device state FAULT, and we have no way of putting into FAULT + # state through its interface. + with pytest.raises(DevFailed): + tango_context.device.Reset() # PROTECTED REGION END # // SKABaseDevice.test_Reset # PROTECTED REGION ID(SKABaseDevice.test_buildState_decorators) ENABLED START # @@ -433,7 +436,9 @@ class TestSKABaseDevice(object): # tango logging target must be enabled by default assert tango_context.device.loggingTargets == ("tango::logger", ) - with mock.patch("ska.base.base_device.LoggingUtils.create_logging_handler") as mocked_creator: + with mock.patch( + "ska.base.base_device.LoggingUtils.create_logging_handler" + ) as mocked_creator: def null_creator(target, tango_logger): handler = logging.NullHandler() @@ -490,7 +495,7 @@ class TestSKABaseDevice(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKABaseDevice.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE # PROTECTED REGION END # // SKABaseDevice.test_adminMode # PROTECTED REGION ID(SKABaseDevice.test_controlMode_decorators) ENABLED START # diff --git a/tests/test_capability_device.py b/tests/test_capability_device.py index 1f75b9be..a837f553 100644 --- a/tests/test_capability_device.py +++ b/tests/test_capability_device.py @@ -7,15 +7,10 @@ # ######################################################################################### """Contain the tests for the SKACapability.""" - -# Standard imports -import sys -import os - -# Imports import re import pytest + # PROTECTED REGION ID(SKACapability.test_additional_imports) ENABLED START # # PROTECTED REGION END # // SKACapability.test_additional_imports # Device test case diff --git a/tests/test_logger_device.py b/tests/test_logger_device.py index cf794703..d60e5038 100644 --- a/tests/test_logger_device.py +++ b/tests/test_logger_device.py @@ -8,15 +8,9 @@ ######################################################################################### """Contain the tests for the SKALogger.""" -# Standard imports -import sys -import os - -# Imports import re import pytest from tango import DevState, DeviceProxy - import tango # PROTECTED REGION ID(SKALogger.test_additional_imports) ENABLED START # @@ -24,7 +18,8 @@ from ska.base.control_model import ( AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode ) # PROTECTED REGION END # // SKALogger.test_additional_imports -# Device test case + + # PROTECTED REGION ID(SKALogger.test_SKALogger_decorators) ENABLED START # @pytest.mark.usefixtures("tango_context", "initialize_device") # PROTECTED REGION END # // SKALogger.test_SKALogger_decorators @@ -53,7 +48,7 @@ class TestSKALogger(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKALogger.test_State) ENABLED START # - assert tango_context.device.State() == DevState.UNKNOWN + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKALogger.test_State # PROTECTED REGION ID(SKALogger.test_Status_decorators) ENABLED START # @@ -61,7 +56,7 @@ class TestSKALogger(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKALogger.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in UNKNOWN state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKALogger.test_Status # PROTECTED REGION ID(SKALogger.test_SetLoggingLevel_decorators) ENABLED START # @@ -100,14 +95,6 @@ class TestSKALogger(object): assert (re.match(versionPattern, versionInfo[0])) is not None # PROTECTED REGION END # // SKALogger.test_GetVersionInfo - # PROTECTED REGION ID(SKALogger.test_Reset_decorators) ENABLED START # - # PROTECTED REGION END # // SKALogger.test_Reset_decorators - def test_Reset(self, tango_context): - """Test for Reset""" - # PROTECTED REGION ID(SKALogger.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None - # PROTECTED REGION END # // SKALogger.test_Reset - # PROTECTED REGION ID(SKALogger.test_buildState_decorators) ENABLED START # # PROTECTED REGION END # // SKALogger.test_buildState_decorators def test_buildState(self, tango_context): @@ -149,7 +136,7 @@ class TestSKALogger(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKALogger.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE # PROTECTED REGION END # // SKALogger.test_adminMode # PROTECTED REGION ID(SKALogger.test_controlMode_decorators) ENABLED START # diff --git a/tests/test_master_device.py b/tests/test_master_device.py index a3c4584f..c775e4b9 100644 --- a/tests/test_master_device.py +++ b/tests/test_master_device.py @@ -8,11 +8,6 @@ ######################################################################################### """Contain the tests for the SKAMaster.""" -# Standard imports -import sys -import os - -# Imports import re import pytest from tango import DevState @@ -20,7 +15,8 @@ from tango import DevState # PROTECTED REGION ID(SKAMaster.test_additional_imports) ENABLED START # from ska.base.control_model import AdminMode, ControlMode, HealthState, SimulationMode, TestMode # PROTECTED REGION END # // SKAMaster.test_additional_imports -# Device test case + + # PROTECTED REGION ID(SKAMaster.test_SKAMaster_decorators) ENABLED START # @pytest.mark.usefixtures("tango_context") # PROTECTED REGION END # // SKAMaster.test_SKAMaster_decorators @@ -56,7 +52,7 @@ class TestSKAMaster(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKAMaster.test_State) ENABLED START # - assert tango_context.device.State() == DevState.UNKNOWN + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKAMaster.test_State # PROTECTED REGION ID(SKAMaster.test_Status_decorators) ENABLED START # @@ -64,7 +60,7 @@ class TestSKAMaster(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKAMaster.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in UNKNOWN state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKAMaster.test_Status # PROTECTED REGION ID(SKAMaster.test_GetVersionInfo_decorators) ENABLED START # @@ -76,7 +72,7 @@ class TestSKAMaster(object): r'SKAMaster, lmcbaseclasses, [0-9].[0-9].[0-9], ' r'A set of generic base devices for SKA Telescope.') versionInfo = tango_context.device.GetVersionInfo() - assert (re.match(versionPattern, versionInfo[0])) != None + assert (re.match(versionPattern, versionInfo[0])) is not None # PROTECTED REGION END # // SKAMaster.test_GetVersionInfo # PROTECTED REGION ID(SKAMaster.test_isCapabilityAchievable_failure_decorators) ENABLED START # @@ -95,15 +91,6 @@ class TestSKAMaster(object): assert tango_context.device.isCapabilityAchievable([[1], ['BAND1']]) is True # PROTECTED REGION END # // SKAMaster.test_isCapabilityAchievable_success - # PROTECTED REGION ID(SKAMaster.test_Reset_decorators) ENABLED START # - # PROTECTED REGION END # // SKAMaster.test_Reset_decorators - def test_Reset(self, tango_context): - """Test for Reset""" - # PROTECTED REGION ID(SKAMaster.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None - # PROTECTED REGION END # // SKAMaster.test_Reset - - # PROTECTED REGION ID(SKAMaster.test_elementLoggerAddress_decorators) ENABLED START # # PROTECTED REGION END # // SKAMaster.test_elementLoggerAddress_decorators def test_elementLoggerAddress(self, tango_context): @@ -144,7 +131,9 @@ class TestSKAMaster(object): buildPattern = re.compile( r'lmcbaseclasses, [0-9].[0-9].[0-9], ' r'A set of generic base devices for SKA Telescope') - assert (re.match(buildPattern, tango_context.device.buildState)) != None + assert ( + re.match(buildPattern, tango_context.device.buildState) + ) is not None # PROTECTED REGION END # // SKAMaster.test_buildState # PROTECTED REGION ID(SKAMaster.test_versionId_decorators) ENABLED START # @@ -153,7 +142,9 @@ class TestSKAMaster(object): """Test for versionId""" # PROTECTED REGION ID(SKAMaster.test_versionId) ENABLED START # versionIdPattern = re.compile(r'[0-9].[0-9].[0-9]') - assert (re.match(versionIdPattern, tango_context.device.versionId)) != None + assert ( + re.match(versionIdPattern, tango_context.device.versionId) + ) is not None # PROTECTED REGION END # // SKAMaster.test_versionId # PROTECTED REGION ID(SKAMaster.test_healthState_decorators) ENABLED START # @@ -169,7 +160,7 @@ class TestSKAMaster(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKAMaster.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE # PROTECTED REGION END # // SKAMaster.test_adminMode # PROTECTED REGION ID(SKAMaster.test_controlMode_decorators) ENABLED START # diff --git a/tests/test_obs_device.py b/tests/test_obs_device.py index 2b36c09e..7d9d1d18 100644 --- a/tests/test_obs_device.py +++ b/tests/test_obs_device.py @@ -8,10 +8,6 @@ ######################################################################################### """Contain the tests for the SKAObsDevice.""" -# Standard imports -import sys -import os - # Imports import re import pytest @@ -23,6 +19,7 @@ from ska.base.control_model import ( ) # PROTECTED REGION END # // SKAObsDevice.test_additional_imports + # Device test case # PROTECTED REGION ID(SKAObsDevice.test_SKAObsDevice_decorators) ENABLED START # @pytest.mark.usefixtures("tango_context", "initialize_device") @@ -55,7 +52,7 @@ class TestSKAObsDevice(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKAObsDevice.test_State) ENABLED START # - assert tango_context.device.State() == DevState.UNKNOWN + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKAObsDevice.test_State # PROTECTED REGION ID(SKAObsDevice.test_Status_decorators) ENABLED START # @@ -63,7 +60,7 @@ class TestSKAObsDevice(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKAObsDevice.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in UNKNOWN state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKAObsDevice.test_Status # PROTECTED REGION ID(SKAObsDevice.test_GetVersionInfo_decorators) ENABLED START # @@ -78,20 +75,12 @@ class TestSKAObsDevice(object): assert (re.match(versionPattern, versionInfo[0])) is not None # PROTECTED REGION END # // SKAObsDevice.test_GetVersionInfo - # PROTECTED REGION ID(SKAObsDevice.test_Reset_decorators) ENABLED START # - # PROTECTED REGION END # // SKAObsDevice.test_Reset_decorators - def test_Reset(self, tango_context): - """Test for Reset""" - # PROTECTED REGION ID(SKAObsDevice.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None - # PROTECTED REGION END # // SKAObsDevice.test_Reset - # PROTECTED REGION ID(SKAObsDevice.test_obsState_decorators) ENABLED START # # PROTECTED REGION END # // SKAObsDevice.test_obsState_decorators def test_obsState(self, tango_context): """Test for obsState""" # PROTECTED REGION ID(SKAObsDevice.test_obsState) ENABLED START # - assert tango_context.device.obsState == ObsState.IDLE + assert tango_context.device.obsState == ObsState.EMPTY # PROTECTED REGION END # // SKAObsDevice.test_obsState # PROTECTED REGION ID(SKAObsDevice.test_obsMode_decorators) ENABLED START # @@ -126,7 +115,7 @@ class TestSKAObsDevice(object): buildPattern = re.compile( r'lmcbaseclasses, [0-9].[0-9].[0-9], ' r'A set of generic base devices for SKA Telescope') - assert (re.match(buildPattern, tango_context.device.buildState)) != None + assert (re.match(buildPattern, tango_context.device.buildState)) is not None # PROTECTED REGION END # // SKAObsDevice.test_buildState # PROTECTED REGION ID(SKAObsDevice.test_versionId_decorators) ENABLED START # @@ -135,7 +124,7 @@ class TestSKAObsDevice(object): """Test for versionId""" # PROTECTED REGION ID(SKAObsDevice.test_versionId) ENABLED START # versionIdPattern = re.compile(r'[0-9].[0-9].[0-9]') - assert (re.match(versionIdPattern, tango_context.device.versionId)) != None + assert (re.match(versionIdPattern, tango_context.device.versionId)) is not None # PROTECTED REGION END # // SKAObsDevice.test_versionId # PROTECTED REGION ID(SKAObsDevice.test_healthState_decorators) ENABLED START # @@ -151,7 +140,7 @@ class TestSKAObsDevice(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKAObsDevice.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE # PROTECTED REGION END # // SKAObsDevice.test_adminMode # PROTECTED REGION ID(SKAObsDevice.test_controlMode_decorators) ENABLED START # diff --git a/tests/test_subarray_device.py b/tests/test_subarray_device.py index 9376dabd..9058e73d 100644 --- a/tests/test_subarray_device.py +++ b/tests/test_subarray_device.py @@ -8,24 +8,23 @@ ######################################################################################### """Contain the tests for the SKASubarray.""" -# Standard imports -import sys -import os - -# Imports +import itertools import re import pytest -from tango import DevState, DevSource + +from tango import DevState, DevSource, DevFailed # PROTECTED REGION ID(SKASubarray.test_additional_imports) ENABLED START # +from ska.base import SKASubarray, SKASubarrayResourceManager +from ska.base.commands import ResultCode from ska.base.control_model import ( AdminMode, ControlMode, HealthState, ObsMode, ObsState, SimulationMode, TestMode ) +from ska.base.faults import CommandError # PROTECTED REGION END # // SKASubarray.test_additional_imports -# Device test case -# PROTECTED REGION ID(SKASubarray.test_SKASubarray_decorators) ENABLED START # + + @pytest.mark.usefixtures("tango_context", "initialize_device") -# PROTECTED REGION END # // SKASubarray.test_SKASubarray_decorators class TestSKASubarray(object): """Test case for packet generation.""" @@ -56,49 +55,29 @@ class TestSKASubarray(object): def test_Abort(self, tango_context): """Test for Abort""" # PROTECTED REGION ID(SKASubarray.test_Abort) ENABLED START # - assert tango_context.device.Abort() is None + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + assert tango_context.device.Abort() == [ + [ResultCode.OK], ["Abort command completed OK"] + ] # PROTECTED REGION END # // SKASubarray.test_Abort - # PROTECTED REGION ID(SKASubarray.test_ConfigureCapability_decorators) ENABLED START # - # PROTECTED REGION END # // SKASubarray.test_ConfigureCapability_decorators - def test_ConfigureCapability(self, tango_context): - """Test for ConfigureCapability""" - # PROTECTED REGION ID(SKASubarray.test_ConfigureCapability) ENABLED START # - tango_context.device.adminMode = AdminMode.ONLINE - tango_context.device.AssignResources(["BAND1"]) - tango_context.device.ConfigureCapability([[2], ["BAND1"]]) - # The obsState attribute is changed by ConfigureCapability, but + # PROTECTED REGION ID(SKASubarray.test_Configure_decorators) ENABLED START # + # PROTECTED REGION END # // SKASubarray.test_Configure_decorators + def test_Configure(self, tango_context): + """Test for Configure""" + # PROTECTED REGION ID(SKASubarray.test_Configure) ENABLED START # + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + # The obsState attribute is changed by Configure, but # as it is a polled attribute the value in the cache may be stale, # so change source to ensure we read directly from the device tango_context.device.set_source(DevSource.DEV) assert tango_context.device.obsState == ObsState.READY - assert tango_context.device.configuredCapabilities == ("BAND1:2", ) - # PROTECTED REGION END # // SKASubarray.test_ConfigureCapability - - # PROTECTED REGION ID(SKASubarray.test_DeconfigureAllCapabilities_decorators) ENABLED START # - # PROTECTED REGION END # // SKASubarray.test_DeconfigureAllCapabilities_decorators - def test_DeconfigureAllCapabilities(self, tango_context): - """Test for DeconfigureAllCapabilities""" - # PROTECTED REGION ID(SKASubarray.test_DeconfigureAllCapabilities) ENABLED START # - tango_context.device.adminMode = AdminMode.ONLINE - tango_context.device.AssignResources(["BAND1"]) - tango_context.device.ConfigureCapability([[3], ["BAND1"]]) - tango_context.device.DeconfigureAllCapabilities("BAND1") - assert tango_context.device.configuredCapabilities == ("BAND1:0", ) - # PROTECTED REGION END # // SKASubarray.test_DeconfigureAllCapabilities - - # TODO: Fix the test case. - # PROTECTED REGION ID(SKASubarray.test_DeconfigureCapability_decorators) ENABLED START # - # PROTECTED REGION END # // SKASubarray.test_DeconfigureCapability_decorators - def test_DeconfigureCapability(self, tango_context): - """Test for DeconfigureCapability""" - # PROTECTED REGION ID(SKASubarray.test_DeconfigureCapability) ENABLED START # - tango_context.device.adminMode = AdminMode.ONLINE - tango_context.device.AssignResources(["BAND1"]) - tango_context.device.ConfigureCapability([[1], ["BAND1"]]) - tango_context.device.DeconfigureCapability([[1], ["BAND1"]]) - assert tango_context.device.configuredCapabilities == ("BAND1:0", ) - # PROTECTED REGION END # // SKASubarray.test_DeconfigureCapability + assert tango_context.device.configuredCapabilities == ("BAND1:2", "BAND2:0") + # PROTECTED REGION END # // SKASubarray.test_Configure # PROTECTED REGION ID(SKASubarray.test_GetVersionInfo_decorators) ENABLED START # # PROTECTED REGION END # // SKASubarray.test_GetVersionInfo_decorators @@ -117,7 +96,7 @@ class TestSKASubarray(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKASubarray.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in DISABLE state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKASubarray.test_Status # PROTECTED REGION ID(SKASubarray.test_State_decorators) ENABLED START # @@ -125,7 +104,7 @@ class TestSKASubarray(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKASubarray.test_State) ENABLED START # - assert tango_context.device.State() == DevState.DISABLE + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKASubarray.test_State # PROTECTED REGION ID(SKASubarray.test_AssignResources_decorators) ENABLED START # @@ -133,18 +112,26 @@ class TestSKASubarray(object): def test_AssignResources(self, tango_context): """Test for AssignResources""" # PROTECTED REGION ID(SKASubarray.test_AssignResources) ENABLED START # - tango_context.device.AssignResources(['BAND1', 'BAND2']) - assert tango_context.device.State() == DevState.ON and \ - tango_context.device.assignedResources == ('BAND1', 'BAND2') + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}') + assert tango_context.device.State() == DevState.ON + assert tango_context.device.assignedResources == ('BAND1', 'BAND2') tango_context.device.ReleaseAllResources() + with pytest.raises(DevFailed): + tango_context.device.AssignResources('Invalid JSON') # PROTECTED REGION END # // SKASubarray.test_AssignResources # PROTECTED REGION ID(SKASubarray.test_EndSB_decorators) ENABLED START # # PROTECTED REGION END # // SKASubarray.test_EndSB_decorators - def test_EndSB(self, tango_context): + def test_End(self, tango_context): """Test for EndSB""" # PROTECTED REGION ID(SKASubarray.test_EndSB) ENABLED START # - assert tango_context.device.EndSB() is None + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + assert tango_context.device.End() == [ + [ResultCode.OK], ["End command completed OK"] + ] # PROTECTED REGION END # // SKASubarray.test_EndSB # PROTECTED REGION ID(SKASubarray.test_EndScan_decorators) ENABLED START # @@ -152,24 +139,23 @@ class TestSKASubarray(object): def test_EndScan(self, tango_context): """Test for EndScan""" # PROTECTED REGION ID(SKASubarray.test_EndScan) ENABLED START # - assert tango_context.device.EndScan() is None + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + tango_context.device.Scan('{"id": 123}') + assert tango_context.device.EndScan() == [ + [ResultCode.OK], ["EndScan command completed OK"] + ] # PROTECTED REGION END # // SKASubarray.test_EndScan - # PROTECTED REGION ID(SKASubarray.test_Pause_decorators) ENABLED START # - # PROTECTED REGION END # // SKASubarray.test_Pause_decorators - def test_Pause(self, tango_context): - """Test for Pause""" - # PROTECTED REGION ID(SKASubarray.test_Pause) ENABLED START # - assert tango_context.device.Pause() is None - # PROTECTED REGION END # // SKASubarray.test_Pause - # PROTECTED REGION ID(SKASubarray.test_ReleaseAllResources_decorators) ENABLED START # # PROTECTED REGION END # // SKASubarray.test_ReleaseAllResources_decorators def test_ReleaseAllResources(self, tango_context): """Test for ReleaseAllResources""" # PROTECTED REGION ID(SKASubarray.test_ReleaseAllResources) ENABLED START # # assert tango_context.device.ReleaseAllResources() == [""] - tango_context.device.AssignResources(['BAND1', 'BAND2']) + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}') tango_context.device.ReleaseAllResources() assert tango_context.device.assignedResources is None # PROTECTED REGION END # // SKASubarray.test_ReleaseAllResources @@ -179,9 +165,9 @@ class TestSKASubarray(object): def test_ReleaseResources(self, tango_context): """Test for ReleaseResources""" # PROTECTED REGION ID(SKASubarray.test_ReleaseResources) ENABLED START # - # assert tango_context.device.ReleaseResources([""]) == [""] - tango_context.device.AssignResources(['BAND1', 'BAND2']) - tango_context.device.ReleaseResources(['BAND1']) + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}') + tango_context.device.ReleaseResources('{"example": ["BAND1"]}') assert tango_context.device.State() == DevState.ON and\ tango_context.device.assignedResources == ('BAND2',) tango_context.device.ReleaseAllResources() @@ -189,26 +175,32 @@ class TestSKASubarray(object): # PROTECTED REGION ID(SKASubarray.test_Reset_decorators) ENABLED START # # PROTECTED REGION END # // SKASubarray.test_Reset_decorators - def test_Reset(self, tango_context): + def test_ObsReset(self, tango_context): """Test for Reset""" # PROTECTED REGION ID(SKASubarray.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + tango_context.device.Abort() + assert tango_context.device.ObsReset() == [ + [ResultCode.OK], ["ObsReset command completed OK"] + ] # PROTECTED REGION END # // SKASubarray.test_Reset - # PROTECTED REGION ID(SKASubarray.test_Resume_decorators) ENABLED START # - # PROTECTED REGION END # // SKASubarray.test_Resume_decorators - def test_Resume(self, tango_context): - """Test for Resume""" - # PROTECTED REGION ID(SKASubarray.test_Resume) ENABLED START # - assert tango_context.device.Resume() is None - # PROTECTED REGION END # // SKASubarray.test_Resume - # PROTECTED REGION ID(SKASubarray.test_Scan_decorators) ENABLED START # # PROTECTED REGION END # // SKASubarray.test_Scan_decorators def test_Scan(self, tango_context): """Test for Scan""" # PROTECTED REGION ID(SKASubarray.test_Scan) ENABLED START # - assert tango_context.device.Scan([""]) is None + tango_context.device.On() + tango_context.device.AssignResources('{"example": ["BAND1"]}') + tango_context.device.Configure('{"BAND1": 2}') + assert tango_context.device.Scan('{"id": 123}') == [ + [ResultCode.STARTED], ["Scan command STARTED - config {'id': 123}"] + ] + tango_context.device.EndScan() + with pytest.raises(DevFailed): + tango_context.device.Scan('Invalid JSON') # PROTECTED REGION END # // SKASubarray.test_Scan # PROTECTED REGION ID(SKASubarray.test_activationTime_decorators) ENABLED START # @@ -224,7 +216,11 @@ class TestSKASubarray(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKASubarray.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE + assert tango_context.device.state() == DevState.OFF + tango_context.device.adminMode = AdminMode.OFFLINE + assert tango_context.device.adminMode == AdminMode.OFFLINE + assert tango_context.device.state() == DevState.DISABLE # PROTECTED REGION END # // SKASubarray.test_adminMode # PROTECTED REGION ID(SKASubarray.test_buildState_decorators) ENABLED START # @@ -283,7 +279,7 @@ class TestSKASubarray(object): def test_obsState(self, tango_context): """Test for obsState""" # PROTECTED REGION ID(SKASubarray.test_obsState) ENABLED START # - assert tango_context.device.obsState == ObsState.IDLE + assert tango_context.device.obsState == ObsState.EMPTY # PROTECTED REGION END # // SKASubarray.test_obsState # PROTECTED REGION ID(SKASubarray.test_simulationMode_decorators) ENABLED START # @@ -316,7 +312,7 @@ class TestSKASubarray(object): def test_assignedResources(self, tango_context): """Test for assignedResources""" # PROTECTED REGION ID(SKASubarray.test_assignedResources) ENABLED START # - assert tango_context.device.assignedResources == None + assert tango_context.device.assignedResources is None # PROTECTED REGION END # // SKASubarray.test_assignedResources # PROTECTED REGION ID(SKASubarray.test_configuredCapabilities_decorators) ENABLED START # @@ -324,5 +320,281 @@ class TestSKASubarray(object): def test_configuredCapabilities(self, tango_context): """Test for configuredCapabilities""" # PROTECTED REGION ID(SKASubarray.test_configuredCapabilities) ENABLED START # - assert tango_context.device.configuredCapabilities == ("BAND1:0", ) + assert tango_context.device.configuredCapabilities == ("BAND1:0", "BAND2:0") # PROTECTED REGION END # // SKASubarray.test_configuredCapabilities + + @pytest.mark.parametrize( + 'state_under_test, action_under_test', + itertools.product( + [ + # not testing FAULT or OBSFAULT states because in the current + # implementation the interface cannot be used to get the device + # into these states + "DISABLED", "OFF", "EMPTY", "IDLE", "READY", "SCANNING", + "ABORTED", + ], + [ + # not testing 'reset' action because in the current + # implementation the interface cannot be used to get the device + # into a state from which 'reset' is a valid action + "notfitted", "offline", "online", "maintenance", "on", "off", + "assign", "release", "release (all)", "releaseall", + "configure", "scan", "endscan", "end", "abort", "obsreset", + "restart"] + ) + ) + def test_state_machine(self, tango_context, + 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? + """ + + states = { + "DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.DISABLE, ObsState.EMPTY), + "FAULT": # not tested + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE, AdminMode.ONLINE, AdminMode.MAINTENANCE], + DevState.FAULT, ObsState.EMPTY), + "OFF": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.OFF, ObsState.EMPTY), + "EMPTY": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.EMPTY), + "IDLE": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.IDLE), + "READY": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.READY), + "SCANNING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.SCANNING), + "ABORTED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.ABORTED), + "OBSFAULT": # not tested + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.FAULT), + } + + def assert_state(state): + (admin_modes, dev_state, obs_state) = states[state] + assert tango_context.device.adminMode in admin_modes + assert tango_context.device.state() == dev_state + assert tango_context.device.obsState == obs_state + + actions = { + "notfitted": + lambda d: d.write_attribute("adminMode", AdminMode.NOT_FITTED), + "offline": + lambda d: d.write_attribute("adminMode", AdminMode.OFFLINE), + "online": + lambda d: d.write_attribute("adminMode", AdminMode.ONLINE), + "maintenance": + lambda d: d.write_attribute("adminMode", AdminMode.MAINTENANCE), + "on": + lambda d: d.On(), + "off": + lambda d: d.Off(), + "reset": + lambda d: d.Reset(), # not tested + "assign": + lambda d: d.AssignResources('{"example": ["BAND1", "BAND2"]}'), + "release": + lambda d: d.ReleaseResources('{"example": ["BAND1"]}'), + "release (all)": + lambda d: d.ReleaseResources('{"example": ["BAND1", "BAND2"]}'), + "releaseall": + lambda d: d.ReleaseAllResources(), + "configure": + lambda d: d.Configure('{"BAND1": 2, "BAND2": 2}'), + "scan": + lambda d: d.Scan('{"id": 123}'), + "endscan": + lambda d: d.EndScan(), + "end": + lambda d: d.End(), + "abort": + lambda d: d.Abort(), + "obsreset": + lambda d: d.ObsReset(), + "restart": + lambda d: d.Restart(), + } + + def perform_action(action): + actions[action](tango_context.device) + + transitions = { + ("DISABLED", "notfitted"): "DISABLED", + ("DISABLED", "offline"): "DISABLED", + ("DISABLED", "online"): "OFF", + ("DISABLED", "maintenance"): "OFF", + ("OFF", "notfitted"): "DISABLED", + ("OFF", "offline"): "DISABLED", + ("OFF", "online"): "OFF", + ("OFF", "maintenance"): "OFF", + ("OFF", "on"): "EMPTY", + ("EMPTY", "off"): "OFF", + ("EMPTY", "assign"): "IDLE", + ("IDLE", "assign"): "IDLE", + ("IDLE", "release"): "IDLE", + ("IDLE", "release (all)"): "EMPTY", + ("IDLE", "releaseall"): "EMPTY", + ("IDLE", "configure"): "READY", + ("IDLE", "abort"): "ABORTED", + ("READY", "configure"): "READY", + ("READY", "end"): "IDLE", + ("READY", "abort"): "ABORTED", + ("READY", "scan"): "SCANNING", + ("SCANNING", "endscan"): "READY", + ("SCANNING", "abort"): "ABORTED", + ("ABORTED", "obsreset"): "IDLE", + ("ABORTED", "restart"): "EMPTY", + } + + setups = { + "DISABLED": + ['offline'], + "OFF": + [], + "EMPTY": + ['on'], + "IDLE": + ['on', 'assign'], + "READY": + ['on', 'assign', 'configure'], + "SCANNING": + ['on', 'assign', 'configure', 'scan'], + "ABORTED": + ['on', 'assign', 'abort'], + } + + # bypass cache for this test because we are testing for a change + # in the polled attribute obsState + tango_context.device.set_source(DevSource.DEV) + + state = "OFF" # debugging only + assert_state(state) # debugging only + + # Put the device into the state under test + for action in setups[state_under_test]: + perform_action(action) + state = transitions[state, action] # debugging only + assert_state(state) # 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 + 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(DevFailed): + perform_action(action_under_test) + assert_state(state_under_test) + + +@pytest.fixture +def resource_manager(): + yield SKASubarrayResourceManager() + + +class TestSKASubarrayResourceManager: + def test_ResourceManager_assign(self, resource_manager): + # create a resource manager and check that it is empty + assert not len(resource_manager) + assert resource_manager.get() == set() + + resource_manager.assign('{"example": ["A"]}') + assert len(resource_manager) == 1 + assert resource_manager.get() == set(["A"]) + + resource_manager.assign('{"example": ["A"]}') + assert len(resource_manager) == 1 + assert resource_manager.get() == set(["A"]) + + resource_manager.assign('{"example": ["A", "B"]}') + assert len(resource_manager) == 2 + assert resource_manager.get() == set(["A", "B"]) + + resource_manager.assign('{"example": ["A"]}') + assert len(resource_manager) == 2 + assert resource_manager.get() == set(["A", "B"]) + + resource_manager.assign('{"example": ["A", "C"]}') + assert len(resource_manager) == 3 + assert resource_manager.get() == set(["A", "B", "C"]) + + resource_manager.assign('{"example": ["D"]}') + assert len(resource_manager) == 4 + assert resource_manager.get() == set(["A", "B", "C", "D"]) + + def test_ResourceManager_release(self, resource_manager): + resource_manager = SKASubarrayResourceManager() + resource_manager.assign('{"example": ["A", "B", "C", "D"]}') + + # okay to release resources not assigned; does nothing + resource_manager.release('{"example": ["E"]}') + assert len(resource_manager) == 4 + assert resource_manager.get() == set(["A", "B", "C", "D"]) + + # check release does what it should + resource_manager.release('{"example": ["D"]}') + assert len(resource_manager) == 3 + assert resource_manager.get() == set(["A", "B", "C"]) + + # okay to release resources both assigned and not assigned + resource_manager.release('{"example": ["C", "D"]}') + assert len(resource_manager) == 2 + assert resource_manager.get() == set(["A", "B"]) + + # check release all does what it should + resource_manager.release_all() + assert len(resource_manager) == 0 + assert resource_manager.get() == set() + + # okay to call release_all when already empty + resource_manager.release_all() + assert len(resource_manager) == 0 + assert resource_manager.get() == set() + + +class TestSKASubarray_commands: + """ + This class contains tests of SKASubarray commands + """ + + def test_AssignCommand(self, resource_manager, state_model): + """ + Test for SKASubarray.AssignResourcesCommand + """ + assign_resources = SKASubarray.AssignResourcesCommand( + resource_manager, + state_model + ) + + # until the state_model is in the right state for it, the + # command's is_allowed() method will return False, and an + # attempt to call the command will raise a CommandError, and + # there will be no side-effect on the resource manager + for action in ["init_started", "init_succeeded", "on_succeeded"]: + assert not assign_resources.is_allowed() + with pytest.raises(CommandError): + assign_resources('{"example": ["foo"]}') + assert not len(resource_manager) + assert resource_manager.get() == set() + + state_model.perform_action(action) + + # now that the state_model is in the right state, is_allowed() + # should return True, and the command should succeed, and we + # should see the result in the resource manager + assert assign_resources.is_allowed() + assert assign_resources('{"example": ["foo"]}') == ( + ResultCode.OK, "AssignResources command completed OK" + ) + assert len(resource_manager) == 1 + assert resource_manager.get() == set(["foo"]) diff --git a/tests/test_subarray_state_model.py b/tests/test_subarray_state_model.py new file mode 100644 index 00000000..8c8d2f16 --- /dev/null +++ b/tests/test_subarray_state_model.py @@ -0,0 +1,273 @@ +######################################################################################### +# -*- coding: utf-8 -*- +# +# This file is part of the SKASubarray project +# +# +# +######################################################################################### +"""Contain the tests for the SKASubarray.""" + +import itertools +import pytest + +from tango import DevState + +from ska.base.control_model import AdminMode, ObsState +from ska.base.faults import StateModelError + + +class TestSKASubarrayStateModel(): + """ + Test cases for SKASubarrayStateModel. + """ + + @pytest.mark.parametrize( + 'state_under_test, action_under_test', + itertools.product( + ["UNINITIALISED", "INIT_ENABLED", "INIT_DISABLED", "FAULT_ENABLED", + "FAULT_DISABLED", "DISABLED", "OFF", "EMPTY", + "RESOURCING", "IDLE", "CONFIGURING", "READY", "SCANNING", + "ABORTING", "ABORTED", "OBSFAULT"], + ["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", "assign_started", + "resourcing_succeeded_no_resources", "resourcing_succeeded_some_resources", + "resourcing_failed", "release_started", "configure_started", + "configure_succeeded", "configure_failed", "scan_started", + "scan_succeeded", "scan_failed", "end_scan_succeeded", + "end_scan_failed", "abort_started", "abort_succeeded", + "abort_failed", "obs_reset_started", "obs_reset_succeeded", + "obs_reset_failed", "restart_started", "restart_succeeded", + "restart_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, None), + "FAULT_ENABLED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.FAULT, None), + "FAULT_DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.FAULT, None), + "INIT_ENABLED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.INIT, ObsState.EMPTY), + "INIT_DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.INIT, ObsState.EMPTY), + "DISABLED": + ([AdminMode.NOT_FITTED, AdminMode.OFFLINE], DevState.DISABLE, ObsState.EMPTY), + "OFF": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.OFF, ObsState.EMPTY), + "EMPTY": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.EMPTY), + "RESOURCING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.RESOURCING), + "IDLE": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.IDLE), + "CONFIGURING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.CONFIGURING), + "READY": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.READY), + "SCANNING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.SCANNING), + "ABORTING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.ABORTING), + "ABORTED": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.ABORTED), + "RESETTING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.RESETTING), + "RESTARTING": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.RESTARTING), + "OBSFAULT": + ([AdminMode.ONLINE, AdminMode.MAINTENANCE], DevState.ON, ObsState.FAULT), + } + + def assert_state(state): + (admin_modes, state, obs_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 + if obs_state is not None: + assert state_model.obs_state == obs_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'): "EMPTY", + ('OFF', 'on_failed'): "FAULT_ENABLED", + ('OFF', 'fatal_error'): "FAULT_ENABLED", + ('EMPTY', 'off_succeeded'): "OFF", + ('EMPTY', 'off_failed'): "FAULT_ENABLED", + ('EMPTY', 'assign_started'): "RESOURCING", + ('EMPTY', 'fatal_error'): "OBSFAULT", + ('RESOURCING', 'resourcing_succeeded_some_resources'): "IDLE", + ('RESOURCING', 'resourcing_succeeded_no_resources'): "EMPTY", + ('RESOURCING', 'resourcing_failed'): "OBSFAULT", + ('RESOURCING', 'fatal_error'): "OBSFAULT", + ('IDLE', 'assign_started'): "RESOURCING", + ('IDLE', 'release_started'): "RESOURCING", + ('IDLE', 'configure_started'): "CONFIGURING", + ('IDLE', 'abort_started'): "ABORTING", + ('IDLE', 'fatal_error'): "OBSFAULT", + ('CONFIGURING', 'configure_succeeded'): "READY", + ('CONFIGURING', 'configure_failed'): "OBSFAULT", + ('CONFIGURING', 'abort_started'): "ABORTING", + ('CONFIGURING', 'fatal_error'): "OBSFAULT", + ('READY', 'end_succeeded'): "IDLE", + ('READY', 'end_failed'): "OBSFAULT", + ('READY', 'configure_started'): "CONFIGURING", + ('READY', 'abort_started'): "ABORTING", + ('READY', 'scan_started'): "SCANNING", + ('READY', 'fatal_error'): "OBSFAULT", + ('SCANNING', 'scan_succeeded'): "READY", + ('SCANNING', 'scan_failed'): "OBSFAULT", + ('SCANNING', 'end_scan_succeeded'): "READY", + ('SCANNING', 'end_scan_failed'): "OBSFAULT", + ('SCANNING', 'abort_started'): "ABORTING", + ('SCANNING', 'fatal_error'): "OBSFAULT", + ('ABORTING', 'abort_succeeded'): "ABORTED", + ('ABORTING', 'abort_failed'): "OBSFAULT", + ('ABORTING', 'fatal_error'): "OBSFAULT", + ('ABORTED', 'obs_reset_started'): "RESETTING", + ('ABORTED', 'restart_started'): "RESTARTING", + ('ABORTED', 'fatal_error'): "OBSFAULT", + ('OBSFAULT', 'obs_reset_started'): "RESETTING", + ('OBSFAULT', 'restart_started'): "RESTARTING", + ('OBSFAULT', 'fatal_error'): "OBSFAULT", + ('RESETTING', 'obs_reset_succeeded'): "IDLE", + ('RESETTING', 'obs_reset_failed'): "OBSFAULT", + ('RESETTING', 'fatal_error'): "OBSFAULT", + ('RESTARTING', 'restart_succeeded'): "EMPTY", + ('RESTARTING', 'restart_failed'): "OBSFAULT", + ('RESTARTING', 'fatal_error'): "OBSFAULT", + } + + 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'], + "EMPTY": ['init_started', 'init_succeeded', 'on_succeeded'], + "RESOURCING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started' + ], + "IDLE": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources'], + "CONFIGURING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'configure_started' + ], + "READY": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'configure_started', 'configure_succeeded' + ], + "SCANNING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'configure_started', 'configure_succeeded', 'scan_started' + ], + "ABORTING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'abort_started' + ], + "ABORTED": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'abort_started', 'abort_succeeded' + ], + "OBSFAULT": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'fatal_error' + ], + "RESETTING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'abort_started', 'abort_succeeded', 'obs_reset_started' + ], + "RESTARTING": [ + 'init_started', 'init_succeeded', 'on_succeeded', + 'assign_started', 'resourcing_succeeded_some_resources', + 'abort_started', 'abort_succeeded', 'restart_started' + ], + } + + # 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_tel_state_device.py b/tests/test_tel_state_device.py index 5b696b0b..a5c5361f 100644 --- a/tests/test_tel_state_device.py +++ b/tests/test_tel_state_device.py @@ -8,11 +8,6 @@ ######################################################################################### """Contain the tests for the SKATelState.""" -# Standard imports -import sys -import os - -# Imports import re import pytest from tango import DevState @@ -20,7 +15,8 @@ from tango import DevState # PROTECTED REGION ID(SKATelState.test_additional_imports) ENABLED START # from ska.base.control_model import AdminMode, ControlMode, HealthState, SimulationMode, TestMode # PROTECTED REGION END # // SKATelState.test_additional_imports -# Device test case + + # PROTECTED REGION ID(SKATelState.test_SKATelState_decorators) ENABLED START # @pytest.mark.usefixtures("tango_context", "initialize_device") # PROTECTED REGION END # // SKATelState.test_SKATelState_decorators @@ -53,7 +49,7 @@ class TestSKATelState(object): def test_State(self, tango_context): """Test for State""" # PROTECTED REGION ID(SKATelState.test_State) ENABLED START # - assert tango_context.device.State() == DevState.UNKNOWN + assert tango_context.device.State() == DevState.OFF # PROTECTED REGION END # // SKATelState.test_State # PROTECTED REGION ID(SKATelState.test_Status_decorators) ENABLED START # @@ -61,7 +57,7 @@ class TestSKATelState(object): def test_Status(self, tango_context): """Test for Status""" # PROTECTED REGION ID(SKATelState.test_Status) ENABLED START # - assert tango_context.device.Status() == "The device is in UNKNOWN state." + assert tango_context.device.Status() == "The device is in OFF state." # PROTECTED REGION END # // SKATelState.test_Status # PROTECTED REGION ID(SKATelState.test_GetVersionInfo_decorators) ENABLED START # @@ -76,15 +72,6 @@ class TestSKATelState(object): assert (re.match(versionPattern, versionInfo[0])) is not None # PROTECTED REGION END # // SKATelState.test_GetVersionInfo - # PROTECTED REGION ID(SKATelState.test_Reset_decorators) ENABLED START # - # PROTECTED REGION END # // SKATelState.test_Reset_decorators - def test_Reset(self, tango_context): - """Test for Reset""" - # PROTECTED REGION ID(SKATelState.test_Reset) ENABLED START # - assert tango_context.device.Reset() is None - # PROTECTED REGION END # // SKATelState.test_Reset - - # PROTECTED REGION ID(SKATelState.test_buildState_decorators) ENABLED START # # PROTECTED REGION END # // SKATelState.test_buildState_decorators def test_buildState(self, tango_context): @@ -118,7 +105,7 @@ class TestSKATelState(object): def test_adminMode(self, tango_context): """Test for adminMode""" # PROTECTED REGION ID(SKATelState.test_adminMode) ENABLED START # - assert tango_context.device.adminMode == AdminMode.ONLINE + assert tango_context.device.adminMode == AdminMode.MAINTENANCE # PROTECTED REGION END # // SKATelState.test_adminMode # PROTECTED REGION ID(SKATelState.test_controlMode_decorators) ENABLED START # diff --git a/tests/test_utils.py b/tests/test_utils.py index c12e38fa..fb749b7f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -122,6 +122,7 @@ BAD_GROUP_KEYS = [ ('basic_no_subgroups', 'bk1_bad_keys', ), ] + def _jsonify_group_configs(group_configs): """Returns list of JSON definitions for groups.""" definitions = [] -- GitLab