Commits

Matt Pryor committed be58e5d

Fully tested asynchronous task implementation.

  • Participants
  • Parent commits 6703059

Comments (0)

Files changed (22)

 {
     "name" : "mkjpryor/async",
     "type" : "library",
-    "description" : "Asyncronous promise resolution using generators",
+    "description" : "Asyncronous tasks using generators",
     "homepage" : "https://bitbucket.org/mkjpryor/async/wiki/Home",
     "authors" : [
         {
         }
     ],
     "require": {
-        "php": ">=5.5.0",
-        "react/promise": "1.*"
+        "php" : ">=5.5.0",
+        "react/promise" : "1.*"
+    },
+    "require-dev" : {
+        "phpunit/phpunit" : "*"
     },
     "autoload" : {
         "psr-0" : {

src/Async/AsyncAction.php

-<?php
-
-namespace Async;
-
-
-/**
- * Invokable action class for use with {@see \Async\Scheduler}
- * 
- * Allows functions to use the following syntax to wait for a value from a
- * React promise without blocking:
- * 
- *   $value = (yield $promise);
- */
-class AsyncAction {
-    /**
-     * The callable that will return the generator
-     *
-     * @var callable
-     */
-    protected $initializer = null;
-    
-    /**
-     * The generator that is yielding promises
-     *
-     * @var \Generator
-     */
-    protected $generator = null;
-    
-    /**
-     * The (future) result of the current promise
-     *
-     * @var \Async\PromisedResult
-     */
-    protected $result = null;
-    
-    /**
-     * Create a new AsyncAction
-     * 
-     * The given initializer function is called the first time the action is invoked,
-     * and should return an object that implements the same interface as {@see \Generator}
-     * and yields promises
-     * 
-     * @param callable $initializer
-     */
-    public function __construct(callable $initializer) {
-        $this->initializer = $initializer;
-    }
-        
-    /**
-     * Invokes the action
-     * 
-     * @return boolean
-     */
-    public function __invoke() {
-        if( $this->result ) {
-            // If we have a current promise that is not complete, there is nothing
-            // to do now, but we would like to be scheduled again
-            if( !$this->result->isComplete() ) return true;
-        
-            // If we have a completed promise, send it's result to the action
-            if( $this->result->hasError() ) {
-                $this->generator->throw($this->result->error());
-            } else {
-                $this->generator->send($this->result->result());
-            }
-            
-            // We are done with that result
-            $this->result = null;
-        }
-        
-        // If the generator has not yet been created, create it
-        if( $this->generator === null ) {
-            $this->generator = call_user_func($this->initializer);
-            $this->generator->rewind();
-        }
-        
-        // If the generator has not yielded another promise, we are done
-        if( !$this->generator->valid() ) return false;
-        
-        // Otherwise, create a promised result with the promise and schedule ourselves
-        // to run next time
-        $this->result = new PromisedResult($this->generator->current());
-        return true;
-    }
-}

src/Async/PromisedResult.php

-<?php
-
-namespace Async;
-
-use React\Promise\PromiseInterface;
-
-
-/**
- * Class representing a promise result
- * It assumes that any rejected promises will give an exception as their reason
- * 
- * It can exist in one of three states:
- *   > Incomplete - neither result nor error will be meaningful
- *   > Complete (no error) - contains the value that the promise was resolved with
- *   > Complete (error) - contains the exception that the promise was rejected with
- */
-class PromisedResult {
-    /**
-     * Indicates if the promise has completed
-     *
-     * @var boolean
-     */
-    protected $complete = false;
-    /**
-     * The result of the promise
-     *
-     * @var mixed
-     */
-    protected $result = null;
-    /**
-     * The exception thrown by the promise
-     *
-     * @var \Exception
-     */
-    protected $exception = null;
-    
-    /**
-     * Create a new PromisedResult representing the eventual result of the given
-     * promise
-     * 
-     * @param \React\Promise\PromiseInterface $promise
-     */
-    public function __construct(PromiseInterface $promise) {
-        $promise->then(
-            function($result) {
-                // On successful completion, store the result
-                $this->complete = true;
-                $this->result = $result;
-            },
-            function(\Exception $exception) {
-                // On error, store the failure
-                $this->complete = true;
-                $this->exception = $exception;
-            }
-        );
-    }
-    
-    /**
-     * Indicates if the promise has completed
-     * 
-     * @return boolean
-     */
-    public function isComplete() {
-        return $this->complete;
-    }
-    
-    /**
-     * Indicates if there was an error
-     * 
-     * @return boolean
-     */
-    public function hasError() {
-        return $this->exception !== null;
-    }
-    
-    /**
-     * The result of the promise
-     * 
-     * This is only meaningful is isCompleted() = true and hasError() = false
-     * 
-     * @return mixed
-     */
-    public function result() {
-        return $this->result;
-    }
-    
-    /**
-     * The exception thrown by the promise
-     * 
-     * This is only meaningful is isCompleted() = hasError() = true
-     * 
-     * @return \Exception
-     */
-    public function error() {
-        return $this->exception;
-    }
-}

src/Async/Scheduler.php

 
 namespace Async;
 
+use Async\Task\Task;
+
 
 class Scheduler {
     /**
     protected $running = false;
     
     /**
-     * The queue of actions to execute next time
+     * The queue of tasks to execute next time
      *
-     * @var \SplPriorityQueue
+     * @var \SplObjectStorage
      */
     protected $next = null;
     
     public function __construct() {
-        $this->next = static::createQueue();
+        $this->next = new \SplObjectStorage();
     }
     
     /**
-     * Add an action to the scheduler with the given priority. Each action will
-     * be run each time the scheduler ticks, with those with the highest priority
-     * run first.
-     * 
-     * Actions should accept no arguments, and should return a boolean
+     * Add a task to the scheduler to be run on the next tick
      * 
-     * A return value of false means the action will not be scheduled again
-     * A return value of true means that the action will continue to be scheduled
+     * Tasks will continue to be scheduled until they are complete
      * 
-     * All actions will be run at least once
-     * 
-     * @param callable $action
-     * @param integer $priority
+     * @param \Async\Task $task
      */
-    public function add(callable $action, $priority = 0) {       
-        // Default priority is 0
-        $this->next->insert($action, $priority);
+    public function add(Task $task) {
+        // Just add the task to be run next tick
+        $this->next->attach($task);
     }
     
     /**
-     * Run all scheduled actions once
+     * Run all scheduled tasks once
      * 
-     * Return true if there are actions to run next time, false otherwise
+     * Return true if there are tasks to run next time, false otherwise
      * 
      * @return boolean
      */
         // Get the queue of actions for this tick
         // This is in case any of the actions adds an action to be called on
         // the next tick
-        $current = $this->next;
+        $tasks = $this->next;
         
         // Initialise the queue for next tick
-        $this->next = static::createQueue();
+        $this->next = new \SplObjectStorage();
         
-        foreach( $current as $action ) {
-            // Execute the action
-            $continue = call_user_func($action['data']);
+        foreach( $tasks as $task ) {
+            /* @var $task \Async\Task\Task */
             
-            // If the action has no return value (null) or the return value is
-            // truthy, schedule it for next time
-            if( $continue === null || $continue )
-                $this->next->insert($action['data'], $action['priority']);
+            // Execute the task
+            $task->tick($this);
+            // If the task is not complete, reschedule it
+            if( !$task->isComplete() ) $this->add($task);
         }
         
-        // If $this->running is false, we always want to return false
-        // Otherwise, we return false if there are no actions for next time, true otherwise
-        return $this->running && !$this->next->isEmpty();
+        // Return true if there are tasks to be run next time, false if there are
+        // no more tasks
+        return count($this->next) > 0;
     }
     
     /**
         $this->running = true;
         
         // Tick until there are no more actions or we are manually stopped
-        while( $this->tick() ) { /* NOOP */ }
+        while( $this->running && $this->tick() ) { /* NOOP */ }
     }
     
     /**
     public function stop() {
         $this->running = false;
     }
-    
-    /**
-     * Returns a new \SPLPriorityQueue with the extract flags set correctly
-     * 
-     * @return \SplPriorityQueue
-     */
-    protected static function createQueue() {
-        $queue = new \SplPriorityQueue();
-        $queue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
-        return $queue;
-    }
 }

src/Async/Task/CallableTask.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Task that takes any callable object and runs it once. The result of running
+ * the callable becomes the result of the task.
+ */
+class CallableTask implements Task {
+    /**
+     * The callable object that the task will run
+     *
+     * @var callable
+     */
+    protected $callable = null;
+    
+    /**
+     * Indicates if the task is complete or not
+     *
+     * @var boolean
+     */
+    protected $complete = false;
+    
+    /**
+     * The result of the callable, if the call was successful
+     *
+     * @var mixed
+     */
+    protected $result = null;
+    
+    /**
+     * The exception thrown by the callable, if the call was unsuccessful
+     *
+     * @var \Exception
+     */
+    protected $exception = null;
+    
+    /**
+     * Create a new task from the given callable
+     * 
+     * If provided, the given arguments are passed when the callable is executed
+     * 
+     * @param callable $callable
+     * @param array $args
+     */
+    public function __construct(callable $callable, array $args = []) {
+        // The actual callable we will execute is a closure that captures the
+        // arguments
+        $this->callable = function() use($callable, $args) {
+            return call_user_func_array($callable, $args);
+        };
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isComplete() {
+        return $this->complete;
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function isFaulted() {
+        return $this->exception !== null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        return $this->result;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getException() {
+        return $this->exception;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function tick(\Async\Scheduler $scheduler) {
+        if( $this->isComplete() ) return;
+        
+        // On tick, we just run the callable and store the result
+        try {
+            $this->result = call_user_func($this->callable);
+        }
+        catch( \Exception $e ) {
+            $this->exception = $e;
+        }
+        
+        $this->complete = true;
+    }
+}

src/Async/Task/GeneratorTask.php

+<?php
+
+namespace Async\Task;
+
+use Async\Scheduler;
+
+
+class GeneratorTask implements Task {
+    /**
+     * The generator we are using
+     *
+     * @var \Generator
+     */
+    protected $generator = null;
+    
+    /**
+     * The task that the generator is paused waiting for
+     *
+     * @var \Async\Task\Task
+     */
+    protected $waiting = null;
+    
+    /**
+     * The exception that caused us to exit, if any
+     *
+     * @var \Exception
+     */
+    protected $exception = null;
+    
+    /**
+     * Creates a new task using the given generator
+     * 
+     * @param \Generator $generator
+     */
+    public function __construct(\Generator $generator) {
+        $this->generator = $generator;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isComplete() {
+        // We are complete if the generator has no more elements
+        return !$this->generator->valid();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isFaulted() {
+        return $this->exception !== null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        // There is never a result from a generator task
+        return null;
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function getException() {
+        return $this->exception;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function tick(Scheduler $scheduler) {
+        // Check if we are waiting for a task to complete
+        if( $this->waiting ) {
+            if( $this->waiting->isComplete() ) {
+                // If the task we are waiting for is complete, we need to resume
+                // the generator, passing the result of running the task
+                // We want to catch any errors that are thrown
+                try {
+                    if( $this->waiting->isFaulted() ) {
+                        $this->generator->throw($this->waiting->getException());
+                    }
+                    else {
+                        $this->generator->send($this->waiting->getResult());
+                    }
+                }
+                catch( \Exception $e ) {
+                    $this->exception = $e;
+                }
+            }
+            else {
+                // If the task we are waiting for is not complete, there is nothing
+                // to do
+                return;
+            }
+            
+            // If we get this far, we are done waiting for that task
+            $this->waiting = null;
+        }
+        
+        // If we are complete (i.e. the generator has no more items or has thrown
+        // an error), there is nothing more to do
+        if( $this->isComplete() ) return;
+        
+        // Otherwise, we wait on the yielded task to complete
+        $this->waiting = $this->generator->current();
+        // Schedule the task we are waiting on with the scheduler
+        $scheduler->add($this->waiting);
+    }
+}

src/Async/Task/PromiseTask.php

+<?php
+
+namespace Async\Task;
+
+use React\Promise\PromiseInterface;
+
+
+/**
+ * Task that waits for a React promise to complete
+ */
+class PromiseTask implements Task {
+    /**
+     * Indicates if the task is complete or not
+     *
+     * @var boolean
+     */
+    protected $complete = false;
+    
+    /**
+     * The value that the promise was resolved with if it was resolved
+     *
+     * @var mixed
+     */
+    protected $result = null;
+    
+    /**
+     * The exception that the promise was rejected with if it was rejected
+     *
+     * @var \Exception
+     */
+    protected $exception = null;
+    
+    /**
+     * Create a new task from the given promise
+     * 
+     * @param \React\Promise\PromiseInterface $promise
+     */
+    public function __construct(PromiseInterface $promise) {
+        // When the promise completes, we want to store the result to be
+        // processed on the next tick
+        $promise->then(
+            function($result) {
+                $this->complete = true;
+                $this->result = $result;
+            },
+            function(\Exception $exception) {
+                $this->complete = true;
+                $this->exception = $exception;
+            }
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isComplete() {
+        return $this->complete;
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function isFaulted() {
+        return $this->exception !== null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        return $this->result;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getException() {
+        return $this->exception;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function tick(\Async\Scheduler $scheduler) {
+        /*
+         * This is a NOOP for a promise task - all the work is done in the
+         * callbacks given to then
+         */
+    }
+}

src/Async/Task/RecurringTask.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Task that takes any callable object and runs it once per tick for the specified
+ * number of times
+ */
+class RecurringTask implements Task {
+    /**
+     * The callable object that the task will run
+     *
+     * @var callable
+     */
+    protected $callable = null;
+    
+    /**
+     * The number of times that the the task should be invoked
+     *
+     * @var integer
+     */
+    protected $times = 0;
+    
+    /**
+     * The number of times that the tick method has been called
+     *
+     * @var integer
+     */
+    protected $ticks = 0;
+    
+    /**
+     * The exception thrown by the callable, if there is one
+     *
+     * @var \Exception
+     */
+    protected $exception = null;
+    
+    /**
+     * Create a new task from the given callable
+     * 
+     * If $args are given, they are the arguments passed whenever the callable is executed
+     * 
+     * If $times is given and >= 0, it is the number of times that the task will be
+     * invoked
+     * If $times < 0, the task will be invoked forever
+     * 
+     * @param callable $callable
+     * @param array $args
+     * @param integer $times  $times < 0 means run forever
+     */
+    public function __construct(callable $callable, array $args = [], $times = -1) {
+        // The actual callable we will execute is a closure that captures the
+        // arguments
+        $this->callable = function() use($callable, $args) {
+            call_user_func_array($callable, $args);
+        };
+        
+        $this->times = $times;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isComplete() {
+        /*
+         * The task is complete if:
+         * 
+         *   1. There is an error
+         *   2. $times > 0 and we have been invoked $times times
+         */
+        return $this->isFaulted() ||
+               ( $this->times >= 0 && $this->ticks >= $this->times );
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function isFaulted() {
+        return $this->exception !== null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        // There is never a result from this task
+        return null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getException() {
+        return $this->exception;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function tick(\Async\Scheduler $scheduler) {
+        // If we are complete, there is nothing to do
+        if( $this->isComplete() ) return;
+        
+        // Increment the counter
+        $this->ticks++;
+        
+        // On tick, we just run the callable, catching any exceptions
+        try {
+            call_user_func($this->callable);
+        }
+        catch( \Exception $e ) {
+            $this->exception = $e;
+        }
+    }
+}

src/Async/Task/Task.php

+<?php
+
+namespace Async\Task;
+
+use Async\Scheduler;
+
+
+interface Task {
+    /**
+     * Indicates if the task has completed
+     * 
+     * @return boolean
+     */
+    public function isComplete();
+    
+    /**
+     * Indicates if there was an error running the task
+     * 
+     * @return boolean
+     */
+    public function isFaulted();
+    
+    /**
+     * Gets the result of running the task, if there is one, or null otherwise
+     * 
+     * If the task is not complete, null is returned
+     * 
+     * @return mixed
+     */
+    public function getResult();
+    
+    /**
+     * Gets the exception that caused the error, if there is one, or null otherwise
+     * 
+     * If the task is not complete, null is returned
+     * 
+     * @return \Exception
+     */
+    public function getException();
+    
+    /**
+     * Perform one 'tick' of the task
+     * 
+     * @param \Async\Scheduler $scheduler
+     */
+    public function tick(Scheduler $scheduler);
+}

test/Async/Test/AsyncActionTest.php

-<?php
-
-namespace Async\Test;
-
-
-class AsyncActionTest extends \PHPUnit_Framework_TestCase {
-    /**
-     * The action object under test
-     *
-     * @var \Async\AsyncAction
-     */
-    protected $action;
-    
-    /*
-     * Variables controlling the state of the generator
-     */
-    /** @var \React\Promise\Deferred */
-    protected $promise = null;
-    protected $current = null;
-    protected $exit = false;
-    
-    protected function setUp() {
-        $this->action = new \Async\AsyncAction(function() {
-            while( true ) {
-                // If we have been told to exit, exit
-                if( $this->exit ) return;
-    
-                // If not, yield the current promise and wait for it's return
-                $this->current = (yield $this->promise);
-            }
-        });
-    }
-    
-    public function testGeneratorExit() {
-        $this->exit = true;
-        
-        // When the generator produces no more promises, the action should not
-        // ask to be re-scheduled
-        $this->assertFalse($this->action->__invoke());
-    }
-    
-    public function testPreResolved() {
-        // In this test, we will yield a pre-resolved promise
-        $this->promise = \React\Promise\When::resolve("resolved");
-        
-        // On this call, the promise will be yielded, but current should not have
-        // been set
-        // It should also ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        $this->assertNull($this->current);
-        
-        // Let's exit this time
-        $this->exit = true;
-        
-        // On this call, current should be set to the resolved value, and it
-        // should not ask to be re-scheduled
-        $this->assertFalse($this->action->__invoke());
-        $this->assertEquals("resolved", $this->current);
-    }
-    
-    public function testPreRejected() {
-        // In this test, we will yield a pre-rejected promise
-        $this->promise = \React\Promise\When::reject(new \Exception("rejected"));
-        
-        // On this call, the promise will be yielded, but the exception will not
-        // be thrown
-        // It should also ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        
-        // On this call, the exception should be thrown
-        $this->setExpectedException("\Exception", "rejected");
-        $this->action->__invoke();
-    }
-    
-    public function testDelayedResolution() {
-        // In this test, we will delay resolving the promise for multiple invocations
-        // of the action
-        $this->promise = new \React\Promise\Deferred();
-        
-        // On this call, the promise will be yielded, and the action should ask
-        // to be re-scheduled
-        // $this->current should not be set
-        $this->assertTrue($this->action->__invoke());
-        $this->assertNull($this->current);
-        
-        // When the generator is resumed, we want it to exit
-        $this->exit = true;
-        
-        // On this call, the promise has not been resolved, so the action should
-        // just ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        $this->assertNull($this->current);
-        
-        // Resolve the promise
-        $this->promise->resolve("resolved");
-        
-        // On this call, the promise has been resolved, so the generator should
-        // be resumed and current should be set
-        // Since we are exiting, the action should not ask to be re-scheduled
-        $this->assertFalse($this->action->__invoke());
-        $this->assertEquals("resolved", $this->current);
-    }
-    
-    public function testDelayedRejection() {
-        // In this test, we will delay rejecting the promise for multiple invocations
-        // of the action
-        $this->promise = new \React\Promise\Deferred();
-        
-        // On this call, the promise will be yielded, and the action should ask
-        // to be re-scheduled
-        // No exception should be thrown
-        $this->assertTrue($this->action->__invoke());
-        
-        // When the generator is resumed, we want it to exit
-        $this->exit = true;
-        
-        // On this call, the promise has not been rejected, so the action should
-        // just ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        
-        // Reject the promise
-        $this->promise->reject(new \Exception('rejected'));
-        
-        // On this call, the promise has been rejected, so the generator should
-        // be resumed and the exception should be thrown
-        $this->setExpectedException('\Exception', 'rejected');
-        $this->action->__invoke();
-    }
-    
-    public function testMultipleResolutionsThenRejection() {
-        // In this test, we will yield and resolve two promises before yielding and
-        // rejecting a third
-        $this->promise = new \React\Promise\Deferred();
-        
-        // On this call the promise will be yielded, and current will not be set
-        // The action should ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        $this->assertNull($this->current);
-        
-        // On this call, the promise has not been resolved, so the action should
-        // just ask to be re-scheduled
-        $this->assertTrue($this->action->__invoke());
-        $this->assertNull($this->current);
-        
-        // Resolve the current promise
-        $this->promise->resolve("resolved once");
-        
-        // Create a new promise to be yielded
-        $this->promise = new \React\Promise\Deferred();
-        
-        // On this call, the first promise has been resolved, so the generator should
-        // be resumed, current should be set and the second promise should be yielded
-        // The action should ask to be scheduled again
-        $this->assertTrue($this->action->__invoke());
-        $this->assertEquals("resolved once", $this->current);
-        
-        // Resolve the current promise
-        $this->promise->resolve("resolved twice");
-        
-        // Create a new promise to be yielded
-        $this->promise = new \React\Promise\Deferred();
-        
-        // On this call, the second promise has been resolved, so the generator should
-        // be resumed, current should be set and the third promise should be yielded
-        // The action should ask to be scheduled again
-        $this->assertTrue($this->action->__invoke());
-        $this->assertEquals("resolved twice", $this->current);
-        
-        // Reject the current promise
-        $this->promise->reject(new \Exception('rejected'));
-        
-        // On this call, the promise has been rejected, so the generator should
-        // be resumed and the exception should be thrown
-        $this->setExpectedException('\Exception', 'rejected');
-        $this->action->__invoke();
-    }
-}

test/Async/Test/PromisedResultTest.php

-<?php
-
-namespace Async\Test;
-
-
-class PromisedResultTest extends \PHPUnit_Framework_TestCase {
-    public function testPreResolved() {
-        // This test checks that the promised result immediately reports itself
-        // as complete if created with a promise that is already resolved
-        
-        $result = new \Async\PromisedResult(
-            \React\Promise\When::resolve("resolved")
-        );
-        
-        // Check that the result reports as complete
-        $this->assertTrue($result->isComplete());
-        
-        // Check that it reports no error
-        $this->assertFalse($result->hasError());
-        
-        // Check that the result is correct
-        $this->assertEquals("resolved", $result->result());
-    }
-    
-    public function testPreRejected() {
-        // This test checks that the promised result immediately reports itself
-        // as complete if created with a promise that is already rejected
-        
-        $result = new \Async\PromisedResult(
-            \React\Promise\When::reject(new \Exception("rejected"))
-        );
-        
-        // Check that the result reports as complete
-        $this->assertTrue($result->isComplete());
-        
-        // Check that it reports an error
-        $this->assertTrue($result->hasError());
-        
-        // Check that the error is correct
-        $this->assertEquals("rejected", $result->error()->getMessage());
-    }
-    
-    public function testDelayedResolve() {
-        // This test checks that the promised result initially reports itself as
-        // incomplete before later resolving with a value
-        
-        $deferred = new \React\Promise\Deferred();
-        
-        // Create a new PromisedResult with the promise from the deferred
-        $result = new \Async\PromisedResult($deferred->promise());
-        
-        // Check that it reports as not completed
-        $this->assertFalse($result->isComplete());
-        
-        // Mark the promise as resolved
-        $deferred->resolve("resolved");
-        
-        // Check that the result now reports as complete
-        $this->assertTrue($result->isComplete());
-        
-        // Check that it reports no error
-        $this->assertFalse($result->hasError());
-        
-        // Check that the result is correct
-        $this->assertEquals("resolved", $result->result());
-    }
-    
-    public function testDelayedReject() {
-        // This test checks that the promised result initially reports itself as
-        // incomplete before later resolving with an error
-        
-        $deferred = new \React\Promise\Deferred();
-        
-        // Create a new PromisedResult with the promise from the deferred
-        $result = new \Async\PromisedResult($deferred->promise());
-        
-        // Check that it reports as not completed
-        $this->assertFalse($result->isComplete());
-        
-        // Reject the promise with an error
-        $deferred->reject(new \Exception("rejected"));
-        
-        // Check that the result now reports as complete
-        $this->assertTrue($result->isComplete());
-        
-        // Check that it reports an error
-        $this->assertTrue($result->hasError());
-        
-        // Check that the error is correct
-        $this->assertEquals("rejected", $result->error()->getMessage());
-    }
-}

test/Async/Test/Scheduler/AddTaskTask.php

+<?php
+
+namespace Async\Test\Scheduler;
+
+
+class AddTaskTask implements \Async\Task\Task {
+    protected $taskToAdd = null;
+    
+    public function __construct(\Async\Task\Task $task) {
+        $this->taskToAdd = $task;
+    }
+    
+    public function isComplete() {
+        // Never complete
+        return false;
+    }
+
+    public function isFaulted() {
+        // Never has an error
+        return false;
+    }
+
+    public function getResult() {
+        // Never has a result
+        return null;
+    }
+
+    public function getException() {
+        // Never has an error
+        return null;
+    }
+
+    public function tick(\Async\Scheduler $scheduler) {
+        // Just add the task that we were told to add
+        $scheduler->add($this->taskToAdd);
+    }    
+}

test/Async/Test/Scheduler/ExitingTask.php

+<?php
+
+namespace Async\Test\Scheduler;
+
+
+class ExitingTask implements \Async\Task\Task {
+    protected $count = 0;
+    
+    public function isComplete() {
+        // Never complete
+        return false;
+    }
+
+    public function isFaulted() {
+        // Never have an error
+        return false;
+    }
+
+    public function getResult() {
+        // Never have a result
+        return null;
+    }
+
+    public function getException() {
+        // Never have an error
+        return null;
+    }
+
+    public function tick(\Async\Scheduler $scheduler) {
+        $this->count++;
+        
+        if( $this->count >= 3 ) $scheduler->stop();
+    }    
+}

test/Async/Test/Scheduler/SchedulerTest.php

+<?php
+
+namespace Async\Test\Scheduler;
+
+
+class SchedulerTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that Scheduler::run exits immediately when no tasks are scheduled
+     */
+    public function testNoTasksExitsImmediately() {        
+        $scheduler = new \Async\Scheduler();
+        
+        $scheduler->run();
+        
+        $this->assertTrue(true, "We should get here as there are no actions");
+    }
+    
+    /**
+     * Test that Task::tick is invoked when the scheduler ticks
+     */
+    public function testTaskInvokedOnTick() {
+        $scheduler = new \Async\Scheduler();
+        
+        // Add a task that expects tick to be called exactly once
+        $task = $this->getMock('\Async\Task\Task');
+        $task->expects($this->once())->method('tick')->with($scheduler);
+        $scheduler->add($task);
+        
+        $scheduler->tick();
+    }
+    
+    /**
+     * Test that incomplete tasks are rescheduled
+     */
+    public function testIncompleteTaskRescheduled() {
+        $scheduler = new \Async\Scheduler();
+        
+        /*
+         * Add a task that:
+         * 
+         *   1. Returns false when isComplete is called
+         *   2. Expects tick to be called twice
+         */
+        $task = $this->getMock('\Async\Task\Task');
+        $task->expects($this->any())->method('isComplete')->will($this->returnValue(false));
+        $task->expects($this->exactly(2))->method('tick')->with($scheduler);
+        $scheduler->add($task);
+        
+        $scheduler->tick();
+        $scheduler->tick();
+    }
+    
+    /**
+     * Test that complete tasks are not rescheduled
+     */
+    public function testCompleteTaskNotRescheduled() {
+        $scheduler = new \Async\Scheduler();
+        
+        /*
+         * Add a task that:
+         * 
+         *   1. Returns true when isComplete is called
+         *   2. Expects tick to be called only once
+         */
+        $task = $this->getMock('\Async\Task\Task');
+        $task->expects($this->any())->method('isComplete')->will($this->returnValue(true));
+        $task->expects($this->exactly(1))->method('tick')->with($scheduler);
+        $scheduler->add($task);
+        
+        $scheduler->tick();
+        $scheduler->tick();
+    }
+    
+    /**
+     * Test that multiple tasks can be executed each tick
+     */
+    public function testMultipleTasks() {
+        $scheduler = new \Async\Scheduler();
+        
+        // Add two tasks that expect tick to be called exactly once
+        $task1 = $this->getMock('\Async\Task\Task');
+        $task1->expects($this->once())->method('tick')->with($scheduler);
+        $scheduler->add($task1);
+        $task2 = $this->getMock('\Async\Task\Task');
+        $task2->expects($this->once())->method('tick')->with($scheduler);
+        $scheduler->add($task2);
+        
+        $scheduler->tick();
+    }
+    
+    /**
+     * Test that calling stop stops a running scheduler
+     */
+    public function testStop() {
+        // Check that stop can be used to stop a running scheduler
+        $scheduler = new \Async\Scheduler();
+        
+        // Add a task that invokes stop on the scheduler
+        $scheduler->add(new ExitingTask());
+        
+        $scheduler->run();
+        
+        // If we get to here, stop has worked successfully
+        $this->assertTrue(true, "We should get here if stop is working");
+    }
+    
+    /**
+     * Test that a task added during a scheduler 'tick' is not executed until the
+     * next tick
+     */
+    public function testAddTaskMidTick() {
+        $scheduler = new \Async\Scheduler();
+        
+        // Create the task that will be added mid-tick
+        $task = $this->getMock('\Async\Task\Task');
+        // The task only expects to be called once
+        $task->expects($this->once())->method('tick')->with($scheduler);
+        
+        // Add a task that will add that task to the scheduler when invoked
+        $scheduler->add(new AddTaskTask($task));
+        
+        // Tick the scheduler twice
+        $scheduler->tick();
+        $scheduler->tick();
+    }
+}

test/Async/Test/SchedulerTest.php

-<?php
-
-namespace Async\Test;
-
-
-class CallableStub {
-    public function __invoke() {}
-}
-
-
-class SchedulerTest extends \PHPUnit_Framework_TestCase {
-    public function testNoActionsExitsImmediately() {
-        $scheduler = new \Async\Scheduler();
-        
-        $scheduler->run();
-        
-        $this->assertTrue(true, "We should get here as there are no actions");
-    }
-    
-    public function testActionCalledOnTick() {
-        $scheduler = new \Async\Scheduler();
-        
-        // Add an action that expects to be called exactly once
-        $action = $this->getMock('\Async\Test\CallableStub');
-        $action->expects($this->once())->method('__invoke');
-        $scheduler->add($action);
-        
-        $scheduler->tick();
-    }
-    
-    public function testNullReturnValueCausesReschedule() {
-        $scheduler = new \Async\Scheduler();
-        
-        // Add an action that expects to be called exactly twice
-        // and returns nothing
-        $action = $this->getMock('\Async\Test\CallableStub');
-        $action->expects($this->exactly(2))->method('__invoke');
-        $scheduler->add($action);
-        
-        $scheduler->tick();
-        $scheduler->tick();
-    }
-    
-    public function testReturnTrueReschedule() {
-        $scheduler = new \Async\Scheduler();
-        
-        // Add an action that expects to be called twice and will return true when called
-        $action = $this->getMock('\Async\Test\CallableStub');
-        $action->expects($this->exactly(2))->method('__invoke')
-               ->will($this->returnValue(true));
-        $scheduler->add($action);
-        
-        $scheduler->tick();
-        $scheduler->tick();
-    }
-    
-    public function testReturnFalseNoReschedule() {
-        $scheduler = new \Async\Scheduler();
-        
-        // Add an action that expects to be called only once and will return false when called
-        $action = $this->getMock('\Async\Test\CallableStub');
-        $action->expects($this->once())->method('__invoke')
-               ->will($this->returnValue(false));
-        $scheduler->add($action);
-        
-        $scheduler->tick();
-        $scheduler->tick();
-    }
-    
-    public function testPriorityCallingOrder() {
-        $scheduler = new \Async\Scheduler();
-        
-        $calls = array();
-        
-        $scheduler->add(function() use(&$calls) { $calls[] = 'first'; }, 1);
-        $scheduler->add(function() use(&$calls) { $calls[] = 'second'; }, 2);
-        
-        $scheduler->tick();
-        
-        // Check that the calls occurred in the correct order
-        $this->assertEquals(['second', 'first'], $calls);
-        
-        // Check that priorities are maintained on subsequent ticks
-        $calls = array();
-        $scheduler->tick();
-        $this->assertEquals(['second', 'first'], $calls);
-    }
-    
-    public function testStop() {
-        // Check that stop can be used to stop a running scheduler
-        $scheduler = new \Async\Scheduler();
-        
-        // Add an action that increments a variable and stops the scheduler after the third
-        // invocation
-        $count = 0;
-        $scheduler->add(function() use(&$count, $scheduler) {
-            $count++;
-            
-            if( $count >= 3 ) $scheduler->stop();
-        });
-        
-        $scheduler->run();
-        
-        // If we get to here, stop has worked successfully
-        $this->assertEquals(3, $count);
-    }
-    
-    public function testAddActionMidTick() {
-        // In this test we check that an action added during a scheduler 'tick'
-        // is not executed until the next tick
-        $scheduler = new \Async\Scheduler();
-        
-        // Create the action that will be added mid-tick
-        $action = $this->getMock('\Async\Test\CallableStub');
-        // The action only expects to be called once
-        $action->expects($this->once())->method('__invoke');
-        
-        // Add an action that will add another action to the scheduler when it
-        // is run
-        $scheduler->add(function() use($scheduler, $action) {
-            $scheduler->add($action);
-        });
-        
-        // Tick the scheduler twice
-        $scheduler->tick();
-        $scheduler->tick();
-    }
-}

test/Async/Test/Task/CallableStub.php

+<?php
+
+namespace Async\Test\Task;
+
+
+/**
+ * Stub callable object to allow mocking of callbacks
+ */
+class CallableStub {
+    public function __invoke() { }
+}

test/Async/Test/Task/CallableTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+class CallableTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that the task becomes complete after the callable object runs and that
+     * the return value becomes the task result
+     */
+    public function testTaskResultIsReturnValue() {
+        // Create a callable that expects to be called once and returns a known
+        // value
+        $callable = $this->getMock('\Async\Test\Task\CallableStub');
+        $callable->expects($this->once())->method('__invoke')
+                                         ->will($this->returnValue("returned"));
+        
+        $task = new \Async\Task\CallableTask($callable);
+        
+        // Check that the task is not complete yet
+        $this->assertFalse($task->isComplete());
+        
+        // Run the task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Check that the task is complete without error and the result is the
+        // expected value
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals("returned", $task->getResult());
+    }
+    
+    /**
+     * Test that the task becomes complete after the callable object runs and that
+     * the thrown exception becomes the task error
+     */
+    public function testTaskErrorIsThrownException() {
+        // Create a callable that expects to be called once and throws an exception
+        $callable = $this->getMock('\Async\Test\Task\CallableStub');
+        $callable->expects($this->once())->method('__invoke')
+                                         ->will($this->throwException(new \Exception("thrown")));
+        
+        $task = new \Async\Task\CallableTask($callable);
+        
+        // Check that the task is not complete yet
+        $this->assertFalse($task->isComplete());
+        
+        // Run the task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Check that the task is complete with the thrown error
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertEquals("thrown", $task->getException()->getMessage());
+    }
+    
+    /**
+     * Test that any given arguments are passed correctly
+     */
+    public function testArgumentsPassed() {
+        // Create a callable that expects to be called once with known arguments
+        $callable = $this->getMock('\Async\Test\Task\CallableStub');
+        $callable->expects($this->once())->method('__invoke')
+                                         ->with(1, 2, 3);
+        
+        // Create a task passing arguments
+        $task = new \Async\Task\CallableTask($callable, [1, 2, 3]);
+        
+        // Run the task
+        $task->tick($this->getMock('\Async\Scheduler'));
+    }
+    
+    /**
+     * Test that the callable is only executed once even if the task is ticked
+     * multiple times
+     */
+    public function testCallableExecutedOnlyOnce() {
+        // Create a callable that expects to be called once
+        $callable = $this->getMock('\Async\Test\Task\CallableStub');
+        $callable->expects($this->once())->method('__invoke');
+        
+        $task = new \Async\Task\CallableTask($callable);
+        
+        // Run the task twice
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+    }
+}

test/Async/Test/Task/GeneratorHelper.php

+<?php
+
+namespace Async\Test\Task;
+
+
+/**
+ * Class to move logic for manipulating and inspecting the state of a generator
+ * out of test cases
+ */
+class GeneratorHelper {
+    // The value returned from the last yielded task
+    protected $current = null;
+    // The task to yield next time the generator is resumed
+    protected $task = null;
+    // The exception to throw next time the generator is resumed
+    protected $exceptionToThrow = null;
+    // The exception that was caught last time the generator was resumed
+    protected $caughtException = null;
+    
+    public function generator() {
+        while( true ) {
+            // If there is an exception to throw, throw it
+            if( $this->exceptionToThrow ) throw $this->exceptionToThrow;
+            
+            // If there is no task to yield, exit the loop
+            if( $this->task === null ) break;
+            
+            // Capture the task we will yield in a variable and null the instance
+            // variable, so we don't automatically yield it again next
+            $task = $this->task;
+            $this->task = null;
+            
+            // Yield the task and receive the result back
+            // Capture the result/any caught exceptions as required
+            try {
+                $this->current = ( yield $task );
+                $this->caughtException = null;
+            }
+            catch( \Exception $exception ) {
+                $this->caughtException = $exception;
+                $this->current = null;
+            }
+        }
+    }
+    
+    public function current() {
+        return $this->current;
+    }
+    
+    public function caughtException() {
+        return $this->caughtException;
+    }
+    
+    public function yieldTask(\Async\Task\Task $task) {
+        $this->task = $task;
+    }
+    
+    public function throwException(\Exception $exception) {
+        $this->exceptionToThrow = $exception;
+    }
+}

test/Async/Test/Task/GeneratorTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+class GeneratorTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that a generator task with an empty generator is immediately complete
+     */
+    public function testTaskCompleteWithEmptyGenerator() {
+        // Create an empty generator (i.e. don't give it a task to yield)
+        $generator = new GeneratorHelper();
+        
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        
+        // The task should be instantly complete, as the generator has no task
+        // to yield
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+    }
+    
+    /**
+     * Test that tasks yielded by the generator are added to the scheduler
+     */
+    public function testYieldedTaskIsScheduled() {
+        $generator = new GeneratorHelper();
+        
+        // Create a stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Create a mock scheduler that expects the yielded task to be added
+        $scheduler = $this->getMock('\Async\Scheduler');
+        $scheduler->expects($this->once())->method('add')->with($stub);
+        
+        // Tick the task and check that the task gets scheduled
+        $task->tick($scheduler);
+    }
+    
+    /**
+     * Test that the result from a completed task is sent back to the generator
+     */
+    public function testTaskResultSent() {
+        $generator = new GeneratorHelper();
+        
+        // Create a stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the first yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Check that the task is not complete and that nothing has been sent
+        // back to the generator yet
+        $this->assertFalse($task->isComplete());
+        $this->assertNull($generator->current());
+        
+        // Complete the yielded task with a result
+        $stub->setResult("result");
+        
+        // Tick the task and verify that the generator received the result
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $this->assertEquals("result", $generator->current());
+    }
+    
+    /**
+     * Test that the exception from a failed task is thrown into the generator
+     */
+    public function testTaskErrorThrown() {
+        $generator = new GeneratorHelper();
+        
+        // Create a stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the first yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Check that the task is not complete and that nothing has been sent
+        // back to the generator yet
+        $this->assertFalse($task->isComplete());
+        $this->assertNull($generator->current());
+        $this->assertNull($generator->caughtException());
+        
+        // Complete the yielded task with a failure
+        $stub->setException(new \Exception('failure'));
+        
+        // Tick the task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // The generator should have received an exception
+        $this->assertNull($generator->current());
+        $this->assertEquals('failure', $generator->caughtException()->getMessage());
+    }
+    
+    /**
+     * Test that the result of a yielded task is correctly returned to the generator
+     * even when the task takes several ticks to complete
+     */
+    public function testDelayedTaskCompletion() {
+        $generator = new GeneratorHelper();
+        
+        // Create a stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the first yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Tick the task several times
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Verify that nothing has been sent back to the generator yet
+        $this->assertFalse($task->isComplete());
+        $this->assertNull($generator->current());
+        
+        // Complete the task with a result
+        $stub->setResult('result');
+        
+        // Tick the generator task again
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Verify that current has been set
+        $this->assertEquals('result', $generator->current());
+    }
+    
+    /**
+     * Test that the generator successfully yields and receives a return value from
+     * more than one task, and completes successfully when the last one completes
+     */
+    public function testMultipleYieldedTasks() {
+        $generator = new GeneratorHelper();
+        
+        // Create the first stub task to be yielded
+        $stub1 = new TaskStub();
+        $generator->yieldTask($stub1);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the first yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Complete the first yielded task with a result
+        $stub1->setResult("stub result 1");
+        
+        // Create the second stub task to be yielded
+        $stub2 = new TaskStub();
+        $generator->yieldTask($stub2);
+        
+        // Check that the task is not complete and that nothing has been sent
+        // back to the generator yet
+        $this->assertFalse($task->isComplete());
+        $this->assertNull($generator->current());
+        
+        // Tick the task and verify that the generator received the result of the
+        // first stub task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $this->assertEquals("stub result 1", $generator->current());
+        
+        // Complete the second yielded task with a result
+        $stub2->setResult("stub result 2");
+        
+        // Check that the task is not complete and that the second result has not
+        // yet been sent to the generator
+        $this->assertFalse($task->isComplete());
+        $this->assertEquals("stub result 1", $generator->current());
+        
+        // Tick the generator task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        // Verify that the generator received the result of the second task
+        $this->assertEquals("stub result 2", $generator->current());
+        // Veriry that the generator task completed with no error
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        // For completeness, test that the result is null
+        $this->assertNull($task->getResult());
+    }
+    
+    /**
+     * Tests that throwing an exception from the generator results in task failure
+     * with the given exception as the reason
+     */
+    public function testTaskFailureOnUncaughtException() {
+        $generator = new GeneratorHelper();
+        
+        // Create the stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Complete the first yielded task with a result
+        $stub->setResult("result");
+        
+        // Set the exception to throw when the generator is resumed
+        $generator->throwException(new \Exception('thrown'));
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Tick the generator task and verify that it has failed with the
+        // thrown exception as the reason
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertEquals('thrown', $task->getException()->getMessage());
+    }
+    
+    /**
+     * Test that ticking the generator task after the generator has finished
+     * has no effect
+     */
+    public function testTickAfterTaskCompletionIsNullOperation() {
+        $generator = new GeneratorHelper();
+        
+        // Create a stub task to be yielded
+        $stub = new TaskStub();
+        $generator->yieldTask($stub);
+        
+        // Create the generator task under test
+        $task = new \Async\Task\GeneratorTask($generator->generator());
+        // Tick the generator task to receive the first yielded task
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Check that the task is not complete and that nothing has been sent
+        // back to the generator yet
+        $this->assertFalse($task->isComplete());
+        
+        // Complete the yielded task with a result
+        $stub->setResult("result");
+        
+        // Tick the generator task and check that the task is complete and that
+        // the result was received by the generator
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $this->assertTrue($task->isComplete());
+        $this->assertEquals('result', $generator->current());
+        
+        // Tick the task a few more times and verify that the current value of
+        // the generator is still the same
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        $this->assertTrue($task->isComplete());
+        $this->assertEquals('result', $generator->current());
+        
+    }
+}

test/Async/Test/Task/PromiseTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+class PromiseTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that a promise task created with a pre-resolved promise is immediately
+     * marked as complete
+     */
+    public function testPreResolved() {
+        $promise = \React\Promise\When::resolve("resolved");
+        
+        $task = new \Async\Task\PromiseTask($promise);
+        
+        // The task should be complete with no error
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        
+        // The result of the task should be the promise result
+        $this->assertEquals("resolved", $task->getResult());
+    }
+    
+    /**
+     * Test that a promise task created with a pre-rejected promise is immediately
+     * marked as complete with an error
+     */
+    public function testPreRejected() {
+        $promise = \React\Promise\When::reject(new \Exception("rejected"));
+        
+        $task = new \Async\Task\PromiseTask($promise);
+        
+        // The task should be complete with an error
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        
+        // The message of the task's exception should be that given to the promise
+        $this->assertEquals("rejected", $task->getException()->getMessage());
+    }
+    
+    /**
+     * Test that a promise task created with a delayed promise becomes complete
+     * when the promise is resolved
+     */
+    public function testDelayedResolution() {
+        $promise = new \React\Promise\Deferred();
+        
+        $task = new \Async\Task\PromiseTask($promise->promise());
+        
+        // The task should not yet be complete
+        $this->assertFalse($task->isComplete());
+        
+        // Resolve the promise
+        $promise->resolve("resolved");
+        
+        // The task should be complete with no error
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        
+        // The result of the task should be the promise result
+        $this->assertEquals("resolved", $task->getResult());
+    }
+    
+    /**
+     * Test that a promise task created with a delayed promise becomes complete
+     * when the promise is rejected
+     */
+    public function testDelayedRejection() {
+        $promise = new \React\Promise\Deferred();
+        
+        $task = new \Async\Task\PromiseTask($promise->promise());
+        
+        // The task should not yet be complete
+        $this->assertFalse($task->isComplete());
+        
+        // Reject the promise
+        $promise->reject(new \Exception("rejected"));
+        
+        // The task should be complete with an error
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        
+        // The message of the task's exception should be that given to the promise
+        $this->assertEquals("rejected", $task->getException()->getMessage());
+    }
+    
+    /**
+     * Test that tick is a noop for a promise task
+     */
+    public function testTickNoop() {
+        $promise = new \React\Promise\Deferred();
+        
+        $task = new \Async\Task\PromiseTask($promise->promise());
+        
+        // The task should not yet be complete
+        $this->assertFalse($task->isComplete());
+        
+        // Tick a few times
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // The task should not have completed
+        $this->assertFalse($task->isComplete());
+        
+        // Resolve the promise
+        $promise->resolve("resolved");
+        
+        // The task should be complete with no error
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());        
+        // The result of the task should be the promise result
+        $this->assertEquals("resolved", $task->getResult());
+        
+        // Tick a few more times
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        $task->tick($this->getMock('\Async\Scheduler'));
+        
+        // Verify that the result is unchanged
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());        
+        // The result of the task should be the promise result
+        $this->assertEquals("resolved", $task->getResult());
+    }
+}

test/Async/Test/Task/RecurringTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+class RecurringTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that the callable is executed correctly when the task is ticked
+     */
+    public function testCallableExecutedOnTick() {
+        // Create a callable that expects to be called once
+        $callable = $this->getMock('\Async\Test\Task\CallableStub');
+        $callable->expects($this->exactly(2))->method('__invoke');
+        
+        $task = new \Async\Task\RecurringTask($callable);
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Tick the task - $callable should be called
+        $task->tick($this->getMock('\Async\Scheduler'));