diff --git a/composer.json b/composer.json index eb970cc..d84e31b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "async-interop/event-loop-implementation": "dev-master" }, "require-dev": { - "amphp/loop": "dev-master" + "amphp/loop": "dev-master", + "phpunit/phpunit": "^4|^5" }, "minimum-stability": "dev", "autoload": { diff --git a/lib/Deferred.php b/lib/Deferred.php index 5b9f61b..1ce59ba 100644 --- a/lib/Deferred.php +++ b/lib/Deferred.php @@ -4,6 +4,7 @@ namespace Amp; use Interop\Async\Awaitable; +// @codeCoverageIgnoreStart try { if (@assert(false)) { production: // PHP 7 production environment (zend.assertions=0) @@ -75,4 +76,4 @@ try { } } catch (\AssertionError $exception) { goto development; // zend.assertions=1 and assert.exception=1, use development definition. -} +} // @codeCoverageIgnoreEnd diff --git a/lib/functions.php b/lib/functions.php index d16983d..c0d67b2 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -144,13 +144,23 @@ function capture(Awaitable $awaitable, $className, callable $functor) { */ function timeout(Awaitable $awaitable, $timeout) { $deferred = new Deferred; + $resolved = false; - $watcher = Loop::delay($timeout, function () use ($deferred) { - $deferred->fail(new TimeoutException); + $watcher = Loop::delay($timeout, function () use (&$resolved, $deferred) { + if (!$resolved) { + $resolved = true; + $deferred->fail(new TimeoutException); + } }); - $awaitable->when(function () use ($awaitable, $deferred, $watcher) { + $awaitable->when(function () use (&$resolved, $awaitable, $deferred, $watcher) { Loop::cancel($watcher); + + if ($resolved) { + return; + } + + $resolved = true; $deferred->resolve($awaitable); }); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..dfc05dd --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + test + + test/ExtendedCoroutineTest.php + test/ExtendedCoroutineTest.php + + + + + lib + + + + + + \ No newline at end of file diff --git a/test/AdaptTest.php b/test/AdaptTest.php new file mode 100644 index 0000000..9a7255d --- /dev/null +++ b/test/AdaptTest.php @@ -0,0 +1,105 @@ +awaitable = $awaitable; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) { + $this->awaitable->when(function ($exception, $value) use ($onFulfilled, $onRejected) { + if ($exception) { + if ($onRejected) { + $onRejected($exception); + } + return; + } + + if ($onFulfilled) { + $onFulfilled($value); + } + }); + } +} + +class AdaptTest extends \PHPUnit_Framework_TestCase { + public function testThenCalled() { + $mock = $this->getMockBuilder(PromiseMock::class) + ->disableOriginalConstructor() + ->getMock(); + + $mock->expects($this->once()) + ->method("then") + ->with( + $this->callback(function ($resolve) { + return is_callable($resolve); + }), + $this->callback(function ($reject) { + return is_callable($reject); + }) + ); + + $awaitable = Amp\adapt($mock); + + $this->assertInstanceOf(Awaitable::class, $awaitable); + } + + /** + * @depends testThenCalled + */ + public function testAwaitableFulfilled() { + $value = 1; + + $promise = new PromiseMock(new Success($value)); + + $awaitable = Amp\adapt($promise); + + $awaitable->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertSame($value, $result); + } + + /** + * @depends testThenCalled + */ + public function testAwaitableRejected() { + $exception = new \Exception; + + $promise = new PromiseMock(new Failure($exception)); + + $awaitable = Amp\adapt($promise); + + $awaitable->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testScalarValue() { + Amp\adapt(1); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNonThenableObject() { + Amp\adapt(new \stdClass); + } +} diff --git a/test/AllTest.php b/test/AllTest.php new file mode 100644 index 0000000..8d13a06 --- /dev/null +++ b/test/AllTest.php @@ -0,0 +1,77 @@ +when($callback); + + $this->assertSame([], $result); + } + + public function testSuccessfulAwaitablesArray() { + $awaitables = [new Success(1), new Success(2), new Success(3)]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\all($awaitables)->when($callback); + + $this->assertSame([1, 2, 3], $result); + } + + public function testPendingAwatiablesArray() { + Loop::execute(function () use (&$result) { + $awaitables = [ + new Pause(0.2, 1), + new Pause(0.3, 2), + new Pause(0.1, 3), + ]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\all($awaitables)->when($callback); + }); + + $this->assertEquals([1, 2, 3], $result); + } + + public function testArrayKeysPreserved() { + $expected = ['one' => 1, 'two' => 2, 'three' => 3]; + + Loop::execute(function () use (&$result) { + $awaitables = [ + 'one' => new Pause(0.2, 1), + 'two' => new Pause(0.3, 2), + 'three' => new Pause(0.1, 3), + ]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\all($awaitables)->when($callback); + }); + + $this->assertEquals($expected, $result); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNonAwaitable() { + Amp\all([1]); + } +} diff --git a/test/CaptureTest.php b/test/CaptureTest.php new file mode 100644 index 0000000..28b11d8 --- /dev/null +++ b/test/CaptureTest.php @@ -0,0 +1,113 @@ +assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $awaitable->when($callback); + + $this->assertFalse($invoked); + $this->assertSame($value, $result); + } + + public function testFailedAwaitable() { + $invoked = false; + $callback = function ($exception) use (&$invoked, &$reason) { + $invoked = true; + $reason = $exception; + return -1; + }; + + $exception = new \Exception; + + $awaitable = new Failure($exception); + + $awaitable = Amp\capture($awaitable, \Exception::class, $callback); + $this->assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $awaitable->when($callback); + + $this->assertTrue($invoked); + $this->assertSame($exception, $reason); + $this->assertSame(-1, $result); + } + + /** + * @depends testFailedAwaitable + */ + public function testCallbackThrowing() { + $invoked = false; + $callback = function ($exception) use (&$invoked) { + $invoked = true; + throw new \Exception; + }; + + $exception = new \Exception; + + $awaitable = new Failure($exception); + + $awaitable = Amp\capture($awaitable, \Exception::class, $callback); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + + $this->assertTrue($invoked); + $this->assertNotSame($exception, $reason); + } + + /** + * @depends testFailedAwaitable + */ + public function testUnmatchedExceptionClass() { + $invoked = false; + $callback = function ($exception) use (&$invoked, &$reason) { + $invoked = true; + $reason = $exception; + return -1; + }; + + $exception = new \LogicException; + + $awaitable = new Failure($exception); + + $awaitable = Amp\capture($awaitable, \RuntimeException::class, $callback); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + + $this->assertFalse($invoked); + $this->assertSame($exception, $reason); + } +} diff --git a/test/CoroutineTest.php b/test/CoroutineTest.php new file mode 100644 index 0000000..350c798 --- /dev/null +++ b/test/CoroutineTest.php @@ -0,0 +1,539 @@ +assertSame($value, $yielded); + } + + public function testYieldFailedAwaitable() { + $exception = new \Exception; + + $generator = function () use (&$yielded, $exception) { + $yielded = (yield new Failure($exception)); + }; + + $coroutine = new Coroutine($generator()); + + $this->assertNull($yielded); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + } + + /** + * @depends testYieldSuccessfulAwaitable + */ + public function testYieldPendingAwaitable() { + $value = 1; + + Loop::execute(function () use (&$yielded, $value) { + $generator = function () use (&$yielded, $value) { + $yielded = (yield new Pause(self::TIMEOUT, $value)); + }; + + $coroutine = new Coroutine($generator()); + }); + + $this->assertSame($value, $yielded); + } + + /** + * @depends testYieldFailedAwaitable + */ + public function testCatchingFailedAwaitableException() { + $exception = new \Exception; + + $fail = false; + $generator = function () use (&$fail, &$result, $exception) { + try { + yield new Failure($exception); + } catch (\Exception $exception) { + $result = $exception; + return; + } + + $fail = true; + }; + + $coroutine = new Coroutine($generator()); + + if ($fail) { + $this->fail("Failed awaitable reason not thrown into generator"); + } + + } + + /** + * @todo Remove once PHP 7 is required. + */ + public function testSucceedsWithCoroutineResult() { + $value = 1; + + $generator = function () use ($value) { + yield Coroutine::result($value); + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertSame($value, $result); + } + + /** + * @depends testSucceedsWithCoroutineResult + * @todo Remove once PHP 7 is required. + */ + public function testSucceedsWithCoroutineAfterSuccessfulAwaitable() { + $value = 1; + + $generator = function () use ($value) { + yield Coroutine::result(yield new Success($value)); + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertSame($value, $result); + } + + /** + * @depends testSucceedsWithCoroutineResult + * @todo Remove once PHP 7 is required. + */ + public function testSucceedsWithCoroutineAfterFailedAwaitable() { + $value = 1; + + $generator = function () use ($value) { + try { + yield new Failure(new \Exception); + } catch (\Exception $exception) { + yield Coroutine::result($value); + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertSame($value, $result); + } + + public function testInvalidYield() { + $generator = function () { + yield 1; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + } + + /** + * @depends testInvalidYield + */ + public function testInvalidYieldAfterYieldAwaitable() { + $generator = function () { + yield new Success; + yield 1; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + } + + /** + * @depends testInvalidYield + */ + public function testCatchesExceptionAfterInvalidYield() { + $generator = function () { + try { + yield 1; + } catch (\Exception $exception) { + yield Coroutine::result(1); + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + } + + /** + * @depends testSucceedsWithCoroutineResult + * @todo Remove once PHP 7 is required. + */ + public function testYieldsAfterCoroutineResult() { + $generator = function () { + yield Coroutine::result(1); + yield new Success; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + } + + /** + * @depends testYieldsAfterCoroutineResult + * @todo Remove once PHP 7 is required. + */ + public function testCatchesExceptionAfterInvalidResult() { + $generator = function () { + yield Coroutine::result(1); + + try { + yield new Success; + } catch (\Exception $exception) { + yield Coroutine::result(2); + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + } + + /** + * @depends testInvalidYield + */ + public function testThrowAfterInvalidYield() { + $exception = new \Exception; + + $generator = function () use ($exception) { + try { + yield 1; + } catch (\Exception $reason) { + throw $exception; + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception) use (&$reason) { + $reason = $exception; + }); + + $this->assertInstanceOf(InvalidYieldException::class, $reason); + $this->assertSame($exception, $reason->getPrevious()); + } + + /** + * @depends testYieldFailedAwaitable + */ + public function testCatchingFailedAwaitableExceptionWithNoFurtherYields() { + $exception = new \Exception; + + $generator = function () use ($exception) { + try { + yield new Failure($exception); + } catch (\Exception $exception) { + // No further yields in generator. + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertNull($result); + } + + public function testGeneratorThrowingExceptionFailsCoroutine() { + $exception = new \Exception; + + $generator = function () use ($exception) { + throw $exception; + yield; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + } + + /** + * @depends testGeneratorThrowingExceptionFailsCoroutine + */ + public function testGeneratorThrowingExceptionWithFinallyFailsCoroutine() { + $exception = new \Exception; + + $invoked = false; + $generator = function () use (&$invoked, $exception) { + try { + throw $exception; + yield; + } finally { + $invoked = true; + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + $this->assertTrue($invoked); + } + + /** + * @depends testYieldFailedAwaitable + * @depends testGeneratorThrowingExceptionWithFinallyFailsCoroutine + */ + public function testGeneratorYieldingFailedAwaitableWithFinallyFailsCoroutine() { + $exception = new \Exception; + + $invoked = false; + $generator = function () use (&$invoked, $exception) { + try { + yield new Failure($exception); + } finally { + $invoked = true; + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + $this->assertTrue($invoked); + } + + /** + * @depends testGeneratorThrowingExceptionFailsCoroutine + */ + public function testGeneratorThrowingExceptionAfterPendingAwaitableWithFinallyFailsCoroutine() { + $exception = new \Exception; + $value = 1; + + Loop::execute(function () use (&$yielded, &$invoked, &$reason, $exception, $value) { + $invoked = false; + $generator = function () use (&$yielded, &$invoked, $exception, $value) { + try { + $yielded = (yield new Pause(self::TIMEOUT, $value)); + throw $exception; + } finally { + $invoked = true; + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + }); + + $this->assertSame($exception, $reason); + $this->assertTrue($invoked); + $this->assertSame($value, $yielded); + } + + /** + * Note that yielding in a finally block is not recommended. + * + * @depends testYieldPendingAwaitable + * @depends testGeneratorThrowingExceptionWithFinallyFailsCoroutine + */ + public function testGeneratorThrowingExceptionWithFinallyYieldingPendingAwaitable() { + $exception = new \Exception; + $value = 1; + + Loop::execute(function () use (&$yielded, &$reason, $exception, $value) { + $generator = function () use (&$yielded, $exception, $value) { + try { + throw $exception; + } finally { + $yielded = (yield new Pause(self::TIMEOUT, $value)); + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + }); + + $this->assertSame($value, $yielded); + $this->assertSame($exception, $reason); + } + + /** + * @depends testYieldPendingAwaitable + * @depends testGeneratorThrowingExceptionWithFinallyFailsCoroutine + */ + public function testGeneratorThrowingExceptionWithFinallyBlockThrowing() { + $exception = new \Exception; + + $generator = function () use ($exception) { + try { + throw new \Exception; + } finally { + throw $exception; + } + + yield; // Unreachable, but makes function a generator. + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason) { + $reason = $exception; + }); + + $this->assertSame($exception, $reason); + } + + /** + * @depends testYieldSuccessfulAwaitable + */ + public function testYieldConsecutiveSucceeded() { + $invoked = false; + Loop::execute(function () use (&$invoked) { + $count = 1000; + $awaitable = new Success; + + $generator = function () use ($count, $awaitable) { + for ($i = 0; $i < $count; ++$i) { + yield $awaitable; + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$invoked) { + $invoked = true; + }); + }); + + $this->assertTrue($invoked); + } + + /** + * @depends testYieldFailedAwaitable + */ + public function testYieldConsecutiveFailed() { + $invoked = false; + Loop::execute(function () use (&$invoked) { + $count = 1000; + $awaitable = new Failure(new \Exception); + + $generator = function () use ($count, $awaitable) { + for ($i = 0; $i < $count; ++$i) { + try { + yield $awaitable; + } catch (\Exception $exception) { + // Ignore and continue. + } + } + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$invoked) { + $invoked = true; + }); + }); + } + + /** + * @depends testYieldSuccessfulAwaitable + */ + public function testFastInvalidGenerator() { + $generator = function () { + if (false) { + yield new Success; + } + }; + + $coroutine = new Coroutine($generator()); + + $invoked = false; + $coroutine->when(function ($exception, $value) use (&$invoked) { + $invoked = true; + }); + + $this->assertTrue($invoked); + } + + public function testCoroutineFunction() { + $callable = Amp\coroutine(function () { + yield; + }); + + $this->assertInstanceOf(Coroutine::class, $callable()); + } + + /** + * @depends testCoroutineFunction + * @expectedException \LogicException + */ + public function testCoroutineFunctionWithNonGeneratorCallback() { + $callable = Amp\coroutine(function () {}); + + $this->assertInstanceOf(Coroutine::class, $callable()); + } +} diff --git a/test/DeferredTest.php b/test/DeferredTest.php new file mode 100644 index 0000000..647f6a8 --- /dev/null +++ b/test/DeferredTest.php @@ -0,0 +1,60 @@ +deferred = new Deferred; + } + + public function testGetAwaitable() { + $awaitable = $this->deferred->getAwaitable(); + $this->assertInstanceOf(Awaitable::class, $awaitable); + } + + /** + * @depends testGetAwaitable + */ + public function testResolve() { + $value = "Resolution value"; + $awaitable = $this->deferred->getAwaitable(); + + $invoked = false; + $awaitable->when(function ($exception, $value) use (&$invoked, &$result) { + $invoked = true; + $result = $value; + }); + + $this->deferred->resolve($value); + + $this->assertTrue($invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testGetAwaitable + */ + public function testFail() { + $exception = new \Exception; + $awaitable = $this->deferred->getAwaitable(); + + $invoked = false; + $awaitable->when(function ($exception, $value) use (&$invoked, &$result) { + $invoked = true; + $result = $exception; + }); + + $this->deferred->fail($exception); + + $this->assertTrue($invoked); + $this->assertSame($exception, $result); + } +} diff --git a/test/ExtendedCoroutineTest.php b/test/ExtendedCoroutineTest.php new file mode 100644 index 0000000..39ce7e4 --- /dev/null +++ b/test/ExtendedCoroutineTest.php @@ -0,0 +1,80 @@ +when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + + $this->assertSame($value, $result); + } + + /** + * @depends testCoroutineResolvedWithReturn + */ + public function testYieldFromGenerator() { + $value = 1; + + $generator = function () use ($value) { + $generator = function () use ($value) { + return yield new Success($value); + }; + + return yield from $generator(); + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + + $this->assertSame($value, $result); + } + + /** + * @depends testCoroutineResolvedWithReturn + */ + public function testFastReturningGenerator() + { + $value = 1; + + $generator = function () use ($value) { + if (true) { + return $value; + } + + yield; + + return -$value; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$result) { + $result = $value; + }); + + $this->assertSame($value, $result); + } + +} \ No newline at end of file diff --git a/test/FailureTest.php b/test/FailureTest.php new file mode 100644 index 0000000..61b9708 --- /dev/null +++ b/test/FailureTest.php @@ -0,0 +1,57 @@ +when($callback); + + $this->assertSame(1, $invoked); + $this->assertSame($exception, $reason); + } + + /** + * @depends testWhen + */ + public function testWhenThrowingForwardsToLoopHandlerOnSuccess() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $success = new Failure(new \Exception); + + $success->when($callback); + }); + + $this->assertSame(1, $invoked); + } +} diff --git a/test/FutureTest.php b/test/FutureTest.php new file mode 100644 index 0000000..c8eb7e4 --- /dev/null +++ b/test/FutureTest.php @@ -0,0 +1,312 @@ +future = new Future; + } + + public function testWhenOnSuccess() { + $value = "Resolution value"; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $value; + }; + + $this->future->when($callback); + + $this->future->resolve($value); + + $this->assertSame(1, $invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testWhenOnSuccess + */ + public function testMultipleWhensOnSuccess() { + $value = "Resolution value"; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $value; + }; + + $this->future->when($callback); + $this->future->when($callback); + $this->future->when($callback); + + $this->future->resolve($value); + + $this->assertSame(3, $invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testWhenOnSuccess + */ + public function testWhenAfterSuccess() { + $value = "Resolution value"; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $value; + }; + + $this->future->resolve($value); + + $this->future->when($callback); + + $this->assertSame(1, $invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testWhenAfterSuccess + */ + public function testMultipleWhenAfterSuccess() { + $value = "Resolution value"; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $value; + }; + + $this->future->resolve($value); + + $this->future->when($callback); + $this->future->when($callback); + $this->future->when($callback); + + $this->assertSame(3, $invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testWhenOnSuccess + */ + public function testWhenThrowingForwardsToLoopHandlerOnSuccess() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $this->future->when($callback); + + $this->future->resolve($expected); + }); + + $this->assertSame(1, $invoked); + } + + /** + * @depends testWhenAfterSuccess + */ + public function testWhenThrowingForwardsToLoopHandlerAfterSuccess() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $this->future->resolve($expected); + + $this->future->when($callback); + }); + + $this->assertSame(1, $invoked); + } + + public function testWhenOnFail() { + $exception = new \Exception; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $exception; + }; + + $this->future->when($callback); + + $this->future->fail($exception); + + $this->assertSame(1, $invoked); + $this->assertSame($exception, $result); + } + + /** + * @depends testWhenOnFail + */ + public function testMultipleWhensOnFail() { + $exception = new \Exception; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $exception; + }; + + $this->future->when($callback); + $this->future->when($callback); + $this->future->when($callback); + + $this->future->fail($exception); + + $this->assertSame(3, $invoked); + $this->assertSame($exception, $result); + } + + /** + * @depends testWhenOnFail + */ + public function testWhenAfterFail() { + $exception = new \Exception; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $exception; + }; + + $this->future->fail($exception); + + $this->future->when($callback); + + $this->assertSame(1, $invoked); + $this->assertSame($exception, $result); + } + + /** + * @depends testWhenAfterFail + */ + public function testMultipleWhensAfterFail() { + $exception = new \Exception; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $exception; + }; + + $this->future->fail($exception); + + $this->future->when($callback); + $this->future->when($callback); + $this->future->when($callback); + + $this->assertSame(3, $invoked); + $this->assertSame($exception, $result); + } + + /** + * @depends testWhenOnSuccess + */ + public function testWhenThrowingForwardsToLoopHandlerOnFail() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $this->future->when($callback); + + $this->future->fail(new \Exception); + }); + + $this->assertSame(1, $invoked); + } + + /** + * @depends testWhenOnSuccess + */ + public function testWhenThrowingForwardsToLoopHandlerAfterFail() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $this->future->fail(new \Exception); + + $this->future->when($callback); + }); + + $this->assertSame(1, $invoked); + } + + public function testResolveWithAwaitableBeforeWhen() { + $awaitable = $this->getMockBuilder(Awaitable::class)->getMock(); + + $awaitable->expects($this->once()) + ->method("when") + ->with($this->callback("is_callable")); + + $this->future->resolve($awaitable); + + $this->future->when(function () {}); + } + + public function testResolveWithAwaitableAfterWhen() { + $awaitable = $this->getMockBuilder(Awaitable::class)->getMock(); + + $awaitable->expects($this->once()) + ->method("when") + ->with($this->callback("is_callable")); + + $this->future->when(function () {}); + + $this->future->resolve($awaitable); + } + + /** + * @expectedException \LogicException + */ + public function testDoubleResolve() { + $this->future->resolve(); + $this->future->resolve(); + } +} diff --git a/test/PipeTest.php b/test/PipeTest.php new file mode 100644 index 0000000..bfff9a5 --- /dev/null +++ b/test/PipeTest.php @@ -0,0 +1,84 @@ +assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $awaitable->when($callback); + + $this->assertTrue($invoked); + $this->assertSame($value + 1, $result); + } + + public function testFailedAwaitable() { + $invoked = false; + $callback = function ($value) use (&$invoked) { + $invoked = true; + return $value + 1; + }; + + $exception = new \Exception; + + $awaitable = new Failure($exception); + + $awaitable = Amp\pipe($awaitable, $callback); + $this->assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + + $this->assertFalse($invoked); + $this->assertSame($exception, $reason); + } + + /** + * @depends testSuccessfulAwaitable + */ + public function testCallbackThrowing() { + $exception = new \Exception; + $callback = function ($value) use (&$invoked, $exception) { + $invoked = true; + throw $exception; + }; + + $value = 1; + + $awaitable = new Success($value); + + $awaitable = Amp\pipe($awaitable, $callback); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + + $this->assertTrue($invoked); + $this->assertSame($exception, $reason); + } +} diff --git a/test/RethrowTest.php b/test/RethrowTest.php new file mode 100644 index 0000000..5df7fc5 --- /dev/null +++ b/test/RethrowTest.php @@ -0,0 +1,26 @@ +assertSame($exception, $reason); + return; + } + + $this->fail('Failed awaitable reason should be thrown from loop'); + } +} diff --git a/test/SomeTest.php b/test/SomeTest.php new file mode 100644 index 0000000..9a58236 --- /dev/null +++ b/test/SomeTest.php @@ -0,0 +1,106 @@ +when($callback); + + $this->assertSame([1, 2], $result); + } + + public function testSuccessfulAndFailedAwaitablesArray() { + $awaitables = [new Failure(new \Exception), new Failure(new \Exception), new Success(3)]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\some($awaitables, 1)->when($callback); + + $this->assertSame([2 => 3], $result); + } + + public function testTooManyFailedAwaitables() { + $awaitables = [new Failure(new \Exception), new Failure(new \Exception), new Success(3)]; + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + Amp\some($awaitables, 2)->when($callback); + + $this->assertInstanceOf(MultiReasonException::class, $reason); + + $reasons = $reason->getReasons(); + + foreach ($reasons as $reason) { + $this->assertInstanceOf(\Exception::class, $reason); + } + } + + public function testPendingAwatiablesArray() { + Loop::execute(function () use (&$result) { + $awaitables = [ + new Pause(0.2, 1), + new Pause(0.3, 2), + new Pause(0.1, 3), + ]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\some($awaitables, 2)->when($callback); + }); + + $this->assertEquals([0 => 1, 2 => 3], $result); + } + + public function testArrayKeysPreserved() { + $expected = ['one' => 1, 'two' => 2, 'three' => 3]; + + Loop::execute(function () use (&$result) { + $awaitables = [ + 'one' => new Pause(0.2, 1), + 'two' => new Pause(0.3, 2), + 'three' => new Pause(0.1, 3), + ]; + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + Amp\some($awaitables, 3)->when($callback); + }); + + $this->assertEquals($expected, $result); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNonAwaitable() { + Amp\some([1], 1); + } +} diff --git a/test/SuccessTest.php b/test/SuccessTest.php new file mode 100644 index 0000000..5af7802 --- /dev/null +++ b/test/SuccessTest.php @@ -0,0 +1,58 @@ +getMockBuilder(Awaitable::class)->getMock()); + } + + public function testWhen() { + $value = "Resolution value"; + + $invoked = 0; + $callback = function ($exception, $value) use (&$invoked, &$result) { + ++$invoked; + $result = $value; + }; + + $success = new Success($value); + + $success->when($callback); + + $this->assertSame(1, $invoked); + $this->assertSame($value, $result); + } + + /** + * @depends testWhen + */ + public function testWhenThrowingForwardsToLoopHandlerOnSuccess() { + Loop::execute(function () use (&$invoked) { + $invoked = 0; + $expected = new \Exception; + + Loop::setErrorHandler(function ($exception) use (&$invoked, $expected) { + ++$invoked; + $this->assertSame($expected, $exception); + }); + + $callback = function () use ($expected) { + throw $expected; + }; + + $success = new Success; + + $success->when($callback); + }); + + $this->assertSame(1, $invoked); + } +} diff --git a/test/TimeoutTest.php b/test/TimeoutTest.php new file mode 100644 index 0000000..f78438c --- /dev/null +++ b/test/TimeoutTest.php @@ -0,0 +1,92 @@ +assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $awaitable->when($callback); + + $this->assertSame($value, $result); + }); + } + + public function testFailedAwaitable() { + Loop::execute(function () { + $exception = new \Exception; + + $awaitable = new Failure($exception); + + $awaitable = Amp\timeout($awaitable, 100); + $this->assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + + $this->assertSame($exception, $reason); + }); + } + + /** + * @depends testSuccessfulAwaitable + */ + public function testFastPending() { + $value = 1; + + Loop::execute(function () use (&$result, $value) { + $awaitable = new Pause(50, $value); + + $awaitable = Amp\timeout($awaitable, 100); + $this->assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $awaitable->when($callback); + }); + + $this->assertSame($value, $result); + } + + /** + * @depends testSuccessfulAwaitable + */ + public function testSlowPending() { + Loop::execute(function () use (&$reason) { + $awaitable = new Pause(200); + + $awaitable = Amp\timeout($awaitable, 100); + $this->assertInstanceOf(Awaitable::class, $awaitable); + + $callback = function ($exception, $value) use (&$reason) { + $reason = $exception; + }; + + $awaitable->when($callback); + }); + + $this->assertInstanceOf(Amp\TimeoutException::class, $reason); + } +} diff --git a/test/WaitTest.php b/test/WaitTest.php new file mode 100644 index 0000000..d8bd765 --- /dev/null +++ b/test/WaitTest.php @@ -0,0 +1,65 @@ +assertSame($value, $result); + } + + public function testWaitOnFailedAwaitable() + { + $exception = new \Exception(); + + $awaitable = new Failure($exception); + + try { + $result = Amp\wait($awaitable); + } catch (\Exception $e) { + $this->assertSame($exception, $e); + return; + } + + $this->fail('Rejection exception should be thrown from wait().'); + } + + /** + * @depends testWaitOnSuccessfulAwaitable + */ + public function testWaitOnPendingAwaitable() + { + Loop::execute(function () { + $value = 1; + + $awaitable = new Pause(100, $value); + + $result = Amp\wait($awaitable); + + $this->assertSame($value, $result); + }); + } + + /** + * @expectedException \LogicException + */ + public function testAwaitableWithNoResolutionPathThrowsException() + { + $awaitable = new Future; + + $result = Amp\wait($awaitable); + } +}