[Subversion] / Trellis / testreactor.py  

View of /Trellis/testreactor.py

Parent Directory | Revision Log
Revision: 2413 - (download) (as text)
Fri Nov 2 23:37:58 2007 UTC (16 years, 5 months ago) by pje
File size: 7093 byte(s)
Event loops for tests, wx, and twisted.  No docs, and
only minimal testing so far, but much hacking was done
to get these to work more-or-less correctly.  wx needs
some mocking, and it turns out that the Time service
really needs to make next_event_time() dirty when new
events are added, even if self._tick doesn't change.
That'll have to be fixed later, though (as with the wx
mocks issue).
"""Support for writing Twisted unit tests"""

from twisted.internet import reactor, selectreactor, base, task
from twisted.internet.selectreactor import _select as realselect
from twisted.python.runtime import seconds as realtime
from twisted.python import failure
from time import sleep as realsleep
import unittest

__all__ = [
    "install", "uninstall", "ReactorTestCase", "TimeoutError", "EarlyExit"
]


class TimeoutError(AssertionError):
    """A requested timeout expired

    This is a subclass of AssertionError so that unittest will treat tests
    raising it as "Failed" rather than as being in "Error".  So, you don't
    need to trap and reraise this error; just let it pass through.
    """

class EarlyExit(AssertionError):
    """The run loop was exited sooner than expected (e.g. before a result)
    
    This is a subclass of AssertionError so that unittest will treat tests
    raising it as "Failed" rather than as being in "Error".  So, you don't
    need to trap and reraise this error; just let it pass through.
    """












# Note - the class below includes 'object' in its bases so that __mro__
# will be sane for super() use when mixed with other TestCase derivatives
#
class ReactorTestCase(unittest.TestCase, object):
    """Test case mixin that ensures the test reactor is ready for use

    Note that if you include other TestCase-derived base classes in your
    subclass' bases, you should include them *after* this class, to ensure
    that this class' setUp/tearDown methods get called.  (It will then call
    the remaining classes' setUp and tearDown methods using super().)
    """

    def setUp(self):
        super(ReactorTestCase,self).setUp()
        install()

    def tearDown(self):
        uninstall()
        super(ReactorTestCase,self).tearDown()






















class TestReactor(selectreactor.SelectReactor):
    """Add test-specific features to the Twisted default reactor"""

    def start(self):
        """Reset the reactor completely

        You don't need to use this if you use ReactorTestCase, which wraps your
        tests in fresh restarts.
        """
        if self.waker is not None:
            # make sure that subsequent shutdowns won't disconnect an
            # active waker, due to wakers assuming they're singletons  :(
            self.removeReader(self.waker)
        self.__dict__.clear()
        self.__init__()
        self._simulated_time = realtime()
        self._use_realtime = None
        
    def waitFor(self, seconds, early_ok=True):
        """Run the event loop for `seconds` of simulated time

        If `early_ok` is set to False, this raises EarlyExit if the runloop
        exits before the time expires.
        """
        finish = self.callLater(seconds, self.crash)
        self.running = 1
        self.mainLoop()
        if finish.active():
            finish.cancel()
            if not early_ok:
                raise EarlyExit("Reactor exited before %s seconds" % seconds)










    def waitUntil(self, deferred, timeout):
        """Return a result or error from `deferred`

        `timeout` is a timeout value in seconds of simulated time.  If the
        timeout elapses without a result or error from the deferred,
        TimeoutError is raised.  This can also raise EarlyExit if the runloop
        is exited without the deferred having fired.
        """
        finish = self.callLater(timeout, self.crash)
        result = []

        def callback(value):
            if finish.active():
                # Only stop the reactor if the original call
                # is still active
                finish.cancel()
                self.crash()
                result.append(value)
                if isinstance(value,failure.Failure):
                    value = None    # don't pass on an exception
            # always return the value, so that any chained callbacks work
            return value

        deferred.addCallbacks(callback, callback) # call it either way

        if not result:  # don't infinite loop if the deferred has already fired
            self.running = 1
            self.mainLoop()

        if result:
            result, = result
            if isinstance(result,failure.Failure):
                result.raiseException()
            return result

        if finish.active():
            finish.cancel()
            raise EarlyExit("Reactor exited before a result was produced")
        else:
            raise TimeoutError("%s simulated seconds elapsed" % timeout)
        
    def getTime(self):
        """Get the current simulated (or real) time"""
        if self._use_realtime:
            return realtime()
        self._use_realtime = False  # once we've read it, disallow changing it
        return self._simulated_time

    def setTime(self,seconds):
        """Set the current simulated time"""
        if self._use_realtime:
            raise AssertionError("Can't set time when using real time")
        self._simulated_time = seconds

    def sleep(self,seconds):
        """Advance the simulated clock, or sleep for the number of seconds"""
        if self._use_realtime:
            realsleep(seconds)
        else:
            self.setTime(self.getTime()+seconds)

    def select(self,r,w,e,timeout=None):
        """Pretend the select() time elapses, then select for 0 seconds"""
        if not self._use_realtime:
            if timeout is not None:
                self.sleep(timeout)
            timeout = 0
        return realselect(r,w,e,timeout)














    def _setMode(self,mode):
        if self._use_realtime is not None and self._use_realtime<>mode:
            raise AssertionError(
                "Cannot change clock mode without restarting reactor"
            )
        self._use_realtime = mode

    def useRealTime(self):
        """Use wall-clock time for the current test"""
        self._setMode(True)

    def useSimulatedTime(self):
        """Use simulated time for the current test (default)"""
        self._setMode(False)



























def install():
    """Install and reset the test reactor"""
    if not isinstance(reactor,TestReactor):
        assert reactor.__class__ is selectreactor.SelectReactor, (
            "testreactor can only be installed over the default reactor"
        )
        reactor.__class__ = TestReactor

    reactor.start()

    # If Twisted defined these as methods on reactors, we wouldn't
    # need to monkeypatch their globals like this.  :(
    base.seconds = reactor.getTime
    task.seconds = reactor.getTime
    selectreactor.sleep = reactor.sleep
    selectreactor._select = reactor.select


def uninstall():
    """Shut down and uninstall the test reactor"""
    try:
        if reactor.running:
            reactor.stop()
    finally:
        if isinstance(reactor,TestReactor):
            reactor.__class__ = selectreactor.SelectReactor
        
        # Use real time
        base.seconds = realtime
        task.seconds = realtime
        selectreactor.sleep = realsleep
        selectreactor._select = realselect










cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help