Move `call_until_true` to tempest/lib

This `call_until_true()` is handy and could be used in Tempest plugins.
Let's move it to tempest/lib.

Also add some unit tests.

Change-Id: Ie379030baa336239e6027c8f3cdbeb74c561f66b
diff --git a/releasenotes/notes/move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml b/releasenotes/notes/move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml
new file mode 100644
index 0000000..543cf7b
--- /dev/null
+++ b/releasenotes/notes/move-call-until-true-to-tempest-lib-c9ea70dd6fe9bd15.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+  - The ``call_until_true`` function is moved from the ``tempest.test`` module
+   to the ``tempest.lib.common.utils.test_utils`` module. Backward
+   compatibilty is preserved until Ocata.
diff --git a/requirements.txt b/requirements.txt
index d698cda..a773d16 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,3 +22,4 @@
 PrettyTable<0.8,>=0.7 # BSD
 os-testr>=0.7.0 # Apache-2.0
 urllib3>=1.15.1 # MIT
+debtcollector>=1.2.0 # Apache-2.0
diff --git a/tempest/api/compute/admin/test_simple_tenant_usage.py b/tempest/api/compute/admin/test_simple_tenant_usage.py
index a4ed8dc..dbc22e0 100644
--- a/tempest/api/compute/admin/test_simple_tenant_usage.py
+++ b/tempest/api/compute/admin/test_simple_tenant_usage.py
@@ -16,6 +16,7 @@
 import datetime
 
 from tempest.api.compute import base
+from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions as e
 from tempest import test
 
@@ -59,8 +60,8 @@
                 return True
             except e.InvalidHTTPResponseBody:
                 return False
-        self.assertEqual(test.call_until_true(is_valid, duration, 1), True,
-                         "%s not return valid response in %s secs" % (
+        self.assertEqual(test_utils.call_until_true(is_valid, duration, 1),
+                         True, "%s not return valid response in %s secs" % (
                              func.__name__, duration))
         return self.resp
 
diff --git a/tempest/lib/common/utils/test_utils.py b/tempest/lib/common/utils/test_utils.py
index 50a1a7d..3b28701 100644
--- a/tempest/lib/common/utils/test_utils.py
+++ b/tempest/lib/common/utils/test_utils.py
@@ -14,6 +14,7 @@
 #    under the License.
 import inspect
 import re
+import time
 
 from oslo_log import log as logging
 
@@ -83,3 +84,24 @@
         return func(*args, **kwargs)
     except exceptions.NotFound:
         pass
+
+
+def call_until_true(func, duration, sleep_for):
+    """Call the given function until it returns True (and return True)
+
+    or until the specified duration (in seconds) elapses (and return False).
+
+    :param func: A zero argument callable that returns True on success.
+    :param duration: The number of seconds for which to attempt a
+        successful call of the function.
+    :param sleep_for: The number of seconds to sleep after an unsuccessful
+                      invocation of the function.
+    """
+    now = time.time()
+    timeout = now + duration
+    while now < timeout:
+        if func():
+            return True
+        time.sleep(sleep_for)
+        now = time.time()
+    return False
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index fdccfc3..8cd090a 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -513,7 +513,7 @@
                       'should_succeed':
                       'reachable' if should_succeed else 'unreachable'
                   })
