Matt Pryor avatar Matt Pryor committed fdad96f

Added any/all/some tasks that wait for subtasks to complete/fail before producing combined results.

Comments (0)

Files changed (8)

src/Async/Task/AllTask.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Task that waits for all the given tasks to complete before itself completing
+ */
+class AllTask extends SomeTask {
+    /**
+     * Create a task that waits for all of the given tasks to complete before
+     * completing itself
+     * 
+     * The task result will be an array containing the results of the tasks, with
+     * keys preserved from the $tasks array
+     * 
+     * If one of the given tasks fails, the task will be faulted, and the exception
+     * will be a {@see \Async\Task\MultipleFailureException} containing the exception
+     * that the first failed task failed with. This is so that the failed task
+     * can be identified by looking at the set key in getFailures
+     * 
+     * @param \Async\Task\Task[] $tasks
+     */
+    public function __construct(array $tasks) {
+        // Hard-code $howMany to be the number of tasks in parent constructor
+        parent::__construct($tasks, count($tasks));
+    }
+}

src/Async/Task/AnyTask.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Task that waits for one of many tasks to complete before itself completing
+ */
+class AnyTask extends SomeTask {
+    /**
+     * Create a task that waits for any one of the given tasks to complete before
+     * completing itself
+     * 
+     * The task result is the result of the completed task
+     * 
+     * If all the tasks fail, the task will be faulted and the exception will be
+     * a {@see \Async\Task\MultipleFailureException} consisting of the combined
+     * reasons for the failures
+     * 
+     * @param \Async\Task\Task[] $tasks
+     */
+    public function __construct(array $tasks) {
+        // Hard-code $howMany to be 1 in parent constructor
+        parent::__construct($tasks, 1);
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        $result = parent::getResult();
+        
+        // The result from the parent is an array
+        // In this case, it should have length 0 or 1
+        // We use reset to get the first value rather than $result[0] since keys
+        // are preserved from the given task array
+        return count($result) >= 1 ? reset($result) : null;
+    }
+}

src/Async/Task/MultipleFailureException.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Exception representing multiple task failures
+ */
+class MultipleFailureException extends \Exception {
+    protected $exceptions = [];
+    
+    /**
+     * Create a new multiple failure exception from the given exceptions
+     * 
+     * @param array $exceptions
+     */
+    public function __construct(array $exceptions) {
+        $this->exceptions = $exceptions;
+        
+        parent::__construct(
+            count($exceptions) . " tasks failed - use getFailures to access individual failures"
+        );
+    }
+    
+    /**
+     * Get the array of exceptions that caused this exception
+     * 
+     * @return \Exception[]
+     */
+    public function getFailures() {
+        return $this->exceptions;
+    }
+}

src/Async/Task/SomeTask.php

