diff --git a/.pylintrc b/.pylintrc index b525115dfdbae35c577421c23b5d06fe6dd748f8..ddbbd9ecc51865295421a6d8a49fdfd06e0633de 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 159363b523554e5e4d3309b437f0a849c92efd69..05e9522e520fd7378d36f9906b34967bdedef8ed 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 cd8bc1314002d87e95ddb86444bfd61b4722a0be..aab1c1081d7b94f89247294ebae4bc66723b3d6f 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 0000000000000000000000000000000000000000..63c5d7100961e1c3cfa3970473916c840f4360d1 --- /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 4db79893338aa3fbdf320ff54c852e1896d79b03..93f320456ed91dd4d416971c7368675030a78213 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 Binary files /dev/null and b/docs/source/images/ADR-8.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index 68e1c1ff4a1ff5f049fe46fe154474b41d7f2609..adec635929c105702b9a76a53ba22effc83ddd0f 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 dd03cbdc27904a443c912d3e329ed0456cb5338b..a6813c57a0939f69dc2fe50e4c5e48efdb163804 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 ef62353029e7084f3431e35b36f4566ecaa5d3fb..a04e519cfccea1c30e0cf38d025f9edfbe4b1874 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 22dcbeae281cbccbdc66718538bf74ba738a9c6e..7ffc297955514b6c6f9c5207d4f2d7e94a609a32 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 d7c3d9d066122948e2fff94f51719c5c363cdc24..f5b5b10f65b4464193c8b2d5de6bc40760be2115 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 4849959f8ca42e2a6c19c5744f07817354d53322..6b37e806bf8709964adc1df9b04979fe1f688f59 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 a583296bea9b4436ff8c79cf26efc4ba76382619..866600078e08168344fe9f256666f1229bde28f7 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 fee6b291a6318210840eb132e1a1110bed7cad57..3622127756242079c7f33346303d1722c8daaee9 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 90b2540108106243177c391e5a49a40d4a8d3a05..0aebc36fa558f5f0a056b05f4c07f5b4cdb86fd3 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 4478f8e6a34147e695a30f5fac561273522b976d..cebd343850fee474796a5d3e1514374c0fa1dc40 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 2a05b700db001bf509c84c16306d78f5067d096a..848c479307980e2650f43f84c822a5847d47e9de 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 effa193670c5acd1f7adde2632dabf5612252300..63f9c63b21059ac4307104354eaeb83943248ea0 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 930905aaa781bf1ebd0b9a73fa67d8198a04e2fe..dedbb54f58ed57c29ca3a5ca5297741edf79f28a 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 0000000000000000000000000000000000000000..1d6fc20d3cc00bfb3bf5d4c85fe864708574d773 --- /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 36482485f8acb0dde6d322879a8e1324b7c60b42..52328e8ade3cb7d5c2ca2a476fd5bfd8e6e66ca0 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 0fb8cf06f5b2aa557fa09e0f00f77251a2b66b62..b25218b6ad9a3b7d2b76ad024c22cfd8236b1730 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 6f1113f88f2295e75c53770fc97016097c399495..72f03d2cf291bd62b2135e94257cbbe432d5a793 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 1b227c8a8026cd4cc7651c34944060cffeacd324..bc5038198b293844d4717febb1fbd06811c91be2 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 18f63d8997737156af3993a3696d22b6c2b7a57a..90e5d9834ad8e9dde56dfdb5ee899992b3b1fd6b 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 7ec0d525f8fca6ebf0d17f91571b76091239fc6e..f6b57c23af647733b1970b8af313b9070f0f7456 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 1b785c5833126318b97ac125fa986a6dc0ca0ac5..db105c86e02d60aa1beb4821a6c93e45564fed30 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 d9b3b0fd2cb2a60407d95b6d1870c08a23905771..c040e37e1173d62620665c0f2cad51377418d3f5 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 7a5b93d1e002a8c92976ac4c343aceffc1ee8727..a1fe8ca0abce482b70152fd1ff21df306e28c44a 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 ce97603d5261dec15e1c2444afc6b1661b88c958..da3a018dbc6ea1b04b50b49e2b5a649fa8d45a85 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 e82e28c94bea73a4ec669696848032107d9af129..b97224d71337a9300def21f93fadc8a30f3c1737 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 d220092b03409b966a7c9483b888fb67d36bbf71..37d37633ea1971c2a3570d036a5cc2791404a417 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 1f75b9beab08dfe04be1ddabe2b8581cd2adac78..a837f5532fa0edd8997c3b4fbd2c0a1658609ab3 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 cf794703480598836fed7a1a5708fdd3607fd8c6..d60e5038851971a4dcb15fe717f70a7615284cf8 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 a3c4584f291539d661e8fac7ad40a86878588ace..c775e4b99c61e463b131dc8ee99dc0ea16e234f2 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 2b36c09e45bd9fb6ad0f52c900703cea5e5c0f36..7d9d1d187e9bf7ff330b9315b95e8f7a7a2be7be 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 9376dabd29ee1e5af86917ef69cf41e16eacd05b..9058e73db2f2acd75e9fd918207522e231e7b296 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 0000000000000000000000000000000000000000..8c8d2f16ccc4e0fd7833ec380510968afbc0a44b --- /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 5b696b0b0664aa55f99ec18478f5010870dfefd1..a5c5361ffde0399206e9cff61c7892d7542b9b98 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 c12e38fa537d54b8124b3fa0d74b6971e2b4cc71..fb749b7f630cec318d74020ff6e5e5d236e87ccb 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 = []