okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View 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}.
"""

View File

@@ -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())

View 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)

View 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()

View File

@@ -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

View File

@@ -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()]

View File

@@ -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***REMOVED***iron.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)

View File

@@ -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

View File

@@ -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())]

View File

@@ -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")

View 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

View File

@@ -0,0 +1,6 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Distributed trial test runner tests.
"""

View File

@@ -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]
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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)))

View File

@@ -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))

View File

@@ -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)
)

View File

@@ -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)

View 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)

View File

@@ -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())]

View File

@@ -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
"""

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View 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.
"""

View File

@@ -0,0 +1 @@
!.gitignore

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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()

View 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

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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]
)

View File

@@ -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)),
),
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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_[AWS-SECRET-REMOVED]ter(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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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_[AWS-SECRET-REMOVED]n(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")

View File

@@ -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"
)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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),
)

View File

@@ -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()))

View File

@@ -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)

View 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",
]

View 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")