Source code for tango_simlib.utilities.testutils

#########################################################################################
# Copyright 2017 SKA South Africa (http://ska.ac.za/)                                   #
#                                                                                       #
# BSD license - see LICENSE.txt for details                                             #
#########################################################################################
from __future__ import absolute_import, division, print_function
from future import standard_library

standard_library.install_aliases()  # noqa: E402

import errno
import logging
import mock
import os
import shutil
import sys
import tempfile
import time

from builtins import object

LOGGER = logging.getLogger(__name__)


[docs]def cleanup_tempfile(test_instance, unlink=False, *mkstemp_args, **mkstemp_kwargs): """Return filename of a new tempfile and add cleanup callback to test_instance. Will not raise an error if the file is not present when trying to delete. If unlink=True the actual temp file will be deleted immediately. This is useful if you want to check behaviour in absence of a named file. Extra args and kwargs are passed on to the tempfile.mkstemp call. """ os_fh, fname = tempfile.mkstemp(*mkstemp_args, **mkstemp_kwargs) os.close(os_fh) # Close the low-level file handle if unlink: os.unlinkI(fname) def cleanup(): try: os.unlink(fname) except OSError as e: if e.errno == errno.ENOENT: pass else: raise test_instance.addCleanup(cleanup) return fname
[docs]def cleanup_tempdir(test_instance, *mkdtemp_args, **mkdtemp_kwargs): """Return filname of a new tempfile and add cleanup callback to test_instance. Will not raise an error if the directory is not present when trying to delete. Extra args and kwargs are passed on to the tempfile.mkdtemp call """ dirname = tempfile.mkdtemp(*mkdtemp_args, **mkdtemp_kwargs) def cleanup(): try: shutil.rmtree(dirname) except OSError as e: if e.errno == errno.ENOENT: pass else: raise test_instance.addCleanup(cleanup) return dirname
[docs]def set_attributes_polling(test_case, device_proxy, device_server, poll_periods): """Set attribute polling and restore after test Parameters ---------- test_case : unittest.TestCase instance Unit test case class instance device_proxy : PyTango.DeviceProxy instance The Tango device proxy instance device_server : PyTango.Device instance The instance of the device class `device_proxy` is talking to poll_periods : dict {"attribute_name" : poll_period} `poll_poriod` in milliseconds as per Tango APIs, 0 or falsy to disable polling. Returns ------- restore_polling : function This function can be used to restore polling if it is to happen before the end of the test. Should be idempotent if only one set_attributes_polling() is called per test. """ # TODO (NM 2016-04-11) check if this is still needed after upgrade to Tango 9.x For # some reason it only works if the device_proxy is used to set polling, but the # device_server is used to clear the polling. If polling is cleared using device_proxy # it seem to be impossible to restore the polling afterwards. attributes = poll_periods.keys() initial_polling = { attr: device_proxy.get_attribute_poll_period(attr) for attr in attributes } retry_time = 0.5 for attr in attributes: initial_period = initial_polling[attr] new_period = poll_periods[attr] # Disable polling for attributes with poll_period of zero / falsy # zero initial_period implies no polling currently configed if not new_period and initial_period != 0: LOGGER.debug("not setting polling for {}".format(attr)) device_server.stop_poll_attribute(attr) else: # Set the polling LOGGER.debug("setting polling for {}".format(attr)) try: device_proxy.poll_attribute(attr, new_period) # TODO See (NM 2016-04-11) comment below about back-to-back calls time.sleep(0.05) except Exception: retry = True LOGGER.warning( "Setting polling of attribute {} in {} due to unhandled" "exception in poll_attribute command".format(attr, retry_time), exc_info=True, ) else: retry = False if retry: time.sleep(retry_time) device_proxy.poll_attribute(attr, new_period) def restore_polling(): """Restore initial polling, for use during cleanup / teardown""" for attr, period in initial_polling.items(): if period == 0: continue # zero period implies no polling, nothing to do try: device_proxy.poll_attribute(attr, period) # TODO (NM 2016-04-11) For some reason Tango doesn't seem to handle # back-to-back calls, and even with the sleep it sometimes goes bad. Need # to check if this is fixed (and core dumps) when we upgrade to Tango 9.x time.sleep(0.05) except Exception: retry = True LOGGER.warning( "retrying restore of attribute {} in {} due to unhandled" "exception in poll_attribute command".format(attr, retry_time), exc_info=True, ) else: retry = False if retry: time.sleep(retry_time) device_proxy.poll_attribute(attr, period) test_case.addCleanup(restore_polling) return restore_polling
[docs]def disable_attributes_polling(test_case, device_proxy, device_server, attributes): """Disable polling for a tango device server, en re-eable at end of test""" new_periods = {attr: 0 for attr in attributes} return set_attributes_polling(test_case, device_proxy, device_server, new_periods)
[docs]class ClassCleanupUnittestMixin(object): """Implement class-level setup/deardown semantics that emulate addCleanup() Subclasses can define a setUpClassWithCleanup() method that wraps addCleanup such that cls.addCleanup() can be used to add cleanup methods that will be called at class tear-down time. """ _class_cleanups = []
[docs] @classmethod def setUpClassWithCleanup(cls): """Do class-level setup and ensure that cleanup functions are called It is inteded that subclasses override this class method In this method calls to `cls.addCleanup` is forwarded to `cls.addCleanupClass`, which means callables registered with `cls.addCleanup()` is added to the class-level cleanup function stack. """ super(ClassCleanupUnittestMixin, cls).setUpClassWithCleanup()
[docs] @classmethod def addCleanupClass(cls, function, *args, **kwargs): """Add a cleanup that will be called at class tear-down time""" cls._class_cleanups.append((function, args, kwargs))
[docs] @classmethod def doCleanupsClass(cls): """Run class-level cleanups registered with `cls.addCleanupClass()`""" results = [] while cls._class_cleanups: function, args, kwargs = cls._class_cleanups.pop() try: function(*args, **kwargs) except Exception: LOGGER.exception("Exception calling class cleanup function") results.append(sys.exc_info()) if results: LOGGER.error("Exception(s) raised during class cleanup")
[docs] @classmethod def setUpClass(cls): """Call `setUpClassWithCleanup` with `cls.addCleanup` for class-level cleanup Any exceptions raised during `cls.setUpClassWithCleanup` will result in the cleanups registered up to that point being called before logging the exception with traceback. """ try: with mock.patch.object(cls, "addCleanup") as cls_addCleanup: cls_addCleanup.side_effect = cls.addCleanupClass cls.setUpClassWithCleanup() except Exception: LOGGER.exception("Exception during setUpClass") cls.doCleanupsClass()
[docs] @classmethod def tearDownClass(cls): cls.doCleanupsClass()