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