1# fixtures: Fixtures with cleanups for testing and convenience. 2# 3# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> 4# 5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause 6# license at the users choice. A copy of both licenses are available in the 7# project source as Apache-2.0 and BSD. You may not use this file except in 8# compliance with one of these two licences. 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# license you chose for the specific language governing permissions and 14# limitations under that license. 15 16__all__ = [ 17 'CompoundFixture', 18 'Fixture', 19 'FunctionFixture', 20 'MethodFixture', 21 'MultipleExceptions', 22 'SetupError', 23 ] 24 25import itertools 26import sys 27 28import six 29from testtools.compat import ( 30 advance_iterator, 31 ) 32from testtools.helpers import try_import 33 34from fixtures.callmany import ( 35 CallMany, 36 # Deprecated, imported for compatibility. 37 MultipleExceptions, 38 ) 39 40gather_details = try_import("testtools.testcase.gather_details") 41 42# This would be better in testtools (or a common library) 43def combine_details(source_details, target_details): 44 """Add every value from source to target deduping common keys.""" 45 for name, content_object in source_details.items(): 46 new_name = name 47 disambiguator = itertools.count(1) 48 while new_name in target_details: 49 new_name = '%s-%d' % (name, advance_iterator(disambiguator)) 50 name = new_name 51 target_details[name] = content_object 52 53 54class SetupError(Exception): 55 """Setup failed. 56 57 args[0] will be a details dict. 58 """ 59 60 61class Fixture(object): 62 """A Fixture representing some state or resource. 63 64 Often used in tests, a Fixture must be setUp before using it, and cleanUp 65 called after it is finished with (because many Fixture classes have 66 external resources such as temporary directories). 67 68 The reset() method can be called to perform cleanUp and setUp automatically 69 and potentially faster. 70 """ 71 72 def addCleanup(self, cleanup, *args, **kwargs): 73 """Add a clean function to be called from cleanUp. 74 75 All cleanup functions are called - see cleanUp for details on how 76 multiple exceptions are handled. 77 78 If for some reason you need to cancel cleanups, call 79 self._clear_cleanups. 80 81 :param cleanup: A callable to call during cleanUp. 82 :param *args: Positional args for cleanup. 83 :param kwargs: Keyword args for cleanup. 84 :return: None 85 """ 86 self._cleanups.push(cleanup, *args, **kwargs) 87 88 def addDetail(self, name, content_object): 89 """Add a detail to the Fixture. 90 91 This may only be called after setUp has been called. 92 93 :param name: The name for the detail being added. Overrides existing 94 identically named details. 95 :param content_object: The content object (meeting the 96 testtools.content.Content protocol) being added. 97 """ 98 self._details[name] = content_object 99 100 def cleanUp(self, raise_first=True): 101 """Cleanup the fixture. 102 103 This function will free all resources managed by the Fixture, restoring 104 it (and any external facilities such as databases, temporary 105 directories and so forth_ to their original state. 106 107 This should not typically be overridden, see addCleanup instead. 108 109 cleanUp may be called once and only once after setUp() has been called. 110 The base implementation of setUp will automatically call cleanUp if 111 an exception occurs within setUp itself. 112 113 :param raise_first: Deprecated parameter from before testtools gained 114 MultipleExceptions. raise_first defaults to True. When True 115 if a single exception is raised, it is reraised after all the 116 cleanUps have run. If multiple exceptions are raised, they are 117 all wrapped into a MultipleExceptions object, and that is reraised. 118 Thus, to catch a specific exception from cleanUp, you need to catch 119 both the exception and MultipleExceptions, and then check within 120 a MultipleExceptions instance for the type you're catching. 121 :return: A list of the exc_info() for each exception that occured if 122 raise_first was False 123 """ 124 try: 125 return self._cleanups(raise_errors=raise_first) 126 finally: 127 self._remove_state() 128 129 def _clear_cleanups(self): 130 """Clean the cleanup queue without running them. 131 132 This is a helper that can be useful for subclasses which define 133 reset(): they may perform something equivalent to a typical cleanUp 134 without actually calling the cleanups. 135 136 This also clears the details dict. 137 """ 138 self._cleanups = CallMany() 139 self._details = {} 140 self._detail_sources = [] 141 142 def _remove_state(self): 143 """Remove the internal state. 144 145 Called from cleanUp to put the fixture back into a not-ready state. 146 """ 147 self._cleanups = None 148 self._details = None 149 self._detail_sources = None 150 151 def __enter__(self): 152 self.setUp() 153 return self 154 155 def __exit__(self, exc_type, exc_val, exc_tb): 156 try: 157 self._cleanups() 158 finally: 159 self._remove_state() 160 return False # propagate exceptions from the with body. 161 162 def getDetails(self): 163 """Get the current details registered with the fixture. 164 165 This does not return the internal dictionary: mutating it will have no 166 effect. If you need to mutate it, just do so directly. 167 168 :return: Dict from name -> content_object. 169 """ 170 result = dict(self._details) 171 for source in self._detail_sources: 172 combine_details(source.getDetails(), result) 173 return result 174 175 def setUp(self): 176 """Prepare the Fixture for use. 177 178 This should not be overridden. Concrete fixtures should implement 179 _setUp. Overriding of setUp is still supported, just not recommended. 180 181 After setUp has completed, the fixture will have one or more attributes 182 which can be used (these depend totally on the concrete subclass). 183 184 :raises: MultipleExceptions if _setUp fails. The last exception 185 captured within the MultipleExceptions will be a SetupError 186 exception. 187 :return: None. 188 189 :changed in 1.3: The recommendation to override setUp has been 190 reversed - before 1.3, setUp() should be overridden, now it should 191 not be. 192 :changed in 1.3.1: BaseException is now caught, and only subclasses of 193 Exception are wrapped in MultipleExceptions. 194 """ 195 self._clear_cleanups() 196 try: 197 self._setUp() 198 except: 199 err = sys.exc_info() 200 details = {} 201 if gather_details is not None: 202 # Materialise all details since we're about to cleanup. 203 gather_details(self.getDetails(), details) 204 else: 205 details = self.getDetails() 206 errors = [err] + self.cleanUp(raise_first=False) 207 try: 208 raise SetupError(details) 209 except SetupError: 210 errors.append(sys.exc_info()) 211 if issubclass(err[0], Exception): 212 raise MultipleExceptions(*errors) 213 else: 214 six.reraise(*err) 215 216 def _setUp(self): 217 """Template method for subclasses to override. 218 219 Override this to customise the fixture. When overriding 220 be sure to include self.addCleanup calls to restore the fixture to 221 an un-setUp state, so that a single Fixture instance can be reused. 222 223 Fixtures will never have a body in _setUp - calling super() is 224 entirely at the discretion of subclasses. 225 226 :return: None. 227 """ 228 229 def reset(self): 230 """Reset a setUp Fixture to the 'just setUp' state again. 231 232 The default implementation calls 233 self.cleanUp() 234 self.setUp() 235 236 but this function may be overridden to provide an optimised routine to 237 achieve the same result. 238 239 :return: None. 240 """ 241 self.cleanUp() 242 self.setUp() 243 244 def useFixture(self, fixture): 245 """Use another fixture. 246 247 The fixture will be setUp, and self.addCleanup(fixture.cleanUp) called. 248 If the fixture fails to set up, useFixture will attempt to gather its 249 details into this fixture's details to aid in debugging. 250 251 :param fixture: The fixture to use. 252 :return: The fixture, after setting it up and scheduling a cleanup for 253 it. 254 :raises: Any errors raised by the fixture's setUp method. 255 """ 256 try: 257 fixture.setUp() 258 except MultipleExceptions as e: 259 if e.args[-1][0] is SetupError: 260 combine_details(e.args[-1][1].args[0], self._details) 261 raise 262 except: 263 # The child failed to come up and didn't raise MultipleExceptions 264 # which we can understand... capture any details it has (copying 265 # the content, it may go away anytime). 266 if gather_details is not None: 267 gather_details(fixture.getDetails(), self._details) 268 raise 269 else: 270 self.addCleanup(fixture.cleanUp) 271 # Calls to getDetails while this fixture is setup will return 272 # details from the child fixture. 273 self._detail_sources.append(fixture) 274 return fixture 275 276 277class FunctionFixture(Fixture): 278 """An adapter to use function(s) as a Fixture. 279 280 Typically used when an existing object or function interface exists but you 281 wish to use it as a Fixture (e.g. because fixtures are in use in your test 282 suite and this will fit in better). 283 284 To adapt an object with differently named setUp and cleanUp methods: 285 fixture = FunctionFixture(object.install, object.__class__.remove) 286 Note that the indirection via __class__ is to get an unbound method 287 which can accept the result from install. See also MethodFixture which 288 is specialised for objects. 289 290 To adapt functions: 291 fixture = FunctionFixture(tempfile.mkdtemp, shutil.rmtree) 292 293 With a reset function: 294 fixture = FunctionFixture(setup, cleanup, reset) 295 296 :ivar fn_result: The result of the setup_fn. Undefined outside of the 297 setUp, cleanUp context. 298 """ 299 300 def __init__(self, setup_fn, cleanup_fn=None, reset_fn=None): 301 """Create a FunctionFixture. 302 303 :param setup_fn: A callable which takes no parameters and returns the 304 thing you want to use. e.g. 305 def setup_fn(): 306 return 42 307 The result of setup_fn is assigned to the fn_result attribute bu 308 FunctionFixture.setUp. 309 :param cleanup_fn: Optional callable which takes a single parameter, which 310 must be that which is returned from the setup_fn. This is called 311 from cleanUp. 312 :param reset_fn: Optional callable which takes a single parameter like 313 cleanup_fn, but also returns a new object for use as the fn_result: 314 if defined this replaces the use of cleanup_fn and setup_fn when 315 reset() is called. 316 """ 317 super(FunctionFixture, self).__init__() 318 self.setup_fn = setup_fn 319 self.cleanup_fn = cleanup_fn 320 self.reset_fn = reset_fn 321 322 def _setUp(self): 323 fn_result = self.setup_fn() 324 self._maybe_cleanup(fn_result) 325 326 def reset(self): 327 if self.reset_fn is None: 328 super(FunctionFixture, self).reset() 329 else: 330 self._clear_cleanups() 331 fn_result = self.reset_fn(self.fn_result) 332 self._maybe_cleanup(fn_result) 333 334 def _maybe_cleanup(self, fn_result): 335 self.addCleanup(delattr, self, 'fn_result') 336 if self.cleanup_fn is not None: 337 self.addCleanup(self.cleanup_fn, fn_result) 338 self.fn_result = fn_result 339 340 341class MethodFixture(Fixture): 342 """An adapter to use a function as a Fixture. 343 344 Typically used when an existing object exists but you wish to use it as a 345 Fixture (e.g. because fixtures are in use in your test suite and this will 346 fit in better). 347 348 To adapt an object with setUp / tearDown methods: 349 fixture = MethodFixture(object) 350 If setUp / tearDown / reset are missing, they simply won't be called. 351 352 The object is exposed on fixture.obj. 353 354 To adapt an object with differently named setUp and cleanUp methods: 355 fixture = MethodFixture(object, setup=object.mySetUp, 356 teardown=object.myTearDown) 357 358 With a differently named reset function: 359 fixture = MethodFixture(object, reset=object.myReset) 360 361 :ivar obj: The object which is being wrapped. 362 """ 363 364 def __init__(self, obj, setup=None, cleanup=None, reset=None): 365 """Create a MethodFixture. 366 367 :param obj: The object to wrap. Exposed as fixture.obj 368 :param setup: A method which takes no parameters. e.g. 369 def setUp(self): 370 self.value = 42 371 If setup is not supplied, and the object has a setUp method, that 372 method is used, otherwise nothing will happen during fixture.setUp. 373 :param cleanup: Optional method to cleanup the object's state. If 374 not supplied the method 'tearDown' is used if it exists. 375 :param reset: Optional method to reset the wrapped object for use. 376 If not supplied, then the method 'reset' is used if it exists, 377 otherwise cleanUp and setUp are called as per Fixture.reset(). 378 """ 379 super(MethodFixture, self).__init__() 380 self.obj = obj 381 if setup is None: 382 setup = getattr(obj, 'setUp', None) 383 if setup is None: 384 setup = lambda: None 385 self._setup = setup 386 if cleanup is None: 387 cleanup = getattr(obj, 'tearDown', None) 388 if cleanup is None: 389 cleanup = lambda: None 390 self._cleanup = cleanup 391 if reset is None: 392 reset = getattr(obj, 'reset', None) 393 self._reset = reset 394 395 def _setUp(self): 396 self._setup() 397 398 def cleanUp(self): 399 super(MethodFixture, self).cleanUp() 400 self._cleanup() 401 402 def reset(self): 403 if self._reset is None: 404 super(MethodFixture, self).reset() 405 else: 406 self._reset() 407 408 409class CompoundFixture(Fixture): 410 """A fixture that combines many fixtures. 411 412 :ivar fixtures: The list of fixtures that make up this one. (read only). 413 """ 414 415 def __init__(self, fixtures): 416 """Construct a fixture made of many fixtures. 417 418 :param fixtures: An iterable of fixtures. 419 """ 420 super(CompoundFixture, self).__init__() 421 self.fixtures = list(fixtures) 422 423 def _setUp(self): 424 for fixture in self.fixtures: 425 self.useFixture(fixture) 426