+<?php
+
+namespace Async\Task;
+
+
+/**
+ * Task that takes a set of tasks and waits for a specified number of them to
+ * successfully complete before completing itself
+ */
+class SomeTask implements Task {
+    /**
+     * The tasks we are waiting for
+     *
+     * @var \Async\Task\Task[]
+     */
+    protected $tasks = [];
+    
+    /**
+     * The number of tasks we are waiting for
+     *
+     * @var type 
+     */
+    protected $howMany = 1;
+    
+    /**
+     * Indicates if the child tasks have been scheduled yet
+     *
+     * @var boolean
+     */
+    protected $scheduled = false;
+    
+    /**
+     * The results of the completed tasks
+     *
+     * @var array
+     */
+    protected $results = [];
+    
+    /**
+     * The exceptions of the failed tasks
+     *
+     * @var array
+     */
+    protected $exceptions = [];
+    
+    /**
+     * Create a new task that waits for $howMany of the given tasks to complete
+     * before completing
+     * 
+     * The task result will be an array containing the results of the first
+     * $howMany tasks to complete successfully, with keys preserved from the
+     * $tasks array
+     * 
+     * If it becomes impossible for $howMany tasks to complete, the task will
+     * be faulted, and the exception will be a {@see \Async\Task\MultipleFailureException}
+     * consisting of the combined reasons for the failures
+     * 
+     * @param array $tasks
+     */
+    public function __construct(array $tasks, $howMany) {
+        $this->tasks = $tasks;
+        $this->howMany = $howMany;
+    }
+    
+    /**
+     * {@inheritdoc}
+     */
+    public function isComplete() {
+        /*
+         * We are complete if:
+         * 
+         *   1. $howMany tasks have returned successfully
+         *   2. It is not possible for $howMany tasks to return successfully
+         *      (i.e. (count($tasks) - $howMany + 1) tasks have failed)
+         */
+        
+        return count($this->results) === $this->howMany ||
+               count($this->exceptions) === (count($this->tasks) - $this->howMany + 1);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isFaulted() {
+        // The task is faulted if enough tasks have failed
+        return count($this->exceptions) === (count($this->tasks) - $this->howMany + 1);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getResult() {
+        return $this->results;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getException() {
+        return $this->isFaulted() ? new MultipleFailureException($this->exceptions) : null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function tick(\Async\Scheduler $scheduler) {
+        // Check for completed tasks
+        // We do this first in case we have been given tasks that have also been
+        // scheduled through other means that are already complete
+        foreach( $this->tasks as $key => $task ) {
+            // If we are complete, we don't need to check any more tasks
+            if( $this->isComplete() ) return;
+            
+            if( $task->isComplete() ) {
+                // If the task is complete, add the result/exception to our arrays
+                // as appropriate
+                if( $task->isFaulted() ) {
+                    $this->exceptions[$key] = $task->getException();
+                }
+                else {
+                    $this->results[$key] = $task->getResult();
+                }
+            }
+        }
+        
+        // If we have not yet scheduled our child tasks, schedule any incomplete ones
+        if( !$this->scheduled ) {
+            foreach( $this->tasks as $task ) {
+                if( !$task->isComplete() ) {
+                    $scheduler->add($task);
+                }
+            }
+            
+            $this->scheduled = true;
+        }
+    }    
+}

src/Async/Util.php

         
         return new Task\CallableTask(function() use($object) { return $object; });
     }
+    
+    /**
+     * Shorthand for creating a SomeTask
+     * 
+     * @param \Async\Task\Task[] $tasks
+     * @param integer $howMany
+     * @return \Async\Task\SomeTask
+     * 
+     * @codeCoverageIgnore
+     */
+    public static function some(array $tasks, $howMany) {
+        return new Task\SomeTask($tasks, $howMany);
+    }
+    
+    /**
+     * Shorthand for creating an AllTask
+     * 
+     * @param \Async\Task\Task[] $tasks
+     * @return \Async\Task\AllTask
+     * 
+     * @codeCoverageIgnore
+     */
+    public static function all(array $tasks) {
+        return new Task\AllTask($tasks);
+    }
+    
+    /**
+     * Shorthand for creating an AnyTask
+     * 
+     * @param \Async\Task\Task[] $tasks
+     * @return \Async\Task\AnyTask
+     * 
+     * @codeCoverageIgnore
+     */
+    public static function any(array $tasks) {
+        return new Task\AnyTask($tasks);
+    }
 }

test/Async/Test/Task/AllTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+/**
+ * NOTE that AllTask is a subclass of SomeTask - only AllTask specific functionality
+ * is tested here (i.e. the requirement of all tasks completing successfully)
+ * 
+ * More stringent tests of SomeTask can be found in {@see \Async\Test\Task\SomeTaskTest}
+ */
+class AllTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Tests that an AllTask completes successfully only after all subtasks have
+     * completed
+     */
+    public function testCompletesWhenAllSubTasksComplete() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        $task = new \Async\Task\AllTask($tasks);
+        
+        // Complete each subtask in turn, checking that the all task does not
+        // complete too early
+        $i = 0;
+        foreach( $tasks as $subTask ) {
+            $i++;
+            $this->assertFalse($task->isComplete());
+            $subTask->setResult($i);
+            $task->tick($this->getMock(\Async\Scheduler::class));
+        }
+        
+        // Check that the all task is now complete with the correct result        
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([1, 2, 3, 4], $task->getResult());
+    }
+    
+    /**
+     * Tests that an AllTask fails when a single subtask fails
+     */
+    public function testFailsWhenOneSubTaskFails() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        $task = new \Async\Task\AllTask($tasks);
+        
+        // Complete a subtask, checking that the task is not yet complete
+        $this->assertFalse($task->isComplete());
+        
+        $tasks[0]->setResult(10);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertFalse($task->isComplete());
+        
+        // Fail a single task and check that the task is now failed
+        $tasks[1]->setException(new \Exception("failure"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is now complete with an error
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('1 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [1 => new \Exception("failure")],
+            $task->getException()->getFailures()
+        );
+    }
+}

test/Async/Test/Task/AnyTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+/**
+ * NOTE that AnyTask is a subclass of SomeTask - only AnyTask specific functionality
+ * is tested here (i.e. the requirement of only a single task to complete and the
+ * result being the result of the completed task rather than a one-element array)
+ * 
+ * More stringent tests of SomeTask can be found in {@see \Async\Test\Task\SomeTaskTest}
+ */
+class AnyTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Tests that an AnyTask completes successfully after one subtask completes
+     * successfully, and that its result is the result of the completed task
+     */
+    public function testCompletesWithCorrectResultWhenOneSubTaskCompletes() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        $task = new \Async\Task\AnyTask($tasks);
+        
+        $this->assertFalse($task->isComplete());
+        
+        // Fail a task and check that the task has not completed
+        $tasks[0]->setException(new \Exception("failure"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertFalse($task->isComplete());
+        
+        // Complete a task and check that the task completes successfully with
+        // the correct result
+        $tasks[1]->setResult(10);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals(10, $task->getResult());
+    }
+    
+    /**
+     * Tests that an AnyTask fails only when all the subtasks fail
+     */
+    public function testFailsWhenAllSubTasksFail() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        $task = new \Async\Task\AnyTask($tasks);
+        
+        // Fail each subtask in turn, checking that the task is not yet complete
+        $i = 0;
+        foreach( $tasks as $subTask ) {
+            $i++;
+            $this->assertFalse($task->isComplete());
+            $subTask->setException(new \Exception("failure $i"));
+            $task->tick($this->getMock(\Async\Scheduler::class));
+        }
+        
+        // Check that the task is now complete with an error
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('4 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                2 => new \Exception("failure 3"),
+                3 => new \Exception("failure 4")
+            ],
+            $task->getException()->getFailures()
+        );
+    }
+}

