From 3533a1436ab67d3b51c49abd4bf32bf000544425 Mon Sep 17 00:00:00 2001 From: cpascual Date: Fri, 13 Jan 2017 11:45:44 +0100 Subject: [PATCH 1/6] Fix-365: Refactor TangoAttribute push_event Taurus4 introduced a regression related to the merge of TaurusAttribute and TaurusConfiguration. The TangoAttribute.push_event method handles all events but it doesn't do it properly for configuration-related error events received before the TangoAttribute has been initialized. In this situation, the TangoAttribute ends up flagged as subscribed but the subscription was not actually done, preventing to become properly subscribed when a non-error event comes later on. It also may incorrectly deactivate the polling mechanism. Refactor the push_event code to clean it and correctly handle conf errors. --- lib/taurus/core/tango/tangoattribute.py | 119 +++++++++++++++--------- 1 file changed, 74 insertions(+), 45 deletions(-) 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 From 14d108479af4e72a12c07e3b96c1a0b128aab5e5 Mon Sep 17 00:00:00 2001 From: Carlos Falcon Date: Wed, 18 Jan 2017 11:28:42 +0100 Subject: [PATCH 2/6] Skip test_timer in travis The test-timer test is not deterministic. It may fail occasionally (e.g. if the machine is overloaded) cause testsuite failures that interfere with CI. Exclude this test in travis. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 850872ef7..b175a64de 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 "taurustestsuite -e 'taurus\.core\.util\.test\.test_timer'" From 6a1e970819d8895681eae8d617b8613bcec17f2c Mon Sep 17 00:00:00 2001 From: cfalcon Date: Wed, 18 Jan 2017 17:27:06 +0100 Subject: [PATCH 3/6] Use write+read in Tango write_read_attr tests AttributeTestCase.write_read_attr helper uses attr.write(with_read=True) for reading the value. This may lead to reading None in some cases (e.g. attr.isUsingEvents() returns True) and cause the test to fail. Do separated write and read (without cache) instead. --- lib/taurus/core/tango/test/test_tangoattribute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/taurus/core/tango/test/test_tangoattribute.py b/lib/taurus/core/tango/test/test_tangoattribute.py index 33a53fec8..f6cbfb5fd 100644 --- a/lib/taurus/core/tango/test/test_tangoattribute.py +++ b/lib/taurus/core/tango/test/test_tangoattribute.py @@ -764,7 +764,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__)) From 58613d4c725169a8e845350ddf099047f61df7e0 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Wed, 18 Jan 2017 16:55:43 +0100 Subject: [PATCH 4/6] Make taurus.qt tests more robust (partial workaround for #261) BaseWidgetTestCase based tests make calls to QApplication.processEvents to emulate the Qt main loop. It seems that a single call is sometimes insufficient to process all needed events for the test, which causes spurious failures (see https://github.com/taurus-org/taurus/issues/261) As a workaround, implement a BaseWidgetTestCase.processEvents method that loops several times over QApplication.processEvents and use it in the tests. Also remove previous workaround based on sleeps after setModel. Also re-enable skipped tauruslabel background tests since this fix seems to make them work again. --- lib/taurus/qt/qtgui/base/test/test_taurusbase.py | 5 ----- .../qt/qtgui/display/test/test_tauruslabel.py | 6 +++--- lib/taurus/qt/qtgui/panel/test/test_taurusvalue.py | 4 ++-- lib/taurus/qt/qtgui/test/base.py | 13 ++++++------- 4 files changed, 11 insertions(+), 17 deletions(-) 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): From c94e4478417fbf83c7b34697f27a97ab73156f25 Mon Sep 17 00:00:00 2001 From: cfalcon Date: Wed, 18 Jan 2017 12:24:37 +0100 Subject: [PATCH 5/6] Enable previously disabled tango attribute tests Some tests were commented out because they break test isolation. The fix of #365 may have solved the root cause of this, so reenable them. --- .../core/tango/test/test_tangoattribute.py | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/lib/taurus/core/tango/test/test_tangoattribute.py b/lib/taurus/core/tango/test/test_tangoattribute.py index f6cbfb5fd..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 From 46ac23a0d92bcbe1d54f5fb31fbb77774a383a44 Mon Sep 17 00:00:00 2001 From: cpascual Date: Thu, 19 Jan 2017 10:19:38 +0100 Subject: [PATCH 6/6] Wait on DS start to prevent some race conditions in tests Some random test failures (see #261) seem to be related to test DS not being properly initialized. Add a waiting time (controlled by the TAURUS_STARTER_WAIT env var) during starting of DS to alleviate this. Set TAURUS_STARTER_WAIT to 5 in Travis (i.e., wait 5 s after each DS start) --- .travis.yml | 2 +- lib/taurus/core/tango/starter.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b175a64de..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 -e 'taurus\.core\.util\.test\.test_timer'" + - 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: