| 1 | # -*- test-case-name: twisted.trial.test.test_util -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | # |
|---|
| 5 | |
|---|
| 6 | """ |
|---|
| 7 | A collection of utility functions and classes, used internally by Trial. |
|---|
| 8 | |
|---|
| 9 | This code is for Trial's internal use. Do NOT use this code if you are writing |
|---|
| 10 | tests. It is subject to change at the Trial maintainer's whim. There is |
|---|
| 11 | nothing here in this module for you to use unless you are maintaining Trial. |
|---|
| 12 | |
|---|
| 13 | Any non-Trial Twisted code that uses this module will be shot. |
|---|
| 14 | |
|---|
| 15 | Maintainer: Jonathan Lange |
|---|
| 16 | """ |
|---|
| 17 | |
|---|
| 18 | import traceback, sys |
|---|
| 19 | from random import randrange |
|---|
| 20 | |
|---|
| 21 | from twisted.internet import defer, utils, interfaces |
|---|
| 22 | from twisted.python.failure import Failure |
|---|
| 23 | from twisted.python import deprecate, versions |
|---|
| 24 | from twisted.python.lockfile import FilesystemLock |
|---|
| 25 | from twisted.python.filepath import FilePath |
|---|
| 26 | |
|---|
| 27 | DEFAULT_TIMEOUT = object() |
|---|
| 28 | DEFAULT_TIMEOUT_DURATION = 120.0 |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | class 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 | |
|---|
| 62 | class _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 | |
|---|
| 168 | def 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 | |
|---|
| 183 | def 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 | |
|---|
| 209 | def 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 | |
|---|
| 234 | def 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() |
|---|
| 250 | def 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 | |
|---|
| 263 | deprecate.deprecatedModuleAttribute( |
|---|
| 264 | versions.Version("Twisted", 10, 1, 0), |
|---|
| 265 | "Please use twisted.python.reflect.namedAny instead.", |
|---|
| 266 | __name__, "findObject") |
|---|
| 267 | |
|---|
| 268 | |
|---|
| 269 | |
|---|
| 270 | def 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 | |
|---|
| 311 | def _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 | |
|---|
| 341 | class _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 | |
|---|
| 351 | def _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 | |
|---|
| 377 | class _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 | |
|---|
| 385 | def _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'] |
|---|