1. Arjan Scherpenisse
  2. sparked

Commits

Arjan Scherpenisse  committed d9ad366

Made sparked.application more testable and added tests to StateMachine as well.

  • Participants
  • Parent commits 01032b3
  • Branches default

Comments (0)

Files changed (2)

File sparked/application.py

View file
  • Ignore whitespace
 
 from twisted.application import service
 from twisted.python import log, usage, filepath
-from twisted.internet import reactor
 
 from sparked import monitors, events, __version__
 
     appId = None
 
 
-    def __init__(self, appName, baseOpts, appOpts):
+    def __init__(self, appName, baseOpts, appOpts, reactor=None):
         service.MultiService.__init__(self)
 
         self.appName = appName
         self.baseOpts = baseOpts
         self.appOpts = appOpts
-        self.appId = baseOpts['id'] or appName
+        self.appId = baseOpts.get('id', appName)
 
-        self.state = StateMachine(self)
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+
+        self.state = StateMachine(self, reactor)
         self.events = events.EventDispatcher()
 
         self.createMonitors()
         prevHandler = signal.getsignal(signal.SIGUSR2)
         signal.signal(signal.SIGUSR2, lambda sig, frame: doReload(prevHandler))
 
-        reactor.callLater(0, self.starting)
-        reactor.callLater(0, self.loadOptions, firstTime=True)
-        reactor.callLater(0, self.state.set, "start")
-        reactor.callLater(0, self.started)
+        def trap(f):
+            try:
+                f()
+            except:
+                log.err()
+                self.reactor.stop()
+        self.reactor.callLater(0, trap, self.starting)
+        self.reactor.callLater(0, self.loadOptions, firstTime=True)
+        self.reactor.callLater(0, self.state.set, "start")
+        self.reactor.callLater(0, trap, self.started)
 
 
     def path(self, kind):
         # Make sure the monitors talk to us in the log.
         m.verbose = True
 
-        reactor.callLater(0, m.setServiceParent, self)
+        self.reactor.callLater(0, m.setServiceParent, self)
         self.monitors = m
         return m
 
     nextStateAfter = None
 
 
-    def __init__(self, parent):
+    def __init__(self, parent, reactor=None):
         self._listeners = []
         self.addListener(parent)
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
 
 
     def set(self, newstate):
         self.nextStateAfter = None
 
         if self._state:
-            self._call("exit_%s" % self._state)
+            self._call("exit_%s" % self._state, True)  # call reversed
 
         log.msg("%s --> %s" % (self._state, newstate))
         self._state = newstate
         self._afterStart = time.time()
         self._afterStop = self._afterStart + after
         self.nextStateAfter = after
-        self._statechanger = reactor.callLater(after, self.set, newstate)
+        self._statechanger = self.reactor.callLater(after, self.set, newstate)
 
 
     def bumpAfter(self, after=None):
         return self._state
 
 
-    def _call(self, cb, *arg):
-        for l in self._listeners:
+    def _call(self, cb, reverse=False):
+        listeners = self._listeners
+        if reverse:
+            listeners = listeners[::-1]
+        for l in listeners:
             try:
-                getattr(l, cb)(*arg)
+                getattr(l, cb)()
             except AttributeError:
                 pass
 

File sparked/test/test_application.py

View file
  • Ignore whitespace
 import tempfile
 
 from twisted.trial import unittest
+from twisted.internet import task
 
