diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py index 6fcfafa22411a2f46b3c077afb4dbd06bd9ee14f..07ca38de73d81269b30d4ace30a09341e0085793 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation.py @@ -6,88 +6,89 @@ # See LICENSE.txt for more info. # PyTango imports -from tango import DevState, AttrWriteType, DevString -from tango.server import Device, command, attribute +from tango import AttrWriteType +from tango.server import attribute +from tango import DevState + import numpy -from time import time from tangostationcontrol.common.entrypoint import entry -from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions -from tangostationcontrol.common.lofar_version import get_version -from tangostationcontrol.devices.device_decorators import only_in_states, only_when_on, fault_on_error - +from tangostationcontrol.common.lofar_logging import device_logging_to_python +from tangostationcontrol.common.lofar_logging import log_exceptions +from tangostationcontrol.devices.device_decorators import fault_on_error +from tangostationcontrol.devices.device_decorators import only_when_on +from tangostationcontrol.devices.device_decorators import only_in_states +from tangostationcontrol.devices.lofar_device import lofar_device + +from datetime import datetime from json import loads - +from time import time import logging + logger = logging.getLogger() __all__ = ["Observation", "main"] + @device_logging_to_python() -class Observation(Device): +class Observation(lofar_device): """ Observation Device for LOFAR2.0 - This Tango device is responsible for the set-up of hardware for a specific observation. It will, if necessary keep tabs on HW MPs to signal issues that are not caught by MPs being outside their nominal range. + This Tango device is responsible for the set-up of hardware for a + specific observation. It will, if necessary keep tabs on HW MPs to signal + issues that are not caught by MPs being outside their nominal range. The lifecycle of instances of this device is controlled by ObservationControl """ + # Attributes - version_R = attribute(dtype = str, access = AttrWriteType.READ, fget = lambda self: get_version()) - observation_running_R = attribute(dtype = numpy.float, access = AttrWriteType.READ, polling_period = 1000, period = 1000, rel_change = "1.0") - observation_id_R = attribute(dtype = numpy.int64, access = AttrWriteType.READ) - stop_time_R = attribute(dtype = numpy.float, access = AttrWriteType.READ) + observation_running_R = attribute(dtype=numpy.float, access=AttrWriteType.READ, polling_period=1000, period=1000, + rel_change="1.0") + observation_id_R = attribute(dtype=numpy.int64, access=AttrWriteType.READ) + stop_time_R = attribute(dtype=numpy.float, access=AttrWriteType.READ) + + observation_settings_RW = attribute(dtype=str, access=AttrWriteType.READ_WRITE) - # Core functions - @log_exceptions() def init_device(self): - Device.init_device(self) - self.set_state(DevState.OFF) + """Setup some class member variables for observation state""" + + super().init_device() + self._observation_settings = loads("{}") self._observation_id = -1 - self._stop_time = 0.0 + self._stop_time = datetime.now() + + def configure_for_initialise(self): + """Load the JSON from the attribute and configure member variables""" + + super().configure_for_initialise() - @log_exceptions() - def delete_device(self): - """Hook to delete resources allocated in init_device. - This method allows for any memory or other resources - allocated in the init_device method to be released. - This method is called by the device destructor and by - the device Init command (a Tango built-in). - """ - logger.debug("Shutting down...") - if self.get_state() != DevState.OFF: - self.Off() - logger.debug("Shut down. Good bye.") - - # Lifecycle functions - @command(dtype_in = DevString) - @only_in_states([DevState.OFF]) - @log_exceptions() - def Initialise(self, parameters: DevString = None): - self.set_state(DevState.INIT) # ObservationControl takes already good care of checking that the # parameters are in order and sufficient. It is therefore unnecessary # at the moment to check the parameters here again. # This could change when the parameter check becomes depending on # certain aspects that only an Observation device can know. - self.observation_parameters = loads(parameters) + parameters = loads(self._observation_settings) - self._observation_id = int(self.observation_parameters.get("id")) - self._stop_time = float(self.observation_parameters.get("stop_time")) - self.set_state(DevState.STANDBY) - logger.info("The observation with ID={} is configured. It will begin as soon as On() is called and it is supposed to stop at {}.".format(self._observation_id, self._stop_time)) + self._observation_id = parameters["observation_id"] + self._stop_time = datetime.fromisoformat(parameters["stop_time"]) - @command() - @only_in_states([DevState.STANDBY]) - @log_exceptions() - def On(self): - self.set_state(DevState.ON) - logger.info("Started the observation with ID={}.".format(self._observation_id)) + logger.info( + f"The observation with ID={self._observation_id} is " + "configured. It will begin as soon as On() is called and it is" + f"supposed to stop at {self._stop_time}") - @command() - @log_exceptions() - def Off(self): - self.stop_polling(True) - self.set_state(DevState.OFF) - logger.info("Stopped the observation with ID={}.".format(self._observation_id)) + def configure_for_off(self): + """Indicate the observation has stopped""" + + super().configure_for_off() + + logger.info(f"Stopped the observation with ID={self._observation_id}.") + + def configure_for_on(self): + """Indicate the observation has started""" + + super().configure_for_on() + + logger.info(f"Started the observation with ID={self._observation_id}.") @only_when_on() @fault_on_error() @@ -101,13 +102,28 @@ class Observation(Device): @log_exceptions() def read_stop_time_R(self): """Return the stop_time_R attribute.""" - return self._stop_time + return self._stop_time.timestamp() + + @fault_on_error() + @log_exceptions() + def read_observation_settings_RW(self): + """Return current observation_parameters string""" + return self._observation_settings + + @only_in_states([DevState.OFF]) + @fault_on_error() + @log_exceptions() + def write_observation_settings_RW(self, parameters: str): + """No validation on configuring parameters as task of control device""" + self._observation_settings = parameters @only_when_on() @fault_on_error() @log_exceptions() def read_observation_running_R(self): """Return the observation_running_R attribute.""" + # TODO(Corne): Actually keep track of the running time and perform proper + # value return time() diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_control.py b/tangostationcontrol/tangostationcontrol/devices/observation_control.py index d6701bc38bf03074e0983f1dacf92d02f11676bb..cfb3f2f1785301c0eea96a3c65052000c7f3b1f9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation_control.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_control.py @@ -5,28 +5,30 @@ # Distributed under the terms of the APACHE license. # See LICENSE.txt for more info. -# PyTango imports +from datetime import datetime +from json import loads +import logging +import time + +import numpy from tango import Except, DevFailed, DevState, AttrWriteType, DebugIt, DeviceProxy, Util, DevBoolean, DevString from tango.server import Device, command, attribute from tango import EventType -import numpy -import time -from json import loads - from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions from tangostationcontrol.common.lofar_version import get_version -from tangostationcontrol.devices.device_decorators import only_in_states, only_when_on, fault_on_error +from tangostationcontrol.devices.device_decorators import only_when_on, fault_on_error +from tangostationcontrol.devices.lofar_device import lofar_device from tangostationcontrol.devices.observation import Observation -import logging logger = logging.getLogger() __all__ = ["ObservationControl", "main"] + @device_logging_to_python() -class ObservationControl(Device): +class ObservationControl(lofar_device): """ Observation Control Device Server for LOFAR2.0 The ObservationControl Tango device controls the instantiation of a Tango Dynamic Device from the Observation class. ObservationControl then keeps a record of the Observation devices and if they are still alive. @@ -104,49 +106,12 @@ class ObservationControl(Device): # device's name. self.myTangoDomain = self.get_name().split('/')[0] - @log_exceptions() - @DebugIt() - def delete_device(self): - """Hook to delete resources allocated in init_device. - This method allows for any memory or other resources - allocated in the init_device method to be released. - This method is called by the device destructor and by - the device Init command (a Tango built-in). - """ - if self.get_state != DevState.OFF: - self.Off() - # Lifecycle functions - @command() - @only_in_states([DevState.OFF]) - @log_exceptions() - @DebugIt() - def Initialise(self): - self.set_state(DevState.INIT) + def configure_for_initialise(self): self.running_observations.clear() - self.set_state(DevState.STANDBY) - - @command() - @only_in_states([DevState.STANDBY]) - @log_exceptions() - @DebugIt() - def On(self): - self.set_state(DevState.ON) - @command() - @log_exceptions() - @DebugIt() - def Off(self): - if self.get_state() != DevState.OFF: - self.stop_all_observations() - self.set_state(DevState.OFF) - - @command() - @log_exceptions() - @DebugIt() - def Fault(self): + def configure_for_off(self): self.stop_all_observations() - self.set_state(DevState.FAULT) @only_when_on() @fault_on_error() @@ -212,17 +177,17 @@ class ObservationControl(Device): # Parameter check, do not execute an observation in case # the parameters are not sufficiently defined. - obs_id = int(parameter_dict.get("obs_id")) - stop_time = float(parameter_dict.get("stop_time")) - # TODO(Once ticket https://support.astron.nl/jira/browse/L2SS-254 is - # done, this needs to be replaced by a proper JSON verification - # against a schema.) + obs_id = int(parameter_dict["observation_id"]) + stop_datetime = datetime.fromisoformat(parameter_dict["stop_time"]) + # TODO(Jan David): Once ticket https://support.astron.nl/jira/browse/L2SS-254 + # is done, this needs to be replaced by a proper JSON + # verification against a schema. if obs_id is None or obs_id < 1: # Do not execute error = "Cannot start an observation with ID={} because the observation ID is invalid. The ID must be any integer >= 1.".format(obs_id) Except.throw_exception("IllegalCommand", error, __name__) - elif stop_time is None or stop_time <= time.time(): - error = "Cannot start an observation with ID={} because the parameter stop_time parameter value=\"{}\" is invalid. It needs to be expressed as the number of seconds since the Unix epoch.".format(obs_id, stop_time) + elif stop_datetime is None or stop_datetime <= datetime.now(): + error = "Cannot start an observation with ID={} because the parameter stop_time parameter value=\"{}\" is invalid. It needs to be expressed in ISO 8601 format.".format(obs_id, stop_datetime) Except.throw_exception("IllegalCommand", error, __name__) elif len(parameters) == 0: error = "Cannot start an observation with ID={} because the parameter set is empty.".format(obs_id) @@ -267,7 +232,7 @@ class ObservationControl(Device): # Store everything about the observation in this dict. I store this # dict at the end in self.running_observations. observation = {"parameters": self.check_and_convert_parameters(parameters)} - obs_id = int(observation["parameters"].get("obs_id")) + observation_id = observation['parameters']['observation_id'] # The class name of the Observation class is needed to create and # delete the device. @@ -275,14 +240,14 @@ class ObservationControl(Device): observation["class_name"] = class_name # Generate the Tango DB name for the Observation device. - device_name = "{}/{}/{}".format(self.myTangoDomain, class_name, obs_id) + device_name = f"{self.myTangoDomain}/{class_name}/{observation_id}" observation["device_name"] = device_name try: # Create the Observation device and instantiate it. self.create_dynamic_device(class_name, device_name) except DevFailed as ex: - error_string = "Cannot create the Observation device instance {} for ID={}. This means that the observation did not start.".format(device_name, obs_id) + error_string = "Cannot create the Observation device instance {} for ID={}. This means that the observation did not start.".format(device_name, observation_id) logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) @@ -291,10 +256,14 @@ class ObservationControl(Device): device_proxy = DeviceProxy(device_name) observation["device_proxy"] = device_proxy + # Configure the dynamic device its attribute for the observation + # parameters. + device_proxy.observation_settings_RW = parameters + # Take the Observation device through the motions. Pass the # entire JSON set of parameters so that it can pull from it what it # needs. - device_proxy.Initialise(parameters) + device_proxy.Initialise() # The call to On will actually tell the Observation device to # become fully active. @@ -302,7 +271,7 @@ class ObservationControl(Device): except DevFailed as ex: # Remove the device again. self.delete_dynamic_device(class_name, device_name) - error_string = "Cannot access the Observation device instance for observation ID={} with device class name={} and device instance name={}. This means that the observation likely did not start but certainly cannot be controlled and/or forcefully be stopped.".format(obs_id, class_name, device_name) + error_string = "Cannot access the Observation device instance for observation ID={} with device class name={} and device instance name={}. This means that the observation likely did not start but certainly cannot be controlled and/or forcefully be stopped.".format(observation_id, class_name, device_name) logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) @@ -324,19 +293,19 @@ class ObservationControl(Device): # event. And since the call back checks if the obs_id is in the dict # this triggers an error message if the ID is not already known. # There is no harm in copying the dict twice. - self.running_observations[obs_id] = observation + self.running_observations[observation_id] = observation - # Right. Now subscribe to periodic events. + # Right. Now subscribe to periodic events. event_id = device_proxy.subscribe_event(attribute_name.split('/')[-1], EventType.PERIODIC_EVENT, self.observation_running_callback) observation["event_id"] = event_id # Finally update the self.running_observation dict's entry of this # observation with the complete set of info. - self.running_observations[obs_id] = observation - logger.info("Successfully started an observation with ID={}.".format(obs_id)) + self.running_observations[observation_id] = observation + logger.info(f"Successfully started an observation with ID={observation_id}.") except DevFailed as ex: self.delete_dynamic_device(class_name, device_name) - error_string = "Cannot access the Observation device instance for observation ID={} with device class name={} and device instance name={}. This means that the observation cannot be controlled and/or forcefully be stopped.".format(obs_id, Observation.__name__, device_name) + error_string = "Cannot access the Observation device instance for observation ID={} with device class name={} and device instance name={}. This means that the observation cannot be controlled and/or forcefully be stopped.".format(observation_id, Observation.__name__, device_name) logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py index 47907110bd0be8576720b11aea8c9431f452ffdd..198a4cba29453a2e60665362edfa44b1cc52f63f 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_temperature_manager.py @@ -62,15 +62,11 @@ class TestDeviceTemperatureManager(AbstractTestBases.TestDeviceBase): devices = [DeviceProxy("STAT/SDP/1"), DeviceProxy("STAT/UNB2/1"), DeviceProxy("STAT/RECV/1"), DeviceProxy("STAT/APSCT/1"), DeviceProxy("STAT/APSPU/1"), DeviceProxy("STAT/PDU/1")] - # sleeping here to make sure we've dealt with any pre-existing events - time.sleep(2) - # make sure none of the devices are in the OFF state. Any other state is fine for dev in devices: if dev.state() == DevState.OFF: dev.initialise() - dev.on() - + # toggle the attribute to make sure we get a change event to True self.recv_proxy.HBAT_LED_on_RW = [[False] * 32] * 96 self.recv_proxy.HBAT_LED_on_RW = [[True] * 32] * 96 diff --git a/tangostationcontrol/tangostationcontrol/test/devices/device_base.py b/tangostationcontrol/tangostationcontrol/test/devices/device_base.py new file mode 100644 index 0000000000000000000000000000000000000000..85c8c908ba03a93a137b30a19b07208cec99094e --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/devices/device_base.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from tangostationcontrol.devices import lofar_device + +from tangostationcontrol.test import base + +import mock + + +class DeviceTestCase(base.TestCase): + """BaseClass for device test cases to perform common DeviceProxy patching + + Only to be used on devices inheriting lofar_device, use device_proxy_patch + to patch additional devices their proxy. + """ + + def setUp(self): + super(DeviceTestCase, self).setUp() + + # Patch DeviceProxy to allow making the proxies during initialisation + # that we otherwise avoid using + for device in [lofar_device]: + self.device_proxy_patch(device) + + def device_proxy_patch(self, device): + proxy_patcher = mock.patch.object( + device, 'DeviceProxy') + proxy_patcher.start() + self.addCleanup(proxy_patcher.stop) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py index c0e56a8439ce4fe244d8fd2c9b192205fe137b7a..b27c59e0f0deedd607398ad796b60550eea06e81 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py @@ -8,16 +8,16 @@ # See LICENSE.txt for more info. import numpy -import unittest -import mock from tango.test_context import DeviceTestContext -from tangostationcontrol.devices import antennafield, lofar_device +from tangostationcontrol.devices import antennafield from tangostationcontrol.devices.antennafield import HBATToRecvMapper from tangostationcontrol.test import base +from tangostationcontrol.test.devices import device_base -class TestHBATToRecvMapper(unittest.TestCase): + +class TestHBATToRecvMapper(base.TestCase): # A mapping where HBATs are all not mapped to power RCUs power_not_connected = [[-1, -1]] * 48 @@ -302,22 +302,16 @@ class TestHBATToRecvMapper(unittest.TestCase): actual = mapper.map_write("HBAT_PWR_on_RW", set_values) numpy.testing.assert_equal(expected, actual) -class TestAntennafieldDevice(base.TestCase): + +class TestAntennafieldDevice(device_base.DeviceTestCase): # some dummy values for mandatory properties at_properties = {'OPC_Server_Name': 'example.com', 'OPC_Server_Port': 4840, 'OPC_Time_Out': 5.0, 'Antenna_Field_Reference_ITRF' : [3.0, 3.0, 3.0], 'Antenna_Field_Reference_ETRS' : [7.0, 7.0, 7.0]} def setUp(self): - super(TestAntennafieldDevice, self).setUp() - - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) + # DeviceTestCase setUp patches lofar_device DeviceProxy + super(TestAntennafieldDevice, self).setUp() def test_read_Antenna_Field_Reference(self): """Verify if Antenna coordinates are correctly provided""" diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py index d79a22f31b24d954b7f672b83ffe29243064ae4f..7b84e6992d9dbe7f2c36db920ba18f53eb052e17 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py @@ -7,21 +7,11 @@ # Distributed under the terms of the APACHE license. # See LICENSE.txt for more info. -from tangostationcontrol.devices import tilebeam, lofar_device +from tangostationcontrol.test.devices import device_base -import mock -from tangostationcontrol.test import base - -class TestBeamDevice(base.TestCase): +class TestBeamDevice(device_base.DeviceTestCase): def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy super(TestBeamDevice, self).setUp() - - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [tilebeam, lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py b/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py index ea81159f1a5290f4e1eb80ad8b7ea8a79b52d387..ca54101ed14e8affee1dce064ea92bf5ba9065a9 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py @@ -7,34 +7,34 @@ # # Distributed under the terms of the APACHE license. # See LICENSE.txt for more info. -from tango.test_context import DeviceTestContext -from tangostationcontrol.devices import temperature_manager, lofar_device - -import mock -from tangostationcontrol.test import base +import time +from tango.test_context import DeviceTestContext +from tangostationcontrol.devices import temperature_manager +from tangostationcontrol.test.devices import device_base -class TestTemperatureManagerDevice(base.TestCase): +class TestTemperatureManagerDevice(device_base.DeviceTestCase): def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy super(TestTemperatureManagerDevice, self).setUp() - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [temperature_manager, lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) + # Patch additional devices using baseclass device_proxy_patch + for device in [temperature_manager]: + self.device_proxy_patch(device) def test_alarm(self): with DeviceTestContext(temperature_manager.TemperatureManager, process=True, timeout=10) as proxy: proxy.initialise() proxy.on() + # TODO(Corne): Remove this once race condition in is_alarming_R + # addressed. L2SS-816 + time.sleep(1) + self.assertFalse(proxy.is_alarming_R) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py index 15434810dd7bf9d3162ce64282661f3fa358b3de..d97cf7b9ebc6625cf9568fb2db24cab4edd51bc7 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py @@ -12,21 +12,14 @@ from tango.server import attribute from tangostationcontrol.devices import lofar_device -import mock +from tangostationcontrol.test.devices import device_base -from tangostationcontrol.test import base -class TestLofarDevice(base.TestCase): +class TestLofarDevice(device_base.DeviceTestCase): + def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy super(TestLofarDevice, self).setUp() - - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) def test_read_attribute(self): """ Test whether read_attribute really returns the attribute. """ diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py new file mode 100644 index 0000000000000000000000000000000000000000..f98a7c4754262e76cf3bb4d72630fcc3baf5f94a --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + + +class TestObservationBase: + + # TODO(Corne): Use this once working on L2SS-774 + VALID_JSON = ''' + { + "observation_id": 12345, + "stop_time": "2106-02-07T00:00:00", + "antenna_mask": [0,1,2,9], + "filter": "HBA_110_190", + "SAPs": [{ + "subbands": [10, 20, 30], + "pointing": { + "angle1": 1.5, "angle2": 0, "direction_type": "J2000" + } + }], + "tile_beam": + { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" }, + "first_beamlet": 0 + } + ''' diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py new file mode 100644 index 0000000000000000000000000000000000000000..93420364a8e8d28f2cea790a9be177f590a9ffea --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +import mock + +from tango.test_context import DeviceTestContext +from tango import DevState +from tango import DevFailed + +import json +from datetime import datetime +from datetime import timedelta + +from tangostationcontrol.devices import observation_control + +from tangostationcontrol.test import base +from tangostationcontrol.test.devices import test_observation_base + + +class TestObservationControlDevice(base.TestCase, test_observation_base.TestObservationBase): + + def setUp(self): + super(TestObservationControlDevice, self).setUp() + + def on_device_assert(self, proxy): + """Transition the device to ON and assert intermediate states""" + + proxy.Off() + self.assertEqual(DevState.OFF, proxy.state()) + proxy.Initialise() + self.assertEqual(DevState.STANDBY, proxy.state()) + proxy.On() + self.assertEqual(DevState.ON, proxy.state()) + + def mock_dynamic_devices(self): + observation_proxy = mock.patch.object( + observation_control, 'DeviceProxy') + self.m_observation_control = observation_proxy.start() + self.addCleanup(observation_proxy.stop) + + observation_dynamic_device = mock.patch.object( + observation_control.ObservationControl, 'create_dynamic_device', + autospec=True) + self.m_observation_dynamic_device = observation_dynamic_device.start() + self.addCleanup(observation_dynamic_device.stop) + + def test_device_on(self): + """Transition the ObservationControl device to ON state""" + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + def test_no_observation_running(self): + """Assert no current observations on fresh boot""" + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertFalse(proxy.is_any_observation_running()) + self.assertFalse(proxy.is_observation_running(12345)) + self.assertFalse(proxy.is_observation_running(54321)) + + def test_check_and_convert_parameters_invalid_id(self): + """Test invalid parameter detection""" + self.mock_dynamic_devices() + + parameters = json.loads(self.VALID_JSON) + parameters['observation_id'] = -1 + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertRaises( + DevFailed, proxy.start_observation, json.dumps(parameters)) + + def test_check_and_convert_parameters_invalid_time(self): + """Test invalid parameter detection""" + self.mock_dynamic_devices() + + parameters = json.loads(self.VALID_JSON) + parameters['stop_time'] = (datetime.now() - timedelta(seconds=1)).isoformat() + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertRaises( + DevFailed, proxy.start_observation, json.dumps(parameters)) + + def test_check_and_convert_parameters_invalid_empty(self): + """Test empty parameter detection""" + self.mock_dynamic_devices() + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertRaises( + DevFailed, proxy.start_observation, "{}") + + @mock.patch.object( + observation_control.ObservationControl, 'delete_dynamic_device') + def test_start_observation(self, m_delete_device): + """Test starting an observation""" + self.mock_dynamic_devices() + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + proxy.start_observation(self.VALID_JSON) + + self.assertTrue(proxy.is_any_observation_running()) + self.assertTrue(proxy.is_observation_running(12345)) + + @mock.patch.object( + observation_control.ObservationControl, 'delete_dynamic_device') + def test_start_observation_multiple(self, m_delete_device): + """Test starting multiple observations""" + self.mock_dynamic_devices() + + second_observation_json = json.loads(self.VALID_JSON) + second_observation_json['observation_id'] = 54321 + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + proxy.start_observation(self.VALID_JSON) + proxy.start_observation(json.dumps(second_observation_json)) + + self.assertTrue(proxy.is_any_observation_running()) + self.assertTrue(proxy.is_observation_running(12345)) + self.assertTrue(proxy.is_observation_running(54321)) + + def test_stop_observation_invalid_id(self): + """Test stop_observation exceptions for invalid ids""" + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertRaises(DevFailed, proxy.stop_observation, -1) + + def test_stop_observation_invalid_running(self): + """Test stop_observation exceptions for not running""" + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + self.assertRaises(DevFailed, proxy.stop_observation, 2) + + def test_is_any_observation_running_after_stop_all_observations(self): + """Test whether is_any_observation_running conforms when we start & stop an observation""" + self.mock_dynamic_devices() + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + proxy.start_observation(self.VALID_JSON) + proxy.stop_all_observations() + + # Test false + self.assertFalse(proxy.is_any_observation_running()) + + def test_start_stop_observation(self): + """Test starting and stopping an observation""" + self.mock_dynamic_devices() + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + # uses ID 12345 + proxy.start_observation(self.VALID_JSON) + proxy.stop_observation(12345) + + # Test false + self.assertFalse(proxy.is_observation_running(12345)) + + def test_start_multi_stop_all_observation(self): + """Test starting and stopping multiple observations""" + self.mock_dynamic_devices() + + second_observation_json = json.loads(self.VALID_JSON) + second_observation_json['observation_id'] = 54321 + + with DeviceTestContext(observation_control.ObservationControl, + process=True) as proxy: + self.on_device_assert(proxy) + + # uses ID 12345 + proxy.start_observation(self.VALID_JSON) + proxy.start_observation(json.dumps(second_observation_json)) + proxy.stop_all_observations() + + # Test false + self.assertFalse(proxy.is_observation_running(12345)) + self.assertFalse(proxy.is_observation_running(54321)) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py new file mode 100644 index 0000000000000000000000000000000000000000..1087fdd9f2fa927b7ca88867d246802c09ffc6aa --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from json import loads +from datetime import datetime + +from tango.test_context import DeviceTestContext +from tango import DevFailed +from tango import DevState + +from tangostationcontrol.devices import observation + +from tangostationcontrol.test.devices import device_base +from tangostationcontrol.test.devices import test_observation_base + + +class TestObservationDevice(device_base.DeviceTestCase, test_observation_base.TestObservationBase): + + def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy + super(TestObservationDevice, self).setUp() + + def test_init_valid(self): + """Initialize an observation with valid JSON""" + + with DeviceTestContext(observation.Observation, process=True) as proxy: + proxy.off() + proxy.observation_settings_RW = self.VALID_JSON + proxy.Initialise() + self.assertEqual(DevState.STANDBY, proxy.state()) + + def test_init_invalid(self): + """Initialize an observation with _invalid_ JSON""" + + with DeviceTestContext(observation.Observation, process=True) as proxy: + proxy.off() + proxy.observation_settings_RW = "{}" + proxy.Initialise() + self.assertEqual(DevState.FAULT, proxy.state()) + + def test_prohibit_rewriting_settings(self): + """Test that changing observation settings is disallowed once init""" + + with DeviceTestContext(observation.Observation, process=True) as proxy: + proxy.off() + proxy.observation_settings_RW = self.VALID_JSON + proxy.Initialise() + + with self.assertRaises(DevFailed): + proxy.write_attribute( + "observation_settings_RW", self.VALID_JSON) + + def test_attribute_match(self): + """Test that JSON data is exposed to attributes""" + + data = loads(self.VALID_JSON) + stop_timestamp = datetime.fromisoformat(data["stop_time"]).timestamp() + + with DeviceTestContext(observation.Observation, process=True) as proxy: + proxy.off() + proxy.observation_settings_RW = self.VALID_JSON + proxy.Initialise() + proxy.On() + + self.assertEqual(DevState.ON, proxy.state()) + self.assertEqual(stop_timestamp, proxy.stop_time_R) + self.assertEqual(data["observation_id"], proxy.observation_id_R) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py index 4ec6e4278e556f4617c433adb98b4e7d93917c4c..cece1ea75344d4f74543c332ca609d93000411ab 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py @@ -9,28 +9,21 @@ from tango.test_context import DeviceTestContext -from tangostationcontrol.devices import recv, lofar_device +from tangostationcontrol.devices import recv -import mock import numpy -from tangostationcontrol.test import base +from tangostationcontrol.test.devices import device_base -class TestRecvDevice(base.TestCase): + +class TestRecvDevice(device_base.DeviceTestCase): # some dummy values for mandatory properties recv_properties = {'OPC_Server_Name': 'example.com', 'OPC_Server_Port': 4840, 'OPC_Time_Out': 5.0} def setUp(self): - super(TestRecvDevice, self).setUp() - - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) + # DeviceTestCase setUp patches lofar_device DeviceProxy + super(TestRecvDevice, self).setUp() def test_calculate_HBAT_bf_delay_steps(self): """Verify HBAT beamforming calculations are correctly executed""" diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_snmp_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_snmp_device.py index 6289b2a33162031b01998aeeb84cc2119fd78860..23d81f6d5634b6e4cb1e7ca9ee935484efbdbcc7 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_snmp_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_snmp_device.py @@ -9,30 +9,22 @@ from tango.test_context import DeviceTestContext -from tangostationcontrol.devices import snmp_device, lofar_device +from tangostationcontrol.devices import snmp_device -import mock from os import path -from tangostationcontrol.test import base +from tangostationcontrol.test.devices import device_base -class TestSNMPDevice(base.TestCase): +class TestSNMPDevice(device_base.DeviceTestCase): # some dummy values for mandatory properties snmp_properties = {'SNMP_community': 'localhost', 'SNMP_host': 161, 'SNMP_rel_mib_dir': "SNMP_mib_loading", 'SNMP_timeout': 5.0} def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy super(TestSNMPDevice, self).setUp() - # Patch DeviceProxy to allow making the proxies during initialisation - # that we otherwise avoid using - for device in [lofar_device]: - proxy_patcher = mock.patch.object( - device, 'DeviceProxy') - proxy_patcher.start() - self.addCleanup(proxy_patcher.stop) - def test_get_mib_dir(self): with DeviceTestContext(snmp_device.SNMP, properties=self.snmp_properties, process=True) as proxy: