blob: 43cf9e86b360617901dd8953f6c2cf87e300b0b9 [file] [log] [blame]
ZhiQiang Fan39f97222013-09-20 04:49:44 +08001# Copyright 2012 OpenStack Foundation
Jay Pipes051075a2012-04-28 17:39:37 -04002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Attila Fazekasf86fa312013-07-30 19:56:39 +020016import atexit
Masayuki Igawa80c1b9f2013-10-07 17:19:11 +090017import functools
Ian Wienand98c35f32013-07-23 20:34:23 +100018import os
Marc Kodererb2978da2014-03-26 13:45:43 +010019import re
Attila Fazekas53943322014-02-10 16:07:34 +010020import sys
Jay Pipes051075a2012-04-28 17:39:37 -040021import time
Marc Koderer24eb89c2014-01-31 11:23:33 +010022import urllib
23import uuid
Jay Pipes051075a2012-04-28 17:39:37 -040024
Matthew Treinish78561ad2013-07-26 11:41:56 -040025import fixtures
Doug Hellmann583ce2c2015-03-11 14:55:46 +000026from oslo_log import log as logging
Matthew Treinish21905512015-07-13 10:33:35 -040027from oslo_serialization import jsonutils as json
Doug Hellmann583ce2c2015-03-11 14:55:46 +000028from oslo_utils import importutils
Chris Hoge296558c2015-02-19 00:29:49 -060029import six
Matthew Treinishc1802bc2015-12-03 18:48:11 -050030from tempest_lib import decorators
Marc Koderer674c8fc2014-03-17 09:45:04 +010031import testscenarios
ivan-zhu1feeb382013-01-24 10:14:39 +080032import testtools
Jay Pipes051075a2012-04-28 17:39:37 -040033
Matthew Treinish3e046852013-07-23 16:00:24 -040034from tempest import clients
Jamie Lennox15350172015-08-17 10:54:25 +100035from tempest.common import cred_client
Andrea Frittoli (andreaf)290b3e12015-10-08 10:25:02 +010036from tempest.common import credentials_factory as credentials
Rohan Kanade9ce97df2013-12-10 18:59:35 +053037from tempest.common import fixed_network
Marc Koderer6ee82dc2014-02-17 10:26:29 +010038import tempest.common.generator.valid_generator as valid
nithya-ganesan222efd72015-01-22 12:20:27 +000039import tempest.common.validation_resources as vresources
Attila Fazekasdc216422013-01-29 15:12:14 +010040from tempest import config
Matthew Treinish16c43792013-09-09 19:55:23 +000041from tempest import exceptions
Jay Pipes051075a2012-04-28 17:39:37 -040042
43LOG = logging.getLogger(__name__)
44
Sean Dague86bd8422013-12-20 09:56:44 -050045CONF = config.CONF
46
Matthew Treinishc1802bc2015-12-03 18:48:11 -050047idempotent_id = decorators.idempotent_id
48
Jay Pipes051075a2012-04-28 17:39:37 -040049
Yaroslav Lobankovda999f72015-06-30 20:32:55 +030050def attr(**kwargs):
liuchenhong00caec52015-07-19 22:40:28 +080051 """A decorator which applies the testtools attr decorator
Chris Yeoh55530bb2013-02-08 16:04:27 +103052
Matthew Treinisha74f5d42014-02-07 20:25:44 -050053 This decorator applies the testtools.testcase.attr if it is in the list of
54 attributes to testtools we want to apply.
Attila Fazekasb2902af2013-02-16 16:22:44 +010055 """
Chris Yeoh55530bb2013-02-08 16:04:27 +103056
57 def decorator(f):
Giulio Fidente4946a052013-05-14 12:23:51 +020058 if 'type' in kwargs and isinstance(kwargs['type'], str):
59 f = testtools.testcase.attr(kwargs['type'])(f)
60 elif 'type' in kwargs and isinstance(kwargs['type'], list):
61 for attr in kwargs['type']:
62 f = testtools.testcase.attr(attr)(f)
Matthew Treinisha74f5d42014-02-07 20:25:44 -050063 return f
Chris Yeoh55530bb2013-02-08 16:04:27 +103064
65 return decorator
66
67
Matthew Treinish3d8c7322014-08-03 23:53:28 -040068def get_service_list():
Matthew Treinish8afbffd2014-01-21 23:56:13 +000069 service_list = {
70 'compute': CONF.service_available.nova,
71 'image': CONF.service_available.glance,
Adam Gandelman4a48a602014-03-20 18:23:18 -070072 'baremetal': CONF.service_available.ironic,
Matthew Treinish8afbffd2014-01-21 23:56:13 +000073 'volume': CONF.service_available.cinder,
74 'orchestration': CONF.service_available.heat,
75 # NOTE(mtreinish) nova-network will provide networking functionality
76 # if neutron isn't available, so always set to True.
77 'network': True,
78 'identity': True,
79 'object_storage': CONF.service_available.swift,
80 'dashboard': CONF.service_available.horizon,
ekhugen7aff0992014-08-04 19:01:57 +000081 'telemetry': CONF.service_available.ceilometer,
Matthew Treinishb66c94e2015-03-11 13:00:48 -040082 'data_processing': CONF.service_available.sahara,
83 'database': CONF.service_available.trove
Matthew Treinish8afbffd2014-01-21 23:56:13 +000084 }
Matthew Treinish3d8c7322014-08-03 23:53:28 -040085 return service_list
Matthew Treinish16c43792013-09-09 19:55:23 +000086
Matthew Treinish3d8c7322014-08-03 23:53:28 -040087
Yaroslav Lobankovda999f72015-06-30 20:32:55 +030088def services(*args):
Matthew Treinish3d8c7322014-08-03 23:53:28 -040089 """A decorator used to set an attr for each service used in a test case
90
91 This decorator applies a testtools attr for each service that gets
92 exercised by a test case.
93 """
Matthew Treinish16c43792013-09-09 19:55:23 +000094 def decorator(f):
Matthew Treinish3d8c7322014-08-03 23:53:28 -040095 services = ['compute', 'image', 'baremetal', 'volume', 'orchestration',
96 'network', 'identity', 'object_storage', 'dashboard',
Matthew Treinishb66c94e2015-03-11 13:00:48 -040097 'telemetry', 'data_processing', 'database']
Matthew Treinish16c43792013-09-09 19:55:23 +000098 for service in args:
Matthew Treinish3d8c7322014-08-03 23:53:28 -040099 if service not in services:
100 raise exceptions.InvalidServiceTag('%s is not a valid '
101 'service' % service)
Matthew Treinish16c43792013-09-09 19:55:23 +0000102 attr(type=list(args))(f)
Matthew Treinish8afbffd2014-01-21 23:56:13 +0000103
104 @functools.wraps(f)
105 def wrapper(self, *func_args, **func_kwargs):
Matthew Treinish3d8c7322014-08-03 23:53:28 -0400106 service_list = get_service_list()
107
Matthew Treinish8afbffd2014-01-21 23:56:13 +0000108 for service in args:
109 if not service_list[service]:
110 msg = 'Skipped because the %s service is not available' % (
111 service)
112 raise testtools.TestCase.skipException(msg)
113 return f(self, *func_args, **func_kwargs)
114 return wrapper
Matthew Treinish16c43792013-09-09 19:55:23 +0000115 return decorator
116
117
Yaroslav Lobankovda999f72015-06-30 20:32:55 +0300118def stresstest(**kwargs):
Marc Koderer32221b8e2013-08-23 13:57:50 +0200119 """Add stress test decorator
120
121 For all functions with this decorator a attr stress will be
122 set automatically.
123
124 @param class_setup_per: allowed values are application, process, action
125 ``application``: once in the stress job lifetime
126 ``process``: once in the worker process lifetime
127 ``action``: on each action
Marc Kodererb0604412013-09-02 09:43:40 +0200128 @param allow_inheritance: allows inheritance of this attribute
Marc Koderer32221b8e2013-08-23 13:57:50 +0200129 """
130 def decorator(f):
131 if 'class_setup_per' in kwargs:
132 setattr(f, "st_class_setup_per", kwargs['class_setup_per'])
133 else:
134 setattr(f, "st_class_setup_per", 'process')
Marc Kodererb0604412013-09-02 09:43:40 +0200135 if 'allow_inheritance' in kwargs:
136 setattr(f, "st_allow_inheritance", kwargs['allow_inheritance'])
137 else:
138 setattr(f, "st_allow_inheritance", False)
Marc Koderer32221b8e2013-08-23 13:57:50 +0200139 attr(type='stress')(f)
140 return f
141 return decorator
142
143
Yaroslav Lobankovda999f72015-06-30 20:32:55 +0300144def requires_ext(**kwargs):
Matthew Treinishe3d26142013-11-26 19:14:58 +0000145 """A decorator to skip tests if an extension is not enabled
146
147 @param extension
148 @param service
149 """
150 def decorator(func):
151 @functools.wraps(func)
152 def wrapper(*func_args, **func_kwargs):
153 if not is_extension_enabled(kwargs['extension'],
154 kwargs['service']):
155 msg = "Skipped because %s extension: %s is not enabled" % (
156 kwargs['service'], kwargs['extension'])
157 raise testtools.TestCase.skipException(msg)
158 return func(*func_args, **func_kwargs)
159 return wrapper
160 return decorator
161
162
163def is_extension_enabled(extension_name, service):
164 """A function that will check the list of enabled extensions from config
165
166 """
Matthew Treinishe3d26142013-11-26 19:14:58 +0000167 config_dict = {
Matthew Treinishbc0e03e2014-01-30 16:51:06 +0000168 'compute': CONF.compute_feature_enabled.api_extensions,
Matthew Treinishbc0e03e2014-01-30 16:51:06 +0000169 'volume': CONF.volume_feature_enabled.api_extensions,
170 'network': CONF.network_feature_enabled.api_extensions,
171 'object': CONF.object_storage_feature_enabled.discoverable_apis,
Jane Zadorozhna121576d2015-06-23 12:57:13 +0300172 'identity': CONF.identity_feature_enabled.api_extensions
Matthew Treinishe3d26142013-11-26 19:14:58 +0000173 }
Simeon Monov5d7effe2014-07-16 07:32:38 +0300174 if len(config_dict[service]) == 0:
175 return False
Matthew Treinishe3d26142013-11-26 19:14:58 +0000176 if config_dict[service][0] == 'all':
177 return True
178 if extension_name in config_dict[service]:
179 return True
180 return False
181
Ian Wienand98c35f32013-07-23 20:34:23 +1000182
Attila Fazekasf86fa312013-07-30 19:56:39 +0200183at_exit_set = set()
184
185
186def validate_tearDownClass():
187 if at_exit_set:
Sean Dagueeb1523b2014-03-10 10:17:44 -0400188 LOG.error(
189 "tearDownClass does not call the super's "
190 "tearDownClass in these classes: \n"
191 + str(at_exit_set))
192
Attila Fazekasf86fa312013-07-30 19:56:39 +0200193
194atexit.register(validate_tearDownClass)
195
Attila Fazekas53943322014-02-10 16:07:34 +0100196
Matthew Treinish2474f412014-11-17 18:11:56 -0500197class BaseTestCase(testtools.testcase.WithAttributes,
198 testtools.TestCase):
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100199 """The test base class defines Tempest framework for class level fixtures.
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000200
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100201 `setUpClass` and `tearDownClass` are defined here and cannot be overwritten
202 by subclasses (enforced via hacking rule T105).
203
204 Set-up is split in a series of steps (setup stages), which can be
205 overwritten by test classes. Set-up stages are:
206 - skip_checks
207 - setup_credentials
208 - setup_clients
209 - resource_setup
210
211 Tear-down is also split in a series of steps (teardown stages), which are
212 stacked for execution only if the corresponding setup stage had been
213 reached during the setup phase. Tear-down stages are:
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700214 - clear_credentials (defined in the base test class)
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100215 - resource_cleanup
216 """
Attila Fazekasc43fec82013-04-09 23:17:52 +0200217
Attila Fazekasf86fa312013-07-30 19:56:39 +0200218 setUpClassCalled = False
Marc Koderer24eb89c2014-01-31 11:23:33 +0100219 _service = None
Attila Fazekasf86fa312013-07-30 19:56:39 +0200220
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000221 # NOTE(andreaf) credentials holds a list of the credentials to be allocated
Andrea Frittoli (andreaf)825b2d32015-04-08 20:58:01 +0100222 # at class setup time. Credential types can be 'primary', 'alt', 'admin' or
223 # a list of roles - the first element of the list being a label, and the
224 # rest the actual roles
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000225 credentials = []
nithya-ganesan222efd72015-01-22 12:20:27 +0000226 # Resources required to validate a server using ssh
227 validation_resources = {}
Matthew Treinish9f756a02014-01-15 10:26:07 -0500228 network_resources = {}
229
Sean Dague2ef32ac2014-06-09 11:32:23 -0400230 # NOTE(sdague): log_format is defined inline here instead of using the oslo
231 # default because going through the config path recouples config to the
232 # stress tests too early, and depending on testr order will fail unit tests
233 log_format = ('%(asctime)s %(process)d %(levelname)-8s '
234 '[%(name)s] %(message)s')
235
Pavel Sedlák1053bd32013-04-16 16:47:40 +0200236 @classmethod
237 def setUpClass(cls):
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100238 # It should never be overridden by descendants
Pavel Sedlák1053bd32013-04-16 16:47:40 +0200239 if hasattr(super(BaseTestCase, cls), 'setUpClass'):
240 super(BaseTestCase, cls).setUpClass()
Attila Fazekasf86fa312013-07-30 19:56:39 +0200241 cls.setUpClassCalled = True
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100242 # Stack of (name, callable) to be invoked in reverse order at teardown
243 cls.teardowns = []
244 # All the configuration checks that may generate a skip
245 cls.skip_checks()
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100246 try:
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100247 # Allocation of all required credentials and client managers
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700248 cls.teardowns.append(('credentials', cls.clear_credentials))
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100249 cls.setup_credentials()
250 # Shortcuts to clients
251 cls.setup_clients()
252 # Additional class-wide test resources
253 cls.teardowns.append(('resources', cls.resource_cleanup))
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100254 cls.resource_setup()
255 except Exception:
256 etype, value, trace = sys.exc_info()
Matthew Treinished2ad4f2014-12-23 15:18:32 -0500257 LOG.info("%s raised in %s.setUpClass. Invoking tearDownClass." % (
258 etype, cls.__name__))
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100259 cls.tearDownClass()
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100260 try:
Matthew Treinish843227d2015-04-23 10:17:17 -0400261 six.reraise(etype, value, trace)
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100262 finally:
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100263 del trace # to avoid circular refs
Pavel Sedlák1053bd32013-04-16 16:47:40 +0200264
Attila Fazekasf86fa312013-07-30 19:56:39 +0200265 @classmethod
266 def tearDownClass(cls):
Attila Fazekas5d275302013-08-29 12:35:12 +0200267 at_exit_set.discard(cls)
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100268 # It should never be overridden by descendants
Attila Fazekasf86fa312013-07-30 19:56:39 +0200269 if hasattr(super(BaseTestCase, cls), 'tearDownClass'):
270 super(BaseTestCase, cls).tearDownClass()
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100271 # Save any existing exception, we always want to re-raise the original
272 # exception only
273 etype, value, trace = sys.exc_info()
274 # If there was no exception during setup we shall re-raise the first
275 # exception in teardown
276 re_raise = (etype is None)
277 while cls.teardowns:
278 name, teardown = cls.teardowns.pop()
279 # Catch any exception in tearDown so we can re-raise the original
280 # exception at the end
281 try:
282 teardown()
283 except Exception as te:
284 sys_exec_info = sys.exc_info()
285 tetype = sys_exec_info[0]
286 # TODO(andreaf): Till we have the ability to cleanup only
287 # resources that were successfully setup in resource_cleanup,
288 # log AttributeError as info instead of exception.
289 if tetype is AttributeError and name == 'resources':
290 LOG.info("tearDownClass of %s failed: %s" % (name, te))
291 else:
292 LOG.exception("teardown of %s failed: %s" % (name, te))
293 if not etype:
294 etype, value, trace = sys_exec_info
295 # If exceptions were raised during teardown, an not before, re-raise
296 # the first one
297 if re_raise and etype is not None:
298 try:
Matthew Treinish843227d2015-04-23 10:17:17 -0400299 six.reraise(etype, value, trace)
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100300 finally:
301 del trace # to avoid circular refs
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100302
303 @classmethod
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100304 def skip_checks(cls):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000305 """Class level skip checks.
306
307 Subclasses verify in here all conditions that might prevent the
308 execution of the entire test class.
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100309 Checks implemented here may not make use API calls, and should rely on
310 configuration alone.
311 In general skip checks that require an API call are discouraged.
312 If one is really needed it may be implemented either in the
313 resource_setup or at test level.
314 """
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100315 identity_version = cls.get_identity_version()
316 if 'admin' in cls.credentials and not credentials.is_admin_available(
317 identity_version=identity_version):
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000318 msg = "Missing Identity Admin API credentials in configuration."
319 raise cls.skipException(msg)
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100320 if 'alt' in cls.credentials and not credentials.is_alt_available(
321 identity_version=identity_version):
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000322 msg = "Missing a 2nd set of API credentials in configuration."
323 raise cls.skipException(msg)
Andrea Frittoli (andreaf)41601412015-05-12 16:39:03 +0100324 if hasattr(cls, 'identity_version'):
325 if cls.identity_version == 'v2':
326 if not CONF.identity_feature_enabled.api_v2:
327 raise cls.skipException("Identity api v2 is not enabled")
328 elif cls.identity_version == 'v3':
329 if not CONF.identity_feature_enabled.api_v3:
330 raise cls.skipException("Identity api v3 is not enabled")
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100331
332 @classmethod
333 def setup_credentials(cls):
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000334 """Allocate credentials and the client managers from them.
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000335
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000336 A test class that requires network resources must override
337 setup_credentials and defined the required resources before super
338 is invoked.
339 """
340 for credentials_type in cls.credentials:
341 # This may raise an exception in case credentials are not available
342 # In that case we want to let the exception through and the test
343 # fail accordingly
Andrea Frittoli (andreaf)825b2d32015-04-08 20:58:01 +0100344 if isinstance(credentials_type, six.string_types):
345 manager = cls.get_client_manager(
346 credential_type=credentials_type)
347 setattr(cls, 'os_%s' % credentials_type, manager)
348 # Setup some common aliases
349 # TODO(andreaf) The aliases below are a temporary hack
350 # to avoid changing too much code in one patch. They should
351 # be removed eventually
352 if credentials_type == 'primary':
353 cls.os = cls.manager = cls.os_primary
354 if credentials_type == 'admin':
355 cls.os_adm = cls.admin_manager = cls.os_admin
356 if credentials_type == 'alt':
357 cls.alt_manager = cls.os_alt
358 elif isinstance(credentials_type, list):
359 manager = cls.get_client_manager(roles=credentials_type[1:],
360 force_new=True)
361 setattr(cls, 'os_roles_%s' % credentials_type[0], manager)
Andrea Frittolia5ddd552014-08-19 18:30:00 +0100362
363 @classmethod
364 def setup_clients(cls):
365 """Create links to the clients into the test object."""
366 # TODO(andreaf) There is a fair amount of code that could me moved from
367 # base / test classes in here. Ideally tests should be able to only
368 # specify which client is `client` and nothing else.
Andrea Frittoli73ee2472014-09-15 12:31:53 +0100369 pass
Attila Fazekasf86fa312013-07-30 19:56:39 +0200370
Emily Hugenbruch5bd4cbf2014-12-17 21:38:38 +0000371 @classmethod
372 def resource_setup(cls):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000373 """Class level resource setup for test cases."""
nithya-ganesan222efd72015-01-22 12:20:27 +0000374 if hasattr(cls, "os"):
375 cls.validation_resources = vresources.create_validation_resources(
376 cls.os, cls.validation_resources)
377 else:
zhangguoqing6c096642016-01-04 06:17:21 +0000378 LOG.warning("Client manager not found, validation resources not"
379 " created")
Emily Hugenbruch5bd4cbf2014-12-17 21:38:38 +0000380
381 @classmethod
382 def resource_cleanup(cls):
383 """Class level resource cleanup for test cases.
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000384
Emily Hugenbruch5bd4cbf2014-12-17 21:38:38 +0000385 Resource cleanup must be able to handle the case of partially setup
386 resources, in case a failure during `resource_setup` should happen.
387 """
nithya-ganesan222efd72015-01-22 12:20:27 +0000388 if cls.validation_resources:
389 if hasattr(cls, "os"):
390 vresources.clear_validation_resources(cls.os,
391 cls.validation_resources)
392 cls.validation_resources = {}
393 else:
zhangguoqing6c096642016-01-04 06:17:21 +0000394 LOG.warning("Client manager not found, validation resources "
395 "not deleted")
Emily Hugenbruch5bd4cbf2014-12-17 21:38:38 +0000396
Attila Fazekasf86fa312013-07-30 19:56:39 +0200397 def setUp(self):
398 super(BaseTestCase, self).setUp()
399 if not self.setUpClassCalled:
400 raise RuntimeError("setUpClass does not calls the super's"
401 "setUpClass in the "
402 + self.__class__.__name__)
403 at_exit_set.add(self.__class__)
Matthew Treinish78561ad2013-07-26 11:41:56 -0400404 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
405 try:
406 test_timeout = int(test_timeout)
407 except ValueError:
408 test_timeout = 0
409 if test_timeout > 0:
Attila Fazekasf86fa312013-07-30 19:56:39 +0200410 self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400411
412 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
413 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
Attila Fazekasf86fa312013-07-30 19:56:39 +0200414 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
415 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400416 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
417 os.environ.get('OS_STDERR_CAPTURE') == '1'):
Attila Fazekasf86fa312013-07-30 19:56:39 +0200418 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
419 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
Attila Fazekas31388072013-08-15 08:58:07 +0200420 if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
421 os.environ.get('OS_LOG_CAPTURE') != '0'):
Attila Fazekas31388072013-08-15 08:58:07 +0200422 self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
Sean Dague2ef32ac2014-06-09 11:32:23 -0400423 format=self.log_format,
Attila Fazekas90445be2013-10-24 16:46:03 +0200424 level=None))
Matthew Treinish78561ad2013-07-26 11:41:56 -0400425
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100426 @property
427 def credentials_provider(self):
428 return self._get_credentials_provider()
429
Jamie Lennox15350172015-08-17 10:54:25 +1000430 @property
431 def identity_utils(self):
432 """A client that abstracts v2 and v3 identity operations.
433
434 This can be used for creating and tearing down projects in tests. It
435 should not be used for testing identity features.
436 """
437 if CONF.identity.auth_version == 'v2':
438 client = self.os_admin.identity_client
Daniel Melladob04da902015-11-20 17:43:12 +0100439 project_client = self.os_admin.tenants_client
Daniel Mellado6b16b922015-12-07 12:43:08 +0000440 roles_client = self.os_admin.roles_client
Jamie Lennox15350172015-08-17 10:54:25 +1000441 else:
442 client = self.os_admin.identity_v3_client
Daniel Melladob04da902015-11-20 17:43:12 +0100443 project_client = None
Daniel Mellado6b16b922015-12-07 12:43:08 +0000444 roles_client = None
Jamie Lennox15350172015-08-17 10:54:25 +1000445
446 try:
447 domain = client.auth_provider.credentials.project_domain_name
448 except AttributeError:
449 domain = 'Default'
450
Daniel Melladob04da902015-11-20 17:43:12 +0100451 return cred_client.get_creds_client(client, project_client,
Daniel Mellado6b16b922015-12-07 12:43:08 +0000452 roles_client,
Daniel Melladob04da902015-11-20 17:43:12 +0100453 project_domain_name=domain)
Jamie Lennox15350172015-08-17 10:54:25 +1000454
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100455 @classmethod
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100456 def get_identity_version(cls):
457 """Returns the identity version used by the test class"""
458 identity_version = getattr(cls, 'identity_version', None)
459 return identity_version or CONF.identity.auth_version
460
461 @classmethod
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100462 def _get_credentials_provider(cls):
463 """Returns a credentials provider
464
465 If no credential provider exists yet creates one.
466 It uses self.identity_version if defined, or the configuration value
467 """
468 if (not hasattr(cls, '_creds_provider') or not cls._creds_provider or
469 not cls._creds_provider.name == cls.__name__):
470 force_tenant_isolation = getattr(cls, 'force_tenant_isolation',
471 False)
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100472
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700473 cls._creds_provider = credentials.get_credentials_provider(
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100474 name=cls.__name__, network_resources=cls.network_resources,
475 force_tenant_isolation=force_tenant_isolation,
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100476 identity_version=cls.get_identity_version())
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100477 return cls._creds_provider
478
Matthew Treinish3e046852013-07-23 16:00:24 -0400479 @classmethod
Andrea Frittoli (andreaf)41601412015-05-12 16:39:03 +0100480 def get_client_manager(cls, credential_type=None, roles=None,
481 force_new=None):
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100482 """Returns an OpenStack client manager
483
484 Returns an OpenStack client manager based on either credential_type
485 or a list of roles. If neither is specified, it defaults to
486 credential_type 'primary'
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100487 :param credential_type: string - primary, alt or admin
488 :param roles: list of roles
489
lei zhangdd552b22015-11-25 20:41:48 +0800490 :returns: the created client manager
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100491 :raises skipException: if the requested credentials are not available
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700492 """
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100493 if all([roles, credential_type]):
494 msg = "Cannot get credentials by type and roles at the same time"
495 raise ValueError(msg)
496 if not any([roles, credential_type]):
497 credential_type = 'primary'
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100498 cred_provider = cls._get_credentials_provider()
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100499 if roles:
500 for role in roles:
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100501 if not cred_provider.is_role_available(role):
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100502 skip_msg = (
503 "%s skipped because the configured credential provider"
504 " is not able to provide credentials with the %s role "
505 "assigned." % (cls.__name__, role))
506 raise cls.skipException(skip_msg)
507 params = dict(roles=roles)
508 if force_new is not None:
509 params.update(force_new=force_new)
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100510 creds = cred_provider.get_creds_by_roles(**params)
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000511 else:
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100512 credentials_method = 'get_%s_creds' % credential_type
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100513 if hasattr(cred_provider, credentials_method):
514 creds = getattr(cred_provider, credentials_method)()
Andrea Frittoli (andreaf)737fac92015-05-12 16:14:35 +0100515 else:
516 raise exceptions.InvalidCredentials(
517 "Invalid credentials type %s" % credential_type)
518 return clients.Manager(credentials=creds, service=cls._service)
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700519
520 @classmethod
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700521 def clear_credentials(cls):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000522 """Clears creds if set"""
Attila Fazekas5b0d9262015-05-20 10:17:39 +0200523 if hasattr(cls, '_creds_provider'):
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700524 cls._creds_provider.clear_creds()
Ryan Hsu6c4bb3d2013-10-21 21:22:50 -0700525
526 @classmethod
nithya-ganesan222efd72015-01-22 12:20:27 +0000527 def set_validation_resources(cls, keypair=None, floating_ip=None,
528 security_group=None,
529 security_group_rules=None):
530 """Specify which ssh server validation resources should be created.
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000531
nithya-ganesan222efd72015-01-22 12:20:27 +0000532 Each of the argument must be set to either None, True or False, with
533 None - use default from config (security groups and security group
534 rules get created when set to None)
535 False - Do not create the validation resource
536 True - create the validation resource
537
538 @param keypair
539 @param security_group
540 @param security_group_rules
541 @param floating_ip
542 """
Matthew Treinishe5cca002015-05-11 15:36:50 -0400543 if not CONF.validation.run_validation:
544 return
nithya-ganesan222efd72015-01-22 12:20:27 +0000545 if keypair is None:
546 if CONF.validation.auth_method.lower() == "keypair":
547 keypair = True
548 else:
549 keypair = False
550 if floating_ip is None:
551 if CONF.validation.connect_method.lower() == "floating":
552 floating_ip = True
553 else:
554 floating_ip = False
555 if security_group is None:
Brandon Palmc6cc91d2015-08-19 13:20:21 -0500556 security_group = CONF.validation.security_group
nithya-ganesan222efd72015-01-22 12:20:27 +0000557 if security_group_rules is None:
Brandon Palmc6cc91d2015-08-19 13:20:21 -0500558 security_group_rules = CONF.validation.security_group_rules
559
nithya-ganesan222efd72015-01-22 12:20:27 +0000560 if not cls.validation_resources:
561 cls.validation_resources = {
562 'keypair': keypair,
563 'security_group': security_group,
564 'security_group_rules': security_group_rules,
565 'floating_ip': floating_ip}
566
567 @classmethod
Andrea Frittoli7d5ed592015-02-10 01:10:23 +0000568 def set_network_resources(cls, network=False, router=False, subnet=False,
Matthew Treinish9f756a02014-01-15 10:26:07 -0500569 dhcp=False):
570 """Specify which network resources should be created
571
572 @param network
573 @param router
574 @param subnet
575 @param dhcp
576 """
Salvatore Orlando5a337242014-01-15 22:49:22 +0000577 # network resources should be set only once from callers
578 # in order to ensure that even if it's called multiple times in
579 # a chain of overloaded methods, the attribute is set only
580 # in the leaf class
Andrea Frittoli7d5ed592015-02-10 01:10:23 +0000581 if not cls.network_resources:
582 cls.network_resources = {
Salvatore Orlando5a337242014-01-15 22:49:22 +0000583 'network': network,
584 'router': router,
585 'subnet': subnet,
586 'dhcp': dhcp}
Matthew Treinish9f756a02014-01-15 10:26:07 -0500587
Rohan Kanade9ce97df2013-12-10 18:59:35 +0530588 @classmethod
589 def get_tenant_network(cls):
590 """Get the network to be used in testing
591
592 :return: network dict including 'id' and 'name'
593 """
Andrea Frittoli (andreaf)17209bb2015-05-22 10:16:57 -0700594 # Make sure cred_provider exists and get a network client
John Warren9487a182015-09-14 18:12:56 -0400595 networks_client = cls.get_client_manager().compute_networks_client
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100596 cred_provider = cls._get_credentials_provider()
Andrea Frittoli700711e2015-04-02 11:39:38 +0100597 # In case of nova network, isolated tenants are not able to list the
598 # network configured in fixed_network_name, even if the can use it
599 # for their servers, so using an admin network client to validate
600 # the network name
601 if (not CONF.service_available.neutron and
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100602 credentials.is_admin_available(
603 identity_version=cls.get_identity_version())):
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100604 admin_creds = cred_provider.get_admin_creds()
John Warren9487a182015-09-14 18:12:56 -0400605 admin_manager = clients.Manager(admin_creds)
606 networks_client = admin_manager.compute_networks_client
Andrea Frittoli (andreaf)940f8c62015-10-30 16:39:24 +0900607 return fixed_network.get_tenant_network(
608 cred_provider, networks_client, CONF.compute.fixed_network_name)
Rohan Kanade9ce97df2013-12-10 18:59:35 +0530609
Mark Maglana5885eb32014-02-28 10:57:34 -0800610 def assertEmpty(self, list, msg=None):
611 self.assertTrue(len(list) == 0, msg)
612
613 def assertNotEmpty(self, list, msg=None):
614 self.assertTrue(len(list) > 0, msg)
615
Attila Fazekasdc216422013-01-29 15:12:14 +0100616
Marc Koderer24eb89c2014-01-31 11:23:33 +0100617class NegativeAutoTest(BaseTestCase):
618
619 _resources = {}
620
621 @classmethod
622 def setUpClass(cls):
623 super(NegativeAutoTest, cls).setUpClass()
Andrea Frittolib21de6c2015-02-06 20:12:38 +0000624 os = cls.get_client_manager(credential_type='primary')
Marc Koderer24eb89c2014-01-31 11:23:33 +0100625 cls.client = os.negative_client
626
627 @staticmethod
Marc Koderer674c8fc2014-03-17 09:45:04 +0100628 def load_tests(*args):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000629 """Wrapper for testscenarios
630
631 To set the mandatory scenarios variable only in case a real test
632 loader is in place. Will be automatically called in case the variable
633 "load_tests" is set.
Marc Koderer674c8fc2014-03-17 09:45:04 +0100634 """
635 if getattr(args[0], 'suiteClass', None) is not None:
636 loader, standard_tests, pattern = args
637 else:
638 standard_tests, module, loader = args
639 for test in testtools.iterate_tests(standard_tests):
Marc Koderer4f44d722014-08-07 14:04:58 +0200640 schema = getattr(test, '_schema', None)
Marc Koderer3dd31052014-11-27 09:31:00 +0100641 if schema is not None:
Marc Koderer4f44d722014-08-07 14:04:58 +0200642 setattr(test, 'scenarios',
643 NegativeAutoTest.generate_scenario(schema))
Marc Koderer674c8fc2014-03-17 09:45:04 +0100644 return testscenarios.load_tests_apply_scenarios(*args)
645
646 @staticmethod
Marc Koderer4f44d722014-08-07 14:04:58 +0200647 def generate_scenario(description):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000648 """Generates the test scenario list for a given description.
Marc Koderer24eb89c2014-01-31 11:23:33 +0100649
Marc Koderer4f44d722014-08-07 14:04:58 +0200650 :param description: A file or dictionary with the following entries:
Marc Koderer24eb89c2014-01-31 11:23:33 +0100651 name (required) name for the api
652 http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
653 url (required) the url to be appended to the catalog url with '%s'
654 for each resource mentioned
655 resources: (optional) A list of resource names such as "server",
656 "flavor", etc. with an element for each '%s' in the url. This
657 method will call self.get_resource for each element when
658 constructing the positive test case template so negative
659 subclasses are expected to return valid resource ids when
660 appropriate.
661 json-schema (optional) A valid json schema that will be used to
662 create invalid data for the api calls. For "GET" and "HEAD",
663 the data is used to generate query strings appended to the url,
664 otherwise for the body of the http call.
665 """
Marc Koderer24eb89c2014-01-31 11:23:33 +0100666 LOG.debug(description)
Marc Koderer674c8fc2014-03-17 09:45:04 +0100667 generator = importutils.import_class(
668 CONF.negative.test_generator)()
Marc Koderer6ee82dc2014-02-17 10:26:29 +0100669 generator.validate_schema(description)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100670 schema = description.get("json-schema", None)
671 resources = description.get("resources", [])
672 scenario_list = []
Marc Koderer424c84f2014-02-06 17:02:19 +0100673 expected_result = None
Marc Koderer24eb89c2014-01-31 11:23:33 +0100674 for resource in resources:
Marc Koderer424c84f2014-02-06 17:02:19 +0100675 if isinstance(resource, dict):
676 expected_result = resource['expected_result']
677 resource = resource['name']
Marc Koderer24eb89c2014-01-31 11:23:33 +0100678 LOG.debug("Add resource to test %s" % resource)
679 scn_name = "inv_res_%s" % (resource)
680 scenario_list.append((scn_name, {"resource": (resource,
Marc Koderer424c84f2014-02-06 17:02:19 +0100681 str(uuid.uuid4())),
682 "expected_result": expected_result
Marc Koderer24eb89c2014-01-31 11:23:33 +0100683 }))
684 if schema is not None:
Marc Kodererf07f5d12014-09-01 09:47:23 +0200685 for scenario in generator.generate_scenarios(schema):
686 scenario_list.append((scenario['_negtest_name'],
687 scenario))
Marc Koderer24eb89c2014-01-31 11:23:33 +0100688 LOG.debug(scenario_list)
689 return scenario_list
690
Marc Koderer4f44d722014-08-07 14:04:58 +0200691 def execute(self, description):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000692 """Execute a http call
693
Marc Koderer24eb89c2014-01-31 11:23:33 +0100694 Execute a http call on an api that are expected to
695 result in client errors. First it uses invalid resources that are part
696 of the url, and then invalid data for queries and http request bodies.
697
Marc Koderer4f44d722014-08-07 14:04:58 +0200698 :param description: A json file or dictionary with the following
699 entries:
Marc Koderer24eb89c2014-01-31 11:23:33 +0100700 name (required) name for the api
701 http-method (required) one of HEAD,GET,PUT,POST,PATCH,DELETE
702 url (required) the url to be appended to the catalog url with '%s'
703 for each resource mentioned
704 resources: (optional) A list of resource names such as "server",
705 "flavor", etc. with an element for each '%s' in the url. This
706 method will call self.get_resource for each element when
707 constructing the positive test case template so negative
708 subclasses are expected to return valid resource ids when
709 appropriate.
710 json-schema (optional) A valid json schema that will be used to
711 create invalid data for the api calls. For "GET" and "HEAD",
712 the data is used to generate query strings appended to the url,
713 otherwise for the body of the http call.
714
715 """
Marc Koderer24eb89c2014-01-31 11:23:33 +0100716 LOG.info("Executing %s" % description["name"])
717 LOG.debug(description)
Marc Kodererf07f5d12014-09-01 09:47:23 +0200718 generator = importutils.import_class(
719 CONF.negative.test_generator)()
720 schema = description.get("json-schema", None)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100721 method = description["http-method"]
722 url = description["url"]
Marc Kodererf07f5d12014-09-01 09:47:23 +0200723 expected_result = None
724 if "default_result_code" in description:
725 expected_result = description["default_result_code"]
Marc Koderer24eb89c2014-01-31 11:23:33 +0100726
727 resources = [self.get_resource(r) for
728 r in description.get("resources", [])]
729
730 if hasattr(self, "resource"):
731 # Note(mkoderer): The resources list already contains an invalid
732 # entry (see get_resource).
733 # We just send a valid json-schema with it
Marc Koderer6ee82dc2014-02-17 10:26:29 +0100734 valid_schema = None
Marc Koderer24eb89c2014-01-31 11:23:33 +0100735 if schema:
Marc Koderer6ee82dc2014-02-17 10:26:29 +0100736 valid_schema = \
737 valid.ValidTestGenerator().generate_valid(schema)
738 new_url, body = self._http_arguments(valid_schema, url, method)
Marc Kodererf07f5d12014-09-01 09:47:23 +0200739 elif hasattr(self, "_negtest_name"):
740 schema_under_test = \
741 valid.ValidTestGenerator().generate_valid(schema)
742 local_expected_result = \
743 generator.generate_payload(self, schema_under_test)
744 if local_expected_result is not None:
745 expected_result = local_expected_result
746 new_url, body = \
747 self._http_arguments(schema_under_test, url, method)
Marc Koderer1c247c82014-03-20 08:24:38 +0100748 else:
749 raise Exception("testscenarios are not active. Please make sure "
750 "that your test runner supports the load_tests "
751 "mechanism")
Marc Koderer424c84f2014-02-06 17:02:19 +0100752
Marc Kodererf857fda2014-03-05 15:58:00 +0100753 if "admin_client" in description and description["admin_client"]:
Andrea Frittoli (andreaf)32d0de12015-10-09 14:43:53 +0100754 if not credentials.is_admin_available(
755 identity_version=self.get_identity_version()):
David Kranzafecec02015-03-23 14:27:15 -0400756 msg = ("Missing Identity Admin API credentials in"
757 "configuration.")
758 raise self.skipException(msg)
Andrea Frittoli (andreaf)1f342412015-05-12 16:37:19 +0100759 creds = self.credentials_provider.get_admin_creds()
David Kranzafecec02015-03-23 14:27:15 -0400760 os_adm = clients.Manager(credentials=creds)
761 client = os_adm.negative_client
Marc Kodererf857fda2014-03-05 15:58:00 +0100762 else:
763 client = self.client
764 resp, resp_body = client.send_request(method, new_url,
765 resources, body=body)
Marc Kodererf07f5d12014-09-01 09:47:23 +0200766 self._check_negative_response(expected_result, resp.status, resp_body)
Marc Koderer24eb89c2014-01-31 11:23:33 +0100767
768 def _http_arguments(self, json_dict, url, method):
769 LOG.debug("dict: %s url: %s method: %s" % (json_dict, url, method))
770 if not json_dict:
771 return url, None
772 elif method in ["GET", "HEAD", "PUT", "DELETE"]:
773 return "%s?%s" % (url, urllib.urlencode(json_dict)), None
774 else:
775 return url, json.dumps(json_dict)
776
Marc Kodererf07f5d12014-09-01 09:47:23 +0200777 def _check_negative_response(self, expected_result, result, body):
Marc Koderer24eb89c2014-01-31 11:23:33 +0100778 self.assertTrue(result >= 400 and result < 500 and result != 413,
779 "Expected client error, got %s:%s" %
780 (result, body))
781 self.assertTrue(expected_result is None or expected_result == result,
782 "Expected %s, got %s:%s" %
783 (expected_result, result, body))
784
785 @classmethod
786 def set_resource(cls, name, resource):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000787 """Register a resoruce for a test
788
Marc Koderer24eb89c2014-01-31 11:23:33 +0100789 This function can be used in setUpClass context to register a resoruce
790 for a test.
791
792 :param name: The name of the kind of resource such as "flavor", "role",
793 etc.
794 :resource: The id of the resource
795 """
796 cls._resources[name] = resource
797
798 def get_resource(self, name):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000799 """Return a valid uuid for a type of resource.
800
801 If a real resource is needed as part of a url then this method should
802 return one. Otherwise it can return None.
Marc Koderer24eb89c2014-01-31 11:23:33 +0100803
804 :param name: The name of the kind of resource such as "flavor", "role",
805 etc.
806 """
Marc Koderer424c84f2014-02-06 17:02:19 +0100807 if isinstance(name, dict):
808 name = name['name']
Marc Koderer24eb89c2014-01-31 11:23:33 +0100809 if hasattr(self, "resource") and self.resource[0] == name:
810 LOG.debug("Return invalid resource (%s) value: %s" %
811 (self.resource[0], self.resource[1]))
812 return self.resource[1]
813 if name in self._resources:
814 return self._resources[name]
815 return None
816
817
Marc Kodererb2978da2014-03-26 13:45:43 +0100818def SimpleNegativeAutoTest(klass):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000819 """This decorator registers a test function on basis of the class name."""
Sean Dague5e1bcd92015-04-27 09:08:36 -0400820 @attr(type=['negative'])
Marc Kodererb2978da2014-03-26 13:45:43 +0100821 def generic_test(self):
Marc Koderer4f44d722014-08-07 14:04:58 +0200822 if hasattr(self, '_schema'):
823 self.execute(self._schema)
Marc Kodererb2978da2014-03-26 13:45:43 +0100824
825 cn = klass.__name__
826 cn = cn.replace('JSON', '')
827 cn = cn.replace('Test', '')
828 # NOTE(mkoderer): replaces uppercase chars inside the class name with '_'
829 lower_cn = re.sub('(?<!^)(?=[A-Z])', '_', cn).lower()
830 func_name = 'test_%s' % lower_cn
831 setattr(klass, func_name, generic_test)
832 return klass
833
834
Sean Dague35a7caf2013-05-10 10:38:22 -0400835def call_until_true(func, duration, sleep_for):
Ken'ichi Ohmichi2e2ee192015-11-19 09:48:27 +0000836 """Call the given function until it returns True (and return True)
837
838 or until the specified duration (in seconds) elapses (and return False).
Sean Dague35a7caf2013-05-10 10:38:22 -0400839
840 :param func: A zero argument callable that returns True on success.
841 :param duration: The number of seconds for which to attempt a
842 successful call of the function.
843 :param sleep_for: The number of seconds to sleep after an unsuccessful
844 invocation of the function.
845 """
846 now = time.time()
847 timeout = now + duration
848 while now < timeout:
849 if func():
850 return True
Sean Dague35a7caf2013-05-10 10:38:22 -0400851 time.sleep(sleep_for)
852 now = time.time()
853 return False