root/trunk/twisted/trial/util.py

Revision 30752, 14.1 KB (checked in by exarkun, 15 months ago)

Rewrite the copyright headers to exclude date information.

Author: exarkun
Reviewer: glyph
Fixes: #4857

To avoid the need to perpetually update copyright dates in each file in Twisted,
remove the dates from most files and just leave them in the LICENSE file.

As a side effect, some files also have had a trailing newline added where it was
missing before.

Line 
1# -*- test-case-name: twisted.trial.test.test_util -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4#
5
6"""
7A collection of utility functions and classes, used internally by Trial.
8
9This code is for Trial's internal use.  Do NOT use this code if you are writing
10tests.  It is subject to change at the Trial maintainer's whim.  There is
11nothing here in this module for you to use unless you are maintaining Trial.
12
13Any non-Trial Twisted code that uses this module will be shot.
14
15Maintainer: Jonathan Lange
16"""
17
18import traceback, sys
19from random import randrange
20
21from twisted.internet import defer, utils, interfaces
22from twisted.python.failure import Failure
23from twisted.python import deprecate, versions
24from twisted.python.lockfile import FilesystemLock
25from twisted.python.filepath import FilePath
26
27DEFAULT_TIMEOUT = object()
28DEFAULT_TIMEOUT_DURATION = 120.0
29
30
31
32class DirtyReactorAggregateError(Exception):
33    """
34    Passed to L{twisted.trial.itrial.IReporter.addError} when the reactor is
35    left in an unclean state after a test.
36
37    @ivar delayedCalls: The L{DelayedCall} objects which weren't cleaned up.
38    @ivar selectables: The selectables which weren't cleaned up.
39    """
40
41    def __init__(self, delayedCalls, selectables=None):
42        self.delayedCalls = delayedCalls
43        self.selectables = selectables
44
45    def __str__(self):
46        """
47        Return a multi-line message describing all of the unclean state.
48        """
49        msg = "Reactor was unclean."
50        if self.delayedCalls:
51            msg += ("\nDelayedCalls: (set "
52                    "twisted.internet.base.DelayedCall.debug = True to "
53                    "debug)\n")
54            msg += "\n".join(map(str, self.delayedCalls))
55        if self.selectables:
56            msg += "\nSelectables:\n"
57            msg += "\n".join(map(str, self.selectables))
58        return msg
59
60
61
62class _Janitor(object):
63    """
64    The guy that cleans up after you.
65
66    @ivar test: The L{TestCase} to report errors about.
67    @ivar result: The L{IReporter} to report errors to.
68    @ivar reactor: The reactor to use. If None, the global reactor
69        will be used.
70    """
71    def __init__(self, test, result, reactor=None):
72        """
73        @param test: See L{_Janitor.test}.
74        @param result: See L{_Janitor.result}.
75        @param reactor: See L{_Janitor.reactor}.
76        """
77        self.test = test
78        self.result = result
79        self.reactor = reactor
80
81
82    def postCaseCleanup(self):
83        """
84        Called by L{unittest.TestCase} after a test to catch any logged errors
85        or pending L{DelayedCall}s.
86        """
87        calls = self._cleanPending()
88        if calls:
89            aggregate = DirtyReactorAggregateError(calls)
90            self.result.addError(self.test, Failure(aggregate))
91            return False
92        return True
93
94
95    def postClassCleanup(self):
96        """
97        Called by L{unittest.TestCase} after the last test in a C{TestCase}
98        subclass. Ensures the reactor is clean by murdering the threadpool,
99        catching any pending L{DelayedCall}s, open sockets etc.
100        """
101        selectables = self._cleanReactor()
102        calls = self._cleanPending()
103        if selectables or calls:
104            aggregate = DirtyReactorAggregateError(calls, selectables)
105            self.result.addError(self.test, Failure(aggregate))
106        self._cleanThreads()
107
108
109    def _getReactor(self):
110        """
111        Get either the passed-in reactor or the global reactor.
112        """
113        if self.reactor is not None:
114            reactor = self.reactor
115        else:
116            from twisted.internet import reactor
117        return reactor
118
119
120    def _cleanPending(self):
121        """
122        Cancel all pending calls and return their string representations.
123        """
124        reactor = self._getReactor()
125
126        # flush short-range timers
127        reactor.iterate(0)
128        reactor.iterate(0)
129
130        delayedCallStrings = []
131        for p in reactor.getDelayedCalls():
132            if p.active():
133                delayedString = str(p)
134                p.cancel()
135            else:
136                print "WEIRDNESS! pending timed call not active!"
137            delayedCallStrings.append(delayedString)
138        return delayedCallStrings
139    _cleanPending = utils.suppressWarnings(
140        _cleanPending, (('ignore',), {'category': DeprecationWarning,
141                                      'message':
142                                      r'reactor\.iterate cannot be used.*'}))
143
144    def _cleanThreads(self):
145        reactor = self._getReactor()
146        if interfaces.IReactorThreads.providedBy(reactor):
147            if reactor.threadpool is not None:
148                # Stop the threadpool now so that a new one is created.
149                # This improves test isolation somewhat (although this is a
150                # post class cleanup hook, so it's only isolating classes
151                # from each other, not methods from each other).
152                reactor._stopThreadPool()
153
154    def _cleanReactor(self):
155        """
156        Remove all selectables from the reactor, kill any of them that were
157        processes, and return their string representation.
158        """
159        reactor = self._getReactor()
160        selectableStrings = []
161        for sel in reactor.removeAll():
162            if interfaces.IProcessTransport.providedBy(sel):
163                sel.signalProcess('KILL')
164            selectableStrings.append(repr(sel))
165        return selectableStrings
166
167
168def excInfoOrFailureToExcInfo(err):
169    """
170    Coerce a Failure to an _exc_info, if err is a Failure.
171
172    @param err: Either a tuple such as returned by L{sys.exc_info} or a
173        L{Failure} object.
174    @return: A tuple like the one returned by L{sys.exc_info}. e.g.
175        C{exception_type, exception_object, traceback_object}.
176    """
177    if isinstance(err, Failure):
178        # Unwrap the Failure into a exc_info tuple.
179        err = (err.type, err.value, err.getTracebackObject())
180    return err
181
182
183def suppress(action='ignore', **kwarg):
184    """
185    Sets up the .suppress tuple properly, pass options to this method as you
186    would the stdlib warnings.filterwarnings()
187
188    So, to use this with a .suppress magic attribute you would do the
189    following:
190
191      >>> from twisted.trial import unittest, util
192      >>> import warnings
193      >>>
194      >>> class TestFoo(unittest.TestCase):
195      ...     def testFooBar(self):
196      ...         warnings.warn("i am deprecated", DeprecationWarning)
197      ...     testFooBar.suppress = [util.suppress(message='i am deprecated')]
198      ...
199      >>>
200
201    Note that as with the todo and timeout attributes: the module level
202    attribute acts as a default for the class attribute which acts as a default
203    for the method attribute. The suppress attribute can be overridden at any
204    level by specifying C{.suppress = []}
205    """
206    return ((action,), kwarg)
207
208
209def profiled(f, outputFile):
210    def _(*args, **kwargs):
211        if sys.version_info[0:2] != (2, 4):
212            import profile
213            prof = profile.Profile()
214            try:
215                result = prof.runcall(f, *args, **kwargs)
216                prof.dump_stats(outputFile)
217            except SystemExit:
218                pass
219            prof.print_stats()
220            return result
221        else: # use hotshot, profile is broken in 2.4
222            import hotshot.stats
223            prof = hotshot.Profile(outputFile)
224            try:
225                return prof.runcall(f, *args, **kwargs)
226            finally:
227                stats = hotshot.stats.load(outputFile)
228                stats.strip_dirs()
229                stats.sort_stats('cum')   # 'time'
230                stats.print_stats(100)
231    return _
232
233
234def getPythonContainers(meth):
235    """Walk up the Python tree from method 'meth', finding its class, its module
236    and all containing packages."""
237    containers = []
238    containers.append(meth.im_class)
239    moduleName = meth.im_class.__module__
240    while moduleName is not None:
241        module = sys.modules.get(moduleName, None)
242        if module is None:
243            module = __import__(moduleName)
244        containers.append(module)
245        moduleName = getattr(module, '__module__', None)
246    return containers
247
248
249_DEFAULT = object()
250def acquireAttribute(objects, attr, default=_DEFAULT):
251    """Go through the list 'objects' sequentially until we find one which has
252    attribute 'attr', then return the value of that attribute.  If not found,
253    return 'default' if set, otherwise, raise AttributeError. """
254    for obj in objects:
255        if hasattr(obj, attr):
256            return getattr(obj, attr)
257    if default is not _DEFAULT:
258        return default
259    raise AttributeError('attribute %r not found in %r' % (attr, objects))
260
261
262
263deprecate.deprecatedModuleAttribute(
264    versions.Version("Twisted", 10, 1, 0),
265    "Please use twisted.python.reflect.namedAny instead.",
266    __name__, "findObject")
267
268
269
270def findObject(name):
271    """Get a fully-named package, module, module-global object or attribute.
272    Forked from twisted.python.reflect.namedAny.
273
274    Returns a tuple of (bool, obj).  If bool is True, the named object exists
275    and is returned as obj.  If bool is False, the named object does not exist
276    and the value of obj is unspecified.
277    """
278    names = name.split('.')
279    topLevelPackage = None
280    moduleNames = names[:]
281    while not topLevelPackage:
282        trialname = '.'.join(moduleNames)
283        if len(trialname) == 0:
284            return (False, None)
285        try:
286            topLevelPackage = __import__(trialname)
287        except ImportError:
288            # if the ImportError happened in the module being imported,
289            # this is a failure that should be handed to our caller.
290            # count stack frames to tell the difference.
291            exc_info = sys.exc_info()
292            if len(traceback.extract_tb(exc_info[2])) > 1:
293                try:
294                    # Clean up garbage left in sys.modules.
295                    del sys.modules[trialname]
296                except KeyError:
297                    # Python 2.4 has fixed this.  Yay!
298                    pass
299                raise exc_info[0], exc_info[1], exc_info[2]
300            moduleNames.pop()
301    obj = topLevelPackage
302    for n in names[1:]:
303        try:
304            obj = getattr(obj, n)
305        except AttributeError:
306            return (False, obj)
307    return (True, obj)
308
309
310
311def _runSequentially(callables, stopOnFirstError=False):
312    """
313    Run the given callables one after the other. If a callable returns a
314    Deferred, wait until it has finished before running the next callable.
315
316    @param callables: An iterable of callables that take no parameters.
317
318    @param stopOnFirstError: If True, then stop running callables as soon as
319        one raises an exception or fires an errback. False by default.
320
321    @return: A L{Deferred} that fires a list of C{(flag, value)} tuples. Each
322        tuple will be either C{(SUCCESS, <return value>)} or C{(FAILURE,
323        <Failure>)}.
324    """
325    results = []
326    for f in callables:
327        d = defer.maybeDeferred(f)
328        thing = defer.waitForDeferred(d)
329        yield thing
330        try:
331            results.append((defer.SUCCESS, thing.getResult()))
332        except:
333            results.append((defer.FAILURE, Failure()))
334            if stopOnFirstError:
335                break
336    yield results
337_runSequentially = defer.deferredGenerator(_runSequentially)
338
339
340
341class _NoTrialMarker(Exception):
342    """
343    No trial marker file could be found.
344
345    Raised when trial attempts to remove a trial temporary working directory
346    that does not contain a marker file.
347    """
348
349
350
351def _removeSafely(path):
352    """
353    Safely remove a path, recursively.
354
355    If C{path} does not contain a node named C{_trial_marker}, a
356    L{_NoTrialmarker} exception is raised and the path is not removed.
357    """
358    if not path.child('_trial_marker').exists():
359        raise _NoTrialMarker(
360            '%r is not a trial temporary path, refusing to remove it'
361            % (path,))
362    try:
363        path.remove()
364    except OSError, e:
365        print ("could not remove %r, caught OSError [Errno %s]: %s"
366               % (path, e.errno, e.strerror))
367        try:
368            newPath = FilePath('_trial_temp_old%s' % (randrange(1000000),))
369            path.moveTo(newPath)
370        except OSError, e:
371            print ("could not rename path, caught OSError [Errno %s]: %s"
372                   % (e.errno,e.strerror))
373            raise
374
375
376
377class _WorkingDirectoryBusy(Exception):
378    """
379    A working directory was specified to the runner, but another test run is
380    currently using that directory.
381    """
382
383
384
385def _unusedTestDirectory(base):
386    """
387    Find an unused directory named similarly to C{base}.
388
389    Once a directory is found, it will be locked and a marker dropped into it to
390    identify it as a trial temporary directory.
391
392    @param base: A template path for the discovery process.  If this path
393        exactly cannot be used, a path which varies only in a suffix of the
394        basename will be used instead.
395    @type base: L{FilePath}
396
397    @return: A two-tuple.  The first element is a L{FilePath} representing the
398        directory which was found and created.  The second element is a locked
399        L{FilesystemLock}.  Another call to C{_unusedTestDirectory} will not be
400        able to reused the the same name until the lock is released, either
401        explicitly or by this process exiting.
402    """
403    counter = 0
404    while True:
405        if counter:
406            testdir = base.sibling('%s-%d' % (base.basename(), counter))
407        else:
408            testdir = base
409
410        testDirLock = FilesystemLock(testdir.path + '.lock')
411        if testDirLock.lock():
412            # It is not in use
413            if testdir.exists():
414                # It exists though - delete it
415                _removeSafely(testdir)
416
417            # Create it anew and mark it as ours so the next _removeSafely on it
418            # succeeds.
419            testdir.makedirs()
420            testdir.child('_trial_marker').setContent('')
421            return testdir, testDirLock
422        else:
423            # It is in use
424            if base.basename() == '_trial_temp':
425                counter += 1
426            else:
427                raise _WorkingDirectoryBusy()
428
429
430__all__ = ['excInfoOrFailureToExcInfo', 'suppress']
Note: See TracBrowser for help on using the browser.