Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
+++ server/lib/gutenbach/server/job.py	(revision 2620618442a1550bb343604351c81fb0dd01135b)
@@ -4,4 +4,5 @@
 import logging
 import os
+import time
 
 # initialize logger
@@ -23,9 +24,8 @@
         self.name = name
         self.priority = priority
+        self._why_done = None
 
         if document is not None:
             self.spool(document)
-
-        self._why_done = None
 
     def __repr__(self):
@@ -183,11 +183,14 @@
     @property
     def state(self):
-        """
+        """State status codes; equivalent to the IPP job-state status
+        codes.
+        
         State transitions are as follows:
-HELD ---> PENDING ---> PROCESSING <--> STOPPED (aka paused)
-             ^              |---> CANCELLED
-             |              |---> ABORTED
-             |              |---> COMPLETE ---|
-             |--------------------------------|
+        HELD ---> PENDING ---> PROCESSING <--> STOPPED (aka paused)
+                     ^              |---> CANCELLED
+                     |              |---> ABORTED
+                     |              |---> COMPLETE ---|
+                     |--------------------------------|
+                     
         """
         if self.is_ready:
Index: server/lib/gutenbach/server/player.py
===================================================================
--- server/lib/gutenbach/server/player.py	(revision 345c4768c17a73ba2749c01a590747645f06393f)
+++ server/lib/gutenbach/server/player.py	(revision 2620618442a1550bb343604351c81fb0dd01135b)
@@ -18,9 +18,18 @@
             self._callback = None
             self._paused = False
+            self._done = False
+            self._dryrun = False
+            self._dryrun_time = 0.5
 
     @property
     def is_playing(self):
         with self.lock:
-            playing = self.player and self.player.poll() is None
+            if self._dryrun:
+                playing = self.isAlive() and not self.is_done
+            else:
+                playing = self.isAlive() and \
+                          not self.is_done and \
+                          self.player is not None and \
+                          self.player.poll() is None
         return playing
 
@@ -33,7 +42,5 @@
     @property
     def is_done(self):
-        with self.lock:
-            done = self.player and self.player.poll() is not None
-        return done
+        return self._done
 
     @property
@@ -50,4 +57,5 @@
         
         self.mplayer_play()
+        self._done = True
         with self.lock:
             if self.callback:
@@ -60,4 +68,13 @@
         logger.info("playing file '%s'" % self.fh.name)
         self._paused = False
+
+        if self._dryrun:
+            step = 0.01
+            while self._dryrun_time > 0:
+                time.sleep(step)
+                self._dryrun_time -= step
+                while self.is_paused:
+                    time.sleep(0.01)
+            return
         
         # open mplayer
@@ -79,5 +96,5 @@
 
         logger.info("mplayer finished with code %d" % self.player.returncode)
-
+        
         # get output from mplayer and log it
         with self.lock:
@@ -91,17 +108,21 @@
 
     def mplayer_pause(self):
-        if not self.isAlive():
-            return
-        
         with self.lock:
             if self.is_playing:
-                self.player.stdin.write("pause\n")
+                if not self._dryrun:
+                    self.player.stdin.write("pause\n")
                 self._paused = not(self._paused)
+                logger.info("paused: %s", self.is_paused)
+            else:
+                logger.warning("trying to pause non-playing job")
                 
     def mplayer_stop(self):
-        if not self.isAlive():
-            return
-        
         with self.lock:
             if self.is_playing:
-                self.player.stdin.write("quit\n")
+                if not self._dryrun:
+                    self.player.stdin.write("quit\n")
+                else:
+                    self._dryrun_time = 0.0
+                logger.info("stopped")
+            else:
+                logger.warning("trying to stop non-playing job")
Index: server/test/server/job.py
===================================================================
--- server/test/server/job.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
+++ server/test/server/job.py	(revision 2620618442a1550bb343604351c81fb0dd01135b)
@@ -5,4 +5,5 @@
 import tempfile
 import sys
+import time
 
 def make_tempfile():
@@ -113,24 +114,94 @@
             priority=1)
         self.assertRaises(errors.InvalidDocument, job.spool, "hello")
+        self.assertRaises(errors.InvalidDocument, job.spool, [])
+        self.assertRaises(errors.InvalidDocument, job.spool, 12345)
+
+        fh = make_tempfile()
+        job = GutenbachJob(
+            job_id=1,
+            creator="foo",
+            name="test",
+            priority=1,
+            document=fh)
+        self.assertRaises(errors.InvalidJobStateException, job.spool, "hello")
+        self.assertRaises(errors.InvalidJobStateException, job.spool, [])
+        self.assertRaises(errors.InvalidJobStateException, job.spool, 12345)
+
+class TestOperations(unittest.TestCase):
+
+    def setUp(self):
+        fh = make_tempfile()
+        self.job = GutenbachJob(
+            job_id=1,
+            creator="foo",
+            name="test",
+            priority=1,
+            document=fh)
+        self.job.player._dryrun = True
+
+    def testPlay(self):
+        self.job.play()
+        time.sleep(0.01)
+        self.assertTrue(self.job.is_playing)
+
+        while self.job.is_playing:
+            time.sleep(0.1)
+
+        self.assertTrue(self.job.is_done)
+        self.assertTrue(self.job.is_completed)
+        self.assertFalse(self.job.is_aborted)
+        self.assertFalse(self.job.is_cancelled)
+
+    def testPause(self):
+        self.job.play()
+        time.sleep(0.01)
+        self.assertTrue(self.job.is_playing)
+        self.assertFalse(self.job.is_paused)
+
+        self.job.pause()
+        self.assertTrue(self.job.is_playing)
+        self.assertTrue(self.job.is_paused)
+
+        time.sleep(0.6)
+        self.assertTrue(self.job.is_playing)
+        self.assertTrue(self.job.is_paused)
+
+        self.job.pause()
+        self.assertTrue(self.job.is_playing)
+        self.assertFalse(self.job.is_paused)
         
-class TestGoodGutenbachJob(unittest.TestCase):
-
-    def setUp(self):
-        self.job = GutenbachJob(job_id=1, creator="foo", name="test")
-
-    def testSpool(self):
-        fh = make_tempfile()
-        self.assertTrue(self.job.is_valid)
-        self.assertFalse(self.job.is_ready)
-        self.job.spool(fh)
-        self.assertTrue(self.job.is_ready)
-        
-        # Verify various properties
-        self.assertEqual(self.job.document, fh.name)
-        self.assertNotEqual(self.job.player, None)
-        self.assertEqual(self.job.state, States.PENDING)
+        while self.job.is_playing:
+            time.sleep(0.1)
+            
+        self.assertTrue(self.job.is_done)
+        self.assertTrue(self.job.is_completed)
+        self.assertFalse(self.job.is_aborted)
+        self.assertFalse(self.job.is_cancelled)
+
+    def testCancel(self):
+        self.job.play()
+        time.sleep(0.01)
+        self.assertTrue(self.job.is_playing)
+        self.assertFalse(self.job.is_cancelled)
+
+        self.job.cancel()
+        time.sleep(0.01)
+        self.assertFalse(self.job.is_playing)
+        self.assertTrue(self.job.is_done)
+        self.assertTrue(self.job.is_cancelled)
+        self.assertFalse(self.job.is_aborted)
+
+    def testAbort(self):
+        self.job.play()
+        time.sleep(0.01)
+        self.assertTrue(self.job.is_playing)
+        self.assertFalse(self.job.is_aborted)
 
         self.job.abort()
-
+        time.sleep(0.01)
+        self.assertFalse(self.job.is_playing)
+        self.assertTrue(self.job.is_done)
+        self.assertFalse(self.job.is_cancelled)
+        self.assertTrue(self.job.is_aborted)
 
 if __name__ == "__main__":
