diff --git a/README.md b/README.md index d4f408b4ea3fc7ebfad35dcef303669acc49b063..99c756984e8dfe60fd5ae159dd6a04898163e469 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,20 @@ Most notably, you will have web interfaces available at: # Development -For development you will need several dependencies including: +For development, you will need several dependencies including: ``` git g++ gcc make docker docker-compose shellcheck graphviz python3-dev \ python3-pip python3-tox libboost-python-dev libtango-cpp pkg-config ``` +Please source `setup.sh` file prior to developing to install any git hooks +and setup environment variables. + +```sh +source setup.sh +``` + Of these docker-compose must be at least 2.0 and Python 3.10 or higher. Alternatively, tox can be installed through pip using `pip install tox`. @@ -110,8 +117,10 @@ Next change the version in the following places: # Release Notes +* 0.12.1 Add `AbstractHierarchy` and `AbstractHierarchyDevice` classes and + functionality * 0.12.0 Add `Calibration_SDP_Subband_Weights_<XXX>MHz_R` attributes to implement HDF5 calibration tables -* 0.11.2 Fix sleep duration in archiver test +* 0.11.2 Fix sleep duration in archiver test * 0.11.1 Fix event unsubscription in TemperatureManager * 0.11.0 Added StationManager device * 0.10.0 Add `AntennaToSdpMapper` and fpga_sdp_info_* mapped attributes in `Antennafield` device diff --git a/setup.sh b/setup.sh index 23a98f323d7d01a18b7f341dc763eb1490095dab..ecab83f49583b812de428516c5b23a03ef9be651 100644 --- a/setup.sh +++ b/setup.sh @@ -2,53 +2,28 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -# Set up the LOFAR2.0 environment. -# For the time being it is assumend that the LOFAR2.0 environment has to -# co-exist with a LOFAR1 environment. +# Set up station control development -# Set these for the host where you run SKA's Tango Docker images. -# And export those directories for LOFAR in Tango Docker images. - -# Pass a directory as first parameter to this script. This will -# then be used as LOFAR20_DIR. Otherwise this file's directory -# be used to determine the tango directory location. +# This file's directory is used to determine the station control directory +# location. ABSOLUTE_PATH=$(realpath $(dirname ${BASH_SOURCE})) export LOFAR20_DIR=${1:-${ABSOLUTE_PATH}} +# Install git post-checkout hook upon next execution of git command +# git alias eventually automatically uninstalled if [ ! -f "${LOFAR20_DIR}/.git/hooks/post-checkout" ]; then alias git="cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-checkout; cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-merge; unalias git; git" fi # This needs to be modified for a development environment. -# In case you run multiple Docker networks on the same host in parallel, you need to specify a unique -# network name for each of them. +# In case you run multiple Docker networks on the same host in parallel, +# you need to specify a unique network name for each of them. export NETWORK_MODE=tangonet -# It is assumed that the Tango host, the computer that runs the TangoDB, is this host. -# If this is not true, then modify to the Tango host's FQDN and port. -# Example: export TANGO_HOST=station-xk25.astron.nl:10000 +# It is assumed that the Tango host, the computer that runs the TangoDB, +# is this host. If this is not true, then modify to the Tango host's FQDN and +# port. Example: export TANGO_HOST=station-xk25.astron.nl:10000 export TANGO_HOST=$(hostname):10000 -# -# NO MODIFICATION BEYOND THIS POINT! -# - -# Remove all LOFAR1 related environment modifications -function remove_lofar() -{ - tmp=${1//:/ } - echo "$(for new in $(for i in ${tmp}; do printf "%s\n" ${i}; done | egrep -v '/opt/lofar/|/opt/WinCC|/opt/stationtest|/opt/operations'); do printf "%s:" ${new}; done)" -} - -unset LOFARROOT -export PATH=$(remove_lofar ${PATH}) -export LD_LIBRARY_PATH=$(remove_lofar ${LD_LIBRARY_PATH}) -export PYTHON_PATH=$(remove_lofar ${PYTHON_PATH}) - - -# Allow read access for everybody to allow Docker the forwarding of X11. +# Allow read access for everybody to allow Docker forwarding of X11. chmod a+r ~/.Xauthority - -# Source the LOFAR2.0 Python3 venv if it exists. -[ -z ${VIRTUAL_ENV} ] && [ -d ${LOFAR20_DIR}/lofar2.0_venv ] && source ${LOFAR20_DIR}/lofar2.0_venv/bin/activate - diff --git a/tangostationcontrol/MANIFEST.in b/tangostationcontrol/MANIFEST.in index 389b2862f97c448983b3c1375ca819f201be1219..4f8b2d03b275d161613f418c6f50d8378462ac4f 100644 --- a/tangostationcontrol/MANIFEST.in +++ b/tangostationcontrol/MANIFEST.in @@ -1,3 +1,7 @@ include LICENSE include README.md include VERSION + +recursive-include docs * +recursive-exclude tangostationcontrol/test * +recursive-exclude tangostationcontrol/integration_test * diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index ac454c6a1fc3f05f60d3772b45f0b1a5db4b9f87..34a83616bb5aa9a70c5713bc45cd45498a50ba24 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.12.0 +0.12.1 diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg index 263ae8197e4f527226332aa7fb872398c1ad7c97..5ccd4a8ac711b068f986fd11c257ab5c7eb29380 100644 --- a/tangostationcontrol/setup.cfg +++ b/tangostationcontrol/setup.cfg @@ -21,8 +21,7 @@ classifier = Programming Language :: Python :: 3.10 [options] -package_dir = - =. +include_package_data = true packages = find: python_requires = >3.10 install_requires = @@ -44,7 +43,7 @@ console_scripts = l2ss-antennafield = tangostationcontrol.devices.antennafield:main l2ss-boot = tangostationcontrol.devices.boot:main l2ss-station-manager = tangostationcontrol.devices.station_manager:main - l2ss-docker = tangostationcontrol.devices.docker_device:main + l2ss-docker = tangostationcontrol.devices.docker:main l2ss-observation = tangostationcontrol.devices.observation:main l2ss-observationcontrol = tangostationcontrol.devices.observation_control:main l2ss-recv = tangostationcontrol.devices.recv:main @@ -54,7 +53,7 @@ console_scripts = l2ss-unb2 = tangostationcontrol.devices.unb2:main l2ss-xst = tangostationcontrol.devices.sdp.xst:main l2ss-temperaturemanager = tangostationcontrol.devices.temperature_manager:main - l2ss-configuration = tangostationcontrol.devices.configuration_device:main + l2ss-configuration = tangostationcontrol.devices.configuration:main # The following entry points should eventually be removed / replaced l2ss-hardware-device-template = tangostationcontrol.examples.HW_device_template:main diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index e34a51568dd7596926779bde271e05bead5a98b6..c6cde673ee2cba4933ccd070ea6f96a3ee9a16e0 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -55,7 +55,7 @@ from tangostationcontrol.common.lofar_logging import ( from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.common.type_checking import type_not_sequence from tangostationcontrol.devices.device_decorators import fault_on_error, only_in_states -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice from tangostationcontrol.devices.sdp.common import ( real_imag_to_weights, ) diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py index b172674746ffa8915147f45e3dc506cb09ff92d5..2d67c1e7292d1b54b761d49c32daf0c313324fb3 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apsct.py +++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py @@ -19,7 +19,7 @@ from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py index 24d83f5b9629534b9a390a48e25ca7345c83f7c6..03c19d94bf6d34ef00fb5174754907f28517a408 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apspu.py +++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py @@ -14,7 +14,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper from tangostationcontrol.common.constants import DEFAULT_POLLING_PERIOD from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py index 20c8eeb20fb213cb13dbbab08b9e75cfe040c455..6bc05990be595c2d3dcd3f63d1500d80a4136bf2 100644 --- a/tangostationcontrol/tangostationcontrol/devices/boot.py +++ b/tangostationcontrol/tangostationcontrol/devices/boot.py @@ -24,7 +24,7 @@ from tangostationcontrol.common.lofar_logging import ( ) from tangostationcontrol.common.states import OPERATIONAL_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/ccd.py b/tangostationcontrol/tangostationcontrol/devices/ccd.py index 2d5993c96e0aa0e637baf1b2031156b89ad188cb..b3d166180b1d2b70a664f0a0e01e7b869c0e4124 100644 --- a/tangostationcontrol/tangostationcontrol/devices/ccd.py +++ b/tangostationcontrol/tangostationcontrol/devices/ccd.py @@ -19,7 +19,7 @@ from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/devices/configuration_device.py b/tangostationcontrol/tangostationcontrol/devices/configuration.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/devices/configuration_device.py rename to tangostationcontrol/tangostationcontrol/devices/configuration.py index fc0dd1d0c44809812a67d4257f30aa437ab8896b..8fd7451a90bb6301b78dd2a1cf44c3c5995601b5 100644 --- a/tangostationcontrol/tangostationcontrol/devices/configuration_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/configuration.py @@ -22,7 +22,7 @@ from tangostationcontrol.common.lofar_logging import ( ) from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/docker_device.py b/tangostationcontrol/tangostationcontrol/devices/docker.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/devices/docker_device.py rename to tangostationcontrol/tangostationcontrol/devices/docker.py index 144cf34fc3db116d7f1bbf4773fd2de1b907ed11..3431d31f403aa86152ee3b213e5dba4d1e16a17d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/docker_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/docker.py @@ -21,7 +21,7 @@ from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/__init__.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tangostationcontrol/tangostationcontrol/devices/beam_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/devices/beam_device.py rename to tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py index b45d9b0e7890892c2f463bea4e22e7fce449fa38..542e7ef13b6540ec2cf5404dc0c5dec5c933ae05 100644 --- a/tangostationcontrol/tangostationcontrol/devices/beam_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py @@ -45,7 +45,7 @@ from tangostationcontrol.devices.device_decorators import ( only_in_states, fault_on_error, ) -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice __all__ = ["BeamDevice", "main", "BeamTracker"] diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..76527afeb8108db72ca21966b5f658a6e0e703d2 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +"""Clock Hierarchy for PyTango devices""" + +from typing import Dict +from typing import Optional + +from tango import DeviceProxy + +from tangostationcontrol.devices.interfaces.hierarchy_device import ( + AbstractHierarchyDevice, +) + + +class ClockHierarchyDevice(AbstractHierarchyDevice): + CLOCK_CHILD_PROPERTY = "clock_children" + CLOCK_PARENT_PROPERTY = "clock_parent" + + def init( + self, + device_name: str, + proxies: Optional[Dict[str, DeviceProxy]] = None, + ): + super().init( + device_name, self.CLOCK_CHILD_PROPERTY, self.CLOCK_PARENT_PROPERTY, proxies + ) diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py new file mode 100644 index 0000000000000000000000000000000000000000..a48a7b7f3286044af01d721619a0ac12beb90c39 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py @@ -0,0 +1,29 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +"""Control Hierarchy for PyTango devices""" +from typing import Dict +from typing import Optional + +from tango import DeviceProxy + +from tangostationcontrol.devices.interfaces.hierarchy_device import ( + AbstractHierarchyDevice, +) + + +class ControlHierarchyDevice(AbstractHierarchyDevice): + CONTROL_CHILD_PROPERTY = "control_children" + CONTROL_PARENT_PROPERTY = "control_parent" + + def init( + self, + device_name: str, + proxies: Optional[Dict[str, DeviceProxy]] = None, + ): + super().init( + device_name, + self.CONTROL_CHILD_PROPERTY, + self.CONTROL_PARENT_PROPERTY, + proxies, + ) diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..a62e8a4e3992291de7110b07882591db621f60cc --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py @@ -0,0 +1,206 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +"""Abstract Hierarchy for PyTango devices""" + +from abc import ABC +from typing import Dict +from typing import List +from typing import Union +from typing import Optional + +from tango import DeviceProxy +from tango import DevState + + +class AbstractHierarchy(ABC): + """AbstractHierarchy""" + + children_type = Dict[str, Dict[str, Union[DeviceProxy, "children_type"]]] + + def __init__( + self, + child_property_name: str, + children: List[str] = None, + parent: Optional[str] = None, + proxies: Optional[Dict[str, DeviceProxy]] = None, + ): + """Construct the hierarchy and provide protected access + + :param child_property_name: The name of the PyTango property to identify + children from your own direct (grand)children + :param children: Your direct children, if any, can be empty list + :param parent: Your direct parent, if any, can be None + :param proxies: Pass reference to DeviceProxy cache dictionary, if None + it will be created. This can be used to ensure single + DeviceProxy instances when devices implement _multiple_ + hierarchies. + """ + + self._child_property_name = child_property_name + self._children = {} + self._parent = None + + # Store proxies internally upon creation and only pass references to + # them. Ensures only single instance of DeviceProxy is created per + # device. + self._proxies = proxies or {} + + if parent: + self._parent = DeviceProxy(parent) + + if not children: + children = [] + for child in children: + self._children[child] = None + + def parent(self) -> Optional[str]: + """Return the parent device name if there is a parent + + :return: The device name of the parent or None + """ + + if not self._parent: + return None + + return self._parent.dev_name() + + def _get_or_create_proxy(self, device: str) -> DeviceProxy: + """Create a proxy if it does not yet exist otherwise pass reference + + :param device: full device name + :return: Reference to DeviceProxy from internal cache + """ + + if not self._proxies.get(device): + self._proxies[device] = DeviceProxy(device) + + return self._proxies[device] + + def _get_children(self, child: str, depth: int) -> children_type: + """Recursively create dict structure of DeviceProxy and children + + Built a depth-first recursive dict structure terminating at + :attr:`~depth` by reading the :attr:`~self._child_property` property + of each proxy. depth may be set to -1 for indefinite recursion + + Resulting datastructure of format + _children_ = { + device_string: { + 'proxy': DeviceProxy(device_string), + 'children': _children_ + }, + ... + } + + :warning: Makes no attempt to detect cycles in the tree and if they + exist will never terminate and consume infinite memory! + :param child: full device name string from the current child + :param depth: Maximum depth to recurse to, -1 for indefinite + :return: recursive datastructure of proxies and children as described + """ + + proxy = self._get_or_create_proxy(child) + + # TODO(Corne): Assert if this value changes even if the property + # has become persistent / immutable with the original value + # for the given device. If so resolve potential issues + children = proxy.get_property(self._child_property_name)[ + self._child_property_name + ] + + if len(children) == 0 or depth == 0: + return {"proxy": proxy, "children": {}} + + # Perform depth-first recursion to build tree of children and their + # children + proxies = {} + for child in children: + proxies[child] = self._get_children(child, depth - 1) + + return {"proxy": proxy, "children": proxies} + + def children(self, depth: int = 1) -> children_type: + """Retrieve DeviceProxies of children up to depth + + :param depth: Maximum steps of traversing children, -1 for unlimited + :raises tango.NonDbDevice: Raised if the child device does not exist in + the tango database + :raises tango.ConnectionFailed: Raised if connecting to the tango + database failed + :raises tango.CommunicationFailed: Raised if communication with the + tango database failed + :raises tango.DevFailed: Raised if the tango database encounters an + error + :return: Dict of DeviceProxies, children and grandchildren up to + :attr:`~depth` of recursive structure _children_ = { + device_string: { + 'proxy': DeviceProxy(device_string), + 'children': _children_ + }, + ... + } + """ + + children = {} + for child in self._children.keys(): + children[child] = self._get_children(child, depth - 1) + + return children + + def _child( + self, child_filter: str, children: children_type + ) -> Optional[DeviceProxy]: + """Recurse :attr:`~children` to find device and return it""" + + for name, data in children.items(): + if name == child_filter: + return data["proxy"] + + if not data["children"]: + continue + + result = self._child(child_filter, data["children"]) + if result: + return result + + def child(self, child_filter: str) -> Optional[DeviceProxy]: + """Retrieve DeviceProxy of child matching full name :attr:`~filter` + + :param child_filter: Full name of the device to retrieve DeviceProxy for + :raises tango.NonDbDevice: Raised if the child device does not exist in + the tango database + :raises tango.ConnectionFailed: Raised if connecting to the tango + database failed + :raises tango.CommunicationFailed: Raised if communication with the + tango database failed + :raises tango.DevFailed: Raised if the tango database encounters an + error + :return: Return DeviceProxy of child device or None if it does not exist + """ + return self._child(child_filter, self.children(depth=-1)) + + def read_attribute(self, attribute: str) -> any: + """Allow to read attribute from parent without direct access + + :param attribute: The attribute to read from the parent, can be RW + :return: The data from the attribute + :raises tango.DevFailed: The exception from the DeviceProxy if raised + """ + + if not self._parent: + return None + + return getattr(self._parent, attribute) + + def state(self) -> Optional[DevState]: + """Return the state of the parent without direct access + + :return: The state of the parent if there is one + :raises tango.DevFailed: The exception from the DeviceProxy if raised + """ + + if not self._parent: + return None + + return self._parent.state() diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py new file mode 100644 index 0000000000000000000000000000000000000000..3868f295507865a69bbec2b70e174e4561352622 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +"""Abstract Hierarchy Device for PyTango devices""" + +from typing import Dict +from typing import Optional +import logging + +from tango import Database +from tango import DeviceProxy +from tango import DevState + +from tangostationcontrol.devices.interfaces.hierarchy import AbstractHierarchy + +logger = logging.getLogger() + + +class AbstractHierarchyDevice: + """AbstractHierarchyDevice wraps AbstractHierarchy + + :warning: Do not actually use ABC to make this an abstract class as it will + cause conflicting metaclasses with PyTango Device servers + + See :py:class:`AbstractHierarchy` for implementation details, wrapping done + to prevent pollution of PyTango device namespace + """ + + def __init__(self): + self._hierarchy = None + + def init( + self, + device_name: str, + child_property: str, + parent_property: str, + proxies: Optional[Dict[str, DeviceProxy]] = None, + ): + """Initialize the hierarchy for the device + + :param device_name: Full name of device inheriting AbstractHierarchyDevice + :param child_property: name of the PyTango device property to identify children + :param parent_property: Name of the PyTango device property to identify the + parent + :param proxies: Optional shared dictionary of device proxies for when device + implements multiple hierarchies + :raises tango.ConnectionFailed: Raised if connecting to the tango database + failed + :raises tango.CommunicationFailed: Raised if communication with the + tango database failed + :raises tango.DevFailed: Raised if the tango database encounters an + error + """ + + db = Database() + children = db.get_device_property(device_name, child_property)[child_property] + parent = db.get_device_property(device_name, parent_property)[parent_property] + if len(parent) == 0: + parent = [None] + parent = parent[0] + + if not children and not parent: + logger.warning( + "Device: %s has empty hierarchy, both %s and %s properties are empty", + device_name, + child_property, + parent_property, + exc_info=True, + ) + + self._hierarchy = AbstractHierarchy(child_property, children, parent, proxies) + + def children(self, depth: int = 1) -> AbstractHierarchy.children_type: + return self._hierarchy(depth) + + def child(self, child_filter: str) -> Optional[DeviceProxy]: + return self._hierarchy.child(child_filter) + + def read_parent_attribute(self, attribute: str) -> any: + return self._hierarchy.read_attribute(attribute) + + def parent_state(self) -> DevState: + return self._hierarchy.state() diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/devices/lofar_device.py rename to tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py diff --git a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/devices/opcua_device.py rename to tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py index 11123502d477acd1c0a6a5182195addbd8ba4b04..c4e94dea83c1716258ba21c0bce28b602773e73d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py @@ -14,7 +14,7 @@ import numpy from tango.server import device_property, attribute from tangostationcontrol.clients.opcua_client import OPCUAConnection from tangostationcontrol.common.lofar_logging import log_exceptions -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..5e0458099acdfda2f23be5f1a885ebd73f502746 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +"""Power Hierarchy for PyTango devices""" + +from typing import Dict +from typing import Optional + +from tango import DeviceProxy + +from tangostationcontrol.devices.interfaces.hierarchy_device import ( + AbstractHierarchyDevice, +) + + +class PowerHierarchy(AbstractHierarchyDevice): + POWER_CHILD_PROPERTY = "power_children" + POWER_PARENT_PROPERTY = "power_parent" + + def init( + self, + device_name: str, + proxies: Optional[Dict[str, DeviceProxy]] = None, + ): + super().init( + device_name, self.POWER_CHILD_PROPERTY, self.POWER_PARENT_PROPERTY, proxies + ) diff --git a/tangostationcontrol/tangostationcontrol/devices/snmp_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/devices/snmp_device.py rename to tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py index a758f66b781701eaa6c5d0f7a6bc91296be2c74f..8ca5a9f9a434b4c0d8f73b05652fe97fcf1116d4 100644 --- a/tangostationcontrol/tangostationcontrol/devices/snmp_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py @@ -18,7 +18,7 @@ from tangostationcontrol.common.lofar_logging import ( ) # Additional import -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader")) diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py index 9ca844b2c0fdb0bbed7b68b2375ab1d676a9aa78..e94a99ce6eb8f3cc0f2bdb0b5c4de8e202691d62 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation.py @@ -24,7 +24,7 @@ from tangostationcontrol.configuration import ObservationSettings from tangostationcontrol.devices.device_decorators import fault_on_error from tangostationcontrol.devices.device_decorators import only_in_states from tangostationcontrol.devices.device_decorators import only_when_on -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_control.py b/tangostationcontrol/tangostationcontrol/devices/observation_control.py index 668e399f0034ca21a0c06ea879f7e93da79f11b8..cd3d23714d5d9da48f43d625fe36ae66bd242192 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation_control.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_control.py @@ -22,7 +22,7 @@ from tangostationcontrol.common.lofar_logging import ( ) from tangostationcontrol.configuration import ObservationSettings from tangostationcontrol.devices.device_decorators import only_when_on, fault_on_error -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice from tangostationcontrol.devices.observation import Observation logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/pcon.py b/tangostationcontrol/tangostationcontrol/devices/pcon.py index 6e92053b636965f93e7de06363a33a0dcf04d670..05ce01e3eac61fb1f8ff88c116efd70ebd647558 100644 --- a/tangostationcontrol/tangostationcontrol/devices/pcon.py +++ b/tangostationcontrol/tangostationcontrol/devices/pcon.py @@ -14,7 +14,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper # Additional import from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python -from tangostationcontrol.devices.snmp_device import SNMPDevice +from tangostationcontrol.devices.interfaces.snmp_device import SNMPDevice debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader")) diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py index 1fcbe70f1e9dae7577b44a0d47e235c143e3c706..a4a211e4e9cd15be366130c8462f5addf45851b1 100644 --- a/tangostationcontrol/tangostationcontrol/devices/psoc.py +++ b/tangostationcontrol/tangostationcontrol/devices/psoc.py @@ -20,7 +20,7 @@ from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) -from tangostationcontrol.devices.snmp_device import SNMPDevice +from tangostationcontrol.devices.interfaces.snmp_device import SNMPDevice debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader")) diff --git a/tangostationcontrol/tangostationcontrol/devices/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py index afbd472178e63938e31590bd77de0a4de19373cf..d2af727a02edf259928ba1701c02cf54101a176a 100644 --- a/tangostationcontrol/tangostationcontrol/devices/recv.py +++ b/tangostationcontrol/tangostationcontrol/devices/recv.py @@ -29,7 +29,7 @@ from tangostationcontrol.common.frequency_bands import bands from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py index bd24aeee8a5878b714babdfa914bd08d43d8f4ed..facb0d360798d1be0c0713ecf6b33c1690407c74 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py @@ -35,7 +35,7 @@ from tangostationcontrol.common.constants import ( # Additional import from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import log_exceptions -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice from tangostationcontrol.devices.sdp.common import ( phases_to_weights, ) diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py index ebde55278d4f7ce014731b91362e3655c47dc343..00fe2b9ec64f6b10c4259de3449ecb8ed769b247 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py @@ -26,7 +26,7 @@ from tangostationcontrol.common.constants import ( # Additional import from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import log_exceptions -from tangostationcontrol.devices.beam_device import BeamDevice +from tangostationcontrol.devices.interfaces.beam_device import BeamDevice from tangostationcontrol.devices.device_decorators import TimeIt logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py index 7de573ff163d6c70ebf4b49dc3eb9d371c3729b4..85dd8efd6b57bded79b9b06e080bb8af8c2a3298 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py @@ -28,7 +28,7 @@ from tangostationcontrol.common.constants import ( from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.devices.device_decorators import TimeIt -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice from tangostationcontrol.devices.sdp.common import subband_frequencies __all__ = ["SDP", "main"] diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py index f3cc0ab89923fc3c483f379a9ea63abc844fceed..ef278b0647bf71f53eae683452bc22c8ee87e8bf 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py @@ -17,7 +17,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper from tangostationcontrol.clients.statistics.client import StatisticsClient from tangostationcontrol.common.constants import MAX_ETH_FRAME_SIZE from tangostationcontrol.common.lofar_logging import log_exceptions -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/station_manager.py b/tangostationcontrol/tangostationcontrol/devices/station_manager.py index f4d80197c44e0c6cb7fd2d7e5fcd23882efe2425..09a7a3419ec36ef75349819fac9a63d496a5c077 100644 --- a/tangostationcontrol/tangostationcontrol/devices/station_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/station_manager.py @@ -5,6 +5,7 @@ """ +from enum import Enum import logging # pytango imports @@ -14,10 +15,9 @@ from tango import DebugIt # Additional import from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python -from tangostationcontrol.devices.lofar_device import LOFARDevice from tangostationcontrol.common.lofar_logging import log_exceptions +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice -from enum import Enum logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py index 35aca48310b97f6b8b0ba9d02cf010d7659b9e3d..b37ab02b58a3faf3932b3be4f2bcb5cf7e639ccf 100644 --- a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py @@ -26,7 +26,7 @@ from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/tilebeam.py b/tangostationcontrol/tangostationcontrol/devices/tilebeam.py index 7236fe1d840d6199e01572af77a7376f9aab6b93..0500ff91a9fafab45e31eb0d5cb90eed06d718c0 100644 --- a/tangostationcontrol/tangostationcontrol/devices/tilebeam.py +++ b/tangostationcontrol/tangostationcontrol/devices/tilebeam.py @@ -20,7 +20,7 @@ from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, ) -from tangostationcontrol.devices.beam_device import BeamDevice +from tangostationcontrol.devices.interfaces.beam_device import BeamDevice from tangostationcontrol.devices.device_decorators import TimeIt logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py index 2176be920f510caa4493b9b2afcf0dbc83e8817b..5196001ac7eb2ee15d71badd50731ab660c48f52 100644 --- a/tangostationcontrol/tangostationcontrol/devices/unb2.py +++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py @@ -22,7 +22,7 @@ from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.device_decorators import only_in_states -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py b/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py index 474584f35011de442ee28c6ccadc9c6548442717..b45f0634dfc69f4196f70b62902ab30bdae2b81b 100644 --- a/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py +++ b/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py @@ -5,7 +5,7 @@ import logging # PyTango imports from tango.server import run -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice # Additional import diff --git a/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py b/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py index 43f0c238b430fa59c675939dd89fdc0e9318403e..df65d1304e4fd794eb4ddd4c9d1dade724194839 100644 --- a/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py +++ b/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py @@ -12,7 +12,7 @@ from tango.server import run # Additional import from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper -from tangostationcontrol.devices.lofar_device import LOFARDevice +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice from tangostationcontrol.examples.load_from_disk.ini_client import IniClient logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py index 4b1192ce90fc958cb105662d4dd5ab1155a424ba..9db86f35b0a00d3fd02c8f7b462cc653fc72c4b3 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from tango._tango import DevState -from tangostationcontrol.devices.opcua_device import OPCUADevice +from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice from tangostationcontrol.integration_test import base from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy diff --git a/tangostationcontrol/tangostationcontrol/test/README.md b/tangostationcontrol/tangostationcontrol/test/README.md index 56b174d88e533f075a6db0710924a3f46c7ebdb5..4fdf3d298f0829f2cdb3bb8ca6cab2a343775479 100644 --- a/tangostationcontrol/tangostationcontrol/test/README.md +++ b/tangostationcontrol/tangostationcontrol/test/README.md @@ -1,171 +1,4 @@ -# Unit Testing +# Test files included in Station Control -Is the procedure of testing individual units of code for fit for use. Often -this entails isolating specific segments and testing the behavior given a -certain range of different inputs. Arguably, the most effective forms of unit -testing deliberately uses _edge cases_ and makes assumptions about any other -sections of code that are not currently under test. - -First the underlying technologies as well as how they are applied in Tango -Station Control are discussed. If this seems uninteresting, or you are already -familiar with these concepts you can skip to -[Running tox tasks](#running tox tasks) and -[Debugging unit tests](#debugging-unit-tests). - -### Table of Contents: - -- [Tox](#tox) -- [Testing approach](#testing-approach) -- [Mocking](#mocking) -- [Running tox tasks](#running-tox-tasks) -- [Debugging unit tests](#debugging-unit-tests) - -## Tox - -[Tox](https://tox.readthedocs.io/en/latest/) is a commandline tool to simplify -running Python tasks such as linting and unit testing. Using a simple -[configuration file](../tox.ini) it can setup a -[virtual environment](https://virtualenv.pypa.io/en/latest/) that automatically -installs dependencies before executing the task. These environments persist -after the task finishes preventing the excessive downloading and installation -of dependencies. - -The Tox environments in this project are configured to install any dependencies -listed in [test-requirements.txt](../test-requirements.txt), -this can also easily be verified within our [configuration file](../tox.ini). - -## Testing approach - -For testing [stestr](https://stestr.readthedocs.io/en/latest/) is used this -tool has main advantage the utilization of a highly parallelized unit test -runner that automatically scales to the number of simultaneous threads -supported by the host processor. Other features include automatic test -discovery and pattern matching to only execute a subset of tests. - -However, stestr is incompatible with using breakpoints -(through [pdb](https://docs.python.org/3/library/pdb.html)) directly and will -simply fail the test upon encountering one. The -[debugging unit tests](#debugging-unit-tests) section describes how to mitigate -this and still debug individual unit tests effectively. - -All tests can be found in this test folder and all tests must inherit from -`TestCase`, found in [base.py](base.py). This ensures that any test fixtures run -before and after tests. These test fixtures allow to restore any global modified -state, such as those of static variables. It is the task of the developer -writing the unit test to ensure that any global modification is restored. - -When writing unit tests it is best practice to mimic the directory structure of -the original project inside the test directory. Similarly, copying the filenames -and adding _test__ to the beginning. Below is an example: - -* root - * database - * database_manager.py - * test - * base.py - * database - * test_database_manager.py - -## Mocking - -Contrary to many other programming languages, it is entirely possible to -modify **any** function, object, file, import at runtime. This allows for a -great deal of flexibility but also simplicity in the case of unit tests. - -Isolating functions is as simple as mocking any of the classes or functions it -uses and modifying its return values such as specific behavior can be tested. -Below is a simple demonstration mocking the return value of a function using the -mock decorator. For more detailed explanations see -[the official documentation](https://docs.python.org/3/library/unittest.mock.html). - -```python -from unittest import mock - -# We pretend that `our_custom_module` contains a function `special_char` -import our_custom_module - -def function_under_test(self): - if our_custom_module.special_char(): - return 12 - else: - return 0 - -@mock.patch.object(our_custom_module, "special_char") -def test_function_under_test(self, m_special_char): - """ Test functionality of `function_under_test` - - This mock decorator _temporarily_ overwrites the :py:func:`special_char` - function within :py:module:`our_custom_module`. We get access to this - mocked object as function argument. Concurrent dependencies of these mock - statements are automatically solved by stestr. - """ - - # Mock the return value of the mocked object to be None - m_special_char.return_value = None - - # Assert that function_under_test returns 0 when special_char returns None. - self.assertEqual(0, function_under_test()) - -``` - -## Running tox tasks - -Running tasks defined in Tox might be a little different than what you are used -to. This is due to the Tango devices running from Docker containers. Typically, -the dependencies are only available inside Docker and not on the host. - -The tasks can thus only be accessed by executing Tox from within a Tango device -Docker container. A simple interactive Docker exec is enough to access them: - -```sh -docker exec -it device-sdp /bin/bash -cd /opt/lofar/tango/devices/ -tox -``` - -For specific tasks the `-e` parameter can be used, in addition, any arguments -can be appended after the tasks. These arguments can be interpreted within the -Tox configuration file as `{posargs}`. Below are a few examples: - -```sh -# Execute unit tests with Python 3.10 and only execute the test_get_version test -# from the TestLofarGit class found within util/test_lofar.py -# Noteworthy, this will also execute test_get_version_tag_dirty due to pattern -# matching. -tox -e py310 util.test_lofar.TestLofarGit.test_get_version_tag -# Execute linting -tox -e pep8 -``` - -## Debugging unit tests - -Debugging works by utilizing the -[virtual environment](https://virtualenv.pypa.io/en/latest/)that Tox creates. -These are placed in the .tox/ directory. Each of these environments carries -the same name as found in _tox.ini_, these match the names used for `-e` -arguments - -Debugging unit tests is done by inserting the following code segment just before -where you think issues occur: - -```python -import pdb; pdb.set_trace() -``` - -Now as said stestr will catch any breakpoints and reraise them so we need to -avoid using stestr while debugging. Simply source the virtual environment -created by tox `source .tox/py37/bin/activate`. You should now see that the -shell $PS1 prompt is modified to indicate the environment is active. - -From here execute `python -m testtools.run` and optionally the specific test -case as command line argument. These test will not run in parallel but support -all other features such as autodiscovery, test fixtures and mocking. - -Any breakpoint will be triggered and you can use the pdb interface -(very similar to gdb) to step through the code, modify and print variables. - -Afterwards simply execute `deactivate` to deactivate the virtual environment. -**DO NOT FORGOT TO REMOVE YOUR `pdb.set_trace()` STATEMENTS AFTERWARDS** - -The best approach to prevent committing `import pdb; pdb.set_trace()` is to -ensure that all unit tests succeed beforehand. +Test code shared across unit and integration tests is placed here so it is +accessible by both. diff --git a/tangostationcontrol/tangostationcontrol/test/__init__.py b/tangostationcontrol/tangostationcontrol/test/__init__.py index 68ddd5cdc3efaa38e853aef337c08beb99c50c4c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/tangostationcontrol/tangostationcontrol/test/__init__.py +++ b/tangostationcontrol/tangostationcontrol/test/__init__.py @@ -1,2 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 diff --git a/tangostationcontrol/tangostationcontrol/test/devices/__init__.py b/tangostationcontrol/tangostationcontrol/test/devices/__init__.py index 68ddd5cdc3efaa38e853aef337c08beb99c50c4c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/__init__.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/__init__.py @@ -1,2 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py index 10ebef47cd282df6a1c041f74a4bc9b1d4969670..a84d75bf2ff6e28aeb7b648445591cf69a1544c7 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py @@ -3,7 +3,6 @@ class TestObservationBase: - # TODO(Corne): Use this once working on L2SS-774 VALID_JSON = """ { "observation_id": 12345, diff --git a/tangostationcontrol/test-requirements.txt b/tangostationcontrol/test-requirements.txt index 8d6926cbab34c65c5cad61e6bdf8e0bbd30ec530..475e44ba5ab4d31d5c5a7e94f942ede2afb89d70 100644 --- a/tangostationcontrol/test-requirements.txt +++ b/tangostationcontrol/test-requirements.txt @@ -25,3 +25,5 @@ timeout-decorator>=0.5.0 # MIT xenon>=0.8.0 # MIT prometheus_client # Apache-2.0 python-logstash-async # MIT +pytest >= 7.0.0 # MIT +pytest-cov >= 3.0.0 # MIT diff --git a/tangostationcontrol/test/README.md b/tangostationcontrol/test/README.md new file mode 100644 index 0000000000000000000000000000000000000000..56b174d88e533f075a6db0710924a3f46c7ebdb5 --- /dev/null +++ b/tangostationcontrol/test/README.md @@ -0,0 +1,171 @@ +# Unit Testing + +Is the procedure of testing individual units of code for fit for use. Often +this entails isolating specific segments and testing the behavior given a +certain range of different inputs. Arguably, the most effective forms of unit +testing deliberately uses _edge cases_ and makes assumptions about any other +sections of code that are not currently under test. + +First the underlying technologies as well as how they are applied in Tango +Station Control are discussed. If this seems uninteresting, or you are already +familiar with these concepts you can skip to +[Running tox tasks](#running tox tasks) and +[Debugging unit tests](#debugging-unit-tests). + +### Table of Contents: + +- [Tox](#tox) +- [Testing approach](#testing-approach) +- [Mocking](#mocking) +- [Running tox tasks](#running-tox-tasks) +- [Debugging unit tests](#debugging-unit-tests) + +## Tox + +[Tox](https://tox.readthedocs.io/en/latest/) is a commandline tool to simplify +running Python tasks such as linting and unit testing. Using a simple +[configuration file](../tox.ini) it can setup a +[virtual environment](https://virtualenv.pypa.io/en/latest/) that automatically +installs dependencies before executing the task. These environments persist +after the task finishes preventing the excessive downloading and installation +of dependencies. + +The Tox environments in this project are configured to install any dependencies +listed in [test-requirements.txt](../test-requirements.txt), +this can also easily be verified within our [configuration file](../tox.ini). + +## Testing approach + +For testing [stestr](https://stestr.readthedocs.io/en/latest/) is used this +tool has main advantage the utilization of a highly parallelized unit test +runner that automatically scales to the number of simultaneous threads +supported by the host processor. Other features include automatic test +discovery and pattern matching to only execute a subset of tests. + +However, stestr is incompatible with using breakpoints +(through [pdb](https://docs.python.org/3/library/pdb.html)) directly and will +simply fail the test upon encountering one. The +[debugging unit tests](#debugging-unit-tests) section describes how to mitigate +this and still debug individual unit tests effectively. + +All tests can be found in this test folder and all tests must inherit from +`TestCase`, found in [base.py](base.py). This ensures that any test fixtures run +before and after tests. These test fixtures allow to restore any global modified +state, such as those of static variables. It is the task of the developer +writing the unit test to ensure that any global modification is restored. + +When writing unit tests it is best practice to mimic the directory structure of +the original project inside the test directory. Similarly, copying the filenames +and adding _test__ to the beginning. Below is an example: + +* root + * database + * database_manager.py + * test + * base.py + * database + * test_database_manager.py + +## Mocking + +Contrary to many other programming languages, it is entirely possible to +modify **any** function, object, file, import at runtime. This allows for a +great deal of flexibility but also simplicity in the case of unit tests. + +Isolating functions is as simple as mocking any of the classes or functions it +uses and modifying its return values such as specific behavior can be tested. +Below is a simple demonstration mocking the return value of a function using the +mock decorator. For more detailed explanations see +[the official documentation](https://docs.python.org/3/library/unittest.mock.html). + +```python +from unittest import mock + +# We pretend that `our_custom_module` contains a function `special_char` +import our_custom_module + +def function_under_test(self): + if our_custom_module.special_char(): + return 12 + else: + return 0 + +@mock.patch.object(our_custom_module, "special_char") +def test_function_under_test(self, m_special_char): + """ Test functionality of `function_under_test` + + This mock decorator _temporarily_ overwrites the :py:func:`special_char` + function within :py:module:`our_custom_module`. We get access to this + mocked object as function argument. Concurrent dependencies of these mock + statements are automatically solved by stestr. + """ + + # Mock the return value of the mocked object to be None + m_special_char.return_value = None + + # Assert that function_under_test returns 0 when special_char returns None. + self.assertEqual(0, function_under_test()) + +``` + +## Running tox tasks + +Running tasks defined in Tox might be a little different than what you are used +to. This is due to the Tango devices running from Docker containers. Typically, +the dependencies are only available inside Docker and not on the host. + +The tasks can thus only be accessed by executing Tox from within a Tango device +Docker container. A simple interactive Docker exec is enough to access them: + +```sh +docker exec -it device-sdp /bin/bash +cd /opt/lofar/tango/devices/ +tox +``` + +For specific tasks the `-e` parameter can be used, in addition, any arguments +can be appended after the tasks. These arguments can be interpreted within the +Tox configuration file as `{posargs}`. Below are a few examples: + +```sh +# Execute unit tests with Python 3.10 and only execute the test_get_version test +# from the TestLofarGit class found within util/test_lofar.py +# Noteworthy, this will also execute test_get_version_tag_dirty due to pattern +# matching. +tox -e py310 util.test_lofar.TestLofarGit.test_get_version_tag +# Execute linting +tox -e pep8 +``` + +## Debugging unit tests + +Debugging works by utilizing the +[virtual environment](https://virtualenv.pypa.io/en/latest/)that Tox creates. +These are placed in the .tox/ directory. Each of these environments carries +the same name as found in _tox.ini_, these match the names used for `-e` +arguments + +Debugging unit tests is done by inserting the following code segment just before +where you think issues occur: + +```python +import pdb; pdb.set_trace() +``` + +Now as said stestr will catch any breakpoints and reraise them so we need to +avoid using stestr while debugging. Simply source the virtual environment +created by tox `source .tox/py37/bin/activate`. You should now see that the +shell $PS1 prompt is modified to indicate the environment is active. + +From here execute `python -m testtools.run` and optionally the specific test +case as command line argument. These test will not run in parallel but support +all other features such as autodiscovery, test fixtures and mocking. + +Any breakpoint will be triggered and you can use the pdb interface +(very similar to gdb) to step through the code, modify and print variables. + +Afterwards simply execute `deactivate` to deactivate the virtual environment. +**DO NOT FORGOT TO REMOVE YOUR `pdb.set_trace()` STATEMENTS AFTERWARDS** + +The best approach to prevent committing `import pdb; pdb.set_trace()` is to +ensure that all unit tests succeed beforehand. diff --git a/tangostationcontrol/tangostationcontrol/test/beam/__init__.py b/tangostationcontrol/test/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/beam/__init__.py rename to tangostationcontrol/test/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/base.py b/tangostationcontrol/test/base.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/base.py rename to tangostationcontrol/test/base.py diff --git a/tangostationcontrol/tangostationcontrol/test/clients/__init__.py b/tangostationcontrol/test/beam/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/__init__.py rename to tangostationcontrol/test/beam/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py b/tangostationcontrol/test/beam/test_delays.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/beam/test_delays.py rename to tangostationcontrol/test/beam/test_delays.py index 4993e5cc6c30d44d7980fe952cc67cb46e65e43c..8fa7161c8c45a59231fbc72c070a9853b940d089 100644 --- a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py +++ b/tangostationcontrol/test/beam/test_delays.py @@ -9,9 +9,11 @@ import casacore import mock import numpy import numpy.testing + from tangostationcontrol.beam.delays import Delays from tangostationcontrol.common.constants import MAX_ANTENNA, N_beamlets_ctrl -from tangostationcontrol.test import base + +from test import base class TestDelays(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py b/tangostationcontrol/test/beam/test_geo.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/beam/test_geo.py rename to tangostationcontrol/test/beam/test_geo.py index 7f6711a93a0490760a084bc7f7eba20f369ffffc..9b9dcce6ff0af4d217c727472b40a5a02805d6d5 100644 --- a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py +++ b/tangostationcontrol/test/beam/test_geo.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import numpy.testing + from tangostationcontrol.beam.geo import ETRS_to_ITRF, ETRS_to_GEO, GEO_to_GEOHASH -from tangostationcontrol.test import base + +from test import base class TestETRSToITRF(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py b/tangostationcontrol/test/beam/test_hba_tile.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py rename to tangostationcontrol/test/beam/test_hba_tile.py index 3063008a56cfc767b03df0b8aaf80b894dcaf8b4..19ef26793dc8f84f4ae9957d564e8da709eb3751 100644 --- a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py +++ b/tangostationcontrol/test/beam/test_hba_tile.py @@ -4,8 +4,10 @@ from math import pi import numpy.testing + from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets -from tangostationcontrol.test import base + +from test import base class TestHBATAntennaOffsets(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/common/__init__.py b/tangostationcontrol/test/clients/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/common/__init__.py rename to tangostationcontrol/test/clients/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib b/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib rename to tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/test/clients/test_attr_wrapper.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py rename to tangostationcontrol/test/clients/test_attr_wrapper.py index 91226ed88b2396e45826f6512374dc9b5bb202cc..d540d946b3290d1a6f3cadf4a31c7b1edf208dde 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py +++ b/tangostationcontrol/test/clients/test_attr_wrapper.py @@ -5,22 +5,22 @@ """ import asyncio - import mock -import numpy -import tangostationcontrol.devices.lofar_device + +import tangostationcontrol.devices.interfaces.lofar_device +from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice # External imports +import numpy from tango import DevState, DevFailed, AttrWriteType # Test imports from tango.test_context import DeviceTestContext -from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper -from tangostationcontrol.devices.lofar_device import LOFARDevice -from tangostationcontrol.test import base # Internal imports -from tangostationcontrol.test.clients.test_client import TestClient +from test import base +from test.clients.test_client import TestClient SCALAR_DIMS = (1,) SPECTRUM_DIMS = (4,) @@ -43,7 +43,7 @@ class TestAttributeTypes(base.TestCase): def setUp(self): # Avoid the device trying to access itself as a client self.deviceproxy_patch = mock.patch.object( - tangostationcontrol.devices.lofar_device, "DeviceProxy" + tangostationcontrol.devices.interfaces.lofar_device, "DeviceProxy" ) self.deviceproxy_patch.start() self.addCleanup(self.deviceproxy_patch.stop) @@ -1015,7 +1015,7 @@ class TestAttributeAccess(base.TestCase): def setUp(self): # Avoid the device trying to access itself as a client self.deviceproxy_patch = mock.patch.object( - tangostationcontrol.devices.lofar_device, "DeviceProxy" + tangostationcontrol.devices.interfaces.lofar_device, "DeviceProxy" ) self.deviceproxy_patch.start() self.addCleanup(self.deviceproxy_patch.stop) diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py b/tangostationcontrol/test/clients/test_client.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/clients/test_client.py rename to tangostationcontrol/test/clients/test_client.py diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py b/tangostationcontrol/test/clients/test_opcua_client.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py rename to tangostationcontrol/test/clients/test_opcua_client.py index 643312fedac250d9b064d6070c02bbddc5fe0300..fd4e4dae3999d6fdbc38cd0e47278c6cfe8f7573 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py +++ b/tangostationcontrol/test/clients/test_opcua_client.py @@ -8,9 +8,11 @@ from unittest import mock import asynctest import asyncua import numpy + from tangostationcontrol.clients import opcua_client from tangostationcontrol.clients.opcua_client import OPCUAConnection -from tangostationcontrol.test import base + +from test import base class AttrProps: diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py b/tangostationcontrol/test/clients/test_snmp_client.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py rename to tangostationcontrol/test/clients/test_snmp_client.py index 9f6b895783bf3a1489be88abcec9ec929f16cc2c..296f8c08dc646d3a9d99116cfb0a743c3c94bf4a 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py +++ b/tangostationcontrol/test/clients/test_snmp_client.py @@ -8,11 +8,13 @@ import numpy from pysnmp import hlapi from pysnmp.smi import view, error from pysnmp.smi.rfc1902 import ObjectIdentity + from tangostationcontrol.clients.snmp_client import MIBLoader from tangostationcontrol.clients.snmp_client import SNMPAttribute from tangostationcontrol.clients.snmp_client import SNMPClient from tangostationcontrol.clients.snmp_client import SNMPComm -from tangostationcontrol.test import base + +from test import base class SNMPServerFixture: diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py b/tangostationcontrol/test/clients/test_statistics_client_thread.py similarity index 95% rename from tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py rename to tangostationcontrol/test/clients/test_statistics_client_thread.py index 6d9f01ef76f122b126747fa6ebfe2e64a1f619d9..cef62b535bad63d8801be288b264ff80c0f5311e 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py +++ b/tangostationcontrol/test/clients/test_statistics_client_thread.py @@ -5,7 +5,8 @@ import logging from unittest import mock from tangostationcontrol.clients.statistics.client_thread import StatisticsClientThread -from tangostationcontrol.test import base + +from test import base logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py b/tangostationcontrol/test/clients/test_tcp_replicator.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py rename to tangostationcontrol/test/clients/test_tcp_replicator.py index a6e3a24f67728d90969c1c7fab0353a954670303..f51e9973496da7af6b9399243de725337c99e917 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py +++ b/tangostationcontrol/test/clients/test_tcp_replicator.py @@ -6,8 +6,10 @@ import logging from unittest import mock import timeout_decorator + from tangostationcontrol.clients.tcp_replicator import TCPReplicator -from tangostationcontrol.test import base + +from test import base logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/__init__.py b/tangostationcontrol/test/common/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/configuration/__init__.py rename to tangostationcontrol/test/common/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/common/fake_measures.ztar b/tangostationcontrol/test/common/fake_measures.ztar similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/common/fake_measures.ztar rename to tangostationcontrol/test/common/fake_measures.ztar diff --git a/tangostationcontrol/tangostationcontrol/test/common/fake_measures_newer.ztar b/tangostationcontrol/test/common/fake_measures_newer.ztar similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/common/fake_measures_newer.ztar rename to tangostationcontrol/test/common/fake_measures_newer.ztar diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py b/tangostationcontrol/test/common/test_baselines.py similarity index 96% rename from tangostationcontrol/tangostationcontrol/test/common/test_baselines.py rename to tangostationcontrol/test/common/test_baselines.py index ca1486f27d4da8f83c3fca227947ed22972450c8..eed7d7dda120a57f69885c7a6f9ffa4f503fac09 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py +++ b/tangostationcontrol/test/common/test_baselines.py @@ -4,7 +4,7 @@ from tangostationcontrol.common import baselines from tangostationcontrol.common.constants import MAX_INPUTS -from tangostationcontrol.test import base +from test import base class TestBaselines(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_cables.py b/tangostationcontrol/test/common/test_cables.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/test/common/test_cables.py rename to tangostationcontrol/test/common/test_cables.py index 96a78dd89cb684b5e46e31a2edb635f830816afc..0ffc3b55a9e4ea4a7e457db53979034c74d49659 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_cables.py +++ b/tangostationcontrol/test/common/test_cables.py @@ -3,7 +3,7 @@ from tangostationcontrol.common import cables -from tangostationcontrol.test import base +from test import base class TestCables(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/test/common/test_calibration.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/common/test_calibration.py rename to tangostationcontrol/test/common/test_calibration.py index 6d13f8929dd25dbab41bf0b89ee204ff7bfa3e07..44b87271f5ff535e2fc17b747d446d7be727f86a 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py +++ b/tangostationcontrol/test/common/test_calibration.py @@ -2,12 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 import numpy + from tangostationcontrol.common.calibration import ( delay_compensation, loss_compensation, dB_to_factor, ) -from tangostationcontrol.test import base + +from test import base class TestCalibration(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py b/tangostationcontrol/test/common/test_lofar_logging.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py rename to tangostationcontrol/test/common/test_lofar_logging.py index daaeb69a8d01e3806730410830dff953d1a41ddc..4af2e720736eb7424bd223c732114917d3c49804 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py +++ b/tangostationcontrol/test/common/test_lofar_logging.py @@ -8,8 +8,10 @@ from unittest import mock from tango import device_server, DevFailed from tango.server import Device from tango.test_context import DeviceTestContext + from tangostationcontrol.common import lofar_logging -from tangostationcontrol.test import base + +from test import base class TestExceptionToStr(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_measures.py b/tangostationcontrol/test/common/test_measures.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/common/test_measures.py rename to tangostationcontrol/test/common/test_measures.py index ec8e0369af8451a8cb10f36a7a55433102cfc4ff..808ba0a892184fd86f3da437c74b1d3c3c8b5fc4 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_measures.py +++ b/tangostationcontrol/test/common/test_measures.py @@ -8,7 +8,8 @@ import urllib.request from unittest import mock from tangostationcontrol.common import measures -from tangostationcontrol.test import base + +from test import base # where our WSRT_Measures.ztar surrogate is located # two versions with different timestamps are provided diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py b/tangostationcontrol/test/common/test_observation_controller.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py rename to tangostationcontrol/test/common/test_observation_controller.py index c2d7997c06167b5abfb4beb4aabb228c1f173c02..0de8ae33ba9c8d16eb49c96fc10fbf6380c48a1e 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py +++ b/tangostationcontrol/test/common/test_observation_controller.py @@ -8,10 +8,12 @@ from unittest import mock from unittest.mock import Mock from tango import DevState + from tangostationcontrol.common import ObservationController from tangostationcontrol.common.observation_controller import RunningObservation from tangostationcontrol.configuration import ObservationSettings, Pointing, Sap -from tangostationcontrol.test import base + +from test import base @mock.patch("tango.Util.instance") diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py b/tangostationcontrol/test/common/test_type_checking.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py rename to tangostationcontrol/test/common/test_type_checking.py index f5b1816d39e93899f0e4c82b2eb8afac5e3564df..c42b3b00cff92356284fcc40eb37996219cf6c33 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py +++ b/tangostationcontrol/test/common/test_type_checking.py @@ -3,8 +3,10 @@ import numpy from tango.utils import is_seq + from tangostationcontrol.common import type_checking -from tangostationcontrol.test import base + +from test import base class TestTypeChecking(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/prometheus/__init__.py b/tangostationcontrol/test/configuration/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/prometheus/__init__.py rename to tangostationcontrol/test/configuration/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py b/tangostationcontrol/test/configuration/_mock_requests.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py rename to tangostationcontrol/test/configuration/_mock_requests.py diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py b/tangostationcontrol/test/configuration/test_observation_settings.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py rename to tangostationcontrol/test/configuration/test_observation_settings.py index cba385096eff77a90d799f75da18f7f537df6638..7002c9c63962f4e885a62f2be2081f2c606b4532 100644 --- a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py +++ b/tangostationcontrol/test/configuration/test_observation_settings.py @@ -6,9 +6,11 @@ from unittest import mock import requests from jsonschema.exceptions import ValidationError, RefResolutionError + from tangostationcontrol.configuration import Pointing, ObservationSettings, Sap -from tangostationcontrol.test import base -from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get + +from test import base +from test.configuration._mock_requests import mocked_requests_get @mock.patch("requests.get", side_effect=mocked_requests_get) diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py b/tangostationcontrol/test/configuration/test_pointing.py similarity index 93% rename from tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py rename to tangostationcontrol/test/configuration/test_pointing.py index 0161f0a35bfba492605cb179db7237858286d98d..a5ca5955d508c8bd4744bb0f049ae793f212abeb 100644 --- a/tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py +++ b/tangostationcontrol/test/configuration/test_pointing.py @@ -5,9 +5,11 @@ from unittest import mock import requests from jsonschema.exceptions import ValidationError, RefResolutionError + from tangostationcontrol.configuration import Pointing -from tangostationcontrol.test import base -from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get + +from test import base +from test.configuration._mock_requests import mocked_requests_get @mock.patch("requests.get", side_effect=mocked_requests_get) diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py b/tangostationcontrol/test/configuration/test_sap_settings.py similarity index 95% rename from tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py rename to tangostationcontrol/test/configuration/test_sap_settings.py index c625f4ebfcaaa2faa80b2736b3584e66fd874e3b..a9afbfd9e80f0db49447bcce5b634882e7a43fd8 100644 --- a/tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py +++ b/tangostationcontrol/test/configuration/test_sap_settings.py @@ -5,9 +5,11 @@ from unittest import mock import requests from jsonschema.exceptions import ValidationError, RefResolutionError + from tangostationcontrol.configuration import Pointing, Sap -from tangostationcontrol.test import base -from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get + +from test import base +from test.configuration._mock_requests import mocked_requests_get @mock.patch("requests.get", side_effect=mocked_requests_get) diff --git a/tangostationcontrol/tangostationcontrol/test/statistics/__init__.py b/tangostationcontrol/test/devices/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/statistics/__init__.py rename to tangostationcontrol/test/devices/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md b/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md rename to tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json b/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json rename to tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py b/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py rename to tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py diff --git a/tangostationcontrol/tangostationcontrol/test/devices/device_base.py b/tangostationcontrol/test/devices/device_base.py similarity index 62% rename from tangostationcontrol/tangostationcontrol/test/devices/device_base.py rename to tangostationcontrol/test/devices/device_base.py index 22f8155e20bf3820f3a9ebcadf2e55c13a53fcef..7f782259cd2f5ff2570f29d68f7f20b89bc890e0 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/device_base.py +++ b/tangostationcontrol/test/devices/device_base.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import mock -from tangostationcontrol.devices import lofar_device -from tangostationcontrol.test import base + +from tangostationcontrol.devices.interfaces import lofar_device + +from test import base class DeviceTestCase(base.TestCase): @@ -21,7 +23,13 @@ class DeviceTestCase(base.TestCase): for device in [lofar_device]: self.device_proxy_patch(device) - def device_proxy_patch(self, device): - proxy_patcher = mock.patch.object(device, "DeviceProxy") + def device_proxy_patch(self, device_module): + """Patch a Python module using DeviceProxy to mock its behavior""" + + proxy_patcher = mock.patch.object(device_module, "DeviceProxy") proxy_patcher.start() self.addCleanup(proxy_patcher.stop) + return { + "mock": proxy_patcher, + "object": getattr(proxy_patcher.target, proxy_patcher.attribute), + } diff --git a/tangostationcontrol/test/devices/interfaces/__init__.py b/tangostationcontrol/test/devices/interfaces/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py b/tangostationcontrol/test/devices/interfaces/test_beam_device.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py rename to tangostationcontrol/test/devices/interfaces/test_beam_device.py index 46c921683f3f8b0d5ab6cbd994aeec023eb499d6..b93d2fa8d6b0a5a5b492765f4eae1ee251ffc60f 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py +++ b/tangostationcontrol/test/devices/interfaces/test_beam_device.py @@ -4,21 +4,19 @@ from typing import Callable from typing import Optional import datetime +import logging import random import statistics import time import numpy as np import timeout_decorator - from tango import DevFailed -from tangostationcontrol.test import base -from tangostationcontrol.test.devices import device_base +from tangostationcontrol.devices.interfaces.beam_device import BeamTracker -from tangostationcontrol.devices.beam_device import BeamTracker - -import logging +from test import base +from test.devices import device_base logger = logging.getLogger() diff --git a/tangostationcontrol/test/devices/interfaces/test_hierarchy.py b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..174599a7e884690aa03062a37fe4da67981d4158 --- /dev/null +++ b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py @@ -0,0 +1,334 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import copy +import logging +from typing import Callable +from typing import Dict +from typing import List +from mock import Mock + +from tango import DevState + +from tangostationcontrol.devices.interfaces import hierarchy + +from test.devices import device_base + +logger = logging.getLogger() + + +class TestAbstractHierarchy(device_base.DeviceTestCase): + class ConcreteHierarchy(hierarchy.AbstractHierarchy): + pass + + TEST_PROPERTY_NAME = "control_children" + + def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy + super(TestAbstractHierarchy, self).setUp() + + self.hierarchy_mock = self.device_proxy_patch(hierarchy) + + def test_create_instance(self): + """Test default values and if we can create an instance""" + + test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME + ) + + self.assertEqual({}, test_hierarchy._children) + self.assertEqual(None, test_hierarchy._parent) + + self.assertEqual({}, test_hierarchy.children()) + self.assertEqual(None, test_hierarchy.parent()) + + def test_get_or_create_proxy_cache(self): + """Test if get_or_create_proxy caches without duplicates""" + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None, parent=None + ) + + def test_parent_get_name(self): + """Read the name of the parent through mocking DeviceProxy.dev_name""" + + name_station = "stat/stationmanager/1" + + mock_dev_name = Mock() + mock_dev_name.dev_name.return_value = name_station + self.hierarchy_mock["object"].return_value = mock_dev_name + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None, parent=name_station + ) + + self.assertEqual(name_station, test.parent()) + + def test_parent_get_name_no_parent(self): + """Read the name of the parent when there is no parent""" + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None + ) + + self.assertEqual(None, test.parent()) + + def test_parent_read_attribute(self): + """Read an attribute from the parent and get the mocked data""" + + attribute_value = "0.0.1" + + name_station = "stat/stationmanager/1" + # Mock an attribute + self.hierarchy_mock[ + "object" + ].return_value.FPGA_firmware_version_R = attribute_value + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None, parent=name_station + ) + + self.assertEqual( + attribute_value, test.read_attribute("FPGA_firmware_version_R") + ) + + def test_parent_read_attribute_no_parent(self): + """Ensure that read_attribute returns None if there is no parent""" + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None + ) + + self.assertIsNone(test.read_attribute("FPGA_firmware_version_R")) + + def test_parent_get_state(self): + """Ensure that we can get parent state""" + + name_station = "stat/stationmanager/1" + # Mock the state + self.hierarchy_mock["object"].return_value.state.return_value = DevState.FAULT + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None, parent=name_station + ) + + self.assertEqual(DevState.FAULT, test.state()) + + def test_parent_get_state_no_parent(self): + """Ensure that state returns None if no parent""" + + test = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, children=None + ) + + self.assertIsNone(test.state()) + + def children_test_base( + self, + property_name: str, + direct_children: List[str], + children_properties: List[Dict[str, List[str]]], + fn: Callable[[ConcreteHierarchy], None], + ): + """Base function for testing children() method + + :param fn: Callable to perform actual tests in + """ + + test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy( + property_name, direct_children, None + ) + + self.hierarchy_mock[ + "object" + ].return_value.get_property.side_effect = children_properties + + fn(test_hierarchy) + + def test_get_children_depth_1_no_filter(self): + """Test finding all children at max depth of 1""" + + depth = 1 + + test_children = ["stat/sdp/1", "stat/sdp/2"] + test_property_calls = [ + {self.TEST_PROPERTY_NAME: ["stat/xst/1"]}, # sdp/1 + {self.TEST_PROPERTY_NAME: ["stat/xstbeam/1"]}, # xst/1 + {self.TEST_PROPERTY_NAME: []}, # xstbeam,/1 + {self.TEST_PROPERTY_NAME: []}, # sdp/2 + ] + + def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy): + test = test_hierarchy.children(depth=depth) + + # Test if all keys from test_children are in the children() dict + self.assertSetEqual(set(test_children), set(test.keys())) + + # test if for each child there are no further children + for child in test_children: + self.assertDictEqual({}, test[child]["children"]) + + self.children_test_base( + self.TEST_PROPERTY_NAME, test_children, test_property_calls, test_fn + ) + + def test_get_children_depth_2_no_filter(self): + """Test recursively finding children limited to depth 2""" + + depth = 2 + + test_children = ["stat/sdp/1", "stat/sdp/2"] + # Calls to get_property follow depth-first order + test_property_calls = [ + {self.TEST_PROPERTY_NAME: ["stat/antennafield/1"]}, # sdp/1 + {self.TEST_PROPERTY_NAME: ["stat/tilebeam/1"]}, # antennafield/1 + {self.TEST_PROPERTY_NAME: []}, # tilebeam/1 + {self.TEST_PROPERTY_NAME: []}, # sdp/2 + ] + + def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy): + test = test_hierarchy.children(depth=depth) + + # Test that antennafield is included + self.assertIsNotNone( + test["stat/sdp/1"]["children"].get("stat/antennafield/1") + ) + + # Test that antennafield child is removed due to depth limit + self.assertDictEqual( + {}, test["stat/sdp/1"]["children"]["stat/antennafield/1"]["children"] + ) + + self.children_test_base( + self.TEST_PROPERTY_NAME, test_children, test_property_calls, test_fn + ) + + TEST_CHILDREN_ROOT = ["stat/ccd/1", "stat/psoc/1", "stat/antennafield/1"] + + TEST_CHILDREN_ANTENNAFIELD = [ + "stat/sdp/1", + "stat/tilebeam/1", + "stat/digitalbeam/1", + "stat/aps/1", + ] + TEST_CHILDREN_SDP = ["stat/xst/1", "stat/sst/1", "stat/bst/1"] + TEST_CHILDREN_APS = ["stat/apsct/1", "stat/apspu/1", "stat/unb2/1", "stat/recv/1"] + + # Calls to get_property follow depth-first order + TEST_GET_PROPERTY_CALLS = [ + {TEST_PROPERTY_NAME: []}, # cdd/1 + {TEST_PROPERTY_NAME: []}, # psoc/1 + {TEST_PROPERTY_NAME: TEST_CHILDREN_ANTENNAFIELD}, # antennafield/1 + {TEST_PROPERTY_NAME: TEST_CHILDREN_SDP}, # sdp/1 + {TEST_PROPERTY_NAME: []}, # xst/1 + {TEST_PROPERTY_NAME: []}, # sst/1 + {TEST_PROPERTY_NAME: []}, # bst/1 + {TEST_PROPERTY_NAME: []}, # tilebeam/1 + {TEST_PROPERTY_NAME: ["stat/beamlet/1"]}, # digitalbeam/1 + {TEST_PROPERTY_NAME: []}, # beamlet/1 + {TEST_PROPERTY_NAME: TEST_CHILDREN_APS}, # aps/1 + {TEST_PROPERTY_NAME: []}, # apsct/1 + {TEST_PROPERTY_NAME: []}, # apspu/1 + {TEST_PROPERTY_NAME: []}, # unb2/1 + {TEST_PROPERTY_NAME: []}, # recv/1 + ] + + def test_get_children_depth_elaborate_no_filter(self): + """Create a 3 levels deep hierarchy with ~15 devices""" + + def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy): + test = test_hierarchy.children(depth=-1) + + # Test ccd and psoc have no children + self.assertDictEqual({}, test["stat/ccd/1"]["children"]) + self.assertDictEqual({}, test["stat/psoc/1"]["children"]) + + # Test antennafield has 4 children + self.assertEqual(4, len(test["stat/antennafield/1"]["children"])) + + # Test sdp has 3 children + self.assertEqual( + 3, + len(test["stat/antennafield/1"]["children"]["stat/sdp/1"]["children"]), + ) + + # Test all childs of sdp have no children + for sdp_child in self.TEST_CHILDREN_SDP: + self.assertDictEqual( + {}, + test["stat/antennafield/1"]["children"]["stat/sdp/1"]["children"][ + sdp_child + ]["children"], + ) + + # Test tilebeam has no children + self.assertDictEqual( + {}, + test["stat/antennafield/1"]["children"]["stat/tilebeam/1"]["children"], + ) + + # Test digitalbeam has 1 child + self.assertEqual( + 1, + len( + test["stat/antennafield/1"]["children"]["stat/digitalbeam/1"][ + "children" + ] + ), + ) + + # Test beamlet has no children + self.assertDictEqual( + {}, + test["stat/antennafield/1"]["children"]["stat/digitalbeam/1"][ + "children" + ]["stat/beamlet/1"]["children"], + ) + + # Test aps has 4 children + self.assertEqual( + 4, + len(test["stat/antennafield/1"]["children"]["stat/aps/1"]["children"]), + ) + + # Test all childs of aps have no children + for aps_child in self.TEST_CHILDREN_APS: + self.assertDictEqual( + {}, + test["stat/antennafield/1"]["children"]["stat/aps/1"]["children"][ + aps_child + ]["children"], + ) + + self.children_test_base( + self.TEST_PROPERTY_NAME, + self.TEST_CHILDREN_ROOT, + self.TEST_GET_PROPERTY_CALLS, + test_fn, + ) + + def test_get_child_filter(self): + """Test we can find every device""" + + test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy( + self.TEST_PROPERTY_NAME, self.TEST_CHILDREN_ROOT, None + ) + + self.hierarchy_mock[ + "object" + ].return_value.get_property.side_effect = self.TEST_GET_PROPERTY_CALLS + + all_children = copy.copy(self.TEST_CHILDREN_ROOT) + all_children.extend(self.TEST_CHILDREN_ANTENNAFIELD) + all_children.extend(self.TEST_CHILDREN_SDP) + all_children.extend(self.TEST_CHILDREN_APS) + + # Find all proxies for each child and match that it is the same + # object as cached in `_proxies` + for child in all_children: + result = test_hierarchy.child(child) + self.assertEqual(test_hierarchy._proxies[child], result) + + self.hierarchy_mock[ + "object" + ].return_value.get_property.side_effect = self.TEST_GET_PROPERTY_CALLS diff --git a/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py b/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py new file mode 100644 index 0000000000000000000000000000000000000000..d99553dc42d36d085bda01ee4083adc95ae1bfd7 --- /dev/null +++ b/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py @@ -0,0 +1,62 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +import unittest + +from tango import DeviceProxy +from tango.server import command +from tango.server import device_property +from tango.test_context import DeviceTestContext + +from tangostationcontrol.devices.interfaces import hierarchy +from tangostationcontrol.devices.interfaces import hierarchy_device +from tangostationcontrol.devices.interfaces import lofar_device + +from test.devices import device_base + + +class TestHierarchyDevice(device_base.DeviceTestCase): + def setUp(self): + # DeviceTestCase setUp patches lofar_device DeviceProxy + super(TestHierarchyDevice, self).setUp() + + self.hierarchy_mock = self.device_proxy_patch(hierarchy) + + @unittest.skip("Mocking required Process=False which is broken in tango") + @unittest.mock.patch.object(hierarchy_device, "Database") + def test_get_direct_child_device(self, m_database): + """Test whether read_attribute really returns the attribute.""" + + TEST_PROPERTY_CHILDREN = "hierarchy_children" + TEST_PROPERTY_PARENT = "hierarchy_parent" + TEST_DEVICE_NAME = "stat/antennafield/1" + TEST_CHILD_DEVICE = "stat/sdp/1" + + class MyHierarchyDevice( + lofar_device.LOFARDevice, hierarchy_device.AbstractHierarchyDevice + ): + hierarchy_children = device_property( + dtype="DevVarStringArray", + mandatory=False, + default_value=[TEST_CHILD_DEVICE], + ) + + @command() + def has_child_proxy(self): + self.init( + TEST_DEVICE_NAME, TEST_PROPERTY_CHILDREN, TEST_PROPERTY_PARENT + ) + + if isinstance(self.child(TEST_CHILD_DEVICE), DeviceProxy): + raise Exception + + with DeviceTestContext(MyHierarchyDevice, process=False, timeout=10) as proxy: + m_database.return_value.get_device_property.side_effect = [ + {TEST_PROPERTY_CHILDREN: [TEST_CHILD_DEVICE]}, + {TEST_PROPERTY_PARENT: []}, + ] + + self.hierarchy_mock["object"].return_value.get_property.side_effect = [ + {TEST_PROPERTY_CHILDREN: [TEST_CHILD_DEVICE]}, + {TEST_PROPERTY_CHILDREN: []}, + ] + proxy.has_child_proxy() diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py rename to tangostationcontrol/test/devices/interfaces/test_lofar_device.py index 9621a930ff980f89d568176263ebfddc54a8c73e..2d11c9018fcc11a4bb5c1eeab98b56e5edc418ec 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py +++ b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py @@ -11,8 +11,10 @@ from tango import DevVarBooleanArray from tango.server import attribute from tango.server import command from tango.test_context import DeviceTestContext -from tangostationcontrol.devices import lofar_device -from tangostationcontrol.test.devices import device_base + +from tangostationcontrol.devices.interfaces import lofar_device + +from test.devices import device_base class TestLofarDevice(device_base.DeviceTestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/random_data.py b/tangostationcontrol/test/devices/random_data.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/devices/random_data.py rename to tangostationcontrol/test/devices/random_data.py diff --git a/tangostationcontrol/test/devices/sdp/__init__.py b/tangostationcontrol/test/devices/sdp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py b/tangostationcontrol/test/devices/sdp/test_beamlet_device.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py rename to tangostationcontrol/test/devices/sdp/test_beamlet_device.py index f733dac95841aa5844e329c8fe334729bfbdf5ee..e6a3309dce2986e88306d9b4360fe3d2b4df8591 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py +++ b/tangostationcontrol/test/devices/sdp/test_beamlet_device.py @@ -3,10 +3,12 @@ import numpy import numpy.testing + from tangostationcontrol.common.constants import CLK_200_MHZ from tangostationcontrol.devices.sdp.beamlet import Beamlet from tangostationcontrol.devices.sdp.common import weight_to_complex -from tangostationcontrol.test import base + +from test import base # unpack into 16-bit complex diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py b/tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py rename to tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py index 8dd96fdfe34025fe428662c9e75237587bd490a6..dac9bd0991031e18c4c2e9b6e64cf92037255860 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py +++ b/tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py @@ -24,7 +24,7 @@ from tangostationcontrol.common.constants import ( from tangostationcontrol.devices.sdp import digitalbeam # Internal test imports -from tangostationcontrol.test.devices import device_base +from test.devices import device_base class TestDigitalBeamDevice(device_base.DeviceTestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py b/tangostationcontrol/test/devices/sdp/test_sdp_common.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py rename to tangostationcontrol/test/devices/sdp/test_sdp_common.py index 9ec8d2d68446f780be206d57a2818c72d0d7648f..431a47b7fae830323f9321c655391a74012af5dc 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py +++ b/tangostationcontrol/test/devices/sdp/test_sdp_common.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import numpy + from tangostationcontrol.common.constants import CLK_200_MHZ, CLK_160_MHZ from tangostationcontrol.devices.sdp.common import ( phases_to_weights, @@ -9,7 +10,8 @@ from tangostationcontrol.devices.sdp.common import ( subband_frequency, weight_to_complex, ) -from tangostationcontrol.test import base + +from test import base class TestSDPCommon(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py b/tangostationcontrol/test/devices/sdp/test_statistics_device.py similarity index 97% rename from tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py rename to tangostationcontrol/test/devices/sdp/test_statistics_device.py index 25815b132504d0541229ac667749c975e4f31dbe..42ebee21fdc47b1c9a50eea32581ae7acab64860 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py +++ b/tangostationcontrol/test/devices/sdp/test_statistics_device.py @@ -6,7 +6,7 @@ import mock from tango import server from tango.test_context import DeviceTestContext -from tangostationcontrol.test import base +from test import base class TestStatisticsDevice(base.TestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/test/devices/test_antennafield_device.py similarity index 99% rename from tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py rename to tangostationcontrol/test/devices/test_antennafield_device.py index 15eed2ec48d984ef0acc64a5913860aba520ce3d..10ac1ccec6d9a6c4afbb192d18bb9ec072af38e3 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py +++ b/tangostationcontrol/test/devices/test_antennafield_device.py @@ -24,8 +24,8 @@ from tangostationcontrol.devices.antennafield import ( AntennaQuality, AntennaUse, ) -from tangostationcontrol.test import base -from tangostationcontrol.test.devices import device_base +from test import base +from test.devices import device_base logger = logging.getLogger() diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py b/tangostationcontrol/test/devices/test_device_temperature_manager.py similarity index 94% rename from tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py rename to tangostationcontrol/test/devices/test_device_temperature_manager.py index dc0f004821cfbb9dfefe7ba8b1e59053c3ff52d4..c01e0b9b79ccdf57ffa634168e4ef04b4cc675ea 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py +++ b/tangostationcontrol/test/devices/test_device_temperature_manager.py @@ -4,8 +4,10 @@ import time from tango.test_context import DeviceTestContext + from tangostationcontrol.devices import temperature_manager -from tangostationcontrol.test.devices import device_base + +from test.devices import device_base class TestTemperatureManagerDevice(device_base.DeviceTestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py b/tangostationcontrol/test/devices/test_observation_control_device.py similarity index 90% rename from tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py rename to tangostationcontrol/test/devices/test_observation_control_device.py index 1b502783b03ed975e66af64508d4e6b10e48a6b6..7b5c70be28c9bf76920a2e32dabbb081d2b03e40 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py +++ b/tangostationcontrol/test/devices/test_observation_control_device.py @@ -1,9 +1,10 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test import base from tangostationcontrol.test.devices import test_observation_base +from test import base + class TestObservationControlDevice( base.TestCase, test_observation_base.TestObservationBase diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py b/tangostationcontrol/test/devices/test_observation_device.py similarity index 88% rename from tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py rename to tangostationcontrol/test/devices/test_observation_device.py index 5b98c9286d3e7f410bf01ff4b502c71677a3e868..78392c400fb406fc0d1cdd569eb7c09b97698766 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py +++ b/tangostationcontrol/test/devices/test_observation_device.py @@ -1,9 +1,10 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test.devices import device_base from tangostationcontrol.test.devices import test_observation_base +from test.devices import device_base + class TestObservationDevice( device_base.DeviceTestCase, test_observation_base.TestObservationBase diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py b/tangostationcontrol/test/devices/test_psoc_device.py similarity index 93% rename from tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py rename to tangostationcontrol/test/devices/test_psoc_device.py index 3433b5ab9c85c3d7df658261d74e4ea760147ce0..aa09074a48eb05a10d8264b70c69b7dc7580181c 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py +++ b/tangostationcontrol/test/devices/test_psoc_device.py @@ -1,7 +1,7 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test.devices import device_base +from test.devices import device_base class TestPSOCDevice(device_base.DeviceTestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py b/tangostationcontrol/test/devices/test_recv_device.py similarity index 96% rename from tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py rename to tangostationcontrol/test/devices/test_recv_device.py index db0796d9e67e6b1bd205f34b8e8908cebf095628..c0336753028ad4dd28113307fcbf9c950e068b0e 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py +++ b/tangostationcontrol/test/devices/test_recv_device.py @@ -7,7 +7,8 @@ from tango import DevFailed from tangostationcontrol.common.constants import N_rcu, N_rcu_inp, N_elements from tangostationcontrol.devices import recv -from tangostationcontrol.test.devices import device_base + +from test.devices import device_base class TestRecvDevice(device_base.DeviceTestCase): diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/__init__.py b/tangostationcontrol/test/prometheus/__init__.py similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/toolkit/__init__.py rename to tangostationcontrol/test/prometheus/__init__.py diff --git a/tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py b/tangostationcontrol/test/prometheus/test_archiver_policy.py similarity index 96% rename from tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py rename to tangostationcontrol/test/prometheus/test_archiver_policy.py index 468b61ca243652938d24e7017f0e73eeb1a2f3b4..519bdb7f45f35a5a343731737df3c48343030ec0 100644 --- a/tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py +++ b/tangostationcontrol/test/prometheus/test_archiver_policy.py @@ -5,7 +5,7 @@ import importlib.util import os import sys -from tangostationcontrol.test import base +from test import base module_name = "ArchiverPolicy" file_path = os.path.join( diff --git a/tangostationcontrol/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin b/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin similarity index 100% rename from tangostationcontrol/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin rename to tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin diff --git a/tangostationcontrol/test/statistics/__init__.py b/tangostationcontrol/test/statistics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..68ddd5cdc3efaa38e853aef337c08beb99c50c4c --- /dev/null +++ b/tangostationcontrol/test/statistics/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 diff --git a/tangostationcontrol/test/toolkit/__init__.py b/tangostationcontrol/test/toolkit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..68ddd5cdc3efaa38e853aef337c08beb99c50c4c --- /dev/null +++ b/tangostationcontrol/test/toolkit/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py b/tangostationcontrol/test/toolkit/test_archiver_config_file.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py rename to tangostationcontrol/test/toolkit/test_archiver_config_file.py index 319e183c2f6c75037618ce52a2ce15e68b1bb238..34b8b8d548df6f492a171b76226c98f38e9a52f6 100644 --- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py +++ b/tangostationcontrol/test/toolkit/test_archiver_config_file.py @@ -4,7 +4,7 @@ import json import pkg_resources -from tangostationcontrol.test import base +from test import base from tangostationcontrol.toolkit.archiver_configurator import ( get_global_env_parameters, get_parameters_from_attribute, diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py b/tangostationcontrol/test/toolkit/test_archiver_configurator.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py rename to tangostationcontrol/test/toolkit/test_archiver_configurator.py index 09f0e3b88d5e0421706327162264990f2fad3bf2..f9c9d7b6223c61b264821325d49f02eb9829707c 100644 --- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py +++ b/tangostationcontrol/test/toolkit/test_archiver_configurator.py @@ -4,7 +4,7 @@ import json import pkg_resources -from tangostationcontrol.test import base +from test import base from tangostationcontrol.toolkit.archiver_configurator import ( get_parameters_from_attribute, get_global_env_parameters, diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py b/tangostationcontrol/test/toolkit/test_archiver_util.py similarity index 98% rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py rename to tangostationcontrol/test/toolkit/test_archiver_util.py index b53401778309a86faaa315890bea15382e264dcb..587ee73ae15d58b7cda42c0a8b95c5ac39872894 100644 --- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py +++ b/tangostationcontrol/test/toolkit/test_archiver_util.py @@ -1,7 +1,7 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from tangostationcontrol.test import base +from test import base from tangostationcontrol.toolkit.archiver_util import ( get_attribute_from_fqdn, split_tango_name, diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py b/tangostationcontrol/test/toolkit/test_mib_compiler.py similarity index 95% rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py rename to tangostationcontrol/test/toolkit/test_mib_compiler.py index 36c723c0751d74c83003ef65a30febb231f38e77..3ad5c14115b3252b7a1fce18666f58c47cacc351 100644 --- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py +++ b/tangostationcontrol/test/toolkit/test_mib_compiler.py @@ -7,7 +7,7 @@ from os.path import isfile from tempfile import TemporaryDirectory from unittest import mock -from tangostationcontrol.test import base +from test import base from tangostationcontrol.toolkit.mib_compiler import mib_compiler diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini index 1e04149a8967d99be2aaad6f6d75f53cf656da59..4c335182cef1065cc22df3b63375ab65db1f5236 100644 --- a/tangostationcontrol/tox.ini +++ b/tangostationcontrol/tox.ini @@ -1,10 +1,13 @@ [tox] -minversion = 3.20 +min_version = 4.3.3 +requires = + tox-ignore-env-name-mismatch ~= 0.2.0 envlist = black,pep8,pylint,py310,docs -skipsdist = True [testenv] usedevelop = True +package = wheel +wheel_build_env = .pkg ; Python and tox variables are used to access modules and binaries instead of ; directly. This makes the setup robust for using sitepackages=True. install_command = {envbindir}/pip3 install {opts} {packages} @@ -24,8 +27,8 @@ commands_pre = {envpython} --version pip install --no-cache 'PyTango>=9.3.6,<9.4.0' commands = - {envpython} -m stestr --version - {envpython} -m stestr run {posargs} + {envpython} -m pytest --version + {envpython} -m pytest test/{posargs} ; We can't detect the current Python version for an environment dynamically ; so each Python version specific job needs its own envdir. @@ -50,24 +53,19 @@ commands = {envpython} -m coverage report --omit='*test*' [testenv:{cover,coverage}] -; stestr does not natively support generating coverage reports use -; `PYTHON=python -m coverage run....` to overcome this. +runner = ignore_env_name_mismatch +envdir = {toxworkdir}/coverage setenv = VIRTUAL_ENV={envdir} - PYTHON={envpython} -m coverage run --source tangostationcontrol --parallel-mode commands = - {envpython} -m stestr --version - {envpython} -m coverage --version - {envpython} -m coverage erase - {envpython} -m stestr run {posargs} - {envpython} -m coverage combine - {envpython} -m coverage html --omit='*test*' -d cover - {envpython} -m coverage xml -o coverage.xml - {envpython} -m coverage report --omit='*test*' + {envpython} --version + {envpython} -m pytest --version + {envpython} -m pytest test --cov-report xml --cov-report html --cov-report xml:coverage.xml --cov=test # Use generative name and command prefixes to reuse the same virtualenv # for all linting jobs. [testenv:{pep8,black,pylint,format}] +runner = ignore_env_name_mismatch usedevelop = False envdir = {toxworkdir}/linting commands = @@ -104,6 +102,7 @@ commands = {envpython} -m build [testenv:docs] envdir = {toxworkdir}/docs deps = + -r{toxinidir}/requirements.txt -r{toxinidir}/docs/docs-requirements.txt commands = sphinx-build --version