Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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