-        result = tempest.test.call_until_true(ping, timeout, 1)
+        result = test_utils.call_until_true(ping, timeout, 1)
         LOG.debug('%(caller)s finishes ping %(ip)s in %(timeout)s sec and the '
                   'ping result is %(result)s' % {
                       'caller': caller, 'ip': ip_address, 'timeout': timeout,
@@ -857,9 +857,9 @@
                       show_floatingip(floatingip_id)['floatingip'])
             return status == result['status']
 
-        tempest.test.call_until_true(refresh,
-                                     CONF.network.build_timeout,
-                                     CONF.network.build_interval)
+        test_utils.call_until_true(refresh,
+                                   CONF.network.build_timeout,
+                                   CONF.network.build_interval)
         floating_ip = self.floating_ips_client.show_floatingip(
             floatingip_id)['floatingip']
         self.assertEqual(status, floating_ip['status'],
@@ -914,9 +914,9 @@
                 return not should_succeed
             return should_succeed
 
-        return tempest.test.call_until_true(ping_remote,
-                                            CONF.validation.ping_timeout,
-                                            1)
+        return test_utils.call_until_true(ping_remote,
+                                          CONF.validation.ping_timeout,
+                                          1)
 
     def _create_security_group(self, security_group_rules_client=None,
                                tenant_id=None,
@@ -1249,7 +1249,7 @@
                 return True
             return False
 
-        if not tempest.test.call_until_true(
+        if not test_utils.call_until_true(
             check_state, timeout, interval):
             msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
                    (node_id, state_attr, target_states))
@@ -1273,7 +1273,7 @@
                 self.get_node, instance_id=instance_id)
             return node is not None
 
-        if not tempest.test.call_until_true(
+        if not test_utils.call_until_true(
             _get_node, CONF.baremetal.association_timeout, 1):
             msg = ('Timed out waiting to get Ironic node by instance id %s'
                    % instance_id)
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index f7c7434..dba1c92 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -17,6 +17,7 @@
 from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
+from tempest.lib.common.utils import test_utils
 from tempest.scenario import manager
 from tempest import test
 
@@ -88,9 +89,9 @@
                     ['server'])
             return {'name': secgroup['name']} in body['security_groups']
 
-        if not test.call_until_true(wait_for_secgroup_add,
-                                    CONF.compute.build_timeout,
-                                    CONF.compute.build_interval):
+        if not test_utils.call_until_true(wait_for_secgroup_add,
+                                          CONF.compute.build_timeout,
+                                          CONF.compute.build_interval):
             msg = ('Timed out waiting for adding security group %s to server '
                    '%s' % (secgroup['id'], server['id']))
             raise exceptions.TimeoutException(msg)
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index e0e1204..519dbec 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -263,8 +263,9 @@
                                   if port['id'] != old_port['id']]
             return len(self.new_port_list) == 1
 
-        if not test.call_until_true(check_ports, CONF.network.build_timeout,
-                                    CONF.network.build_interval):
+        if not test_utils.call_until_true(
+                check_ports, CONF.network.build_timeout,
+                CONF.network.build_interval):
             raise exceptions.TimeoutException(
                 "No new port attached to the server in time (%s sec)! "
                 "Old port: %s. Number of new ports: %d" % (
@@ -277,8 +278,9 @@
             self.diff_list = [n for n in new_nic_list if n not in old_nic_list]
             return len(self.diff_list) == 1
 
-        if not test.call_until_true(check_new_nic, CONF.network.build_timeout,
-                                    CONF.network.build_interval):
+        if not test_utils.call_until_true(
+                check_new_nic, CONF.network.build_timeout,
+                CONF.network.build_interval):
             raise exceptions.TimeoutException("Interface not visible on the "
                                               "guest after %s sec"
                                               % CONF.network.build_timeout)
@@ -593,9 +595,9 @@
                 return False
             return True
 
-        self.assertTrue(test.call_until_true(check_new_dns_server,
-                                             renew_timeout,
-                                             renew_delay),
+        self.assertTrue(test_utils.call_until_true(check_new_dns_server,
+                                                   renew_timeout,
+                                                   renew_delay),
                         msg="DHCP renewal failed to fetch "
                             "new DNS nameservers")
 
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 364b6f5..dd86d90 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -187,10 +187,10 @@
             srv2_v6_addr_assigned = functools.partial(
                 guest_has_address, sshv4_2, ips_from_api_2['6'][i])
 
-            self.assertTrue(test.call_until_true(srv1_v6_addr_assigned,
+            self.assertTrue(test_utils.call_until_true(srv1_v6_addr_assigned,
                             CONF.validation.ping_timeout, 1))
 
-            self.assertTrue(test.call_until_true(srv2_v6_addr_assigned,
+            self.assertTrue(test_utils.call_until_true(srv2_v6_addr_assigned,
                             CONF.validation.ping_timeout, 1))
 
         self._check_connectivity(sshv4_1, ips_from_api_2['4'])
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index 60dca3d..e031ff7 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -18,6 +18,7 @@
 
 from tempest import config
 from tempest import exceptions
+from tempest.lib.common.utils import test_utils
 from tempest.scenario import manager
 from tempest import test
 
@@ -70,9 +71,9 @@
                     self.assertEqual(self.fip, result, msg)
                     return 'Verification is successful!'
 
-            if not test.call_until_true(exec_cmd_and_verify_output,
-                                        CONF.compute.build_timeout,
-                                        CONF.compute.build_interval):
+            if not test_utils.call_until_true(exec_cmd_and_verify_output,
+                                              CONF.compute.build_timeout,
+                                              CONF.compute.build_interval):
                 raise exceptions.TimeoutException('Timed out while waiting to '
                                                   'verify metadata on server. '
                                                   '%s is empty.' % md_url)
diff --git a/tempest/scenario/test_stamp_pattern.py b/tempest/scenario/test_stamp_pattern.py
index e7223c7..5fd934c 100644
--- a/tempest/scenario/test_stamp_pattern.py
+++ b/tempest/scenario/test_stamp_pattern.py
@@ -22,6 +22,7 @@
 from tempest.common import waiters
 from tempest import config
 from tempest import exceptions
+from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 from tempest.scenario import manager
@@ -89,9 +90,9 @@
             LOG.debug("Partitions:%s" % part)
             return CONF.compute.volume_device_name in part
 
-        if not test.call_until_true(_func,
-                                    CONF.compute.build_timeout,
-                                    CONF.compute.build_interval):
+        if not test_utils.call_until_true(_func,
+                                          CONF.compute.build_timeout,
+                                          CONF.compute.build_interval):
             raise exceptions.TimeoutException
 
     @decorators.skip_because(bug="1205344")
diff --git a/tempest/stress/actions/ssh_floating.py b/tempest/stress/actions/ssh_floating.py
index 4f8c6bd..c9a4d38 100644
--- a/tempest/stress/actions/ssh_floating.py
+++ b/tempest/stress/actions/ssh_floating.py
@@ -16,8 +16,8 @@
 from tempest.common.utils import data_utils
 from tempest.common import waiters
 from tempest import config
+from tempest.lib.common.utils import test_utils
 import tempest.stress.stressaction as stressaction
-import tempest.test
 
 CONF = config.CONF
 
@@ -52,8 +52,8 @@
     def check_port_ssh(self):
         def func():
             return self.tcp_connect_scan(self.floating['ip'], 22)
-        if not tempest.test.call_until_true(func, self.check_timeout,
-                                            self.check_interval):
+        if not test_utils.call_until_true(func, self.check_timeout,
+                                          self.check_interval):
             raise RuntimeError("Cannot connect to the ssh port.")
 
     def check_icmp_echo(self):
@@ -62,8 +62,8 @@
 
         def func():
             return self.ping_ip_address(self.floating['ip'])
-        if not tempest.test.call_until_true(func, self.check_timeout,
-                                            self.check_interval):
+        if not test_utils.call_until_true(func, self.check_timeout,
+                                          self.check_interval):
             raise RuntimeError("%s(%s): Cannot ping the machine.",
                                self.server_id, self.floating['ip'])
         self.logger.info("%s(%s): pong :)",
@@ -153,8 +153,8 @@
                         ['floating_ip'])
             return floating['instance_id'] is None
 
-        if not tempest.test.call_until_true(func, self.check_timeout,
-                                            self.check_interval):
+        if not test_utils.call_until_true(func, self.check_timeout,
+                                          self.check_interval):
             raise RuntimeError("IP disassociate timeout!")
 
     def run_core(self):
diff --git a/tempest/stress/actions/volume_attach_verify.py b/tempest/stress/actions/volume_attach_verify.py
index 8bbbfc4..6e530aa 100644
--- a/tempest/stress/actions/volume_attach_verify.py
+++ b/tempest/stress/actions/volume_attach_verify.py
@@ -16,8 +16,8 @@
 from tempest.common.utils.linux import remote_client
 from tempest.common import waiters
 from tempest import config
+from tempest.lib.common.utils import test_utils
 import tempest.stress.stressaction as stressaction
-import tempest.test
 
 CONF = config.CONF
 
@@ -105,8 +105,8 @@
                         ['floating_ip'])
             return floating['instance_id'] is None
 
-        if not tempest.test.call_until_true(func, CONF.compute.build_timeout,
-                                            CONF.compute.build_interval):
+        if not test_utils.call_until_true(func, CONF.compute.build_timeout,
+                                          CONF.compute.build_interval):
             raise RuntimeError("IP disassociate timeout!")
 
     def new_server_ops(self):
@@ -179,9 +179,9 @@
                 if self.part_line_re.match(part_line):
                     matching += 1
             return matching == num_match
-        if tempest.test.call_until_true(_part_state,
-                                        CONF.compute.build_timeout,
-                                        CONF.compute.build_interval):
+        if test_utils.call_until_true(_part_state,
+                                      CONF.compute.build_timeout,
+                                      CONF.compute.build_interval):
             return
         else:
             raise RuntimeError("Unexpected partitions: %s",
diff --git a/tempest/test.py b/tempest/test.py
index 97ab25c..609f1f6 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -18,8 +18,8 @@
 import os
 import re
 import sys
-import time
 
+import debtcollector.moves
 import fixtures
 from oslo_log import log as logging
 from oslo_serialization import jsonutils as json
@@ -38,6 +38,7 @@
 from tempest import config
 from tempest import exceptions
 from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
@@ -866,22 +867,6 @@
     return klass
 
 
-def call_until_true(func, duration, sleep_for):
-    """Call the given function until it returns True (and return True)
-
-    or until the specified duration (in seconds) elapses (and return False).
-
-    :param func: A zero argument callable that returns True on success.
-    :param duration: The number of seconds for which to attempt a
-        successful call of the function.
-    :param sleep_for: The number of seconds to sleep after an unsuccessful
-                      invocation of the function.
-    """
-    now = time.time()
-    timeout = now + duration
-    while now < timeout:
-        if func():
-            return True
-        time.sleep(sleep_for)
-        now = time.time()
-    return False
+call_until_true = debtcollector.moves.moved_function(
+    test_utils.call_until_true, 'call_until_true', __name__,
+    version='Newton', removal_version='Ocata')
diff --git a/tempest/tests/lib/common/utils/test_test_utils.py b/tempest/tests/lib/common/utils/test_test_utils.py
index 919e219..29c5684 100644
--- a/tempest/tests/lib/common/utils/test_test_utils.py
+++ b/tempest/tests/lib/common/utils/test_test_utils.py
@@ -17,6 +17,7 @@
 from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions
 from tempest.tests import base
+from tempest.tests import utils
 
 
 class TestTestUtils(base.TestCase):
@@ -76,3 +77,27 @@
         self.assertEqual(
             42, test_utils.call_and_ignore_notfound_exc(m, *args, **kwargs))
         m.assert_called_once_with(*args, **kwargs)
+
+    @mock.patch('time.sleep')
+    @mock.patch('time.time')
+    def test_call_until_true_when_f_never_returns_true(self, m_time, m_sleep):
+        timeout = 42  # The value doesn't matter as we mock time.time()
+        sleep = 60  # The value doesn't matter as we mock time.sleep()
+        m_time.side_effect = utils.generate_timeout_series(timeout)
+        self.assertEqual(
+            False, test_utils.call_until_true(lambda: False, timeout, sleep)
+        )
+        m_sleep.call_args_list = [mock.call(sleep)] * 2
+        m_time.call_args_list = [mock.call()] * 2
+
+    @mock.patch('time.sleep')
+    @mock.patch('time.time')
+    def test_call_until_true_when_f_returns_true(self, m_time, m_sleep):
+        timeout = 42  # The value doesn't matter as we mock time.time()
+        sleep = 60  # The value doesn't matter as we mock time.sleep()
+        m_time.return_value = 0
+        self.assertEqual(
+            True, test_utils.call_until_true(lambda: True, timeout, sleep)
+        )
+        self.assertEqual(0, m_sleep.call_count)
+        self.assertEqual(1, m_time.call_count)