-from sparked.application import getPath, Options
+from sparked.events import EventDispatcher
+from sparked.monitors import MonitorContainer
+from sparked.application import getPath, Options, Application, StateMachine
+
 
 class TestGetPath(unittest.TestCase):
     """
         opta.save(fn)
         self.assertRaises(ValueError, TestOptsComplex3.load, fn)
 
+
+
+class TestApplication(unittest.TestCase):
+    """
+    Test the L{sparked.application.Application} class.
+    """
+
+    def testConstructor(self):
+        """
+        Test if all assignments in the application constructor are set.
+        """
+        app = Application("foo", {}, {})
+        self.assertEquals("foo", app.appId)
+        self.assertEquals({}, app.appOpts)
+        self.assertEquals({}, app.baseOpts)
+
+        self.assertIsInstance(app.state, StateMachine)
+        self.assertIsInstance(app.events, EventDispatcher)
+        self.assertIsInstance(app.monitors, MonitorContainer)
+
+
+    def testConstructorStartupOrder(self):
+        seq = []
+        class TestApp(Application):
+            def starting(app):
+                seq.append("starting")
+                self.assertEquals(None, app.state.get)
+
+            def started(app):
+                seq.append("started")
+                self.assertEquals("start", app.state.get)
+
+        clock = task.Clock()
+
+        TestApp("foo", {}, {}, reactor=clock)
+        clock.advance(0.1)
+
+        self.assertEquals(seq, ["starting", "started"])
+
+
+    def testConstructorStartupCrash(self):
+        """ The L{application.started} call may crash """
+        self.stopped = False
+
+        class TestApp(Application):
+            def starting(app):
+                raise ValueError("Whoooops")
+
+        class StoppableClock(task.Clock):
+            def stop(s):
+                self.stopped = True
+        clock = StoppableClock()
+
+        TestApp("foo", {}, {}, reactor=clock)
+        clock.advance(0.5)
+        self.assertEquals(1, len(self.flushLoggedErrors(ValueError)))
+        self.assertTrue(self.stopped, "Reactor need to be stopped on startup error")
+
+
+    def testConstructorStartupCrash2(self):
+        """ The L{application.started} call may crash as well """
+        self.stopped = False
+
+        class TestApp(Application):
+            def started(app):
+                raise ValueError("Whoooops")
+
+        class StoppableClock(task.Clock):
+            def stop(s):
+                self.stopped = True
+        clock = StoppableClock()
+
+        TestApp("foo", {}, {}, reactor=clock)
+        clock.advance(0.5)
+        self.assertEquals(1, len(self.flushLoggedErrors(ValueError)))
+        self.assertTrue(self.stopped, "Reactor need to be stopped on startup error")
+
+
+
+class TestStateMachine(unittest.TestCase):
+
+    def testConstruct(self):
+        m = StateMachine(None)
+        self.assertEquals(None, m.get)
+
+
+    def testGetSet(self):
+        m = StateMachine(None)
+        m.set("foo")
+        self.assertEquals("foo", m.get)
+
+
+    def testSetAfter(self):
+        clock = task.Clock()
+        m = StateMachine(None, reactor=clock)
+        m.setAfter("foo", 1.0)
+        self.assertEquals(None, m.get)
+        clock.advance(1.0)
+        self.assertEquals("foo", m.get)
+
+
+    def testBumpAfter(self):
+        clock = task.Clock()
+        m = StateMachine(None, reactor=clock)
+        m.setAfter("foo", 1.0)
+        self.assertEquals(None, m.get)
+        clock.advance(0.5)
+        m.bumpAfter()
+        clock.advance(0.5)
+        self.assertEquals(None, m.get)
+        clock.advance(0.5)
+        self.assertEquals("foo", m.get)
+
+
+    def testBumpAfter2(self):
+        clock = task.Clock()
+        m = StateMachine(None, reactor=clock)
+        m.setAfter("foo", 1.0)
+        self.assertEquals(None, m.get)
+        clock.advance(0.5)
+        m.bumpAfter()
+        clock.advance(0.5)
+        self.assertEquals(None, m.get)
+        m.bumpAfter()
+        clock.advance(0.5)
+        self.assertEquals(None, m.get)
+        clock.advance(0.5)
+        self.assertEquals("foo", m.get)
+
+
+    def testCallbacks(self):
+        self.called = []
+        class Listener:
+            def enter_a(s): self.called.append("enter_a")
+            def exit_a(s): self.called.append("exit_a")
+            def enter_b(s): self.called.append("enter_b")
+            def exit_b(s): self.called.append("exit_b")
+        m = StateMachine(Listener())
+        m.set("a")
+        self.assertEquals(self.called, ["enter_a"])
+
+        m.set("a")
+        self.assertEquals(self.called, ["enter_a", "exit_a", "enter_a"])
+
+        m.set("b")
+        self.assertEquals(self.called, ["enter_a", "exit_a", "enter_a", "exit_a", "enter_b"])
+
+        m.set("a")
+        self.assertEquals(self.called, ["enter_a", "exit_a", "enter_a", "exit_a", "enter_b", "exit_b", "enter_a"])
+
+
+    def testExtraListener(self):
+        self.called = []
+        class Listener:
+            post=""
+            def enter_a(s): self.called.append("enter_a"+s.post)
+            def exit_a(s): self.called.append("exit_a"+s.post)
+            def enter_b(s): self.called.append("enter_b"+s.post)
+            def exit_b(s): self.called.append("exit_b"+s.post)
+        m = StateMachine(Listener())
+        l2 = Listener()
+        l2.post = "2"
+        m.addListener(l2)
+        m.set("a")
+        self.assertEquals(self.called, ["enter_a", "enter_a2"])
+        m.set("b")
+        self.assertEquals(self.called, ["enter_a", "enter_a2", "exit_a2", "exit_a", "enter_b", "enter_b2"])
+