test/Async/Test/Task/SomeTaskTest.php

+<?php
+
+namespace Async\Test\Task;
+
+
+class SomeTaskTest extends \PHPUnit_Framework_TestCase {
+    /**
+     * Test that subtasks are only scheduled if they are not already complete
+     */
+    public function testOnlyIncompleteSubTasksScheduled() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Mark the last task as complete already
+        $tasks[3]->setResult(10);
+        
+        // Create a mock scheduler that expects to be called with the first 3 tasks
+        // but not the last
+        $scheduler = $this->getMock(\Async\Scheduler::class);
+        $scheduler->expects($this->exactly(3))->method('add');
+        $scheduler->expects($this->at(0))->method('add')->with($tasks[0]);
+        $scheduler->expects($this->at(1))->method('add')->with($tasks[1]);
+        $scheduler->expects($this->at(2))->method('add')->with($tasks[2]);
+        
+        // Tick the task
+        $task->tick($scheduler);
+    }
+    
+    /**
+     * Test that subtasks are only scheduled on the first tick
+     */
+    public function testSubTasksScheduledOnlyOnce() {
+        $tasks = [new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 1);
+        
+        // Create a mock scheduler that expects to be called with the first 3 tasks
+        // but not the last
+        $scheduler = $this->getMock(\Async\Scheduler::class);
+        $scheduler->expects($this->exactly(2))->method('add');
+        $scheduler->expects($this->at(0))->method('add')->with($tasks[0]);
+        $scheduler->expects($this->at(1))->method('add')->with($tasks[1]);
+        
+        // Tick the task twice - the scheduler should be called twice the first time
+        // and no times the second time
+        $task->tick($scheduler);
+        $task->tick($scheduler);
+    }
+    
+    /**
+     * Tests that a SomeTask completes successfully when enough subtasks complete
+     * successfully
+     */
+    public function testCompletesWhenEnoughSubTasksComplete() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Complete the first task and tick
+        $tasks[0]->setResult(10);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Complete another task and tick
+        $tasks[2]->setResult(12);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is now complete with the correct result
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([0 => 10, 2 => 12], $task->getResult());
+    }
+    
+    /**
+     * Tests that a SomeTask completes with a failure when it is not possible for
+     * enough subtasks to complete successfully
+     */
+    public function testFailsWhenEnoughSubTasksFail() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Fail the first task and tick
+        $tasks[0]->setException(new \Exception("failure 1"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Fail another task and tick
+        $tasks[1]->setException(new \Exception("failure 2"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete, as it is still possible for
+        // two tasks to complete successfully
+        $this->assertFalse($task->isComplete());
+        
+        // Fail a third task and tick
+        $tasks[3]->setException(new \Exception("failure 3"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task has now failed with the correct failure
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('3 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                3 => new \Exception("failure 3")
+            ],
+            $task->getException()->getFailures()
+        );
+    }
+    
+    /**
+     * Test that a SomeTask completes successfully if enough subtasks complete
+     * successfully, even if some subtasks fail
+     */
+    public function testCompletesWithAcceptableSubTaskFailures() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Complete the first task and tick
+        $tasks[0]->setResult(10);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Fail a task and tick
+        $tasks[1]->setException(new \Exception("failure"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Complete a second task and tick
+        $tasks[2]->setResult(12);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is now complete with the correct result
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([0 => 10, 2 => 12], $task->getResult());
+    }
+    
+    /**
+     * Test that a SomeTask fails when it is no longer possible for $howMany
+     * tasks to complete successully, even if there are already successful subtasks
+     */
+    public function testFailsEvenWithSuccessfulSubTasks() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Check that the task is not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Fail the first task and tick
+        $tasks[0]->setException(new \Exception("failure 1"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());
+        
+        // Complete a task successfully and tick
+        $tasks[2]->setResult(10);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete
+        $this->assertFalse($task->isComplete());        
+        
+        // Fail a second task and tick
+        $tasks[1]->setException(new \Exception("failure 2"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task is still not complete, as it is still possible for
+        // two tasks to complete successfully
+        $this->assertFalse($task->isComplete());
+        
+        // Fail a third task and tick
+        $tasks[3]->setException(new \Exception("failure 3"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        
+        // Check that the task has now failed with the correct failure
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('3 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                3 => new \Exception("failure 3")
+            ],
+            $task->getException()->getFailures()
+        );
+    }
+    
+    /**
+     * Check that completing/failing additional subtasks once the task is complete
+     * doesn't affect the result
+     */
+    public function testCompletingAdditionalSubTasksDoesntAffectResult() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 2 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 2);
+        
+        // Complete the first two tasks and check the task is complete with the
+        // expected result
+        $tasks[0]->setResult(10);
+        $tasks[1]->setResult(11);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([0 => 10, 1 => 11], $task->getResult());
+        
+        // Complete the third task and check that the result is unaffected
+        $tasks[2]->setResult(12);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([0 => 10, 1 => 11], $task->getResult());
+        
+        // Fail the fourth task and check the result is unaffected
+        $tasks[3]->setException(new \Exception("failure"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertFalse($task->isFaulted());
+        $this->assertEquals([0 => 10, 1 => 11], $task->getResult());
+    }
+    
+    /**
+     * Tests that once a SomeTask has failed, the failure cannot be affected by
+     * completing/failing more subtasks
+     */
+    public function testCompletingAdditionalSubTasksDoesntAffectFailure() {
+        $tasks = [new TaskStub(), new TaskStub(), new TaskStub(),
+                  new TaskStub(), new TaskStub(), new TaskStub()];
+        
+        // Ask for 3 successful completions before we get a result
+        $task = new \Async\Task\SomeTask($tasks, 3);
+        
+        // Fail the first four tasks and check the task is failed with the
+        // expected exception
+        $tasks[0]->setException(new \Exception("failure 1"));
+        $tasks[1]->setException(new \Exception("failure 2"));
+        $tasks[2]->setException(new \Exception("failure 3"));
+        $tasks[3]->setException(new \Exception("failure 4"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('4 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                2 => new \Exception("failure 3"),
+                3 => new \Exception("failure 4")
+            ],
+            $task->getException()->getFailures()
+        );
+        
+        // Complete the fifth task and check that the failure is unaffected
+        $tasks[4]->setResult(14);
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('4 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                2 => new \Exception("failure 3"),
+                3 => new \Exception("failure 4")
+            ],
+            $task->getException()->getFailures()
+        );
+        
+        // Fail the sixth task and check the failure is unaffected
+        $tasks[5]->setException(new \Exception("failure 6"));
+        $task->tick($this->getMock(\Async\Scheduler::class));
+        $this->assertTrue($task->isComplete());
+        $this->assertTrue($task->isFaulted());
+        $this->assertInstanceOf(
+            \Async\Task\MultipleFailureException::class, $task->getException()
+        );
+        $this->assertContains('4 tasks failed', $task->getException()->getMessage());
+        $this->assertEquals(
+            [
+                0 => new \Exception("failure 1"),
+                1 => new \Exception("failure 2"),
+                2 => new \Exception("failure 3"),
+                3 => new \Exception("failure 4")
+            ],
+            $task->getException()->getFailures()
+        );
+    }
+}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.