Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
|
|
@ -0,0 +1,47 @@
|
|||
# -*- test-case-name: twisted.trial._dist.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This package implements the distributed Trial test runner:
|
||||
|
||||
- The L{twisted.trial._dist.disttrial} module implements a test runner which
|
||||
runs in a manager process and can launch additional worker processes in
|
||||
which to run tests and gather up results from all of them.
|
||||
|
||||
- The L{twisted.trial._dist.options} module defines command line options used
|
||||
to configure the distributed test runner.
|
||||
|
||||
- The L{twisted.trial._dist.managercommands} module defines AMP commands
|
||||
which are sent from worker processes back to the manager process to report
|
||||
the results of tests.
|
||||
|
||||
- The L{twisted.trial._dist.workercommands} module defines AMP commands which
|
||||
are sent from the manager process to the worker processes to control the
|
||||
execution of tests there.
|
||||
|
||||
- The L{twisted.trial._dist.distreporter} module defines a proxy for
|
||||
L{twisted.trial.itrial.IReporter} which enforces the typical requirement
|
||||
that results be passed to a reporter for only one test at a time, allowing
|
||||
any reporter to be used with despite disttrial's simultaneously running
|
||||
tests.
|
||||
|
||||
- The L{twisted.trial._dist.workerreporter} module implements a
|
||||
L{twisted.trial.itrial.IReporter} which is used by worker processes and
|
||||
reports results back to the manager process using AMP commands.
|
||||
|
||||
- The L{twisted.trial._dist.workertrial} module is a runnable script which is
|
||||
the main point for worker processes.
|
||||
|
||||
- The L{twisted.trial._dist.worker} process defines the manager's AMP
|
||||
protocol for accepting results from worker processes and a process protocol
|
||||
for use running workers as local child processes (as opposed to
|
||||
distributing them to another host).
|
||||
|
||||
@since: 12.3
|
||||
"""
|
||||
|
||||
# File descriptors numbers used to set up pipes with the worker.
|
||||
_WORKER_AMP_STDIN = 3
|
||||
|
||||
_WORKER_AMP_STDOUT = 4
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# -*- 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 zope.interface import implementer
|
||||
from twisted.trial.itrial import IReporter
|
||||
from twisted.python.components import proxyForInterface
|
||||
|
||||
|
||||
|
||||
@implementer(IReporter)
|
||||
class DistReporter(proxyForInterface(IReporter)):
|
||||
"""
|
||||
See module docstring.
|
||||
"""
|
||||
|
||||
def __init__(self, original):
|
||||
super(DistReporter, self).__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, fail):
|
||||
"""
|
||||
Queue adding a failure.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addFailure,
|
||||
test, fail))
|
||||
|
||||
|
||||
def addError(self, test, error):
|
||||
"""
|
||||
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, error, todo=None):
|
||||
"""
|
||||
Queue adding an unexpected failure.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addExpectedFailure,
|
||||
test, error, todo))
|
||||
|
||||
|
||||
def addSuccess(self, test):
|
||||
"""
|
||||
Queue adding a success.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.addSuccess, test))
|
||||
|
||||
|
||||
def stopTest(self, test):
|
||||
"""
|
||||
Queue stopping the test, then unroll the queue.
|
||||
"""
|
||||
self.running[test.id()].append((self.original.stopTest, test))
|
||||
for step in self.running[test.id()]:
|
||||
step[0](*step[1:])
|
||||
del self.running[test.id()]
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
# -*- 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 twisted.python.filepath import FilePath
|
||||
from twisted.python.modules import theSystemPath
|
||||
from twisted.internet.defer import DeferredList
|
||||
from twisted.internet.task import cooperate
|
||||
|
||||
from twisted.trial.util import _unusedTestDirectory
|
||||
from twisted.trial._asyncrunner import _iterateTests
|
||||
from twisted.trial._dist.worker import LocalWorker, LocalWorkerAMP
|
||||
from twisted.trial._dist.distreporter import DistReporter
|
||||
from twisted.trial.reporter import UncleanWarningsReporterWrapper
|
||||
from twisted.trial._dist import _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT
|
||||
|
||||
|
||||
|
||||
class DistTrialRunner(object):
|
||||
"""
|
||||
A specialized runner for distributed trial. The runner launches a number of
|
||||
local worker processes which will run tests.
|
||||
|
||||
@ivar _workerNumber: the number of workers to be spawned.
|
||||
@type _workerNumber: C{int}
|
||||
|
||||
@ivar _stream: stream which the reporter will use.
|
||||
|
||||
@ivar _reporterFactory: the reporter class to be used.
|
||||
"""
|
||||
_distReporterFactory = DistReporter
|
||||
|
||||
def _makeResult(self):
|
||||
"""
|
||||
Make reporter factory, and wrap it with a L{DistReporter}.
|
||||
"""
|
||||
reporter = self._reporterFactory(self._stream, self._tbformat,
|
||||
realtime=self._rterrors)
|
||||
if self._uncleanWarnings:
|
||||
reporter = UncleanWarningsReporterWrapper(reporter)
|
||||
return self._distReporterFactory(reporter)
|
||||
|
||||
|
||||
def __init__(self, reporterFactory, workerNumber, workerArguments,
|
||||
stream=None,
|
||||
tracebackFormat='default',
|
||||
realTimeErrors=False,
|
||||
uncleanWarnings=False,
|
||||
logfile='test.log',
|
||||
workingDirectory='_trial_temp'):
|
||||
self._workerNumber = workerNumber
|
||||
self._workerArguments = workerArguments
|
||||
self._reporterFactory = reporterFactory
|
||||
if stream is None:
|
||||
stream = sys.stdout
|
||||
self._stream = stream
|
||||
self._tbformat = tracebackFormat
|
||||
self._rterrors = realTimeErrors
|
||||
self._uncleanWarnings = uncleanWarnings
|
||||
self._result = None
|
||||
self._workingDirectory = workingDirectory
|
||||
self._logFile = logfile
|
||||
self._logFileObserver = None
|
||||
self._logFileObject = None
|
||||
self._logWarnings = False
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def createLocalWorkers(self, protocols, workingDirectory):
|
||||
"""
|
||||
Create local worker protocol instances and return them.
|
||||
|
||||
@param protocols: An iterable of L{LocalWorkerAMP} instances.
|
||||
|
||||
@param workingDirectory: The base path in which we should run the
|
||||
workers.
|
||||
@type workingDirectory: C{str}
|
||||
|
||||
@return: A list of C{quantity} C{LocalWorker} instances.
|
||||
"""
|
||||
return [LocalWorker(protocol,
|
||||
os.path.join(workingDirectory, str(x)),
|
||||
self._logFile)
|
||||
for x, protocol in enumerate(protocols)]
|
||||
|
||||
|
||||
def launchWorkerProcesses(self, spawner, protocols, arguments):
|
||||
"""
|
||||
Spawn processes from a list of process protocols.
|
||||
|
||||
@param spawner: A C{IReactorProcess.spawnProcess} implementation.
|
||||
|
||||
@param protocols: An iterable of C{ProcessProtocol} instances.
|
||||
|
||||
@param arguments: Extra arguments passed to the processes.
|
||||
"""
|
||||
workertrialPath = theSystemPath[
|
||||
'twisted.trial._dist.workertrial'].filePath.path
|
||||
childFDs = {0: 'w', 1: 'r', 2: 'r', _WORKER_AMP_STDIN: 'w',
|
||||
_WORKER_AMP_STDOUT: 'r'}
|
||||
environ = os.environ.copy()
|
||||
# Add an environment variable containing the raw sys.path, to be used by
|
||||
# subprocesses to make sure it's identical to the parent. See
|
||||
# workertrial._setupPath.
|
||||
environ['TRIAL_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)
|
||||
|
||||
|
||||
def _driveWorker(self, worker, result, testCases, cooperate):
|
||||
"""
|
||||
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.
|
||||
|
||||
@param cooperate: The cooperate function to use, to be customized in
|
||||
tests.
|
||||
@type cooperate: C{function}
|
||||
|
||||
@return: A C{Deferred} firing when all the tests are finished.
|
||||
"""
|
||||
|
||||
def resultErrback(error, case):
|
||||
result.original.addFailure(case, error)
|
||||
return error
|
||||
|
||||
def task(case):
|
||||
d = worker.run(case, result)
|
||||
d.addErrback(resultErrback, case)
|
||||
return d
|
||||
|
||||
return cooperate(task(case) for case in testCases).whenDone()
|
||||
|
||||
|
||||
def run(self, suite, reactor=None, cooperate=cooperate,
|
||||
untilFailure=False):
|
||||
"""
|
||||
Spawn local worker processes and load tests. After that, run them.
|
||||
|
||||
@param suite: A tests suite to be run.
|
||||
|
||||
@param reactor: The reactor to use, to be customized in tests.
|
||||
@type reactor: A provider of
|
||||
L{twisted.internet.interfaces.IReactorProcess}
|
||||
|
||||
@param cooperate: The cooperate function to use, to be customized in
|
||||
tests.
|
||||
@type cooperate: C{function}
|
||||
|
||||
@param untilFailure: If C{True}, continue to run the tests until they
|
||||
fail.
|
||||
@type untilFailure: C{bool}.
|
||||
|
||||
@return: The test result.
|
||||
@rtype: L{DistReporter}
|
||||
"""
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
result = self._makeResult()
|
||||
count = suite.countTestCases()
|
||||
self._stream.write("Running %d tests.\n" % (count,))
|
||||
|
||||
if not count:
|
||||
# Take a shortcut if there is no test
|
||||
suite.run(result.original)
|
||||
self.writeResults(result)
|
||||
return result
|
||||
|
||||
testDir, testDirLock = _unusedTestDirectory(
|
||||
FilePath(self._workingDirectory))
|
||||
workerNumber = min(count, self._workerNumber)
|
||||
ampWorkers = [LocalWorkerAMP() for x in range(workerNumber)]
|
||||
workers = self.createLocalWorkers(ampWorkers, testDir.path)
|
||||
processEndDeferreds = [worker.endDeferred for worker in workers]
|
||||
self.launchWorkerProcesses(reactor.spawnProcess, workers,
|
||||
self._workerArguments)
|
||||
|
||||
def runTests():
|
||||
testCases = iter(list(_iterateTests(suite)))
|
||||
|
||||
workerDeferreds = []
|
||||
for worker in ampWorkers:
|
||||
workerDeferreds.append(
|
||||
self._driveWorker(worker, result, testCases,
|
||||
cooperate=cooperate))
|
||||
return DeferredList(workerDeferreds, consumeErrors=True,
|
||||
fireOnOneErrback=True)
|
||||
|
||||
stopping = []
|
||||
|
||||
def nextRun(ign):
|
||||
self.writeResults(result)
|
||||
if not untilFailure:
|
||||
return
|
||||
if not result.wasSuccessful():
|
||||
return
|
||||
d = runTests()
|
||||
return d.addCallback(nextRun)
|
||||
|
||||
def stop(ign):
|
||||
testDirLock.unlock()
|
||||
if not stopping:
|
||||
stopping.append(None)
|
||||
reactor.stop()
|
||||
|
||||
def beforeShutDown():
|
||||
if not stopping:
|
||||
stopping.append(None)
|
||||
d = DeferredList(processEndDeferreds, consumeErrors=True)
|
||||
return d.addCallback(continueShutdown)
|
||||
|
||||
def continueShutdown(ign):
|
||||
self.writeResults(result)
|
||||
return ign
|
||||
|
||||
d = runTests()
|
||||
d.addCallback(nextRun)
|
||||
d.addBoth(stop)
|
||||
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', beforeShutDown)
|
||||
reactor.run()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def runUntilFailure(self, suite):
|
||||
"""
|
||||
Run the tests with local worker processes until they fail.
|
||||
|
||||
@param suite: A tests suite to be run.
|
||||
"""
|
||||
return self.run(suite, untilFailure=True)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# 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 Command, String, Boolean, ListOf, Unicode
|
||||
from twisted.python.compat import _PY3
|
||||
|
||||
NativeString = Unicode if _PY3 else String
|
||||
|
||||
|
||||
|
||||
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'error', NativeString()),
|
||||
(b'errorClass', NativeString()),
|
||||
(b'frames', ListOf(NativeString()))]
|
||||
response = [(b'success', Boolean())]
|
||||
|
||||
|
||||
|
||||
class AddFailure(Command):
|
||||
"""
|
||||
Add a failure.
|
||||
"""
|
||||
arguments = [(b'testName', NativeString()),
|
||||
(b'fail', NativeString()),
|
||||
(b'failClass', NativeString()),
|
||||
(b'frames', ListOf(NativeString()))]
|
||||
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'error', NativeString()),
|
||||
(b'todo', NativeString())]
|
||||
response = [(b'success', Boolean())]
|
||||
|
||||
|
||||
|
||||
class AddUnexpectedSuccess(Command):
|
||||
"""
|
||||
Add an unexpected success.
|
||||
"""
|
||||
arguments = [(b'testName', NativeString()),
|
||||
(b'todo', NativeString())]
|
||||
response = [(b'success', Boolean())]
|
||||
|
||||
|
||||
|
||||
class TestWrite(Command):
|
||||
"""
|
||||
Write test log.
|
||||
"""
|
||||
arguments = [(b'out', NativeString())]
|
||||
response = [(b'success', Boolean())]
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# -*- 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.python.filepath import FilePath
|
||||
from twisted.python.usage import Options
|
||||
from twisted.scripts.trial import _BasicOptions
|
||||
from twisted.application.app import ReactorSelectionMixin
|
||||
|
||||
|
||||
|
||||
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')
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Distributed trial test runner tests.
|
||||
"""
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.distreporter}.
|
||||
"""
|
||||
|
||||
from twisted.python.compat import NativeStringIO as StringIO
|
||||
from twisted.trial._dist.distreporter import DistReporter
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.trial.reporter import TreeReporter
|
||||
|
||||
|
||||
|
||||
class DistReporterTests(TestCase):
|
||||
"""
|
||||
Tests for L{DistReporter}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.stream = StringIO()
|
||||
self.distReporter = DistReporter(TreeReporter(self.stream))
|
||||
self.test = TestCase()
|
||||
|
||||
|
||||
def test_startSuccessStop(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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, "error")
|
||||
self.assertEqual(self.stream.getvalue(), "")
|
||||
self.distReporter.stopTest(self.test)
|
||||
self.assertNotEqual(self.stream.getvalue(), "")
|
||||
|
||||
|
||||
def test_forwardedMethods(self):
|
||||
"""
|
||||
Calling methods of L{DistReporter} add calls to the running queue of
|
||||
the test.
|
||||
"""
|
||||
self.distReporter.startTest(self.test)
|
||||
self.distReporter.addFailure(self.test, "foo")
|
||||
self.distReporter.addError(self.test, "bar")
|
||||
self.distReporter.addSkip(self.test, "egg")
|
||||
self.distReporter.addUnexpectedSuccess(self.test, "spam")
|
||||
self.distReporter.addExpectedFailure(self.test, "err", "foo")
|
||||
self.assertEqual(len(self.distReporter.running[self.test.id()]), 6)
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.disttrial}.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from twisted.internet.protocol import Protocol, ProcessProtocol
|
||||
from twisted.internet.defer import fail, gatherResults, maybeDeferred, succeed
|
||||
from twisted.internet.task import Cooperator, deferLater
|
||||
from twisted.internet.main import CONNECTION_DONE
|
||||
from twisted.internet import reactor, interfaces, error
|
||||
from twisted.python.compat import NativeStringIO as StringIO
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.lockfile import FilesystemLock
|
||||
|
||||
from twisted.test.test_cooperator import FakeScheduler
|
||||
from twisted.test.proto_helpers import MemoryReactorClock
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase, TestCase
|
||||
from twisted.trial.reporter import Reporter, TreeReporter
|
||||
from twisted.trial.reporter import UncleanWarningsReporterWrapper
|
||||
from twisted.trial.runner import TrialSuite, ErrorHolder
|
||||
|
||||
from twisted.trial._dist.disttrial import DistTrialRunner
|
||||
from twisted.trial._dist.distreporter import DistReporter
|
||||
from twisted.trial._dist.worker import LocalWorker
|
||||
|
||||
from zope.interface import implementer, verify
|
||||
|
||||
|
||||
|
||||
class FakeTransport(object):
|
||||
"""
|
||||
A simple fake process transport.
|
||||
"""
|
||||
|
||||
def writeToChild(self, fd, data):
|
||||
"""
|
||||
Ignore write calls.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@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, worker, *args, **kwargs):
|
||||
"""
|
||||
See L{IReactorProcess.spawnProcess}.
|
||||
|
||||
@param worker: See L{IReactorProcess.spawnProcess}.
|
||||
@param args: See L{IReactorProcess.spawnProcess}.
|
||||
@param kwargs: See L{IReactorProcess.spawnProcess}.
|
||||
"""
|
||||
self._workers.append(worker)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.spawnCount += 1
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
See L{IReactorCore.stop}.
|
||||
"""
|
||||
MemoryReactorClock.stop(self)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
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,
|
||||
arg=[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 EternalTerminationPredicateFactory(object):
|
||||
"""
|
||||
A rigged terminationPredicateFactory for which time never pass.
|
||||
"""
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
See: L{task._Timer}
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class DistTrialRunnerTests(TestCase):
|
||||
"""
|
||||
Tests for L{DistTrialRunner}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a runner for testing.
|
||||
"""
|
||||
self.runner = DistTrialRunner(TreeReporter, 4, [],
|
||||
workingDirectory=self.mktemp())
|
||||
self.runner._stream = StringIO()
|
||||
|
||||
|
||||
def reap(self, workers):
|
||||
"""
|
||||
Reap the workers and trap L{ConnectionDone} failures on their
|
||||
C{endDeferred}s.
|
||||
|
||||
@param workers: The workers to reap.
|
||||
@type workers: An iterable of L{LocalWorker}
|
||||
"""
|
||||
|
||||
for worker in workers:
|
||||
worker.endDeferred.addErrback(Failure.trap, error.ConnectionDone)
|
||||
worker.processEnded(Failure(CONNECTION_DONE))
|
||||
|
||||
|
||||
def getFakeSchedulerAndEternalCooperator(self):
|
||||
"""
|
||||
Helper to create fake scheduler and cooperator in tests.
|
||||
|
||||
The cooperator has a termination timer which will never inform
|
||||
the scheduler that the task needs to be terminated.
|
||||
|
||||
@return: L{tuple} of (scheduler, cooperator)
|
||||
"""
|
||||
scheduler = FakeScheduler()
|
||||
cooperator = Cooperator(
|
||||
scheduler=scheduler,
|
||||
terminationPredicateFactory=EternalTerminationPredicateFactory,
|
||||
)
|
||||
return scheduler, cooperator
|
||||
|
||||
|
||||
def test_writeResults(self):
|
||||
"""
|
||||
L{DistTrialRunner.writeResults} writes to the stream specified in the
|
||||
init.
|
||||
"""
|
||||
stringIO = StringIO()
|
||||
result = DistReporter(Reporter(stringIO))
|
||||
self.runner.writeResults(result)
|
||||
self.assertTrue(stringIO.tell() > 0)
|
||||
|
||||
|
||||
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.runner.createLocalWorkers(protocols, "path")
|
||||
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.runner.launchWorkerProcesses(
|
||||
fakeSpawnProcess, protocols, ["foo"])
|
||||
self.assertEqual(arguments[0], arguments[1])
|
||||
self.assertTrue(os.path.exists(arguments[2]))
|
||||
self.assertEqual("foo", arguments[3])
|
||||
self.assertEqual(os.pathsep.join(sys.path),
|
||||
environment["TRIAL_PYTHONPATH"])
|
||||
|
||||
|
||||
def test_run(self):
|
||||
"""
|
||||
C{run} starts the reactor exactly once and spawns each of the workers
|
||||
exactly once.
|
||||
"""
|
||||
workers = []
|
||||
fakeReactor = CountingReactor(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
suite = TrialSuite()
|
||||
for i in range(10):
|
||||
suite.addTest(TestCase())
|
||||
self.runner.run(suite, fakeReactor)
|
||||
self.assertEqual(fakeReactor.runCount, 1)
|
||||
self.assertEqual(fakeReactor.spawnCount, self.runner._workerNumber)
|
||||
|
||||
|
||||
def test_runUsedDirectory(self):
|
||||
"""
|
||||
L{DistTrialRunner} checks if the test directory is already locked, and
|
||||
if it is generates a name based on it.
|
||||
"""
|
||||
|
||||
class CountingReactorWithLock(CountingReactor):
|
||||
|
||||
def spawnProcess(oself, worker, *args, **kwargs):
|
||||
oself._workers.append(worker)
|
||||
self.assertEqual(os.path.abspath(worker._logDirectory),
|
||||
os.path.abspath(
|
||||
os.path.join(workingDirectory + "-1",
|
||||
str(oself.spawnCount))))
|
||||
localLock = FilesystemLock(workingDirectory + "-1.lock")
|
||||
self.assertFalse(localLock.lock())
|
||||
oself.spawnCount += 1
|
||||
worker.makeConnection(FakeTransport())
|
||||
worker._ampProtocol.run = lambda *args: succeed(None)
|
||||
|
||||
newDirectory = self.mktemp()
|
||||
os.mkdir(newDirectory)
|
||||
workingDirectory = os.path.join(newDirectory, "_trial_temp")
|
||||
lock = FilesystemLock(workingDirectory + ".lock")
|
||||
lock.lock()
|
||||
self.addCleanup(lock.unlock)
|
||||
self.runner._workingDirectory = workingDirectory
|
||||
|
||||
workers = []
|
||||
|
||||
fakeReactor = CountingReactorWithLock(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
suite = TrialSuite()
|
||||
for i in range(10):
|
||||
suite.addTest(TestCase())
|
||||
|
||||
self.runner.run(suite, fakeReactor)
|
||||
|
||||
|
||||
def test_minimalWorker(self):
|
||||
"""
|
||||
L{DistTrialRunner} doesn't try to start more workers than the number of
|
||||
tests.
|
||||
"""
|
||||
workers = []
|
||||
fakeReactor = CountingReactor(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
self.runner.run(TestCase(), fakeReactor)
|
||||
self.assertEqual(fakeReactor.runCount, 1)
|
||||
self.assertEqual(fakeReactor.spawnCount, 1)
|
||||
|
||||
|
||||
def test_runUncleanWarnings(self):
|
||||
"""
|
||||
Running with the C{unclean-warnings} option makes L{DistTrialRunner}
|
||||
uses the L{UncleanWarningsReporterWrapper}.
|
||||
"""
|
||||
workers = []
|
||||
fakeReactor = CountingReactor(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
self.runner._uncleanWarnings = True
|
||||
result = self.runner.run(TestCase(), fakeReactor)
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
self.assertIsInstance(result.original,
|
||||
UncleanWarningsReporterWrapper)
|
||||
|
||||
|
||||
def test_runWithoutTest(self):
|
||||
"""
|
||||
When the suite contains no test, L{DistTrialRunner} takes a shortcut
|
||||
path without launching any process or starting the reactor.
|
||||
"""
|
||||
fakeReactor = object()
|
||||
suite = TrialSuite()
|
||||
result = self.runner.run(suite, fakeReactor)
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
output = self.runner._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.
|
||||
"""
|
||||
fakeReactor = object()
|
||||
error = ErrorHolder("an error", Failure(RuntimeError("foo bar")))
|
||||
result = self.runner.run(error, fakeReactor)
|
||||
self.assertIsInstance(result, DistReporter)
|
||||
output = self.runner._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):
|
||||
"""
|
||||
If for some reasons we can't connect to the worker process, the test
|
||||
suite catches and fails.
|
||||
"""
|
||||
|
||||
class CountingReactorWithFail(CountingReactor):
|
||||
|
||||
def spawnProcess(self, worker, *args, **kwargs):
|
||||
self._workers.append(worker)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.spawnCount += 1
|
||||
worker._ampProtocol.run = self.failingRun
|
||||
|
||||
def failingRun(self, case, result):
|
||||
return fail(RuntimeError("oops"))
|
||||
|
||||
scheduler, cooperator = self.getFakeSchedulerAndEternalCooperator()
|
||||
|
||||
workers = []
|
||||
fakeReactor = CountingReactorWithFail(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
result = self.runner.run(TestCase(), fakeReactor,
|
||||
cooperator.cooperate)
|
||||
self.assertEqual(fakeReactor.runCount, 1)
|
||||
self.assertEqual(fakeReactor.spawnCount, 1)
|
||||
scheduler.pump()
|
||||
self.assertEqual(1, len(result.original.failures))
|
||||
|
||||
|
||||
def test_runStopAfterTests(self):
|
||||
"""
|
||||
L{DistTrialRunner} calls C{reactor.stop} and unlocks the test directory
|
||||
once the tests have run.
|
||||
"""
|
||||
class CountingReactorWithSuccess(CountingReactor):
|
||||
|
||||
def spawnProcess(self, worker, *args, **kwargs):
|
||||
self._workers.append(worker)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.spawnCount += 1
|
||||
worker._ampProtocol.run = self.succeedingRun
|
||||
|
||||
def succeedingRun(self, case, result):
|
||||
return succeed(None)
|
||||
|
||||
workingDirectory = self.runner._workingDirectory
|
||||
|
||||
workers = []
|
||||
fakeReactor = CountingReactorWithSuccess(workers)
|
||||
|
||||
self.runner.run(TestCase(), fakeReactor)
|
||||
|
||||
def check():
|
||||
localLock = FilesystemLock(workingDirectory + ".lock")
|
||||
self.assertTrue(localLock.lock())
|
||||
self.assertEqual(1, fakeReactor.stopCount)
|
||||
|
||||
self.assertEqual(list(fakeReactor.triggers.keys()), ["before"])
|
||||
self.assertEqual(list(fakeReactor.triggers["before"]), ["shutdown"])
|
||||
self.reap(workers)
|
||||
|
||||
return deferLater(reactor, 0, check)
|
||||
|
||||
|
||||
def test_runWaitForProcessesDeferreds(self):
|
||||
"""
|
||||
L{DistTrialRunner} waits for the worker processes to stop when the
|
||||
reactor is stopping, and then unlocks the test directory, not trying to
|
||||
stop the reactor again.
|
||||
"""
|
||||
workers = []
|
||||
workingDirectory = self.runner._workingDirectory
|
||||
|
||||
fakeReactor = CountingReactor(workers)
|
||||
self.runner.run(TestCase(), fakeReactor)
|
||||
|
||||
def check(ign):
|
||||
# Let the AMP deferreds fire
|
||||
return deferLater(reactor, 0, realCheck)
|
||||
|
||||
def realCheck():
|
||||
localLock = FilesystemLock(workingDirectory + ".lock")
|
||||
self.assertTrue(localLock.lock())
|
||||
# Stop is not called, as it ought to have been called before
|
||||
self.assertEqual(0, fakeReactor.stopCount)
|
||||
|
||||
self.assertEqual(list(fakeReactor.triggers.keys()), ["before"])
|
||||
self.assertEqual(list(fakeReactor.triggers["before"]), ["shutdown"])
|
||||
self.reap(workers)
|
||||
|
||||
return gatherResults([
|
||||
maybeDeferred(f, *a, **kw)
|
||||
for f, a, kw in fakeReactor.triggers["before"]["shutdown"]
|
||||
]).addCallback(check)
|
||||
|
||||
|
||||
def test_runUntilFailure(self):
|
||||
"""
|
||||
L{DistTrialRunner} can run in C{untilFailure} mode where it will run
|
||||
the given tests until they fail.
|
||||
"""
|
||||
called = []
|
||||
|
||||
class CountingReactorWithSuccess(CountingReactor):
|
||||
|
||||
def spawnProcess(self, worker, *args, **kwargs):
|
||||
self._workers.append(worker)
|
||||
worker.makeConnection(FakeTransport())
|
||||
self.spawnCount += 1
|
||||
worker._ampProtocol.run = self.succeedingRun
|
||||
|
||||
def succeedingRun(self, case, result):
|
||||
called.append(None)
|
||||
if len(called) == 5:
|
||||
return fail(RuntimeError("oops"))
|
||||
return succeed(None)
|
||||
|
||||
workers = []
|
||||
fakeReactor = CountingReactorWithSuccess(workers)
|
||||
self.addCleanup(self.reap, workers)
|
||||
|
||||
scheduler, cooperator = self.getFakeSchedulerAndEternalCooperator()
|
||||
|
||||
result = self.runner.run(
|
||||
TestCase(), fakeReactor, cooperate=cooperator.cooperate,
|
||||
untilFailure=True)
|
||||
scheduler.pump()
|
||||
self.assertEqual(5, len(called))
|
||||
self.assertFalse(result.wasSuccessful())
|
||||
output = self.runner._stream.getvalue()
|
||||
self.assertIn("PASSED", output)
|
||||
self.assertIn("FAIL", output)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for distributed trial's options management.
|
||||
"""
|
||||
|
||||
import os, sys, gc
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.trial._dist.options import WorkerOptions
|
||||
|
||||
|
||||
|
||||
class WorkerOptionsTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerOptions}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Build an L{WorkerOptions} object to be used in the tests.
|
||||
"""
|
||||
self.options = WorkerOptions()
|
||||
|
||||
|
||||
def test_standardOptions(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
L{WorkerOptions.coverdir} returns the C{coverage} child directory of
|
||||
the current directory to be used for storing coverage data.
|
||||
"""
|
||||
self.assertEqual(
|
||||
os.path.realpath(os.path.join(os.getcwd(), "coverage")),
|
||||
self.options.coverdir().path)
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test for distributed trial worker side.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.trial._dist.worker import (
|
||||
LocalWorker, LocalWorkerAMP, LocalWorkerTransport, WorkerProtocol)
|
||||
from twisted.trial._dist import managercommands, workercommands
|
||||
|
||||
from twisted.scripts import trial
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
|
||||
from twisted.internet.interfaces import ITransport, IAddress
|
||||
from twisted.internet.defer import fail, succeed
|
||||
from twisted.internet.main import CONNECTION_DONE
|
||||
from twisted.internet.error import ConnectionDone
|
||||
from twisted.python.reflect import fullyQualifiedName
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.python.compat import NativeStringIO
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class FakeAMP(AMP):
|
||||
"""
|
||||
A fake amp protocol.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class WorkerProtocolTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerProtocol}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up a transport, a result stream and a protocol instance.
|
||||
"""
|
||||
self.serverTransport = StringTransport()
|
||||
self.clientTransport = StringTransport()
|
||||
self.server = WorkerProtocol()
|
||||
self.server.makeConnection(self.serverTransport)
|
||||
self.client = FakeAMP()
|
||||
self.client.makeConnection(self.clientTransport)
|
||||
|
||||
|
||||
def test_run(self):
|
||||
"""
|
||||
Calling the L{workercommands.Run} command on the client returns a
|
||||
response with C{success} sets to C{True}.
|
||||
"""
|
||||
d = self.client.callRemote(workercommands.Run, testCase="doesntexist")
|
||||
|
||||
def check(result):
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
d.addCallback(check)
|
||||
self.server.dataReceived(self.clientTransport.value())
|
||||
self.clientTransport.clear()
|
||||
self.client.dataReceived(self.serverTransport.value())
|
||||
self.serverTransport.clear()
|
||||
return d
|
||||
|
||||
|
||||
def test_start(self):
|
||||
"""
|
||||
The C{start} command changes the current path.
|
||||
"""
|
||||
curdir = os.path.realpath(os.path.curdir)
|
||||
self.addCleanup(os.chdir, curdir)
|
||||
self.server.start('..')
|
||||
self.assertNotEqual(os.path.realpath(os.path.curdir), curdir)
|
||||
|
||||
|
||||
|
||||
class LocalWorkerAMPTests(TestCase):
|
||||
"""
|
||||
Test case for distributed trial's manager-side local worker AMP protocol
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.managerTransport = StringTransport()
|
||||
self.managerAMP = LocalWorkerAMP()
|
||||
self.managerAMP.makeConnection(self.managerTransport)
|
||||
self.result = TestResult()
|
||||
self.workerTransport = StringTransport()
|
||||
self.worker = AMP()
|
||||
self.worker.makeConnection(self.workerTransport)
|
||||
|
||||
config = trial.Options()
|
||||
self.testName = "twisted.doesnexist"
|
||||
config['tests'].append(self.testName)
|
||||
self.testCase = trial._getSuite(config)._tests.pop()
|
||||
|
||||
self.managerAMP.run(self.testCase, self.result)
|
||||
self.managerTransport.clear()
|
||||
|
||||
|
||||
def pumpTransports(self):
|
||||
"""
|
||||
Sends data from C{self.workerTransport} to C{self.managerAMP}, and then
|
||||
data from C{self.managerTransport} back to C{self.worker}.
|
||||
"""
|
||||
self.managerAMP.dataReceived(self.workerTransport.value())
|
||||
self.workerTransport.clear()
|
||||
self.worker.dataReceived(self.managerTransport.value())
|
||||
|
||||
|
||||
def test_runSuccess(self):
|
||||
"""
|
||||
Run a test, and succeed.
|
||||
"""
|
||||
results = []
|
||||
|
||||
d = self.worker.callRemote(managercommands.AddSuccess,
|
||||
testName=self.testName)
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runExpectedFailure(self):
|
||||
"""
|
||||
Run a test, and fail expectedly.
|
||||
"""
|
||||
results = []
|
||||
|
||||
d = self.worker.callRemote(managercommands.AddExpectedFailure,
|
||||
testName=self.testName, error='error',
|
||||
todo='todoReason')
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.expectedFailures[0][0])
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runError(self):
|
||||
"""
|
||||
Run a test, and encounter an error.
|
||||
"""
|
||||
results = []
|
||||
errorClass = fullyQualifiedName(ValueError)
|
||||
d = self.worker.callRemote(managercommands.AddError,
|
||||
testName=self.testName, error='error',
|
||||
errorClass=errorClass,
|
||||
frames=[])
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.errors[0][0])
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runErrorWithFrames(self):
|
||||
"""
|
||||
L{LocalWorkerAMP._buildFailure} recreates the C{Failure.frames} from
|
||||
the C{frames} argument passed to C{AddError}.
|
||||
"""
|
||||
results = []
|
||||
errorClass = fullyQualifiedName(ValueError)
|
||||
d = self.worker.callRemote(managercommands.AddError,
|
||||
testName=self.testName, error='error',
|
||||
errorClass=errorClass,
|
||||
frames=["file.py", "invalid code", "3"])
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.errors[0][0])
|
||||
self.assertEqual(
|
||||
[('file.py', 'invalid code', 3, [], [])],
|
||||
self.result.errors[0][1].frames)
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runFailure(self):
|
||||
"""
|
||||
Run a test, and fail.
|
||||
"""
|
||||
results = []
|
||||
failClass = fullyQualifiedName(RuntimeError)
|
||||
d = self.worker.callRemote(managercommands.AddFailure,
|
||||
testName=self.testName, fail='fail',
|
||||
failClass=failClass,
|
||||
frames=[])
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.failures[0][0])
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runSkip(self):
|
||||
"""
|
||||
Run a test, but skip it.
|
||||
"""
|
||||
results = []
|
||||
|
||||
d = self.worker.callRemote(managercommands.AddSkip,
|
||||
testName=self.testName, reason='reason')
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.skips[0][0])
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_runUnexpectedSuccesses(self):
|
||||
"""
|
||||
Run a test, and succeed unexpectedly.
|
||||
"""
|
||||
results = []
|
||||
|
||||
d = self.worker.callRemote(managercommands.AddUnexpectedSuccess,
|
||||
testName=self.testName,
|
||||
todo='todo')
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual(self.testCase, self.result.unexpectedSuccesses[0][0])
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_testWrite(self):
|
||||
"""
|
||||
L{LocalWorkerAMP.testWrite} writes the data received to its test
|
||||
stream.
|
||||
"""
|
||||
results = []
|
||||
stream = NativeStringIO()
|
||||
self.managerAMP.setTestStream(stream)
|
||||
|
||||
command = managercommands.TestWrite
|
||||
d = self.worker.callRemote(command,
|
||||
out="Some output")
|
||||
d.addCallback(lambda result: results.append(result['success']))
|
||||
self.pumpTransports()
|
||||
|
||||
self.assertEqual("Some output\n", stream.getvalue())
|
||||
self.assertTrue(results)
|
||||
|
||||
|
||||
def test_stopAfterRun(self):
|
||||
"""
|
||||
L{LocalWorkerAMP.run} calls C{stopTest} on its test result once the
|
||||
C{Run} commands has succeeded.
|
||||
"""
|
||||
result = object()
|
||||
stopped = []
|
||||
|
||||
def fakeCallRemote(command, testCase):
|
||||
return succeed(result)
|
||||
|
||||
self.managerAMP.callRemote = fakeCallRemote
|
||||
|
||||
class StopTestResult(TestResult):
|
||||
|
||||
def stopTest(self, test):
|
||||
stopped.append(test)
|
||||
|
||||
|
||||
d = self.managerAMP.run(self.testCase, StopTestResult())
|
||||
self.assertEqual([self.testCase], stopped)
|
||||
return d.addCallback(self.assertIdentical, result)
|
||||
|
||||
|
||||
|
||||
class FakeAMProtocol(AMP):
|
||||
"""
|
||||
A fake implementation of L{AMP} for testing.
|
||||
"""
|
||||
id = 0
|
||||
dataString = b""
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.dataString += data
|
||||
|
||||
|
||||
def setTestStream(self, stream):
|
||||
self.testStream = stream
|
||||
|
||||
|
||||
|
||||
class FakeTransport(object):
|
||||
"""
|
||||
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._testLog.close)
|
||||
self.addCleanup(worker._outLog.close)
|
||||
self.addCleanup(worker._errLog.close)
|
||||
return worker
|
||||
|
||||
|
||||
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(FakeAMProtocol(), '.', '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_outReceived(self):
|
||||
"""
|
||||
L{LocalWorker.outReceived} logs the output into its C{_outLog} log
|
||||
file.
|
||||
"""
|
||||
localWorker = self.tidyLocalWorker(FakeAMProtocol(), '.', '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(FakeAMProtocol(), '.', '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 log streams.
|
||||
"""
|
||||
|
||||
localWorker = self.tidyLocalWorker(FakeAMProtocol(), '.', 'test.log')
|
||||
localWorker.connectionLost(None)
|
||||
self.assertTrue(localWorker._outLog.closed)
|
||||
self.assertTrue(localWorker._errLog.closed)
|
||||
self.assertTrue(localWorker._testLog.closed)
|
||||
|
||||
|
||||
def test_processEnded(self):
|
||||
"""
|
||||
L{LocalWorker.processEnded} calls C{connectionLost} on itself and on
|
||||
the L{AMP} protocol.
|
||||
"""
|
||||
|
||||
transport = FakeTransport()
|
||||
protocol = FakeAMProtocol()
|
||||
localWorker = LocalWorker(protocol, '.', 'test.log')
|
||||
localWorker.makeConnection(transport)
|
||||
localWorker.processEnded(Failure(CONNECTION_DONE))
|
||||
self.assertTrue(localWorker._outLog.closed)
|
||||
self.assertTrue(localWorker._errLog.closed)
|
||||
self.assertTrue(localWorker._testLog.closed)
|
||||
self.assertIdentical(None, protocol.transport)
|
||||
return self.assertFailure(localWorker.endDeferred, ConnectionDone)
|
||||
|
||||
|
||||
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 = FakeAMProtocol()
|
||||
protocol.callRemote = failCallRemote
|
||||
self.tidyLocalWorker(protocol, '.', 'test.log')
|
||||
|
||||
self.assertEqual([], self.flushLoggedErrors(RuntimeError))
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.workerreporter}.
|
||||
"""
|
||||
|
||||
from twisted.python.compat import _PY3
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase, Todo
|
||||
from twisted.trial._dist.workerreporter import WorkerReporter
|
||||
from twisted.trial._dist import managercommands
|
||||
|
||||
|
||||
class FakeAMProtocol(object):
|
||||
"""
|
||||
A fake C{AMP} implementations to track C{callRemote} calls.
|
||||
"""
|
||||
id = 0
|
||||
lastCall = None
|
||||
|
||||
def callRemote(self, command, **kwargs):
|
||||
self.lastCall = command
|
||||
self.lastArgs = kwargs
|
||||
|
||||
|
||||
|
||||
class WorkerReporterTests(TestCase):
|
||||
"""
|
||||
Tests for L{WorkerReporter}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.fakeAMProtocol = FakeAMProtocol()
|
||||
self.workerReporter = WorkerReporter(self.fakeAMProtocol)
|
||||
self.test = TestCase()
|
||||
|
||||
|
||||
def test_addSuccess(self):
|
||||
"""
|
||||
L{WorkerReporter.addSuccess} sends a L{managercommands.AddSuccess}
|
||||
command.
|
||||
"""
|
||||
self.workerReporter.addSuccess(self.test)
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddSuccess)
|
||||
|
||||
|
||||
def test_addError(self):
|
||||
"""
|
||||
L{WorkerReporter.addError} sends a L{managercommands.AddError} command.
|
||||
"""
|
||||
self.workerReporter.addError(self.test, Failure(RuntimeError('error')))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddError)
|
||||
|
||||
|
||||
def test_addErrorTuple(self):
|
||||
"""
|
||||
Adding an error using L{WorkerReporter.addError} as a
|
||||
C{sys.exc_info}-style tuple sends an L{managercommands.AddError}
|
||||
command.
|
||||
"""
|
||||
self.workerReporter.addError(
|
||||
self.test, (RuntimeError, RuntimeError('error'), None))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddError)
|
||||
|
||||
|
||||
def test_addFailure(self):
|
||||
"""
|
||||
L{WorkerReporter.addFailure} sends a L{managercommands.AddFailure}
|
||||
command.
|
||||
"""
|
||||
self.workerReporter.addFailure(self.test,
|
||||
Failure(RuntimeError('fail')))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddFailure)
|
||||
|
||||
|
||||
def test_addFailureTuple(self):
|
||||
"""
|
||||
Adding a failure using L{WorkerReporter.addFailure} as a
|
||||
C{sys.exc_info}-style tuple sends an L{managercommands.AddFailure}
|
||||
message.
|
||||
"""
|
||||
self.workerReporter.addFailure(
|
||||
self.test, (RuntimeError, RuntimeError('fail'), None))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddFailure)
|
||||
|
||||
|
||||
def test_addFailureNonASCII(self):
|
||||
"""
|
||||
L{WorkerReporter.addFailure} sends a L{managercommands.AddFailure}
|
||||
message when called with a L{Failure}, even if it includes encoded
|
||||
non-ASCII content.
|
||||
"""
|
||||
content = u"\N{SNOWMAN}".encode("utf-8")
|
||||
exception = RuntimeError(content)
|
||||
failure = Failure(exception)
|
||||
self.workerReporter.addFailure(self.test, failure)
|
||||
self.assertEqual(
|
||||
self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddFailure,
|
||||
)
|
||||
self.assertEqual(
|
||||
content,
|
||||
self.fakeAMProtocol.lastArgs["fail"],
|
||||
)
|
||||
if _PY3:
|
||||
test_addFailureNonASCII.skip = (
|
||||
"Exceptions only convert to unicode on Python 3"
|
||||
)
|
||||
|
||||
|
||||
def test_addSkip(self):
|
||||
"""
|
||||
L{WorkerReporter.addSkip} sends a L{managercommands.AddSkip} command.
|
||||
"""
|
||||
self.workerReporter.addSkip(self.test, 'reason')
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddSkip)
|
||||
|
||||
|
||||
def test_addExpectedFailure(self):
|
||||
"""
|
||||
L{WorkerReporter.addExpectedFailure} sends a
|
||||
L{managercommands.AddExpectedFailure} command.
|
||||
protocol.
|
||||
"""
|
||||
self.workerReporter.addExpectedFailure(
|
||||
self.test, Failure(RuntimeError('error')), Todo('todo'))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddExpectedFailure)
|
||||
|
||||
|
||||
def test_addExpectedFailureNoTodo(self):
|
||||
"""
|
||||
L{WorkerReporter.addExpectedFailure} sends a
|
||||
L{managercommands.AddExpectedFailure} command.
|
||||
protocol.
|
||||
"""
|
||||
self.workerReporter.addExpectedFailure(
|
||||
self.test, Failure(RuntimeError('error')))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddExpectedFailure)
|
||||
|
||||
|
||||
def test_addUnexpectedSuccess(self):
|
||||
"""
|
||||
L{WorkerReporter.addUnexpectedSuccess} sends a
|
||||
L{managercommands.AddUnexpectedSuccess} command.
|
||||
"""
|
||||
self.workerReporter.addUnexpectedSuccess(self.test, Todo('todo'))
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddUnexpectedSuccess)
|
||||
|
||||
|
||||
def test_addUnexpectedSuccessNoTodo(self):
|
||||
"""
|
||||
L{WorkerReporter.addUnexpectedSuccess} sends a
|
||||
L{managercommands.AddUnexpectedSuccess} command.
|
||||
"""
|
||||
self.workerReporter.addUnexpectedSuccess(self.test)
|
||||
self.assertEqual(self.fakeAMProtocol.lastCall,
|
||||
managercommands.AddUnexpectedSuccess)
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.trial._dist.workertrial}.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import sys
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.trial._dist.workertrial import WorkerLogObserver, main, _setupPath
|
||||
from twisted.trial._dist import (
|
||||
workertrial, _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT, workercommands,
|
||||
managercommands)
|
||||
|
||||
|
||||
|
||||
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(object):
|
||||
|
||||
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.assertIdentical('rb', mode)
|
||||
return self.readStream
|
||||
elif fd == _WORKER_AMP_STDOUT:
|
||||
self.assertEqual('wb', mode)
|
||||
return self.writeStream
|
||||
else:
|
||||
raise AssertionError("Unexpected fd %r" % (fd,))
|
||||
|
||||
|
||||
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)
|
||||
self.assertIn(
|
||||
b"No module named 'doesntexist'", 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(object):
|
||||
count = 0
|
||||
|
||||
def read(oself, size):
|
||||
oself.count += 1
|
||||
if oself.count == 1:
|
||||
raise IOError(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(object):
|
||||
count = 0
|
||||
|
||||
def read(oself, size):
|
||||
oself.count += 1
|
||||
if oself.count == 1:
|
||||
raise IOError("Something else")
|
||||
return ''
|
||||
|
||||
self.readStream = FakeStream()
|
||||
self.assertRaises(IOError, main, self.fdopen)
|
||||
|
||||
|
||||
|
||||
class SetupPathTests(TestCase):
|
||||
"""
|
||||
Tests for L{_setupPath} C{sys.path} manipulation.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(setattr, sys, "path", sys.path[:])
|
||||
|
||||
|
||||
def test_overridePath(self):
|
||||
"""
|
||||
L{_setupPath} overrides C{sys.path} if B{TRIAL_PYTHONPATH} is specified
|
||||
in the environment.
|
||||
"""
|
||||
environ = {"TRIAL_PYTHONPATH": os.pathsep.join(["foo", "bar"])}
|
||||
_setupPath(environ)
|
||||
self.assertEqual(["foo", "bar"], sys.path)
|
||||
|
||||
|
||||
def test_noVariable(self):
|
||||
"""
|
||||
L{_setupPath} doesn't change C{sys.path} if B{TRIAL_PYTHONPATH} is not
|
||||
present in the environment.
|
||||
"""
|
||||
originalPath = sys.path[:]
|
||||
_setupPath({})
|
||||
self.assertEqual(originalPath, sys.path)
|
||||
333
venv/lib/python3.9/site-packages/twisted/trial/_dist/worker.py
Normal file
333
venv/lib/python3.9/site-packages/twisted/trial/_dist/worker.py
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# -*- 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 zope.interface import implementer
|
||||
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
from twisted.internet.interfaces import ITransport, IAddress
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.protocols.amp import AMP
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import namedObject
|
||||
from twisted.trial.unittest import Todo
|
||||
from twisted.trial.runner import TrialSuite, TestLoader
|
||||
from twisted.trial._dist import workercommands, managercommands
|
||||
from twisted.trial._dist import _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT
|
||||
from twisted.trial._dist.workerreporter import WorkerReporter
|
||||
|
||||
|
||||
|
||||
class WorkerProtocol(AMP):
|
||||
"""
|
||||
The worker-side trial distributed protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, forceGarbageCollection=False):
|
||||
self._loader = TestLoader()
|
||||
self._result = WorkerReporter(self)
|
||||
self._forceGarbageCollection = forceGarbageCollection
|
||||
|
||||
|
||||
def run(self, testCase):
|
||||
"""
|
||||
Run a test case by name.
|
||||
"""
|
||||
case = self._loader.loadByName(testCase)
|
||||
suite = TrialSuite([case], self._forceGarbageCollection)
|
||||
suite.run(self._result)
|
||||
return {'success': True}
|
||||
|
||||
workercommands.Run.responder(run)
|
||||
|
||||
|
||||
def start(self, directory):
|
||||
"""
|
||||
Set up the worker, moving into given directory for tests to run in
|
||||
them.
|
||||
"""
|
||||
os.chdir(directory)
|
||||
return {'success': True}
|
||||
|
||||
workercommands.Start.responder(start)
|
||||
|
||||
|
||||
|
||||
class LocalWorkerAMP(AMP):
|
||||
"""
|
||||
Local implementation of the manager commands.
|
||||
"""
|
||||
|
||||
def addSuccess(self, testName):
|
||||
"""
|
||||
Add a success to the reporter.
|
||||
"""
|
||||
self._result.addSuccess(self._testCase)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddSuccess.responder(addSuccess)
|
||||
|
||||
|
||||
def _buildFailure(self, error, errorClass, frames):
|
||||
"""
|
||||
Helper to build a C{Failure} with some traceback.
|
||||
|
||||
@param error: An C{Exception} instance.
|
||||
|
||||
@param error: 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
|
||||
|
||||
|
||||
def addError(self, testName, error, errorClass, frames):
|
||||
"""
|
||||
Add an error to the reporter.
|
||||
"""
|
||||
failure = self._buildFailure(error, errorClass, frames)
|
||||
self._result.addError(self._testCase, failure)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddError.responder(addError)
|
||||
|
||||
|
||||
def addFailure(self, testName, fail, failClass, frames):
|
||||
"""
|
||||
Add a failure to the reporter.
|
||||
"""
|
||||
failure = self._buildFailure(fail, failClass, frames)
|
||||
self._result.addFailure(self._testCase, failure)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddFailure.responder(addFailure)
|
||||
|
||||
|
||||
def addSkip(self, testName, reason):
|
||||
"""
|
||||
Add a skip to the reporter.
|
||||
"""
|
||||
self._result.addSkip(self._testCase, reason)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddSkip.responder(addSkip)
|
||||
|
||||
|
||||
def addExpectedFailure(self, testName, error, todo):
|
||||
"""
|
||||
Add an expected failure to the reporter.
|
||||
"""
|
||||
_todo = Todo(todo)
|
||||
self._result.addExpectedFailure(self._testCase, error, _todo)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddExpectedFailure.responder(addExpectedFailure)
|
||||
|
||||
|
||||
def addUnexpectedSuccess(self, testName, todo):
|
||||
"""
|
||||
Add an unexpected success to the reporter.
|
||||
"""
|
||||
self._result.addUnexpectedSuccess(self._testCase, todo)
|
||||
return {'success': True}
|
||||
|
||||
managercommands.AddUnexpectedSuccess.responder(addUnexpectedSuccess)
|
||||
|
||||
|
||||
def testWrite(self, out):
|
||||
"""
|
||||
Print test output from the worker.
|
||||
"""
|
||||
self._testStream.write(out + '\n')
|
||||
self._testStream.flush()
|
||||
return {'success': True}
|
||||
|
||||
managercommands.TestWrite.responder(testWrite)
|
||||
|
||||
|
||||
def _stopTest(self, result):
|
||||
"""
|
||||
Stop the current running test case, forwarding the result.
|
||||
"""
|
||||
self._result.stopTest(self._testCase)
|
||||
return result
|
||||
|
||||
|
||||
def run(self, testCase, result):
|
||||
"""
|
||||
Run a test.
|
||||
"""
|
||||
self._testCase = testCase
|
||||
self._result = result
|
||||
self._result.startTest(testCase)
|
||||
testCaseId = testCase.id()
|
||||
d = self.callRemote(workercommands.Run, testCase=testCaseId)
|
||||
return d.addCallback(self._stopTest)
|
||||
|
||||
|
||||
def setTestStream(self, stream):
|
||||
"""
|
||||
Set the stream used to log output from tests.
|
||||
"""
|
||||
self._testStream = stream
|
||||
|
||||
|
||||
|
||||
@implementer(IAddress)
|
||||
class LocalWorkerAddress(object):
|
||||
"""
|
||||
A L{IAddress} implementation meant to provide stub addresses for
|
||||
L{ITransport.getPeer} and L{ITransport.getHost}.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@implementer(ITransport)
|
||||
class LocalWorkerTransport(object):
|
||||
"""
|
||||
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 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 name of the main log file for tests output.
|
||||
"""
|
||||
|
||||
def __init__(self, ampProtocol, logDirectory, logFile):
|
||||
self._ampProtocol = ampProtocol
|
||||
self._logDirectory = logDirectory
|
||||
self._logFile = logFile
|
||||
self.endDeferred = Deferred()
|
||||
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
When connection is made, create the AMP protocol instance.
|
||||
"""
|
||||
self._ampProtocol.makeConnection(LocalWorkerTransport(self.transport))
|
||||
if not os.path.exists(self._logDirectory):
|
||||
os.makedirs(self._logDirectory)
|
||||
self._outLog = open(os.path.join(self._logDirectory, 'out.log'), 'wb')
|
||||
self._errLog = open(os.path.join(self._logDirectory, 'err.log'), 'wb')
|
||||
self._testLog = open(
|
||||
os.path.join(self._logDirectory, self._logFile), 'w')
|
||||
self._ampProtocol.setTestStream(self._testLog)
|
||||
logDirectory = self._logDirectory
|
||||
d = self._ampProtocol.callRemote(workercommands.Start,
|
||||
directory=logDirectory)
|
||||
# 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._testLog.close()
|
||||
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
When the process closes, call C{connectionLost} for cleanup purposes
|
||||
and forward the information to the C{_ampProtocol}.
|
||||
"""
|
||||
self.connectionLost(reason)
|
||||
self._ampProtocol.connectionLost(reason)
|
||||
self.endDeferred.callback(reason)
|
||||
|
||||
|
||||
def outReceived(self, data):
|
||||
"""
|
||||
Send data received from stdout to log.
|
||||
"""
|
||||
|
||||
self._outLog.write(data)
|
||||
|
||||
|
||||
def errReceived(self, data):
|
||||
"""
|
||||
Write error data to log.
|
||||
"""
|
||||
self._errLog.write(data)
|
||||
|
||||
|
||||
def childDataReceived(self, childFD, data):
|
||||
"""
|
||||
Handle data received on the specific pipe for the C{_ampProtocol}.
|
||||
"""
|
||||
if childFD == _WORKER_AMP_STDOUT:
|
||||
self._ampProtocol.dataReceived(data)
|
||||
else:
|
||||
ProcessProtocol.childDataReceived(self, childFD, data)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# 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 Command, String, Boolean, Unicode
|
||||
from twisted.python.compat import _PY3
|
||||
|
||||
NativeString = Unicode if _PY3 else String
|
||||
|
||||
|
||||
|
||||
class Run(Command):
|
||||
"""
|
||||
Run a test.
|
||||
"""
|
||||
arguments = [(b'testCase', NativeString())]
|
||||
response = [(b'success', Boolean())]
|
||||
|
||||
|
||||
|
||||
class Start(Command):
|
||||
"""
|
||||
Set up the worker process, giving the running directory.
|
||||
"""
|
||||
arguments = [(b'directory', NativeString())]
|
||||
response = [(b'success', Boolean())]
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
# -*- 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 twisted.python.failure import Failure
|
||||
from twisted.python.reflect import qual
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial._dist import managercommands
|
||||
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
_DEFAULT_TODO = 'Test expected to fail'
|
||||
|
||||
def __init__(self, ampProtocol):
|
||||
"""
|
||||
@param ampProtocol: The communication channel with the trial
|
||||
distributed manager which collects all test results.
|
||||
@type ampProtocol: C{AMP}
|
||||
"""
|
||||
super(WorkerReporter, self).__init__()
|
||||
self.ampProtocol = ampProtocol
|
||||
|
||||
|
||||
def _getFailure(self, error):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Extract frames from a C{Failure} instance.
|
||||
"""
|
||||
frames = []
|
||||
for frame in failure.frames:
|
||||
frames.extend([frame[0], frame[1], str(frame[2])])
|
||||
return frames
|
||||
|
||||
|
||||
def addSuccess(self, test):
|
||||
"""
|
||||
Send a success over.
|
||||
"""
|
||||
super(WorkerReporter, self).addSuccess(test)
|
||||
testName = test.id()
|
||||
self.ampProtocol.callRemote(managercommands.AddSuccess,
|
||||
testName=testName)
|
||||
|
||||
|
||||
def addError(self, test, error):
|
||||
"""
|
||||
Send an error over.
|
||||
"""
|
||||
super(WorkerReporter, self).addError(test, error)
|
||||
testName = test.id()
|
||||
failure = self._getFailure(error)
|
||||
error = failure.getErrorMessage()
|
||||
errorClass = qual(failure.type)
|
||||
frames = [frame for frame in self._getFrames(failure)]
|
||||
self.ampProtocol.callRemote(managercommands.AddError,
|
||||
testName=testName,
|
||||
error=error,
|
||||
errorClass=errorClass,
|
||||
frames=frames)
|
||||
|
||||
|
||||
def addFailure(self, test, fail):
|
||||
"""
|
||||
Send a Failure over.
|
||||
"""
|
||||
super(WorkerReporter, self).addFailure(test, fail)
|
||||
testName = test.id()
|
||||
failure = self._getFailure(fail)
|
||||
fail = failure.getErrorMessage()
|
||||
failClass = qual(failure.type)
|
||||
frames = [frame for frame in self._getFrames(failure)]
|
||||
self.ampProtocol.callRemote(managercommands.AddFailure,
|
||||
testName=testName,
|
||||
fail=fail,
|
||||
failClass=failClass,
|
||||
frames=frames)
|
||||
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
"""
|
||||
Send a skip over.
|
||||
"""
|
||||
super(WorkerReporter, self).addSkip(test, reason)
|
||||
reason = str(reason)
|
||||
testName = test.id()
|
||||
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(WorkerReporter, self).addExpectedFailure(test, error, todo)
|
||||
errorMessage = error.getErrorMessage()
|
||||
testName = test.id()
|
||||
self.ampProtocol.callRemote(managercommands.AddExpectedFailure,
|
||||
testName=testName,
|
||||
error=errorMessage,
|
||||
todo=self._getTodoReason(todo))
|
||||
|
||||
|
||||
def addUnexpectedSuccess(self, test, todo=None):
|
||||
"""
|
||||
Send an unexpected success over.
|
||||
"""
|
||||
super(WorkerReporter, self).addUnexpectedSuccess(test, todo)
|
||||
testName = test.id()
|
||||
self.ampProtocol.callRemote(managercommands.AddUnexpectedSuccess,
|
||||
testName=testName,
|
||||
todo=self._getTodoReason(todo))
|
||||
|
||||
|
||||
def printSummary(self):
|
||||
"""
|
||||
I{Don't} print a summary
|
||||
"""
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# -*- 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 sys
|
||||
import os
|
||||
import errno
|
||||
|
||||
|
||||
|
||||
def _setupPath(environ):
|
||||
"""
|
||||
Override C{sys.path} with what the parent passed in B{TRIAL_PYTHONPATH}.
|
||||
|
||||
@see: twisted.trial._dist.disttrial.DistTrialRunner.launchWorkerProcesses
|
||||
"""
|
||||
if 'TRIAL_PYTHONPATH' in environ:
|
||||
sys.path[:] = environ['TRIAL_PYTHONPATH'].split(os.pathsep)
|
||||
|
||||
|
||||
_setupPath(os.environ)
|
||||
|
||||
|
||||
from twisted.internet.protocol import FileWrapper
|
||||
from twisted.python.log import startLoggingWithObserver, textFromEventDict
|
||||
from twisted.trial._dist.options import WorkerOptions
|
||||
from twisted.trial._dist import _WORKER_AMP_STDIN, _WORKER_AMP_STDOUT
|
||||
|
||||
|
||||
|
||||
class WorkerLogObserver(object):
|
||||
"""
|
||||
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}.
|
||||
@param _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 IOError as e:
|
||||
if e.args[0] == errno.EINTR:
|
||||
if sys.version_info < (3, 0):
|
||||
sys.exc_clear()
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue