diff --git a/.travis.yml b/.travis.yml index 850872ef7..395aa4eab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ before_install: script: - docker exec taurus-test /bin/bash -c "cd taurus ; python setup.py install" - - docker exec taurus-test /bin/bash -c "taurustestsuite" + - docker exec taurus-test /bin/bash -c "TAURUS_STARTER_WAIT=5 taurustestsuite -e 'taurus\.core\.util\.test\.test_timer'" diff --git a/lib/taurus/core/tango/starter.py b/lib/taurus/core/tango/starter.py index bb5b6b051..290dcc718 100644 --- a/lib/taurus/core/tango/starter.py +++ b/lib/taurus/core/tango/starter.py @@ -112,8 +112,12 @@ def startDs(self, synch=True, wait_seconds=10): if self.isRunning(): _log.info('Server %s has been started' % self.ds_name) ############################################################## - # TODO: this workaround doesn't seem necessary (see isRunning) - # time.sleep(3) + # Workaround to avoid race conditions + # TODO: Find root cause of race condition and fix + _wait = float(os.environ.get('TAURUS_STARTER_WAIT', 0)) + if _wait: + _log.info('Waiting %g s after start' % _wait) + time.sleep(_wait) ############################################################## return else: diff --git a/lib/taurus/core/tango/tangoattribute.py b/lib/taurus/core/tango/tangoattribute.py index 233958109..98d440e4b 100755 --- a/lib/taurus/core/tango/tangoattribute.py +++ b/lib/taurus/core/tango/tangoattribute.py @@ -266,8 +266,6 @@ def __init__(self, name, parent, **kwargs): self.call__init__(TaurusAttribute, name, parent, **kwargs) - self._events_working = False - attr_info = None if parent: attr_name = self.getSimpleName() @@ -611,14 +609,14 @@ def _subscribeEvents(self): self.__subscription_state = SubscriptionState.Subscribing self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.CHANGE_EVENT, - self, []) + self, []) # connects to self.push_event callback except: self.__subscription_state = SubscriptionState.PendingSubscribe self._activatePolling() self.__chg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.CHANGE_EVENT, - self, [], True) + self, [], True) # connects to self.push_event callback def _unsubscribeEvents(self): # Careful in this method: This is intended to be executed in the cleanUp @@ -659,7 +657,7 @@ def _subscribeConfEvents(self): self.__cfg_evt_id = self.__dev_hw_obj.subscribe_event( attr_name, PyTango.EventType.ATTR_CONF_EVENT, - self, [], True) + self, [], True) # connects to self.push_event callback except PyTango.DevFailed, e: self.debug("Error trying to subscribe to CONFIGURATION events.") self.traceback() @@ -690,57 +688,88 @@ def _unsubscribeConfEvents(self): self.trace(str(e)) def push_event(self, event): - """Method invoked by the PyTango layer when a change event occurs. - Default implementation propagates the event to all listeners.""" + """Method invoked by the PyTango layer when an event occurs. + It propagates the event to listeners and delegates other tasks to + specific handlers for different event types. + """ + # if it is a configuration event + if isinstance(event, PyTango.AttrConfEventData): + etype, evalue = self._pushConfEvent(event) + # if it is an attribute event + else: + etype, evalue = self._pushAttrEvent(event) - curr_time = time.time() + # notify the listeners if required (i.e, if etype is not None) + if etype is None: + return manager = Manager() sm = self.getSerializationMode() + listeners = tuple(self._listeners) + if sm == TaurusSerializationMode.Concurrent: + manager.addJob(self.fireEvent, None, etype, evalue, + listeners=listeners) + else: + self.fireEvent(etype, evalue, listeners=listeners) + + def _pushAttrEvent(self, event): + """Handler of (non-configuration) events from the PyTango layer. + It handles the subscription and the (de)activation of polling + + :param event: (A PyTango event) + + :return: (evt_type, evt_value) Tuple containing the event type and the + event value. evt_type is a `TaurusEventType` (or None to + indicate that there should not be notification to listeners). + evt_value is a TaurusValue, an Exception, or None. + """ if not event.err: - # if it is a configuration event - if isinstance(event, PyTango.AttrConfEventData): - event_type = TaurusEventType.Config - self._decodeAttrInfoEx(event.attr_conf) - # make sure that there is a self.__attr_value - if self.__attr_value is None: - # TODO: maybe we can avoid this read? - self.__attr_value = self.getValueObj(cache=False) - # if it is an attribute event - else: - event_type = TaurusEventType.Change - self.__attr_value, self.__attr_err = self.decode( - event.attr_value), None - self.__subscription_state = SubscriptionState.Subscribed - self.__subscription_event.set() - if not self.isPollingForced(): - self._deactivatePolling() - # notify the listeners - listeners = tuple(self._listeners) - if sm == TaurusSerializationMode.Concurrent: - manager.addJob(self.fireEvent, None, event_type, - self.__attr_value, listeners=listeners) - else: - self.fireEvent(event_type, self.__attr_value, - listeners=listeners) + self.__attr_value, self.__attr_err = self.decode( + event.attr_value), None + self.__subscription_state = SubscriptionState.Subscribed + self.__subscription_event.set() + if not self.isPollingForced(): + self._deactivatePolling() + return TaurusEventType.Change, self.__attr_value + elif event.errors[0].reason in EVENT_TO_POLLING_EXCEPTIONS: - if self.isPollingActive(): - return - self.info("Activating polling. Reason: %s", event.errors[0].reason) - self.__subscription_state = SubscriptionState.PendingSubscribe - self._activatePolling() + if not self.isPollingActive(): + self.info("Activating polling. Reason: %s", + event.errors[0].reason) + self.__subscription_state = SubscriptionState.PendingSubscribe + self._activatePolling() + return None, None + else: self.__attr_value, self.__attr_err = None, PyTango.DevFailed( *event.errors) self.__subscription_state = SubscriptionState.Subscribed self.__subscription_event.set() self._deactivatePolling() - listeners = tuple(self._listeners) - if sm == TaurusSerializationMode.Concurrent: - manager.addJob(self.fireEvent, None, TaurusEventType.Error, - self.__attr_err, listeners=listeners) - else: - self.fireEvent(TaurusEventType.Error, self.__attr_err, - listeners=listeners) + return TaurusEventType.Error, self.__attr_err + + def _pushConfEvent(self, event): + """Handler of AttrConfEventData events from the PyTango layer. + + :param event: (PyTango.AttrConfEventData) + + :return: (evt_type, evt_value) Tuple containing the event type and the + event value. evt_type is a `TaurusEventType` (or None to + indicate that there should not be notification to listeners). + evt_value is a TaurusValue, an Exception, or None. + """ + if not event.err: + # update conf-related attributes + self._decodeAttrInfoEx(event.attr_conf) + # make sure that there is a self.__attr_value + if self.__attr_value is None: + # TODO: maybe we can avoid this read? + self.__attr_value = self.getValueObj(cache=False) + return TaurusEventType.Config, self.__attr_value + + else: + self.__attr_value, self.__attr_err = None, PyTango.DevFailed( + *event.errors) + return TaurusEventType.Error, self.__attr_err def isWrite(self, cache=True): return self.getTangoWritable(cache) == PyTango.AttrWriteType.WRITE diff --git a/lib/taurus/core/tango/test/test_tangoattribute.py b/lib/taurus/core/tango/test/test_tangoattribute.py index 33a53fec8..cd40f85d2 100644 --- a/lib/taurus/core/tango/test/test_tangoattribute.py +++ b/lib/taurus/core/tango/test/test_tangoattribute.py @@ -55,58 +55,55 @@ # ============================================================================== # Test writing fragment values -## -# DISABLED: these tests break test isolation (looks like Reset is not working -# for configurations). Until we solve it, we disable all these tests -# -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='range', value=[float('-inf'), float('inf')], -# expected=[Quantity(float('-inf')), Quantity(float('inf'))] -# ) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='range', value=[Quantity(float('-inf')), -# Quantity(float('inf'))], -# expected=[Quantity(float('-inf')), Quantity(float('inf'))]) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='range', value=[100, 300], -# expected=[Quantity(100), Quantity(300)]) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='range', value=[Quantity(100), Quantity(300)], -# expected=[Quantity(100), Quantity(300)]) -# @insertTest(helper_name='write_read_conf', attr_name='float_scalar', -# cfg='range', value=[Quantity(-5, 'mm'), Quantity(5, 'mm')], -# expected=[Quantity(-0.005, 'm'), Quantity(5, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='short_spectrum', -# cfg='label', value='Just a Test', -# expected='Just a Test') -# @insertTest(helper_name='write_read_conf', attr_name='boolean_spectrum', -# cfg='label', value='Just_a_Test', -# expected='Just_a_Test') -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar', -# cfg='warnings', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], -# expected=[Quantity(-2, 'mm'), Quantity(0.002, 'm')]) -# @insertTest(helper_name='write_read_conf', attr_name='short_image', -# cfg='warnings', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], -# expected=[Quantity(-0.002, 'm'), Quantity(2, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='float_image', -# cfg='warnings', value=[Quantity(-0.75, 'mm'), -# Quantity(0.75, 'mm')], -# expected=[Quantity(-0.00075, 'm'), Quantity(0.75, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='warnings', value=[100, 300], -# expected=[Quantity(100), Quantity(300)]) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar', -# cfg='alarms', value=[Quantity(-50, 'mm'), Quantity(50, 'mm')], -# expected=[Quantity(-50, 'mm'), Quantity(50, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='short_image', -# cfg='alarms', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], -# expected=[Quantity(-0.002, 'm'), Quantity(2, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='float_image', -# cfg='alarms', value=[Quantity(-0.75, 'mm'), Quantity(0.75, 'mm')], -# expected=[Quantity(-0.00075, 'm'), Quantity(0.75, 'mm')]) -# @insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', -# cfg='alarms', value=[100, 300], -# expected=[Quantity(100), Quantity(300)]) + +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='range', value=[float('-inf'), float('inf')], + expected=[Quantity(float('-inf')), Quantity(float('inf'))] + ) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='range', value=[Quantity(float('-inf')), + Quantity(float('inf'))], + expected=[Quantity(float('-inf')), Quantity(float('inf'))]) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='range', value=[100, 300], + expected=[Quantity(100), Quantity(300)]) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='range', value=[Quantity(100), Quantity(300)], + expected=[Quantity(100), Quantity(300)]) +@insertTest(helper_name='write_read_conf', attr_name='float_scalar', + cfg='range', value=[Quantity(-5, 'mm'), Quantity(5, 'mm')], + expected=[Quantity(-0.005, 'm'), Quantity(5, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='short_spectrum', + cfg='label', value='Just a Test', + expected='Just a Test') +@insertTest(helper_name='write_read_conf', attr_name='boolean_spectrum', + cfg='label', value='Just_a_Test', + expected='Just_a_Test') +@insertTest(helper_name='write_read_conf', attr_name='short_scalar', + cfg='warnings', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], + expected=[Quantity(-2, 'mm'), Quantity(0.002, 'm')]) +@insertTest(helper_name='write_read_conf', attr_name='short_image', + cfg='warnings', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], + expected=[Quantity(-0.002, 'm'), Quantity(2, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='float_image', + cfg='warnings', value=[Quantity(-0.75, 'mm'), + Quantity(0.75, 'mm')], + expected=[Quantity(-0.00075, 'm'), Quantity(0.75, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='warnings', value=[100, 300], + expected=[Quantity(100), Quantity(300)]) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar', + cfg='alarms', value=[Quantity(-50, 'mm'), Quantity(50, 'mm')], + expected=[Quantity(-50, 'mm'), Quantity(50, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='short_image', + cfg='alarms', value=[Quantity(-2, 'mm'), Quantity(2, 'mm')], + expected=[Quantity(-0.002, 'm'), Quantity(2, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='float_image', + cfg='alarms', value=[Quantity(-0.75, 'mm'), Quantity(0.75, 'mm')], + expected=[Quantity(-0.00075, 'm'), Quantity(0.75, 'mm')]) +@insertTest(helper_name='write_read_conf', attr_name='short_scalar_nu', + cfg='alarms', value=[100, 300], + expected=[Quantity(100), Quantity(300)]) # ============================================================================== # Test encode-decode of empty arrays @@ -764,7 +761,8 @@ def write_read_attr(self, attrname=None, setvalue=None, expected=None, if setvalue is None: read_value = a.read() else: - read_value = a.write(setvalue, with_read=True) + a.write(setvalue) + read_value = a.read(cache=False) msg = ('read() for "%s" did not return a TangoAttrValue (got a %s)' % (attrname, read_value.__class__.__name__)) diff --git a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py index 9e646c8f3..3c6c915d4 100644 --- a/lib/taurus/qt/qtgui/base/test/test_taurusbase.py +++ b/lib/taurus/qt/qtgui/base/test/test_taurusbase.py @@ -94,11 +94,6 @@ def setUp(self): def getDisplayValue(self, model=None, expected=None): '''Check if setModel works when using parent model''' self._widget.setModel(model) - # ---------------------------- - # workaround for https://sourceforge.net/p/tauruslib/tickets/334/ - import time - time.sleep(BaseWidgetTestCase._BUG_334_WORKAROUND_TIME) - # ---------------------------- got = self._widget.getDisplayValue() msg = ('getDisplayValue for "%s" should be %r (got %r)' % (model, expected, got)) diff --git a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py index 12ce0be56..d2a78e038 100644 --- a/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py +++ b/lib/taurus/qt/qtgui/display/test/test_tauruslabel.py @@ -143,7 +143,7 @@ def text(self, model=None, expected=None, fgRole=None, maxdepr=0): self._widget.setModel(model) if fgRole is not None: self._widget.setFgRole(fgRole) - self._app.processEvents() + self.processEvents(repetitions=10, sleep=.1) got = str(self._widget.text()) msg = ('wrong text for "%s":\n expected: %s\n got: %s' % (model, expected, got)) @@ -156,11 +156,11 @@ def text(self, model=None, expected=None, fgRole=None, maxdepr=0): # expected: (0, 255, 0) # got: (239, 240, 241) # <-- these values change (unitialized garbage?) # ~~~~~~~~~~~~~~~~~~~~~~~ - @unittest.skip('bgRole tests fail randomly. Skip until fixed.') + #@unittest.skip('bgRole tests fail randomly. Skip until fixed.') def bgRole(self, model=None, expected=None, bgRole=None, maxdepr=0): '''Check that the label text''' self._widget.setModel(model) - self._app.processEvents() + self.processEvents(repetitions=10, sleep=.1) if bgRole is not None: self._widget.setBgRole(bgRole) p = self._widget.palette() diff --git a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py index 32e373ee3..ffddeee6a 100644 --- a/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py +++ b/lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py @@ -54,7 +54,7 @@ def test_bug126(self): self._widget.setModel('tango:' + DEV_NAME + '/double_scalar') label = 'MIXEDcase' w.setLabelConfig(label) - self._app.processEvents() # required + self.processEvents(repetitions=10, sleep=.1) shownLabel = str(w.labelWidget().text()) msg = 'Shown label ("%s") differs from set label ("%s")' % (shownLabel, label) @@ -66,7 +66,7 @@ def texts(self, model=None, expected=None, fgRole=None, maxdepr=0): self._widget.setModel(model) if fgRole is not None: self._widget.setFgRole(fgRole) - self._app.processEvents() + self.processEvents(repetitions=10, sleep=.1) got = (str(self._widget.labelWidget().text()), str(self._widget.readWidget().text()), str(self._widget.writeWidget().displayText()), diff --git a/lib/taurus/qt/qtgui/test/base.py b/lib/taurus/qt/qtgui/test/base.py index 41dc1b091..554c739c4 100644 --- a/lib/taurus/qt/qtgui/test/base.py +++ b/lib/taurus/qt/qtgui/test/base.py @@ -25,6 +25,7 @@ """Utilities for creating generic tests for Taurus widgets""" +import time import taurus.core from taurus.external import unittest from taurus.qt.qtgui.application import TaurusApplication @@ -48,8 +49,6 @@ class BaseWidgetTestCase(object): initargs = [] initkwargs = {} - _BUG_334_WORKAROUND_TIME = 1 # TODO: remove this when proper fix is done - def setUp(self): """ Preconditions: @@ -71,11 +70,6 @@ def setUp(self): if self._klass is not None: self._widget = self._klass(*self.initargs, **self.initkwargs) - # ---------------------------- - # workaround for https://sourceforge.net/p/tauruslib/tickets/334/ - import time - time.sleep(self._BUG_334_WORKAROUND_TIME) - # ---------------------------- def assertMaxDeprecations(self, maximum, msg=None): """Assertion method that checks that the number of deprecations issued @@ -89,6 +83,11 @@ def assertMaxDeprecations(self, maximum, msg=None): (deps, maximum, self._depCounter.pretty())) self.assertTrue(deps <= maximum, msg) + def processEvents(self, repetitions=1, sleep=0): + for i in xrange(repetitions): + time.sleep(sleep) + self._app.processEvents() + class GenericWidgetTestCase(BaseWidgetTestCase):