mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 19:11:08 -05:00
okay fine
This commit is contained in:
50
.venv/lib/python3.12/site-packages/twisted/trial/__init__.py
Normal file
50
.venv/lib/python3.12/site-packages/twisted/trial/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
# Maintainer: Jonathan Lange
|
||||
|
||||
"""
|
||||
Twisted Trial: Asynchronous unit testing framework.
|
||||
|
||||
Trial extends Python's builtin C{unittest} to provide support for asynchronous
|
||||
tests.
|
||||
|
||||
Trial strives to be compatible with other Python xUnit testing frameworks.
|
||||
"Compatibility" is a difficult things to define. In practice, it means that:
|
||||
|
||||
- L{twisted.trial.unittest.TestCase} objects should be able to be used by
|
||||
other test runners without those runners requiring special support for
|
||||
Trial tests.
|
||||
|
||||
- Tests that subclass the standard library C{TestCase} and don't do anything
|
||||
"too weird" should be able to be discoverable and runnable by the Trial
|
||||
test runner without the authors of those tests having to jump through
|
||||
hoops.
|
||||
|
||||
- Tests that implement the interface provided by the standard library
|
||||
C{TestCase} should be runnable by the Trial runner.
|
||||
|
||||
- The Trial test runner and Trial L{unittest.TestCase} objects ought to be
|
||||
able to use standard library C{TestResult} objects, and third party
|
||||
C{TestResult} objects based on the standard library.
|
||||
|
||||
This list is not necessarily exhaustive -- compatibility is hard to define.
|
||||
Contributors who discover more helpful ways of defining compatibility are
|
||||
encouraged to update this document.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
B{Timeouts} for tests should be implemented in the runner. If this is done,
|
||||
then timeouts could work for third-party TestCase objects as well as for
|
||||
L{twisted.trial.unittest.TestCase} objects. Further, Twisted C{TestCase}
|
||||
objects will run in other runners without timing out.
|
||||
See U{http://twistedmatrix.com/trac/ticket/2675}.
|
||||
|
||||
Running tests in a temporary directory should be a feature of the test case,
|
||||
because often tests themselves rely on this behaviour. If the feature is
|
||||
implemented in the runner, then tests will change behaviour (possibly
|
||||
breaking) when run in a different test runner. Further, many tests don't even
|
||||
care about the filesystem.
|
||||
See U{http://twistedmatrix.com/trac/ticket/2916}.
|
||||
"""
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from twisted.scripts.trial import run
|
||||
|
||||
sys.exit(run())
|
||||
176
.venv/lib/python3.12/site-packages/twisted/trial/_asyncrunner.py
Normal file
176
.venv/lib/python3.12/site-packages/twisted/trial/_asyncrunner.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# -*- test-case-name: twisted.trial.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Infrastructure for test running and suites.
|
||||
"""
|
||||
|
||||
|
||||
import doctest
|
||||
import gc
|
||||
import unittest as pyunit
|
||||
from typing import Iterator, Union
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.python import components
|
||||
from twisted.trial import itrial, reporter
|
||||
from twisted.trial._synctest import _logObserver
|
||||
|
||||
|
||||
class TestSuite(pyunit.TestSuite):
|
||||
"""
|
||||
Extend the standard library's C{TestSuite} with a consistently overrideable
|
||||
C{run} method.
|
||||
"""
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Call C{run} on every member of the suite.
|
||||
"""
|
||||
for test in self._tests:
|
||||
if result.shouldStop:
|
||||
break
|
||||
test(result)
|
||||
return result
|
||||
|
||||
|
||||
@implementer(itrial.ITestCase)
|
||||
class TestDecorator(
|
||||
components.proxyForInterface( # type: ignore[misc]
|
||||
itrial.ITestCase, "_originalTest"
|
||||
)
|
||||
):
|
||||
"""
|
||||
Decorator for test cases.
|
||||
|
||||
@param _originalTest: The wrapped instance of test.
|
||||
@type _originalTest: A provider of L{itrial.ITestCase}
|
||||
"""
|
||||
|
||||
def __call__(self, result):
|
||||
"""
|
||||
Run the unit test.
|
||||
|
||||
@param result: A TestResult object.
|
||||
"""
|
||||
return self.run(result)
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Run the unit test.
|
||||
|
||||
@param result: A TestResult object.
|
||||
"""
|
||||
return self._originalTest.run(reporter._AdaptedReporter(result, self.__class__))
|
||||
|
||||
|
||||
def _clearSuite(suite):
|
||||
"""
|
||||
Clear all tests from C{suite}.
|
||||
|
||||
This messes with the internals of C{suite}. In particular, it assumes that
|
||||
the suite keeps all of its tests in a list in an instance variable called
|
||||
C{_tests}.
|
||||
"""
|
||||
suite._tests = []
|
||||
|
||||
|
||||
def decorate(test, decorator):
|
||||
"""
|
||||
Decorate all test cases in C{test} with C{decorator}.
|
||||
|
||||
C{test} can be a test case or a test suite. If it is a test suite, then the
|
||||
structure of the suite is preserved.
|
||||
|
||||
L{decorate} tries to preserve the class of the test suites it finds, but
|
||||
assumes the presence of the C{_tests} attribute on the suite.
|
||||
|
||||
@param test: The C{TestCase} or C{TestSuite} to decorate.
|
||||
|
||||
@param decorator: A unary callable used to decorate C{TestCase}s.
|
||||
|
||||
@return: A decorated C{TestCase} or a C{TestSuite} containing decorated
|
||||
C{TestCase}s.
|
||||
"""
|
||||
|
||||
try:
|
||||
tests = iter(test)
|
||||
except TypeError:
|
||||
return decorator(test)
|
||||
|
||||
# At this point, we know that 'test' is a test suite.
|
||||
_clearSuite(test)
|
||||
|
||||
for case in tests:
|
||||
test.addTest(decorate(case, decorator))
|
||||
return test
|
||||
|
||||
|
||||
class _PyUnitTestCaseAdapter(TestDecorator):
|
||||
"""
|
||||
Adapt from pyunit.TestCase to ITestCase.
|
||||
"""
|
||||
|
||||
|
||||
class _BrokenIDTestCaseAdapter(_PyUnitTestCaseAdapter):
|
||||
"""
|
||||
Adapter for pyunit-style C{TestCase} subclasses that have undesirable id()
|
||||
methods. That is C{unittest.FunctionTestCase} and C{unittest.DocTestCase}.
|
||||
"""
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
Return the fully-qualified Python name of the doctest.
|
||||
"""
|
||||
testID = self._originalTest.shortDescription()
|
||||
if testID is not None:
|
||||
return testID
|
||||
return self._originalTest.id()
|
||||
|
||||
|
||||
class _ForceGarbageCollectionDecorator(TestDecorator):
|
||||
"""
|
||||
Forces garbage collection to be run before and after the test. Any errors
|
||||
logged during the post-test collection are added to the test result as
|
||||
errors.
|
||||
"""
|
||||
|
||||
def run(self, result):
|
||||
gc.collect()
|
||||
TestDecorator.run(self, result)
|
||||
_logObserver._add()
|
||||
gc.collect()
|
||||
for error in _logObserver.getErrors():
|
||||
result.addError(self, error)
|
||||
_logObserver.flushErrors()
|
||||
_logObserver._remove()
|
||||
|
||||
|
||||
components.registerAdapter(_PyUnitTestCaseAdapter, pyunit.TestCase, itrial.ITestCase)
|
||||
|
||||
|
||||
components.registerAdapter(
|
||||
_BrokenIDTestCaseAdapter, pyunit.FunctionTestCase, itrial.ITestCase
|
||||
)
|
||||
|
||||
|
||||
_docTestCase = getattr(doctest, "DocTestCase", None)
|
||||
if _docTestCase:
|
||||
components.registerAdapter(_BrokenIDTestCaseAdapter, _docTestCase, itrial.ITestCase)
|
||||
|
||||
|
||||
def _iterateTests(
|
||||
testSuiteOrCase: Union[pyunit.TestCase, pyunit.TestSuite]
|
||||
) -> Iterator[itrial.ITestCase]:
|
||||
"""
|
||||
Iterate through all of the test cases in C{testSuiteOrCase}.
|
||||
"""
|
||||
try:
|
||||
suite = iter(testSuiteOrCase) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
yield testSuiteOrCase # type: ignore[misc]
|
||||
else:
|
||||
for test in suite:
|
||||
yield from _iterateTests(test)
|
||||
413
.venv/lib/python3.12/site-packages/twisted/trial/_asynctest.py
Normal file
413
.venv/lib/python3.12/site-packages/twisted/trial/_asynctest.py
Normal file
@@ -0,0 +1,413 @@
|
||||
# -*- test-case-name: twisted.trial.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Things likely to be used by writers of unit tests.
|
||||
|
||||
Maintainer: Jonathan Lange
|
||||
"""
|
||||
|
||||
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Callable, List
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
# We can't import reactor at module-level because this code runs before trial
|
||||
# installs a user-specified reactor, installing the default reactor and
|
||||
# breaking reactor installation. See also #6047.
|
||||
from twisted.internet import defer, utils
|
||||
from twisted.python import failure
|
||||
from twisted.trial import itrial, util
|
||||
from twisted.trial._synctest import FailTest, SkipTest, SynchronousTestCase
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
_wait_is_running: List[None] = []
|
||||
|
||||
|
||||
@implementer(itrial.ITestCase)
|
||||
class TestCase(SynchronousTestCase):
|
||||
"""
|
||||
A unit test. The atom of the unit testing universe.
|
||||
|
||||
This class extends L{SynchronousTestCase} which extends C{unittest.TestCase}
|
||||
from the standard library. The main feature is the ability to return
|
||||
C{Deferred}s from tests and fixture methods and to have the suite wait for
|
||||
those C{Deferred}s to fire. Also provides new assertions such as
|
||||
L{assertFailure}.
|
||||
|
||||
@ivar timeout: A real number of seconds. If set, the test will
|
||||
raise an error if it takes longer than C{timeout} seconds.
|
||||
If not set, util.DEFAULT_TIMEOUT_DURATION is used.
|
||||
"""
|
||||
|
||||
def __init__(self, methodName="runTest"):
|
||||
"""
|
||||
Construct an asynchronous test case for C{methodName}.
|
||||
|
||||
@param methodName: The name of a method on C{self}. This method should
|
||||
be a unit test. That is, it should be a short method that calls some of
|
||||
the assert* methods. If C{methodName} is unspecified,
|
||||
L{SynchronousTestCase.runTest} will be used as the test method. This is
|
||||
mostly useful for testing Trial.
|
||||
"""
|
||||
super().__init__(methodName)
|
||||
|
||||
def assertFailure(self, deferred, *expectedFailures):
|
||||
"""
|
||||
Fail if C{deferred} does not errback with one of C{expectedFailures}.
|
||||
Returns the original Deferred with callbacks added. You will need
|
||||
to return this Deferred from your test case.
|
||||
"""
|
||||
|
||||
def _cb(ignore):
|
||||
raise self.failureException(
|
||||
f"did not catch an error, instead got {ignore!r}"
|
||||
)
|
||||
|
||||
def _eb(failure):
|
||||
if failure.check(*expectedFailures):
|
||||
return failure.value
|
||||
else:
|
||||
output = "\nExpected: {!r}\nGot:\n{}".format(
|
||||
expectedFailures, str(failure)
|
||||
)
|
||||
raise self.failureException(output)
|
||||
|
||||
return deferred.addCallbacks(_cb, _eb)
|
||||
|
||||
failUnlessFailure = assertFailure
|
||||
|
||||
def _run(self, methodName, result):
|
||||
from twisted.internet import reactor
|
||||
|
||||
timeout = self.getTimeout()
|
||||
|
||||
def onTimeout(d):
|
||||
e = defer.TimeoutError(
|
||||
f"{self!r} ({methodName}) still running at {timeout} secs"
|
||||
)
|
||||
f = failure.Failure(e)
|
||||
# try to errback the deferred that the test returns (for no gorram
|
||||
# reason) (see issue1005 and test_errorPropagation in
|
||||
# test_deferred)
|
||||
try:
|
||||
d.errback(f)
|
||||
except defer.AlreadyCalledError:
|
||||
# if the deferred has been called already but the *back chain
|
||||
# is still unfinished, crash the reactor and report timeout
|
||||
# error ourself.
|
||||
reactor.crash()
|
||||
self._timedOut = True # see self._wait
|
||||
todo = self.getTodo()
|
||||
if todo is not None and todo.expected(f):
|
||||
result.addExpectedFailure(self, f, todo)
|
||||
else:
|
||||
result.addError(self, f)
|
||||
|
||||
onTimeout = utils.suppressWarnings(
|
||||
onTimeout, util.suppress(category=DeprecationWarning)
|
||||
)
|
||||
method = getattr(self, methodName)
|
||||
if inspect.isgeneratorfunction(method):
|
||||
exc = TypeError(
|
||||
"{!r} is a generator function and therefore will never run".format(
|
||||
method
|
||||
)
|
||||
)
|
||||
return defer.fail(exc)
|
||||
d = defer.maybeDeferred(
|
||||
utils.runWithWarningsSuppressed, self._getSuppress(), method
|
||||
)
|
||||
call = reactor.callLater(timeout, onTimeout, d)
|
||||
d.addBoth(lambda x: call.active() and call.cancel() or x)
|
||||
return d
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
def deferSetUp(self, ignored, result):
|
||||
d = self._run("setUp", result)
|
||||
d.addCallbacks(
|
||||
self.deferTestMethod,
|
||||
self._ebDeferSetUp,
|
||||
callbackArgs=(result,),
|
||||
errbackArgs=(result,),
|
||||
)
|
||||
return d
|
||||
|
||||
def _ebDeferSetUp(self, failure, result):
|
||||
if failure.check(SkipTest):
|
||||
result.addSkip(self, self._getSkipReason(self.setUp, failure.value))
|
||||
else:
|
||||
result.addError(self, failure)
|
||||
if failure.check(KeyboardInterrupt):
|
||||
result.stop()
|
||||
return self.deferRunCleanups(None, result)
|
||||
|
||||
def deferTestMethod(self, ignored, result):
|
||||
d = self._run(self._testMethodName, result)
|
||||
d.addCallbacks(
|
||||
self._cbDeferTestMethod,
|
||||
self._ebDeferTestMethod,
|
||||
callbackArgs=(result,),
|
||||
errbackArgs=(result,),
|
||||
)
|
||||
d.addBoth(self.deferRunCleanups, result)
|
||||
d.addBoth(self.deferTearDown, result)
|
||||
return d
|
||||
|
||||
def _cbDeferTestMethod(self, ignored, result):
|
||||
if self.getTodo() is not None:
|
||||
result.addUnexpectedSuccess(self, self.getTodo())
|
||||
else:
|
||||
self._passed = True
|
||||
return ignored
|
||||
|
||||
def _ebDeferTestMethod(self, f, result):
|
||||
todo = self.getTodo()
|
||||
if todo is not None and todo.expected(f):
|
||||
result.addExpectedFailure(self, f, todo)
|
||||
elif f.check(self.failureException, FailTest):
|
||||
result.addFailure(self, f)
|
||||
elif f.check(KeyboardInterrupt):
|
||||
result.addError(self, f)
|
||||
result.stop()
|
||||
elif f.check(SkipTest):
|
||||
result.addSkip(
|
||||
self, self._getSkipReason(getattr(self, self._testMethodName), f.value)
|
||||
)
|
||||
else:
|
||||
result.addError(self, f)
|
||||
|
||||
def deferTearDown(self, ignored, result):
|
||||
d = self._run("tearDown", result)
|
||||
d.addErrback(self._ebDeferTearDown, result)
|
||||
return d
|
||||
|
||||
def _ebDeferTearDown(self, failure, result):
|
||||
result.addError(self, failure)
|
||||
if failure.check(KeyboardInterrupt):
|
||||
result.stop()
|
||||
self._passed = False
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def deferRunCleanups(self, ignored, result):
|
||||
"""
|
||||
Run any scheduled cleanups and report errors (if any) to the result.
|
||||
object.
|
||||
"""
|
||||
failures = []
|
||||
while len(self._cleanups) > 0:
|
||||
func, args, kwargs = self._cleanups.pop()
|
||||
try:
|
||||
yield func(*args, **kwargs)
|
||||
except Exception:
|
||||
failures.append(failure.Failure())
|
||||
|
||||
for f in failures:
|
||||
result.addError(self, f)
|
||||
self._passed = False
|
||||
|
||||
def _cleanUp(self, result):
|
||||
try:
|
||||
clean = util._Janitor(self, result).postCaseCleanup()
|
||||
if not clean:
|
||||
self._passed = False
|
||||
except BaseException:
|
||||
result.addError(self, failure.Failure())
|
||||
self._passed = False
|
||||
for error in self._observer.getErrors():
|
||||
result.addError(self, error)
|
||||
self._passed = False
|
||||
self.flushLoggedErrors()
|
||||
self._removeObserver()
|
||||
if self._passed:
|
||||
result.addSuccess(self)
|
||||
|
||||
def _classCleanUp(self, result):
|
||||
try:
|
||||
util._Janitor(self, result).postClassCleanup()
|
||||
except BaseException:
|
||||
result.addError(self, failure.Failure())
|
||||
|
||||
def _makeReactorMethod(self, name):
|
||||
"""
|
||||
Create a method which wraps the reactor method C{name}. The new
|
||||
method issues a deprecation warning and calls the original.
|
||||
"""
|
||||
|
||||
def _(*a, **kw):
|
||||
warnings.warn(
|
||||
"reactor.%s cannot be used inside unit tests. "
|
||||
"In the future, using %s will fail the test and may "
|
||||
"crash or hang the test run." % (name, name),
|
||||
stacklevel=2,
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
return self._reactorMethods[name](*a, **kw)
|
||||
|
||||
return _
|
||||
|
||||
def _deprecateReactor(self, reactor):
|
||||
"""
|
||||
Deprecate C{iterate}, C{crash} and C{stop} on C{reactor}. That is,
|
||||
each method is wrapped in a function that issues a deprecation
|
||||
warning, then calls the original.
|
||||
|
||||
@param reactor: The Twisted reactor.
|
||||
"""
|
||||
self._reactorMethods = {}
|
||||
for name in ["crash", "iterate", "stop"]:
|
||||
self._reactorMethods[name] = getattr(reactor, name)
|
||||
setattr(reactor, name, self._makeReactorMethod(name))
|
||||
|
||||
def _undeprecateReactor(self, reactor):
|
||||
"""
|
||||
Restore the deprecated reactor methods. Undoes what
|
||||
L{_deprecateReactor} did.
|
||||
|
||||
@param reactor: The Twisted reactor.
|
||||
"""
|
||||
for name, method in self._reactorMethods.items():
|
||||
setattr(reactor, name, method)
|
||||
self._reactorMethods = {}
|
||||
|
||||
def _runFixturesAndTest(self, result):
|
||||
"""
|
||||
Really run C{setUp}, the test method, and C{tearDown}. Any of these may
|
||||
return L{defer.Deferred}s. After they complete, do some reactor cleanup.
|
||||
|
||||
@param result: A L{TestResult} object.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
self._deprecateReactor(reactor)
|
||||
self._timedOut = False
|
||||
try:
|
||||
d = self.deferSetUp(None, result)
|
||||
try:
|
||||
self._wait(d)
|
||||
finally:
|
||||
self._cleanUp(result)
|
||||
self._classCleanUp(result)
|
||||
finally:
|
||||
self._undeprecateReactor(reactor)
|
||||
|
||||
# f should be a positional only argument but that is a breaking change
|
||||
# see https://github.com/twisted/twisted/issues/11967
|
||||
def addCleanup( # type: ignore[override]
|
||||
self, f: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Extend the base cleanup feature with support for cleanup functions which
|
||||
return Deferreds.
|
||||
|
||||
If the function C{f} returns a Deferred, C{TestCase} will wait until the
|
||||
Deferred has fired before proceeding to the next function.
|
||||
"""
|
||||
return super().addCleanup(f, *args, **kwargs)
|
||||
|
||||
def getSuppress(self):
|
||||
return self._getSuppress()
|
||||
|
||||
def getTimeout(self):
|
||||
"""
|
||||
Returns the timeout value set on this test. Checks on the instance
|
||||
first, then the class, then the module, then packages. As soon as it
|
||||
finds something with a C{timeout} attribute, returns that. Returns
|
||||
L{util.DEFAULT_TIMEOUT_DURATION} if it cannot find anything. See
|
||||
L{TestCase} docstring for more details.
|
||||
"""
|
||||
timeout = util.acquireAttribute(
|
||||
self._parents, "timeout", util.DEFAULT_TIMEOUT_DURATION
|
||||
)
|
||||
try:
|
||||
return float(timeout)
|
||||
except (ValueError, TypeError):
|
||||
# XXX -- this is here because sometimes people will have methods
|
||||
# called 'timeout', or set timeout to 'orange', or something
|
||||
# Particularly, test_news.NewsTestCase and ReactorCoreTestCase
|
||||
# both do this.
|
||||
warnings.warn(
|
||||
"'timeout' attribute needs to be a number.", category=DeprecationWarning
|
||||
)
|
||||
return util.DEFAULT_TIMEOUT_DURATION
|
||||
|
||||
def _wait(self, d, running=_wait_is_running):
|
||||
"""Take a Deferred that only ever callbacks. Block until it happens."""
|
||||
if running:
|
||||
raise RuntimeError("_wait is not reentrant")
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
results = []
|
||||
|
||||
def append(any):
|
||||
if results is not None:
|
||||
results.append(any)
|
||||
|
||||
def crash(ign):
|
||||
if results is not None:
|
||||
reactor.crash()
|
||||
|
||||
crash = utils.suppressWarnings(
|
||||
crash,
|
||||
util.suppress(
|
||||
message=r"reactor\.crash cannot be used.*", category=DeprecationWarning
|
||||
),
|
||||
)
|
||||
|
||||
def stop():
|
||||
reactor.crash()
|
||||
|
||||
stop = utils.suppressWarnings(
|
||||
stop,
|
||||
util.suppress(
|
||||
message=r"reactor\.crash cannot be used.*", category=DeprecationWarning
|
||||
),
|
||||
)
|
||||
|
||||
running.append(None)
|
||||
try:
|
||||
d.addBoth(append)
|
||||
if results:
|
||||
# d might have already been fired, in which case append is
|
||||
# called synchronously. Avoid any reactor stuff.
|
||||
return
|
||||
d.addBoth(crash)
|
||||
reactor.stop = stop
|
||||
try:
|
||||
reactor.run()
|
||||
finally:
|
||||
del reactor.stop
|
||||
|
||||
# If the reactor was crashed elsewhere due to a timeout, hopefully
|
||||
# that crasher also reported an error. Just return.
|
||||
# _timedOut is most likely to be set when d has fired but hasn't
|
||||
# completed its callback chain (see self._run)
|
||||
if results or self._timedOut: # defined in run() and _run()
|
||||
return
|
||||
|
||||
# If the timeout didn't happen, and we didn't get a result or
|
||||
# a failure, then the user probably aborted the test, so let's
|
||||
# just raise KeyboardInterrupt.
|
||||
|
||||
# FIXME: imagine this:
|
||||
# web/test/test_webclient.py:
|
||||
# exc = self.assertRaises(error.Error, wait, method(url))
|
||||
#
|
||||
# wait() will raise KeyboardInterrupt, and assertRaises will
|
||||
# swallow it. Therefore, wait() raising KeyboardInterrupt is
|
||||
# insufficient to stop trial. A suggested solution is to have
|
||||
# this code set a "stop trial" flag, or otherwise notify trial
|
||||
# that it should really try to stop as soon as possible.
|
||||
raise KeyboardInterrupt()
|
||||
finally:
|
||||
results = None
|
||||
running.pop()
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This package implements the distributed Trial test runner:
|
||||
|
||||
- The L{twisted.trial._dist.disttrial} module implements a test runner which
|
||||
runs in a manager process and can launch additional worker processes in
|
||||
which to run tests and gather up results from all of them.
|
||||
|
||||
- The L{twisted.trial._dist.options} module defines command line options used
|
||||
to configure the distributed test runner.
|
||||
|
||||
- The L{twisted.trial._dist.managercommands} module defines AMP commands
|
||||
which are sent from worker processes back to the manager process to report
|
||||
the results of tests.
|
||||
|
||||
- The L{twisted.trial._dist.workercommands} module defines AMP commands which
|
||||
are sent from the manager process to the worker processes to control the
|
||||
execution of tests there.
|
||||
|
||||
- The L{twisted.trial._dist.distreporter} module defines a proxy for
|
||||
L{twisted.trial.itrial.IReporter} which enforces the typical requirement
|
||||
that results be passed to a reporter for only one test at a time, allowing
|
||||
any reporter to be used with despite disttrial's simultaneously running
|
||||
tests.
|
||||
|
||||
- The L{twisted.trial._dist.workerreporter} module implements a
|
||||
L{twisted.trial.itrial.IReporter} which is used by worker processes and
|
||||
reports results back to the manager process using AMP commands.
|
||||
|
||||
- The L{twisted.trial._dist.workertrial} module is a runnable script which is
|
||||
the main point for worker processes.
|
||||
|
||||
- The L{twisted.trial._dist.worker} process defines the manager's AMP
|
||||
protocol for accepting results from worker processes and a process protocol
|
||||
for use running workers as local child processes (as opposed to
|
||||
distributing them to another host).
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
# File descriptors numbers used to set up pipes with the worker.
|
||||
_WORKER_AMP_STDIN = 3
|
||||
|
||||
_WORKER_AMP_STDOUT = 4
|
||||
@@ -0,0 +1,90 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_distreporter -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The reporter is not made to support concurrent test running, so we will
|
||||
hold test results in here and only send them to the reporter once the
|
||||
test is over.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
from types import TracebackType
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.python.components import proxyForInterface
|
||||
from twisted.python.failure import Failure
|
||||
from ..itrial import IReporter, ITestCase
|
||||
|
||||
ReporterFailure = Union[Failure, Tuple[type, Exception, TracebackType]]
|
||||
|
||||
|
||||
@implementer(IReporter)
|
||||
class DistReporter(proxyForInterface(IReporter)): # type: ignore[misc]
|
||||
"""
|
||||
See module docstring.
|
||||
"""
|
||||
|
||||
def __init__(self, original):
|
||||
super().__init__(original)
|
||||
self.running = {}
|
||||
|
||||
def startTest(self, test):
|
||||
"""
|
||||
Queue test starting.
|
||||
"""
|
||||
self.running[test.id()] = []
|
||||
self.running[test.id()].append((self.original.startTest, test))
|
||||
|
||||
def addFailure(self, test: ITestCase, fail: ReporterFailure) -> None:
|
||||
"""
|
||||
Queue adding a failure.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addFailure, test, fail))
|
||||
|
||||
def addError(self, test: ITestCase, error: ReporterFailure) -> None:
|
||||
"""
|
||||
Queue error adding.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addError, test, error))
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
"""
|
||||
Queue adding a skip.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addSkip, test, reason))
|
||||
|
||||
def addUnexpectedSuccess(self, test, todo=None):
|
||||
"""
|
||||
Queue adding an unexpected success.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addUnexpectedSuccess, test, todo))
|
||||
|
||||
def addExpectedFailure(
|
||||
self, test: ITestCase, error: ReporterFailure, todo: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Queue adding an expected failure.
|
||||
"""
|
||||
self.running[test.id()].append(
|
||||
(self.original.addExpectedFailure, test, error, todo)
|
||||
)
|
||||
|
||||
def addSuccess(self, test):
|
||||
"""
|
||||
Queue adding a success.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addSuccess, test))
|
||||
|
||||
def stopTest(self, test):
|
||||
"""
|
||||
Queue stopping the test, then unroll the queue.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.stopTest, test))
|
||||
for step in self.running[test.id()]:
|
||||
step[0](*step[1:])
|
||||
del self.running[test.id()]
|
||||
@@ -0,0 +1,512 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_disttrial -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the trial distributed runner, the management class
|
||||
responsible for coordinating all of trial's behavior at the highest level.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
from os.path import isabs
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
TextIO,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from unittest import TestCase, TestSuite
|
||||
|
||||
from attrs import define, field, frozen
|
||||
from attrs.converters import default_if_none
|
||||
|
||||
from twisted.internet.defer import Deferred, DeferredList, gatherResults
|
||||
from twisted.internet.interfaces import IReactorCore, IReactorProcess
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.lockfile import FilesystemLock
|
||||
from twisted.python.modules import theSystemPath
|
||||
from .._asyncrunner import _iterateTests
|
||||
from ..itrial import IReporter, ITestCase
|
||||
from ..reporter import UncleanWarningsReporterWrapper
|
||||
from ..runner import TestHolder
|
||||
from ..util import _unusedTestDirectory, openTestLog
|
||||
from . import _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT
|
||||
from .distreporter import DistReporter
|
||||
from .functional import countingCalls, discardResult, iterateWhile, takeWhile
|
||||
from .worker import LocalWorker, LocalWorkerAMP, WorkerAction
|
||||
|
||||
|
||||
class IDistTrialReactor(IReactorCore, IReactorProcess):
|
||||
"""
|
||||
The reactor interfaces required by disttrial.
|
||||
"""
|
||||
|
||||
|
||||
def _defaultReactor() -> IDistTrialReactor:
|
||||
"""
|
||||
Get the default reactor, ensuring it is suitable for use with disttrial.
|
||||
"""
|
||||
import twisted.internet.reactor as defaultReactor
|
||||
|
||||
if all(
|
||||
[
|
||||
IReactorCore.providedBy(defaultReactor),
|
||||
IReactorProcess.providedBy(defaultReactor),
|
||||
]
|
||||
):
|
||||
# If it provides each of the interfaces then it provides the
|
||||
# intersection interface. cast it to make it easier to talk about
|
||||
# later on.
|
||||
return cast(IDistTrialReactor, defaultReactor)
|
||||
|
||||
raise TypeError("Reactor does not provide the right interfaces")
|
||||
|
||||
|
||||
@frozen
|
||||
class WorkerPoolConfig:
|
||||
"""
|
||||
Configuration parameters for a pool of test-running workers.
|
||||
|
||||
@ivar numWorkers: The number of workers in the pool.
|
||||
|
||||
@ivar workingDirectory: A directory in which working directories for each
|
||||
of the workers will be created.
|
||||
|
||||
@ivar workerArguments: Extra arguments to pass the worker process in its
|
||||
argv.
|
||||
|
||||
@ivar logFile: The basename of the overall test log file.
|
||||
"""
|
||||
|
||||
numWorkers: int
|
||||
workingDirectory: FilePath[Any]
|
||||
workerArguments: Sequence[str]
|
||||
logFile: str
|
||||
|
||||
|
||||
@define
|
||||
class StartedWorkerPool:
|
||||
"""
|
||||
A pool of workers which have already been started.
|
||||
|
||||
@ivar workingDirectory: A directory holding the working directories for
|
||||
each of the workers.
|
||||
|
||||
@ivar testDirLock: An object representing the cooperative lock this pool
|
||||
holds on its working directory.
|
||||
|
||||
@ivar testLog: The open overall test log file.
|
||||
|
||||
@ivar workers: Objects corresponding to the worker child processes and
|
||||
adapting between process-related interfaces and C{IProtocol}.
|
||||
|
||||
@ivar ampWorkers: AMP protocol instances corresponding to the worker child
|
||||
processes.
|
||||
"""
|
||||
|
||||
workingDirectory: FilePath[Any]
|
||||
testDirLock: FilesystemLock
|
||||
testLog: TextIO
|
||||
workers: List[LocalWorker]
|
||||
ampWorkers: List[LocalWorkerAMP]
|
||||
|
||||
_logger = Logger()
|
||||
|
||||
async def run(self, workerAction: WorkerAction[Any]) -> None:
|
||||
"""
|
||||
Run an action on all of the workers in the pool.
|
||||
"""
|
||||
await gatherResults(
|
||||
discardResult(workerAction(worker)) for worker in self.ampWorkers
|
||||
)
|
||||
return None
|
||||
|
||||
async def join(self) -> None:
|
||||
"""
|
||||
Shut down all of the workers in the pool.
|
||||
|
||||
The pool is unusable after this method is called.
|
||||
"""
|
||||
results = await DeferredList(
|
||||
[Deferred.fromCoroutine(worker.exit()) for worker in self.workers],
|
||||
consumeErrors=True,
|
||||
)
|
||||
for n, (succeeded, failure) in enumerate(results):
|
||||
if not succeeded:
|
||||
self._logger.failure(f"joining disttrial worker #{n} failed", failure)
|
||||
|
||||
del self.workers[:]
|
||||
del self.ampWorkers[:]
|
||||
self.testLog.close()
|
||||
self.testDirLock.unlock()
|
||||
|
||||
|
||||
@frozen
|
||||
class WorkerPool:
|
||||
"""
|
||||
Manage a fixed-size collection of child processes which can run tests.
|
||||
|
||||
@ivar _config: Configuration for the precise way in which the pool is run.
|
||||
"""
|
||||
|
||||
_config: WorkerPoolConfig
|
||||
|
||||
def _createLocalWorkers(
|
||||
self,
|
||||
protocols: Iterable[LocalWorkerAMP],
|
||||
workingDirectory: FilePath[Any],
|
||||
logFile: TextIO,
|
||||
) -> List[LocalWorker]:
|
||||
"""
|
||||
Create local worker protocol instances and return them.
|
||||
|
||||
@param protocols: The process/protocol adapters to use for the created
|
||||
workers.
|
||||
|
||||
@param workingDirectory: The base path in which we should run the
|
||||
workers.
|
||||
|
||||
@param logFile: The test log, for workers to write to.
|
||||
|
||||
@return: A list of C{quantity} C{LocalWorker} instances.
|
||||
"""
|
||||
return [
|
||||
LocalWorker(protocol, workingDirectory.child(str(x)), logFile)
|
||||
for x, protocol in enumerate(protocols)
|
||||
]
|
||||
|
||||
def _launchWorkerProcesses(self, spawner, protocols, arguments):
|
||||
"""
|
||||
Spawn processes from a list of process protocols.
|
||||
|
||||
@param spawner: A C{IReactorProcess.spawnProcess} implementation.
|
||||
|
||||
@param protocols: An iterable of C{ProcessProtocol} instances.
|
||||
|
||||
@param arguments: Extra arguments passed to the processes.
|
||||
"""
|
||||
workertrialPath = theSystemPath["twisted.trial._dist.workertrial"].filePath.path
|
||||
childFDs = {
|
||||
0: "w",
|
||||
1: "r",
|
||||
2: "r",
|
||||
_WORKER_AMP_STDIN: "w",
|
||||
_WORKER_AMP_STDOUT: "r",
|
||||
}
|
||||
environ = os.environ.copy()
|
||||
# Add an environment variable containing the raw sys.path, to be used
|
||||
# by subprocesses to try to make it identical to the parent's.
|
||||
environ["PYTHONPATH"] = os.pathsep.join(sys.path)
|
||||
for worker in protocols:
|
||||
args = [sys.executable, workertrialPath]
|
||||
args.extend(arguments)
|
||||
spawner(worker, sys.executable, args=args, childFDs=childFDs, env=environ)
|
||||
|
||||
async def start(self, reactor: IReactorProcess) -> StartedWorkerPool:
|
||||
"""
|
||||
Launch all of the workers for this pool.
|
||||
|
||||
@return: A started pool object that can run jobs using the workers.
|
||||
"""
|
||||
testDir, testDirLock = _unusedTestDirectory(
|
||||
self._config.workingDirectory,
|
||||
)
|
||||
|
||||
if isabs(self._config.logFile):
|
||||
# Open a log file wherever the user asked.
|
||||
testLogPath = FilePath(self._config.logFile)
|
||||
else:
|
||||
# Open a log file in the chosen working directory (not necessarily
|
||||
# the same as our configured working directory, if that path was
|
||||
# in use).
|
||||
testLogPath = testDir.preauthChild(self._config.logFile)
|
||||
testLog = openTestLog(testLogPath)
|
||||
|
||||
ampWorkers = [LocalWorkerAMP() for x in range(self._config.numWorkers)]
|
||||
workers = self._createLocalWorkers(
|
||||
ampWorkers,
|
||||
testDir,
|
||||
testLog,
|
||||
)
|
||||
self._launchWorkerProcesses(
|
||||
reactor.spawnProcess,
|
||||
workers,
|
||||
self._config.workerArguments,
|
||||
)
|
||||
|
||||
return StartedWorkerPool(
|
||||
testDir,
|
||||
testDirLock,
|
||||
testLog,
|
||||
workers,
|
||||
ampWorkers,
|
||||
)
|
||||
|
||||
|
||||
def shouldContinue(untilFailure: bool, result: IReporter) -> bool:
|
||||
"""
|
||||
Determine whether the test suite should be iterated again.
|
||||
|
||||
@param untilFailure: C{True} if the suite is supposed to run until
|
||||
failure.
|
||||
|
||||
@param result: The test result of the test suite iteration which just
|
||||
completed.
|
||||
"""
|
||||
return untilFailure and result.wasSuccessful()
|
||||
|
||||
|
||||
async def runTests(
|
||||
pool: StartedWorkerPool,
|
||||
testCases: Iterable[ITestCase],
|
||||
result: DistReporter,
|
||||
driveWorker: Callable[
|
||||
[DistReporter, Sequence[ITestCase], LocalWorkerAMP], Awaitable[None]
|
||||
],
|
||||
) -> None:
|
||||
try:
|
||||
# Run the tests using the worker pool.
|
||||
await pool.run(partial(driveWorker, result, testCases))
|
||||
except Exception:
|
||||
# Exceptions from test code are handled somewhere else. An
|
||||
# exception here is a bug in the runner itself. The only
|
||||
# convenient place to put it is in the result, though.
|
||||
result.original.addError(TestHolder("<runTests>"), Failure())
|
||||
|
||||
|
||||
@define
|
||||
class DistTrialRunner:
|
||||
"""
|
||||
A specialized runner for distributed trial. The runner launches a number of
|
||||
local worker processes which will run tests.
|
||||
|
||||
@ivar _maxWorkers: the number of workers to be spawned.
|
||||
|
||||
@ivar _exitFirst: ``True`` to stop the run as soon as a test case fails.
|
||||
``False`` to run through the whole suite and report all of the results
|
||||
at the end.
|
||||
|
||||
@ivar stream: stream which the reporter will use.
|
||||
|
||||
@ivar _reporterFactory: the reporter class to be used.
|
||||
"""
|
||||
|
||||
_distReporterFactory = DistReporter
|
||||
_logger = Logger()
|
||||
|
||||
# accepts a `realtime` keyword argument which we can't annotate, so punt
|
||||
# on the argument annotation
|
||||
_reporterFactory: Callable[..., IReporter]
|
||||
_maxWorkers: int
|
||||
_workerArguments: List[str]
|
||||
_exitFirst: bool = False
|
||||
_reactor: IDistTrialReactor = field(
|
||||
# mypy doesn't understand the converter
|
||||
default=None,
|
||||
converter=default_if_none(factory=_defaultReactor), # type: ignore [misc]
|
||||
)
|
||||
# mypy doesn't understand the converter
|
||||
stream: TextIO = field(default=None, converter=default_if_none(sys.stdout)) # type: ignore [misc]
|
||||
|
||||
_tracebackFormat: str = "default"
|
||||
_realTimeErrors: bool = False
|
||||
_uncleanWarnings: bool = False
|
||||
_logfile: str = "test.log"
|
||||
_workingDirectory: str = "_trial_temp"
|
||||
_workerPoolFactory: Callable[[WorkerPoolConfig], WorkerPool] = WorkerPool
|
||||
|
||||
def _makeResult(self) -> DistReporter:
|
||||
"""
|
||||
Make reporter factory, and wrap it with a L{DistReporter}.
|
||||
"""
|
||||
reporter = self._reporterFactory(
|
||||
self.stream, self._tracebackFormat, realtime=self._realTimeErrors
|
||||
)
|
||||
if self._uncleanWarnings:
|
||||
reporter = UncleanWarningsReporterWrapper(reporter)
|
||||
return self._distReporterFactory(reporter)
|
||||
|
||||
def writeResults(self, result):
|
||||
"""
|
||||
Write test run final outcome to result.
|
||||
|
||||
@param result: A C{TestResult} which will print errors and the summary.
|
||||
"""
|
||||
result.done()
|
||||
|
||||
async def _driveWorker(
|
||||
self,
|
||||
result: DistReporter,
|
||||
testCases: Sequence[ITestCase],
|
||||
worker: LocalWorkerAMP,
|
||||
) -> None:
|
||||
"""
|
||||
Drive a L{LocalWorkerAMP} instance, iterating the tests and calling
|
||||
C{run} for every one of them.
|
||||
|
||||
@param worker: The L{LocalWorkerAMP} to drive.
|
||||
|
||||
@param result: The global L{DistReporter} instance.
|
||||
|
||||
@param testCases: The global list of tests to iterate.
|
||||
|
||||
@return: A coroutine that completes after all of the tests have
|
||||
completed.
|
||||
"""
|
||||
|
||||
async def task(case):
|
||||
try:
|
||||
await worker.run(case, result)
|
||||
except Exception:
|
||||
result.original.addError(case, Failure())
|
||||
|
||||
for case in testCases:
|
||||
await task(case)
|
||||
|
||||
async def runAsync(
|
||||
self,
|
||||
suite: Union[TestCase, TestSuite],
|
||||
untilFailure: bool = False,
|
||||
) -> DistReporter:
|
||||
"""
|
||||
Spawn local worker processes and load tests. After that, run them.
|
||||
|
||||
@param suite: A test or suite to be run.
|
||||
|
||||
@param untilFailure: If C{True}, continue to run the tests until they
|
||||
fail.
|
||||
|
||||
@return: A coroutine that completes with the test result.
|
||||
"""
|
||||
|
||||
# Realize a concrete set of tests to run.
|
||||
testCases = list(_iterateTests(suite))
|
||||
|
||||
# Create a worker pool to use to execute them.
|
||||
poolStarter = self._workerPoolFactory(
|
||||
WorkerPoolConfig(
|
||||
# Don't make it larger than is useful or allowed.
|
||||
min(len(testCases), self._maxWorkers),
|
||||
FilePath(self._workingDirectory),
|
||||
self._workerArguments,
|
||||
self._logfile,
|
||||
),
|
||||
)
|
||||
|
||||
# Announce that we're beginning. countTestCases result is preferred
|
||||
# (over len(testCases)) because testCases may contain synthetic cases
|
||||
# for error reporting purposes.
|
||||
self.stream.write(f"Running {suite.countTestCases()} tests.\n")
|
||||
|
||||
# Start the worker pool.
|
||||
startedPool = await poolStarter.start(self._reactor)
|
||||
|
||||
# The condition that will determine whether the test run repeats.
|
||||
condition = partial(shouldContinue, untilFailure)
|
||||
|
||||
# A function that will run the whole suite once.
|
||||
@countingCalls
|
||||
async def runAndReport(n: int) -> DistReporter:
|
||||
if untilFailure:
|
||||
# If and only if we're running the suite more than once,
|
||||
# provide a report about which run this is.
|
||||
self.stream.write(f"Test Pass {n + 1}\n")
|
||||
|
||||
result = self._makeResult()
|
||||
|
||||
if self._exitFirst:
|
||||
# Keep giving out tests as long as the result object has only
|
||||
# seen success.
|
||||
casesCondition = lambda _: result.original.wasSuccessful()
|
||||
else:
|
||||
casesCondition = lambda _: True
|
||||
|
||||
await runTests(
|
||||
startedPool,
|
||||
takeWhile(casesCondition, testCases),
|
||||
result,
|
||||
self._driveWorker,
|
||||
)
|
||||
self.writeResults(result)
|
||||
return result
|
||||
|
||||
try:
|
||||
# Start submitting tests to workers in the pool. Perhaps repeat
|
||||
# the whole test suite more than once, if appropriate for our
|
||||
# configuration.
|
||||
return await iterateWhile(condition, runAndReport)
|
||||
finally:
|
||||
# Shut down the worker pool.
|
||||
await startedPool.join()
|
||||
|
||||
def _run(self, test: Union[TestCase, TestSuite], untilFailure: bool) -> IReporter:
|
||||
result: Union[Failure, DistReporter, None] = None
|
||||
reactorStopping: bool = False
|
||||
testsInProgress: Deferred[object]
|
||||
|
||||
def capture(r: Union[Failure, DistReporter]) -> None:
|
||||
nonlocal result
|
||||
result = r
|
||||
|
||||
def maybeStopTests() -> Optional[Deferred[object]]:
|
||||
nonlocal reactorStopping
|
||||
reactorStopping = True
|
||||
if result is None:
|
||||
testsInProgress.cancel()
|
||||
return testsInProgress
|
||||
return None
|
||||
|
||||
def maybeStopReactor(result: object) -> object:
|
||||
if not reactorStopping:
|
||||
self._reactor.stop()
|
||||
return result
|
||||
|
||||
self._reactor.addSystemEventTrigger("before", "shutdown", maybeStopTests)
|
||||
|
||||
testsInProgress = (
|
||||
Deferred.fromCoroutine(self.runAsync(test, untilFailure))
|
||||
.addBoth(capture)
|
||||
.addBoth(maybeStopReactor)
|
||||
)
|
||||
|
||||
self._reactor.run()
|
||||
|
||||
if isinstance(result, Failure):
|
||||
result.raiseException()
|
||||
|
||||
# mypy can't see that raiseException raises an exception so we can
|
||||
# only get here if result is not a Failure, so tell mypy result is
|
||||
# certainly a DistReporter at this point.
|
||||
assert isinstance(result, DistReporter), f"{result} is not DistReporter"
|
||||
|
||||
# Unwrap the DistReporter to give the caller some regular IReporter
|
||||
# object. DistReporter isn't type annotated correctly so fix it here.
|
||||
return cast(IReporter, result.original)
|
||||
|
||||
def run(self, test: Union[TestCase, TestSuite]) -> IReporter:
|
||||
"""
|
||||
Run a reactor and a test suite.
|
||||
|
||||
@param test: The test or suite to run.
|
||||
"""
|
||||
return self._run(test, untilFailure=False)
|
||||
|
||||
def runUntilFailure(self, test: Union[TestCase, TestSuite]) -> IReporter:
|
||||
"""
|
||||
Run the tests with local worker processes until they fail.
|
||||
|
||||
@param test: The test or suite to run.
|
||||
"""
|
||||
return self._run(test, untilFailure=True)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
General functional-style helpers for disttrial.
|
||||
"""
|
||||
|
||||
from functools import partial, wraps
|
||||
from typing import Awaitable, Callable, Iterable, Optional, TypeVar
|
||||
|
||||
from twisted.internet.defer import Deferred, succeed
|
||||
|
||||
_A = TypeVar("_A")
|
||||
_B = TypeVar("_B")
|
||||
_C = TypeVar("_C")
|
||||
|
||||
|
||||
def fromOptional(default: _A, optional: Optional[_A]) -> _A:
|
||||
"""
|
||||
Get a definite value from an optional value.
|
||||
|
||||
@param default: The value to return if the optional value is missing.
|
||||
|
||||
@param optional: The optional value to return if it exists.
|
||||
"""
|
||||
if optional is None:
|
||||
return default
|
||||
return optional
|
||||
|
||||
|
||||
def takeWhile(condition: Callable[[_A], bool], xs: Iterable[_A]) -> Iterable[_A]:
|
||||
"""
|
||||
:return: An iterable over C{xs} that stops when C{condition} returns
|
||||
``False`` based on the value of iterated C{xs}.
|
||||
"""
|
||||
for x in xs:
|
||||
if condition(x):
|
||||
yield x
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
async def sequence(a: Awaitable[_A], b: Awaitable[_B]) -> _B:
|
||||
"""
|
||||
Wait for one action to complete and then another.
|
||||
|
||||
If either action fails, failure is propagated. If the first action fails,
|
||||
the second action is not waited on.
|
||||
"""
|
||||
await a
|
||||
return await b
|
||||
|
||||
|
||||
def flip(f: Callable[[_A, _B], _C]) -> Callable[[_B, _A], _C]:
|
||||
"""
|
||||
Create a function like another but with the order of the first two
|
||||
arguments flipped.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def g(b, a):
|
||||
return f(a, b)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
def compose(fx: Callable[[_B], _C], fy: Callable[[_A], _B]) -> Callable[[_A], _C]:
|
||||
"""
|
||||
Create a function that calls one function with an argument and then
|
||||
another function with the result of the first function.
|
||||
"""
|
||||
|
||||
@wraps(fx)
|
||||
@wraps(fy)
|
||||
def g(a):
|
||||
return fx(fy(a))
|
||||
|
||||
return g
|
||||
|
||||
|
||||
# Discard the result of an awaitable and substitute None in its place.
|
||||
discardResult: Callable[[Awaitable[_A]], Deferred[None]] = compose(
|
||||
Deferred.fromCoroutine,
|
||||
partial(flip(sequence), succeed(None)),
|
||||
)
|
||||
|
||||
|
||||
async def iterateWhile(
|
||||
predicate: Callable[[_A], bool],
|
||||
action: Callable[[], Awaitable[_A]],
|
||||
) -> _A:
|
||||
"""
|
||||
Call a function repeatedly until its result fails to satisfy a predicate.
|
||||
|
||||
@param predicate: The check to apply.
|
||||
|
||||
@param action: The function to call.
|
||||
|
||||
@return: The result of C{action} which did not satisfy C{predicate}.
|
||||
"""
|
||||
while True:
|
||||
result = await action()
|
||||
if not predicate(result):
|
||||
return result
|
||||
|
||||
|
||||
def countingCalls(f: Callable[[int], _A]) -> Callable[[], _A]:
|
||||
"""
|
||||
Wrap a function with another that automatically passes an integer counter
|
||||
of the number of calls that have gone through the wrapper.
|
||||
"""
|
||||
counter = 0
|
||||
|
||||
def g() -> _A:
|
||||
nonlocal counter
|
||||
try:
|
||||
result = f(counter)
|
||||
finally:
|
||||
counter += 1
|
||||
return result
|
||||
|
||||
return g
|
||||
@@ -0,0 +1,89 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Commands for reporting test success of failure to the manager.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
from twisted.protocols.amp import Boolean, Command, Integer, Unicode
|
||||
|
||||
NativeString = Unicode
|
||||
|
||||
|
||||
class AddSuccess(Command):
|
||||
"""
|
||||
Add a success.
|
||||
"""
|
||||
|
||||
arguments = [(b"testName", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class AddError(Command):
|
||||
"""
|
||||
Add an error.
|
||||
"""
|
||||
|
||||
arguments = [
|
||||
(b"testName", NativeString()),
|
||||
(b"errorClass", NativeString()),
|
||||
(b"errorStreamId", Integer()),
|
||||
(b"framesStreamId", Integer()),
|
||||
]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class AddFailure(Command):
|
||||
"""
|
||||
Add a failure.
|
||||
"""
|
||||
|
||||
arguments = [
|
||||
(b"testName", NativeString()),
|
||||
(b"failStreamId", Integer()),
|
||||
(b"failClass", NativeString()),
|
||||
(b"framesStreamId", Integer()),
|
||||
]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class AddSkip(Command):
|
||||
"""
|
||||
Add a skip.
|
||||
"""
|
||||
|
||||
arguments = [(b"testName", NativeString()), (b"reason", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class AddExpectedFailure(Command):
|
||||
"""
|
||||
Add an expected failure.
|
||||
"""
|
||||
|
||||
arguments = [
|
||||
(b"testName", NativeString()),
|
||||
(b"errorStreamId", Integer()),
|
||||
(b"todo", NativeString()),
|
||||
]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class AddUnexpectedSuccess(Command):
|
||||
"""
|
||||
Add an unexpected success.
|
||||
"""
|
||||
|
||||
arguments = [(b"testName", NativeString()), (b"todo", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class TestWrite(Command):
|
||||
"""
|
||||
Write test log.
|
||||
"""
|
||||
|
||||
arguments = [(b"out", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_options -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Options handling specific to trial's workers.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
from twisted.application.app import ReactorSelectionMixin
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.usage import Options
|
||||
from twisted.scripts.trial import _BasicOptions
|
||||
|
||||
|
||||
class WorkerOptions(_BasicOptions, Options, ReactorSelectionMixin):
|
||||
"""
|
||||
Options forwarded to the trial distributed worker.
|
||||
"""
|
||||
|
||||
def coverdir(self):
|
||||
"""
|
||||
Return a L{FilePath} representing the directory into which coverage
|
||||
results should be written.
|
||||
"""
|
||||
return FilePath("coverage")
|
||||
100
.venv/lib/python3.12/site-packages/twisted/trial/_dist/stream.py
Normal file
100
.venv/lib/python3.12/site-packages/twisted/trial/_dist/stream.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Buffer byte streams.
|
||||
"""
|
||||
|
||||
from itertools import count
|
||||
from typing import Dict, Iterator, List, TypeVar
|
||||
|
||||
from attrs import Factory, define
|
||||
|
||||
from twisted.protocols.amp import AMP, Command, Integer, String as Bytes
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class StreamOpen(Command):
|
||||
"""
|
||||
Open a new stream.
|
||||
"""
|
||||
|
||||
response = [(b"streamId", Integer())]
|
||||
|
||||
|
||||
class StreamWrite(Command):
|
||||
"""
|
||||
Write a chunk of data to a stream.
|
||||
"""
|
||||
|
||||
arguments = [
|
||||
(b"streamId", Integer()),
|
||||
(b"data", Bytes()),
|
||||
]
|
||||
|
||||
|
||||
@define
|
||||
class StreamReceiver:
|
||||
"""
|
||||
Buffering de-multiplexing byte stream receiver.
|
||||
"""
|
||||
|
||||
_counter: Iterator[int] = count()
|
||||
_streams: Dict[int, List[bytes]] = Factory(dict)
|
||||
|
||||
def open(self) -> int:
|
||||
"""
|
||||
Open a new stream and return its unique identifier.
|
||||
"""
|
||||
newId = next(self._counter)
|
||||
self._streams[newId] = []
|
||||
return newId
|
||||
|
||||
def write(self, streamId: int, chunk: bytes) -> None:
|
||||
"""
|
||||
Write to an open stream using its unique identifier.
|
||||
|
||||
@raise KeyError: If there is no such open stream.
|
||||
"""
|
||||
self._streams[streamId].append(chunk)
|
||||
|
||||
def finish(self, streamId: int) -> List[bytes]:
|
||||
"""
|
||||
Indicate an open stream may receive no further data and return all of
|
||||
its current contents.
|
||||
|
||||
@raise KeyError: If there is no such open stream.
|
||||
"""
|
||||
return self._streams.pop(streamId)
|
||||
|
||||
|
||||
def chunk(data: bytes, chunkSize: int) -> Iterator[bytes]:
|
||||
"""
|
||||
Break a byte string into pieces of no more than ``chunkSize`` length.
|
||||
|
||||
@param data: The byte string.
|
||||
|
||||
@param chunkSize: The maximum length of the resulting pieces. All pieces
|
||||
except possibly the last will be this length.
|
||||
|
||||
@return: The pieces.
|
||||
"""
|
||||
pos = 0
|
||||
while pos < len(data):
|
||||
yield data[pos : pos + chunkSize]
|
||||
pos += chunkSize
|
||||
|
||||
|
||||
async def stream(amp: AMP, chunks: Iterator[bytes]) -> int:
|
||||
"""
|
||||
Send the given stream chunks, one by one, over the given connection.
|
||||
|
||||
The chunks are sent using L{StreamWrite} over a stream opened using
|
||||
L{StreamOpen}.
|
||||
|
||||
@return: The identifier of the stream over which the chunks were sent.
|
||||
"""
|
||||
streamId = (await amp.callRemote(StreamOpen))["streamId"]
|
||||
assert isinstance(streamId, int)
|
||||
|
||||
for oneChunk in chunks:
|
||||
await amp.callRemote(StreamWrite, streamId=streamId, data=oneChunk)
|
||||
return streamId
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Distributed trial test runner tests.
|
||||
"""
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Hamcrest matchers useful throughout the test suite.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"matches_result",
|
||||
"HasSum",
|
||||
"IsSequenceOf",
|
||||
]
|
||||
|
||||
from typing import Any, List, Sequence, Tuple, TypeVar
|
||||
|
||||
from hamcrest import (
|
||||
contains_exactly,
|
||||
contains_string,
|
||||
equal_to,
|
||||
has_length,
|
||||
has_properties,
|
||||
instance_of,
|
||||
)
|
||||
from hamcrest.core.base_matcher import BaseMatcher
|
||||
from hamcrest.core.core.allof import AllOf
|
||||
from hamcrest.core.description import Description
|
||||
from hamcrest.core.matcher import Matcher
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Semigroup(Protocol[T]):
|
||||
"""
|
||||
A type with an associative binary operator.
|
||||
|
||||
Common examples of a semigroup are integers with addition and strings with
|
||||
concatenation.
|
||||
"""
|
||||
|
||||
def __add__(self, other: T) -> T:
|
||||
"""
|
||||
This must be associative: a + (b + c) == (a + b) + c
|
||||
"""
|
||||
|
||||
|
||||
S = TypeVar("S", bound=Semigroup[Any])
|
||||
|
||||
|
||||
def matches_result(
|
||||
successes: Matcher[Any] = equal_to(0),
|
||||
errors: Matcher[Any] = has_length(0),
|
||||
failures: Matcher[Any] = has_length(0),
|
||||
skips: Matcher[Any] = has_length(0),
|
||||
expectedFailures: Matcher[Any] = has_length(0),
|
||||
unexpectedSuccesses: Matcher[Any] = has_length(0),
|
||||
) -> Matcher[Any]:
|
||||
"""
|
||||
Match a L{TestCase} instances with matching attributes.
|
||||
"""
|
||||
return has_properties(
|
||||
{
|
||||
"successes": successes,
|
||||
"errors": errors,
|
||||
"failures": failures,
|
||||
"skips": skips,
|
||||
"expectedFailures": expectedFailures,
|
||||
"unexpectedSuccesses": unexpectedSuccesses,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class HasSum(BaseMatcher[Sequence[S]]):
|
||||
"""
|
||||
Match a sequence the elements of which sum to a value matched by
|
||||
another matcher.
|
||||
|
||||
:ivar sumMatcher: The matcher which must match the sum.
|
||||
:ivar zero: The zero value for the matched type.
|
||||
"""
|
||||
|
||||
def __init__(self, sumMatcher: Matcher[S], zero: S) -> None:
|
||||
self.sumMatcher = sumMatcher
|
||||
self.zero = zero
|
||||
|
||||
def _sum(self, sequence: Sequence[S]) -> S:
|
||||
if not sequence:
|
||||
return self.zero
|
||||
result = self.zero
|
||||
for elem in sequence:
|
||||
result = result + elem
|
||||
return result
|
||||
|
||||
def _matches(self, item: Sequence[S]) -> bool:
|
||||
"""
|
||||
Determine whether the sum of the sequence is matched.
|
||||
"""
|
||||
s = self._sum(item)
|
||||
return self.sumMatcher.matches(s)
|
||||
|
||||
def describe_mismatch(self, item: Sequence[S], description: Description) -> None:
|
||||
"""
|
||||
Describe the mismatch.
|
||||
"""
|
||||
s = self._sum(item)
|
||||
description.append_description_of(self)
|
||||
self.sumMatcher.describe_mismatch(s, description)
|
||||
return None
|
||||
|
||||
def describe_to(self, description: Description) -> None:
|
||||
"""
|
||||
Describe this matcher for error messages.
|
||||
"""
|
||||
description.append_text("a sequence with sum ")
|
||||
description.append_description_of(self.sumMatcher)
|
||||
description.append_text(", ")
|
||||
|
||||
|
||||
class IsSequenceOf(BaseMatcher[Sequence[T]]):
|
||||
"""
|
||||
Match a sequence where every element is matched by another matcher.
|
||||
|
||||
:ivar elementMatcher: The matcher which must match every element of the
|
||||
sequence.
|
||||
"""
|
||||
|
||||
def __init__(self, elementMatcher: Matcher[T]) -> None:
|
||||
self.elementMatcher = elementMatcher
|
||||
|
||||
def _matches(self, item: Sequence[T]) -> bool:
|
||||
"""
|
||||
Determine whether every element of the sequence is matched.
|
||||
"""
|
||||
for elem in item:
|
||||
if not self.elementMatcher.matches(elem):
|
||||
return False
|
||||
return True
|
||||
|
||||
def describe_mismatch(self, item: Sequence[T], description: Description) -> None:
|
||||
"""
|
||||
Describe the mismatch.
|
||||
"""
|
||||
for idx, elem in enumerate(item):
|
||||
if not self.elementMatcher.matches(elem):
|
||||
description.append_description_of(self)
|
||||
description.append_text(f"not sequence with element #{idx} {elem!r}")
|
||||
|
||||
def describe_to(self, description: Description) -> None:
|
||||
"""
|
||||
Describe this matcher for error messages.
|
||||
"""
|
||||
description.append_text("a sequence containing only ")
|
||||
description.append_description_of(self.elementMatcher)
|
||||
description.append_text(", ")
|
||||
|
||||
|
||||
def isFailure(**properties: Matcher[object]) -> Matcher[object]:
|
||||
"""
|
||||
Match an instance of L{Failure} with matching attributes.
|
||||
"""
|
||||
return AllOf(
|
||||
instance_of(Failure),
|
||||
has_properties(**properties),
|
||||
)
|
||||
|
||||
|
||||
def similarFrame(
|
||||
functionName: str, fileName: str
|
||||
) -> Matcher[Sequence[Tuple[str, str, int, List[object], List[object]]]]:
|
||||
"""
|
||||
Match a tuple representation of a frame like those used by
|
||||
L{twisted.python.failure.Failure}.
|
||||
"""
|
||||
# The frames depend on exact layout of the source
|
||||
# code in files and on the filesystem so we won't
|
||||
# bother being very precise here. Just verify we
|
||||
# see some distinctive fragments.
|
||||
#
|
||||
# In particular, the last frame should be a tuple like
|
||||
#
|
||||
# (functionName, fileName, someint, [], [])
|
||||
return contains_exactly(
|
||||
equal_to(functionName),
|
||||
contains_string(fileName), # type: ignore[arg-type]
|
||||
instance_of(int), # type: ignore[arg-type]
|
||||
# Unfortunately Failure makes them sometimes tuples, sometimes
|
||||
# dict_items.
|
||||
has_length(0), # type: ignore[arg-type]
|
||||
has_length(0), # type: ignore[arg-type]
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.distreporter}.
|
||||
"""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial._dist.distreporter import DistReporter
|
||||
from twisted.trial.reporter import TreeReporter
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class DistReporterTests(TestCase):
|
||||
"""
|
||||
Tests for L{DistReporter}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.stream = StringIO()
|
||||
self.distReporter = DistReporter(TreeReporter(self.stream))
|
||||
self.test = TestCase()
|
||||
|
||||
def test_startSuccessStop(self) -> None:
|
||||
"""
|
||||
Success output only gets sent to the stream after the test has stopped.
|
||||
"""
|
||||
self.distReporter.startTest(self.test)
|
||||
self.assertEqual(self.stream.getvalue(), "")
|
||||
self.distReporter.addSuccess(self.test)
|
||||
self.assertEqual(self.stream.getvalue(), "")
|
||||
self.distReporter.stopTest(self.test)
|
||||
self.assertNotEqual(self.stream.getvalue(), "")
|
||||
|
||||
def test_startErrorStop(self) -> None:
|
||||
"""
|
||||
Error output only gets sent to the stream after the test has stopped.
|
||||
"""
|
||||
self.distReporter.startTest(self.test)
|
||||
self.assertEqual(self.stream.getvalue(), "")
|
||||
self.distReporter.addError(self.test, Failure(Exception("error")))
|
||||
self.assertEqual(self.stream.getvalue(), "")
|
||||
self.distReporter.stopTest(self.test)
|
||||
self.assertNotEqual(self.stream.getvalue(), "")
|
||||
|
||||
def test_forwardedMethods(self) -> None:
|
||||
"""
|
||||
Calling methods of L{DistReporter} add calls to the running queue of
|
||||
the test.
|
||||
"""
|
||||
self.distReporter.startTest(self.test)
|
||||
self.distReporter.addFailure(self.test, Failure(Exception("foo")))
|
||||
self.distReporter.addError(self.test, Failure(Exception("bar")))
|
||||
self.distReporter.addSkip(self.test, "egg")
|
||||
self.distReporter.addUnexpectedSuccess(self.test, "spam")
|
||||
self.distReporter.addExpectedFailure(
|
||||
self.test, Failure(Exception("err")), "foo"
|
||||
)
|
||||
self.assertEqual(len(self.distReporter.running[self.test.id()]), 6)
|
||||
@@ -0,0 +1,861 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.disttrial}.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
from io import StringIO
|
||||
from os.path import sep
|
||||
from typing import Callable, List, Set
|
||||
from unittest import TestCase as PyUnitTestCase
|
||||
|
||||
from zope.interface import implementer, verify
|
||||
|
||||
from attrs import Factory, assoc, define, field
|
||||
from hamcrest import (
|
||||
assert_that,
|
||||
contains,
|
||||
ends_with,
|
||||
equal_to,
|
||||
has_length,
|
||||
none,
|
||||
starts_with,
|
||||
)
|
||||
from hamcrest.core.core.allof import AllOf
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import booleans, sampled_from
|
||||
|
||||
from twisted.internet import interfaces
|
||||
from twisted.internet.base import ReactorBase
|
||||
from twisted.internet.defer import CancelledError, Deferred, succeed
|
||||
from twisted.internet.error import ProcessDone
|
||||
from twisted.internet.protocol import ProcessProtocol, Protocol
|
||||
from twisted.internet.test.modulehelpers import AlternateReactor
|
||||
from twisted.internet.testing import MemoryReactorClock
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.lockfile import FilesystemLock
|
||||
from twisted.trial._dist import _WORKER_AMP_STDIN
|
||||
from twisted.trial._dist.distreporter import DistReporter
|
||||
from twisted.trial._dist.disttrial import DistTrialRunner, WorkerPool, WorkerPoolConfig
|
||||
from twisted.trial._dist.functional import (
|
||||
countingCalls,
|
||||
discardResult,
|
||||
fromOptional,
|
||||
iterateWhile,
|
||||
sequence,
|
||||
)
|
||||
from twisted.trial._dist.worker import LocalWorker, RunResult, Worker, WorkerAction
|
||||
from twisted.trial.reporter import (
|
||||
Reporter,
|
||||
TestResult,
|
||||
TreeReporter,
|
||||
UncleanWarningsReporterWrapper,
|
||||
)
|
||||
from twisted.trial.runner import ErrorHolder, TrialSuite
|
||||
from twisted.trial.unittest import SynchronousTestCase, TestCase
|
||||
from ...test import erroneous, sample
|
||||
from .matchers import matches_result
|
||||
|
||||
|
||||
@define
|
||||
class FakeTransport:
|
||||
"""
|
||||
A simple fake process transport.
|
||||
"""
|
||||
|
||||
_closed: Set[int] = field(default=Factory(set))
|
||||
|
||||
def writeToChild(self, fd, data):
|
||||
"""
|
||||
Ignore write calls.
|
||||
"""
|
||||
|
||||
def closeChildFD(self, fd):
|
||||
"""
|
||||
Mark one of the child descriptors as closed.
|
||||
"""
|
||||
self._closed.add(fd)
|
||||
|
||||
|
||||
@implementer(interfaces.IReactorProcess)
|
||||
class CountingReactor(MemoryReactorClock):
|
||||
"""
|
||||
A fake reactor that counts the calls to L{IReactorCore.run},
|
||||
L{IReactorCore.stop}, and L{IReactorProcess.spawnProcess}.
|
||||
"""
|
||||
|
||||
spawnCount = 0
|
||||
stopCount = 0
|
||||
runCount = 0
|
||||
|
||||
def __init__(self, workers):
|
||||
MemoryReactorClock.__init__(self)
|
||||
self._workers = workers
|
||||
|
||||
def spawnProcess(
|
||||
self,
|
||||
workerProto,
|
||||
executable,
|
||||
args=(),
|
||||
env={},
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
"""
|
||||
See L{IReactorProcess.spawnProcess}.
|
||||
|
||||
@param workerProto: See L{IReactorProcess.spawnProcess}.
|
||||
@param args: See L{IReactorProcess.spawnProcess}.
|
||||
@param kwargs: See L{IReactorProcess.spawnProcess}.
|
||||
"""
|
||||
self._workers.append(workerProto)
|
||||
workerProto.makeConnection(FakeTransport())
|
||||
self.spawnCount += 1
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
See L{IReactorCore.stop}.
|
||||
"""
|
||||
MemoryReactorClock.stop(self)
|
||||
# TODO: implementing this more comprehensively in MemoryReactor would
|
||||
# be nice, this is rather hard-coded to disttrial's current
|
||||
# implementation.
|
||||
if "before" in self.triggers:
|
||||
self.triggers["before"]["shutdown"][0][0]()
|
||||
self.stopCount += 1
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
See L{IReactorCore.run}.
|
||||
"""
|
||||
self.runCount += 1
|
||||
|
||||
# The same as IReactorCore.run, except no stop.
|
||||
self.running = True
|
||||
self.hasRun = True
|
||||
|
||||
for f, args, kwargs in self.whenRunningHooks:
|
||||
f(*args, **kwargs)
|
||||
self.stop()
|
||||
# do not count internal 'stop' against trial-initiated .stop() count
|
||||
self.stopCount -= 1
|
||||
|
||||
|
||||
class CountingReactorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{CountingReactor}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.workers = []
|
||||
self.reactor = CountingReactor(self.workers)
|
||||
|
||||
def test_providesIReactorProcess(self):
|
||||
"""
|
||||
L{CountingReactor} instances provide L{IReactorProcess}.
|
||||
"""
|
||||
verify.verifyObject(interfaces.IReactorProcess, self.reactor)
|
||||
|
||||
def test_spawnProcess(self):
|
||||
"""
|
||||
The process protocol for a spawned process is connected to a
|
||||
transport and appended onto the provided C{workers} list, and
|
||||
the reactor's C{spawnCount} increased.
|
||||
"""
|
||||
self.assertFalse(self.reactor.spawnCount)
|
||||
|
||||
proto = Protocol()
|
||||
for count in [1, 2]:
|
||||
self.reactor.spawnProcess(proto, sys.executable, args=[sys.executable])
|
||||
self.assertTrue(proto.transport)
|
||||
self.assertEqual(self.workers, [proto] * count)
|
||||
self.assertEqual(self.reactor.spawnCount, count)
|
||||
|
||||
def test_stop(self):
|
||||
"""
|
||||
Stopping the reactor increments its C{stopCount}
|
||||
"""
|
||||
self.assertFalse(self.reactor.stopCount)
|
||||
for count in [1, 2]:
|
||||
self.reactor.stop()
|
||||
self.assertEqual(self.reactor.stopCount, count)
|
||||
|
||||
def test_run(self):
|
||||
"""
|
||||
Running the reactor increments its C{runCount}, does not imply
|
||||
C{stop}, and calls L{IReactorCore.callWhenRunning} hooks.
|
||||
"""
|
||||
self.assertFalse(self.reactor.runCount)
|
||||
|
||||
whenRunningCalls = []
|
||||
self.reactor.callWhenRunning(whenRunningCalls.append, None)
|
||||
|
||||
for count in [1, 2]:
|
||||
self.reactor.run()
|
||||
self.assertEqual(self.reactor.runCount, count)
|
||||
self.assertEqual(self.reactor.stopCount, 0)
|
||||
self.assertEqual(len(whenRunningCalls), count)
|
||||
|
||||
|
||||
class WorkerPoolTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerPool}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.parent = FilePath(self.mktemp())
|
||||
self.workingDirectory = self.parent.child("_trial_temp")
|
||||
self.config = WorkerPoolConfig(
|
||||
numWorkers=4,
|
||||
workingDirectory=self.workingDirectory,
|
||||
workerArguments=[],
|
||||
logFile="out.log",
|
||||
)
|
||||
self.pool = WorkerPool(self.config)
|
||||
|
||||
def test_createLocalWorkers(self):
|
||||
"""
|
||||
C{_createLocalWorkers} iterates the list of protocols and create one
|
||||
L{LocalWorker} for each.
|
||||
"""
|
||||
protocols = [object() for x in range(4)]
|
||||
workers = self.pool._createLocalWorkers(protocols, FilePath("path"), StringIO())
|
||||
for s in workers:
|
||||
self.assertIsInstance(s, LocalWorker)
|
||||
self.assertEqual(4, len(workers))
|
||||
|
||||
def test_launchWorkerProcesses(self):
|
||||
"""
|
||||
Given a C{spawnProcess} function, C{_launchWorkerProcess} launches a
|
||||
python process with an existing path as its argument.
|
||||
"""
|
||||
protocols = [ProcessProtocol() for i in range(4)]
|
||||
arguments = []
|
||||
environment = {}
|
||||
|
||||
def fakeSpawnProcess(
|
||||
processProtocol,
|
||||
executable,
|
||||
args=(),
|
||||
env={},
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
arguments.append(executable)
|
||||
arguments.extend(args)
|
||||
environment.update(env)
|
||||
|
||||
self.pool._launchWorkerProcesses(fakeSpawnProcess, protocols, ["foo"])
|
||||
self.assertEqual(arguments[0], arguments[1])
|
||||
self.assertTrue(os.path.exists(arguments[2]))
|
||||
self.assertEqual("foo", arguments[3])
|
||||
# The child process runs with PYTHONPATH set to exactly the parent's
|
||||
# import search path so that the child has a good chance of finding
|
||||
# the same source files the parent would have found.
|
||||
self.assertEqual(os.pathsep.join(sys.path), environment["PYTHONPATH"])
|
||||
|
||||
def test_run(self):
|
||||
"""
|
||||
C{run} dispatches the given action to each of its workers exactly once.
|
||||
"""
|
||||
# Make sure the parent of the working directory exists so
|
||||
# manage a lock in it.
|
||||
self.parent.makedirs()
|
||||
|
||||
workers = []
|
||||
starting = self.pool.start(CountingReactor([]))
|
||||
started = self.successResultOf(starting)
|
||||
running = started.run(lambda w: succeed(workers.append(w)))
|
||||
self.successResultOf(running)
|
||||
assert_that(workers, has_length(self.config.numWorkers))
|
||||
|
||||
def test_runUsedDirectory(self):
|
||||
"""
|
||||
L{WorkerPool.start} checks if the test directory is already locked, and if
|
||||
it is generates a name based on it.
|
||||
"""
|
||||
# Make sure the parent of the working directory exists so we can
|
||||
# manage a lock in it.
|
||||
self.parent.makedirs()
|
||||
|
||||
# Lock the directory the runner will expect to use.
|
||||
lock = FilesystemLock(self.workingDirectory.path + ".lock")
|
||||
self.assertTrue(lock.lock())
|
||||
self.addCleanup(lock.unlock)
|
||||
|
||||
# Start up the pool
|
||||
fakeReactor = CountingReactor([])
|
||||
started = self.successResultOf(self.pool.start(fakeReactor))
|
||||
|
||||
# Verify it took a nearby directory instead.
|
||||
self.assertEqual(
|
||||
started.workingDirectory,
|
||||
self.workingDirectory.sibling("_trial_temp-1"),
|
||||
)
|
||||
|
||||
def test_join(self):
|
||||
"""
|
||||
L{StartedWorkerPool.join} causes all of the workers to exit, closes the
|
||||
log file, and unlocks the test directory.
|
||||
"""
|
||||
self.parent.makedirs()
|
||||
|
||||
reactor = CountingReactor([])
|
||||
started = self.successResultOf(self.pool.start(reactor))
|
||||
joining = Deferred.fromCoroutine(started.join())
|
||||
self.assertNoResult(joining)
|
||||
for w in reactor._workers:
|
||||
assert_that(w.transport._closed, contains(_WORKER_AMP_STDIN))
|
||||
for fd in w.transport._closed:
|
||||
w.childConnectionLost(fd)
|
||||
for f in [w.processExited, w.processEnded]:
|
||||
f(Failure(ProcessDone(0)))
|
||||
assert_that(self.successResultOf(joining), none())
|
||||
assert_that(started.testLog.closed, equal_to(True))
|
||||
assert_that(started.testDirLock.locked, equal_to(False))
|
||||
|
||||
@given(
|
||||
booleans(),
|
||||
sampled_from(
|
||||
[
|
||||
"out.log",
|
||||
f"subdir{sep}out.log",
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_logFile(self, absolute: bool, logFile: str) -> None:
|
||||
"""
|
||||
L{WorkerPool.start} creates a L{StartedWorkerPool} configured with a
|
||||
log file based on the L{WorkerPoolConfig.logFile}.
|
||||
"""
|
||||
if absolute:
|
||||
logFile = self.parent.path + sep + logFile
|
||||
|
||||
config = assoc(self.config, logFile=logFile)
|
||||
|
||||
if absolute:
|
||||
matches = equal_to(logFile)
|
||||
else:
|
||||
matches = AllOf(
|
||||
# This might have a suffix if the configured workingDirectory
|
||||
# was found to be in-use already so we don't add a sep suffix.
|
||||
starts_with(config.workingDirectory.path),
|
||||
# This should be exactly the suffix so we add a sep prefix.
|
||||
ends_with(sep + logFile),
|
||||
)
|
||||
|
||||
pool = WorkerPool(config)
|
||||
started = self.successResultOf(pool.start(CountingReactor([])))
|
||||
assert_that(started.testLog.name, matches)
|
||||
|
||||
|
||||
class DistTrialRunnerTests(TestCase):
|
||||
"""
|
||||
Tests for L{DistTrialRunner}.
|
||||
"""
|
||||
|
||||
suite = TrialSuite([sample.FooTest("test_foo")])
|
||||
|
||||
def getRunner(self, **overrides):
|
||||
"""
|
||||
Create a runner for testing.
|
||||
"""
|
||||
args = dict(
|
||||
reporterFactory=TreeReporter,
|
||||
workingDirectory=self.mktemp(),
|
||||
stream=StringIO(),
|
||||
maxWorkers=4,
|
||||
workerArguments=[],
|
||||
workerPoolFactory=partial(LocalWorkerPool, autostop=True),
|
||||
reactor=CountingReactor([]),
|
||||
)
|
||||
args.update(overrides)
|
||||
return DistTrialRunner(**args)
|
||||
|
||||
def test_writeResults(self):
|
||||
"""
|
||||
L{DistTrialRunner.writeResults} writes to the stream specified in the
|
||||
init.
|
||||
"""
|
||||
stringIO = StringIO()
|
||||
result = DistReporter(Reporter(stringIO))
|
||||
runner = self.getRunner()
|
||||
runner.writeResults(result)
|
||||
self.assertTrue(stringIO.tell() > 0)
|
||||
|
||||
def test_minimalWorker(self):
|
||||
"""
|
||||
L{DistTrialRunner.runAsync} doesn't try to start more workers than the
|
||||
number of tests.
|
||||
"""
|
||||
pool = None
|
||||
|
||||
def recordingFactory(*a, **kw):
|
||||
nonlocal pool
|
||||
pool = LocalWorkerPool(*a, autostop=True, **kw)
|
||||
return pool
|
||||
|
||||
maxWorkers = 7
|
||||
numTests = 3
|
||||
|
||||
runner = self.getRunner(
|
||||
maxWorkers=maxWorkers, workerPoolFactory=recordingFactory
|
||||
)
|
||||
suite = TrialSuite([TestCase() for n in range(numTests)])
|
||||
self.successResultOf(runner.runAsync(suite))
|
||||
assert_that(pool._started[0].workers, has_length(numTests))
|
||||
|
||||
def test_runUncleanWarnings(self) -> None:
|
||||
"""
|
||||
Running with the C{unclean-warnings} option makes L{DistTrialRunner} uses
|
||||
the L{UncleanWarningsReporterWrapper}.
|
||||
"""
|
||||
runner = self.getRunner(uncleanWarnings=True)
|
||||
d = runner.runAsync(self.suite)
|
||||
result = self.successResultOf(d)
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
self.assertIsInstance(result.original, UncleanWarningsReporterWrapper)
|
||||
|
||||
def test_runWithoutTest(self):
|
||||
"""
|
||||
L{DistTrialRunner} can run an empty test suite.
|
||||
"""
|
||||
stream = StringIO()
|
||||
runner = self.getRunner(stream=stream)
|
||||
result = self.successResultOf(runner.runAsync(TrialSuite()))
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
output = stream.getvalue()
|
||||
self.assertIn("Running 0 test", output)
|
||||
self.assertIn("PASSED", output)
|
||||
|
||||
def test_runWithoutTestButWithAnError(self):
|
||||
"""
|
||||
Even if there is no test, the suite can contain an error (most likely,
|
||||
an import error): this should make the run fail, and the error should
|
||||
be printed.
|
||||
"""
|
||||
err = ErrorHolder("an error", Failure(RuntimeError("foo bar")))
|
||||
stream = StringIO()
|
||||
runner = self.getRunner(stream=stream)
|
||||
|
||||
result = self.successResultOf(runner.runAsync(err))
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
output = stream.getvalue()
|
||||
self.assertIn("Running 0 test", output)
|
||||
self.assertIn("foo bar", output)
|
||||
self.assertIn("an error", output)
|
||||
self.assertIn("errors=1", output)
|
||||
self.assertIn("FAILED", output)
|
||||
|
||||
def test_runUnexpectedError(self) -> None:
|
||||
"""
|
||||
If for some reasons we can't connect to the worker process, the error is
|
||||
recorded in the result object.
|
||||
"""
|
||||
runner = self.getRunner(workerPoolFactory=BrokenWorkerPool)
|
||||
result = self.successResultOf(runner.runAsync(self.suite))
|
||||
errors = result.original.errors
|
||||
assert_that(errors, has_length(1))
|
||||
assert_that(errors[0][1].type, equal_to(WorkerPoolBroken))
|
||||
|
||||
def test_runUnexpectedErrorCtrlC(self) -> None:
|
||||
"""
|
||||
If the reactor is stopped by C-c (i.e. `run` returns before the test
|
||||
case's Deferred has been fired) we should cancel the pending test run.
|
||||
"""
|
||||
runner = self.getRunner(workerPoolFactory=LocalWorkerPool)
|
||||
with self.assertRaises(CancelledError):
|
||||
runner.run(self.suite)
|
||||
|
||||
def test_runUnexpectedWorkerError(self) -> None:
|
||||
"""
|
||||
If for some reason the worker process cannot run a test, the error is
|
||||
recorded in the result object.
|
||||
"""
|
||||
runner = self.getRunner(
|
||||
workerPoolFactory=partial(
|
||||
LocalWorkerPool, workerFactory=_BrokenLocalWorker, autostop=True
|
||||
)
|
||||
)
|
||||
result = self.successResultOf(runner.runAsync(self.suite))
|
||||
errors = result.original.errors
|
||||
assert_that(errors, has_length(1))
|
||||
assert_that(errors[0][1].type, equal_to(WorkerBroken))
|
||||
|
||||
def test_runWaitForProcessesDeferreds(self) -> None:
|
||||
"""
|
||||
L{DistTrialRunner} waits for the worker pool to stop.
|
||||
"""
|
||||
pool = None
|
||||
|
||||
def recordingFactory(*a, **kw):
|
||||
nonlocal pool
|
||||
pool = LocalWorkerPool(*a, autostop=False, **kw)
|
||||
return pool
|
||||
|
||||
runner = self.getRunner(
|
||||
workerPoolFactory=recordingFactory,
|
||||
)
|
||||
d = Deferred.fromCoroutine(runner.runAsync(self.suite))
|
||||
if pool is None:
|
||||
self.fail("worker pool was never created")
|
||||
|
||||
assert pool is not None
|
||||
stopped = pool._started[0]._stopped
|
||||
self.assertNoResult(d)
|
||||
stopped.callback(None)
|
||||
result = self.successResultOf(d)
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
|
||||
def test_exitFirst(self):
|
||||
"""
|
||||
L{DistTrialRunner} can run in C{exitFirst} mode where it will run until a
|
||||
test fails and then abandon the rest of the suite.
|
||||
"""
|
||||
stream = StringIO()
|
||||
# Construct a suite with a failing test in the middle.
|
||||
suite = TrialSuite(
|
||||
[
|
||||
sample.FooTest("test_foo"),
|
||||
erroneous.TestRegularFail("test_fail"),
|
||||
sample.FooTest("test_bar"),
|
||||
]
|
||||
)
|
||||
runner = self.getRunner(stream=stream, exitFirst=True, maxWorkers=2)
|
||||
d = runner.runAsync(suite)
|
||||
result = self.successResultOf(d)
|
||||
assert_that(
|
||||
result.original,
|
||||
matches_result(
|
||||
successes=1,
|
||||
failures=has_length(1),
|
||||
),
|
||||
)
|
||||
|
||||
def test_runUntilFailure(self):
|
||||
"""
|
||||
L{DistTrialRunner} can run in C{untilFailure} mode where it will run
|
||||
the given tests until they fail.
|
||||
"""
|
||||
stream = StringIO()
|
||||
case = erroneous.EventuallyFailingTestCase("test_it")
|
||||
runner = self.getRunner(stream=stream)
|
||||
d = runner.runAsync(case, untilFailure=True)
|
||||
result = self.successResultOf(d)
|
||||
# The case is hard-coded to fail on its 5th run.
|
||||
self.assertEqual(5, case.n)
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
output = stream.getvalue()
|
||||
|
||||
# It passes each time except the last.
|
||||
self.assertEqual(
|
||||
output.count("PASSED"),
|
||||
case.n - 1,
|
||||
"expected to see PASSED in output",
|
||||
)
|
||||
# It also fails at the end.
|
||||
self.assertIn("FAIL", output)
|
||||
|
||||
# It also reports its progress.
|
||||
for i in range(1, 6):
|
||||
self.assertIn(f"Test Pass {i}", output)
|
||||
|
||||
# It also reports the number of tests run as part of each iteration.
|
||||
self.assertEqual(
|
||||
output.count("Ran 1 tests in"),
|
||||
case.n,
|
||||
"expected to see per-iteration test count in output",
|
||||
)
|
||||
|
||||
def test_run(self) -> None:
|
||||
"""
|
||||
L{DistTrialRunner.run} returns a L{DistReporter} containing the result of
|
||||
the test suite run.
|
||||
"""
|
||||
runner = self.getRunner()
|
||||
result = runner.run(self.suite)
|
||||
assert_that(result.wasSuccessful(), equal_to(True))
|
||||
assert_that(result.successes, equal_to(1))
|
||||
|
||||
def test_installedReactor(self) -> None:
|
||||
"""
|
||||
L{DistTrialRunner.run} uses the installed reactor L{DistTrialRunner} was
|
||||
constructed without a reactor.
|
||||
"""
|
||||
reactor = CountingReactor([])
|
||||
with AlternateReactor(reactor):
|
||||
runner = self.getRunner(reactor=None)
|
||||
result = runner.run(self.suite)
|
||||
assert_that(result.errors, equal_to([]))
|
||||
assert_that(result.failures, equal_to([]))
|
||||
assert_that(result.wasSuccessful(), equal_to(True))
|
||||
assert_that(result.successes, equal_to(1))
|
||||
assert_that(reactor.runCount, equal_to(1))
|
||||
assert_that(reactor.stopCount, equal_to(1))
|
||||
|
||||
def test_wrongInstalledReactor(self) -> None:
|
||||
"""
|
||||
L{DistTrialRunner} raises L{TypeError} if the installed reactor provides
|
||||
neither L{IReactorCore} nor L{IReactorProcess} and no other reactor is
|
||||
given.
|
||||
"""
|
||||
|
||||
class Core(ReactorBase):
|
||||
def installWaker(self):
|
||||
pass
|
||||
|
||||
@implementer(interfaces.IReactorProcess)
|
||||
class Process:
|
||||
def spawnProcess(
|
||||
self,
|
||||
processProtocol,
|
||||
executable,
|
||||
args,
|
||||
env=None,
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=False,
|
||||
childFDs=None,
|
||||
):
|
||||
pass
|
||||
|
||||
class Neither:
|
||||
pass
|
||||
|
||||
# It provides neither
|
||||
with AlternateReactor(Neither()):
|
||||
with self.assertRaises(TypeError):
|
||||
self.getRunner(reactor=None)
|
||||
|
||||
# It is missing IReactorProcess
|
||||
with AlternateReactor(Core()):
|
||||
with self.assertRaises(TypeError):
|
||||
self.getRunner(reactor=None)
|
||||
|
||||
# It is missing IReactorCore
|
||||
with AlternateReactor(Process()):
|
||||
with self.assertRaises(TypeError):
|
||||
self.getRunner(reactor=None)
|
||||
|
||||
def test_runFailure(self):
|
||||
"""
|
||||
If there is an unexpected exception running the test suite then it is
|
||||
re-raised by L{DistTrialRunner.run}.
|
||||
"""
|
||||
|
||||
# Give it a broken worker pool factory. There's no exception handling
|
||||
# for such an error in the implementation..
|
||||
class BrokenFactory(Exception):
|
||||
pass
|
||||
|
||||
def brokenFactory(*args, **kwargs):
|
||||
raise BrokenFactory()
|
||||
|
||||
runner = self.getRunner(workerPoolFactory=brokenFactory)
|
||||
with self.assertRaises(BrokenFactory):
|
||||
runner.run(self.suite)
|
||||
|
||||
|
||||
class FunctionalTests(TestCase):
|
||||
"""
|
||||
Tests for the functional helpers that need it.
|
||||
"""
|
||||
|
||||
def test_fromOptional(self) -> None:
|
||||
"""
|
||||
``fromOptional`` accepts a default value and an ``Optional`` value of the
|
||||
same type and returns the default value if the optional value is
|
||||
``None`` or the optional value otherwise.
|
||||
"""
|
||||
assert_that(fromOptional(1, None), equal_to(1))
|
||||
assert_that(fromOptional(2, 2), equal_to(2))
|
||||
|
||||
def test_discardResult(self) -> None:
|
||||
"""
|
||||
``discardResult`` accepts an awaitable and returns a ``Deferred`` that
|
||||
fires with ``None`` after the awaitable completes.
|
||||
"""
|
||||
a: Deferred[str] = Deferred()
|
||||
d = discardResult(a)
|
||||
self.assertNoResult(d)
|
||||
a.callback("result")
|
||||
assert_that(self.successResultOf(d), none())
|
||||
|
||||
def test_sequence(self) -> None:
|
||||
"""
|
||||
``sequence`` accepts two awaitables and returns an awaitable that waits
|
||||
for the first one to complete and then completes with the result of
|
||||
the second one.
|
||||
"""
|
||||
a: Deferred[str] = Deferred()
|
||||
b: Deferred[int] = Deferred()
|
||||
c = Deferred.fromCoroutine(sequence(a, b))
|
||||
b.callback(42)
|
||||
self.assertNoResult(c)
|
||||
a.callback("hello")
|
||||
assert_that(self.successResultOf(c), equal_to(42))
|
||||
|
||||
def test_iterateWhile(self) -> None:
|
||||
"""
|
||||
``iterateWhile`` executes the actions from its factory until the predicate
|
||||
does not match an action result.
|
||||
"""
|
||||
actions: List[Deferred[int]] = [Deferred(), Deferred(), Deferred()]
|
||||
|
||||
def predicate(value):
|
||||
return value != 42
|
||||
|
||||
d: Deferred[int] = Deferred.fromCoroutine(
|
||||
iterateWhile(predicate, list(actions).pop)
|
||||
)
|
||||
# Let the action it is waiting on complete
|
||||
actions.pop().callback(7)
|
||||
|
||||
# It does not match the predicate so it is not done yet.
|
||||
self.assertNoResult(d)
|
||||
|
||||
# Let the action it is waiting on now complete - with the result it
|
||||
# wants.
|
||||
actions.pop().callback(42)
|
||||
|
||||
assert_that(self.successResultOf(d), equal_to(42))
|
||||
|
||||
def test_countingCalls(self) -> None:
|
||||
"""
|
||||
``countingCalls`` decorates a function so that it is called with an
|
||||
increasing counter and passes the return value through.
|
||||
"""
|
||||
|
||||
@countingCalls
|
||||
def target(n: int) -> int:
|
||||
return n + 1
|
||||
|
||||
for expected in range(1, 10):
|
||||
assert_that(target(), equal_to(expected))
|
||||
|
||||
|
||||
class WorkerPoolBroken(Exception):
|
||||
"""
|
||||
An exception for ``StartedWorkerPoolBroken`` to fail with to allow tests
|
||||
to exercise exception code paths.
|
||||
"""
|
||||
|
||||
|
||||
class StartedWorkerPoolBroken:
|
||||
"""
|
||||
A broken, started worker pool. Its workers cannot run actions. They
|
||||
always raise an exception.
|
||||
"""
|
||||
|
||||
async def run(self, workerAction: WorkerAction[None]) -> None:
|
||||
raise WorkerPoolBroken()
|
||||
|
||||
async def join(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@define
|
||||
class BrokenWorkerPool:
|
||||
"""
|
||||
A worker pool that has workers with a broken ``run`` method.
|
||||
"""
|
||||
|
||||
_config: WorkerPoolConfig
|
||||
|
||||
async def start(
|
||||
self, reactor: interfaces.IReactorProcess
|
||||
) -> StartedWorkerPoolBroken:
|
||||
return StartedWorkerPoolBroken()
|
||||
|
||||
|
||||
class _LocalWorker:
|
||||
"""
|
||||
A L{Worker} that runs tests in this process in the usual way.
|
||||
|
||||
This is a test double for L{LocalWorkerAMP} which allows testing worker
|
||||
pool logic without sending tests over an AMP connection to be run
|
||||
somewhere else..
|
||||
"""
|
||||
|
||||
async def run(self, case: PyUnitTestCase, result: TestResult) -> RunResult:
|
||||
"""
|
||||
Directly run C{case} in the usual way.
|
||||
"""
|
||||
TrialSuite([case]).run(result)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
class WorkerBroken(Exception):
|
||||
"""
|
||||
A worker tried to run a test case but the worker is broken.
|
||||
"""
|
||||
|
||||
|
||||
class _BrokenLocalWorker:
|
||||
"""
|
||||
A L{Worker} that always fails to run test cases.
|
||||
"""
|
||||
|
||||
async def run(self, case: PyUnitTestCase, result: TestResult) -> None:
|
||||
"""
|
||||
Raise an exception instead of running C{case}.
|
||||
"""
|
||||
raise WorkerBroken()
|
||||
|
||||
|
||||
@define
|
||||
class StartedLocalWorkerPool:
|
||||
"""
|
||||
A started L{LocalWorkerPool}.
|
||||
"""
|
||||
|
||||
workingDirectory: FilePath[str]
|
||||
workers: List[Worker]
|
||||
_stopped: Deferred[None]
|
||||
|
||||
async def run(self, workerAction: WorkerAction[None]) -> None:
|
||||
"""
|
||||
Run the action with each local worker.
|
||||
"""
|
||||
for worker in self.workers:
|
||||
await workerAction(worker)
|
||||
|
||||
async def join(self):
|
||||
await self._stopped
|
||||
|
||||
|
||||
@define
|
||||
class LocalWorkerPool:
|
||||
"""
|
||||
Implement a worker pool that runs tests in-process instead of in child
|
||||
processes.
|
||||
"""
|
||||
|
||||
_config: WorkerPoolConfig
|
||||
_started: List[StartedLocalWorkerPool] = field(default=Factory(list))
|
||||
_autostop: bool = False
|
||||
_workerFactory: Callable[[], Worker] = _LocalWorker
|
||||
|
||||
async def start(
|
||||
self, reactor: interfaces.IReactorProcess
|
||||
) -> StartedLocalWorkerPool:
|
||||
workers = [self._workerFactory() for i in range(self._config.numWorkers)]
|
||||
started = StartedLocalWorkerPool(
|
||||
self._config.workingDirectory,
|
||||
workers,
|
||||
(succeed(None) if self._autostop else Deferred()),
|
||||
)
|
||||
self._started.append(started)
|
||||
return started
|
||||
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.test.matchers}.
|
||||
"""
|
||||
|
||||
from typing import Callable, Sequence, Tuple, Type
|
||||
|
||||
from hamcrest import anything, assert_that, contains, contains_string, equal_to, not_
|
||||
from hamcrest.core.matcher import Matcher
|
||||
from hamcrest.core.string_description import StringDescription
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import (
|
||||
binary,
|
||||
booleans,
|
||||
integers,
|
||||
just,
|
||||
lists,
|
||||
one_of,
|
||||
sampled_from,
|
||||
text,
|
||||
tuples,
|
||||
)
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .matchers import HasSum, IsSequenceOf, S, isFailure, similarFrame
|
||||
|
||||
Summer = Callable[[Sequence[S]], S]
|
||||
concatInt = sum
|
||||
concatStr = "".join
|
||||
concatBytes = b"".join
|
||||
|
||||
|
||||
class HasSumTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{HasSum}.
|
||||
"""
|
||||
|
||||
summables = one_of(
|
||||
tuples(lists(integers()), just(concatInt)),
|
||||
tuples(lists(text()), just(concatStr)),
|
||||
tuples(lists(binary()), just(concatBytes)),
|
||||
)
|
||||
|
||||
@given(summables)
|
||||
def test_matches(self, summable: Tuple[Sequence[S], Summer[S]]) -> None:
|
||||
"""
|
||||
L{HasSum} matches a sequence if the elements sum to a value matched by
|
||||
the parameterized matcher.
|
||||
|
||||
:param summable: A tuple of a sequence of values to try to match and a
|
||||
function which can compute the correct sum for that sequence.
|
||||
"""
|
||||
seq, sumFunc = summable
|
||||
expected = sumFunc(seq)
|
||||
zero = sumFunc([])
|
||||
matcher = HasSum(equal_to(expected), zero)
|
||||
|
||||
description = StringDescription()
|
||||
assert_that(matcher.matches(seq, description), equal_to(True))
|
||||
assert_that(str(description), equal_to(""))
|
||||
|
||||
@given(summables)
|
||||
def test_mismatches(
|
||||
self,
|
||||
summable: Tuple[
|
||||
Sequence[S],
|
||||
Summer[S],
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
L{HasSum} does not match a sequence if the elements do not sum to a
|
||||
value matched by the parameterized matcher.
|
||||
|
||||
:param summable: See L{test_matches}.
|
||||
"""
|
||||
seq, sumFunc = summable
|
||||
zero = sumFunc([])
|
||||
# A matcher that never matches.
|
||||
sumMatcher: Matcher[S] = not_(anything())
|
||||
matcher = HasSum(sumMatcher, zero)
|
||||
|
||||
actualDescription = StringDescription()
|
||||
assert_that(matcher.matches(seq, actualDescription), equal_to(False))
|
||||
|
||||
sumMatcherDescription = StringDescription()
|
||||
sumMatcherDescription.append_description_of(sumMatcher)
|
||||
actualStr = str(actualDescription)
|
||||
assert_that(actualStr, contains_string("a sequence with sum"))
|
||||
assert_that(actualStr, contains_string(str(sumMatcherDescription)))
|
||||
|
||||
|
||||
class IsSequenceOfTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{IsSequenceOf}.
|
||||
"""
|
||||
|
||||
sequences = lists(booleans())
|
||||
|
||||
@given(integers(min_value=0, max_value=1000))
|
||||
def test_matches(self, numItems: int) -> None:
|
||||
"""
|
||||
L{IsSequenceOf} matches a sequence if all of the elements are
|
||||
matched by the parameterized matcher.
|
||||
|
||||
:param numItems: The length of a sequence to try to match.
|
||||
"""
|
||||
seq = [True] * numItems
|
||||
matcher = IsSequenceOf(equal_to(True))
|
||||
|
||||
actualDescription = StringDescription()
|
||||
assert_that(matcher.matches(seq, actualDescription), equal_to(True))
|
||||
assert_that(str(actualDescription), equal_to(""))
|
||||
|
||||
@given(integers(min_value=0, max_value=1000), integers(min_value=0, max_value=1000))
|
||||
def test_mismatches(self, numBefore: int, numAfter: int) -> None:
|
||||
"""
|
||||
L{IsSequenceOf} does not match a sequence if any of the elements
|
||||
are not matched by the parameterized matcher.
|
||||
|
||||
:param numBefore: In the sequence to try to match, the number of
|
||||
elements expected to match before an expected mismatch.
|
||||
|
||||
:param numAfter: In the sequence to try to match, the number of
|
||||
elements expected expected to match after an expected mismatch.
|
||||
"""
|
||||
# Hide the non-matching value somewhere in the sequence.
|
||||
seq = [True] * numBefore + [False] + [True] * numAfter
|
||||
matcher = IsSequenceOf(equal_to(True))
|
||||
|
||||
actualDescription = StringDescription()
|
||||
assert_that(matcher.matches(seq, actualDescription), equal_to(False))
|
||||
actualStr = str(actualDescription)
|
||||
assert_that(actualStr, contains_string("a sequence containing only"))
|
||||
assert_that(
|
||||
actualStr, contains_string(f"not sequence with element #{numBefore}")
|
||||
)
|
||||
|
||||
|
||||
class IsFailureTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{isFailure}.
|
||||
"""
|
||||
|
||||
@given(sampled_from([ValueError, ZeroDivisionError, RuntimeError]))
|
||||
def test_matches(self, excType: Type[BaseException]) -> None:
|
||||
"""
|
||||
L{isFailure} matches instances of L{Failure} with matching
|
||||
attributes.
|
||||
|
||||
:param excType: An exception type to wrap in a L{Failure} to be
|
||||
matched against.
|
||||
"""
|
||||
matcher = isFailure(type=equal_to(excType))
|
||||
failure = Failure(excType())
|
||||
assert_that(matcher.matches(failure), equal_to(True))
|
||||
|
||||
@given(sampled_from([ValueError, ZeroDivisionError, RuntimeError]))
|
||||
def test_mismatches(self, excType: Type[BaseException]) -> None:
|
||||
"""
|
||||
L{isFailure} does not match instances of L{Failure} with
|
||||
attributes that don't match.
|
||||
|
||||
:param excType: An exception type to wrap in a L{Failure} to be
|
||||
matched against.
|
||||
"""
|
||||
matcher = isFailure(type=equal_to(excType), other=not_(anything()))
|
||||
failure = Failure(excType())
|
||||
assert_that(matcher.matches(failure), equal_to(False))
|
||||
|
||||
def test_frames(self):
|
||||
"""
|
||||
The L{similarFrame} matcher matches elements of the C{frames} list
|
||||
of a L{Failure}.
|
||||
"""
|
||||
try:
|
||||
raise ValueError("Oh no")
|
||||
except BaseException:
|
||||
f = Failure()
|
||||
|
||||
actualDescription = StringDescription()
|
||||
matcher = isFailure(
|
||||
frames=contains(similarFrame("test_frames", "test_matchers"))
|
||||
)
|
||||
assert_that(
|
||||
matcher.matches(f, actualDescription),
|
||||
equal_to(True),
|
||||
actualDescription,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for distributed trial's options management.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
|
||||
from twisted.trial._dist.options import WorkerOptions
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class WorkerOptionsTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerOptions}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Build an L{WorkerOptions} object to be used in the tests.
|
||||
"""
|
||||
self.options = WorkerOptions()
|
||||
|
||||
def test_standardOptions(self) -> None:
|
||||
"""
|
||||
L{WorkerOptions} supports a subset of standard options supported by
|
||||
trial.
|
||||
"""
|
||||
self.addCleanup(sys.setrecursionlimit, sys.getrecursionlimit())
|
||||
if gc.isenabled():
|
||||
self.addCleanup(gc.enable)
|
||||
gc.enable()
|
||||
self.options.parseOptions(["--recursionlimit", "2000", "--disablegc"])
|
||||
self.assertEqual(2000, sys.getrecursionlimit())
|
||||
self.assertFalse(gc.isenabled())
|
||||
|
||||
def test_coverage(self) -> None:
|
||||
"""
|
||||
L{WorkerOptions.coverdir} returns the C{coverage} child directory of
|
||||
the current directory to be used for storing coverage data.
|
||||
"""
|
||||
self.assertEqual(
|
||||
os.path.realpath(os.path.join(os.getcwd(), "coverage")),
|
||||
self.options.coverdir().path,
|
||||
)
|
||||
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.stream}.
|
||||
"""
|
||||
|
||||
from random import Random
|
||||
from typing import Awaitable, Dict, List, TypeVar, Union
|
||||
|
||||
from hamcrest import (
|
||||
all_of,
|
||||
assert_that,
|
||||
calling,
|
||||
equal_to,
|
||||
has_length,
|
||||
is_,
|
||||
less_than_or_equal_to,
|
||||
raises,
|
||||
)
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import binary, integers, just, lists, randoms, text
|
||||
|
||||
from twisted.internet.defer import Deferred, fail
|
||||
from twisted.internet.interfaces import IProtocol
|
||||
from twisted.internet.protocol import Protocol
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.test.iosim import FakeTransport, connect
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from ..stream import StreamOpen, StreamReceiver, StreamWrite, chunk, stream
|
||||
from .matchers import HasSum, IsSequenceOf
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class StreamReceiverTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{StreamReceiver}
|
||||
"""
|
||||
|
||||
@given(lists(lists(binary())), randoms())
|
||||
def test_streamReceived(self, streams: List[List[bytes]], random: Random) -> None:
|
||||
"""
|
||||
All data passed to L{StreamReceiver.write} is returned by a call to
|
||||
L{StreamReceiver.finish} with a matching C{streamId}.
|
||||
"""
|
||||
receiver = StreamReceiver()
|
||||
streamIds = [receiver.open() for _ in streams]
|
||||
|
||||
# uncorrelate the results with open() order
|
||||
random.shuffle(streamIds)
|
||||
|
||||
expectedData = dict(zip(streamIds, streams))
|
||||
for streamId, strings in expectedData.items():
|
||||
for s in strings:
|
||||
receiver.write(streamId, s)
|
||||
|
||||
# uncorrelate the results with write() order
|
||||
random.shuffle(streamIds)
|
||||
|
||||
actualData = {streamId: receiver.finish(streamId) for streamId in streamIds}
|
||||
|
||||
assert_that(actualData, is_(equal_to(expectedData)))
|
||||
|
||||
@given(integers(), just("data"))
|
||||
def test_writeBadStreamId(self, streamId: int, data: str) -> None:
|
||||
"""
|
||||
L{StreamReceiver.write} raises L{KeyError} if called with a
|
||||
streamId not associated with an open stream.
|
||||
"""
|
||||
receiver = StreamReceiver()
|
||||
assert_that(calling(receiver.write).with_args(streamId, data), raises(KeyError))
|
||||
|
||||
@given(integers())
|
||||
def test_badFinishStreamId(self, streamId: int) -> None:
|
||||
"""
|
||||
L{StreamReceiver.finish} raises L{KeyError} if called with a
|
||||
streamId not associated with an open stream.
|
||||
"""
|
||||
receiver = StreamReceiver()
|
||||
assert_that(calling(receiver.finish).with_args(streamId), raises(KeyError))
|
||||
|
||||
def test_finishRemovesStream(self) -> None:
|
||||
"""
|
||||
L{StreamReceiver.finish} removes the identified stream.
|
||||
"""
|
||||
receiver = StreamReceiver()
|
||||
streamId = receiver.open()
|
||||
receiver.finish(streamId)
|
||||
assert_that(calling(receiver.finish).with_args(streamId), raises(KeyError))
|
||||
|
||||
|
||||
class ChunkTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for ``chunk``.
|
||||
"""
|
||||
|
||||
@given(data=text(), chunkSize=integers(min_value=1))
|
||||
def test_chunk(self, data, chunkSize):
|
||||
"""
|
||||
L{chunk} returns an iterable of L{str} where each element is no
|
||||
longer than the given limit. The concatenation of the strings is also
|
||||
equal to the original input string.
|
||||
"""
|
||||
chunks = list(chunk(data, chunkSize))
|
||||
assert_that(
|
||||
chunks,
|
||||
all_of(
|
||||
IsSequenceOf(
|
||||
has_length(less_than_or_equal_to(chunkSize)),
|
||||
),
|
||||
HasSum(equal_to(data), data[:0]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AMPStreamReceiver(AMP):
|
||||
"""
|
||||
A simple AMP interface to L{StreamReceiver}.
|
||||
"""
|
||||
|
||||
def __init__(self, streams: StreamReceiver) -> None:
|
||||
self.streams = streams
|
||||
|
||||
@StreamOpen.responder
|
||||
def streamOpen(self) -> Dict[str, object]:
|
||||
return {"streamId": self.streams.open()}
|
||||
|
||||
@StreamWrite.responder
|
||||
def streamWrite(self, streamId: int, data: bytes) -> Dict[str, object]:
|
||||
self.streams.write(streamId, data)
|
||||
return {}
|
||||
|
||||
|
||||
def interact(server: IProtocol, client: IProtocol, interaction: Awaitable[T]) -> T:
|
||||
"""
|
||||
Let C{server} and C{client} exchange bytes while C{interaction} runs.
|
||||
"""
|
||||
finished = False
|
||||
result: Union[Failure, T]
|
||||
|
||||
async def to_coroutine() -> T:
|
||||
return await interaction
|
||||
|
||||
def collect_result(r: Union[Failure, T]) -> None:
|
||||
nonlocal result, finished
|
||||
finished = True
|
||||
result = r
|
||||
|
||||
pump = connect(
|
||||
server,
|
||||
FakeTransport(server, isServer=True),
|
||||
client,
|
||||
FakeTransport(client, isServer=False),
|
||||
)
|
||||
interacting = Deferred.fromCoroutine(to_coroutine())
|
||||
interacting.addBoth(collect_result)
|
||||
|
||||
pump.flush()
|
||||
|
||||
if finished:
|
||||
if isinstance(result, Failure):
|
||||
result.raiseException()
|
||||
return result
|
||||
raise Exception("Interaction failed to produce a result.")
|
||||
|
||||
|
||||
class InteractTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the test helper L{interact}.
|
||||
"""
|
||||
|
||||
def test_failure(self):
|
||||
"""
|
||||
If the interaction results in a failure then L{interact} raises an
|
||||
exception.
|
||||
"""
|
||||
|
||||
class ArbitraryException(Exception):
|
||||
pass
|
||||
|
||||
with self.assertRaises(ArbitraryException):
|
||||
interact(Protocol(), Protocol(), fail(ArbitraryException()))
|
||||
|
||||
def test_incomplete(self):
|
||||
"""
|
||||
If the interaction fails to produce a result then L{interact} raises
|
||||
an exception.
|
||||
"""
|
||||
with self.assertRaises(Exception):
|
||||
interact(Protocol(), Protocol(), Deferred())
|
||||
|
||||
|
||||
class StreamTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{stream}.
|
||||
"""
|
||||
|
||||
@given(lists(binary()))
|
||||
def test_stream(self, chunks: List[bytes]) -> None:
|
||||
"""
|
||||
All of the chunks passed to L{stream} are sent in order over a
|
||||
stream using the given AMP connection.
|
||||
"""
|
||||
sender = AMP()
|
||||
streams = StreamReceiver()
|
||||
streamId = interact(
|
||||
AMPStreamReceiver(streams), sender, stream(sender, iter(chunks))
|
||||
)
|
||||
assert_that(streams.finish(streamId), is_(equal_to(chunks)))
|
||||
@@ -0,0 +1,532 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test for distributed trial worker side.
|
||||
"""
|
||||
|
||||
import os
|
||||
from io import BytesIO, StringIO
|
||||
from typing import Type
|
||||
from unittest import TestCase as PyUnitTestCase
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from hamcrest import assert_that, equal_to, has_item, has_length
|
||||
|
||||
from twisted.internet.defer import Deferred, fail
|
||||
from twisted.internet.error import ConnectionLost, ProcessDone
|
||||
from twisted.internet.interfaces import IAddress, ITransport
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.test.iosim import connectedServerAndClient
|
||||
from twisted.trial._dist import managercommands
|
||||
from twisted.trial._dist.worker import (
|
||||
LocalWorker,
|
||||
LocalWorkerAMP,
|
||||
LocalWorkerTransport,
|
||||
NotRunning,
|
||||
WorkerException,
|
||||
WorkerProtocol,
|
||||
)
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial.test import pyunitcases, skipping
|
||||
from twisted.trial.unittest import TestCase, makeTodo
|
||||
from .matchers import isFailure, matches_result, similarFrame
|
||||
|
||||
|
||||
class WorkerProtocolTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerProtocol}.
|
||||
"""
|
||||
|
||||
worker: WorkerProtocol
|
||||
server: LocalWorkerAMP
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Set up a transport, a result stream and a protocol instance.
|
||||
"""
|
||||
self.worker, self.server, pump = connectedServerAndClient(
|
||||
LocalWorkerAMP, WorkerProtocol, greet=False
|
||||
)
|
||||
self.flush = pump.flush
|
||||
|
||||
def test_run(self) -> None:
|
||||
"""
|
||||
Sending the L{workercommands.Run} command to the worker returns a
|
||||
response with C{success} sets to C{True}.
|
||||
"""
|
||||
d = Deferred.fromCoroutine(
|
||||
self.server.run(pyunitcases.PyUnitTest("test_pass"), TestResult())
|
||||
)
|
||||
self.flush()
|
||||
self.assertEqual({"success": True}, self.successResultOf(d))
|
||||
|
||||
def test_start(self) -> None:
|
||||
"""
|
||||
The C{start} command changes the current path.
|
||||
"""
|
||||
curdir = os.path.realpath(os.path.curdir)
|
||||
self.addCleanup(os.chdir, curdir)
|
||||
self.worker.start("..")
|
||||
self.assertNotEqual(os.path.realpath(os.path.curdir), curdir)
|
||||
|
||||
|
||||
class WorkerProtocolErrorTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerProtocol}'s handling of certain errors related to
|
||||
running the tests themselves (i.e., not test errors but test
|
||||
infrastructure/runner errors).
|
||||
"""
|
||||
|
||||
def _runErrorTest(
|
||||
self, brokenTestName: str, loggedExceptionType: Type[BaseException]
|
||||
) -> None:
|
||||
worker, server, pump = connectedServerAndClient(
|
||||
LocalWorkerAMP, WorkerProtocol, greet=False
|
||||
)
|
||||
expectedCase = pyunitcases.BrokenRunInfrastructure(brokenTestName)
|
||||
result = TestResult()
|
||||
Deferred.fromCoroutine(server.run(expectedCase, result))
|
||||
pump.flush()
|
||||
assert_that(result, matches_result(errors=has_length(1)))
|
||||
[(actualCase, errors)] = result.errors
|
||||
assert_that(actualCase, equal_to(expectedCase))
|
||||
|
||||
# Additionally, we expect that the worker protocol logged the failure
|
||||
# once so that it is visible somewhere, even if it cannot deliver it
|
||||
# back to the parent process (which it can in this case). Since the
|
||||
# worker runs in process with us, that failure is in our log so we can
|
||||
# easily make an assertion about it. Also, if we don't flush it, the
|
||||
# test fails. As far as the type goes, we just have to be aware of
|
||||
# the implementation details of `BrokenRunInfrastructure`.
|
||||
assert_that(self.flushLoggedErrors(loggedExceptionType), has_length(1))
|
||||
|
||||
def test_addSuccessError(self) -> None:
|
||||
"""
|
||||
If there is an error reporting success then the test run is marked as
|
||||
an error.
|
||||
"""
|
||||
self._runErrorTest("test_addSuccess", AttributeError)
|
||||
|
||||
def test_addErrorError(self) -> None:
|
||||
"""
|
||||
If there is an error reporting an error then the test run is marked as
|
||||
an error.
|
||||
"""
|
||||
self._runErrorTest("test_addError", AttributeError)
|
||||
|
||||
def test_addFailureError(self) -> None:
|
||||
"""
|
||||
If there is an error reporting a failure then the test run is marked
|
||||
as an error.
|
||||
"""
|
||||
self._runErrorTest("test_addFailure", AttributeError)
|
||||
|
||||
def test_addSkipError(self) -> None:
|
||||
"""
|
||||
If there is an error reporting a skip then the test run is marked
|
||||
as an error.
|
||||
"""
|
||||
self._runErrorTest("test_addSkip", AttributeError)
|
||||
|
||||
def test_addExpectedFailure(self) -> None:
|
||||
"""
|
||||
If there is an error reporting an expected failure then the test
|
||||
run is marked as an error.
|
||||
"""
|
||||
self._runErrorTest("test_addExpectedFailure", AttributeError)
|
||||
|
||||
def test_addUnexpectedSuccess(self) -> None:
|
||||
"""
|
||||
If there is an error reporting an unexpected ccess then the test
|
||||
run is marked as an error.
|
||||
"""
|
||||
self._runErrorTest("test_addUnexpectedSuccess", AttributeError)
|
||||
|
||||
def test_failedFailureReport(self) -> None:
|
||||
"""
|
||||
A failure encountered while reporting a reporting failure is logged.
|
||||
"""
|
||||
worker, server, pump = connectedServerAndClient(
|
||||
LocalWorkerAMP, WorkerProtocol, greet=False
|
||||
)
|
||||
|
||||
# We can easily break everything by eliminating the worker protocol's
|
||||
# transport. This prevents it from ever sending anything to the
|
||||
# manager protocol.
|
||||
worker.transport = None
|
||||
|
||||
expectedCase = pyunitcases.PyUnitTest("test_pass")
|
||||
result = TestResult()
|
||||
Deferred.fromCoroutine(server.run(expectedCase, result))
|
||||
pump.flush()
|
||||
|
||||
# There should be two exceptions logged here. The first is from the
|
||||
# attempt to report the success result. The second is a report that
|
||||
# the first failed.
|
||||
assert_that(self.flushLoggedErrors(ConnectionLost), has_length(2))
|
||||
|
||||
|
||||
class LocalWorkerAMPTests(TestCase):
|
||||
"""
|
||||
Test case for distributed trial's manager-side local worker AMP protocol
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.worker, self.managerAMP, pump = connectedServerAndClient(
|
||||
LocalWorkerAMP, WorkerProtocol, greet=False
|
||||
)
|
||||
self.flush = pump.flush
|
||||
|
||||
def workerRunTest(
|
||||
self, testCase: PyUnitTestCase, makeResult: Type[TestResult] = TestResult
|
||||
) -> TestResult:
|
||||
result = makeResult()
|
||||
d = Deferred.fromCoroutine(self.managerAMP.run(testCase, result))
|
||||
self.flush()
|
||||
self.assertEqual({"success": True}, self.successResultOf(d))
|
||||
return result
|
||||
|
||||
def test_runSuccess(self) -> None:
|
||||
"""
|
||||
Run a test, and succeed.
|
||||
"""
|
||||
result = self.workerRunTest(pyunitcases.PyUnitTest("test_pass"))
|
||||
assert_that(result, matches_result(successes=equal_to(1)))
|
||||
|
||||
def test_runExpectedFailure(self) -> None:
|
||||
"""
|
||||
Run a test, and fail expectedly.
|
||||
"""
|
||||
expectedCase = skipping.SynchronousStrictTodo("test_todo1")
|
||||
result = self.workerRunTest(expectedCase)
|
||||
assert_that(result, matches_result(expectedFailures=has_length(1)))
|
||||
[(actualCase, exceptionMessage, todoReason)] = result.expectedFailures
|
||||
assert_that(actualCase, equal_to(expectedCase))
|
||||
|
||||
# Match the strings used in the test we ran.
|
||||
assert_that(exceptionMessage, equal_to("expected failure"))
|
||||
assert_that(todoReason, equal_to(makeTodo("todo1")))
|
||||
|
||||
def test_runError(self) -> None:
|
||||
"""
|
||||
Run a test, and encounter an error.
|
||||
"""
|
||||
expectedCase = pyunitcases.PyUnitTest("test_error")
|
||||
result = self.workerRunTest(expectedCase)
|
||||
assert_that(result, matches_result(errors=has_length(1)))
|
||||
[(actualCase, failure)] = result.errors
|
||||
assert_that(expectedCase, equal_to(actualCase))
|
||||
assert_that(
|
||||
failure,
|
||||
isFailure(
|
||||
type=equal_to(Exception),
|
||||
value=equal_to(WorkerException("pyunit error")),
|
||||
frames=has_item(similarFrame("test_error", "pyunitcases.py")), # type: ignore[arg-type]
|
||||
),
|
||||
)
|
||||
|
||||
def test_runFailure(self) -> None:
|
||||
"""
|
||||
Run a test, and fail.
|
||||
"""
|
||||
expectedCase = pyunitcases.PyUnitTest("test_fail")
|
||||
result = self.workerRunTest(expectedCase)
|
||||
assert_that(result, matches_result(failures=has_length(1)))
|
||||
[(actualCase, failure)] = result.failures
|
||||
assert_that(expectedCase, equal_to(actualCase))
|
||||
assert_that(
|
||||
failure,
|
||||
isFailure(
|
||||
# AssertionError is the type raised by TestCase.fail
|
||||
type=equal_to(AssertionError),
|
||||
value=equal_to(WorkerException("pyunit failure")),
|
||||
),
|
||||
)
|
||||
|
||||
def test_runSkip(self) -> None:
|
||||
"""
|
||||
Run a test, but skip it.
|
||||
"""
|
||||
expectedCase = pyunitcases.PyUnitTest("test_skip")
|
||||
result = self.workerRunTest(expectedCase)
|
||||
assert_that(result, matches_result(skips=has_length(1)))
|
||||
[(actualCase, skip)] = result.skips
|
||||
assert_that(expectedCase, equal_to(actualCase))
|
||||
assert_that(skip, equal_to("pyunit skip"))
|
||||
|
||||
def test_runUnexpectedSuccesses(self) -> None:
|
||||
"""
|
||||
Run a test, and succeed unexpectedly.
|
||||
"""
|
||||
expectedCase = skipping.SynchronousStrictTodo("test_todo7")
|
||||
result = self.workerRunTest(expectedCase)
|
||||
assert_that(result, matches_result(unexpectedSuccesses=has_length(1)))
|
||||
[(actualCase, unexpectedSuccess)] = result.unexpectedSuccesses
|
||||
assert_that(expectedCase, equal_to(actualCase))
|
||||
assert_that(unexpectedSuccess, equal_to("todo7"))
|
||||
|
||||
def test_testWrite(self) -> None:
|
||||
"""
|
||||
L{LocalWorkerAMP.testWrite} writes the data received to its test
|
||||
stream.
|
||||
"""
|
||||
stream = StringIO()
|
||||
self.managerAMP.setTestStream(stream)
|
||||
d = self.worker.callRemote(managercommands.TestWrite, out="Some output")
|
||||
self.flush()
|
||||
self.assertEqual({"success": True}, self.successResultOf(d))
|
||||
self.assertEqual("Some output\n", stream.getvalue())
|
||||
|
||||
def test_stopAfterRun(self) -> None:
|
||||
"""
|
||||
L{LocalWorkerAMP.run} calls C{stopTest} on its test result once the
|
||||
C{Run} commands has succeeded.
|
||||
"""
|
||||
stopped = []
|
||||
|
||||
class StopTestResult(TestResult):
|
||||
def stopTest(self, test: PyUnitTestCase) -> None:
|
||||
stopped.append(test)
|
||||
|
||||
case = pyunitcases.PyUnitTest("test_pass")
|
||||
self.workerRunTest(case, StopTestResult)
|
||||
assert_that(stopped, equal_to([case]))
|
||||
|
||||
|
||||
class SpyDataLocalWorkerAMP(LocalWorkerAMP):
|
||||
"""
|
||||
A fake implementation of L{LocalWorkerAMP} that records the received
|
||||
data and doesn't automatically dispatch any command..
|
||||
"""
|
||||
|
||||
id = 0
|
||||
dataString = b""
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.dataString += data
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
"""
|
||||
A fake process transport implementation for testing.
|
||||
"""
|
||||
|
||||
dataString = b""
|
||||
calls = 0
|
||||
|
||||
def writeToChild(self, fd, data):
|
||||
self.dataString += data
|
||||
|
||||
def loseConnection(self):
|
||||
self.calls += 1
|
||||
|
||||
|
||||
class LocalWorkerTests(TestCase):
|
||||
"""
|
||||
Tests for L{LocalWorker} and L{LocalWorkerTransport}.
|
||||
"""
|
||||
|
||||
def tidyLocalWorker(self, *args, **kwargs):
|
||||
"""
|
||||
Create a L{LocalWorker}, connect it to a transport, and ensure
|
||||
its log files are closed.
|
||||
|
||||
@param args: See L{LocalWorker}
|
||||
|
||||
@param kwargs: See L{LocalWorker}
|
||||
|
||||
@return: a L{LocalWorker} instance
|
||||
"""
|
||||
worker = LocalWorker(*args, **kwargs)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.addCleanup(worker._outLog.close)
|
||||
self.addCleanup(worker._errLog.close)
|
||||
return worker
|
||||
|
||||
def test_exitBeforeConnected(self):
|
||||
"""
|
||||
L{LocalWorker.exit} fails with L{NotRunning} if it is called before the
|
||||
protocol is connected to a transport.
|
||||
"""
|
||||
worker = LocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), StringIO()
|
||||
)
|
||||
self.failureResultOf(worker.exit(), NotRunning)
|
||||
|
||||
def test_exitAfterDisconnected(self):
|
||||
"""
|
||||
L{LocalWorker.exit} fails with L{NotRunning} if it is called after the the
|
||||
protocol is disconnected from its transport.
|
||||
"""
|
||||
worker = self.tidyLocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), StringIO()
|
||||
)
|
||||
worker.processEnded(Failure(ProcessDone(0)))
|
||||
# Since we're not calling exit until after the process has ended, it
|
||||
# won't consume the ProcessDone failure on the internal `endDeferred`.
|
||||
# Swallow it here.
|
||||
self.failureResultOf(worker.endDeferred, ProcessDone)
|
||||
|
||||
# Now assert that exit behaves.
|
||||
self.failureResultOf(worker.exit(), NotRunning)
|
||||
|
||||
def test_childDataReceived(self):
|
||||
"""
|
||||
L{LocalWorker.childDataReceived} forwards the received data to linked
|
||||
L{AMP} protocol if the right file descriptor, otherwise forwards to
|
||||
C{ProcessProtocol.childDataReceived}.
|
||||
"""
|
||||
localWorker = self.tidyLocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), "test.log"
|
||||
)
|
||||
localWorker._outLog = BytesIO()
|
||||
localWorker.childDataReceived(4, b"foo")
|
||||
localWorker.childDataReceived(1, b"bar")
|
||||
self.assertEqual(b"foo", localWorker._ampProtocol.dataString)
|
||||
self.assertEqual(b"bar", localWorker._outLog.getvalue())
|
||||
|
||||
def test_newlineStyle(self):
|
||||
"""
|
||||
L{LocalWorker} writes the log data with local newlines.
|
||||
"""
|
||||
amp = SpyDataLocalWorkerAMP()
|
||||
tempDir = FilePath(self.mktemp())
|
||||
tempDir.makedirs()
|
||||
logPath = tempDir.child("test.log")
|
||||
|
||||
with open(logPath.path, "wt", encoding="utf-8") as logFile:
|
||||
worker = LocalWorker(amp, tempDir, logFile)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.addCleanup(worker._outLog.close)
|
||||
self.addCleanup(worker._errLog.close)
|
||||
|
||||
expected = "Here comes the \N{sun}!"
|
||||
amp.testWrite(expected)
|
||||
|
||||
self.assertEqual(
|
||||
# os.linesep is the local newline.
|
||||
(expected + os.linesep),
|
||||
# getContent reads in binary mode so we'll see the bytes that
|
||||
# actually ended up in the file.
|
||||
logPath.getContent().decode("utf-8"),
|
||||
)
|
||||
|
||||
def test_outReceived(self):
|
||||
"""
|
||||
L{LocalWorker.outReceived} logs the output into its C{_outLog} log
|
||||
file.
|
||||
"""
|
||||
localWorker = self.tidyLocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), "test.log"
|
||||
)
|
||||
localWorker._outLog = BytesIO()
|
||||
data = b"The quick brown fox jumps over the lazy dog"
|
||||
localWorker.outReceived(data)
|
||||
self.assertEqual(data, localWorker._outLog.getvalue())
|
||||
|
||||
def test_errReceived(self):
|
||||
"""
|
||||
L{LocalWorker.errReceived} logs the errors into its C{_errLog} log
|
||||
file.
|
||||
"""
|
||||
localWorker = self.tidyLocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), "test.log"
|
||||
)
|
||||
localWorker._errLog = BytesIO()
|
||||
data = b"The quick brown fox jumps over the lazy dog"
|
||||
localWorker.errReceived(data)
|
||||
self.assertEqual(data, localWorker._errLog.getvalue())
|
||||
|
||||
def test_write(self):
|
||||
"""
|
||||
L{LocalWorkerTransport.write} forwards the written data to the given
|
||||
transport.
|
||||
"""
|
||||
transport = FakeTransport()
|
||||
localTransport = LocalWorkerTransport(transport)
|
||||
data = b"The quick brown fox jumps over the lazy dog"
|
||||
localTransport.write(data)
|
||||
self.assertEqual(data, transport.dataString)
|
||||
|
||||
def test_writeSequence(self):
|
||||
"""
|
||||
L{LocalWorkerTransport.writeSequence} forwards the written data to the
|
||||
given transport.
|
||||
"""
|
||||
transport = FakeTransport()
|
||||
localTransport = LocalWorkerTransport(transport)
|
||||
data = (b"The quick ", b"brown fox jumps ", b"over the lazy dog")
|
||||
localTransport.writeSequence(data)
|
||||
self.assertEqual(b"".join(data), transport.dataString)
|
||||
|
||||
def test_loseConnection(self):
|
||||
"""
|
||||
L{LocalWorkerTransport.loseConnection} forwards the call to the given
|
||||
transport.
|
||||
"""
|
||||
transport = FakeTransport()
|
||||
localTransport = LocalWorkerTransport(transport)
|
||||
localTransport.loseConnection()
|
||||
|
||||
self.assertEqual(transport.calls, 1)
|
||||
|
||||
def test_connectionLost(self):
|
||||
"""
|
||||
L{LocalWorker.connectionLost} closes the per-worker log streams.
|
||||
"""
|
||||
|
||||
localWorker = self.tidyLocalWorker(
|
||||
SpyDataLocalWorkerAMP(), FilePath(self.mktemp()), "test.log"
|
||||
)
|
||||
localWorker.connectionLost(None)
|
||||
self.assertTrue(localWorker._outLog.closed)
|
||||
self.assertTrue(localWorker._errLog.closed)
|
||||
|
||||
def test_processEnded(self):
|
||||
"""
|
||||
L{LocalWorker.processEnded} calls C{connectionLost} on itself and on
|
||||
the L{AMP} protocol.
|
||||
"""
|
||||
transport = FakeTransport()
|
||||
protocol = SpyDataLocalWorkerAMP()
|
||||
localWorker = LocalWorker(protocol, FilePath(self.mktemp()), "test.log")
|
||||
localWorker.makeConnection(transport)
|
||||
localWorker.processEnded(Failure(ProcessDone(0)))
|
||||
self.assertTrue(localWorker._outLog.closed)
|
||||
self.assertTrue(localWorker._errLog.closed)
|
||||
self.assertIdentical(None, protocol.transport)
|
||||
return self.assertFailure(localWorker.endDeferred, ProcessDone)
|
||||
|
||||
def test_addresses(self):
|
||||
"""
|
||||
L{LocalWorkerTransport.getPeer} and L{LocalWorkerTransport.getHost}
|
||||
return L{IAddress} objects.
|
||||
"""
|
||||
localTransport = LocalWorkerTransport(None)
|
||||
self.assertTrue(verifyObject(IAddress, localTransport.getPeer()))
|
||||
self.assertTrue(verifyObject(IAddress, localTransport.getHost()))
|
||||
|
||||
def test_transport(self):
|
||||
"""
|
||||
L{LocalWorkerTransport} implements L{ITransport} to be able to be used
|
||||
by L{AMP}.
|
||||
"""
|
||||
localTransport = LocalWorkerTransport(None)
|
||||
self.assertTrue(verifyObject(ITransport, localTransport))
|
||||
|
||||
def test_startError(self):
|
||||
"""
|
||||
L{LocalWorker} swallows the exceptions returned by the L{AMP} protocol
|
||||
start method, as it generates unnecessary errors.
|
||||
"""
|
||||
|
||||
def failCallRemote(command, directory):
|
||||
return fail(RuntimeError("oops"))
|
||||
|
||||
protocol = SpyDataLocalWorkerAMP()
|
||||
protocol.callRemote = failCallRemote
|
||||
self.tidyLocalWorker(protocol, FilePath(self.mktemp()), "test.log")
|
||||
self.assertEqual([], self.flushLoggedErrors(RuntimeError))
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.workerreporter}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sized
|
||||
from unittest import TestCase
|
||||
|
||||
from hamcrest import assert_that, equal_to, has_length
|
||||
from hamcrest.core.matcher import Matcher
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.test.iosim import connectedServerAndClient
|
||||
from twisted.trial._dist.worker import LocalWorkerAMP, WorkerProtocol
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial.test import erroneous, pyunitcases, sample, skipping
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .matchers import matches_result
|
||||
|
||||
|
||||
def run(case: SynchronousTestCase, target: TestCase) -> TestResult:
|
||||
"""
|
||||
Run C{target} and return a test result as populated by a worker reporter.
|
||||
|
||||
@param case: A test case to use to help run the target.
|
||||
"""
|
||||
result = TestResult()
|
||||
worker, local, pump = connectedServerAndClient(LocalWorkerAMP, WorkerProtocol)
|
||||
d = Deferred.fromCoroutine(local.run(target, result))
|
||||
pump.flush()
|
||||
assert_that(case.successResultOf(d), equal_to({"success": True}))
|
||||
return result
|
||||
|
||||
|
||||
class WorkerReporterTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{WorkerReporter}.
|
||||
"""
|
||||
|
||||
def assertTestRun(self, target: TestCase, **expectations: Matcher[Sized]) -> None:
|
||||
"""
|
||||
Run the given test and assert that the result matches the given
|
||||
expectations.
|
||||
"""
|
||||
assert_that(run(self, target), matches_result(**expectations))
|
||||
|
||||
def test_outsideReportingContext(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter}'s implementation of test result methods raise
|
||||
L{ValueError} when called outside of the
|
||||
L{WorkerReporter.gatherReportingResults} context manager.
|
||||
"""
|
||||
worker, local, pump = connectedServerAndClient(LocalWorkerAMP, WorkerProtocol)
|
||||
|
||||
case = sample.FooTest("test_foo")
|
||||
with self.assertRaises(ValueError):
|
||||
worker._result.addSuccess(case)
|
||||
|
||||
def test_addSuccess(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates successes.
|
||||
"""
|
||||
self.assertTestRun(sample.FooTest("test_foo"), successes=equal_to(1))
|
||||
|
||||
def test_addError(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates errors from trial's TestCases.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
erroneous.TestAsynchronousFail("test_exception"), errors=has_length(1)
|
||||
)
|
||||
|
||||
def test_addErrorGreaterThan64k(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates errors with large string representations.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
erroneous.TestAsynchronousFail("test_exceptionGreaterThan64k"),
|
||||
errors=has_length(1),
|
||||
)
|
||||
|
||||
def test_addErrorGreaterThan64kEncoded(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates errors with a string representation that
|
||||
is smaller than an implementation-specific limit but which encode to a
|
||||
byte representation that exceeds this limit.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
erroneous.TestAsynchronousFail("test_exceptionGreaterThan64kEncoded"),
|
||||
errors=has_length(1),
|
||||
)
|
||||
|
||||
def test_addErrorTuple(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates errors from pyunit's TestCases.
|
||||
"""
|
||||
self.assertTestRun(pyunitcases.PyUnitTest("test_error"), errors=has_length(1))
|
||||
|
||||
def test_addFailure(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates test failures from trial's TestCases.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
erroneous.TestRegularFail("test_fail"), failures=has_length(1)
|
||||
)
|
||||
|
||||
def test_addFailureGreaterThan64k(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates test failures with large string representations.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
erroneous.TestAsynchronousFail("test_failGreaterThan64k"),
|
||||
failures=has_length(1),
|
||||
)
|
||||
|
||||
def test_addFailureTuple(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates test failures from pyunit's TestCases.
|
||||
"""
|
||||
self.assertTestRun(pyunitcases.PyUnitTest("test_fail"), failures=has_length(1))
|
||||
|
||||
def test_addSkip(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates skips.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
skipping.SynchronousSkipping("test_skip1"), skips=has_length(1)
|
||||
)
|
||||
|
||||
def test_addSkipPyunit(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates skips from L{unittest.TestCase} cases.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
pyunitcases.PyUnitTest("test_skip"),
|
||||
skips=has_length(1),
|
||||
)
|
||||
|
||||
def test_addExpectedFailure(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates expected failures.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
skipping.SynchronousStrictTodo("test_todo1"), expectedFailures=has_length(1)
|
||||
)
|
||||
|
||||
def test_addExpectedFailureGreaterThan64k(self) -> None:
|
||||
"""
|
||||
WorkerReporter propagates expected failures with large string representations.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
skipping.ExpectedFailure("test_expectedFailureGreaterThan64k"),
|
||||
expectedFailures=has_length(1),
|
||||
)
|
||||
|
||||
def test_addUnexpectedSuccess(self) -> None:
|
||||
"""
|
||||
L{WorkerReporter} propagates unexpected successes.
|
||||
"""
|
||||
self.assertTestRun(
|
||||
skipping.SynchronousTodo("test_todo3"), unexpectedSuccesses=has_length(1)
|
||||
)
|
||||
@@ -0,0 +1,147 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.workertrial}.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.trial._dist import (
|
||||
_WORKER_AMP_STDIN,
|
||||
_WORKER_AMP_STDOUT,
|
||||
managercommands,
|
||||
workercommands,
|
||||
workertrial,
|
||||
)
|
||||
from twisted.trial._dist.workertrial import WorkerLogObserver, main
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class FakeAMP(AMP):
|
||||
"""
|
||||
A fake amp protocol.
|
||||
"""
|
||||
|
||||
|
||||
class WorkerLogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerLogObserver}.
|
||||
"""
|
||||
|
||||
def test_emit(self):
|
||||
"""
|
||||
L{WorkerLogObserver} forwards data to L{managercommands.TestWrite}.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
class FakeClient:
|
||||
def callRemote(self, method, **kwargs):
|
||||
calls.append((method, kwargs))
|
||||
|
||||
observer = WorkerLogObserver(FakeClient())
|
||||
observer.emit({"message": ["Some log"]})
|
||||
self.assertEqual(calls, [(managercommands.TestWrite, {"out": "Some log"})])
|
||||
|
||||
|
||||
class MainTests(TestCase):
|
||||
"""
|
||||
Tests for L{main}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.readStream = BytesIO()
|
||||
self.writeStream = BytesIO()
|
||||
self.patch(
|
||||
workertrial, "startLoggingWithObserver", self.startLoggingWithObserver
|
||||
)
|
||||
self.addCleanup(setattr, sys, "argv", sys.argv)
|
||||
sys.argv = ["trial"]
|
||||
|
||||
def fdopen(self, fd, mode=None):
|
||||
"""
|
||||
Fake C{os.fdopen} implementation which returns C{self.readStream} for
|
||||
the stdin fd and C{self.writeStream} for the stdout fd.
|
||||
"""
|
||||
if fd == _WORKER_AMP_STDIN:
|
||||
self.assertEqual("rb", mode)
|
||||
return self.readStream
|
||||
elif fd == _WORKER_AMP_STDOUT:
|
||||
self.assertEqual("wb", mode)
|
||||
return self.writeStream
|
||||
else:
|
||||
raise AssertionError(f"Unexpected fd {fd!r}")
|
||||
|
||||
def startLoggingWithObserver(self, emit, setStdout):
|
||||
"""
|
||||
Override C{startLoggingWithObserver} for not starting logging.
|
||||
"""
|
||||
self.assertFalse(setStdout)
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
If no data is ever written, L{main} exits without writing data out.
|
||||
"""
|
||||
main(self.fdopen)
|
||||
self.assertEqual(b"", self.writeStream.getvalue())
|
||||
|
||||
def test_forwardCommand(self):
|
||||
"""
|
||||
L{main} forwards data from its input stream to a L{WorkerProtocol}
|
||||
instance which writes data to the output stream.
|
||||
"""
|
||||
client = FakeAMP()
|
||||
clientTransport = StringTransport()
|
||||
client.makeConnection(clientTransport)
|
||||
client.callRemote(workercommands.Run, testCase="doesntexist")
|
||||
self.readStream = clientTransport.io
|
||||
self.readStream.seek(0, 0)
|
||||
main(self.fdopen)
|
||||
# Just brazenly encode irrelevant implementation details here, why
|
||||
# not.
|
||||
self.assertIn(b"StreamOpen", self.writeStream.getvalue())
|
||||
|
||||
def test_readInterrupted(self):
|
||||
"""
|
||||
If reading the input stream fails with a C{IOError} with errno
|
||||
C{EINTR}, L{main} ignores it and continues reading.
|
||||
"""
|
||||
excInfos = []
|
||||
|
||||
class FakeStream:
|
||||
count = 0
|
||||
|
||||
def read(oself, size):
|
||||
oself.count += 1
|
||||
if oself.count == 1:
|
||||
raise OSError(errno.EINTR)
|
||||
else:
|
||||
excInfos.append(sys.exc_info())
|
||||
return b""
|
||||
|
||||
self.readStream = FakeStream()
|
||||
main(self.fdopen)
|
||||
self.assertEqual(b"", self.writeStream.getvalue())
|
||||
self.assertEqual([(None, None, None)], excInfos)
|
||||
|
||||
def test_otherReadError(self):
|
||||
"""
|
||||
L{main} only ignores C{IOError} with C{EINTR} errno: otherwise, the
|
||||
error pops out.
|
||||
"""
|
||||
|
||||
class FakeStream:
|
||||
count = 0
|
||||
|
||||
def read(oself, size):
|
||||
oself.count += 1
|
||||
if oself.count == 1:
|
||||
raise OSError("Something else")
|
||||
return ""
|
||||
|
||||
self.readStream = FakeStream()
|
||||
self.assertRaises(IOError, main, self.fdopen)
|
||||
465
.venv/lib/python3.12/site-packages/twisted/trial/_dist/worker.py
Normal file
465
.venv/lib/python3.12/site-packages/twisted/trial/_dist/worker.py
Normal file
@@ -0,0 +1,465 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_worker -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module implements the worker classes.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional, TextIO, TypeVar
|
||||
from unittest import TestCase
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from attrs import frozen
|
||||
from typing_extensions import Protocol, TypedDict
|
||||
|
||||
from twisted.internet.defer import Deferred, DeferredList
|
||||
from twisted.internet.error import ProcessDone
|
||||
from twisted.internet.interfaces import IAddress, ITransport
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
from twisted.logger import Logger
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import namedObject
|
||||
from twisted.trial._dist import (
|
||||
_WORKER_AMP_STDIN,
|
||||
_WORKER_AMP_STDOUT,
|
||||
managercommands,
|
||||
workercommands,
|
||||
)
|
||||
from twisted.trial._dist.workerreporter import WorkerReporter
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial.runner import TestLoader, TrialSuite
|
||||
from twisted.trial.unittest import Todo
|
||||
from .stream import StreamOpen, StreamReceiver, StreamWrite
|
||||
|
||||
|
||||
@frozen(auto_exc=False)
|
||||
class WorkerException(Exception):
|
||||
"""
|
||||
An exception was reported by a test running in a worker process.
|
||||
|
||||
@ivar message: An error message describing the exception.
|
||||
"""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class RunResult(TypedDict):
|
||||
"""
|
||||
Represent the result of a L{workercommands.Run} command.
|
||||
"""
|
||||
|
||||
success: bool
|
||||
|
||||
|
||||
class Worker(Protocol):
|
||||
"""
|
||||
An object that can run actions.
|
||||
"""
|
||||
|
||||
async def run(self, case: TestCase, result: TestResult) -> RunResult:
|
||||
"""
|
||||
Run a test case.
|
||||
"""
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
WorkerAction = Callable[[Worker], Awaitable[_T]]
|
||||
|
||||
|
||||
class WorkerProtocol(AMP):
|
||||
"""
|
||||
The worker-side trial distributed protocol.
|
||||
"""
|
||||
|
||||
logger = Logger()
|
||||
|
||||
def __init__(self, forceGarbageCollection=False):
|
||||
self._loader = TestLoader()
|
||||
self._result = WorkerReporter(self)
|
||||
self._forceGarbageCollection = forceGarbageCollection
|
||||
|
||||
@workercommands.Run.responder
|
||||
async def run(self, testCase: str) -> RunResult:
|
||||
"""
|
||||
Run a test case by name.
|
||||
"""
|
||||
with self._result.gatherReportingResults() as results:
|
||||
case = self._loader.loadByName(testCase)
|
||||
suite = TrialSuite([case], self._forceGarbageCollection)
|
||||
suite.run(self._result)
|
||||
|
||||
allSucceeded = True
|
||||
for success, result in await DeferredList(results, consumeErrors=True):
|
||||
if success:
|
||||
# Nothing to do here, proceed to the next result.
|
||||
continue
|
||||
|
||||
# There was some error reporting a result to the peer.
|
||||
allSucceeded = False
|
||||
|
||||
# We can try to report the error but since something has already
|
||||
# gone wrong we shouldn't be extremely confident that this will
|
||||
# succeed. So we will also log it (and any errors reporting *it*)
|
||||
# to our local log.
|
||||
self.logger.failure(
|
||||
"Result reporting for {id} failed",
|
||||
# The DeferredList type annotation assumes all results succeed
|
||||
failure=result, # type: ignore[arg-type]
|
||||
id=testCase,
|
||||
)
|
||||
try:
|
||||
await self._result.addErrorFallible(
|
||||
testCase,
|
||||
# The DeferredList type annotation assumes all results succeed
|
||||
result, # type: ignore[arg-type]
|
||||
)
|
||||
except BaseException:
|
||||
# We failed to report the failure to the peer. It doesn't
|
||||
# seem very likely that reporting this new failure to the peer
|
||||
# will succeed so just log it locally.
|
||||
self.logger.failure(
|
||||
"Additionally, reporting the reporting failure failed."
|
||||
)
|
||||
|
||||
return {"success": allSucceeded}
|
||||
|
||||
@workercommands.Start.responder
|
||||
def start(self, directory):
|
||||
"""
|
||||
Set up the worker, moving into given directory for tests to run in
|
||||
them.
|
||||
"""
|
||||
os.chdir(directory)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
class LocalWorkerAMP(AMP):
|
||||
"""
|
||||
Local implementation of the manager commands.
|
||||
"""
|
||||
|
||||
def __init__(self, boxReceiver=None, locator=None):
|
||||
super().__init__(boxReceiver, locator)
|
||||
self._streams = StreamReceiver()
|
||||
|
||||
@StreamOpen.responder
|
||||
def streamOpen(self):
|
||||
return {"streamId": self._streams.open()}
|
||||
|
||||
@StreamWrite.responder
|
||||
def streamWrite(self, streamId, data):
|
||||
self._streams.write(streamId, data)
|
||||
return {}
|
||||
|
||||
@managercommands.AddSuccess.responder
|
||||
def addSuccess(self, testName):
|
||||
"""
|
||||
Add a success to the reporter.
|
||||
"""
|
||||
self._result.addSuccess(self._testCase)
|
||||
return {"success": True}
|
||||
|
||||
def _buildFailure(
|
||||
self,
|
||||
error: WorkerException,
|
||||
errorClass: str,
|
||||
frames: List[str],
|
||||
) -> Failure:
|
||||
"""
|
||||
Helper to build a C{Failure} with some traceback.
|
||||
|
||||
@param error: An C{Exception} instance.
|
||||
|
||||
@param errorClass: The class name of the C{error} class.
|
||||
|
||||
@param frames: A flat list of strings representing the information need
|
||||
to approximatively rebuild C{Failure} frames.
|
||||
|
||||
@return: A L{Failure} instance with enough information about a test
|
||||
error.
|
||||
"""
|
||||
errorType = namedObject(errorClass)
|
||||
failure = Failure(error, errorType)
|
||||
for i in range(0, len(frames), 3):
|
||||
failure.frames.append(
|
||||
(frames[i], frames[i + 1], int(frames[i + 2]), [], [])
|
||||
)
|
||||
return failure
|
||||
|
||||
@managercommands.AddError.responder
|
||||
def addError(
|
||||
self,
|
||||
testName: str,
|
||||
errorClass: str,
|
||||
errorStreamId: int,
|
||||
framesStreamId: int,
|
||||
) -> Dict[str, bool]:
|
||||
"""
|
||||
Add an error to the reporter.
|
||||
|
||||
@param errorStreamId: The identifier of a stream over which the text
|
||||
of this error was previously completely sent to the peer.
|
||||
|
||||
@param framesStreamId: The identifier of a stream over which the lines
|
||||
of the traceback for this error were previously completely sent to
|
||||
the peer.
|
||||
|
||||
@param error: A message describing the error.
|
||||
"""
|
||||
error = b"".join(self._streams.finish(errorStreamId)).decode("utf-8")
|
||||
frames = [
|
||||
frame.decode("utf-8") for frame in self._streams.finish(framesStreamId)
|
||||
]
|
||||
# Wrap the error message in ``WorkerException`` because it is not
|
||||
# possible to transfer arbitrary exception values over the AMP
|
||||
# connection to the main process but we must give *some* Exception
|
||||
# (not a str) to the test result object.
|
||||
failure = self._buildFailure(WorkerException(error), errorClass, frames)
|
||||
self._result.addError(self._testCase, failure)
|
||||
return {"success": True}
|
||||
|
||||
@managercommands.AddFailure.responder
|
||||
def addFailure(
|
||||
self,
|
||||
testName: str,
|
||||
failStreamId: int,
|
||||
failClass: str,
|
||||
framesStreamId: int,
|
||||
) -> Dict[str, bool]:
|
||||
"""
|
||||
Add a failure to the reporter.
|
||||
|
||||
@param failStreamId: The identifier of a stream over which the text of
|
||||
this failure was previously completely sent to the peer.
|
||||
|
||||
@param framesStreamId: The identifier of a stream over which the lines
|
||||
of the traceback for this error were previously completely sent to the
|
||||
peer.
|
||||
"""
|
||||
fail = b"".join(self._streams.finish(failStreamId)).decode("utf-8")
|
||||
frames = [
|
||||
frame.decode("utf-8") for frame in self._streams.finish(framesStreamId)
|
||||
]
|
||||
# See addError for info about use of WorkerException here.
|
||||
failure = self._buildFailure(WorkerException(fail), failClass, frames)
|
||||
self._result.addFailure(self._testCase, failure)
|
||||
return {"success": True}
|
||||
|
||||
@managercommands.AddSkip.responder
|
||||
def addSkip(self, testName, reason):
|
||||
"""
|
||||
Add a skip to the reporter.
|
||||
"""
|
||||
self._result.addSkip(self._testCase, reason)
|
||||
return {"success": True}
|
||||
|
||||
@managercommands.AddExpectedFailure.responder
|
||||
def addExpectedFailure(
|
||||
self, testName: str, errorStreamId: int, todo: Optional[str]
|
||||
) -> Dict[str, bool]:
|
||||
"""
|
||||
Add an expected failure to the reporter.
|
||||
|
||||
@param errorStreamId: The identifier of a stream over which the text
|
||||
of this error was previously completely sent to the peer.
|
||||
"""
|
||||
error = b"".join(self._streams.finish(errorStreamId)).decode("utf-8")
|
||||
_todo = Todo("<unknown>" if todo is None else todo)
|
||||
self._result.addExpectedFailure(self._testCase, error, _todo)
|
||||
return {"success": True}
|
||||
|
||||
@managercommands.AddUnexpectedSuccess.responder
|
||||
def addUnexpectedSuccess(self, testName, todo):
|
||||
"""
|
||||
Add an unexpected success to the reporter.
|
||||
"""
|
||||
self._result.addUnexpectedSuccess(self._testCase, todo)
|
||||
return {"success": True}
|
||||
|
||||
@managercommands.TestWrite.responder
|
||||
def testWrite(self, out):
|
||||
"""
|
||||
Print test output from the worker.
|
||||
"""
|
||||
self._testStream.write(out + "\n")
|
||||
self._testStream.flush()
|
||||
return {"success": True}
|
||||
|
||||
async def run(self, testCase: TestCase, result: TestResult) -> RunResult:
|
||||
"""
|
||||
Run a test.
|
||||
"""
|
||||
self._testCase = testCase
|
||||
self._result = result
|
||||
self._result.startTest(testCase)
|
||||
testCaseId = testCase.id()
|
||||
try:
|
||||
return await self.callRemote(workercommands.Run, testCase=testCaseId) # type: ignore[no-any-return]
|
||||
finally:
|
||||
self._result.stopTest(testCase)
|
||||
|
||||
def setTestStream(self, stream):
|
||||
"""
|
||||
Set the stream used to log output from tests.
|
||||
"""
|
||||
self._testStream = stream
|
||||
|
||||
|
||||
@implementer(IAddress)
|
||||
class LocalWorkerAddress:
|
||||
"""
|
||||
A L{IAddress} implementation meant to provide stub addresses for
|
||||
L{ITransport.getPeer} and L{ITransport.getHost}.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(ITransport)
|
||||
class LocalWorkerTransport:
|
||||
"""
|
||||
A stub transport implementation used to support L{AMP} over a
|
||||
L{ProcessProtocol} transport.
|
||||
"""
|
||||
|
||||
def __init__(self, transport):
|
||||
self._transport = transport
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Forward data to transport.
|
||||
"""
|
||||
self._transport.writeToChild(_WORKER_AMP_STDIN, data)
|
||||
|
||||
def writeSequence(self, sequence):
|
||||
"""
|
||||
Emulate C{writeSequence} by iterating data in the C{sequence}.
|
||||
"""
|
||||
for data in sequence:
|
||||
self._transport.writeToChild(_WORKER_AMP_STDIN, data)
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Closes the transport.
|
||||
"""
|
||||
self._transport.loseConnection()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
Return a L{LocalWorkerAddress} instance.
|
||||
"""
|
||||
return LocalWorkerAddress()
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
Return a L{LocalWorkerAddress} instance.
|
||||
"""
|
||||
return LocalWorkerAddress()
|
||||
|
||||
|
||||
class NotRunning(Exception):
|
||||
"""
|
||||
An operation was attempted on a worker process which is not running.
|
||||
"""
|
||||
|
||||
|
||||
class LocalWorker(ProcessProtocol):
|
||||
"""
|
||||
Local process worker protocol. This worker runs as a local process and
|
||||
communicates via stdin/out.
|
||||
|
||||
@ivar _ampProtocol: The L{AMP} protocol instance used to communicate with
|
||||
the worker.
|
||||
|
||||
@ivar _logDirectory: The directory where logs will reside.
|
||||
|
||||
@ivar _logFile: The main log file for tests output.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ampProtocol: LocalWorkerAMP,
|
||||
logDirectory: FilePath[Any],
|
||||
logFile: TextIO,
|
||||
):
|
||||
self._ampProtocol = ampProtocol
|
||||
self._logDirectory = logDirectory
|
||||
self._logFile = logFile
|
||||
self.endDeferred: Deferred[None] = Deferred()
|
||||
|
||||
async def exit(self) -> None:
|
||||
"""
|
||||
Cause the worker process to exit.
|
||||
"""
|
||||
if self.transport is None:
|
||||
raise NotRunning()
|
||||
|
||||
endDeferred = self.endDeferred
|
||||
self.transport.closeChildFD(_WORKER_AMP_STDIN)
|
||||
try:
|
||||
await endDeferred
|
||||
except ProcessDone:
|
||||
pass
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
When connection is made, create the AMP protocol instance.
|
||||
"""
|
||||
self._ampProtocol.makeConnection(LocalWorkerTransport(self.transport))
|
||||
self._logDirectory.makedirs(ignoreExistingDirectory=True)
|
||||
self._outLog = self._logDirectory.child("out.log").open("w")
|
||||
self._errLog = self._logDirectory.child("err.log").open("w")
|
||||
self._ampProtocol.setTestStream(self._logFile)
|
||||
d = self._ampProtocol.callRemote(
|
||||
workercommands.Start,
|
||||
directory=self._logDirectory.path,
|
||||
)
|
||||
# Ignore the potential errors, the test suite will fail properly and it
|
||||
# would just print garbage.
|
||||
d.addErrback(lambda x: None)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
On connection lost, close the log files that we're managing for stdin
|
||||
and stdout.
|
||||
"""
|
||||
self._outLog.close()
|
||||
self._errLog.close()
|
||||
self.transport = None
|
||||
|
||||
def processEnded(self, reason: Failure) -> None:
|
||||
"""
|
||||
When the process closes, call C{connectionLost} for cleanup purposes
|
||||
and forward the information to the C{_ampProtocol}.
|
||||
"""
|
||||
self.connectionLost(reason)
|
||||
self._ampProtocol.connectionLost(reason)
|
||||
self.endDeferred.callback(reason)
|
||||
|
||||
def outReceived(self, data):
|
||||
"""
|
||||
Send data received from stdout to log.
|
||||
"""
|
||||
|
||||
self._outLog.write(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
"""
|
||||
Write error data to log.
|
||||
"""
|
||||
self._errLog.write(data)
|
||||
|
||||
def childDataReceived(self, childFD, data):
|
||||
"""
|
||||
Handle data received on the specific pipe for the C{_ampProtocol}.
|
||||
"""
|
||||
if childFD == _WORKER_AMP_STDOUT:
|
||||
self._ampProtocol.dataReceived(data)
|
||||
else:
|
||||
ProcessProtocol.childDataReceived(self, childFD, data)
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Commands for telling a worker to load tests or run tests.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
from twisted.protocols.amp import Boolean, Command, Unicode
|
||||
|
||||
NativeString = Unicode
|
||||
|
||||
|
||||
class Run(Command):
|
||||
"""
|
||||
Run a test.
|
||||
"""
|
||||
|
||||
arguments = [(b"testCase", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
|
||||
|
||||
class Start(Command):
|
||||
"""
|
||||
Set up the worker process, giving the running directory.
|
||||
"""
|
||||
|
||||
arguments = [(b"directory", NativeString())]
|
||||
response = [(b"success", Boolean())]
|
||||
@@ -0,0 +1,354 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_workerreporter -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test reporter forwarding test results over trial distributed AMP commands.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
from types import TracebackType
|
||||
from typing import Callable, List, Optional, Sequence, Type, TypeVar
|
||||
from unittest import TestCase as PyUnitTestCase
|
||||
|
||||
from attrs import Factory, define
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.protocols.amp import AMP, MAX_VALUE_LENGTH
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import qual
|
||||
from twisted.trial._dist import managercommands
|
||||
from twisted.trial.reporter import TestResult
|
||||
from ..reporter import TrialFailure
|
||||
from .stream import chunk, stream
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
async def addError(
|
||||
amp: AMP, testName: str, errorClass: str, error: str, frames: List[str]
|
||||
) -> None:
|
||||
"""
|
||||
Send an error to the worker manager over an AMP connection.
|
||||
|
||||
First the pieces which can be large are streamed over the connection.
|
||||
Then, L{managercommands.AddError} is called with the rest of the
|
||||
information and the stream IDs.
|
||||
|
||||
:param amp: The connection to use.
|
||||
:param testName: The name (or ID) of the test the error relates to.
|
||||
:param errorClass: The fully qualified name of the error type.
|
||||
:param error: The string representation of the error.
|
||||
:param frames: The lines of the traceback associated with the error.
|
||||
"""
|
||||
|
||||
errorStreamId = await stream(amp, chunk(error.encode("utf-8"), MAX_VALUE_LENGTH))
|
||||
framesStreamId = await stream(amp, (frame.encode("utf-8") for frame in frames))
|
||||
|
||||
await amp.callRemote(
|
||||
managercommands.AddError,
|
||||
testName=testName,
|
||||
errorClass=errorClass,
|
||||
errorStreamId=errorStreamId,
|
||||
framesStreamId=framesStreamId,
|
||||
)
|
||||
|
||||
|
||||
async def addFailure(
|
||||
amp: AMP, testName: str, fail: str, failClass: str, frames: List[str]
|
||||
) -> None:
|
||||
"""
|
||||
Like L{addError} but for failures.
|
||||
|
||||
:param amp: See L{addError}
|
||||
:param testName: See L{addError}
|
||||
:param failClass: The fully qualified name of the exception associated
|
||||
with the failure.
|
||||
:param fail: The string representation of the failure.
|
||||
:param frames: The lines of the traceback associated with the error.
|
||||
"""
|
||||
failStreamId = await stream(amp, chunk(fail.encode("utf-8"), MAX_VALUE_LENGTH))
|
||||
framesStreamId = await stream(amp, (frame.encode("utf-8") for frame in frames))
|
||||
|
||||
await amp.callRemote(
|
||||
managercommands.AddFailure,
|
||||
testName=testName,
|
||||
failClass=failClass,
|
||||
failStreamId=failStreamId,
|
||||
framesStreamId=framesStreamId,
|
||||
)
|
||||
|
||||
|
||||
async def addExpectedFailure(amp: AMP, testName: str, error: str, todo: str) -> None:
|
||||
"""
|
||||
Like L{addError} but for expected failures.
|
||||
|
||||
:param amp: See L{addError}
|
||||
:param testName: See L{addError}
|
||||
:param error: The string representation of the expected failure.
|
||||
:param todo: The string description of the expectation.
|
||||
"""
|
||||
errorStreamId = await stream(amp, chunk(error.encode("utf-8"), MAX_VALUE_LENGTH))
|
||||
|
||||
await amp.callRemote(
|
||||
managercommands.AddExpectedFailure,
|
||||
testName=testName,
|
||||
errorStreamId=errorStreamId,
|
||||
todo=todo,
|
||||
)
|
||||
|
||||
|
||||
@define
|
||||
class ReportingResults:
|
||||
"""
|
||||
A mutable container for the result of sending test results back to the
|
||||
parent process.
|
||||
|
||||
Since it is possible for these sends to fail asynchronously but the
|
||||
L{TestResult} protocol is not well suited for asynchronous result
|
||||
reporting, results are collected on an instance of this class and when the
|
||||
runner believes the test is otherwise complete, it can collect the results
|
||||
and do something with any errors.
|
||||
|
||||
:ivar _reporter: The L{WorkerReporter} this object is associated with.
|
||||
This is the object doing the result reporting.
|
||||
|
||||
:ivar _results: A list of L{Deferred} instances representing the results
|
||||
of reporting operations. This is expected to grow over the course of
|
||||
the test run and then be inspected by the runner once the test is
|
||||
over. The public interface to this list is via the context manager
|
||||
interface.
|
||||
"""
|
||||
|
||||
_reporter: "WorkerReporter"
|
||||
_results: List[Deferred[object]] = Factory(list)
|
||||
|
||||
def __enter__(self) -> Sequence[Deferred[object]]:
|
||||
"""
|
||||
Begin a new reportable context in which results can be collected.
|
||||
|
||||
:return: A sequence which will contain the L{Deferred} instances
|
||||
representing the results of all test result reporting that happens
|
||||
while the context manager is active. The sequence is extended as
|
||||
the test runs so its value should not be consumed until the test
|
||||
is over.
|
||||
"""
|
||||
return self._results
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
excType: Type[BaseException],
|
||||
excValue: BaseException,
|
||||
excTraceback: TracebackType,
|
||||
) -> Literal[False]:
|
||||
"""
|
||||
End the reportable context.
|
||||
"""
|
||||
self._reporter._reporting = None
|
||||
return False
|
||||
|
||||
def record(self, result: Deferred[object]) -> None:
|
||||
"""
|
||||
Record a L{Deferred} instance representing one test result reporting
|
||||
operation.
|
||||
"""
|
||||
self._results.append(result)
|
||||
|
||||
|
||||
class WorkerReporter(TestResult):
|
||||
"""
|
||||
Reporter for trial's distributed workers. We send things not through a
|
||||
stream, but through an C{AMP} protocol's C{callRemote} method.
|
||||
|
||||
@ivar _DEFAULT_TODO: Default message for expected failures and
|
||||
unexpected successes, used only if a C{Todo} is not provided.
|
||||
|
||||
@ivar _reporting: When a "result reporting" context is active, the
|
||||
corresponding context manager. Otherwise, L{None}.
|
||||
"""
|
||||
|
||||
_DEFAULT_TODO = "Test expected to fail"
|
||||
|
||||
ampProtocol: AMP
|
||||
_reporting: Optional[ReportingResults] = None
|
||||
|
||||
def __init__(self, ampProtocol):
|
||||
"""
|
||||
@param ampProtocol: The communication channel with the trial
|
||||
distributed manager which collects all test results.
|
||||
"""
|
||||
super().__init__()
|
||||
self.ampProtocol = ampProtocol
|
||||
|
||||
def gatherReportingResults(self) -> ReportingResults:
|
||||
"""
|
||||
Get a "result reporting" context manager.
|
||||
|
||||
In a "result reporting" context, asynchronous test result reporting
|
||||
methods may be used safely. Their results (in particular, failures)
|
||||
are available from the context manager.
|
||||
"""
|
||||
self._reporting = ReportingResults(self)
|
||||
return self._reporting
|
||||
|
||||
def _getFailure(self, error: TrialFailure) -> Failure:
|
||||
"""
|
||||
Convert a C{sys.exc_info()}-style tuple to a L{Failure}, if necessary.
|
||||
"""
|
||||
if isinstance(error, tuple):
|
||||
return Failure(error[1], error[0], error[2])
|
||||
return error
|
||||
|
||||
def _getFrames(self, failure: Failure) -> List[str]:
|
||||
"""
|
||||
Extract frames from a C{Failure} instance.
|
||||
"""
|
||||
frames: List[str] = []
|
||||
for frame in failure.frames:
|
||||
# The code object's name, the code object's filename, and the line
|
||||
# number.
|
||||
frames.extend([frame[0], frame[1], str(frame[2])])
|
||||
return frames
|
||||
|
||||
def _call(self, f: Callable[[], T]) -> None:
|
||||
"""
|
||||
Call L{f} if and only if a "result reporting" context is active.
|
||||
|
||||
@param f: A function to call. Its result is accumulated into the
|
||||
result reporting context. It may return a L{Deferred} or a
|
||||
coroutine or synchronously raise an exception or return a result
|
||||
value.
|
||||
|
||||
@raise ValueError: If no result reporting context is active.
|
||||
"""
|
||||
if self._reporting is not None:
|
||||
self._reporting.record(maybeDeferred(f))
|
||||
else:
|
||||
raise ValueError(
|
||||
"Cannot call command outside of reporting context manager."
|
||||
)
|
||||
|
||||
def addSuccess(self, test: PyUnitTestCase) -> None:
|
||||
"""
|
||||
Send a success to the parent process.
|
||||
|
||||
This must be called in context managed by L{gatherReportingResults}.
|
||||
"""
|
||||
super().addSuccess(test)
|
||||
testName = test.id()
|
||||
self._call(
|
||||
lambda: self.ampProtocol.callRemote(
|
||||
managercommands.AddSuccess, testName=testName
|
||||
)
|
||||
)
|
||||
|
||||
async def addErrorFallible(self, testName: str, errorObj: TrialFailure) -> None:
|
||||
"""
|
||||
Attempt to report an error to the parent process.
|
||||
|
||||
Unlike L{addError} this can fail asynchronously. This version is for
|
||||
infrastructure code that can apply its own failure handling.
|
||||
|
||||
@return: A L{Deferred} that fires with the result of the attempt.
|
||||
"""
|
||||
failure = self._getFailure(errorObj)
|
||||
errorStr = failure.getErrorMessage()
|
||||
errorClass = qual(failure.type)
|
||||
frames = self._getFrames(failure)
|
||||
await addError(
|
||||
self.ampProtocol,
|
||||
testName,
|
||||
errorClass,
|
||||
errorStr,
|
||||
frames,
|
||||
)
|
||||
|
||||
def addError(self, test: PyUnitTestCase, error: TrialFailure) -> None:
|
||||
"""
|
||||
Send an error to the parent process.
|
||||
"""
|
||||
super().addError(test, error)
|
||||
testName = test.id()
|
||||
self._call(lambda: self.addErrorFallible(testName, error))
|
||||
|
||||
def addFailure(self, test: PyUnitTestCase, fail: TrialFailure) -> None:
|
||||
"""
|
||||
Send a Failure over.
|
||||
"""
|
||||
super().addFailure(test, fail)
|
||||
testName = test.id()
|
||||
failure = self._getFailure(fail)
|
||||
failureMessage = failure.getErrorMessage()
|
||||
failClass = qual(failure.type)
|
||||
frames = self._getFrames(failure)
|
||||
self._call(
|
||||
lambda: addFailure(
|
||||
self.ampProtocol,
|
||||
testName,
|
||||
failureMessage,
|
||||
failClass,
|
||||
frames,
|
||||
),
|
||||
)
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
"""
|
||||
Send a skip over.
|
||||
"""
|
||||
super().addSkip(test, reason)
|
||||
reason = str(reason)
|
||||
testName = test.id()
|
||||
self._call(
|
||||
lambda: self.ampProtocol.callRemote(
|
||||
managercommands.AddSkip, testName=testName, reason=reason
|
||||
)
|
||||
)
|
||||
|
||||
def _getTodoReason(self, todo):
|
||||
"""
|
||||
Get the reason for a C{Todo}.
|
||||
|
||||
If C{todo} is L{None}, return a sensible default.
|
||||
"""
|
||||
if todo is None:
|
||||
return self._DEFAULT_TODO
|
||||
else:
|
||||
return todo.reason
|
||||
|
||||
def addExpectedFailure(self, test, error, todo=None):
|
||||
"""
|
||||
Send an expected failure over.
|
||||
"""
|
||||
super().addExpectedFailure(test, error, todo)
|
||||
errorMessage = error.getErrorMessage()
|
||||
testName = test.id()
|
||||
self._call(
|
||||
lambda: addExpectedFailure(
|
||||
self.ampProtocol,
|
||||
testName=testName,
|
||||
error=errorMessage,
|
||||
todo=self._getTodoReason(todo),
|
||||
)
|
||||
)
|
||||
|
||||
def addUnexpectedSuccess(self, test, todo=None):
|
||||
"""
|
||||
Send an unexpected success over.
|
||||
"""
|
||||
super().addUnexpectedSuccess(test, todo)
|
||||
testName = test.id()
|
||||
self._call(
|
||||
lambda: self.ampProtocol.callRemote(
|
||||
managercommands.AddUnexpectedSuccess,
|
||||
testName=testName,
|
||||
todo=self._getTodoReason(todo),
|
||||
)
|
||||
)
|
||||
|
||||
def printSummary(self):
|
||||
"""
|
||||
I{Don't} print a summary
|
||||
"""
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- test-case-name: twisted.trial._dist.test.test_workertrial -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation of C{AMP} worker commands, and main executable entry point for
|
||||
the workers.
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
|
||||
from twisted.internet.protocol import FileWrapper
|
||||
from twisted.python.log import startLoggingWithObserver, textFromEventDict
|
||||
from twisted.trial._dist import _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT
|
||||
from twisted.trial._dist.options import WorkerOptions
|
||||
|
||||
|
||||
class WorkerLogObserver:
|
||||
"""
|
||||
A log observer that forward its output to a C{AMP} protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
@param protocol: a connected C{AMP} protocol instance.
|
||||
@type protocol: C{AMP}
|
||||
"""
|
||||
self.protocol = protocol
|
||||
|
||||
def emit(self, eventDict):
|
||||
"""
|
||||
Produce a log output.
|
||||
"""
|
||||
from twisted.trial._dist import managercommands
|
||||
|
||||
text = textFromEventDict(eventDict)
|
||||
if text is None:
|
||||
return
|
||||
self.protocol.callRemote(managercommands.TestWrite, out=text)
|
||||
|
||||
|
||||
def main(_fdopen=os.fdopen):
|
||||
"""
|
||||
Main function to be run if __name__ == "__main__".
|
||||
|
||||
@param _fdopen: If specified, the function to use in place of C{os.fdopen}.
|
||||
@type _fdopen: C{callable}
|
||||
"""
|
||||
config = WorkerOptions()
|
||||
config.parseOptions()
|
||||
|
||||
from twisted.trial._dist.worker import WorkerProtocol
|
||||
|
||||
workerProtocol = WorkerProtocol(config["force-gc"])
|
||||
|
||||
protocolIn = _fdopen(_WORKER_AMP_STDIN, "rb")
|
||||
protocolOut = _fdopen(_WORKER_AMP_STDOUT, "wb")
|
||||
workerProtocol.makeConnection(FileWrapper(protocolOut))
|
||||
|
||||
observer = WorkerLogObserver(workerProtocol)
|
||||
startLoggingWithObserver(observer.emit, False)
|
||||
|
||||
while True:
|
||||
try:
|
||||
r = protocolIn.read(1)
|
||||
except OSError as e:
|
||||
if e.args[0] == errno.EINTR:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
if r == b"":
|
||||
break
|
||||
else:
|
||||
workerProtocol.dataReceived(r)
|
||||
protocolOut.flush()
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
if config.tracer:
|
||||
sys.settrace(None)
|
||||
results = config.tracer.results()
|
||||
results.write_results(
|
||||
show_missing=True, summary=False, coverdir=config.coverdir().path
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1469
.venv/lib/python3.12/site-packages/twisted/trial/_synctest.py
Normal file
1469
.venv/lib/python3.12/site-packages/twisted/trial/_synctest.py
Normal file
File diff suppressed because it is too large
Load Diff
157
.venv/lib/python3.12/site-packages/twisted/trial/itrial.py
Normal file
157
.venv/lib/python3.12/site-packages/twisted/trial/itrial.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Interfaces for Trial.
|
||||
|
||||
Maintainer: Jonathan Lange
|
||||
"""
|
||||
|
||||
|
||||
import zope.interface as zi
|
||||
|
||||
|
||||
class ITestCase(zi.Interface):
|
||||
"""
|
||||
The interface that a test case must implement in order to be used in Trial.
|
||||
"""
|
||||
|
||||
failureException = zi.Attribute(
|
||||
"The exception class that is raised by failed assertions"
|
||||
)
|
||||
|
||||
def __call__(result):
|
||||
"""
|
||||
Run the test. Should always do exactly the same thing as run().
|
||||
"""
|
||||
|
||||
def countTestCases():
|
||||
"""
|
||||
Return the number of tests in this test case. Usually 1.
|
||||
"""
|
||||
|
||||
def id():
|
||||
"""
|
||||
Return a unique identifier for the test, usually the fully-qualified
|
||||
Python name.
|
||||
"""
|
||||
|
||||
def run(result):
|
||||
"""
|
||||
Run the test, storing the results in C{result}.
|
||||
|
||||
@param result: A L{TestResult}.
|
||||
"""
|
||||
|
||||
def shortDescription():
|
||||
"""
|
||||
Return a short description of the test.
|
||||
"""
|
||||
|
||||
|
||||
class IReporter(zi.Interface):
|
||||
"""
|
||||
I report results from a run of a test suite.
|
||||
"""
|
||||
|
||||
shouldStop = zi.Attribute(
|
||||
"A boolean indicating that this reporter would like the " "test run to stop."
|
||||
)
|
||||
testsRun = zi.Attribute(
|
||||
"""
|
||||
The number of tests that seem to have been run according to this
|
||||
reporter.
|
||||
"""
|
||||
)
|
||||
|
||||
def startTest(method):
|
||||
"""
|
||||
Report the beginning of a run of a single test method.
|
||||
|
||||
@param method: an object that is adaptable to ITestMethod
|
||||
"""
|
||||
|
||||
def stopTest(method):
|
||||
"""
|
||||
Report the status of a single test method
|
||||
|
||||
@param method: an object that is adaptable to ITestMethod
|
||||
"""
|
||||
|
||||
def addSuccess(test):
|
||||
"""
|
||||
Record that test passed.
|
||||
"""
|
||||
|
||||
def addError(test, error):
|
||||
"""
|
||||
Record that a test has raised an unexpected exception.
|
||||
|
||||
@param test: The test that has raised an error.
|
||||
@param error: The error that the test raised. It will either be a
|
||||
three-tuple in the style of C{sys.exc_info()} or a
|
||||
L{Failure<twisted.python.failure.Failure>} object.
|
||||
"""
|
||||
|
||||
def addFailure(test, failure):
|
||||
"""
|
||||
Record that a test has failed with the given failure.
|
||||
|
||||
@param test: The test that has failed.
|
||||
@param failure: The failure that the test failed with. It will
|
||||
either be a three-tuple in the style of C{sys.exc_info()}
|
||||
or a L{Failure<twisted.python.failure.Failure>} object.
|
||||
"""
|
||||
|
||||
def addExpectedFailure(test, failure, todo=None):
|
||||
"""
|
||||
Record that the given test failed, and was expected to do so.
|
||||
|
||||
In Twisted 15.5 and prior, C{todo} was a mandatory parameter.
|
||||
|
||||
@type test: L{unittest.TestCase}
|
||||
@param test: The test which this is about.
|
||||
@type failure: L{failure.Failure}
|
||||
@param failure: The error which this test failed with.
|
||||
@type todo: L{unittest.Todo}
|
||||
@param todo: The reason for the test's TODO status. If L{None}, a
|
||||
generic reason is used.
|
||||
"""
|
||||
|
||||
def addUnexpectedSuccess(test, todo=None):
|
||||
"""
|
||||
Record that the given test failed, and was expected to do so.
|
||||
|
||||
In Twisted 15.5 and prior, C{todo} was a mandatory parameter.
|
||||
|
||||
@type test: L{unittest.TestCase}
|
||||
@param test: The test which this is about.
|
||||
@type todo: L{unittest.Todo}
|
||||
@param todo: The reason for the test's TODO status. If L{None}, a
|
||||
generic reason is used.
|
||||
"""
|
||||
|
||||
def addSkip(test, reason):
|
||||
"""
|
||||
Record that a test has been skipped for the given reason.
|
||||
|
||||
@param test: The test that has been skipped.
|
||||
@param reason: An object that the test case has specified as the reason
|
||||
for skipping the test.
|
||||
"""
|
||||
|
||||
def wasSuccessful():
|
||||
"""
|
||||
Return a boolean indicating whether all test results that were reported
|
||||
to this reporter were successful or not.
|
||||
"""
|
||||
|
||||
def done():
|
||||
"""
|
||||
Called when the test run is complete.
|
||||
|
||||
This gives the result object an opportunity to display a summary of
|
||||
information to the user. Once you have called C{done} on an
|
||||
L{IReporter} object, you should assume that the L{IReporter} object is
|
||||
no longer usable.
|
||||
"""
|
||||
1
.venv/lib/python3.12/site-packages/twisted/trial/newsfragments/.gitignore
vendored
Normal file
1
.venv/lib/python3.12/site-packages/twisted/trial/newsfragments/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1289
.venv/lib/python3.12/site-packages/twisted/trial/reporter.py
Normal file
1289
.venv/lib/python3.12/site-packages/twisted/trial/reporter.py
Normal file
File diff suppressed because it is too large
Load Diff
987
.venv/lib/python3.12/site-packages/twisted/trial/runner.py
Normal file
987
.venv/lib/python3.12/site-packages/twisted/trial/runner.py
Normal file
@@ -0,0 +1,987 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_runner -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A miscellany of code used to run Trial tests.
|
||||
|
||||
Maintainer: Jonathan Lange
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TestSuite",
|
||||
"DestructiveTestSuite",
|
||||
"ErrorHolder",
|
||||
"LoggedSuite",
|
||||
"TestHolder",
|
||||
"TestLoader",
|
||||
"TrialRunner",
|
||||
"TrialSuite",
|
||||
"filenameToModule",
|
||||
"isPackage",
|
||||
"isPackageDirectory",
|
||||
"isTestCase",
|
||||
"name",
|
||||
"samefile",
|
||||
"NOT_IN_TEST",
|
||||
]
|
||||
|
||||
import doctest
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import unittest as pyunit
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from typing import Callable, Generator, List, Optional, TextIO, Type, Union
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from attrs import define
|
||||
from typing_extensions import ParamSpec, Protocol, TypeAlias, TypeGuard
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python import failure, filepath, log, modules, reflect
|
||||
from twisted.trial import unittest, util
|
||||
from twisted.trial._asyncrunner import _ForceGarbageCollectionDecorator, _iterateTests
|
||||
from twisted.trial._synctest import _logObserver
|
||||
from twisted.trial.itrial import ITestCase
|
||||
from twisted.trial.reporter import UncleanWarningsReporterWrapper, _ExitWrapper
|
||||
|
||||
# These are imported so that they remain in the public API for t.trial.runner
|
||||
from twisted.trial.unittest import TestSuite
|
||||
from . import itrial
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
class _Debugger(Protocol):
|
||||
def runcall(
|
||||
self, f: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> object:
|
||||
...
|
||||
|
||||
|
||||
def isPackage(module):
|
||||
"""Given an object return True if the object looks like a package"""
|
||||
if not isinstance(module, types.ModuleType):
|
||||
return False
|
||||
basename = os.path.splitext(os.path.basename(module.__file__))[0]
|
||||
return basename == "__init__"
|
||||
|
||||
|
||||
def isPackageDirectory(dirname):
|
||||
"""
|
||||
Is the directory at path 'dirname' a Python package directory?
|
||||
Returns the name of the __init__ file (it may have a weird extension)
|
||||
if dirname is a package directory. Otherwise, returns False
|
||||
"""
|
||||
|
||||
def _getSuffixes():
|
||||
return importlib.machinery.all_suffixes()
|
||||
|
||||
for ext in _getSuffixes():
|
||||
initFile = "__init__" + ext
|
||||
if os.path.exists(os.path.join(dirname, initFile)):
|
||||
return initFile
|
||||
return False
|
||||
|
||||
|
||||
def samefile(filename1, filename2):
|
||||
"""
|
||||
A hacky implementation of C{os.path.samefile}. Used by L{filenameToModule}
|
||||
when the platform doesn't provide C{os.path.samefile}. Do not use this.
|
||||
"""
|
||||
return os.path.abspath(filename1) == os.path.abspath(filename2)
|
||||
|
||||
|
||||
def filenameToModule(fn):
|
||||
"""
|
||||
Given a filename, do whatever possible to return a module object matching
|
||||
that file.
|
||||
|
||||
If the file in question is a module in Python path, properly import and
|
||||
return that module. Otherwise, load the source manually.
|
||||
|
||||
@param fn: A filename.
|
||||
@return: A module object.
|
||||
@raise ValueError: If C{fn} does not exist.
|
||||
"""
|
||||
oldFn = fn
|
||||
|
||||
if (3, 8) <= sys.version_info < (3, 10) and not os.path.isabs(fn):
|
||||
# module.__spec__.__file__ is supposed to be absolute in py3.8+
|
||||
# importlib.util.spec_from_file_location does this automatically from
|
||||
# 3.10+
|
||||
# This was backported to 3.8 and 3.9, but then reverted in 3.8.11 and
|
||||
# 3.9.6
|
||||
# See https://twistedmatrix.com/trac/ticket/10230
|
||||
# and https://bugs.python.org/issue44070
|
||||
fn = os.path.join(os.getcwd(), fn)
|
||||
|
||||
if not os.path.exists(fn):
|
||||
raise ValueError(f"{oldFn!r} doesn't exist")
|
||||
|
||||
moduleName = reflect.filenameToModuleName(fn)
|
||||
try:
|
||||
ret = reflect.namedAny(moduleName)
|
||||
except (ValueError, AttributeError):
|
||||
# Couldn't find module. The file 'fn' is not in PYTHONPATH
|
||||
return _importFromFile(fn, moduleName=moduleName)
|
||||
|
||||
# >=3.7 has __file__ attribute as None, previously __file__ was not present
|
||||
if getattr(ret, "__file__", None) is None:
|
||||
# This isn't a Python module in a package, so import it from a file
|
||||
return _importFromFile(fn, moduleName=moduleName)
|
||||
|
||||
# ensure that the loaded module matches the file
|
||||
retFile = os.path.splitext(ret.__file__)[0] + ".py"
|
||||
# not all platforms (e.g. win32) have os.path.samefile
|
||||
same = getattr(os.path, "samefile", samefile)
|
||||
if os.path.isfile(fn) and not same(fn, retFile):
|
||||
del sys.modules[ret.__name__]
|
||||
ret = _importFromFile(fn, moduleName=moduleName)
|
||||
return ret
|
||||
|
||||
|
||||
def _importFromFile(fn, *, moduleName):
|
||||
fn = _resolveDirectory(fn)
|
||||
if not moduleName:
|
||||
moduleName = os.path.splitext(os.path.split(fn)[-1])[0]
|
||||
if moduleName in sys.modules:
|
||||
return sys.modules[moduleName]
|
||||
|
||||
spec = importlib.util.spec_from_file_location(moduleName, fn)
|
||||
if not spec:
|
||||
raise SyntaxError(fn)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
sys.modules[moduleName] = module
|
||||
return module
|
||||
|
||||
|
||||
def _resolveDirectory(fn):
|
||||
if os.path.isdir(fn):
|
||||
initFile = isPackageDirectory(fn)
|
||||
if initFile:
|
||||
fn = os.path.join(fn, initFile)
|
||||
else:
|
||||
raise ValueError(f"{fn!r} is not a package directory")
|
||||
return fn
|
||||
|
||||
|
||||
def _getMethodNameInClass(method):
|
||||
"""
|
||||
Find the attribute name on the method's class which refers to the method.
|
||||
|
||||
For some methods, notably decorators which have not had __name__ set correctly:
|
||||
|
||||
getattr(method.im_class, method.__name__) != method
|
||||
"""
|
||||
if getattr(method.im_class, method.__name__, object()) != method:
|
||||
for alias in dir(method.im_class):
|
||||
if getattr(method.im_class, alias, object()) == method:
|
||||
return alias
|
||||
return method.__name__
|
||||
|
||||
|
||||
class DestructiveTestSuite(TestSuite):
|
||||
"""
|
||||
A test suite which remove the tests once run, to minimize memory usage.
|
||||
"""
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Almost the same as L{TestSuite.run}, but with C{self._tests} being
|
||||
empty at the end.
|
||||
"""
|
||||
while self._tests:
|
||||
if result.shouldStop:
|
||||
break
|
||||
test = self._tests.pop(0)
|
||||
test(result)
|
||||
return result
|
||||
|
||||
|
||||
# When an error occurs outside of any test, the user will see this string
|
||||
# in place of a test's name.
|
||||
NOT_IN_TEST = "<not in test>"
|
||||
|
||||
|
||||
class LoggedSuite(TestSuite):
|
||||
"""
|
||||
Any errors logged in this suite will be reported to the L{TestResult}
|
||||
object.
|
||||
"""
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Run the suite, storing all errors in C{result}. If an error is logged
|
||||
while no tests are running, then it will be added as an error to
|
||||
C{result}.
|
||||
|
||||
@param result: A L{TestResult} object.
|
||||
"""
|
||||
observer = _logObserver
|
||||
observer._add()
|
||||
super().run(result)
|
||||
observer._remove()
|
||||
for error in observer.getErrors():
|
||||
result.addError(TestHolder(NOT_IN_TEST), error)
|
||||
observer.flushErrors()
|
||||
|
||||
|
||||
class TrialSuite(TestSuite):
|
||||
"""
|
||||
Suite to wrap around every single test in a C{trial} run. Used internally
|
||||
by Trial to set up things necessary for Trial tests to work, regardless of
|
||||
what context they are run in.
|
||||
"""
|
||||
|
||||
def __init__(self, tests=(), forceGarbageCollection=False):
|
||||
if forceGarbageCollection:
|
||||
newTests = []
|
||||
for test in tests:
|
||||
test = unittest.decorate(test, _ForceGarbageCollectionDecorator)
|
||||
newTests.append(test)
|
||||
tests = newTests
|
||||
suite = LoggedSuite(tests)
|
||||
super().__init__([suite])
|
||||
|
||||
def _bail(self):
|
||||
from twisted.internet import reactor
|
||||
|
||||
d = defer.Deferred()
|
||||
reactor.addSystemEventTrigger("after", "shutdown", lambda: d.callback(None))
|
||||
reactor.fireSystemEvent("shutdown") # radix's suggestion
|
||||
# As long as TestCase does crap stuff with the reactor we need to
|
||||
# manually shutdown the reactor here, and that requires util.wait
|
||||
# :(
|
||||
# so that the shutdown event completes
|
||||
unittest.TestCase("mktemp")._wait(d)
|
||||
|
||||
def run(self, result):
|
||||
try:
|
||||
TestSuite.run(self, result)
|
||||
finally:
|
||||
self._bail()
|
||||
|
||||
|
||||
_Loadable: TypeAlias = Union[
|
||||
modules.PythonAttribute,
|
||||
modules.PythonModule,
|
||||
pyunit.TestCase,
|
||||
Type[pyunit.TestCase],
|
||||
]
|
||||
|
||||
|
||||
def name(thing: _Loadable) -> str:
|
||||
"""
|
||||
@param thing: an object from modules (instance of PythonModule,
|
||||
PythonAttribute), a TestCase subclass, or an instance of a TestCase.
|
||||
"""
|
||||
if isinstance(thing, pyunit.TestCase):
|
||||
return thing.id()
|
||||
|
||||
if isinstance(thing, (modules.PythonAttribute, modules.PythonModule)):
|
||||
return thing.name
|
||||
|
||||
if isTestCase(thing):
|
||||
# TestCase subclass
|
||||
return reflect.qual(thing)
|
||||
|
||||
# Based on the type of thing, this is unreachable. Maybe someone calls
|
||||
# this from un-type-checked code though. Also, even with the type
|
||||
# information, mypy fails to determine this is unreachable and complains
|
||||
# about a missing return without _something_ here.
|
||||
raise TypeError(f"Cannot name {thing!r}")
|
||||
|
||||
|
||||
def isTestCase(obj: type) -> TypeGuard[Type[pyunit.TestCase]]:
|
||||
"""
|
||||
@return: C{True} if C{obj} is a class that contains test cases, C{False}
|
||||
otherwise. Used to find all the tests in a module.
|
||||
"""
|
||||
try:
|
||||
return issubclass(obj, pyunit.TestCase)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
@implementer(ITestCase)
|
||||
class TestHolder:
|
||||
"""
|
||||
Placeholder for a L{TestCase} inside a reporter. As far as a L{TestResult}
|
||||
is concerned, this looks exactly like a unit test.
|
||||
"""
|
||||
|
||||
failureException = None
|
||||
|
||||
def __init__(self, description):
|
||||
"""
|
||||
@param description: A string to be displayed L{TestResult}.
|
||||
"""
|
||||
self.description = description
|
||||
|
||||
def __call__(self, result):
|
||||
return self.run(result)
|
||||
|
||||
def id(self):
|
||||
return self.description
|
||||
|
||||
def countTestCases(self):
|
||||
return 0
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
This test is just a placeholder. Run the test successfully.
|
||||
|
||||
@param result: The C{TestResult} to store the results in.
|
||||
@type result: L{twisted.trial.itrial.IReporter}.
|
||||
"""
|
||||
result.startTest(self)
|
||||
result.addSuccess(self)
|
||||
result.stopTest(self)
|
||||
|
||||
def shortDescription(self):
|
||||
return self.description
|
||||
|
||||
|
||||
class ErrorHolder(TestHolder):
|
||||
"""
|
||||
Used to insert arbitrary errors into a test suite run. Provides enough
|
||||
methods to look like a C{TestCase}, however, when it is run, it simply adds
|
||||
an error to the C{TestResult}. The most common use-case is for when a
|
||||
module fails to import.
|
||||
"""
|
||||
|
||||
def __init__(self, description, error):
|
||||
"""
|
||||
@param description: A string used by C{TestResult}s to identify this
|
||||
error. Generally, this is the name of a module that failed to import.
|
||||
|
||||
@param error: The error to be added to the result. Can be an `exc_info`
|
||||
tuple or a L{twisted.python.failure.Failure}.
|
||||
"""
|
||||
super().__init__(description)
|
||||
self.error = util.excInfoOrFailureToExcInfo(error)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<ErrorHolder description={!r} error={!r}>".format(
|
||||
self.description,
|
||||
self.error[1],
|
||||
)
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Run the test, reporting the error.
|
||||
|
||||
@param result: The C{TestResult} to store the results in.
|
||||
@type result: L{twisted.trial.itrial.IReporter}.
|
||||
"""
|
||||
result.startTest(self)
|
||||
result.addError(self, self.error)
|
||||
result.stopTest(self)
|
||||
|
||||
|
||||
@define
|
||||
class TestLoader:
|
||||
"""
|
||||
I find tests inside function, modules, files -- whatever -- then return
|
||||
them wrapped inside a Test (either a L{TestSuite} or a L{TestCase}).
|
||||
|
||||
@ivar methodPrefix: A string prefix. C{TestLoader} will assume that all the
|
||||
methods in a class that begin with C{methodPrefix} are test cases.
|
||||
|
||||
@ivar modulePrefix: A string prefix. Every module in a package that begins
|
||||
with C{modulePrefix} is considered a module full of tests.
|
||||
|
||||
@ivar forceGarbageCollection: A flag applied to each C{TestCase} loaded.
|
||||
See L{unittest.TestCase} for more information.
|
||||
|
||||
@ivar sorter: A key function used to sort C{TestCase}s, test classes,
|
||||
modules and packages.
|
||||
|
||||
@ivar suiteFactory: A callable which is passed a list of tests (which
|
||||
themselves may be suites of tests). Must return a test suite.
|
||||
"""
|
||||
|
||||
methodPrefix = "test"
|
||||
modulePrefix = "test_"
|
||||
|
||||
suiteFactory: Type[TestSuite] = TestSuite
|
||||
sorter: Callable[[_Loadable], object] = name
|
||||
|
||||
def sort(self, xs):
|
||||
"""
|
||||
Sort the given things using L{sorter}.
|
||||
|
||||
@param xs: A list of test cases, class or modules.
|
||||
"""
|
||||
return sorted(xs, key=self.sorter)
|
||||
|
||||
def findTestClasses(self, module):
|
||||
"""Given a module, return all Trial test classes"""
|
||||
classes = []
|
||||
for name, val in inspect.getmembers(module):
|
||||
if isTestCase(val):
|
||||
classes.append(val)
|
||||
return self.sort(classes)
|
||||
|
||||
def findByName(self, _name, recurse=False):
|
||||
"""
|
||||
Find and load tests, given C{name}.
|
||||
|
||||
@param _name: The qualified name of the thing to load.
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect
|
||||
modules in the package itself.
|
||||
|
||||
@return: If C{name} is a filename, return the module. If C{name} is a
|
||||
fully-qualified Python name, return the object it refers to.
|
||||
"""
|
||||
if os.sep in _name:
|
||||
# It's a file, try and get the module name for this file.
|
||||
name = reflect.filenameToModuleName(_name)
|
||||
|
||||
try:
|
||||
# Try and import it, if it's on the path.
|
||||
# CAVEAT: If you have two twisteds, and you try and import the
|
||||
# one NOT on your path, it'll load the one on your path. But
|
||||
# that's silly, nobody should do that, and existing Trial does
|
||||
# that anyway.
|
||||
__import__(name)
|
||||
except ImportError:
|
||||
# If we can't import it, look for one NOT on the path.
|
||||
return self.loadFile(_name, recurse=recurse)
|
||||
|
||||
else:
|
||||
name = _name
|
||||
|
||||
obj = parent = remaining = None
|
||||
|
||||
for searchName, remainingName in _qualNameWalker(name):
|
||||
# Walk down the qualified name, trying to import a module. For
|
||||
# example, `twisted.test.test_paths.FilePathTests` would try
|
||||
# the full qualified name, then just up to test_paths, and then
|
||||
# just up to test, and so forth.
|
||||
# This gets us the highest level thing which is a module.
|
||||
try:
|
||||
obj = reflect.namedModule(searchName)
|
||||
# If we reach here, we have successfully found a module.
|
||||
# obj will be the module, and remaining will be the remaining
|
||||
# part of the qualified name.
|
||||
remaining = remainingName
|
||||
break
|
||||
|
||||
except ImportError:
|
||||
# Check to see where the ImportError happened. If it happened
|
||||
# in this file, ignore it.
|
||||
tb = sys.exc_info()[2]
|
||||
|
||||
# Walk down to the deepest frame, where it actually happened.
|
||||
while tb.tb_next is not None:
|
||||
tb = tb.tb_next
|
||||
|
||||
# Get the filename that the ImportError originated in.
|
||||
filenameWhereHappened = tb.tb_frame.f_code.co_filename
|
||||
|
||||
# If it originated in the reflect file, then it's because it
|
||||
# doesn't exist. If it originates elsewhere, it's because an
|
||||
# ImportError happened in a module that does exist.
|
||||
if filenameWhereHappened != reflect.__file__:
|
||||
raise
|
||||
|
||||
if remaining == "":
|
||||
raise reflect.ModuleNotFound(f"The module {name} does not exist.")
|
||||
|
||||
if obj is None:
|
||||
# If it's none here, we didn't get to import anything.
|
||||
# Try something drastic.
|
||||
obj = reflect.namedAny(name)
|
||||
remaining = name.split(".")[len(".".split(obj.__name__)) + 1 :]
|
||||
|
||||
try:
|
||||
for part in remaining:
|
||||
# Walk down the remaining modules. Hold on to the parent for
|
||||
# methods, as on Python 3, you can no longer get the parent
|
||||
# class from just holding onto the method.
|
||||
parent, obj = obj, getattr(obj, part)
|
||||
except AttributeError:
|
||||
raise AttributeError(f"{name} does not exist.")
|
||||
|
||||
return self.loadAnything(
|
||||
obj, parent=parent, qualName=remaining, recurse=recurse
|
||||
)
|
||||
|
||||
def loadModule(self, module):
|
||||
"""
|
||||
Return a test suite with all the tests from a module.
|
||||
|
||||
Included are TestCase subclasses and doctests listed in the module's
|
||||
__doctests__ module. If that's not good for you, put a function named
|
||||
either C{testSuite} or C{test_suite} in your module that returns a
|
||||
TestSuite, and I'll use the results of that instead.
|
||||
|
||||
If C{testSuite} and C{test_suite} are both present, then I'll use
|
||||
C{testSuite}.
|
||||
"""
|
||||
## XXX - should I add an optional parameter to disable the check for
|
||||
## a custom suite.
|
||||
## OR, should I add another method
|
||||
if not isinstance(module, types.ModuleType):
|
||||
raise TypeError(f"{module!r} is not a module")
|
||||
if hasattr(module, "testSuite"):
|
||||
return module.testSuite()
|
||||
elif hasattr(module, "test_suite"):
|
||||
return module.test_suite()
|
||||
suite = self.suiteFactory()
|
||||
for testClass in self.findTestClasses(module):
|
||||
suite.addTest(self.loadClass(testClass))
|
||||
if not hasattr(module, "__doctests__"):
|
||||
return suite
|
||||
docSuite = self.suiteFactory()
|
||||
for docTest in module.__doctests__:
|
||||
docSuite.addTest(self.loadDoctests(docTest))
|
||||
return self.suiteFactory([suite, docSuite])
|
||||
|
||||
loadTestsFromModule = loadModule
|
||||
|
||||
def loadClass(self, klass):
|
||||
"""
|
||||
Given a class which contains test cases, return a list of L{TestCase}s.
|
||||
|
||||
@param klass: The class to load tests from.
|
||||
"""
|
||||
if not isinstance(klass, type):
|
||||
raise TypeError(f"{klass!r} is not a class")
|
||||
if not isTestCase(klass):
|
||||
raise ValueError(f"{klass!r} is not a test case")
|
||||
names = self.getTestCaseNames(klass)
|
||||
tests = self.sort(
|
||||
[self._makeCase(klass, self.methodPrefix + name) for name in names]
|
||||
)
|
||||
return self.suiteFactory(tests)
|
||||
|
||||
loadTestsFromTestCase = loadClass
|
||||
|
||||
def getTestCaseNames(self, klass):
|
||||
"""
|
||||
Given a class that contains C{TestCase}s, return a list of names of
|
||||
methods that probably contain tests.
|
||||
"""
|
||||
return reflect.prefixedMethodNames(klass, self.methodPrefix)
|
||||
|
||||
def _makeCase(self, klass, methodName):
|
||||
return klass(methodName)
|
||||
|
||||
def loadPackage(self, package, recurse=False):
|
||||
"""
|
||||
Load tests from a module object representing a package, and return a
|
||||
TestSuite containing those tests.
|
||||
|
||||
Tests are only loaded from modules whose name begins with 'test_'
|
||||
(or whatever C{modulePrefix} is set to).
|
||||
|
||||
@param package: a types.ModuleType object (or reasonable facsimile
|
||||
obtained by importing) which may contain tests.
|
||||
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect modules
|
||||
in the package itself.
|
||||
|
||||
@raise TypeError: If C{package} is not a package.
|
||||
|
||||
@return: a TestSuite created with my suiteFactory, containing all the
|
||||
tests.
|
||||
"""
|
||||
if not isPackage(package):
|
||||
raise TypeError(f"{package!r} is not a package")
|
||||
pkgobj = modules.getModule(package.__name__)
|
||||
if recurse:
|
||||
discovery = pkgobj.walkModules()
|
||||
else:
|
||||
discovery = pkgobj.iterModules()
|
||||
discovered = []
|
||||
for disco in discovery:
|
||||
if disco.name.split(".")[-1].startswith(self.modulePrefix):
|
||||
discovered.append(disco)
|
||||
suite = self.suiteFactory()
|
||||
for modinfo in self.sort(discovered):
|
||||
try:
|
||||
module = modinfo.load()
|
||||
except BaseException:
|
||||
thingToAdd = ErrorHolder(modinfo.name, failure.Failure())
|
||||
else:
|
||||
thingToAdd = self.loadModule(module)
|
||||
suite.addTest(thingToAdd)
|
||||
return suite
|
||||
|
||||
def loadDoctests(self, module):
|
||||
"""
|
||||
Return a suite of tests for all the doctests defined in C{module}.
|
||||
|
||||
@param module: A module object or a module name.
|
||||
"""
|
||||
if isinstance(module, str):
|
||||
try:
|
||||
module = reflect.namedAny(module)
|
||||
except BaseException:
|
||||
return ErrorHolder(module, failure.Failure())
|
||||
if not inspect.ismodule(module):
|
||||
warnings.warn("trial only supports doctesting modules")
|
||||
return
|
||||
extraArgs = {}
|
||||
|
||||
# Work around Python issue2604: DocTestCase.tearDown clobbers globs
|
||||
def saveGlobals(test):
|
||||
"""
|
||||
Save C{test.globs} and replace it with a copy so that if
|
||||
necessary, the original will be available for the next test
|
||||
run.
|
||||
"""
|
||||
test._savedGlobals = getattr(test, "_savedGlobals", test.globs)
|
||||
test.globs = test._savedGlobals.copy()
|
||||
|
||||
extraArgs["setUp"] = saveGlobals
|
||||
return doctest.DocTestSuite(module, **extraArgs)
|
||||
|
||||
def loadAnything(self, obj, recurse=False, parent=None, qualName=None):
|
||||
"""
|
||||
Load absolutely anything (as long as that anything is a module,
|
||||
package, class, or method (with associated parent class and qualname).
|
||||
|
||||
@param obj: The object to load.
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect
|
||||
modules in the package itself.
|
||||
@param parent: If C{obj} is a method, this is the parent class of the
|
||||
method. C{qualName} is also required.
|
||||
@param qualName: If C{obj} is a method, this a list containing is the
|
||||
qualified name of the method. C{parent} is also required.
|
||||
|
||||
@return: A C{TestCase} or C{TestSuite}.
|
||||
"""
|
||||
if isinstance(obj, types.ModuleType):
|
||||
# It looks like a module
|
||||
if isPackage(obj):
|
||||
# It's a package, so recurse down it.
|
||||
return self.loadPackage(obj, recurse=recurse)
|
||||
# Otherwise get all the tests in the module.
|
||||
return self.loadTestsFromModule(obj)
|
||||
elif isinstance(obj, type) and issubclass(obj, pyunit.TestCase):
|
||||
# We've found a raw test case, get the tests from it.
|
||||
return self.loadTestsFromTestCase(obj)
|
||||
elif (
|
||||
isinstance(obj, types.FunctionType)
|
||||
and isinstance(parent, type)
|
||||
and issubclass(parent, pyunit.TestCase)
|
||||
):
|
||||
# We've found a method, and its parent is a TestCase. Instantiate
|
||||
# it with the name of the method we want.
|
||||
name = qualName[-1]
|
||||
inst = parent(name)
|
||||
|
||||
# Sanity check to make sure that the method we have got from the
|
||||
# test case is the same one as was passed in. This doesn't actually
|
||||
# use the function we passed in, because reasons.
|
||||
assert getattr(inst, inst._testMethodName).__func__ == obj
|
||||
|
||||
return inst
|
||||
elif isinstance(obj, TestSuite):
|
||||
# We've found a test suite.
|
||||
return obj
|
||||
else:
|
||||
raise TypeError(f"don't know how to make test from: {obj}")
|
||||
|
||||
def loadByName(self, name, recurse=False):
|
||||
"""
|
||||
Load some tests by name.
|
||||
|
||||
@param name: The qualified name for the test to load.
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect
|
||||
modules in the package itself.
|
||||
"""
|
||||
try:
|
||||
return self.suiteFactory([self.findByName(name, recurse=recurse)])
|
||||
except BaseException:
|
||||
return self.suiteFactory([ErrorHolder(name, failure.Failure())])
|
||||
|
||||
loadTestsFromName = loadByName
|
||||
|
||||
def loadByNames(self, names: List[str], recurse: bool = False) -> TestSuite:
|
||||
"""
|
||||
Load some tests by a list of names.
|
||||
|
||||
@param names: A L{list} of qualified names.
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect
|
||||
modules in the package itself.
|
||||
"""
|
||||
things = []
|
||||
errors = []
|
||||
for name in names:
|
||||
try:
|
||||
things.append(self.loadByName(name, recurse=recurse))
|
||||
except BaseException:
|
||||
errors.append(ErrorHolder(name, failure.Failure()))
|
||||
things.extend(errors)
|
||||
return self.suiteFactory(self._uniqueTests(things))
|
||||
|
||||
def _uniqueTests(self, things):
|
||||
"""
|
||||
Gather unique suite objects from loaded things. This will guarantee
|
||||
uniqueness of inherited methods on TestCases which would otherwise hash
|
||||
to same value and collapse to one test unexpectedly if using simpler
|
||||
means: e.g. set().
|
||||
"""
|
||||
seen = set()
|
||||
for testthing in things:
|
||||
testthings = testthing._tests
|
||||
for thing in testthings:
|
||||
# This is horrible.
|
||||
if str(thing) not in seen:
|
||||
yield thing
|
||||
seen.add(str(thing))
|
||||
|
||||
def loadFile(self, fileName, recurse=False):
|
||||
"""
|
||||
Load a file, and then the tests in that file.
|
||||
|
||||
@param fileName: The file name to load.
|
||||
@param recurse: A boolean. If True, inspect modules within packages
|
||||
within the given package (and so on), otherwise, only inspect
|
||||
modules in the package itself.
|
||||
"""
|
||||
name = reflect.filenameToModuleName(fileName)
|
||||
try:
|
||||
module = SourceFileLoader(name, fileName).load_module()
|
||||
return self.loadAnything(module, recurse=recurse)
|
||||
except OSError:
|
||||
raise ValueError(f"{fileName} is not a Python file.")
|
||||
|
||||
|
||||
def _qualNameWalker(qualName):
|
||||
"""
|
||||
Given a Python qualified name, this function yields a 2-tuple of the most
|
||||
specific qualified name first, followed by the next-most-specific qualified
|
||||
name, and so on, paired with the remainder of the qualified name.
|
||||
|
||||
@param qualName: A Python qualified name.
|
||||
@type qualName: L{str}
|
||||
"""
|
||||
# Yield what we were just given
|
||||
yield (qualName, [])
|
||||
|
||||
# If they want more, split the qualified name up
|
||||
qualParts = qualName.split(".")
|
||||
|
||||
for index in range(1, len(qualParts)):
|
||||
# This code here will produce, from the example walker.texas.ranger:
|
||||
# (walker.texas, ["ranger"])
|
||||
# (walker, ["texas", "ranger"])
|
||||
yield (".".join(qualParts[:-index]), qualParts[-index:])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _testDirectory(workingDirectory: str) -> Generator[None, None, None]:
|
||||
"""
|
||||
A context manager which obtains a lock on a trial working directory
|
||||
and enters (L{os.chdir}) it and then reverses these things.
|
||||
|
||||
@param workingDirectory: A pattern for the basename of the working
|
||||
directory to acquire.
|
||||
"""
|
||||
currentDir = os.getcwd()
|
||||
base = filepath.FilePath(workingDirectory)
|
||||
testdir, testDirLock = util._unusedTestDirectory(base)
|
||||
os.chdir(testdir.path)
|
||||
|
||||
yield
|
||||
|
||||
os.chdir(currentDir)
|
||||
testDirLock.unlock()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _logFile(logfile: str) -> Generator[None, None, None]:
|
||||
"""
|
||||
A context manager which adds a log observer and then removes it.
|
||||
|
||||
@param logfile: C{"-"} f or stdout logging, otherwise the path to a log
|
||||
file to which to write.
|
||||
"""
|
||||
if logfile == "-":
|
||||
logFile = sys.stdout
|
||||
else:
|
||||
logFile = util.openTestLog(filepath.FilePath(logfile))
|
||||
|
||||
logFileObserver = log.FileLogObserver(logFile)
|
||||
observerFunction = logFileObserver.emit
|
||||
log.startLoggingWithObserver(observerFunction, 0)
|
||||
|
||||
yield
|
||||
|
||||
log.removeObserver(observerFunction)
|
||||
logFile.close()
|
||||
|
||||
|
||||
class _Runner(Protocol):
|
||||
stream: TextIO
|
||||
|
||||
def run(self, test: Union[pyunit.TestCase, pyunit.TestSuite]) -> itrial.IReporter:
|
||||
...
|
||||
|
||||
def runUntilFailure(
|
||||
self, test: Union[pyunit.TestCase, pyunit.TestSuite]
|
||||
) -> itrial.IReporter:
|
||||
...
|
||||
|
||||
|
||||
@define
|
||||
class TrialRunner:
|
||||
"""
|
||||
A specialised runner that the trial front end uses.
|
||||
|
||||
@ivar reporterFactory: A callable to create a reporter to use.
|
||||
|
||||
@ivar mode: Either C{None} for a normal test run, L{TrialRunner.DEBUG} for
|
||||
a run in the debugger, or L{TrialRunner.DRY_RUN} to collect and report
|
||||
the tests but not call any of them.
|
||||
|
||||
@ivar logfile: The path to the file to write the test run log.
|
||||
|
||||
@ivar stream: The file to report results to.
|
||||
|
||||
@ivar profile: C{True} to run the tests with a profiler enabled.
|
||||
|
||||
@ivar _tracebackFormat: A format name to use with L{Failure} for reporting
|
||||
failures.
|
||||
|
||||
@ivar _realTimeErrors: C{True} if errors should be reported as they
|
||||
happen. C{False} if they should only be reported at the end of the
|
||||
test run in the summary.
|
||||
|
||||
@ivar uncleanWarnings: C{True} to report dirty reactor errors as warnings,
|
||||
C{False} to report them as test-failing errors.
|
||||
|
||||
@ivar workingDirectory: A path template to a directory which will be the
|
||||
process's working directory while the tests are running.
|
||||
|
||||
@ivar _forceGarbageCollection: C{True} to perform a full garbage
|
||||
collection at least after each test. C{False} to let garbage
|
||||
collection run only when it normally would.
|
||||
|
||||
@ivar debugger: In debug mode, an object to use to launch the debugger.
|
||||
|
||||
@ivar _exitFirst: C{True} to stop after the first failed test. C{False}
|
||||
to run the whole suite.
|
||||
|
||||
@ivar log: An object to give to the reporter to use as a log publisher.
|
||||
"""
|
||||
|
||||
DEBUG = "debug"
|
||||
DRY_RUN = "dry-run"
|
||||
|
||||
reporterFactory: Callable[[TextIO, str, bool, log.LogPublisher], itrial.IReporter]
|
||||
mode: Optional[str] = None
|
||||
logfile: str = "test.log"
|
||||
stream: TextIO = sys.stdout
|
||||
profile: bool = False
|
||||
_tracebackFormat: str = "default"
|
||||
_realTimeErrors: bool = False
|
||||
uncleanWarnings: bool = False
|
||||
workingDirectory: str = "_trial_temp"
|
||||
_forceGarbageCollection: bool = False
|
||||
debugger: Optional[_Debugger] = None
|
||||
_exitFirst: bool = False
|
||||
|
||||
_log: log.LogPublisher = log # type: ignore[assignment]
|
||||
|
||||
def _makeResult(self) -> itrial.IReporter:
|
||||
reporter = self.reporterFactory(
|
||||
self.stream, self.tbformat, self.rterrors, self._log
|
||||
)
|
||||
if self._exitFirst:
|
||||
reporter = _ExitWrapper(reporter)
|
||||
if self.uncleanWarnings:
|
||||
reporter = UncleanWarningsReporterWrapper(reporter)
|
||||
return reporter
|
||||
|
||||
@property
|
||||
def tbformat(self) -> str:
|
||||
return self._tracebackFormat
|
||||
|
||||
@property
|
||||
def rterrors(self) -> bool:
|
||||
return self._realTimeErrors
|
||||
|
||||
def run(self, test: Union[pyunit.TestCase, pyunit.TestSuite]) -> itrial.IReporter:
|
||||
"""
|
||||
Run the test or suite and return a result object.
|
||||
"""
|
||||
test = unittest.decorate(test, ITestCase)
|
||||
if self.profile:
|
||||
run = util.profiled(self._runWithoutDecoration, "profile.data")
|
||||
else:
|
||||
run = self._runWithoutDecoration
|
||||
return run(test, self._forceGarbageCollection)
|
||||
|
||||
def _runWithoutDecoration(
|
||||
self,
|
||||
test: Union[pyunit.TestCase, pyunit.TestSuite],
|
||||
forceGarbageCollection: bool = False,
|
||||
) -> itrial.IReporter:
|
||||
"""
|
||||
Private helper that runs the given test but doesn't decorate it.
|
||||
"""
|
||||
result = self._makeResult()
|
||||
# decorate the suite with reactor cleanup and log starting
|
||||
# This should move out of the runner and be presumed to be
|
||||
# present
|
||||
suite = TrialSuite([test], forceGarbageCollection)
|
||||
if self.mode == self.DRY_RUN:
|
||||
for single in _iterateTests(suite):
|
||||
result.startTest(single)
|
||||
result.addSuccess(single)
|
||||
result.stopTest(single)
|
||||
else:
|
||||
if self.mode == self.DEBUG:
|
||||
assert self.debugger is not None
|
||||
run = lambda: self.debugger.runcall(suite.run, result)
|
||||
else:
|
||||
run = lambda: suite.run(result)
|
||||
|
||||
with _testDirectory(self.workingDirectory), _logFile(self.logfile):
|
||||
run()
|
||||
|
||||
result.done()
|
||||
return result
|
||||
|
||||
def runUntilFailure(
|
||||
self, test: Union[pyunit.TestCase, pyunit.TestSuite]
|
||||
) -> itrial.IReporter:
|
||||
"""
|
||||
Repeatedly run C{test} until it fails.
|
||||
"""
|
||||
count = 0
|
||||
while True:
|
||||
count += 1
|
||||
self.stream.write("Test Pass %d\n" % (count,))
|
||||
if count == 1:
|
||||
# If test is a TestSuite, run *mutates it*. So only follow
|
||||
# this code-path once! Otherwise the decorations accumulate
|
||||
# forever.
|
||||
result = self.run(test)
|
||||
else:
|
||||
result = self._runWithoutDecoration(test)
|
||||
if result.testsRun == 0:
|
||||
break
|
||||
if not result.wasSuccessful():
|
||||
break
|
||||
return result
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Unit tests for the Trial unit-testing framework.
|
||||
"""
|
||||
|
||||
from hypothesis import HealthCheck, settings
|
||||
|
||||
|
||||
def _activateHypothesisProfile() -> None:
|
||||
"""
|
||||
Load a Hypothesis profile appropriate for a Twisted test suite.
|
||||
"""
|
||||
deterministic = settings(
|
||||
# Disable the deadline. It is too hard to guarantee that a particular
|
||||
# piece of Python code will always run in less than some fixed amount
|
||||
# of time. Hardware capabilities, the OS scheduler, the Python
|
||||
# garbage collector, and other factors all combine to make substantial
|
||||
# outliers possible. Such failures are a distraction from development
|
||||
# and a hassle on continuous integration environments.
|
||||
deadline=None,
|
||||
suppress_health_check=[
|
||||
# With the same reasoning as above, disable the Hypothesis time
|
||||
# limit on data generation by example search strategies.
|
||||
HealthCheck.too_slow,
|
||||
],
|
||||
# When a developer is working on one set of changes, or continuous
|
||||
# integration system is testing them, it is disruptive for Hypothesis
|
||||
# to discover a bug in pre-existing code. This is just what
|
||||
# Hypothesis will do by default, by exploring a pseudo-randomly
|
||||
# different set of examples each time. Such failures are a
|
||||
# distraction from development and a hassle in continuous integration
|
||||
# environments.
|
||||
derandomize=True,
|
||||
)
|
||||
|
||||
settings.register_profile("twisted_trial_test_profile_deterministic", deterministic)
|
||||
settings.load_profile("twisted_trial_test_profile_deterministic")
|
||||
|
||||
|
||||
_activateHypothesisProfile()
|
||||
233
.venv/lib/python3.12/site-packages/twisted/trial/test/detests.py
Normal file
233
.venv/lib/python3.12/site-packages/twisted/trial/test/detests.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for Deferred handling by L{twisted.trial.unittest.TestCase}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from twisted.internet import defer, reactor, threads
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.util import runWithWarningsSuppressed
|
||||
from twisted.trial import unittest
|
||||
from twisted.trial.util import suppress as SUPPRESS
|
||||
|
||||
|
||||
class DeferredSetUpOK(unittest.TestCase):
|
||||
def setUp(self):
|
||||
d = defer.succeed("value")
|
||||
d.addCallback(self._cb_setUpCalled)
|
||||
return d
|
||||
|
||||
def _cb_setUpCalled(self, ignored):
|
||||
self._setUpCalled = True
|
||||
|
||||
def test_ok(self):
|
||||
self.assertTrue(self._setUpCalled)
|
||||
|
||||
|
||||
class DeferredSetUpFail(unittest.TestCase):
|
||||
testCalled = False
|
||||
|
||||
def setUp(self):
|
||||
return defer.fail(unittest.FailTest("i fail"))
|
||||
|
||||
def test_ok(self):
|
||||
DeferredSetUpFail.testCalled = True
|
||||
self.fail("I should not get called")
|
||||
|
||||
|
||||
class DeferredSetUpCallbackFail(unittest.TestCase):
|
||||
testCalled = False
|
||||
|
||||
def setUp(self):
|
||||
d = defer.succeed("value")
|
||||
d.addCallback(self._cb_setUpCalled)
|
||||
return d
|
||||
|
||||
def _cb_setUpCalled(self, ignored):
|
||||
self.fail("deliberate failure")
|
||||
|
||||
def test_ok(self):
|
||||
DeferredSetUpCallbackFail.testCalled = True
|
||||
|
||||
|
||||
class DeferredSetUpError(unittest.TestCase):
|
||||
testCalled = False
|
||||
|
||||
def setUp(self):
|
||||
return defer.fail(RuntimeError("deliberate error"))
|
||||
|
||||
def test_ok(self):
|
||||
DeferredSetUpError.testCalled = True
|
||||
|
||||
|
||||
class DeferredSetUpNeverFire(unittest.TestCase):
|
||||
testCalled = False
|
||||
|
||||
def setUp(self):
|
||||
return defer.Deferred()
|
||||
|
||||
def test_ok(self):
|
||||
DeferredSetUpNeverFire.testCalled = True
|
||||
|
||||
|
||||
class DeferredSetUpSkip(unittest.TestCase):
|
||||
testCalled = False
|
||||
|
||||
def setUp(self):
|
||||
d = defer.succeed("value")
|
||||
d.addCallback(self._cb1)
|
||||
return d
|
||||
|
||||
def _cb1(self, ignored):
|
||||
raise unittest.SkipTest("skip me")
|
||||
|
||||
def test_ok(self):
|
||||
DeferredSetUpSkip.testCalled = True
|
||||
|
||||
|
||||
class DeferredTests(unittest.TestCase):
|
||||
touched = False
|
||||
|
||||
def _cb_fail(self, reason):
|
||||
self.fail(reason)
|
||||
|
||||
def _cb_error(self, reason):
|
||||
raise RuntimeError(reason)
|
||||
|
||||
def _cb_skip(self, reason):
|
||||
raise unittest.SkipTest(reason)
|
||||
|
||||
def _touchClass(self, ignored):
|
||||
self.__class__.touched = True
|
||||
|
||||
def setUp(self):
|
||||
self.__class__.touched = False
|
||||
|
||||
def test_pass(self):
|
||||
return defer.succeed("success")
|
||||
|
||||
def test_passGenerated(self):
|
||||
self._touchClass(None)
|
||||
yield None
|
||||
|
||||
test_passGenerated = runWithWarningsSuppressed(
|
||||
[
|
||||
SUPPRESS(
|
||||
message="twisted.internet.defer.deferredGenerator was " "deprecated"
|
||||
)
|
||||
],
|
||||
defer.deferredGenerator,
|
||||
test_passGenerated,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_passInlineCallbacks(self):
|
||||
"""
|
||||
Test case that is decorated with L{defer.inlineCallbacks}.
|
||||
"""
|
||||
self._touchClass(None)
|
||||
yield None
|
||||
|
||||
def test_fail(self):
|
||||
return defer.fail(self.failureException("I fail"))
|
||||
|
||||
def test_failureInCallback(self):
|
||||
d = defer.succeed("fail")
|
||||
d.addCallback(self._cb_fail)
|
||||
return d
|
||||
|
||||
def test_errorInCallback(self):
|
||||
d = defer.succeed("error")
|
||||
d.addCallback(self._cb_error)
|
||||
return d
|
||||
|
||||
def test_skip(self):
|
||||
d = defer.succeed("skip")
|
||||
d.addCallback(self._cb_skip)
|
||||
d.addCallback(self._touchClass)
|
||||
return d
|
||||
|
||||
def test_thread(self):
|
||||
return threads.deferToThread(lambda: None)
|
||||
|
||||
def test_expectedFailure(self):
|
||||
d = defer.succeed("todo")
|
||||
d.addCallback(self._cb_error)
|
||||
return d
|
||||
|
||||
test_expectedFailure.todo = "Expected failure" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class TimeoutTests(unittest.TestCase):
|
||||
timedOut: Failure | None = None
|
||||
|
||||
def test_pass(self):
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(0, d.callback, "hoorj!")
|
||||
return d
|
||||
|
||||
test_pass.timeout = 2 # type: ignore[attr-defined]
|
||||
|
||||
def test_passDefault(self):
|
||||
# test default timeout
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(0, d.callback, "hoorj!")
|
||||
return d
|
||||
|
||||
def test_timeout(self):
|
||||
return defer.Deferred()
|
||||
|
||||
test_timeout.timeout = 0.1 # type: ignore[attr-defined]
|
||||
|
||||
def test_timeoutZero(self):
|
||||
return defer.Deferred()
|
||||
|
||||
test_timeoutZero.timeout = 0 # type: ignore[attr-defined]
|
||||
|
||||
def test_expectedFailure(self):
|
||||
return defer.Deferred()
|
||||
|
||||
test_expectedFailure.timeout = 0.1 # type: ignore[attr-defined]
|
||||
test_expectedFailure.todo = "i will get it right, eventually" # type: ignore[attr-defined]
|
||||
|
||||
def test_skip(self):
|
||||
return defer.Deferred()
|
||||
|
||||
test_skip.timeout = 0.1 # type: ignore[attr-defined]
|
||||
test_skip.skip = "i will get it right, eventually" # type: ignore[attr-defined]
|
||||
|
||||
def test_errorPropagation(self):
|
||||
def timedOut(err):
|
||||
self.__class__.timedOut = err
|
||||
return err
|
||||
|
||||
d = defer.Deferred()
|
||||
d.addErrback(timedOut)
|
||||
return d
|
||||
|
||||
test_errorPropagation.timeout = 0.1 # type: ignore[attr-defined]
|
||||
|
||||
def test_calledButNeverCallback(self):
|
||||
d = defer.Deferred()
|
||||
|
||||
def neverFire(r):
|
||||
return defer.Deferred()
|
||||
|
||||
d.addCallback(neverFire)
|
||||
d.callback(1)
|
||||
return d
|
||||
|
||||
test_calledButNeverCallback.timeout = 0.1 # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class TestClassTimeoutAttribute(unittest.TestCase):
|
||||
timeout = 0.2
|
||||
|
||||
def setUp(self):
|
||||
self.d = defer.Deferred()
|
||||
|
||||
def testMethod(self):
|
||||
self.methodCalled = True
|
||||
return self.d
|
||||
@@ -0,0 +1,261 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_tests -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Definitions of test cases with various interesting error-related behaviors, to
|
||||
be used by test modules to exercise different features of trial's test runner.
|
||||
|
||||
See the L{twisted.trial.test.test_tests} module docstring for details about how
|
||||
this code is arranged.
|
||||
|
||||
Some of these tests are also used by L{twisted.trial._dist.test}.
|
||||
"""
|
||||
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.task import deferLater
|
||||
from twisted.trial import unittest, util
|
||||
|
||||
|
||||
class FoolishError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LargeError(Exception):
|
||||
"""
|
||||
An exception which has a string representation of at least a specified
|
||||
number of characters.
|
||||
"""
|
||||
|
||||
def __init__(self, minSize: int) -> None:
|
||||
Exception.__init__(self)
|
||||
self.minSize = minSize
|
||||
|
||||
def __str__(self):
|
||||
large = "x" * self.minSize
|
||||
return f"LargeError<I fail: {large}>"
|
||||
|
||||
|
||||
class FailureInSetUpMixin:
|
||||
def setUp(self):
|
||||
raise FoolishError("I am a broken setUp method")
|
||||
|
||||
def test_noop(self):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousTestFailureInSetUp(FailureInSetUpMixin, unittest.SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestFailureInSetUp(FailureInSetUpMixin, unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class FailureInTearDownMixin:
|
||||
def tearDown(self):
|
||||
raise FoolishError("I am a broken tearDown method")
|
||||
|
||||
def test_noop(self):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousTestFailureInTearDown(
|
||||
FailureInTearDownMixin, unittest.SynchronousTestCase
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestFailureInTearDown(FailureInTearDownMixin, unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class FailureButTearDownRunsMixin:
|
||||
"""
|
||||
A test fails, but its L{tearDown} still runs.
|
||||
"""
|
||||
|
||||
tornDown = False
|
||||
|
||||
def tearDown(self):
|
||||
self.tornDown = True
|
||||
|
||||
def test_fails(self):
|
||||
"""
|
||||
A test that fails.
|
||||
"""
|
||||
raise FoolishError("I am a broken test")
|
||||
|
||||
|
||||
class SynchronousTestFailureButTearDownRuns(
|
||||
FailureButTearDownRunsMixin, unittest.SynchronousTestCase
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestFailureButTearDownRuns(
|
||||
FailureButTearDownRunsMixin, unittest.TestCase
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TestRegularFail(unittest.SynchronousTestCase):
|
||||
def test_fail(self):
|
||||
self.fail("I fail")
|
||||
|
||||
def test_subfail(self):
|
||||
self.subroutine()
|
||||
|
||||
def subroutine(self):
|
||||
self.fail("I fail inside")
|
||||
|
||||
|
||||
class TestAsynchronousFail(unittest.TestCase):
|
||||
"""
|
||||
Test failures for L{unittest.TestCase} based classes.
|
||||
"""
|
||||
|
||||
text = "I fail"
|
||||
|
||||
def test_fail(self) -> defer.Deferred[None]:
|
||||
"""
|
||||
A test which fails in the callback of the returned L{defer.Deferred}.
|
||||
"""
|
||||
return deferLater(reactor, 0, self.fail, "I fail later") # type: ignore[arg-type]
|
||||
|
||||
def test_failGreaterThan64k(self) -> defer.Deferred[None]:
|
||||
"""
|
||||
A test which fails in the callback of the returned L{defer.Deferred}
|
||||
with a very long string.
|
||||
"""
|
||||
return deferLater(reactor, 0, self.fail, "I fail later: " + "x" * 2**16) # type: ignore[arg-type]
|
||||
|
||||
def test_exception(self) -> None:
|
||||
"""
|
||||
A test which raises an exception synchronously.
|
||||
"""
|
||||
raise Exception(self.text)
|
||||
|
||||
def test_exceptionGreaterThan64k(self) -> None:
|
||||
"""
|
||||
A test which raises an exception with a long string representation
|
||||
synchronously.
|
||||
"""
|
||||
raise LargeError(2**16)
|
||||
|
||||
def test_exceptionGreaterThan64kEncoded(self) -> None:
|
||||
"""
|
||||
A test which synchronously raises an exception with a long string
|
||||
representation including non-ascii content.
|
||||
"""
|
||||
# The exception text itself is not greater than 64k but SNOWMAN
|
||||
# encodes to 3 bytes with UTF-8 so the length of the UTF-8 encoding of
|
||||
# the string representation of this exception will be greater than 2
|
||||
# ** 16.
|
||||
raise Exception("\N{SNOWMAN}" * 2**15)
|
||||
|
||||
|
||||
class ErrorTest(unittest.SynchronousTestCase):
|
||||
"""
|
||||
A test case which has a L{test_foo} which will raise an error.
|
||||
|
||||
@ivar ran: boolean indicating whether L{test_foo} has been run.
|
||||
"""
|
||||
|
||||
ran = False
|
||||
|
||||
def test_foo(self):
|
||||
"""
|
||||
Set C{self.ran} to True and raise a C{ZeroDivisionError}
|
||||
"""
|
||||
self.ran = True
|
||||
1 / 0
|
||||
|
||||
|
||||
@skipIf(True, "skipping this test")
|
||||
class TestSkipTestCase(unittest.SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class DelayedCall(unittest.TestCase):
|
||||
hiddenExceptionMsg = "something blew up"
|
||||
|
||||
def go(self):
|
||||
raise RuntimeError(self.hiddenExceptionMsg)
|
||||
|
||||
def testHiddenException(self):
|
||||
"""
|
||||
What happens if an error is raised in a DelayedCall and an error is
|
||||
also raised in the test?
|
||||
|
||||
L{test_reporter.ErrorReportingTests.testHiddenException} checks that
|
||||
both errors get reported.
|
||||
|
||||
Note that this behaviour is deprecated. A B{real} test would return a
|
||||
Deferred that got triggered by the callLater. This would guarantee the
|
||||
delayed call error gets reported.
|
||||
"""
|
||||
reactor.callLater(0, self.go)
|
||||
reactor.iterate(0.01)
|
||||
self.fail("Deliberate failure to mask the hidden exception")
|
||||
|
||||
testHiddenException.suppress = [ # type: ignore[attr-defined]
|
||||
util.suppress(
|
||||
message=r"reactor\.iterate cannot be used.*", category=DeprecationWarning
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class ReactorCleanupTests(unittest.TestCase):
|
||||
def test_leftoverPendingCalls(self):
|
||||
def _():
|
||||
print("foo!")
|
||||
|
||||
reactor.callLater(10000.0, _)
|
||||
|
||||
|
||||
class SocketOpenTest(unittest.TestCase):
|
||||
def test_socketsLeftOpen(self):
|
||||
f = protocol.Factory()
|
||||
f.protocol = protocol.Protocol
|
||||
reactor.listenTCP(0, f)
|
||||
|
||||
|
||||
class TimingOutDeferred(unittest.TestCase):
|
||||
def test_alpha(self):
|
||||
pass
|
||||
|
||||
def test_deferredThatNeverFires(self):
|
||||
self.methodCalled = True
|
||||
d = defer.Deferred()
|
||||
return d
|
||||
|
||||
def test_omega(self):
|
||||
pass
|
||||
|
||||
|
||||
def unexpectedException(self):
|
||||
"""i will raise an unexpected exception...
|
||||
... *CAUSE THAT'S THE KINDA GUY I AM*
|
||||
|
||||
>>> 1/0
|
||||
"""
|
||||
|
||||
|
||||
class EventuallyFailingTestCase(unittest.SynchronousTestCase):
|
||||
"""
|
||||
A test suite that fails after it is run a few times.
|
||||
"""
|
||||
|
||||
n: int = 0
|
||||
|
||||
def test_it(self):
|
||||
"""
|
||||
Run successfully a few times and then fail forever after.
|
||||
"""
|
||||
self.n += 1
|
||||
if self.n >= 5:
|
||||
self.fail("eventually failing")
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Hamcrest matchers useful throughout the test suite.
|
||||
"""
|
||||
|
||||
from typing import IO, Callable, Optional, TypeVar
|
||||
|
||||
from hamcrest.core.base_matcher import BaseMatcher
|
||||
from hamcrest.core.description import Description
|
||||
from hamcrest.core.matcher import Matcher
|
||||
|
||||
from twisted.python.filepath import IFilePath
|
||||
from twisted.python.reflect import fullyQualifiedName
|
||||
|
||||
_A = TypeVar("_A")
|
||||
_B = TypeVar("_B")
|
||||
|
||||
|
||||
class _MatchAfter(BaseMatcher[_A]):
|
||||
"""
|
||||
The implementation of L{after}.
|
||||
|
||||
@ivar f: The function to apply.
|
||||
@ivar m: The matcher to use on the result.
|
||||
|
||||
@ivar _e: After trying to apply the function fails with an exception, the
|
||||
exception that was raised. This can later be used by
|
||||
L{describe_mismatch}.
|
||||
"""
|
||||
|
||||
def __init__(self, f: Callable[[_A], _B], m: Matcher[_B]) -> None:
|
||||
self.f = f
|
||||
self.m = m
|
||||
self._e: Optional[Exception] = None
|
||||
|
||||
def _matches(self, item: _A) -> bool:
|
||||
"""
|
||||
Apply the function and delegate matching on the result.
|
||||
"""
|
||||
try:
|
||||
transformed = self.f(item)
|
||||
except Exception as e:
|
||||
self._e = e
|
||||
return False
|
||||
else:
|
||||
return self.m.matches(transformed)
|
||||
|
||||
def describe_mismatch(self, item: _A, mismatch_description: Description) -> None:
|
||||
"""
|
||||
Describe the mismatching item or the exception that occurred while
|
||||
pre-processing it.
|
||||
|
||||
@note: Since the exception reporting here depends on mutable state it
|
||||
will only work as long as PyHamcrest calls methods in the right
|
||||
order. The PyHamcrest Matcher interface doesn't seem to allow
|
||||
implementing this functionality in a more reliable way (see the
|
||||
implementation of L{assert_that}).
|
||||
"""
|
||||
if self._e is None:
|
||||
super().describe_mismatch(item, mismatch_description)
|
||||
else:
|
||||
mismatch_description.append_text(
|
||||
f"{fullyQualifiedName(self.f)}({item!r}) raised\n"
|
||||
f"{fullyQualifiedName(self._e.__class__)}: {self._e}"
|
||||
)
|
||||
|
||||
def describe_to(self, description: Description) -> None:
|
||||
"""
|
||||
Create a text description of the match requirement.
|
||||
"""
|
||||
description.append_text(f"[after {self.f}] ")
|
||||
self.m.describe_to(description)
|
||||
|
||||
|
||||
def after(f: Callable[[_A], _B], m: Matcher[_B]) -> Matcher[_A]:
|
||||
"""
|
||||
Create a matcher which calls C{f} and uses C{m} to match the result.
|
||||
"""
|
||||
return _MatchAfter(f, m)
|
||||
|
||||
|
||||
def fileContents(m: Matcher[str], encoding: str = "utf-8") -> Matcher[IFilePath]:
|
||||
"""
|
||||
Create a matcher which matches a L{FilePath} the contents of which are
|
||||
matched by L{m}.
|
||||
"""
|
||||
|
||||
def getContent(p: IFilePath) -> str:
|
||||
f: IO[bytes]
|
||||
with p.open() as f:
|
||||
return f.read().decode(encoding)
|
||||
|
||||
return after(getContent, m)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
|
||||
|
||||
"""
|
||||
Mock test module that contains a C{test_suite} method. L{runner.TestLoader}
|
||||
should load the tests from the C{test_suite}, not from the C{Foo} C{TestCase}.
|
||||
|
||||
See {twisted.trial.test.test_loader.LoaderTest.test_loadModuleWith_test_suite}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial import runner, unittest
|
||||
|
||||
|
||||
class Foo(unittest.SynchronousTestCase):
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def test_suite():
|
||||
ts = runner.TestSuite()
|
||||
ts.name = "MyCustomSuite"
|
||||
return ts
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
|
||||
|
||||
"""
|
||||
Mock test module that contains a C{testSuite} method. L{runner.TestLoader}
|
||||
should load the tests from the C{testSuite}, not from the C{Foo} C{TestCase}.
|
||||
|
||||
See L{twisted.trial.test.test_loader.LoaderTest.test_loadModuleWith_testSuite}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial import runner, unittest
|
||||
|
||||
|
||||
class Foo(unittest.SynchronousTestCase):
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def testSuite():
|
||||
ts = runner.TestSuite()
|
||||
ts.name = "MyCustomSuite"
|
||||
return ts
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
|
||||
|
||||
"""
|
||||
Mock test module that contains both a C{test_suite} and a C{testSuite} method.
|
||||
L{runner.TestLoader} should load the tests from the C{testSuite}, not from the
|
||||
C{Foo} C{TestCase} nor from the C{test_suite} method.
|
||||
|
||||
See {twisted.trial.test.test_loader.LoaderTest.test_loadModuleWithBothCustom}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial import runner, unittest
|
||||
|
||||
|
||||
class Foo(unittest.SynchronousTestCase):
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def test_suite():
|
||||
ts = runner.TestSuite()
|
||||
ts.name = "test_suite"
|
||||
return ts
|
||||
|
||||
|
||||
def testSuite():
|
||||
ts = runner.TestSuite()
|
||||
ts.name = "testSuite"
|
||||
return ts
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# this module is a trivial class with doctests to test trial's doctest
|
||||
# support.
|
||||
|
||||
|
||||
class Counter:
|
||||
"""a simple counter object for testing trial's doctest support
|
||||
|
||||
>>> c = Counter()
|
||||
>>> c.value()
|
||||
0
|
||||
>>> c += 3
|
||||
>>> c.value()
|
||||
3
|
||||
>>> c.incr()
|
||||
>>> c.value() == 4
|
||||
True
|
||||
>>> c == 4
|
||||
True
|
||||
>>> c != 9
|
||||
True
|
||||
|
||||
"""
|
||||
|
||||
_count = 0
|
||||
|
||||
def __init__(self, initialValue=0, maxval=None):
|
||||
self._count = initialValue
|
||||
self.maxval = maxval
|
||||
|
||||
def __iadd__(self, other):
|
||||
"""add other to my value and return self
|
||||
|
||||
>>> c = Counter(100)
|
||||
>>> c += 333
|
||||
>>> c == 433
|
||||
True
|
||||
"""
|
||||
if self.maxval is not None and ((self._count + other) > self.maxval):
|
||||
raise ValueError("sorry, counter got too big")
|
||||
else:
|
||||
self._count += other
|
||||
return self
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""equality operator, compare other to my value()
|
||||
|
||||
>>> c = Counter()
|
||||
>>> c == 0
|
||||
True
|
||||
>>> c += 10
|
||||
>>> c.incr()
|
||||
>>> c == 10 # fail this test on purpose
|
||||
True
|
||||
|
||||
"""
|
||||
return self._count == other
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
"""inequality operator
|
||||
|
||||
>>> c = Counter()
|
||||
>>> c != 10
|
||||
True
|
||||
"""
|
||||
return not self.__eq__(other)
|
||||
|
||||
def incr(self):
|
||||
"""increment my value by 1
|
||||
|
||||
>>> from twisted.trial.test.mockdoctest import Counter
|
||||
>>> c = Counter(10, 11)
|
||||
>>> c.incr()
|
||||
>>> c.value() == 11
|
||||
True
|
||||
>>> c.incr()
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
File "twisted/trial/test/mockdoctest.py", line 51, in incr
|
||||
self.__iadd__(1)
|
||||
File "twisted/trial/test/mockdoctest.py", line 39, in __iadd__
|
||||
raise ValueError, "sorry, counter got too big"
|
||||
ValueError: sorry, counter got too big
|
||||
"""
|
||||
self.__iadd__(1)
|
||||
|
||||
def value(self):
|
||||
"""return this counter's value
|
||||
|
||||
>>> c = Counter(555)
|
||||
>>> c.value() == 555
|
||||
True
|
||||
"""
|
||||
return self._count
|
||||
|
||||
def unexpectedException(self):
|
||||
"""i will raise an unexpected exception...
|
||||
... *CAUSE THAT'S THE KINDA GUY I AM*
|
||||
|
||||
>>> 1/0
|
||||
"""
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.trial.test.moduleself -*-
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class Foo(unittest.SynchronousTestCase):
|
||||
def testFoo(self) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_log -*-
|
||||
|
||||
# fodder for test_script, which parses files for emacs local variable
|
||||
# declarations. This one is supposed to have:
|
||||
# test-case-name: twisted.trial.test.test_log.
|
||||
# in the first line
|
||||
# The class declaration is irrelevant
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
@@ -0,0 +1,7 @@
|
||||
# fodder for test_script, which parses files for emacs local variable
|
||||
# declarations. This one is supposed to have none.
|
||||
# The class declaration is irrelevant
|
||||
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_script -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for handling of trial's --order option.
|
||||
"""
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class FooTest(unittest.TestCase):
|
||||
"""
|
||||
Used to make assertions about the order its tests will be run in.
|
||||
"""
|
||||
|
||||
def test_first(self) -> None:
|
||||
pass
|
||||
|
||||
def test_second(self) -> None:
|
||||
pass
|
||||
|
||||
def test_third(self) -> None:
|
||||
pass
|
||||
|
||||
def test_fourth(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class BazTest(unittest.TestCase):
|
||||
"""
|
||||
Used to make assertions about the order the test cases in this module are
|
||||
run in.
|
||||
"""
|
||||
|
||||
def test_baz(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class BarTest(unittest.TestCase):
|
||||
"""
|
||||
Used to make assertions about the order the test cases in this module are
|
||||
run in.
|
||||
"""
|
||||
|
||||
def test_bar(self) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
|
||||
"""
|
||||
Classes and functions used by L{twisted.trial.test.test_util}
|
||||
and L{twisted.trial.test.test_loader}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Python 3 has some funny import caching, which we don't want.
|
||||
# invalidate_caches clears it out for us.
|
||||
from importlib import invalidate_caches as invalidateImportCaches
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
testModule = """
|
||||
from twisted.trial import unittest
|
||||
|
||||
class FooTest(unittest.SynchronousTestCase):
|
||||
def testFoo(self):
|
||||
pass
|
||||
"""
|
||||
|
||||
dosModule = testModule.replace("\n", "\r\n")
|
||||
|
||||
|
||||
testSample = """
|
||||
'''This module is used by test_loader to test the Trial test loading
|
||||
functionality. Do NOT change the number of tests in this module.
|
||||
Do NOT change the names the tests in this module.
|
||||
'''
|
||||
|
||||
import unittest as pyunit
|
||||
from twisted.trial import unittest
|
||||
|
||||
class FooTest(unittest.SynchronousTestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def test_bar(self):
|
||||
pass
|
||||
|
||||
|
||||
class PyunitTest(pyunit.TestCase):
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
def test_bar(self):
|
||||
pass
|
||||
|
||||
|
||||
class NotATest:
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
|
||||
class AlphabetTest(unittest.SynchronousTestCase):
|
||||
def test_a(self):
|
||||
pass
|
||||
|
||||
def test_b(self):
|
||||
pass
|
||||
|
||||
def test_c(self):
|
||||
pass
|
||||
"""
|
||||
|
||||
testInheritanceSample = """
|
||||
'''This module is used by test_loader to test the Trial test loading
|
||||
functionality. Do NOT change the number of tests in this module.
|
||||
Do NOT change the names the tests in this module.
|
||||
'''
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
class X:
|
||||
|
||||
def test_foo(self):
|
||||
pass
|
||||
|
||||
class A(unittest.SynchronousTestCase, X):
|
||||
pass
|
||||
|
||||
class B(unittest.SynchronousTestCase, X):
|
||||
pass
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PackageTest(unittest.SynchronousTestCase):
|
||||
files = [
|
||||
("badpackage/__init__.py", "frotz\n"),
|
||||
("badpackage/test_module.py", ""),
|
||||
("unimportablepackage/__init__.py", ""),
|
||||
("unimportablepackage/test_module.py", "import notarealmoduleok\n"),
|
||||
("package2/__init__.py", ""),
|
||||
("package2/test_module.py", "import frotz\n"),
|
||||
("package/__init__.py", ""),
|
||||
("package/frotz.py", "frotz\n"),
|
||||
("package/test_bad_module.py", 'raise ZeroDivisionError("fake error")'),
|
||||
("package/test_dos_module.py", dosModule),
|
||||
("package/test_import_module.py", "import frotz"),
|
||||
("package/test_module.py", testModule),
|
||||
("goodpackage/__init__.py", ""),
|
||||
("goodpackage/test_sample.py", testSample),
|
||||
("goodpackage/sub/__init__.py", ""),
|
||||
("goodpackage/sub/test_sample.py", testSample),
|
||||
("inheritancepackage/__init__.py", ""),
|
||||
("inheritancepackage/test_x.py", testInheritanceSample),
|
||||
]
|
||||
|
||||
def _toModuleName(self, filename):
|
||||
name = os.path.splitext(filename)[0]
|
||||
segs = name.split("/")
|
||||
if segs[-1] == "__init__":
|
||||
segs = segs[:-1]
|
||||
return ".".join(segs)
|
||||
|
||||
def getModules(self):
|
||||
"""
|
||||
Return matching module names for files listed in C{self.files}.
|
||||
"""
|
||||
return [self._toModuleName(filename) for (filename, code) in self.files]
|
||||
|
||||
def cleanUpModules(self):
|
||||
modules = self.getModules()
|
||||
modules.sort()
|
||||
modules.reverse()
|
||||
for module in modules:
|
||||
try:
|
||||
del sys.modules[module]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def createFiles(self, files, parentDir="."):
|
||||
for filename, contents in self.files:
|
||||
filename = os.path.join(parentDir, filename)
|
||||
self._createDirectory(filename)
|
||||
with open(filename, "w") as fd:
|
||||
fd.write(contents)
|
||||
|
||||
def _createDirectory(self, filename):
|
||||
directory = os.path.dirname(filename)
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
def setUp(self, parentDir=None):
|
||||
invalidateImportCaches()
|
||||
if parentDir is None:
|
||||
parentDir = self.mktemp()
|
||||
self.parent = parentDir
|
||||
self.createFiles(self.files, parentDir)
|
||||
|
||||
def tearDown(self):
|
||||
self.cleanUpModules()
|
||||
|
||||
|
||||
class SysPathManglingTest(PackageTest):
|
||||
def setUp(self, parent=None):
|
||||
invalidateImportCaches()
|
||||
self.oldPath = sys.path[:]
|
||||
self.newPath = sys.path[:]
|
||||
if parent is None:
|
||||
parent = self.mktemp()
|
||||
PackageTest.setUp(self, parent)
|
||||
self.newPath.append(self.parent)
|
||||
self.mangleSysPath(self.newPath)
|
||||
|
||||
def tearDown(self):
|
||||
PackageTest.tearDown(self)
|
||||
self.mangleSysPath(self.oldPath)
|
||||
|
||||
def mangleSysPath(self, pathVar):
|
||||
sys.path[:] = pathVar
|
||||
@@ -0,0 +1,115 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Sample test cases defined using the standard library L{unittest.TestCase}
|
||||
class which are used as data by test cases which are actually part of the
|
||||
trial test suite to verify handling of handling of such cases.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from sys import exc_info
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
||||
class PyUnitTest(unittest.TestCase):
|
||||
def test_pass(self):
|
||||
"""
|
||||
A passing test.
|
||||
"""
|
||||
|
||||
def test_error(self):
|
||||
"""
|
||||
A test which raises an exception to cause an error.
|
||||
"""
|
||||
raise Exception("pyunit error")
|
||||
|
||||
def test_fail(self):
|
||||
"""
|
||||
A test which uses L{unittest.TestCase.fail} to cause a failure.
|
||||
"""
|
||||
self.fail("pyunit failure")
|
||||
|
||||
@unittest.skip("pyunit skip")
|
||||
def test_skip(self):
|
||||
"""
|
||||
A test which uses the L{unittest.skip} decorator to cause a skip.
|
||||
"""
|
||||
|
||||
|
||||
class _NonStringId:
|
||||
"""
|
||||
A class that looks a little like a TestCase, but not enough so to
|
||||
actually be used as one. This helps L{BrokenRunInfrastructure} use some
|
||||
interfaces incorrectly to provoke certain failure conditions.
|
||||
"""
|
||||
|
||||
def id(self) -> object:
|
||||
return object()
|
||||
|
||||
|
||||
class BrokenRunInfrastructure(unittest.TestCase):
|
||||
"""
|
||||
A test suite that is broken at the level of integration between
|
||||
L{TestCase.run} and the results object.
|
||||
"""
|
||||
|
||||
def run(self, result):
|
||||
"""
|
||||
Override the normal C{run} behavior to pass the result object
|
||||
along to the test method. Each test method needs the result object so
|
||||
that it can implement its particular kind of brokenness.
|
||||
"""
|
||||
return getattr(self, self._testMethodName)(result)
|
||||
|
||||
def test_addSuccess(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addSuccess} interface.
|
||||
"""
|
||||
|
||||
result.addSuccess(_NonStringId())
|
||||
|
||||
def test_addError(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addError} interface.
|
||||
"""
|
||||
try:
|
||||
raise Exception("test_addError")
|
||||
except BaseException:
|
||||
err = exc_info()
|
||||
|
||||
result.addError(_NonStringId(), err)
|
||||
|
||||
def test_addFailure(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addFailure} interface.
|
||||
"""
|
||||
try:
|
||||
raise Exception("test_addFailure")
|
||||
except BaseException:
|
||||
err = exc_info()
|
||||
|
||||
result.addFailure(_NonStringId(), err)
|
||||
|
||||
def test_addSkip(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addSkip} interface.
|
||||
"""
|
||||
result.addSkip(_NonStringId(), "test_addSkip")
|
||||
|
||||
def test_addExpectedFailure(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addExpectedFailure} interface.
|
||||
"""
|
||||
try:
|
||||
raise Exception("test_addExpectedFailure")
|
||||
except BaseException:
|
||||
err = Failure()
|
||||
result.addExpectedFailure(_NonStringId(), err)
|
||||
|
||||
def test_addUnexpectedSuccess(self, result):
|
||||
"""
|
||||
Violate the L{TestResult.addUnexpectedSuccess} interface.
|
||||
"""
|
||||
result.addUnexpectedSuccess(_NonStringId())
|
||||
@@ -0,0 +1,95 @@
|
||||
"""This module is used by test_loader to test the Trial test loading
|
||||
functionality. Do NOT change the number of tests in this module. Do NOT change
|
||||
the names the tests in this module.
|
||||
"""
|
||||
|
||||
|
||||
import unittest as pyunit
|
||||
|
||||
from twisted.python.util import mergeFunctionMetadata
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class FooTest(unittest.SynchronousTestCase):
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
def test_bar(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def badDecorator(fn):
|
||||
"""
|
||||
Decorate a function without preserving the name of the original function.
|
||||
Always return a function with the same name.
|
||||
"""
|
||||
|
||||
def nameCollision(*args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return nameCollision
|
||||
|
||||
|
||||
def goodDecorator(fn):
|
||||
"""
|
||||
Decorate a function and preserve the original name.
|
||||
"""
|
||||
|
||||
def nameCollision(*args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return mergeFunctionMetadata(fn, nameCollision)
|
||||
|
||||
|
||||
class DecorationTest(unittest.SynchronousTestCase):
|
||||
def test_badDecorator(self) -> None:
|
||||
"""
|
||||
This test method is decorated in a way that gives it a confusing name
|
||||
that collides with another method.
|
||||
"""
|
||||
|
||||
test_badDecorator = badDecorator(test_badDecorator)
|
||||
|
||||
def test_goodDecorator(self) -> None:
|
||||
"""
|
||||
This test method is decorated in a way that preserves its name.
|
||||
"""
|
||||
|
||||
test_goodDecorator = goodDecorator(test_goodDecorator)
|
||||
|
||||
def renamedDecorator(self) -> None:
|
||||
"""
|
||||
This is secretly a test method and will be decorated and then renamed so
|
||||
test discovery can find it.
|
||||
"""
|
||||
|
||||
test_renamedDecorator = goodDecorator(renamedDecorator)
|
||||
|
||||
def nameCollision(self) -> None:
|
||||
"""
|
||||
This isn't a test, it's just here to collide with tests.
|
||||
"""
|
||||
|
||||
|
||||
class PyunitTest(pyunit.TestCase):
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
def test_bar(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class NotATest:
|
||||
def test_foo(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class AlphabetTest(unittest.SynchronousTestCase):
|
||||
def test_a(self) -> None:
|
||||
pass
|
||||
|
||||
def test_b(self) -> None:
|
||||
pass
|
||||
|
||||
def test_c(self) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- test-case-name: twisted.trial.test.test_log,twisted.trial.test.test_runner -*-
|
||||
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# fodder for test_script, which parses files for emacs local variable
|
||||
# declarations. This one is supposed to have:
|
||||
# test-case-name: twisted.trial.test.test_log,twisted.trial.test.test_runner
|
||||
# in the second line
|
||||
# The class declaration is irrelevant
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
@@ -0,0 +1,280 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_tests -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Definitions of test cases with various interesting behaviors, to be used by
|
||||
L{twisted.trial.test.test_tests} and other test modules to exercise different
|
||||
features of trial's test runner.
|
||||
|
||||
See the L{twisted.trial.test.test_tests} module docstring for details about how
|
||||
this code is arranged.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial.unittest import FailTest, SkipTest, SynchronousTestCase, TestCase
|
||||
|
||||
|
||||
class SkippingMixin:
|
||||
def test_skip1(self):
|
||||
raise SkipTest("skip1")
|
||||
|
||||
def test_skip2(self):
|
||||
raise RuntimeError("I should not get raised")
|
||||
|
||||
test_skip2.skip = "skip2" # type: ignore[attr-defined]
|
||||
|
||||
def test_skip3(self):
|
||||
self.fail("I should not fail")
|
||||
|
||||
test_skip3.skip = "skip3" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SynchronousSkipping(SkippingMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousSkipping(SkippingMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class SkippingSetUpMixin:
|
||||
def setUp(self):
|
||||
raise SkipTest("skipSetUp")
|
||||
|
||||
def test_1(self):
|
||||
pass
|
||||
|
||||
def test_2(self):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousSkippingSetUp(SkippingSetUpMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousSkippingSetUp(SkippingSetUpMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class DeprecatedReasonlessSkipMixin:
|
||||
def test_1(self):
|
||||
raise SkipTest()
|
||||
|
||||
|
||||
class SynchronousDeprecatedReasonlessSkip(
|
||||
DeprecatedReasonlessSkipMixin, SynchronousTestCase
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousDeprecatedReasonlessSkip(DeprecatedReasonlessSkipMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class SkippedClassMixin:
|
||||
skip = "class"
|
||||
|
||||
def setUp(self):
|
||||
self.__class__._setUpRan = True
|
||||
|
||||
def test_skip1(self):
|
||||
raise SkipTest("skip1")
|
||||
|
||||
def test_skip2(self):
|
||||
raise RuntimeError("Ought to skip me")
|
||||
|
||||
test_skip2.skip = "skip2" # type: ignore
|
||||
|
||||
def test_skip3(self):
|
||||
pass
|
||||
|
||||
def test_skip4(self):
|
||||
raise RuntimeError("Skip me too")
|
||||
|
||||
|
||||
class SynchronousSkippedClass(SkippedClassMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousSkippedClass(SkippedClassMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TodoMixin:
|
||||
def test_todo1(self):
|
||||
self.fail("deliberate failure")
|
||||
|
||||
test_todo1.todo = "todo1" # type: ignore[attr-defined]
|
||||
|
||||
def test_todo2(self):
|
||||
raise RuntimeError("deliberate error")
|
||||
|
||||
test_todo2.todo = "todo2" # type: ignore[attr-defined]
|
||||
|
||||
def test_todo3(self):
|
||||
"""unexpected success"""
|
||||
|
||||
test_todo3.todo = "todo3" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SynchronousTodo(TodoMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTodo(TodoMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class SetUpTodoMixin:
|
||||
def setUp(self):
|
||||
raise RuntimeError("deliberate error")
|
||||
|
||||
def test_todo1(self):
|
||||
pass
|
||||
|
||||
test_todo1.todo = "setUp todo1" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SynchronousSetUpTodo(SetUpTodoMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousSetUpTodo(SetUpTodoMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TearDownTodoMixin:
|
||||
def tearDown(self):
|
||||
raise RuntimeError("deliberate error")
|
||||
|
||||
def test_todo1(self):
|
||||
pass
|
||||
|
||||
test_todo1.todo = "tearDown todo1" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SynchronousTearDownTodo(TearDownTodoMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTearDownTodo(TearDownTodoMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TodoClassMixin:
|
||||
todo = "class"
|
||||
|
||||
def test_todo1(self):
|
||||
pass
|
||||
|
||||
test_todo1.todo = "method" # type: ignore[attr-defined]
|
||||
|
||||
def test_todo2(self):
|
||||
pass
|
||||
|
||||
def test_todo3(self):
|
||||
self.fail("Deliberate Failure")
|
||||
|
||||
test_todo3.todo = "method" # type: ignore[attr-defined]
|
||||
|
||||
def test_todo4(self):
|
||||
self.fail("Deliberate Failure")
|
||||
|
||||
|
||||
class SynchronousTodoClass(TodoClassMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTodoClass(TodoClassMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class StrictTodoMixin:
|
||||
def test_todo1(self):
|
||||
raise RuntimeError("expected failure")
|
||||
|
||||
test_todo1.todo = (RuntimeError, "todo1") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo2(self):
|
||||
raise RuntimeError("expected failure")
|
||||
|
||||
test_todo2.todo = ((RuntimeError, OSError), "todo2") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo3(self):
|
||||
raise RuntimeError("we had no idea!")
|
||||
|
||||
test_todo3.todo = (OSError, "todo3") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo4(self):
|
||||
raise RuntimeError("we had no idea!")
|
||||
|
||||
test_todo4.todo = ((OSError, SyntaxError), "todo4") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo5(self):
|
||||
self.fail("deliberate failure")
|
||||
|
||||
test_todo5.todo = (FailTest, "todo5") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo6(self):
|
||||
self.fail("deliberate failure")
|
||||
|
||||
test_todo6.todo = (RuntimeError, "todo6") # type: ignore[attr-defined]
|
||||
|
||||
def test_todo7(self):
|
||||
pass
|
||||
|
||||
test_todo7.todo = (RuntimeError, "todo7") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SynchronousStrictTodo(StrictTodoMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousStrictTodo(StrictTodoMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AddCleanupMixin:
|
||||
def setUp(self):
|
||||
self.log = ["setUp"]
|
||||
|
||||
def brokenSetUp(self):
|
||||
self.log = ["setUp"]
|
||||
raise RuntimeError("Deliberate failure")
|
||||
|
||||
def skippingSetUp(self):
|
||||
self.log = ["setUp"]
|
||||
raise SkipTest("Don't do this")
|
||||
|
||||
def append(self, thing):
|
||||
self.log.append(thing)
|
||||
|
||||
def tearDown(self):
|
||||
self.log.append("tearDown")
|
||||
|
||||
def runTest(self):
|
||||
self.log.append("runTest")
|
||||
|
||||
|
||||
class SynchronousAddCleanup(AddCleanupMixin, SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousAddCleanup(AddCleanupMixin, TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class ExpectedFailure(SynchronousTestCase):
|
||||
"""
|
||||
Hold a test that has an expected failure with an exception that has a
|
||||
large string representation.
|
||||
"""
|
||||
|
||||
def test_expectedFailureGreaterThan64k(self) -> None:
|
||||
"""
|
||||
Fail, but expectedly.
|
||||
"""
|
||||
raise RuntimeError("x" * (2**16 + 1))
|
||||
|
||||
test_expectedFailureGreaterThan64k.todo = "short todo string" # type: ignore[attr-defined]
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_tests -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases used to make sure that warning suppression works at the module,
|
||||
method, and class levels.
|
||||
|
||||
See the L{twisted.trial.test.test_tests} module docstring for details about how
|
||||
this code is arranged.
|
||||
"""
|
||||
|
||||
|
||||
import warnings
|
||||
|
||||
from twisted.trial import unittest, util
|
||||
|
||||
METHOD_WARNING_MSG = "method warning message"
|
||||
CLASS_WARNING_MSG = "class warning message"
|
||||
MODULE_WARNING_MSG = "module warning message"
|
||||
|
||||
|
||||
class MethodWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class ClassWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class ModuleWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class EmitMixin:
|
||||
"""
|
||||
Mixin for emiting a variety of warnings.
|
||||
"""
|
||||
|
||||
def _emit(self):
|
||||
warnings.warn(METHOD_WARNING_MSG, MethodWarning)
|
||||
warnings.warn(CLASS_WARNING_MSG, ClassWarning)
|
||||
warnings.warn(MODULE_WARNING_MSG, ModuleWarning)
|
||||
|
||||
|
||||
class SuppressionMixin(EmitMixin):
|
||||
suppress = [util.suppress(message=CLASS_WARNING_MSG)]
|
||||
|
||||
def testSuppressMethod(self):
|
||||
self._emit()
|
||||
|
||||
testSuppressMethod.suppress = [util.suppress(message=METHOD_WARNING_MSG)] # type: ignore[attr-defined]
|
||||
|
||||
def testSuppressClass(self):
|
||||
self._emit()
|
||||
|
||||
def testOverrideSuppressClass(self):
|
||||
self._emit()
|
||||
|
||||
testOverrideSuppressClass.suppress = [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class SetUpSuppressionMixin:
|
||||
def setUp(self):
|
||||
self._emit()
|
||||
|
||||
|
||||
class TearDownSuppressionMixin:
|
||||
def tearDown(self):
|
||||
self._emit()
|
||||
|
||||
|
||||
class TestSuppression2Mixin(EmitMixin):
|
||||
def testSuppressModule(self):
|
||||
self._emit()
|
||||
|
||||
|
||||
suppress = [util.suppress(message=MODULE_WARNING_MSG)]
|
||||
|
||||
|
||||
class SynchronousTestSuppression(SuppressionMixin, unittest.SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousTestSetUpSuppression(
|
||||
SetUpSuppressionMixin, SynchronousTestSuppression
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousTestTearDownSuppression(
|
||||
TearDownSuppressionMixin, SynchronousTestSuppression
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class SynchronousTestSuppression2(TestSuppression2Mixin, unittest.SynchronousTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestSuppression(SuppressionMixin, unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestSetUpSuppression(
|
||||
SetUpSuppressionMixin, AsynchronousTestSuppression
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestTearDownSuppression(
|
||||
TearDownSuppressionMixin, AsynchronousTestSuppression
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class AsynchronousTestSuppression2(TestSuppression2Mixin, unittest.TestCase):
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for async assertions provided by C{twisted.trial.unittest.TestCase}.
|
||||
"""
|
||||
|
||||
|
||||
import unittest as pyunit
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python import failure
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class AsynchronousAssertionsTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{TestCase}'s asynchronous extensions to L{SynchronousTestCase}.
|
||||
That is, assertFailure.
|
||||
"""
|
||||
|
||||
def test_assertFailure(self):
|
||||
d = defer.maybeDeferred(lambda: 1 / 0)
|
||||
return self.assertFailure(d, ZeroDivisionError)
|
||||
|
||||
def test_assertFailure_wrongException(self):
|
||||
d = defer.maybeDeferred(lambda: 1 / 0)
|
||||
self.assertFailure(d, OverflowError)
|
||||
d.addCallbacks(
|
||||
lambda x: self.fail("Should have failed"),
|
||||
lambda x: x.trap(self.failureException),
|
||||
)
|
||||
return d
|
||||
|
||||
def test_assertFailure_noException(self):
|
||||
d = defer.succeed(None)
|
||||
self.assertFailure(d, ZeroDivisionError)
|
||||
d.addCallbacks(
|
||||
lambda x: self.fail("Should have failed"),
|
||||
lambda x: x.trap(self.failureException),
|
||||
)
|
||||
return d
|
||||
|
||||
def test_assertFailure_moreInfo(self):
|
||||
"""
|
||||
In the case of assertFailure failing, check that we get lots of
|
||||
information about the exception that was raised.
|
||||
"""
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
f = failure.Failure()
|
||||
d = defer.fail(f)
|
||||
d = self.assertFailure(d, RuntimeError)
|
||||
d.addErrback(self._checkInfo, f)
|
||||
return d
|
||||
|
||||
def _checkInfo(self, assertionFailure, f):
|
||||
assert assertionFailure.check(self.failureException)
|
||||
output = assertionFailure.getErrorMessage()
|
||||
self.assertIn(f.getErrorMessage(), output)
|
||||
self.assertIn(f.getBriefTraceback(), output)
|
||||
|
||||
def test_assertFailure_masked(self):
|
||||
"""
|
||||
A single wrong assertFailure should fail the whole test.
|
||||
"""
|
||||
|
||||
class ExampleFailure(Exception):
|
||||
pass
|
||||
|
||||
class TC(unittest.TestCase):
|
||||
failureException = ExampleFailure
|
||||
|
||||
def test_assertFailure(self):
|
||||
d = defer.maybeDeferred(lambda: 1 / 0)
|
||||
self.assertFailure(d, OverflowError)
|
||||
self.assertFailure(d, ZeroDivisionError)
|
||||
return d
|
||||
|
||||
test = TC("test_assertFailure")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
self.assertEqual(1, len(result.failures))
|
||||
@@ -0,0 +1,258 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for returning Deferreds from a TestCase.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest as pyunit
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import reporter, unittest, util
|
||||
from twisted.trial.test import detests
|
||||
|
||||
|
||||
class SetUpTests(unittest.TestCase):
|
||||
def _loadSuite(
|
||||
self, klass: type[pyunit.TestCase]
|
||||
) -> tuple[reporter.TestResult, pyunit.TestSuite]:
|
||||
loader = pyunit.TestLoader()
|
||||
r = reporter.TestResult()
|
||||
s = loader.loadTestsFromTestCase(klass)
|
||||
return r, s
|
||||
|
||||
def test_success(self) -> None:
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpOK)
|
||||
suite(result)
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
|
||||
def test_fail(self) -> None:
|
||||
self.assertFalse(detests.DeferredSetUpFail.testCalled)
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpFail)
|
||||
suite(result)
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertFalse(detests.DeferredSetUpFail.testCalled)
|
||||
|
||||
def test_callbackFail(self) -> None:
|
||||
self.assertFalse(detests.DeferredSetUpCallbackFail.testCalled)
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpCallbackFail)
|
||||
suite(result)
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertFalse(detests.DeferredSetUpCallbackFail.testCalled)
|
||||
|
||||
def test_error(self) -> None:
|
||||
self.assertFalse(detests.DeferredSetUpError.testCalled)
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpError)
|
||||
suite(result)
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertFalse(detests.DeferredSetUpError.testCalled)
|
||||
|
||||
def test_skip(self) -> None:
|
||||
self.assertFalse(detests.DeferredSetUpSkip.testCalled)
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpSkip)
|
||||
suite(result)
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
self.assertEqual(len(result.skips), 1)
|
||||
self.assertFalse(detests.DeferredSetUpSkip.testCalled)
|
||||
|
||||
|
||||
class NeverFireTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._oldTimeout = util.DEFAULT_TIMEOUT_DURATION
|
||||
util.DEFAULT_TIMEOUT_DURATION = 0.1
|
||||
|
||||
def tearDown(self) -> None:
|
||||
util.DEFAULT_TIMEOUT_DURATION = self._oldTimeout
|
||||
|
||||
def _loadSuite(
|
||||
self, klass: type[pyunit.TestCase]
|
||||
) -> tuple[reporter.TestResult, pyunit.TestSuite]:
|
||||
loader = pyunit.TestLoader()
|
||||
r = reporter.TestResult()
|
||||
s = loader.loadTestsFromTestCase(klass)
|
||||
return r, s
|
||||
|
||||
def test_setUp(self) -> None:
|
||||
self.assertFalse(detests.DeferredSetUpNeverFire.testCalled)
|
||||
result, suite = self._loadSuite(detests.DeferredSetUpNeverFire)
|
||||
suite(result)
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertFalse(detests.DeferredSetUpNeverFire.testCalled)
|
||||
assert isinstance(result.errors[0][1], Failure)
|
||||
self.assertTrue(result.errors[0][1].check(defer.TimeoutError))
|
||||
|
||||
|
||||
class TestTester(unittest.TestCase):
|
||||
def getTest(self, name: str) -> pyunit.TestCase:
|
||||
raise NotImplementedError("must override me")
|
||||
|
||||
def runTest(self, name: str) -> reporter.TestResult: # type: ignore[override]
|
||||
result = reporter.TestResult()
|
||||
self.getTest(name).run(result)
|
||||
return result
|
||||
|
||||
|
||||
class DeferredTests(TestTester):
|
||||
def getTest(self, name: str) -> detests.DeferredTests:
|
||||
return detests.DeferredTests(name)
|
||||
|
||||
def test_pass(self) -> None:
|
||||
result = self.runTest("test_pass")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
|
||||
def test_passGenerated(self) -> None:
|
||||
result = self.runTest("test_passGenerated")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertTrue(detests.DeferredTests.touched)
|
||||
|
||||
test_passGenerated.supress = [ # type: ignore[attr-defined]
|
||||
util.suppress(message="twisted.internet.defer.deferredGenerator is deprecated")
|
||||
]
|
||||
|
||||
def test_passInlineCallbacks(self) -> None:
|
||||
"""
|
||||
The body of a L{defer.inlineCallbacks} decorated test gets run.
|
||||
"""
|
||||
result = self.runTest("test_passInlineCallbacks")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertTrue(detests.DeferredTests.touched)
|
||||
|
||||
def test_fail(self) -> None:
|
||||
result = self.runTest("test_fail")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 1)
|
||||
|
||||
def test_failureInCallback(self) -> None:
|
||||
result = self.runTest("test_failureInCallback")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 1)
|
||||
|
||||
def test_errorInCallback(self) -> None:
|
||||
result = self.runTest("test_errorInCallback")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
|
||||
def test_skip(self) -> None:
|
||||
result = self.runTest("test_skip")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.skips), 1)
|
||||
self.assertFalse(detests.DeferredTests.touched)
|
||||
|
||||
def test_todo(self) -> None:
|
||||
result = self.runTest("test_expectedFailure")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
self.assertEqual(len(result.failures), 0)
|
||||
self.assertEqual(len(result.expectedFailures), 1)
|
||||
|
||||
def test_thread(self) -> None:
|
||||
result = self.runTest("test_thread")
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertTrue(result.wasSuccessful(), result.errors)
|
||||
|
||||
|
||||
class TimeoutTests(TestTester):
|
||||
def getTest(self, name: str) -> detests.TimeoutTests:
|
||||
return detests.TimeoutTests(name)
|
||||
|
||||
def _wasTimeout(self, error: Failure) -> None:
|
||||
self.assertEqual(error.check(defer.TimeoutError), defer.TimeoutError)
|
||||
|
||||
def test_pass(self) -> None:
|
||||
result = self.runTest("test_pass")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
|
||||
def test_passDefault(self) -> None:
|
||||
result = self.runTest("test_passDefault")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
|
||||
def test_timeout(self) -> None:
|
||||
result = self.runTest("test_timeout")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
assert isinstance(result.errors[0][1], Failure)
|
||||
self._wasTimeout(result.errors[0][1])
|
||||
|
||||
def test_timeoutZero(self) -> None:
|
||||
result = self.runTest("test_timeoutZero")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
assert isinstance(result.errors[0][1], Failure)
|
||||
self._wasTimeout(result.errors[0][1])
|
||||
|
||||
def test_skip(self) -> None:
|
||||
result = self.runTest("test_skip")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.skips), 1)
|
||||
|
||||
def test_todo(self) -> None:
|
||||
result = self.runTest("test_expectedFailure")
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.expectedFailures), 1)
|
||||
assert isinstance(result.expectedFailures[0][1], Failure)
|
||||
self._wasTimeout(result.expectedFailures[0][1])
|
||||
|
||||
def test_errorPropagation(self) -> None:
|
||||
result = self.runTest("test_errorPropagation")
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
assert detests.TimeoutTests.timedOut is not None
|
||||
self._wasTimeout(detests.TimeoutTests.timedOut)
|
||||
|
||||
def test_classTimeout(self) -> None:
|
||||
loader = pyunit.TestLoader()
|
||||
suite = loader.loadTestsFromTestCase(detests.TestClassTimeoutAttribute)
|
||||
result = reporter.TestResult()
|
||||
suite.run(result)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
assert isinstance(result.errors[0][1], Failure)
|
||||
self._wasTimeout(result.errors[0][1])
|
||||
|
||||
def test_callbackReturnsNonCallingDeferred(self) -> None:
|
||||
# hacky timeout
|
||||
# raises KeyboardInterrupt because Trial sucks
|
||||
from twisted.internet import reactor
|
||||
|
||||
call = reactor.callLater(2, reactor.crash) # type: ignore[attr-defined]
|
||||
result = self.runTest("test_calledButNeverCallback")
|
||||
if call.active():
|
||||
call.cancel()
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
assert isinstance(result.errors[0][1], Failure)
|
||||
self._wasTimeout(result.errors[0][1])
|
||||
|
||||
|
||||
# The test loader erroneously attempts to run this:
|
||||
del TestTester
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test Twisted's doctest support.
|
||||
"""
|
||||
import unittest as pyunit
|
||||
|
||||
from twisted.trial import itrial, reporter, runner, unittest
|
||||
from twisted.trial.test import mockdoctest
|
||||
|
||||
|
||||
class RunnersTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for Twisted's doctest support.
|
||||
"""
|
||||
|
||||
def test_id(self) -> None:
|
||||
"""
|
||||
Check that the id() of the doctests' case object contains the FQPN of
|
||||
the actual tests.
|
||||
"""
|
||||
loader = runner.TestLoader()
|
||||
suite = loader.loadDoctests(mockdoctest)
|
||||
idPrefix = "twisted.trial.test.mockdoctest.Counter"
|
||||
for test in suite._tests:
|
||||
self.assertIn(idPrefix, itrial.ITestCase(test).id())
|
||||
|
||||
def test_basicTrialIntegration(self) -> None:
|
||||
"""
|
||||
L{loadDoctests} loads all of the doctests in the given module.
|
||||
"""
|
||||
loader = runner.TestLoader()
|
||||
suite = loader.loadDoctests(mockdoctest)
|
||||
self.assertEqual(7, suite.countTestCases())
|
||||
|
||||
def _testRun(self, suite: pyunit.TestSuite) -> None:
|
||||
"""
|
||||
Run C{suite} and check the result.
|
||||
"""
|
||||
result = reporter.TestResult()
|
||||
suite.run(result)
|
||||
self.assertEqual(5, result.successes)
|
||||
self.assertEqual(2, len(result.failures))
|
||||
|
||||
def test_expectedResults(self, count: int = 1) -> None:
|
||||
"""
|
||||
Trial can correctly run doctests with its xUnit test APIs.
|
||||
"""
|
||||
suite = runner.TestLoader().loadDoctests(mockdoctest)
|
||||
self._testRun(suite)
|
||||
|
||||
def test_repeatable(self) -> None:
|
||||
"""
|
||||
Doctests should be runnable repeatably.
|
||||
"""
|
||||
suite = runner.TestLoader().loadDoctests(mockdoctest)
|
||||
self._testRun(suite)
|
||||
self._testRun(suite)
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for interrupting tests with Control-C.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from twisted.trial import reporter, runner, unittest
|
||||
|
||||
|
||||
class TrialTest(unittest.SynchronousTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.output = StringIO()
|
||||
self.reporter = reporter.TestResult()
|
||||
self.loader = runner.TestLoader()
|
||||
|
||||
|
||||
class InterruptInTestTests(TrialTest):
|
||||
test_03_doNothing_run: bool | None
|
||||
|
||||
class InterruptedTest(unittest.TestCase):
|
||||
def test_02_raiseInterrupt(self) -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def test_01_doNothing(self) -> None:
|
||||
pass
|
||||
|
||||
def test_03_doNothing(self) -> None:
|
||||
InterruptInTestTests.test_03_doNothing_run = True
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.suite = self.loader.loadClass(InterruptInTestTests.InterruptedTest)
|
||||
InterruptInTestTests.test_03_doNothing_run = None
|
||||
|
||||
def test_setUpOK(self) -> None:
|
||||
self.assertEqual(3, self.suite.countTestCases())
|
||||
self.assertEqual(0, self.reporter.testsRun)
|
||||
self.assertFalse(self.reporter.shouldStop)
|
||||
|
||||
def test_interruptInTest(self) -> None:
|
||||
runner.TrialSuite([self.suite]).run(self.reporter)
|
||||
self.assertTrue(self.reporter.shouldStop)
|
||||
self.assertEqual(2, self.reporter.testsRun)
|
||||
self.assertFalse(
|
||||
InterruptInTestTests.test_03_doNothing_run, "test_03_doNothing ran."
|
||||
)
|
||||
|
||||
|
||||
class InterruptInSetUpTests(TrialTest):
|
||||
testsRun = 0
|
||||
test_02_run: bool
|
||||
|
||||
class InterruptedTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if InterruptInSetUpTests.testsRun > 0:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def test_01(self) -> None:
|
||||
InterruptInSetUpTests.testsRun += 1
|
||||
|
||||
def test_02(self) -> None:
|
||||
InterruptInSetUpTests.testsRun += 1
|
||||
InterruptInSetUpTests.test_02_run = True
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.suite = self.loader.loadClass(InterruptInSetUpTests.InterruptedTest)
|
||||
InterruptInSetUpTests.test_02_run = False
|
||||
InterruptInSetUpTests.testsRun = 0
|
||||
|
||||
def test_setUpOK(self) -> None:
|
||||
self.assertEqual(0, InterruptInSetUpTests.testsRun)
|
||||
self.assertEqual(2, self.suite.countTestCases())
|
||||
self.assertEqual(0, self.reporter.testsRun)
|
||||
self.assertFalse(self.reporter.shouldStop)
|
||||
|
||||
def test_interruptInSetUp(self) -> None:
|
||||
runner.TrialSuite([self.suite]).run(self.reporter)
|
||||
self.assertTrue(self.reporter.shouldStop)
|
||||
self.assertEqual(2, self.reporter.testsRun)
|
||||
self.assertFalse(InterruptInSetUpTests.test_02_run, "test_02 ran")
|
||||
|
||||
|
||||
class InterruptInTearDownTests(TrialTest):
|
||||
testsRun = 0
|
||||
test_02_run: bool
|
||||
|
||||
class InterruptedTest(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
if InterruptInTearDownTests.testsRun > 0:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def test_01(self) -> None:
|
||||
InterruptInTearDownTests.testsRun += 1
|
||||
|
||||
def test_02(self) -> None:
|
||||
InterruptInTearDownTests.testsRun += 1
|
||||
InterruptInTearDownTests.test_02_run = True
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.suite = self.loader.loadClass(InterruptInTearDownTests.InterruptedTest)
|
||||
InterruptInTearDownTests.testsRun = 0
|
||||
InterruptInTearDownTests.test_02_run = False
|
||||
|
||||
def test_setUpOK(self) -> None:
|
||||
self.assertEqual(0, InterruptInTearDownTests.testsRun)
|
||||
self.assertEqual(2, self.suite.countTestCases())
|
||||
self.assertEqual(0, self.reporter.testsRun)
|
||||
self.assertFalse(self.reporter.shouldStop)
|
||||
|
||||
def test_interruptInTearDown(self) -> None:
|
||||
runner.TrialSuite([self.suite]).run(self.reporter)
|
||||
self.assertEqual(1, self.reporter.testsRun)
|
||||
self.assertTrue(self.reporter.shouldStop)
|
||||
self.assertFalse(InterruptInTearDownTests.test_02_run, "test_02 ran")
|
||||
@@ -0,0 +1,649 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for loading tests by name.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest as pyunit
|
||||
from hashlib import md5
|
||||
from operator import attrgetter
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Callable, Generator
|
||||
|
||||
from hamcrest import assert_that, equal_to, has_properties
|
||||
from hamcrest.core.matcher import Matcher
|
||||
|
||||
from twisted.python import filepath, util
|
||||
from twisted.python.modules import PythonAttribute, PythonModule, getModule
|
||||
from twisted.python.reflect import ModuleNotFound
|
||||
from twisted.trial import reporter, runner, unittest
|
||||
from twisted.trial._asyncrunner import _iterateTests
|
||||
from twisted.trial.itrial import ITestCase
|
||||
from twisted.trial.test import packages
|
||||
from .matchers import after
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import SupportsRichComparison
|
||||
|
||||
|
||||
def testNames(tests: pyunit.TestCase | pyunit.TestSuite) -> list[str]:
|
||||
"""
|
||||
Return the id of each test within the given test suite or case.
|
||||
"""
|
||||
names = []
|
||||
for test in _iterateTests(tests):
|
||||
names.append(test.id())
|
||||
return names
|
||||
|
||||
|
||||
class FinderPy3Tests(packages.SysPathManglingTest):
|
||||
def setUp(self) -> None: # type: ignore[override]
|
||||
super().setUp()
|
||||
self.loader = runner.TestLoader()
|
||||
|
||||
def test_findNonModule(self) -> None:
|
||||
"""
|
||||
findByName, if given something findable up until the last entry, will
|
||||
raise AttributeError (as it cannot tell if 'nonexistent' here is
|
||||
supposed to be a module or a class).
|
||||
"""
|
||||
self.assertRaises(
|
||||
AttributeError, self.loader.findByName, "twisted.trial.test.nonexistent"
|
||||
)
|
||||
|
||||
def test_findNonPackage(self) -> None:
|
||||
self.assertRaises(ModuleNotFound, self.loader.findByName, "nonextant")
|
||||
|
||||
def test_findNonFile(self) -> None:
|
||||
"""
|
||||
findByName, given a file path that doesn't exist, will raise a
|
||||
ValueError saying that it is not a Python file.
|
||||
"""
|
||||
path = util.sibpath(__file__, "nonexistent.py")
|
||||
self.assertRaises(ValueError, self.loader.findByName, path)
|
||||
|
||||
def test_findFileWithImportError(self) -> None:
|
||||
"""
|
||||
findByName will re-raise ImportErrors inside modules that it has found
|
||||
and imported.
|
||||
"""
|
||||
self.assertRaises(
|
||||
ImportError, self.loader.findByName, "unimportablepackage.test_module"
|
||||
)
|
||||
|
||||
|
||||
def looselyResembles(module: ModuleType) -> Matcher[ModuleType]:
|
||||
"""
|
||||
Match a module with a L{ModuleSpec} like that of the given module.
|
||||
|
||||
@return: A matcher for a module spec that has the same name and origin as
|
||||
the given module spec, though the origin may be structurally inequal
|
||||
as long as it is semantically equal.
|
||||
"""
|
||||
expected = module.__spec__
|
||||
# Technically possible but not expected in any of the tests written so
|
||||
# far.
|
||||
assert expected is not None
|
||||
match_spec = has_properties(
|
||||
{
|
||||
"name": equal_to(expected.name),
|
||||
"origin": after(
|
||||
filepath.FilePath,
|
||||
equal_to(filepath.FilePath(expected.origin)),
|
||||
),
|
||||
}
|
||||
)
|
||||
return after(attrgetter("__spec__"), match_spec)
|
||||
|
||||
|
||||
class FileTests(packages.SysPathManglingTest):
|
||||
"""
|
||||
Tests for L{runner.filenameToModule}.
|
||||
"""
|
||||
|
||||
def test_notFile(self) -> None:
|
||||
"""
|
||||
L{runner.filenameToModule} raises a C{ValueError} when a non-existing
|
||||
file is passed.
|
||||
"""
|
||||
err = self.assertRaises(ValueError, runner.filenameToModule, "it")
|
||||
self.assertEqual(str(err), "'it' doesn't exist")
|
||||
|
||||
def test_moduleInPath(self) -> None:
|
||||
"""
|
||||
If the file in question is a module on the Python path, then it should
|
||||
properly import and return that module.
|
||||
"""
|
||||
sample1 = runner.filenameToModule(util.sibpath(__file__, "sample.py"))
|
||||
from twisted.trial.test import sample as sample2
|
||||
|
||||
self.assertEqual(sample2, sample1)
|
||||
|
||||
def test_moduleNotInPath(self) -> None:
|
||||
"""
|
||||
If passed the path to a file containing the implementation of a
|
||||
module within a package which is not on the import path,
|
||||
L{runner.filenameToModule} returns a module object loosely
|
||||
resembling the module defined by that file anyway.
|
||||
"""
|
||||
|
||||
self.mangleSysPath(self.oldPath)
|
||||
sample1 = runner.filenameToModule(
|
||||
os.path.join(self.parent, "goodpackage", "test_sample.py")
|
||||
)
|
||||
self.assertEqual(sample1.__name__, "goodpackage.test_sample")
|
||||
|
||||
self.cleanUpModules()
|
||||
self.mangleSysPath(self.newPath)
|
||||
from goodpackage import test_sample as sample2 # type: ignore[import-not-found]
|
||||
|
||||
self.assertIsNot(sample1, sample2)
|
||||
assert_that(sample1, looselyResembles(sample2))
|
||||
|
||||
def test_packageInPath(self) -> None:
|
||||
"""
|
||||
If the file in question is a package on the Python path, then it should
|
||||
properly import and return that package.
|
||||
"""
|
||||
package1 = runner.filenameToModule(os.path.join(self.parent, "goodpackage"))
|
||||
|
||||
self.assertIs(package1, sys.modules["goodpackage"])
|
||||
|
||||
def test_packageNotInPath(self) -> None:
|
||||
"""
|
||||
If passed the path to a directory which represents a package which
|
||||
is not on the import path, L{runner.filenameToModule} returns a
|
||||
module object loosely resembling the package defined by that
|
||||
directory anyway.
|
||||
"""
|
||||
self.mangleSysPath(self.oldPath)
|
||||
package1 = runner.filenameToModule(os.path.join(self.parent, "goodpackage"))
|
||||
self.assertEqual(package1.__name__, "goodpackage")
|
||||
|
||||
self.cleanUpModules()
|
||||
self.mangleSysPath(self.newPath)
|
||||
import goodpackage
|
||||
|
||||
self.assertIsNot(package1, goodpackage)
|
||||
assert_that(package1, looselyResembles(goodpackage))
|
||||
|
||||
def test_directoryNotPackage(self) -> None:
|
||||
"""
|
||||
L{runner.filenameToModule} raises a C{ValueError} when the name of an
|
||||
empty directory is passed that isn't considered a valid Python package
|
||||
because it doesn't contain a C{__init__.py} file.
|
||||
"""
|
||||
emptyDir = filepath.FilePath(self.parent).child("emptyDirectory")
|
||||
emptyDir.createDirectory()
|
||||
|
||||
err = self.assertRaises(ValueError, runner.filenameToModule, emptyDir.path)
|
||||
self.assertEqual(str(err), f"{emptyDir.path!r} is not a package directory")
|
||||
|
||||
def test_filenameNotPython(self) -> None:
|
||||
"""
|
||||
L{runner.filenameToModule} raises a C{SyntaxError} when a non-Python
|
||||
file is passed.
|
||||
"""
|
||||
filename = filepath.FilePath(self.parent).child("notpython")
|
||||
filename.setContent(b"This isn't python")
|
||||
self.assertRaises(SyntaxError, runner.filenameToModule, filename.path)
|
||||
|
||||
def test_filenameMatchesPackage(self) -> None:
|
||||
"""
|
||||
The C{__file__} attribute of the module should match the package name.
|
||||
"""
|
||||
filename = filepath.FilePath(self.parent).child("goodpackage.py")
|
||||
filename.setContent(packages.testModule.encode("utf8"))
|
||||
|
||||
try:
|
||||
module = runner.filenameToModule(filename.path)
|
||||
self.assertEqual(filename.path, module.__file__)
|
||||
finally:
|
||||
filename.remove()
|
||||
|
||||
def test_directory(self) -> None:
|
||||
"""
|
||||
Test loader against a filesystem directory containing an empty
|
||||
C{__init__.py} file. It should handle 'path' and 'path/' the same way.
|
||||
"""
|
||||
goodDir = filepath.FilePath(self.parent).child("goodDirectory")
|
||||
goodDir.createDirectory()
|
||||
goodDir.child("__init__.py").setContent(b"")
|
||||
|
||||
try:
|
||||
module = runner.filenameToModule(goodDir.path)
|
||||
self.assertTrue(module.__name__.endswith("goodDirectory"))
|
||||
module = runner.filenameToModule(goodDir.path + os.path.sep)
|
||||
self.assertTrue(module.__name__.endswith("goodDirectory"))
|
||||
finally:
|
||||
goodDir.remove()
|
||||
|
||||
|
||||
class LoaderTests(packages.SysPathManglingTest):
|
||||
"""
|
||||
Tests for L{trial.TestLoader}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None: # type: ignore[override]
|
||||
self.loader = runner.TestLoader()
|
||||
packages.SysPathManglingTest.setUp(self)
|
||||
|
||||
def test_sortCases(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadClass(sample.AlphabetTest)
|
||||
self.assertEqual(
|
||||
["test_a", "test_b", "test_c"],
|
||||
[test._testMethodName for test in suite._tests],
|
||||
)
|
||||
newOrder = ["test_b", "test_c", "test_a"]
|
||||
sortDict = dict(zip(newOrder, range(3)))
|
||||
self.loader.sorter = lambda x: sortDict.get(x.shortDescription(), -1) # type: ignore[arg-type, union-attr, call-arg]
|
||||
suite = self.loader.loadClass(sample.AlphabetTest)
|
||||
self.assertEqual(newOrder, [test._testMethodName for test in suite._tests])
|
||||
|
||||
def test_loadFailure(self) -> None:
|
||||
"""
|
||||
Loading a test that fails and getting the result of it ends up with one
|
||||
test ran and one failure.
|
||||
"""
|
||||
suite = self.loader.loadByName(
|
||||
"twisted.trial.test.erroneous.TestRegularFail.test_fail"
|
||||
)
|
||||
result = reporter.TestResult()
|
||||
suite.run(result)
|
||||
self.assertEqual(result.testsRun, 1)
|
||||
self.assertEqual(len(result.failures), 1)
|
||||
|
||||
def test_loadBadDecorator(self) -> None:
|
||||
"""
|
||||
A decorated test method for which the decorator has failed to set the
|
||||
method's __name__ correctly is loaded and its name in the class scope
|
||||
discovered.
|
||||
"""
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadAnything(
|
||||
sample.DecorationTest.test_badDecorator,
|
||||
parent=sample.DecorationTest,
|
||||
qualName=["sample", "DecorationTest", "test_badDecorator"],
|
||||
)
|
||||
self.assertEqual(1, suite.countTestCases())
|
||||
self.assertEqual("test_badDecorator", suite._testMethodName)
|
||||
|
||||
def test_loadGoodDecorator(self) -> None:
|
||||
"""
|
||||
A decorated test method for which the decorator has set the method's
|
||||
__name__ correctly is loaded and the only name by which it goes is used.
|
||||
"""
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadAnything(
|
||||
sample.DecorationTest.test_goodDecorator,
|
||||
parent=sample.DecorationTest,
|
||||
qualName=["sample", "DecorationTest", "test_goodDecorator"],
|
||||
)
|
||||
self.assertEqual(1, suite.countTestCases())
|
||||
self.assertEqual("test_goodDecorator", suite._testMethodName)
|
||||
|
||||
def test_loadRenamedDecorator(self) -> None:
|
||||
"""
|
||||
Load a decorated method which has been copied to a new name inside the
|
||||
class. Thus its __name__ and its key in the class's __dict__ no
|
||||
longer match.
|
||||
"""
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadAnything(
|
||||
sample.DecorationTest.test_renamedDecorator,
|
||||
parent=sample.DecorationTest,
|
||||
qualName=["sample", "DecorationTest", "test_renamedDecorator"],
|
||||
)
|
||||
self.assertEqual(1, suite.countTestCases())
|
||||
self.assertEqual("test_renamedDecorator", suite._testMethodName)
|
||||
|
||||
def test_loadClass(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadClass(sample.FooTest)
|
||||
self.assertEqual(2, suite.countTestCases())
|
||||
self.assertEqual(
|
||||
["test_bar", "test_foo"], [test._testMethodName for test in suite._tests]
|
||||
)
|
||||
|
||||
def test_loadNonClass(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
self.assertRaises(TypeError, self.loader.loadClass, sample)
|
||||
self.assertRaises(TypeError, self.loader.loadClass, sample.FooTest.test_foo)
|
||||
self.assertRaises(TypeError, self.loader.loadClass, "string")
|
||||
self.assertRaises(TypeError, self.loader.loadClass, ("foo", "bar"))
|
||||
|
||||
def test_loadNonTestCase(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
self.assertRaises(ValueError, self.loader.loadClass, sample.NotATest)
|
||||
|
||||
def test_loadModule(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadModule(sample)
|
||||
self.assertEqual(10, suite.countTestCases())
|
||||
|
||||
def test_loadNonModule(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
self.assertRaises(TypeError, self.loader.loadModule, sample.FooTest)
|
||||
self.assertRaises(TypeError, self.loader.loadModule, sample.FooTest.test_foo)
|
||||
self.assertRaises(TypeError, self.loader.loadModule, "string")
|
||||
self.assertRaises(TypeError, self.loader.loadModule, ("foo", "bar"))
|
||||
|
||||
def test_loadPackage(self) -> None:
|
||||
import goodpackage
|
||||
|
||||
suite = self.loader.loadPackage(goodpackage)
|
||||
self.assertEqual(7, suite.countTestCases())
|
||||
|
||||
def test_loadNonPackage(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
self.assertRaises(TypeError, self.loader.loadPackage, sample.FooTest)
|
||||
self.assertRaises(TypeError, self.loader.loadPackage, sample.FooTest.test_foo)
|
||||
self.assertRaises(TypeError, self.loader.loadPackage, "string")
|
||||
self.assertRaises(TypeError, self.loader.loadPackage, ("foo", "bar"))
|
||||
|
||||
def test_loadModuleAsPackage(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
## XXX -- should this instead raise a ValueError? -- jml
|
||||
self.assertRaises(TypeError, self.loader.loadPackage, sample)
|
||||
|
||||
def test_loadPackageRecursive(self) -> None:
|
||||
import goodpackage
|
||||
|
||||
suite = self.loader.loadPackage(goodpackage, recurse=True)
|
||||
self.assertEqual(14, suite.countTestCases())
|
||||
|
||||
def test_loadAnythingOnModule(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadAnything(sample)
|
||||
self.assertEqual(
|
||||
sample.__name__, suite._tests[0]._tests[0].__class__.__module__
|
||||
)
|
||||
|
||||
def test_loadAnythingOnClass(self) -> None:
|
||||
from twisted.trial.test import sample
|
||||
|
||||
suite = self.loader.loadAnything(sample.FooTest)
|
||||
self.assertEqual(2, suite.countTestCases())
|
||||
|
||||
def test_loadAnythingOnPackage(self) -> None:
|
||||
import goodpackage
|
||||
|
||||
suite = self.loader.loadAnything(goodpackage)
|
||||
self.assertTrue(isinstance(suite, self.loader.suiteFactory))
|
||||
self.assertEqual(7, suite.countTestCases())
|
||||
|
||||
def test_loadAnythingOnPackageRecursive(self) -> None:
|
||||
import goodpackage
|
||||
|
||||
suite = self.loader.loadAnything(goodpackage, recurse=True)
|
||||
self.assertTrue(isinstance(suite, self.loader.suiteFactory))
|
||||
self.assertEqual(14, suite.countTestCases())
|
||||
|
||||
def test_loadAnythingOnString(self) -> None:
|
||||
# the important thing about this test is not the string-iness
|
||||
# but the non-handledness.
|
||||
self.assertRaises(TypeError, self.loader.loadAnything, "goodpackage")
|
||||
|
||||
def test_importErrors(self) -> None:
|
||||
import package # type: ignore[import-not-found]
|
||||
|
||||
suite = self.loader.loadPackage(package, recurse=True)
|
||||
result = reporter.Reporter()
|
||||
suite.run(result)
|
||||
self.assertEqual(False, result.wasSuccessful())
|
||||
self.assertEqual(2, len(result.errors))
|
||||
errors = [test.id() for test, error in result.errors]
|
||||
errors.sort()
|
||||
self.assertEqual(
|
||||
errors, ["package.test_bad_module", "package.test_import_module"]
|
||||
)
|
||||
|
||||
def test_differentInstances(self) -> None:
|
||||
"""
|
||||
L{TestLoader.loadClass} returns a suite with each test method
|
||||
represented by a different instances of the L{TestCase} they are
|
||||
defined on.
|
||||
"""
|
||||
|
||||
class DistinctInstances(pyunit.TestCase):
|
||||
def test_1(self) -> None:
|
||||
self.first = "test1Run"
|
||||
|
||||
def test_2(self) -> None:
|
||||
self.assertFalse(hasattr(self, "first"))
|
||||
|
||||
suite = self.loader.loadClass(DistinctInstances)
|
||||
result = reporter.Reporter()
|
||||
suite.run(result)
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
|
||||
def test_loadModuleWith_test_suite(self) -> None:
|
||||
"""
|
||||
Check that C{test_suite} is used when present and other L{TestCase}s are
|
||||
not included.
|
||||
"""
|
||||
from twisted.trial.test import mockcustomsuite
|
||||
|
||||
suite = self.loader.loadModule(mockcustomsuite)
|
||||
self.assertEqual(0, suite.countTestCases())
|
||||
self.assertEqual("MyCustomSuite", getattr(suite, "name", None))
|
||||
|
||||
def test_loadModuleWith_testSuite(self) -> None:
|
||||
"""
|
||||
Check that C{testSuite} is used when present and other L{TestCase}s are
|
||||
not included.
|
||||
"""
|
||||
from twisted.trial.test import mockcustomsuite2
|
||||
|
||||
suite = self.loader.loadModule(mockcustomsuite2)
|
||||
self.assertEqual(0, suite.countTestCases())
|
||||
self.assertEqual("MyCustomSuite", getattr(suite, "name", None))
|
||||
|
||||
def test_loadModuleWithBothCustom(self) -> None:
|
||||
"""
|
||||
Check that if C{testSuite} and C{test_suite} are both present in a
|
||||
module then C{testSuite} gets priority.
|
||||
"""
|
||||
from twisted.trial.test import mockcustomsuite3
|
||||
|
||||
suite = self.loader.loadModule(mockcustomsuite3)
|
||||
self.assertEqual("testSuite", getattr(suite, "name", None))
|
||||
|
||||
def test_customLoadRaisesAttributeError(self) -> None:
|
||||
"""
|
||||
Make sure that any C{AttributeError}s raised by C{testSuite} are not
|
||||
swallowed by L{TestLoader}.
|
||||
"""
|
||||
|
||||
def testSuite() -> None:
|
||||
raise AttributeError("should be reraised")
|
||||
|
||||
from twisted.trial.test import mockcustomsuite2
|
||||
|
||||
mockcustomsuite2.testSuite, original = (testSuite, mockcustomsuite2.testSuite)
|
||||
try:
|
||||
self.assertRaises(AttributeError, self.loader.loadModule, mockcustomsuite2)
|
||||
finally:
|
||||
mockcustomsuite2.testSuite = original
|
||||
|
||||
# XXX - duplicated and modified from test_script
|
||||
def assertSuitesEqual(
|
||||
self,
|
||||
test1: pyunit.TestCase | pyunit.TestSuite,
|
||||
test2: pyunit.TestCase | pyunit.TestSuite,
|
||||
) -> None:
|
||||
names1 = testNames(test1)
|
||||
names2 = testNames(test2)
|
||||
names1.sort()
|
||||
names2.sort()
|
||||
self.assertEqual(names1, names2)
|
||||
|
||||
def test_loadByNamesDuplicate(self) -> None:
|
||||
"""
|
||||
Check that loadByNames ignores duplicate names
|
||||
"""
|
||||
module = "twisted.trial.test.test_log"
|
||||
suite1 = self.loader.loadByNames([module, module], True)
|
||||
suite2 = self.loader.loadByName(module, True)
|
||||
self.assertSuitesEqual(suite1, suite2)
|
||||
|
||||
def test_loadByNamesPreservesOrder(self) -> None:
|
||||
"""
|
||||
L{TestLoader.loadByNames} preserves the order of tests provided to it.
|
||||
"""
|
||||
modules = [
|
||||
"inheritancepackage.test_x.A.test_foo",
|
||||
"twisted.trial.test.sample",
|
||||
"goodpackage",
|
||||
"twisted.trial.test.test_log",
|
||||
"twisted.trial.test.sample.FooTest",
|
||||
"package.test_module",
|
||||
]
|
||||
suite1 = self.loader.loadByNames(modules)
|
||||
suite2 = runner.TestSuite(map(self.loader.loadByName, modules))
|
||||
self.assertEqual(testNames(suite1), testNames(suite2))
|
||||
|
||||
def test_loadDifferentNames(self) -> None:
|
||||
"""
|
||||
Check that loadByNames loads all the names that it is given
|
||||
"""
|
||||
modules = ["goodpackage", "package.test_module"]
|
||||
suite1 = self.loader.loadByNames(modules)
|
||||
suite2 = runner.TestSuite(map(self.loader.loadByName, modules))
|
||||
self.assertSuitesEqual(suite1, suite2)
|
||||
|
||||
def test_loadInheritedMethods(self) -> None:
|
||||
"""
|
||||
Check that test methods names which are inherited from are all
|
||||
loaded rather than just one.
|
||||
"""
|
||||
methods = [
|
||||
"inheritancepackage.test_x.A.test_foo",
|
||||
"inheritancepackage.test_x.B.test_foo",
|
||||
]
|
||||
suite1 = self.loader.loadByNames(methods)
|
||||
suite2 = runner.TestSuite(map(self.loader.loadByName, methods))
|
||||
self.assertSuitesEqual(suite1, suite2)
|
||||
|
||||
|
||||
class ZipLoadingTests(LoaderTests):
|
||||
def setUp(self) -> None: # type: ignore[override]
|
||||
from twisted.python.test.test_zippath import zipit
|
||||
|
||||
LoaderTests.setUp(self)
|
||||
zipit(self.parent, self.parent + ".zip")
|
||||
self.parent += ".zip"
|
||||
self.mangleSysPath(self.oldPath + [self.parent])
|
||||
|
||||
|
||||
class PackageOrderingTests(packages.SysPathManglingTest):
|
||||
def setUp(self) -> None: # type: ignore[override]
|
||||
self.loader = runner.TestLoader()
|
||||
self.topDir = self.mktemp()
|
||||
parent = os.path.join(self.topDir, "uberpackage")
|
||||
os.makedirs(parent)
|
||||
open(os.path.join(parent, "__init__.py"), "wb").close()
|
||||
packages.SysPathManglingTest.setUp(self, parent)
|
||||
self.mangleSysPath(self.oldPath + [self.topDir])
|
||||
|
||||
def _trialSortAlgorithm(
|
||||
self, sorter: Callable[[PythonModule | PythonAttribute], SupportsRichComparison]
|
||||
) -> Generator[PythonModule | PythonAttribute, None, None]:
|
||||
"""
|
||||
Right now, halfway by accident, trial sorts like this:
|
||||
|
||||
1. all modules are grouped together in one list and sorted.
|
||||
|
||||
2. within each module, the classes are grouped together in one list
|
||||
and sorted.
|
||||
|
||||
3. finally within each class, each test method is grouped together
|
||||
in a list and sorted.
|
||||
|
||||
This attempts to return a sorted list of testable thingies following
|
||||
those rules, so that we can compare the behavior of loadPackage.
|
||||
|
||||
The things that show as 'cases' are errors from modules which failed to
|
||||
import, and test methods. Let's gather all those together.
|
||||
"""
|
||||
pkg = getModule("uberpackage")
|
||||
testModules = []
|
||||
for testModule in pkg.walkModules():
|
||||
if testModule.name.split(".")[-1].startswith("test_"):
|
||||
testModules.append(testModule)
|
||||
sortedModules = sorted(testModules, key=sorter) # ONE
|
||||
for modinfo in sortedModules:
|
||||
# Now let's find all the classes.
|
||||
module = modinfo.load(None)
|
||||
if module is None:
|
||||
yield modinfo
|
||||
else:
|
||||
testClasses = []
|
||||
for attrib in modinfo.iterAttributes():
|
||||
if runner.isTestCase(attrib.load()):
|
||||
testClasses.append(attrib)
|
||||
sortedClasses = sorted(testClasses, key=sorter) # TWO
|
||||
for clsinfo in sortedClasses:
|
||||
testMethods = []
|
||||
for attr in clsinfo.iterAttributes():
|
||||
if attr.name.split(".")[-1].startswith("test"):
|
||||
testMethods.append(attr)
|
||||
sortedMethods = sorted(testMethods, key=sorter) # THREE
|
||||
yield from sortedMethods
|
||||
|
||||
def loadSortedPackages(
|
||||
self, sorter: Callable[[runner._Loadable], SupportsRichComparison] = runner.name
|
||||
) -> None:
|
||||
"""
|
||||
Verify that packages are loaded in the correct order.
|
||||
"""
|
||||
import uberpackage # type: ignore[import-not-found]
|
||||
|
||||
self.loader.sorter = sorter
|
||||
suite = self.loader.loadPackage(uberpackage, recurse=True)
|
||||
# XXX: Work around strange, unexplained Zope crap.
|
||||
# jml, 2007-11-15.
|
||||
suite = unittest.decorate(suite, ITestCase)
|
||||
resultingTests = list(_iterateTests(suite))
|
||||
manifest = list(self._trialSortAlgorithm(sorter))
|
||||
for number, (manifestTest, actualTest) in enumerate(
|
||||
zip(manifest, resultingTests)
|
||||
):
|
||||
self.assertEqual(
|
||||
manifestTest.name,
|
||||
actualTest.id(),
|
||||
"#%d: %s != %s" % (number, manifestTest.name, actualTest.id()),
|
||||
)
|
||||
self.assertEqual(len(manifest), len(resultingTests))
|
||||
|
||||
def test_sortPackagesDefaultOrder(self) -> None:
|
||||
self.loadSortedPackages()
|
||||
|
||||
def test_sortPackagesSillyOrder(self) -> None:
|
||||
def sillySorter(s: runner._Loadable) -> str:
|
||||
# This has to work on fully-qualified class names and class
|
||||
# objects, which is silly, but it's the "spec", such as it is.
|
||||
# if isinstance(s, type):
|
||||
# return s.__module__+'.'+s.__name__
|
||||
n = runner.name(s)
|
||||
d = md5(n.encode("utf8")).hexdigest()
|
||||
return d
|
||||
|
||||
self.loadSortedPackages(sillySorter)
|
||||
@@ -0,0 +1,270 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test the interaction between trial and errors logged during test run.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from twisted.internet import reactor, task
|
||||
from twisted.python import failure, log
|
||||
from twisted.trial import _synctest, reporter, unittest
|
||||
|
||||
|
||||
def makeFailure():
|
||||
"""
|
||||
Return a new, realistic failure.
|
||||
"""
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
f = failure.Failure()
|
||||
return f
|
||||
|
||||
|
||||
class Mask:
|
||||
"""
|
||||
Hide C{MockTest}s from Trial's automatic test finder.
|
||||
"""
|
||||
|
||||
class FailureLoggingMixin:
|
||||
def test_silent(self):
|
||||
"""
|
||||
Don't log any errors.
|
||||
"""
|
||||
|
||||
def test_single(self):
|
||||
"""
|
||||
Log a single error.
|
||||
"""
|
||||
log.err(makeFailure())
|
||||
|
||||
def test_double(self):
|
||||
"""
|
||||
Log two errors.
|
||||
"""
|
||||
log.err(makeFailure())
|
||||
log.err(makeFailure())
|
||||
|
||||
def test_singleThenFail(self):
|
||||
"""
|
||||
Log a single error, then fail.
|
||||
"""
|
||||
log.err(makeFailure())
|
||||
1 + None
|
||||
|
||||
class SynchronousFailureLogging(FailureLoggingMixin, unittest.SynchronousTestCase):
|
||||
pass
|
||||
|
||||
class AsynchronousFailureLogging(FailureLoggingMixin, unittest.TestCase):
|
||||
def test_inCallback(self):
|
||||
"""
|
||||
Log an error in an asynchronous callback.
|
||||
"""
|
||||
return task.deferLater(reactor, 0, lambda: log.err(makeFailure()))
|
||||
|
||||
|
||||
class ObserverTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{_synctest._LogObserver}, a helper for the implementation of
|
||||
L{SynchronousTestCase.flushLoggedErrors}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.result = reporter.TestResult()
|
||||
self.observer = _synctest._LogObserver()
|
||||
|
||||
def test_msg(self):
|
||||
"""
|
||||
Test that a standard log message doesn't go anywhere near the result.
|
||||
"""
|
||||
self.observer.gotEvent(
|
||||
{
|
||||
"message": ("some message",),
|
||||
"time": time.time(),
|
||||
"isError": 0,
|
||||
"system": "-",
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.observer.getErrors(), [])
|
||||
|
||||
def test_error(self):
|
||||
"""
|
||||
Test that an observed error gets added to the result
|
||||
"""
|
||||
f = makeFailure()
|
||||
self.observer.gotEvent(
|
||||
{
|
||||
"message": (),
|
||||
"time": time.time(),
|
||||
"isError": 1,
|
||||
"system": "-",
|
||||
"failure": f,
|
||||
"why": None,
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.observer.getErrors(), [f])
|
||||
|
||||
def test_flush(self):
|
||||
"""
|
||||
Check that flushing the observer with no args removes all errors.
|
||||
"""
|
||||
self.test_error()
|
||||
flushed = self.observer.flushErrors()
|
||||
self.assertEqual(self.observer.getErrors(), [])
|
||||
self.assertEqual(len(flushed), 1)
|
||||
self.assertTrue(flushed[0].check(ZeroDivisionError))
|
||||
|
||||
def _makeRuntimeFailure(self):
|
||||
return failure.Failure(RuntimeError("test error"))
|
||||
|
||||
def test_flushByType(self):
|
||||
"""
|
||||
Check that flushing the observer remove all failures of the given type.
|
||||
"""
|
||||
self.test_error() # log a ZeroDivisionError to the observer
|
||||
f = self._makeRuntimeFailure()
|
||||
self.observer.gotEvent(
|
||||
dict(
|
||||
message=(), time=time.time(), isError=1, system="-", failure=f, why=None
|
||||
)
|
||||
)
|
||||
flushed = self.observer.flushErrors(ZeroDivisionError)
|
||||
self.assertEqual(self.observer.getErrors(), [f])
|
||||
self.assertEqual(len(flushed), 1)
|
||||
self.assertTrue(flushed[0].check(ZeroDivisionError))
|
||||
|
||||
def test_ignoreErrors(self):
|
||||
"""
|
||||
Check that C{_ignoreErrors} actually causes errors to be ignored.
|
||||
"""
|
||||
self.observer._ignoreErrors(ZeroDivisionError)
|
||||
f = makeFailure()
|
||||
self.observer.gotEvent(
|
||||
{
|
||||
"message": (),
|
||||
"time": time.time(),
|
||||
"isError": 1,
|
||||
"system": "-",
|
||||
"failure": f,
|
||||
"why": None,
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.observer.getErrors(), [])
|
||||
|
||||
def test_clearIgnores(self):
|
||||
"""
|
||||
Check that C{_clearIgnores} ensures that previously ignored errors
|
||||
get captured.
|
||||
"""
|
||||
self.observer._ignoreErrors(ZeroDivisionError)
|
||||
self.observer._clearIgnores()
|
||||
f = makeFailure()
|
||||
self.observer.gotEvent(
|
||||
{
|
||||
"message": (),
|
||||
"time": time.time(),
|
||||
"isError": 1,
|
||||
"system": "-",
|
||||
"failure": f,
|
||||
"why": None,
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.observer.getErrors(), [f])
|
||||
|
||||
|
||||
class LogErrorsMixin:
|
||||
"""
|
||||
High-level tests demonstrating the expected behaviour of logged errors
|
||||
during tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.result = reporter.TestResult()
|
||||
|
||||
def tearDown(self):
|
||||
self.flushLoggedErrors(ZeroDivisionError)
|
||||
|
||||
def test_singleError(self):
|
||||
"""
|
||||
Test that a logged error gets reported as a test error.
|
||||
"""
|
||||
test = self.MockTest("test_single")
|
||||
test(self.result)
|
||||
self.assertEqual(len(self.result.errors), 1)
|
||||
self.assertTrue(
|
||||
self.result.errors[0][1].check(ZeroDivisionError), self.result.errors[0][1]
|
||||
)
|
||||
self.assertEqual(0, self.result.successes)
|
||||
|
||||
def test_twoErrors(self):
|
||||
"""
|
||||
Test that when two errors get logged, they both get reported as test
|
||||
errors.
|
||||
"""
|
||||
test = self.MockTest("test_double")
|
||||
test(self.result)
|
||||
self.assertEqual(len(self.result.errors), 2)
|
||||
self.assertEqual(0, self.result.successes)
|
||||
|
||||
def test_errorsIsolated(self):
|
||||
"""
|
||||
Check that an error logged in one test doesn't fail the next test.
|
||||
"""
|
||||
t1 = self.MockTest("test_single")
|
||||
t2 = self.MockTest("test_silent")
|
||||
t1(self.result)
|
||||
t2(self.result)
|
||||
self.assertEqual(len(self.result.errors), 1)
|
||||
self.assertEqual(self.result.errors[0][0], t1)
|
||||
self.assertEqual(1, self.result.successes)
|
||||
|
||||
def test_errorsIsolatedWhenTestFails(self):
|
||||
"""
|
||||
An error logged in a failed test doesn't fail the next test.
|
||||
"""
|
||||
t1 = self.MockTest("test_singleThenFail")
|
||||
t2 = self.MockTest("test_silent")
|
||||
t1(self.result)
|
||||
t2(self.result)
|
||||
|
||||
self.assertEqual(len(self.result.errors), 2)
|
||||
self.assertEqual(self.result.errors[0][0], t1)
|
||||
self.result.errors[0][1].trap(TypeError)
|
||||
|
||||
self.assertEqual(self.result.errors[1][0], t1)
|
||||
self.result.errors[1][1].trap(ZeroDivisionError)
|
||||
|
||||
self.assertEqual(1, self.result.successes)
|
||||
|
||||
def test_boundedObservers(self):
|
||||
"""
|
||||
There are no extra log observers after a test runs.
|
||||
"""
|
||||
# XXX trial is *all about* global log state. It should really be fixed.
|
||||
observer = _synctest._LogObserver()
|
||||
self.patch(_synctest, "_logObserver", observer)
|
||||
observers = log.theLogPublisher.observers[:]
|
||||
test = self.MockTest()
|
||||
test(self.result)
|
||||
self.assertEqual(observers, log.theLogPublisher.observers)
|
||||
|
||||
|
||||
class SynchronousLogErrorsTests(LogErrorsMixin, unittest.SynchronousTestCase):
|
||||
MockTest = Mask.SynchronousFailureLogging
|
||||
|
||||
|
||||
class AsynchronousLogErrorsTests(LogErrorsMixin, unittest.TestCase):
|
||||
MockTest = Mask.AsynchronousFailureLogging
|
||||
|
||||
def test_inCallback(self):
|
||||
"""
|
||||
Test that errors logged in callbacks get reported as test errors.
|
||||
"""
|
||||
test = self.MockTest("test_inCallback")
|
||||
test(self.result)
|
||||
self.assertEqual(len(self.result.errors), 1)
|
||||
self.assertTrue(
|
||||
self.result.errors[0][1].check(ZeroDivisionError), self.result.errors[0][1]
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Tests for L{twisted.trial.test.matchers}.
|
||||
"""
|
||||
from hamcrest import anything, assert_that, contains_string, equal_to, not_
|
||||
from hamcrest.core.core.allof import AllOf
|
||||
from hamcrest.core.string_description import StringDescription
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import just, sampled_from, text
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .matchers import fileContents
|
||||
|
||||
|
||||
class FileContentsTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{fileContents}.
|
||||
"""
|
||||
|
||||
@given(text(), just("utf-8"))
|
||||
def test_matches(self, contents: str, encoding: str) -> None:
|
||||
"""
|
||||
L{fileContents} matches a L{IFilePath} that refers to a file that
|
||||
contains a string that is matched by the parameterized matcher.
|
||||
|
||||
:param contents: The text string to place in the file and match
|
||||
against.
|
||||
|
||||
:param encoding: The text encoding to use to encode C{contents} when
|
||||
writing to the file.
|
||||
"""
|
||||
p = FilePath(self.mktemp())
|
||||
p.setContent(contents.encode(encoding))
|
||||
|
||||
description = StringDescription()
|
||||
assert_that(
|
||||
fileContents(equal_to(contents)).matches(p, description), equal_to(True)
|
||||
)
|
||||
assert_that(str(description), equal_to(""))
|
||||
|
||||
@given(
|
||||
just("some text, it doesn't matter what"),
|
||||
sampled_from(["ascii", "latin-1", "utf-8"]),
|
||||
)
|
||||
def test_mismatches(self, contents: str, encoding: str) -> None:
|
||||
"""
|
||||
L{fileContents} does not match an L{IFilePath} that refers to a
|
||||
file that contains a string that is not matched by the parameterized
|
||||
matcher.
|
||||
|
||||
:param contents: The text string to place in the file and match
|
||||
against.
|
||||
|
||||
:param encoding: The text encoding to use to encode C{contents} when
|
||||
writing to the file.
|
||||
"""
|
||||
p = FilePath(self.mktemp())
|
||||
p.setContent(contents.encode(encoding))
|
||||
|
||||
description = StringDescription()
|
||||
assert_that(
|
||||
fileContents(not_(anything())).matches(p, description), equal_to(False)
|
||||
)
|
||||
assert_that(str(description), equal_to(f"was <{p}>"))
|
||||
|
||||
def test_ioerror(self) -> None:
|
||||
"""
|
||||
L{fileContents} reports details of any I/O error encountered while
|
||||
attempting to match.
|
||||
"""
|
||||
p = FilePath(self.mktemp())
|
||||
|
||||
description = StringDescription()
|
||||
assert_that(fileContents(anything()).matches(p, description), equal_to(False))
|
||||
assert_that(
|
||||
str(description),
|
||||
# It must contain at least ...
|
||||
AllOf(
|
||||
# the name of the matcher.
|
||||
contains_string("fileContents"),
|
||||
# the name of the exception raised.
|
||||
contains_string("FileNotFoundError"),
|
||||
# the repr (so weird values are escaped) of the path being
|
||||
# matched against.
|
||||
contains_string(repr(p.path)),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the output generated by trial.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from io import StringIO
|
||||
from typing import TypeVar
|
||||
|
||||
from twisted.scripts import trial
|
||||
from twisted.trial import runner
|
||||
from twisted.trial.test import packages
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
_noModuleError = "No module named 'frotz'"
|
||||
|
||||
|
||||
def runTrial(*args: str) -> str:
|
||||
from twisted.trial import reporter
|
||||
|
||||
config = trial.Options()
|
||||
config.parseOptions(args)
|
||||
output = StringIO()
|
||||
myRunner = runner.TrialRunner(
|
||||
reporter.VerboseTextReporter,
|
||||
stream=output,
|
||||
workingDirectory=config["temp-directory"],
|
||||
)
|
||||
suite = trial._getSuite(config)
|
||||
myRunner.run(suite)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class ImportErrorsTests(packages.SysPathManglingTest):
|
||||
"""Actually run trial as if on the command line and check that the output
|
||||
is what we expect.
|
||||
"""
|
||||
|
||||
def debug(self) -> None:
|
||||
pass
|
||||
|
||||
parent = "_testImportErrors"
|
||||
|
||||
def runTrial(self, *args: str) -> str:
|
||||
return runTrial("--temp-directory", self.mktemp(), *args)
|
||||
|
||||
def _print(self, stuff: _T) -> _T:
|
||||
print(stuff)
|
||||
return stuff
|
||||
|
||||
def assertIn( # type: ignore[override]
|
||||
self, container: str, containee: str, *args: object, **kwargs: object
|
||||
) -> str:
|
||||
# redefined to be useful in callbacks
|
||||
super().assertIn(containee, container, *args, **kwargs)
|
||||
return container
|
||||
|
||||
def assertNotIn( # type: ignore[override]
|
||||
self, container: str, containee: str, *args: object, **kwargs: object
|
||||
) -> str:
|
||||
# redefined to be useful in callbacks
|
||||
super().assertNotIn(containee, container, *args, **kwargs)
|
||||
return container
|
||||
|
||||
def test_trialRun(self) -> None:
|
||||
self.runTrial()
|
||||
|
||||
def test_nonexistentModule(self) -> str:
|
||||
d = self.runTrial("twisted.doesntexist")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "twisted.doesntexist")
|
||||
return d
|
||||
|
||||
def test_nonexistentPackage(self) -> str:
|
||||
d = self.runTrial("doesntexist")
|
||||
self.assertIn(d, "doesntexist")
|
||||
self.assertIn(d, "ModuleNotFound")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
return d
|
||||
|
||||
def test_nonexistentPackageWithModule(self) -> str:
|
||||
d = self.runTrial("doesntexist.barney")
|
||||
self.assertIn(d, "doesntexist.barney")
|
||||
self.assertIn(d, "ObjectNotFound")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
return d
|
||||
|
||||
def test_badpackage(self) -> str:
|
||||
d = self.runTrial("badpackage")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "badpackage")
|
||||
self.assertNotIn(d, "IOError")
|
||||
return d
|
||||
|
||||
def test_moduleInBadpackage(self) -> str:
|
||||
d = self.runTrial("badpackage.test_module")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "badpackage.test_module")
|
||||
self.assertNotIn(d, "IOError")
|
||||
return d
|
||||
|
||||
def test_badmodule(self) -> str:
|
||||
d = self.runTrial("package.test_bad_module")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "package.test_bad_module")
|
||||
self.assertNotIn(d, "IOError")
|
||||
self.assertNotIn(d, "<module ")
|
||||
return d
|
||||
|
||||
def test_badimport(self) -> str:
|
||||
d = self.runTrial("package.test_import_module")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "package.test_import_module")
|
||||
self.assertNotIn(d, "IOError")
|
||||
self.assertNotIn(d, "<module ")
|
||||
return d
|
||||
|
||||
def test_recurseImport(self) -> str:
|
||||
d = self.runTrial("package")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "test_bad_module")
|
||||
self.assertIn(d, "test_import_module")
|
||||
self.assertNotIn(d, "<module ")
|
||||
self.assertNotIn(d, "IOError")
|
||||
return d
|
||||
|
||||
def test_recurseImportErrors(self) -> str:
|
||||
d = self.runTrial("package2")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, "package2")
|
||||
self.assertIn(d, "test_module")
|
||||
self.assertIn(d, _noModuleError)
|
||||
self.assertNotIn(d, "<module ")
|
||||
self.assertNotIn(d, "IOError")
|
||||
return d
|
||||
|
||||
def test_nonRecurseImportErrors(self) -> str:
|
||||
d = self.runTrial("-N", "package2")
|
||||
self.assertIn(d, "[ERROR]")
|
||||
self.assertIn(d, _noModuleError)
|
||||
self.assertNotIn(d, "<module ")
|
||||
return d
|
||||
|
||||
def test_regularRun(self) -> str:
|
||||
d = self.runTrial("package.test_module")
|
||||
self.assertNotIn(d, "[ERROR]")
|
||||
self.assertNotIn(d, "IOError")
|
||||
self.assertIn(d, "OK")
|
||||
self.assertIn(d, "PASSED (successes=1)")
|
||||
return d
|
||||
|
||||
def test_filename(self) -> str:
|
||||
self.mangleSysPath(self.oldPath)
|
||||
d = self.runTrial(os.path.join(self.parent, "package", "test_module.py"))
|
||||
self.assertNotIn(d, "[ERROR]")
|
||||
self.assertNotIn(d, "IOError")
|
||||
self.assertIn(d, "OK")
|
||||
self.assertIn(d, "PASSED (successes=1)")
|
||||
return d
|
||||
|
||||
def test_dosFile(self) -> str:
|
||||
## XXX -- not really an output test, more of a script test
|
||||
self.mangleSysPath(self.oldPath)
|
||||
d = self.runTrial(os.path.join(self.parent, "package", "test_dos_module.py"))
|
||||
self.assertNotIn(d, "[ERROR]")
|
||||
self.assertNotIn(d, "IOError")
|
||||
self.assertIn(d, "OK")
|
||||
self.assertIn(d, "PASSED (successes=1)")
|
||||
return d
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
# Maintainer: Jonathan Lange
|
||||
|
||||
"""
|
||||
Tests for L{twisted.plugins.twisted_trial}.
|
||||
"""
|
||||
|
||||
from twisted.plugin import getPlugins
|
||||
from twisted.trial import unittest
|
||||
from twisted.trial.itrial import IReporter
|
||||
|
||||
|
||||
class PluginsTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for Trial's reporter plugins.
|
||||
"""
|
||||
|
||||
def getPluginsByLongOption(self, longOption):
|
||||
"""
|
||||
Return the Trial reporter plugin with the given long option.
|
||||
|
||||
If more than one is found, raise ValueError. If none are found, raise
|
||||
IndexError.
|
||||
"""
|
||||
plugins = [
|
||||
plugin for plugin in getPlugins(IReporter) if plugin.longOpt == longOption
|
||||
]
|
||||
if len(plugins) > 1:
|
||||
raise ValueError(
|
||||
"More than one plugin found with long option %r: %r"
|
||||
% (longOption, plugins)
|
||||
)
|
||||
return plugins[0]
|
||||
|
||||
def test_subunitPlugin(self) -> None:
|
||||
"""
|
||||
One of the reporter plugins is the subunit reporter plugin.
|
||||
"""
|
||||
subunitPlugin = self.getPluginsByLongOption("subunit")
|
||||
self.assertEqual("Subunit Reporter", subunitPlugin.name)
|
||||
self.assertEqual("twisted.trial.reporter", subunitPlugin.module)
|
||||
self.assertEqual("subunit", subunitPlugin.longOpt)
|
||||
self.assertIdentical(None, subunitPlugin.shortOpt)
|
||||
self.assertEqual("SubunitReporter", subunitPlugin.klass)
|
||||
@@ -0,0 +1,257 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
import unittest as pyunit
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.itrial import IReporter, ITestCase
|
||||
from twisted.trial.test import pyunitcases
|
||||
from twisted.trial.unittest import PyUnitResultAdapter, SynchronousTestCase
|
||||
|
||||
|
||||
class PyUnitTestTests(SynchronousTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.original = pyunitcases.PyUnitTest("test_pass")
|
||||
self.test = ITestCase(self.original)
|
||||
|
||||
def test_callable(self) -> None:
|
||||
"""
|
||||
Tests must be callable in order to be used with Python's unittest.py.
|
||||
"""
|
||||
self.assertTrue(callable(self.test), f"{self.test!r} is not callable.")
|
||||
|
||||
|
||||
class PyUnitResultTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests to show that PyUnitResultAdapter wraps TestResult objects from the
|
||||
standard library 'unittest' module in such a way as to make them usable and
|
||||
useful from Trial.
|
||||
"""
|
||||
|
||||
# Once erroneous is ported to Python 3 this can be replaced with
|
||||
# erroneous.ErrorTest:
|
||||
class ErrorTest(SynchronousTestCase):
|
||||
"""
|
||||
A test case which has a L{test_foo} which will raise an error.
|
||||
|
||||
@ivar ran: boolean indicating whether L{test_foo} has been run.
|
||||
"""
|
||||
|
||||
ran = False
|
||||
|
||||
def test_foo(self) -> None:
|
||||
"""
|
||||
Set C{self.ran} to True and raise a C{ZeroDivisionError}
|
||||
"""
|
||||
self.ran = True
|
||||
1 / 0
|
||||
|
||||
def test_dontUseAdapterWhenReporterProvidesIReporter(self) -> None:
|
||||
"""
|
||||
The L{PyUnitResultAdapter} is only used when the result passed to
|
||||
C{run} does *not* provide L{IReporter}.
|
||||
"""
|
||||
|
||||
@implementer(IReporter)
|
||||
class StubReporter: # type: ignore[misc]
|
||||
"""
|
||||
A reporter which records data about calls made to it.
|
||||
|
||||
@ivar errors: Errors passed to L{addError}.
|
||||
@ivar failures: Failures passed to L{addFailure}.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.errors: list[Failure] = []
|
||||
self.failures: list[None] = []
|
||||
|
||||
def startTest(self, test: object) -> None:
|
||||
"""
|
||||
Do nothing.
|
||||
"""
|
||||
|
||||
def stopTest(self, test: object) -> None:
|
||||
"""
|
||||
Do nothing.
|
||||
"""
|
||||
|
||||
def addError(self, test: object, error: Failure) -> None:
|
||||
"""
|
||||
Record the error.
|
||||
"""
|
||||
self.errors.append(error)
|
||||
|
||||
test = self.ErrorTest("test_foo")
|
||||
result = StubReporter()
|
||||
test.run(result)
|
||||
self.assertIsInstance(result.errors[0], Failure)
|
||||
|
||||
def test_success(self) -> None:
|
||||
class SuccessTest(SynchronousTestCase):
|
||||
ran = False
|
||||
|
||||
def test_foo(s) -> None:
|
||||
s.ran = True
|
||||
|
||||
test = SuccessTest("test_foo")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
|
||||
self.assertTrue(test.ran)
|
||||
self.assertEqual(1, result.testsRun)
|
||||
self.assertTrue(result.wasSuccessful())
|
||||
|
||||
def test_failure(self) -> None:
|
||||
class FailureTest(SynchronousTestCase):
|
||||
ran = False
|
||||
|
||||
def test_foo(s) -> None:
|
||||
s.ran = True
|
||||
s.fail("boom!")
|
||||
|
||||
test = FailureTest("test_foo")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
|
||||
self.assertTrue(test.ran)
|
||||
self.assertEqual(1, result.testsRun)
|
||||
self.assertEqual(1, len(result.failures))
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
|
||||
def test_error(self) -> None:
|
||||
test = self.ErrorTest("test_foo")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
|
||||
self.assertTrue(test.ran)
|
||||
self.assertEqual(1, result.testsRun)
|
||||
self.assertEqual(1, len(result.errors))
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
|
||||
def test_setUpError(self) -> None:
|
||||
class ErrorTest(SynchronousTestCase):
|
||||
ran = False
|
||||
|
||||
def setUp(self) -> None:
|
||||
1 / 0
|
||||
|
||||
def test_foo(s) -> None:
|
||||
s.ran = True
|
||||
|
||||
test = ErrorTest("test_foo")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
|
||||
self.assertFalse(test.ran)
|
||||
self.assertEqual(1, result.testsRun)
|
||||
self.assertEqual(1, len(result.errors))
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
|
||||
def test_tracebackFromFailure(self) -> None:
|
||||
"""
|
||||
Errors added through the L{PyUnitResultAdapter} have the same traceback
|
||||
information as if there were no adapter at all.
|
||||
"""
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
exc_info = sys.exc_info()
|
||||
f = Failure()
|
||||
pyresult = pyunit.TestResult()
|
||||
result = PyUnitResultAdapter(pyresult)
|
||||
result.addError(self, f)
|
||||
self.assertEqual(
|
||||
pyresult.errors[0][1], "".join(traceback.format_exception(*exc_info))
|
||||
)
|
||||
|
||||
def test_traceback(self) -> None:
|
||||
"""
|
||||
As test_tracebackFromFailure, but covering more code.
|
||||
"""
|
||||
|
||||
class ErrorTest(SynchronousTestCase):
|
||||
exc_info = None
|
||||
|
||||
def test_foo(self) -> None:
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
self.exc_info = sys.exc_info()
|
||||
raise
|
||||
|
||||
test = ErrorTest("test_foo")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
|
||||
# We can't test that the tracebacks are equal, because Trial's
|
||||
# machinery inserts a few extra frames on the top and we don't really
|
||||
# want to trim them off without an extremely good reason.
|
||||
#
|
||||
# So, we just test that the result's stack ends with the
|
||||
# exception's stack.
|
||||
assert test.exc_info is not None
|
||||
expected_stack = "".join(traceback.format_tb(test.exc_info[2]))
|
||||
observed_stack = "\n".join(result.errors[0][1].splitlines()[:-1])
|
||||
|
||||
self.assertEqual(
|
||||
expected_stack.strip(), observed_stack[-len(expected_stack) :].strip()
|
||||
)
|
||||
|
||||
def test_tracebackFromCleanFailure(self) -> None:
|
||||
"""
|
||||
Errors added through the L{PyUnitResultAdapter} have the same
|
||||
traceback information as if there were no adapter at all, even
|
||||
if the Failure that held the information has been cleaned.
|
||||
"""
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
exc_info = sys.exc_info()
|
||||
f = Failure()
|
||||
f.cleanFailure()
|
||||
pyresult = pyunit.TestResult()
|
||||
result = PyUnitResultAdapter(pyresult)
|
||||
result.addError(self, f)
|
||||
tback = "".join(traceback.format_exception(*exc_info))
|
||||
self.assertEqual(
|
||||
pyresult.errors[0][1].endswith("ZeroDivisionError: division by zero\n"),
|
||||
tback.endswith("ZeroDivisionError: division by zero\n"),
|
||||
)
|
||||
|
||||
def test_trialSkip(self) -> None:
|
||||
"""
|
||||
Skips using trial's skipping functionality are reported as skips in
|
||||
the L{pyunit.TestResult}.
|
||||
"""
|
||||
|
||||
class SkipTest(SynchronousTestCase):
|
||||
@skipIf(True, "Let's skip!")
|
||||
def test_skip(self) -> None:
|
||||
1 / 0
|
||||
|
||||
test = SkipTest("test_skip")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
self.assertEqual(result.skipped, [(test, "Let's skip!")])
|
||||
|
||||
def test_pyunitSkip(self) -> None:
|
||||
"""
|
||||
Skips using pyunit's skipping functionality are reported as skips in
|
||||
the L{pyunit.TestResult}.
|
||||
"""
|
||||
|
||||
class SkipTest(SynchronousTestCase):
|
||||
@pyunit.skip("skippy")
|
||||
def test_skip(self) -> None:
|
||||
1 / 0
|
||||
|
||||
test = SkipTest("test_skip")
|
||||
result = pyunit.TestResult()
|
||||
test.run(result)
|
||||
self.assertEqual(result.skipped, [(test, "skippy")])
|
||||
File diff suppressed because it is too large
Load Diff
1041
.venv/lib/python3.12/site-packages/twisted/trial/test/test_runner.py
Normal file
1041
.venv/lib/python3.12/site-packages/twisted/trial/test/test_runner.py
Normal file
File diff suppressed because it is too large
Load Diff
1055
.venv/lib/python3.12/site-packages/twisted/trial/test/test_script.py
Normal file
1055
.venv/lib/python3.12/site-packages/twisted/trial/test/test_script.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial.util}
|
||||
"""
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
@skipIf(True, "Skip all tests when @skipIf is used on a class")
|
||||
class SkipDecoratorUsedOnClass(TestCase):
|
||||
"""
|
||||
All tests should be skipped because @skipIf decorator is used on
|
||||
this class.
|
||||
"""
|
||||
|
||||
def test_shouldNeverRun_1(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
def test_shouldNeverRun_2(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
|
||||
@skipIf(True, "")
|
||||
class SkipDecoratorUsedOnClassWithEmptyReason(TestCase):
|
||||
"""
|
||||
All tests should be skipped because @skipIf decorator is used on
|
||||
this class, even if the reason is an empty string
|
||||
"""
|
||||
|
||||
def test_shouldNeverRun_1(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
def test_shouldNeverRun_2(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
|
||||
class SkipDecoratorUsedOnMethods(TestCase):
|
||||
"""
|
||||
Only methods where @skipIf decorator is used should be skipped.
|
||||
"""
|
||||
|
||||
@skipIf(True, "skipIf decorator used so skip test")
|
||||
def test_shouldNeverRun(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
@skipIf(True, "")
|
||||
def test_shouldNeverRunWithEmptyReason(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
def test_shouldShouldRun(self) -> None:
|
||||
self.assertTrue(True, "Test should run and not be skipped")
|
||||
|
||||
@skipIf(False, "should not skip")
|
||||
def test_shouldShouldRunWithSkipIfFalse(self) -> None:
|
||||
self.assertTrue(True, "Test should run and not be skipped")
|
||||
|
||||
@skipIf(False, "")
|
||||
def test_shouldShouldRunWithSkipIfFalseEmptyReason(self) -> None:
|
||||
self.assertTrue(True, "Test should run and not be skipped")
|
||||
|
||||
|
||||
class SkipAttributeOnClass(TestCase):
|
||||
"""
|
||||
All tests should be skipped because skip attribute is set on
|
||||
this class.
|
||||
"""
|
||||
|
||||
skip = "'skip' attribute set on this class, so skip all tests"
|
||||
|
||||
def test_one(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
def test_two(self) -> None:
|
||||
raise Exception("Test should skip and never reach here")
|
||||
|
||||
|
||||
class SkipAttributeOnMethods(TestCase):
|
||||
"""
|
||||
Only methods where @skipIf decorator is used should be skipped.
|
||||
"""
|
||||
|
||||
def test_one(self) -> None:
|
||||
raise Exception("Should never reach here")
|
||||
|
||||
test_one.skip = "skip test, skip attribute set on method" # type: ignore[attr-defined]
|
||||
|
||||
def test_shouldNotSkip(self) -> None:
|
||||
self.assertTrue(True, "Test should run and not be skipped")
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for warning suppression features of Trial.
|
||||
"""
|
||||
|
||||
|
||||
import unittest as pyunit
|
||||
|
||||
from twisted.python.reflect import namedAny
|
||||
from twisted.trial import unittest
|
||||
from twisted.trial.test import suppression
|
||||
|
||||
|
||||
class SuppressionMixin:
|
||||
"""
|
||||
Tests for the warning suppression features of
|
||||
L{twisted.trial.unittest.SynchronousTestCase}.
|
||||
"""
|
||||
|
||||
def runTests(self, suite):
|
||||
suite.run(pyunit.TestResult())
|
||||
|
||||
def _load(self, cls, methodName):
|
||||
"""
|
||||
Return a new L{unittest.TestSuite} with a single test method in it.
|
||||
|
||||
@param cls: A L{TestCase} subclass defining a test method.
|
||||
|
||||
@param methodName: The name of the test method from C{cls}.
|
||||
"""
|
||||
return pyunit.TestSuite([cls(methodName)])
|
||||
|
||||
def _assertWarnings(self, warnings, which):
|
||||
"""
|
||||
Assert that a certain number of warnings with certain messages were
|
||||
emitted in a certain order.
|
||||
|
||||
@param warnings: A list of emitted warnings, as returned by
|
||||
C{flushWarnings}.
|
||||
|
||||
@param which: A list of strings giving warning messages that should
|
||||
appear in C{warnings}.
|
||||
|
||||
@raise self.failureException: If the warning messages given by C{which}
|
||||
do not match the messages in the warning information in C{warnings},
|
||||
or if they do not appear in the same order.
|
||||
"""
|
||||
self.assertEqual([warning["message"] for warning in warnings], which)
|
||||
|
||||
def test_setUpSuppression(self):
|
||||
"""
|
||||
Suppressions defined by the test method being run are applied to any
|
||||
warnings emitted while running the C{setUp} fixture.
|
||||
"""
|
||||
self.runTests(self._load(self.TestSetUpSuppression, "testSuppressMethod"))
|
||||
warningsShown = self.flushWarnings([self.TestSetUpSuppression._emit])
|
||||
self._assertWarnings(
|
||||
warningsShown,
|
||||
[
|
||||
suppression.CLASS_WARNING_MSG,
|
||||
suppression.MODULE_WARNING_MSG,
|
||||
suppression.CLASS_WARNING_MSG,
|
||||
suppression.MODULE_WARNING_MSG,
|
||||
],
|
||||
)
|
||||
|
||||
def test_tearDownSuppression(self):
|
||||
"""
|
||||
Suppressions defined by the test method being run are applied to any
|
||||
warnings emitted while running the C{tearDown} fixture.
|
||||
"""
|
||||
self.runTests(self._load(self.TestTearDownSuppression, "testSuppressMethod"))
|
||||
warningsShown = self.flushWarnings([self.TestTearDownSuppression._emit])
|
||||
self._assertWarnings(
|
||||
warningsShown,
|
||||
[
|
||||
suppression.CLASS_WARNING_MSG,
|
||||
suppression.MODULE_WARNING_MSG,
|
||||
suppression.CLASS_WARNING_MSG,
|
||||
suppression.MODULE_WARNING_MSG,
|
||||
],
|
||||
)
|
||||
|
||||
def test_suppressMethod(self):
|
||||
"""
|
||||
A suppression set on a test method prevents warnings emitted by that
|
||||
test method which the suppression matches from being emitted.
|
||||
"""
|
||||
self.runTests(self._load(self.TestSuppression, "testSuppressMethod"))
|
||||
warningsShown = self.flushWarnings([self.TestSuppression._emit])
|
||||
self._assertWarnings(
|
||||
warningsShown,
|
||||
[suppression.CLASS_WARNING_MSG, suppression.MODULE_WARNING_MSG],
|
||||
)
|
||||
|
||||
def test_suppressClass(self):
|
||||
"""
|
||||
A suppression set on a L{SynchronousTestCase} subclass prevents warnings
|
||||
emitted by any test methods defined on that class which match the
|
||||
suppression from being emitted.
|
||||
"""
|
||||
self.runTests(self._load(self.TestSuppression, "testSuppressClass"))
|
||||
warningsShown = self.flushWarnings([self.TestSuppression._emit])
|
||||
self.assertEqual(warningsShown[0]["message"], suppression.METHOD_WARNING_MSG)
|
||||
self.assertEqual(warningsShown[1]["message"], suppression.MODULE_WARNING_MSG)
|
||||
self.assertEqual(len(warningsShown), 2)
|
||||
|
||||
def test_suppressModule(self):
|
||||
"""
|
||||
A suppression set on a module prevents warnings emitted by any test
|
||||
mewthods defined in that module which match the suppression from being
|
||||
emitted.
|
||||
"""
|
||||
self.runTests(self._load(self.TestSuppression2, "testSuppressModule"))
|
||||
warningsShown = self.flushWarnings([self.TestSuppression._emit])
|
||||
self.assertEqual(warningsShown[0]["message"], suppression.METHOD_WARNING_MSG)
|
||||
self.assertEqual(warningsShown[1]["message"], suppression.CLASS_WARNING_MSG)
|
||||
self.assertEqual(len(warningsShown), 2)
|
||||
|
||||
def test_overrideSuppressClass(self):
|
||||
"""
|
||||
The suppression set on a test method completely overrides a suppression
|
||||
with wider scope; if it does not match a warning emitted by that test
|
||||
method, the warning is emitted, even if a wider suppression matches.
|
||||
"""
|
||||
self.runTests(self._load(self.TestSuppression, "testOverrideSuppressClass"))
|
||||
warningsShown = self.flushWarnings([self.TestSuppression._emit])
|
||||
self.assertEqual(warningsShown[0]["message"], suppression.METHOD_WARNING_MSG)
|
||||
self.assertEqual(warningsShown[1]["message"], suppression.CLASS_WARNING_MSG)
|
||||
self.assertEqual(warningsShown[2]["message"], suppression.MODULE_WARNING_MSG)
|
||||
self.assertEqual(len(warningsShown), 3)
|
||||
|
||||
|
||||
class SynchronousSuppressionTests(SuppressionMixin, unittest.SynchronousTestCase):
|
||||
"""
|
||||
@see: L{twisted.trial.test.test_tests}
|
||||
"""
|
||||
|
||||
TestSetUpSuppression = namedAny(
|
||||
"twisted.trial.test.suppression.SynchronousTestSetUpSuppression"
|
||||
)
|
||||
TestTearDownSuppression = namedAny(
|
||||
"twisted.trial.test.suppression.SynchronousTestTearDownSuppression"
|
||||
)
|
||||
TestSuppression = namedAny(
|
||||
"twisted.trial.test.suppression.SynchronousTestSuppression"
|
||||
)
|
||||
TestSuppression2 = namedAny(
|
||||
"twisted.trial.test.suppression.SynchronousTestSuppression2"
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Direct unit tests for L{twisted.trial.unittest.SynchronousTestCase} and
|
||||
L{twisted.trial.unittest.TestCase}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase, TestCase
|
||||
|
||||
|
||||
class TestCaseMixin:
|
||||
"""
|
||||
L{TestCase} tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a couple instances of C{MyTestCase}, each for the same test
|
||||
method, to be used in the test methods of this class.
|
||||
"""
|
||||
self.first = self.MyTestCase("test_1")
|
||||
self.second = self.MyTestCase("test_1")
|
||||
|
||||
def test_equality(self):
|
||||
"""
|
||||
In order for one test method to be runnable twice, two TestCase
|
||||
instances with the same test method name must not compare as equal.
|
||||
"""
|
||||
self.assertTrue(self.first == self.first)
|
||||
self.assertTrue(self.first != self.second)
|
||||
self.assertFalse(self.first == self.second)
|
||||
|
||||
def test_hashability(self):
|
||||
"""
|
||||
In order for one test method to be runnable twice, two TestCase
|
||||
instances with the same test method name should not have the same
|
||||
hash value.
|
||||
"""
|
||||
container = {}
|
||||
container[self.first] = None
|
||||
container[self.second] = None
|
||||
self.assertEqual(len(container), 2)
|
||||
|
||||
|
||||
class SynchronousTestCaseTests(TestCaseMixin, SynchronousTestCase):
|
||||
class MyTestCase(SynchronousTestCase):
|
||||
"""
|
||||
Some test methods which can be used to test behaviors of
|
||||
L{SynchronousTestCase}.
|
||||
"""
|
||||
|
||||
def test_1(self):
|
||||
pass
|
||||
|
||||
|
||||
# Yes, subclass SynchronousTestCase again. There are no interesting behaviors
|
||||
# of self being tested below, only of self.MyTestCase.
|
||||
class AsynchronousTestCaseTests(TestCaseMixin, SynchronousTestCase):
|
||||
class MyTestCase(TestCase):
|
||||
"""
|
||||
Some test methods which can be used to test behaviors of
|
||||
L{TestCase}.
|
||||
"""
|
||||
|
||||
def test_1(self):
|
||||
pass
|
||||
1468
.venv/lib/python3.12/site-packages/twisted/trial/test/test_tests.py
Normal file
1468
.venv/lib/python3.12/site-packages/twisted/trial/test/test_tests.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,652 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial.util}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
from typing import Generator
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from hamcrest import assert_that, equal_to
|
||||
|
||||
from twisted.internet.base import DelayedCall
|
||||
from twisted.internet.interfaces import IProcessTransport
|
||||
from twisted.python import filepath
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial import util
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from twisted.trial.util import (
|
||||
DirtyReactorAggregateError,
|
||||
_Janitor,
|
||||
acquireAttribute,
|
||||
excInfoOrFailureToExcInfo,
|
||||
openTestLog,
|
||||
)
|
||||
|
||||
|
||||
class MktempTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{TestCase.mktemp}, a helper function for creating temporary file
|
||||
or directory names.
|
||||
"""
|
||||
|
||||
def test_name(self) -> None:
|
||||
"""
|
||||
The path name returned by C{mktemp} is directly beneath a directory
|
||||
which identifies the test method which created the name.
|
||||
"""
|
||||
name = self.mktemp()
|
||||
dirs = os.path.dirname(name).split(os.sep)[:-1]
|
||||
self.assertEqual(
|
||||
dirs, ["twisted.trial.test.test_util", "MktempTests", "test_name"]
|
||||
)
|
||||
|
||||
def test_unique(self) -> None:
|
||||
"""
|
||||
Repeated calls to C{mktemp} return different values.
|
||||
"""
|
||||
name = self.mktemp()
|
||||
self.assertNotEqual(name, self.mktemp())
|
||||
|
||||
def test_created(self) -> None:
|
||||
"""
|
||||
The directory part of the path name returned by C{mktemp} exists.
|
||||
"""
|
||||
name = self.mktemp()
|
||||
dirname = os.path.dirname(name)
|
||||
self.assertTrue(os.path.exists(dirname))
|
||||
self.assertFalse(os.path.exists(name))
|
||||
|
||||
def test_location(self) -> None:
|
||||
"""
|
||||
The path returned by C{mktemp} is beneath the current working directory.
|
||||
"""
|
||||
path = os.path.abspath(self.mktemp())
|
||||
self.assertTrue(path.startswith(os.getcwd()))
|
||||
|
||||
|
||||
class DirtyReactorAggregateErrorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the L{DirtyReactorAggregateError}.
|
||||
"""
|
||||
|
||||
def test_formatDelayedCall(self) -> None:
|
||||
"""
|
||||
Delayed calls are formatted nicely.
|
||||
"""
|
||||
error = DirtyReactorAggregateError(["Foo", "bar"])
|
||||
self.assertEqual(
|
||||
str(error),
|
||||
"""\
|
||||
Reactor was unclean.
|
||||
DelayedCalls: (set twisted.internet.base.DelayedCall.debug = True to debug)
|
||||
Foo
|
||||
bar""",
|
||||
)
|
||||
|
||||
def test_formatSelectables(self) -> None:
|
||||
"""
|
||||
Selectables are formatted nicely.
|
||||
"""
|
||||
error = DirtyReactorAggregateError([], ["selectable 1", "selectable 2"])
|
||||
self.assertEqual(
|
||||
str(error),
|
||||
"""\
|
||||
Reactor was unclean.
|
||||
Selectables:
|
||||
selectable 1
|
||||
selectable 2""",
|
||||
)
|
||||
|
||||
def test_formatDelayedCallsAndSelectables(self) -> None:
|
||||
"""
|
||||
Both delayed calls and selectables can appear in the same error.
|
||||
"""
|
||||
error = DirtyReactorAggregateError(["bleck", "Boozo"], ["Sel1", "Sel2"])
|
||||
self.assertEqual(
|
||||
str(error),
|
||||
"""\
|
||||
Reactor was unclean.
|
||||
DelayedCalls: (set twisted.internet.base.DelayedCall.debug = True to debug)
|
||||
bleck
|
||||
Boozo
|
||||
Selectables:
|
||||
Sel1
|
||||
Sel2""",
|
||||
)
|
||||
|
||||
|
||||
class StubReactor:
|
||||
"""
|
||||
A reactor stub which contains enough functionality to be used with the
|
||||
L{_Janitor}.
|
||||
|
||||
@ivar iterations: A list of the arguments passed to L{iterate}.
|
||||
@ivar removeAllCalled: Number of times that L{removeAll} was called.
|
||||
@ivar selectables: The value that will be returned from L{removeAll}.
|
||||
@ivar delayedCalls: The value to return from L{getDelayedCalls}.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, delayedCalls: list[DelayedCall], selectables: list[object] | None = None
|
||||
) -> None:
|
||||
"""
|
||||
@param delayedCalls: See L{StubReactor.delayedCalls}.
|
||||
@param selectables: See L{StubReactor.selectables}.
|
||||
"""
|
||||
self.delayedCalls = delayedCalls
|
||||
self.iterations: list[float | None] = []
|
||||
self.removeAllCalled = 0
|
||||
if not selectables:
|
||||
selectables = []
|
||||
self.selectables = selectables
|
||||
|
||||
def iterate(self, timeout: float | None = None) -> None:
|
||||
"""
|
||||
Increment C{self.iterations}.
|
||||
"""
|
||||
self.iterations.append(timeout)
|
||||
|
||||
def getDelayedCalls(self) -> list[DelayedCall]:
|
||||
"""
|
||||
Return C{self.delayedCalls}.
|
||||
"""
|
||||
return self.delayedCalls
|
||||
|
||||
def removeAll(self) -> list[object]:
|
||||
"""
|
||||
Increment C{self.removeAllCalled} and return C{self.selectables}.
|
||||
"""
|
||||
self.removeAllCalled += 1
|
||||
return self.selectables
|
||||
|
||||
|
||||
class StubErrorReporter:
|
||||
"""
|
||||
A subset of L{twisted.trial.itrial.IReporter} which records L{addError}
|
||||
calls.
|
||||
|
||||
@ivar errors: List of two-tuples of (test, error) which were passed to
|
||||
L{addError}.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.errors: list[tuple[object, Failure]] = []
|
||||
|
||||
def addError(self, test: object, error: Failure) -> None:
|
||||
"""
|
||||
Record parameters in C{self.errors}.
|
||||
"""
|
||||
self.errors.append((test, error))
|
||||
|
||||
|
||||
class JanitorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{_Janitor}!
|
||||
"""
|
||||
|
||||
def test_cleanPendingSpinsReactor(self) -> None:
|
||||
"""
|
||||
During pending-call cleanup, the reactor will be spun twice with an
|
||||
instant timeout. This is not a requirement, it is only a test for
|
||||
current behavior. Hopefully Trial will eventually not do this kind of
|
||||
reactor stuff.
|
||||
"""
|
||||
reactor = StubReactor([])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
jan._cleanPending()
|
||||
self.assertEqual(reactor.iterations, [0, 0])
|
||||
|
||||
def test_cleanPendingCancelsCalls(self) -> None:
|
||||
"""
|
||||
During pending-call cleanup, the janitor cancels pending timed calls.
|
||||
"""
|
||||
|
||||
def func() -> str:
|
||||
return "Lulz"
|
||||
|
||||
cancelled: list[DelayedCall] = []
|
||||
delayedCall = DelayedCall(300, func, (), {}, cancelled.append, lambda x: None)
|
||||
reactor = StubReactor([delayedCall])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
jan._cleanPending()
|
||||
self.assertEqual(cancelled, [delayedCall])
|
||||
|
||||
def test_cleanPendingReturnsDelayedCallStrings(self) -> None:
|
||||
"""
|
||||
The Janitor produces string representations of delayed calls from the
|
||||
delayed call cleanup method. It gets the string representations
|
||||
*before* cancelling the calls; this is important because cancelling the
|
||||
call removes critical debugging information from the string
|
||||
representation.
|
||||
"""
|
||||
delayedCall = DelayedCall(
|
||||
300, lambda: None, (), {}, lambda x: None, lambda x: None, seconds=lambda: 0
|
||||
)
|
||||
delayedCallString = str(delayedCall)
|
||||
reactor = StubReactor([delayedCall])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
strings = jan._cleanPending()
|
||||
self.assertEqual(strings, [delayedCallString])
|
||||
|
||||
def test_cleanReactorRemovesSelectables(self) -> None:
|
||||
"""
|
||||
The Janitor will remove selectables during reactor cleanup.
|
||||
"""
|
||||
reactor = StubReactor([])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
jan._cleanReactor()
|
||||
self.assertEqual(reactor.removeAllCalled, 1)
|
||||
|
||||
def test_cleanReactorKillsProcesses(self) -> None:
|
||||
"""
|
||||
The Janitor will kill processes during reactor cleanup.
|
||||
"""
|
||||
|
||||
@implementer(IProcessTransport)
|
||||
class StubProcessTransport: # type: ignore[misc]
|
||||
"""
|
||||
A stub L{IProcessTransport} provider which records signals.
|
||||
@ivar signals: The signals passed to L{signalProcess}.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.signals: list[str | int] = []
|
||||
|
||||
def signalProcess(self, signal: str | int) -> None:
|
||||
"""
|
||||
Append C{signal} to C{self.signals}.
|
||||
"""
|
||||
self.signals.append(signal)
|
||||
|
||||
pt = StubProcessTransport()
|
||||
reactor = StubReactor([], [pt])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
jan._cleanReactor()
|
||||
self.assertEqual(pt.signals, ["KILL"])
|
||||
|
||||
def test_cleanReactorReturnsSelectableStrings(self) -> None:
|
||||
"""
|
||||
The Janitor returns string representations of the selectables that it
|
||||
cleaned up from the reactor cleanup method.
|
||||
"""
|
||||
|
||||
class Selectable:
|
||||
"""
|
||||
A stub Selectable which only has an interesting string
|
||||
representation.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "(SELECTABLE!)"
|
||||
|
||||
reactor = StubReactor([], [Selectable()])
|
||||
jan = _Janitor(None, None, reactor=reactor)
|
||||
self.assertEqual(jan._cleanReactor(), ["(SELECTABLE!)"])
|
||||
|
||||
def test_postCaseCleanupNoErrors(self) -> None:
|
||||
"""
|
||||
The post-case cleanup method will return True and not call C{addError}
|
||||
on the result if there are no pending calls.
|
||||
"""
|
||||
reactor = StubReactor([])
|
||||
test = object()
|
||||
reporter = StubErrorReporter()
|
||||
jan = _Janitor(test, reporter, reactor=reactor)
|
||||
self.assertTrue(jan.postCaseCleanup())
|
||||
self.assertEqual(reporter.errors, [])
|
||||
|
||||
def test_postCaseCleanupWithErrors(self) -> None:
|
||||
"""
|
||||
The post-case cleanup method will return False and call C{addError} on
|
||||
the result with a L{DirtyReactorAggregateError} Failure if there are
|
||||
pending calls.
|
||||
"""
|
||||
delayedCall = DelayedCall(
|
||||
300, lambda: None, (), {}, lambda x: None, lambda x: None, seconds=lambda: 0
|
||||
)
|
||||
delayedCallString = str(delayedCall)
|
||||
reactor = StubReactor([delayedCall], [])
|
||||
test = object()
|
||||
reporter = StubErrorReporter()
|
||||
jan = _Janitor(test, reporter, reactor=reactor)
|
||||
self.assertFalse(jan.postCaseCleanup())
|
||||
self.assertEqual(len(reporter.errors), 1)
|
||||
self.assertEqual(reporter.errors[0][1].value.delayedCalls, [delayedCallString])
|
||||
|
||||
def test_postClassCleanupNoErrors(self) -> None:
|
||||
"""
|
||||
The post-class cleanup method will not call C{addError} on the result
|
||||
if there are no pending calls or selectables.
|
||||
"""
|
||||
reactor = StubReactor([])
|
||||
test = object()
|
||||
reporter = StubErrorReporter()
|
||||
jan = _Janitor(test, reporter, reactor=reactor)
|
||||
jan.postClassCleanup()
|
||||
self.assertEqual(reporter.errors, [])
|
||||
|
||||
def test_postClassCleanupWithPendingCallErrors(self) -> None:
|
||||
"""
|
||||
The post-class cleanup method call C{addError} on the result with a
|
||||
L{DirtyReactorAggregateError} Failure if there are pending calls.
|
||||
"""
|
||||
delayedCall = DelayedCall(
|
||||
300, lambda: None, (), {}, lambda x: None, lambda x: None, seconds=lambda: 0
|
||||
)
|
||||
delayedCallString = str(delayedCall)
|
||||
reactor = StubReactor([delayedCall], [])
|
||||
test = object()
|
||||
reporter = StubErrorReporter()
|
||||
jan = _Janitor(test, reporter, reactor=reactor)
|
||||
jan.postClassCleanup()
|
||||
self.assertEqual(len(reporter.errors), 1)
|
||||
self.assertEqual(reporter.errors[0][1].value.delayedCalls, [delayedCallString])
|
||||
|
||||
def test_postClassCleanupWithSelectableErrors(self) -> None:
|
||||
"""
|
||||
The post-class cleanup method call C{addError} on the result with a
|
||||
L{DirtyReactorAggregateError} Failure if there are selectables.
|
||||
"""
|
||||
selectable = "SELECTABLE HERE"
|
||||
reactor = StubReactor([], [selectable])
|
||||
test = object()
|
||||
reporter = StubErrorReporter()
|
||||
jan = _Janitor(test, reporter, reactor=reactor)
|
||||
jan.postClassCleanup()
|
||||
self.assertEqual(len(reporter.errors), 1)
|
||||
self.assertEqual(reporter.errors[0][1].value.selectables, [repr(selectable)])
|
||||
|
||||
|
||||
class RemoveSafelyTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{util._removeSafely}.
|
||||
"""
|
||||
|
||||
def test_removeSafelyNoTrialMarker(self) -> None:
|
||||
"""
|
||||
If a path doesn't contain a node named C{"_trial_marker"}, that path is
|
||||
not removed by L{util._removeSafely} and a L{util._NoTrialMarker}
|
||||
exception is raised instead.
|
||||
"""
|
||||
directory = self.mktemp().encode("utf-8")
|
||||
os.mkdir(directory)
|
||||
dirPath = filepath.FilePath(directory)
|
||||
self.assertRaises(util._NoTrialMarker, util._removeSafely, dirPath)
|
||||
|
||||
def test_removeSafelyRemoveFailsMoveSucceeds(self) -> None:
|
||||
"""
|
||||
If an L{OSError} is raised while removing a path in
|
||||
L{util._removeSafely}, an attempt is made to move the path to a new
|
||||
name.
|
||||
"""
|
||||
|
||||
def dummyRemove() -> None:
|
||||
"""
|
||||
Raise an C{OSError} to emulate the branch of L{util._removeSafely}
|
||||
in which path removal fails.
|
||||
"""
|
||||
raise OSError()
|
||||
|
||||
# Patch stdout so we can check the print statements in _removeSafely
|
||||
out = StringIO()
|
||||
self.patch(sys, "stdout", out)
|
||||
|
||||
# Set up a trial directory with a _trial_marker
|
||||
directory = self.mktemp().encode("utf-8")
|
||||
os.mkdir(directory)
|
||||
dirPath = filepath.FilePath(directory)
|
||||
dirPath.child(b"_trial_marker").touch()
|
||||
# Ensure that path.remove() raises an OSError
|
||||
dirPath.remove = dummyRemove # type: ignore[method-assign]
|
||||
|
||||
util._removeSafely(dirPath)
|
||||
self.assertIn("could not remove FilePath", out.getvalue())
|
||||
|
||||
def test_removeSafelyRemoveFailsMoveFails(self) -> None:
|
||||
"""
|
||||
If an L{OSError} is raised while removing a path in
|
||||
L{util._removeSafely}, an attempt is made to move the path to a new
|
||||
name. If that attempt fails, the L{OSError} is re-raised.
|
||||
"""
|
||||
|
||||
def dummyRemove() -> None:
|
||||
"""
|
||||
Raise an C{OSError} to emulate the branch of L{util._removeSafely}
|
||||
in which path removal fails.
|
||||
"""
|
||||
raise OSError("path removal failed")
|
||||
|
||||
def dummyMoveTo(destination: object, followLinks: bool = True) -> None:
|
||||
"""
|
||||
Raise an C{OSError} to emulate the branch of L{util._removeSafely}
|
||||
in which path movement fails.
|
||||
"""
|
||||
raise OSError("path movement failed")
|
||||
|
||||
# Patch stdout so we can check the print statements in _removeSafely
|
||||
out = StringIO()
|
||||
self.patch(sys, "stdout", out)
|
||||
|
||||
# Set up a trial directory with a _trial_marker
|
||||
directory = self.mktemp().encode("utf-8")
|
||||
os.mkdir(directory)
|
||||
dirPath = filepath.FilePath(directory)
|
||||
dirPath.child(b"_trial_marker").touch()
|
||||
|
||||
# Ensure that path.remove() and path.moveTo() both raise OSErrors
|
||||
dirPath.remove = dummyRemove # type: ignore[method-assign]
|
||||
dirPath.moveTo = dummyMoveTo # type: ignore[method-assign]
|
||||
|
||||
error = self.assertRaises(OSError, util._removeSafely, dirPath)
|
||||
self.assertEqual(str(error), "path movement failed")
|
||||
self.assertIn("could not remove FilePath", out.getvalue())
|
||||
|
||||
|
||||
class ExcInfoTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{excInfoOrFailureToExcInfo}.
|
||||
"""
|
||||
|
||||
def test_excInfo(self) -> None:
|
||||
"""
|
||||
L{excInfoOrFailureToExcInfo} returns exactly what it is passed, if it is
|
||||
passed a tuple like the one returned by L{sys.exc_info}.
|
||||
"""
|
||||
info = (ValueError, ValueError("foo"), None)
|
||||
self.assertTrue(info is excInfoOrFailureToExcInfo(info))
|
||||
|
||||
def test_failure(self) -> None:
|
||||
"""
|
||||
When called with a L{Failure} instance, L{excInfoOrFailureToExcInfo}
|
||||
returns a tuple like the one returned by L{sys.exc_info}, with the
|
||||
elements taken from the type, value, and traceback of the failure.
|
||||
"""
|
||||
try:
|
||||
1 / 0
|
||||
except BaseException:
|
||||
f = Failure()
|
||||
self.assertEqual((f.type, f.value, f.tb), excInfoOrFailureToExcInfo(f))
|
||||
|
||||
|
||||
class AcquireAttributeTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{acquireAttribute}.
|
||||
"""
|
||||
|
||||
def test_foundOnEarlierObject(self) -> None:
|
||||
"""
|
||||
The value returned by L{acquireAttribute} is the value of the requested
|
||||
attribute on the first object in the list passed in which has that
|
||||
attribute.
|
||||
"""
|
||||
self.value = value = object()
|
||||
self.assertTrue(value is acquireAttribute([self, object()], "value"))
|
||||
|
||||
def test_foundOnLaterObject(self) -> None:
|
||||
"""
|
||||
The same as L{test_foundOnEarlierObject}, but for the case where the 2nd
|
||||
element in the object list has the attribute and the first does not.
|
||||
"""
|
||||
self.value = value = object()
|
||||
self.assertTrue(value is acquireAttribute([object(), self], "value"))
|
||||
|
||||
def test_notFoundException(self) -> None:
|
||||
"""
|
||||
If none of the objects passed in the list to L{acquireAttribute} have
|
||||
the requested attribute, L{AttributeError} is raised.
|
||||
"""
|
||||
self.assertRaises(AttributeError, acquireAttribute, [object()], "foo")
|
||||
|
||||
def test_notFoundDefault(self) -> None:
|
||||
"""
|
||||
If none of the objects passed in the list to L{acquireAttribute} have
|
||||
the requested attribute and a default value is given, the default value
|
||||
is returned.
|
||||
"""
|
||||
default = object()
|
||||
self.assertTrue(default is acquireAttribute([object()], "foo", default))
|
||||
|
||||
|
||||
class ListToPhraseTests(SynchronousTestCase):
|
||||
"""
|
||||
Input is transformed into a string representation of the list,
|
||||
with each item separated by delimiter (defaulting to a comma) and the final
|
||||
two being separated by a final delimiter.
|
||||
"""
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
If things is empty, an empty string is returned.
|
||||
"""
|
||||
sample: list[None] = []
|
||||
expected = ""
|
||||
result = util._listToPhrase(sample, "and")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_oneWord(self) -> None:
|
||||
"""
|
||||
With a single item, the item is returned.
|
||||
"""
|
||||
sample = ["One"]
|
||||
expected = "One"
|
||||
result = util._listToPhrase(sample, "and")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_twoWords(self) -> None:
|
||||
"""
|
||||
Two words are separated by the final delimiter.
|
||||
"""
|
||||
sample = ["One", "Two"]
|
||||
expected = "One and Two"
|
||||
result = util._listToPhrase(sample, "and")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_threeWords(self) -> None:
|
||||
"""
|
||||
With more than two words, the first two are separated by the delimiter.
|
||||
"""
|
||||
sample = ["One", "Two", "Three"]
|
||||
expected = "One, Two, and Three"
|
||||
result = util._listToPhrase(sample, "and")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_fourWords(self) -> None:
|
||||
"""
|
||||
If a delimiter is specified, it is used instead of the default comma.
|
||||
"""
|
||||
sample = ["One", "Two", "Three", "Four"]
|
||||
expected = "One; Two; Three; or Four"
|
||||
result = util._listToPhrase(sample, "or", delimiter="; ")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_notString(self) -> None:
|
||||
"""
|
||||
If something in things is not a string, it is converted into one.
|
||||
"""
|
||||
sample = [1, 2, "three"]
|
||||
expected = "1, 2, and three"
|
||||
result = util._listToPhrase(sample, "and")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_stringTypeError(self) -> None:
|
||||
"""
|
||||
If things is a string, a TypeError is raised.
|
||||
"""
|
||||
sample = "One, two, three"
|
||||
error = self.assertRaises(TypeError, util._listToPhrase, sample, "and")
|
||||
self.assertEqual(str(error), "Things must be a list or a tuple")
|
||||
|
||||
def test_iteratorTypeError(self) -> None:
|
||||
"""
|
||||
If things is an iterator, a TypeError is raised.
|
||||
"""
|
||||
sample = iter([1, 2, 3])
|
||||
error = self.assertRaises(TypeError, util._listToPhrase, sample, "and")
|
||||
self.assertEqual(str(error), "Things must be a list or a tuple")
|
||||
|
||||
def test_generatorTypeError(self) -> None:
|
||||
"""
|
||||
If things is a generator, a TypeError is raised.
|
||||
"""
|
||||
|
||||
def sample() -> Generator[int, None, None]:
|
||||
yield from range(2)
|
||||
|
||||
error = self.assertRaises(TypeError, util._listToPhrase, sample, "and")
|
||||
self.assertEqual(str(error), "Things must be a list or a tuple")
|
||||
|
||||
|
||||
class OpenTestLogTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for C{openTestLog}.
|
||||
"""
|
||||
|
||||
def test_utf8(self) -> None:
|
||||
"""
|
||||
The log file is opened in text mode and uses UTF-8 for encoding.
|
||||
"""
|
||||
# Modern OSes are running default locale in UTF-8 and this is what is
|
||||
# used by Python at startup. For this test, we force an ASCII default
|
||||
# encoding so that we can see that UTF-8 is used even if it isn't the
|
||||
# platform default.
|
||||
currentLocale = locale.getlocale()
|
||||
self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale)
|
||||
locale.setlocale(locale.LC_ALL, ("C", "ascii"))
|
||||
|
||||
text = "Here comes the \N{SUN}"
|
||||
p = filepath.FilePath(self.mktemp())
|
||||
with openTestLog(p) as f:
|
||||
f.write(text)
|
||||
|
||||
with open(p.path, "rb") as f:
|
||||
written = f.read()
|
||||
|
||||
assert_that(text.encode("utf-8"), equal_to(written))
|
||||
|
||||
def test_append(self) -> None:
|
||||
"""
|
||||
The log file is opened in append mode so if runner configuration specifies
|
||||
an existing log file its contents are not wiped out.
|
||||
"""
|
||||
existingText = "Hello, world.\n "
|
||||
newText = "Goodbye, world.\n"
|
||||
expected = f"Hello, world.{os.linesep} Goodbye, world.{os.linesep}"
|
||||
p = filepath.FilePath(self.mktemp())
|
||||
with openTestLog(p) as f:
|
||||
f.write(existingText)
|
||||
with openTestLog(p) as f:
|
||||
f.write(newText)
|
||||
|
||||
assert_that(
|
||||
p.getContent().decode("utf-8"),
|
||||
equal_to(expected),
|
||||
)
|
||||
@@ -0,0 +1,539 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for Trial's interaction with the Python warning system.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from io import StringIO
|
||||
from typing import Mapping, Sequence, TypeVar
|
||||
from unittest import TestResult
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial._synctest import (
|
||||
_collectWarnings,
|
||||
_setWarningRegistryToNone,
|
||||
_Warning,
|
||||
)
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
|
||||
|
||||
class Mask:
|
||||
"""
|
||||
Hide a test case definition from trial's automatic discovery mechanism.
|
||||
"""
|
||||
|
||||
class MockTests(SynchronousTestCase):
|
||||
"""
|
||||
A test case which is used by L{FlushWarningsTests} to verify behavior
|
||||
which cannot be verified by code inside a single test method.
|
||||
"""
|
||||
|
||||
message = "some warning text"
|
||||
category: type[Warning] = UserWarning
|
||||
|
||||
def test_unflushed(self) -> None:
|
||||
"""
|
||||
Generate a warning and don't flush it.
|
||||
"""
|
||||
warnings.warn(self.message, self.category)
|
||||
|
||||
def test_flushed(self) -> None:
|
||||
"""
|
||||
Generate a warning and flush it.
|
||||
"""
|
||||
warnings.warn(self.message, self.category)
|
||||
self.assertEqual(len(self.flushWarnings()), 1)
|
||||
|
||||
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
|
||||
|
||||
class FlushWarningsTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for C{flushWarnings}, an API for examining the warnings
|
||||
emitted so far in a test.
|
||||
"""
|
||||
|
||||
def assertDictSubset(self, set: Mapping[_K, _V], subset: Mapping[_K, _V]) -> None:
|
||||
"""
|
||||
Assert that all the keys present in C{subset} are also present in
|
||||
C{set} and that the corresponding values are equal.
|
||||
"""
|
||||
for k, v in subset.items():
|
||||
self.assertEqual(set[k], v)
|
||||
|
||||
def assertDictSubsets(
|
||||
self, sets: Sequence[Mapping[_K, _V]], subsets: Sequence[Mapping[_K, _V]]
|
||||
) -> None:
|
||||
"""
|
||||
For each pair of corresponding elements in C{sets} and C{subsets},
|
||||
assert that the element from C{subsets} is a subset of the element from
|
||||
C{sets}.
|
||||
"""
|
||||
self.assertEqual(len(sets), len(subsets))
|
||||
for a, b in zip(sets, subsets):
|
||||
self.assertDictSubset(a, b)
|
||||
|
||||
def test_none(self) -> None:
|
||||
"""
|
||||
If no warnings are emitted by a test, C{flushWarnings} returns an empty
|
||||
list.
|
||||
"""
|
||||
self.assertEqual(self.flushWarnings(), [])
|
||||
|
||||
def test_several(self) -> None:
|
||||
"""
|
||||
If several warnings are emitted by a test, C{flushWarnings} returns a
|
||||
list containing all of them.
|
||||
"""
|
||||
firstMessage = "first warning message"
|
||||
firstCategory = UserWarning
|
||||
warnings.warn(message=firstMessage, category=firstCategory)
|
||||
|
||||
secondMessage = "second warning message"
|
||||
secondCategory = RuntimeWarning
|
||||
warnings.warn(message=secondMessage, category=secondCategory)
|
||||
|
||||
self.assertDictSubsets(
|
||||
self.flushWarnings(),
|
||||
[
|
||||
{"category": firstCategory, "message": firstMessage},
|
||||
{"category": secondCategory, "message": secondMessage},
|
||||
],
|
||||
)
|
||||
|
||||
def test_repeated(self) -> None:
|
||||
"""
|
||||
The same warning triggered twice from the same place is included twice
|
||||
in the list returned by C{flushWarnings}.
|
||||
"""
|
||||
message = "the message"
|
||||
category = RuntimeWarning
|
||||
for i in range(2):
|
||||
warnings.warn(message=message, category=category)
|
||||
|
||||
self.assertDictSubsets(
|
||||
self.flushWarnings(), [{"category": category, "message": message}] * 2
|
||||
)
|
||||
|
||||
def test_cleared(self) -> None:
|
||||
"""
|
||||
After a particular warning event has been returned by C{flushWarnings},
|
||||
it is not returned by subsequent calls.
|
||||
"""
|
||||
message = "the message"
|
||||
category = RuntimeWarning
|
||||
warnings.warn(message=message, category=category)
|
||||
self.assertDictSubsets(
|
||||
self.flushWarnings(), [{"category": category, "message": message}]
|
||||
)
|
||||
self.assertEqual(self.flushWarnings(), [])
|
||||
|
||||
def test_unflushed(self) -> None:
|
||||
"""
|
||||
Any warnings emitted by a test which are not flushed are emitted to the
|
||||
Python warning system.
|
||||
"""
|
||||
result = TestResult()
|
||||
case = Mask.MockTests("test_unflushed")
|
||||
case.run(result)
|
||||
warningsShown = self.flushWarnings([Mask.MockTests.test_unflushed])
|
||||
self.assertEqual(warningsShown[0]["message"], "some warning text")
|
||||
self.assertIdentical(warningsShown[0]["category"], UserWarning)
|
||||
|
||||
where = type(case).test_unflushed.__code__
|
||||
filename = where.co_filename
|
||||
# If someone edits MockTests.test_unflushed, the value added to
|
||||
# firstlineno might need to change.
|
||||
lineno = where.co_firstlineno + 4
|
||||
|
||||
self.assertEqual(warningsShown[0]["filename"], filename)
|
||||
self.assertEqual(warningsShown[0]["lineno"], lineno)
|
||||
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
|
||||
def test_flushed(self) -> None:
|
||||
"""
|
||||
Any warnings emitted by a test which are flushed are not emitted to the
|
||||
Python warning system.
|
||||
"""
|
||||
result = TestResult()
|
||||
case = Mask.MockTests("test_flushed")
|
||||
output = StringIO()
|
||||
monkey = self.patch(sys, "stdout", output)
|
||||
case.run(result)
|
||||
monkey.restore()
|
||||
self.assertEqual(output.getvalue(), "")
|
||||
|
||||
def test_warningsConfiguredAsErrors(self) -> None:
|
||||
"""
|
||||
If a warnings filter has been installed which turns warnings into
|
||||
exceptions, tests have an error added to the reporter for them for each
|
||||
unflushed warning.
|
||||
"""
|
||||
|
||||
class CustomWarning(Warning):
|
||||
pass
|
||||
|
||||
result = TestResult()
|
||||
case = Mask.MockTests("test_unflushed")
|
||||
case.category = CustomWarning
|
||||
|
||||
originalWarnings = warnings.filters[:]
|
||||
try:
|
||||
warnings.simplefilter("error")
|
||||
case.run(result)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertIdentical(result.errors[0][0], case)
|
||||
self.assertTrue(
|
||||
# Different python versions differ in whether they report the
|
||||
# fully qualified class name or just the class name.
|
||||
result.errors[0][1]
|
||||
.splitlines()[-1]
|
||||
.endswith("CustomWarning: some warning text")
|
||||
)
|
||||
finally:
|
||||
warnings.filters[:] = originalWarnings # type: ignore[index]
|
||||
|
||||
def test_flushedWarningsConfiguredAsErrors(self) -> None:
|
||||
"""
|
||||
If a warnings filter has been installed which turns warnings into
|
||||
exceptions, tests which emit those warnings but flush them do not have
|
||||
an error added to the reporter.
|
||||
"""
|
||||
|
||||
class CustomWarning(Warning):
|
||||
pass
|
||||
|
||||
result = TestResult()
|
||||
case = Mask.MockTests("test_flushed")
|
||||
case.category = CustomWarning
|
||||
|
||||
originalWarnings = warnings.filters[:]
|
||||
try:
|
||||
warnings.simplefilter("error")
|
||||
case.run(result)
|
||||
self.assertEqual(result.errors, [])
|
||||
finally:
|
||||
warnings.filters[:] = originalWarnings # type: ignore[index]
|
||||
|
||||
def test_multipleFlushes(self) -> None:
|
||||
"""
|
||||
Any warnings emitted after a call to C{flushWarnings} can be flushed by
|
||||
another call to C{flushWarnings}.
|
||||
"""
|
||||
warnings.warn("first message")
|
||||
self.assertEqual(len(self.flushWarnings()), 1)
|
||||
warnings.warn("second message")
|
||||
self.assertEqual(len(self.flushWarnings()), 1)
|
||||
|
||||
def test_filterOnOffendingFunction(self) -> None:
|
||||
"""
|
||||
The list returned by C{flushWarnings} includes only those
|
||||
warnings which refer to the source of the function passed as the value
|
||||
for C{offendingFunction}, if a value is passed for that parameter.
|
||||
"""
|
||||
firstMessage = "first warning text"
|
||||
firstCategory = UserWarning
|
||||
|
||||
def one() -> None:
|
||||
warnings.warn(firstMessage, firstCategory, stacklevel=1)
|
||||
|
||||
secondMessage = "some text"
|
||||
secondCategory = RuntimeWarning
|
||||
|
||||
def two() -> None:
|
||||
warnings.warn(secondMessage, secondCategory, stacklevel=1)
|
||||
|
||||
one()
|
||||
two()
|
||||
|
||||
self.assertDictSubsets(
|
||||
self.flushWarnings(offendingFunctions=[one]),
|
||||
[{"category": firstCategory, "message": firstMessage}],
|
||||
)
|
||||
self.assertDictSubsets(
|
||||
self.flushWarnings(offendingFunctions=[two]),
|
||||
[{"category": secondCategory, "message": secondMessage}],
|
||||
)
|
||||
|
||||
def test_functionBoundaries(self) -> None:
|
||||
"""
|
||||
Verify that warnings emitted at the very edges of a function are still
|
||||
determined to be emitted from that function.
|
||||
"""
|
||||
|
||||
def warner() -> None:
|
||||
warnings.warn("first line warning")
|
||||
warnings.warn("internal line warning")
|
||||
warnings.warn("last line warning")
|
||||
|
||||
warner()
|
||||
self.assertEqual(len(self.flushWarnings(offendingFunctions=[warner])), 3)
|
||||
|
||||
def test_invalidFilter(self) -> None:
|
||||
"""
|
||||
If an object which is neither a function nor a method is included in the
|
||||
C{offendingFunctions} list, C{flushWarnings} raises L{ValueError}. Such
|
||||
a call flushes no warnings.
|
||||
"""
|
||||
warnings.warn("oh no")
|
||||
self.assertRaises(ValueError, self.flushWarnings, [None])
|
||||
self.assertEqual(len(self.flushWarnings()), 1)
|
||||
|
||||
def test_missingSource(self) -> None:
|
||||
"""
|
||||
Warnings emitted by a function the source code of which is not
|
||||
available can still be flushed.
|
||||
"""
|
||||
package = FilePath(self.mktemp().encode("utf-8")).child(
|
||||
b"twisted_private_helper"
|
||||
)
|
||||
package.makedirs()
|
||||
package.child(b"__init__.py").setContent(b"")
|
||||
package.child(b"missingsourcefile.py").setContent(
|
||||
b"""
|
||||
import warnings
|
||||
def foo():
|
||||
warnings.warn("oh no")
|
||||
"""
|
||||
)
|
||||
pathEntry = package.parent().path.decode("utf-8")
|
||||
sys.path.insert(0, pathEntry)
|
||||
self.addCleanup(sys.path.remove, pathEntry)
|
||||
|
||||
# since import is a synthetic thing that we made up just for this test,
|
||||
# it's a local type ignore rather than being present in the mypy config
|
||||
# file like everything else
|
||||
from twisted_private_helper import ( # type: ignore[import-not-found]
|
||||
missingsourcefile,
|
||||
)
|
||||
|
||||
self.addCleanup(sys.modules.pop, "twisted_private_helper")
|
||||
self.addCleanup(sys.modules.pop, missingsourcefile.__name__)
|
||||
package.child(b"missingsourcefile.py").remove()
|
||||
|
||||
missingsourcefile.foo()
|
||||
self.assertEqual(len(self.flushWarnings([missingsourcefile.foo])), 1)
|
||||
|
||||
def test_renamedSource(self) -> None:
|
||||
"""
|
||||
Warnings emitted by a function defined in a file which has been renamed
|
||||
since it was initially compiled can still be flushed.
|
||||
|
||||
This is testing the code which specifically supports working around the
|
||||
unfortunate behavior of CPython to write a .py source file name into
|
||||
the .pyc files it generates and then trust that it is correct in
|
||||
various places. If source files are renamed, .pyc files may not be
|
||||
regenerated, but they will contain incorrect filenames.
|
||||
"""
|
||||
package = FilePath(self.mktemp().encode("utf-8")).child(
|
||||
b"twisted_private_helper"
|
||||
)
|
||||
package.makedirs()
|
||||
package.child(b"__init__.py").setContent(b"")
|
||||
package.child(b"module.py").setContent(
|
||||
b"""
|
||||
import warnings
|
||||
def foo():
|
||||
warnings.warn("oh no")
|
||||
"""
|
||||
)
|
||||
pathEntry = package.parent().path.decode("utf-8")
|
||||
sys.path.insert(0, pathEntry)
|
||||
self.addCleanup(sys.path.remove, pathEntry)
|
||||
|
||||
# Import it to cause pycs to be generated
|
||||
from twisted_private_helper import module
|
||||
|
||||
# Clean up the state resulting from that import; we're not going to use
|
||||
# this module, so it should go away.
|
||||
del sys.modules["twisted_private_helper"]
|
||||
del sys.modules[module.__name__]
|
||||
|
||||
# Some Python versions have extra state related to the just
|
||||
# imported/renamed package. Clean it up too. See also
|
||||
# http://bugs.python.org/issue15912
|
||||
try:
|
||||
from importlib import invalidate_caches
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
invalidate_caches()
|
||||
|
||||
# Rename the source directory
|
||||
package.moveTo(package.sibling(b"twisted_renamed_helper"))
|
||||
|
||||
# Import the newly renamed version
|
||||
from twisted_renamed_helper import module # type: ignore[import-not-found]
|
||||
|
||||
self.addCleanup(sys.modules.pop, "twisted_renamed_helper")
|
||||
self.addCleanup(sys.modules.pop, module.__name__)
|
||||
|
||||
# Generate the warning
|
||||
module.foo()
|
||||
|
||||
# Flush it
|
||||
self.assertEqual(len(self.flushWarnings([module.foo])), 1)
|
||||
|
||||
def test_offendingFunctions_deep_branch(self) -> None:
|
||||
"""
|
||||
In Python 3.6 the dis.findlinestarts documented behaviour
|
||||
was changed such that the reported lines might not be sorted ascending.
|
||||
In Python 3.10 PEP 626 introduced byte-code change such that the last
|
||||
line of a function wasn't always associated with the last byte-code.
|
||||
In the past flushWarning was not detecting that such a function was
|
||||
associated with any warnings.
|
||||
"""
|
||||
|
||||
def foo(a: int = 1, b: int = 1) -> None:
|
||||
if a:
|
||||
if b:
|
||||
warnings.warn("oh no")
|
||||
else:
|
||||
pass
|
||||
|
||||
# Generate the warning
|
||||
foo()
|
||||
|
||||
# Flush it
|
||||
self.assertEqual(len(self.flushWarnings([foo])), 1)
|
||||
|
||||
|
||||
class FakeWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class CollectWarningsTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{_collectWarnings}.
|
||||
"""
|
||||
|
||||
def test_callsObserver(self) -> None:
|
||||
"""
|
||||
L{_collectWarnings} calls the observer with each emitted warning.
|
||||
"""
|
||||
firstMessage = "dummy calls observer warning"
|
||||
secondMessage = firstMessage[::-1]
|
||||
thirdMessage = Warning(1, 2, 3)
|
||||
events: list[str | _Warning] = []
|
||||
|
||||
def f() -> None:
|
||||
events.append("call")
|
||||
warnings.warn(firstMessage)
|
||||
warnings.warn(secondMessage)
|
||||
warnings.warn(thirdMessage)
|
||||
events.append("returning")
|
||||
|
||||
_collectWarnings(events.append, f)
|
||||
|
||||
self.assertEqual(events[0], "call")
|
||||
assert isinstance(events[1], _Warning)
|
||||
self.assertEqual(events[1].message, firstMessage)
|
||||
assert isinstance(events[2], _Warning)
|
||||
self.assertEqual(events[2].message, secondMessage)
|
||||
assert isinstance(events[3], _Warning)
|
||||
self.assertEqual(events[3].message, str(thirdMessage))
|
||||
self.assertEqual(events[4], "returning")
|
||||
self.assertEqual(len(events), 5)
|
||||
|
||||
def test_suppresses(self) -> None:
|
||||
"""
|
||||
Any warnings emitted by a call to a function passed to
|
||||
L{_collectWarnings} are not actually emitted to the warning system.
|
||||
"""
|
||||
output = StringIO()
|
||||
self.patch(sys, "stdout", output)
|
||||
_collectWarnings(lambda x: None, warnings.warn, "text")
|
||||
self.assertEqual(output.getvalue(), "")
|
||||
|
||||
def test_callsFunction(self) -> None:
|
||||
"""
|
||||
L{_collectWarnings} returns the result of calling the callable passed to
|
||||
it with the parameters given.
|
||||
"""
|
||||
arguments = []
|
||||
value = object()
|
||||
|
||||
def f(*args: object, **kwargs: object) -> object:
|
||||
arguments.append((args, kwargs))
|
||||
return value
|
||||
|
||||
result = _collectWarnings(lambda x: None, f, 1, "a", b=2, c="d")
|
||||
self.assertEqual(arguments, [((1, "a"), {"b": 2, "c": "d"})])
|
||||
self.assertIdentical(result, value)
|
||||
|
||||
def test_duplicateWarningCollected(self) -> None:
|
||||
"""
|
||||
Subsequent emissions of a warning from a particular source site can be
|
||||
collected by L{_collectWarnings}. In particular, the per-module
|
||||
emitted-warning cache should be bypassed (I{__warningregistry__}).
|
||||
"""
|
||||
# Make sure the worst case is tested: if __warningregistry__ isn't in a
|
||||
# module's globals, then the warning system will add it and start using
|
||||
# it to avoid emitting duplicate warnings. Delete __warningregistry__
|
||||
# to ensure that even modules which are first imported as a test is
|
||||
# running still interact properly with the warning system.
|
||||
global __warningregistry__
|
||||
del __warningregistry__ # type: ignore[name-defined]
|
||||
|
||||
def f() -> None:
|
||||
warnings.warn("foo")
|
||||
|
||||
warnings.simplefilter("default")
|
||||
f()
|
||||
events: list[_Warning] = []
|
||||
_collectWarnings(events.append, f)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0].message, "foo")
|
||||
self.assertEqual(len(self.flushWarnings()), 1)
|
||||
|
||||
def test_immutableObject(self) -> None:
|
||||
"""
|
||||
L{_collectWarnings}'s behavior is not altered by the presence of an
|
||||
object which cannot have attributes set on it as a value in
|
||||
C{sys.modules}.
|
||||
"""
|
||||
key = object()
|
||||
sys.modules[key] = key # type: ignore[index, assignment]
|
||||
self.addCleanup(sys.modules.pop, key) # type: ignore[arg-type]
|
||||
self.test_duplicateWarningCollected()
|
||||
|
||||
def test_setWarningRegistryChangeWhileIterating(self) -> None:
|
||||
"""
|
||||
If the dictionary passed to L{_setWarningRegistryToNone} changes size
|
||||
partway through the process, C{_setWarningRegistryToNone} continues to
|
||||
set C{__warningregistry__} to L{None} on the rest of the values anyway.
|
||||
|
||||
|
||||
This might be caused by C{sys.modules} containing something that's not
|
||||
really a module and imports things on setattr. py.test does this, as
|
||||
does L{twisted.python.deprecate.deprecatedModuleAttribute}.
|
||||
"""
|
||||
d: dict[object, A | None] = {}
|
||||
|
||||
class A:
|
||||
def __init__(self, key: object) -> None:
|
||||
self.__dict__["_key"] = key
|
||||
|
||||
def __setattr__(self, value: object, item: object) -> None:
|
||||
d[self._key] = None # type: ignore[attr-defined]
|
||||
|
||||
key1 = object()
|
||||
key2 = object()
|
||||
d[key1] = A(key2)
|
||||
|
||||
key3 = object()
|
||||
key4 = object()
|
||||
d[key3] = A(key4)
|
||||
|
||||
_setWarningRegistryToNone(d)
|
||||
|
||||
# If both key2 and key4 were added, then both A instanced were
|
||||
# processed.
|
||||
self.assertEqual({key1, key2, key3, key4}, set(d.keys()))
|
||||
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
# Used in test_tests.UnhandledDeferredTests
|
||||
|
||||
|
||||
class TestBleeding(unittest.TestCase):
|
||||
"""This test creates an unhandled Deferred and leaves it in a cycle.
|
||||
|
||||
The Deferred is left in a cycle so that the garbage collector won't pick it
|
||||
up immediately. We were having some problems where unhandled Deferreds in
|
||||
one test were failing random other tests. (See #1507, #1213)
|
||||
"""
|
||||
|
||||
def test_unhandledDeferred(self):
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
f = defer.fail()
|
||||
# these two lines create the cycle. don't remove them
|
||||
l = [f]
|
||||
l.append(l)
|
||||
39
.venv/lib/python3.12/site-packages/twisted/trial/unittest.py
Normal file
39
.venv/lib/python3.12/site-packages/twisted/trial/unittest.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- test-case-name: twisted.trial.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Things likely to be used by writers of unit tests.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial._asyncrunner import TestDecorator, TestSuite, decorate
|
||||
from twisted.trial._asynctest import TestCase
|
||||
|
||||
# Define the public API from the two implementation modules
|
||||
from twisted.trial._synctest import (
|
||||
FailTest,
|
||||
PyUnitResultAdapter,
|
||||
SkipTest,
|
||||
SynchronousTestCase,
|
||||
Todo,
|
||||
makeTodo,
|
||||
)
|
||||
|
||||
# Further obscure the origins of these objects, to reduce surprise (and this is
|
||||
# what the values were before code got shuffled around between files, but was
|
||||
# otherwise unchanged).
|
||||
FailTest.__module__ = SkipTest.__module__ = __name__
|
||||
|
||||
__all__ = [
|
||||
"decorate",
|
||||
"FailTest",
|
||||
"makeTodo",
|
||||
"PyUnitResultAdapter",
|
||||
"SkipTest",
|
||||
"SynchronousTestCase",
|
||||
"TestCase",
|
||||
"TestDecorator",
|
||||
"TestSuite",
|
||||
"Todo",
|
||||
]
|
||||
407
.venv/lib/python3.12/site-packages/twisted/trial/util.py
Normal file
407
.venv/lib/python3.12/site-packages/twisted/trial/util.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# -*- test-case-name: twisted.trial.test.test_util -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
#
|
||||
|
||||
"""
|
||||
A collection of utility functions and classes, used internally by Trial.
|
||||
|
||||
This code is for Trial's internal use. Do NOT use this code if you are writing
|
||||
tests. It is subject to change at the Trial maintainer's whim. There is
|
||||
nothing here in this module for you to use unless you are maintaining Trial.
|
||||
|
||||
Any non-Trial Twisted code that uses this module will be shot.
|
||||
|
||||
Maintainer: Jonathan Lange
|
||||
|
||||
@var DEFAULT_TIMEOUT_DURATION: The default timeout which will be applied to
|
||||
asynchronous (ie, Deferred-returning) test methods, in seconds.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from random import randrange
|
||||
from typing import Any, Callable, TextIO, TypeVar
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from twisted.internet import interfaces, utils
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.lockfile import FilesystemLock
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_TIMEOUT_DURATION",
|
||||
"excInfoOrFailureToExcInfo",
|
||||
"suppress",
|
||||
"acquireAttribute",
|
||||
]
|
||||
|
||||
DEFAULT_TIMEOUT = object()
|
||||
DEFAULT_TIMEOUT_DURATION = 120.0
|
||||
|
||||
|
||||
class DirtyReactorAggregateError(Exception):
|
||||
"""
|
||||
Passed to L{twisted.trial.itrial.IReporter.addError} when the reactor is
|
||||
left in an unclean state after a test.
|
||||
|
||||
@ivar delayedCalls: The L{DelayedCall<twisted.internet.base.DelayedCall>}
|
||||
objects which weren't cleaned up.
|
||||
@ivar selectables: The selectables which weren't cleaned up.
|
||||
"""
|
||||
|
||||
def __init__(self, delayedCalls, selectables=None):
|
||||
self.delayedCalls = delayedCalls
|
||||
self.selectables = selectables
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Return a multi-line message describing all of the unclean state.
|
||||
"""
|
||||
msg = "Reactor was unclean."
|
||||
if self.delayedCalls:
|
||||
msg += (
|
||||
"\nDelayedCalls: (set "
|
||||
"twisted.internet.base.DelayedCall.debug = True to "
|
||||
"debug)\n"
|
||||
)
|
||||
msg += "\n".join(map(str, self.delayedCalls))
|
||||
if self.selectables:
|
||||
msg += "\nSelectables:\n"
|
||||
msg += "\n".join(map(str, self.selectables))
|
||||
return msg
|
||||
|
||||
|
||||
class _Janitor:
|
||||
"""
|
||||
The guy that cleans up after you.
|
||||
|
||||
@ivar test: The L{TestCase} to report errors about.
|
||||
@ivar result: The L{IReporter} to report errors to.
|
||||
@ivar reactor: The reactor to use. If None, the global reactor
|
||||
will be used.
|
||||
"""
|
||||
|
||||
def __init__(self, test, result, reactor=None):
|
||||
"""
|
||||
@param test: See L{_Janitor.test}.
|
||||
@param result: See L{_Janitor.result}.
|
||||
@param reactor: See L{_Janitor.reactor}.
|
||||
"""
|
||||
self.test = test
|
||||
self.result = result
|
||||
self.reactor = reactor
|
||||
|
||||
def postCaseCleanup(self):
|
||||
"""
|
||||
Called by L{unittest.TestCase} after a test to catch any logged errors
|
||||
or pending L{DelayedCall<twisted.internet.base.DelayedCall>}s.
|
||||
"""
|
||||
calls = self._cleanPending()
|
||||
if calls:
|
||||
aggregate = DirtyReactorAggregateError(calls)
|
||||
self.result.addError(self.test, Failure(aggregate))
|
||||
return False
|
||||
return True
|
||||
|
||||
def postClassCleanup(self):
|
||||
"""
|
||||
Called by L{unittest.TestCase} after the last test in a C{TestCase}
|
||||
subclass. Ensures the reactor is clean by murdering the threadpool,
|
||||
catching any pending
|
||||
L{DelayedCall<twisted.internet.base.DelayedCall>}s, open sockets etc.
|
||||
"""
|
||||
selectables = self._cleanReactor()
|
||||
calls = self._cleanPending()
|
||||
if selectables or calls:
|
||||
aggregate = DirtyReactorAggregateError(calls, selectables)
|
||||
self.result.addError(self.test, Failure(aggregate))
|
||||
self._cleanThreads()
|
||||
|
||||
def _getReactor(self):
|
||||
"""
|
||||
Get either the passed-in reactor or the global reactor.
|
||||
"""
|
||||
if self.reactor is not None:
|
||||
reactor = self.reactor
|
||||
else:
|
||||
from twisted.internet import reactor
|
||||
return reactor
|
||||
|
||||
def _cleanPending(self):
|
||||
"""
|
||||
Cancel all pending calls and return their string representations.
|
||||
"""
|
||||
reactor = self._getReactor()
|
||||
|
||||
# flush short-range timers
|
||||
reactor.iterate(0)
|
||||
reactor.iterate(0)
|
||||
|
||||
delayedCallStrings = []
|
||||
for p in reactor.getDelayedCalls():
|
||||
if p.active():
|
||||
delayedString = str(p)
|
||||
p.cancel()
|
||||
else:
|
||||
print("WEIRDNESS! pending timed call not active!")
|
||||
delayedCallStrings.append(delayedString)
|
||||
return delayedCallStrings
|
||||
|
||||
_cleanPending = utils.suppressWarnings(
|
||||
_cleanPending,
|
||||
(
|
||||
("ignore",),
|
||||
{
|
||||
"category": DeprecationWarning,
|
||||
"message": r"reactor\.iterate cannot be used.*",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def _cleanThreads(self):
|
||||
reactor = self._getReactor()
|
||||
if interfaces.IReactorThreads.providedBy(reactor):
|
||||
if reactor.threadpool is not None:
|
||||
# Stop the threadpool now so that a new one is created.
|
||||
# This improves test isolation somewhat (although this is a
|
||||
# post class cleanup hook, so it's only isolating classes
|
||||
# from each other, not methods from each other).
|
||||
reactor._stopThreadPool()
|
||||
|
||||
def _cleanReactor(self):
|
||||
"""
|
||||
Remove all selectables from the reactor, kill any of them that were
|
||||
processes, and return their string representation.
|
||||
"""
|
||||
reactor = self._getReactor()
|
||||
selectableStrings = []
|
||||
for sel in reactor.removeAll():
|
||||
if interfaces.IProcessTransport.providedBy(sel):
|
||||
sel.signalProcess("KILL")
|
||||
selectableStrings.append(repr(sel))
|
||||
return selectableStrings
|
||||
|
||||
|
||||
_DEFAULT = object()
|
||||
|
||||
|
||||
def acquireAttribute(objects, attr, default=_DEFAULT):
|
||||
"""
|
||||
Go through the list 'objects' sequentially until we find one which has
|
||||
attribute 'attr', then return the value of that attribute. If not found,
|
||||
return 'default' if set, otherwise, raise AttributeError.
|
||||
"""
|
||||
for obj in objects:
|
||||
if hasattr(obj, attr):
|
||||
return getattr(obj, attr)
|
||||
if default is not _DEFAULT:
|
||||
return default
|
||||
raise AttributeError(f"attribute {attr!r} not found in {objects!r}")
|
||||
|
||||
|
||||
def excInfoOrFailureToExcInfo(err):
|
||||
"""
|
||||
Coerce a Failure to an _exc_info, if err is a Failure.
|
||||
|
||||
@param err: Either a tuple such as returned by L{sys.exc_info} or a
|
||||
L{Failure} object.
|
||||
@return: A tuple like the one returned by L{sys.exc_info}. e.g.
|
||||
C{exception_type, exception_object, traceback_object}.
|
||||
"""
|
||||
if isinstance(err, Failure):
|
||||
# Unwrap the Failure into an exc_info tuple.
|
||||
err = (err.type, err.value, err.getTracebackObject())
|
||||
return err
|
||||
|
||||
|
||||
def suppress(action="ignore", **kwarg):
|
||||
"""
|
||||
Sets up the .suppress tuple properly, pass options to this method as you
|
||||
would the stdlib warnings.filterwarnings()
|
||||
|
||||
So, to use this with a .suppress magic attribute you would do the
|
||||
following:
|
||||
|
||||
>>> from twisted.trial import unittest, util
|
||||
>>> import warnings
|
||||
>>>
|
||||
>>> class TestFoo(unittest.TestCase):
|
||||
... def testFooBar(self):
|
||||
... warnings.warn("i am deprecated", DeprecationWarning)
|
||||
... testFooBar.suppress = [util.suppress(message='i am deprecated')]
|
||||
...
|
||||
>>>
|
||||
|
||||
Note that as with the todo and timeout attributes: the module level
|
||||
attribute acts as a default for the class attribute which acts as a default
|
||||
for the method attribute. The suppress attribute can be overridden at any
|
||||
level by specifying C{.suppress = []}
|
||||
"""
|
||||
return ((action,), kwarg)
|
||||
|
||||
|
||||
# This should be deleted, and replaced with twisted.application's code; see
|
||||
# https://github.com/twisted/twisted/issues/6016:
|
||||
_P = ParamSpec("_P")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def profiled(f: Callable[_P, _T], outputFile: str) -> Callable[_P, _T]:
|
||||
def _(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
||||
import profile
|
||||
|
||||
prof = profile.Profile()
|
||||
try:
|
||||
result = prof.runcall(f, *args, **kwargs)
|
||||
prof.dump_stats(outputFile)
|
||||
except SystemExit:
|
||||
pass
|
||||
prof.print_stats()
|
||||
return result
|
||||
|
||||
return _
|
||||
|
||||
|
||||
class _NoTrialMarker(Exception):
|
||||
"""
|
||||
No trial marker file could be found.
|
||||
|
||||
Raised when trial attempts to remove a trial temporary working directory
|
||||
that does not contain a marker file.
|
||||
"""
|
||||
|
||||
|
||||
def _removeSafely(path):
|
||||
"""
|
||||
Safely remove a path, recursively.
|
||||
|
||||
If C{path} does not contain a node named C{_trial_marker}, a
|
||||
L{_NoTrialMarker} exception is raised and the path is not removed.
|
||||
"""
|
||||
if not path.child(b"_trial_marker").exists():
|
||||
raise _NoTrialMarker(
|
||||
f"{path!r} is not a trial temporary path, refusing to remove it"
|
||||
)
|
||||
try:
|
||||
path.remove()
|
||||
except OSError as e:
|
||||
print(
|
||||
"could not remove %r, caught OSError [Errno %s]: %s"
|
||||
% (path, e.errno, e.strerror)
|
||||
)
|
||||
try:
|
||||
newPath = FilePath(
|
||||
b"_trial_temp_old" + str(randrange(10000000)).encode("utf-8")
|
||||
)
|
||||
path.moveTo(newPath)
|
||||
except OSError as e:
|
||||
print(
|
||||
"could not rename path, caught OSError [Errno %s]: %s"
|
||||
% (e.errno, e.strerror)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class _WorkingDirectoryBusy(Exception):
|
||||
"""
|
||||
A working directory was specified to the runner, but another test run is
|
||||
currently using that directory.
|
||||
"""
|
||||
|
||||
|
||||
def _unusedTestDirectory(base):
|
||||
"""
|
||||
Find an unused directory named similarly to C{base}.
|
||||
|
||||
Once a directory is found, it will be locked and a marker dropped into it
|
||||
to identify it as a trial temporary directory.
|
||||
|
||||
@param base: A template path for the discovery process. If this path
|
||||
exactly cannot be used, a path which varies only in a suffix of the
|
||||
basename will be used instead.
|
||||
@type base: L{FilePath}
|
||||
|
||||
@return: A two-tuple. The first element is a L{FilePath} representing the
|
||||
directory which was found and created. The second element is a locked
|
||||
L{FilesystemLock<twisted.python.lockfile.FilesystemLock>}. Another
|
||||
call to C{_unusedTestDirectory} will not be able to reused the
|
||||
same name until the lock is released, either explicitly or by this
|
||||
process exiting.
|
||||
"""
|
||||
counter = 0
|
||||
while True:
|
||||
if counter:
|
||||
testdir = base.sibling("%s-%d" % (base.basename(), counter))
|
||||
else:
|
||||
testdir = base
|
||||
|
||||
testdir.parent().makedirs(ignoreExistingDirectory=True)
|
||||
testDirLock = FilesystemLock(testdir.path + ".lock")
|
||||
if testDirLock.lock():
|
||||
# It is not in use
|
||||
if testdir.exists():
|
||||
# It exists though - delete it
|
||||
_removeSafely(testdir)
|
||||
|
||||
# Create it anew and mark it as ours so the next _removeSafely on
|
||||
# it succeeds.
|
||||
testdir.makedirs()
|
||||
testdir.child(b"_trial_marker").setContent(b"")
|
||||
return testdir, testDirLock
|
||||
else:
|
||||
# It is in use
|
||||
if base.basename() == "_trial_temp":
|
||||
counter += 1
|
||||
else:
|
||||
raise _WorkingDirectoryBusy()
|
||||
|
||||
|
||||
def _listToPhrase(things, finalDelimiter, delimiter=", "):
|
||||
"""
|
||||
Produce a string containing each thing in C{things},
|
||||
separated by a C{delimiter}, with the last couple being separated
|
||||
by C{finalDelimiter}
|
||||
|
||||
@param things: The elements of the resulting phrase
|
||||
@type things: L{list} or L{tuple}
|
||||
|
||||
@param finalDelimiter: What to put between the last two things
|
||||
(typically 'and' or 'or')
|
||||
@type finalDelimiter: L{str}
|
||||
|
||||
@param delimiter: The separator to use between each thing,
|
||||
not including the last two. Should typically include a trailing space.
|
||||
@type delimiter: L{str}
|
||||
|
||||
@return: The resulting phrase
|
||||
@rtype: L{str}
|
||||
"""
|
||||
if not isinstance(things, (list, tuple)):
|
||||
raise TypeError("Things must be a list or a tuple")
|
||||
if not things:
|
||||
return ""
|
||||
if len(things) == 1:
|
||||
return str(things[0])
|
||||
if len(things) == 2:
|
||||
return f"{str(things[0])} {finalDelimiter} {str(things[1])}"
|
||||
else:
|
||||
strThings = []
|
||||
for thing in things:
|
||||
strThings.append(str(thing))
|
||||
return "{}{}{} {}".format(
|
||||
delimiter.join(strThings[:-1]),
|
||||
delimiter,
|
||||
finalDelimiter,
|
||||
strThings[-1],
|
||||
)
|
||||
|
||||
|
||||
def openTestLog(path: FilePath[Any]) -> TextIO:
|
||||
"""
|
||||
Open the given path such that test log messages can be written to it.
|
||||
"""
|
||||
path.parent().makedirs(ignoreExistingDirectory=True)
|
||||
# Always use UTF-8 because, considering all platforms, the system default
|
||||
# encoding can not reliably encode all code points.
|
||||
return open(path.path, "a", encoding="utf-8", errors="strict")
|
||||
Reference in New Issue
Block a user