diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdbce6..9b33c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ ### master -- n/a +- Added `Reactor::coroutine()` method +- Added `Amp\coroutine()` function +- `YieldCommands` "enum" constant class removed -- yield keys now live in + the reactor class +- New optional `"coroutine"` yield key for self-documenting generator + yields. +- New optional `"async"` yield key for self-documenting promise yields. +- New `"return"` yield key for specifying the return value of a resolved + Generator coroutine. If not specified a resolved coroutine result is + equal to null. +- The final value yielded by a resolved `Generator` is *no longer* used + as its "return" value. Instead, generators must manually use the new + `"return"` yield key specifically to designate the value that should + be used to resolve the promise associated with generator resolution. +- `GeneratorResolver` trait renamed to `CoroutineResolver` and is now an + abstract class extended by the various `Reactor` implementations. +- Implicit "all" array combinator resolution is now removed. Use the + explicit form instead: + +```php +function() { + list($a, $b, $c) = (yield 'all' => [$promise1, $promise2, $promise3]); +}; +``` ### v0.15.3 diff --git a/lib/CoroutineResolver.php b/lib/CoroutineResolver.php new file mode 100644 index 0000000..8937ea3 --- /dev/null +++ b/lib/CoroutineResolver.php @@ -0,0 +1,307 @@ +advanceGenerator($gen, $promisor, null); + + return $promisor; + } + + private function advanceGenerator(\Generator $gen, Promisor $promisor, $return) { + try { + if (!$gen->valid()) { + $promisor->succeed($return); + return; + } + list($promise, $noWait, $return) = $this->promisifyGeneratorYield($gen, $return); + $this->immediately(function() use ($gen, $promisor, $return, $promise, $noWait) { + if ($noWait) { + $this->sendToGenerator($gen, $promisor, $return); + } else { + $promise->when(function($error, $result) use ($gen, $promisor, $return) { + $this->sendToGenerator($gen, $promisor, $return, $error, $result); + }); + } + }); + } catch (\Exception $uncaught) { + $promisor->fail($uncaught); + } + } + + private function sendToGenerator(\Generator $gen, Promisor $promisor, $return = null, \Exception $error = null, $result = null) { + try { + if ($error) { + $gen->throw($error); + } else { + $gen->send($result); + } + $this->advanceGenerator($gen, $promisor, $return); + } catch (\Exception $uncaught) { + $promisor->fail($uncaught); + } + } + + private function promisifyGeneratorYield(\Generator $gen, $return) { + $noWait = false; + $promise = null; + + $key = $gen->key(); + $yielded = $gen->current(); + + if (is_string($key)) { + goto explicit_key; + } else { + goto implicit_key; + } + + implicit_key: { + if ($yielded instanceof Promise) { + $promise = $yielded; + } elseif ($yielded instanceof \Generator) { + $promise = $this->coroutine($yielded); + } elseif (isset($yielded)) { + $promise = new Failure(new \LogicException( + sprintf( + 'Unresolvable %s type requires yield key', + is_object($yielded) ? get_class($yielded) : gettype($yielded) + ) + )); + } else { + $promise = new Failure(new \LogicException( + 'Empty yield without key (yield; or yield null;)' + )); + } + + goto return_struct; + } + + explicit_key: { + $key = strtolower($key); + if ($key[0] === self::NOWAIT_PREFIX) { + $noWait = true; + $key = substr($key, 1); + } + + switch ($key) { + case self::ASYNC: + goto async; + case self::COROUTINE: + goto coroutine; + case self::CORETURN: + goto coreturn; + case self::ALL: + // fallthrough + case self::ANY: + // fallthrough + case self::SOME: + goto combinator; + case self::PAUSE: + goto pause; + case self::BIND: + goto bind; + case self::IMMEDIATELY: + goto immediately; + case self::ONCE: + // fallthrough + case self::REPEAT: + goto schedule; + case self::ON_READABLE: + $ioWatchMethod = 'onReadable'; + goto io_watcher; + case self::ON_WRITABLE: + $ioWatchMethod = 'onWritable'; + goto io_watcher; + case self::ENABLE: + // fallthrough + case self::DISABLE: + // fallthrough + case self::CANCEL: + goto watcher_control; + case self::NOWAIT: + $noWait = true; + goto implicit_key; + default: + if ($noWait) { + $promise = new Failure(new \LogicException( + 'Cannot use standalone @ "nowait" prefix' + )); + goto return_struct; + } else { + goto unknown_key; + } + } + } + + coreturn: { + $return = $yielded; + $promise = new Success; + goto return_struct; + } + + unknown_key: { + $promise = new Failure(new \DomainException( + sprintf('Unknown yield key: "%s"', $key) + )); + goto return_struct; + } + + async: { + if ($yielded instanceof Promise) { + $promise = $yielded; + } else { + $promise = new Failure(new \DomainException( + sprintf( + '"%s" yield command expects Promise; %s yielded', + $key, + gettype($yielded) + ) + )); + } + goto return_struct; + } + + coroutine: { + if ($yielded instanceof \Generator) { + $promise = $this->coroutine($yielded); + } else { + $promise = new Failure(new \DomainException( + sprintf( + '"%s" yield command expects Generator; %s yielded', + $key, + gettype($yielded) + ) + )); + } + goto return_struct; + } + + combinator: { + if (!is_array($yielded)) { + $promise = new Failure(new \DomainException( + sprintf('"%s" yield command expects array; %s yielded', $key, gettype($yielded)) + )); + goto return_struct; + } + + $promises = []; + foreach ($yielded as $index => $element) { + if ($element instanceof Promise) { + $promise = $element; + } elseif ($element instanceof \Generator) { + $promise = $this->coroutine($element); + } else { + $promise = new Success($element); + } + + $promises[$index] = $promise; + } + + $combinatorFunction = __NAMESPACE__ . "\\{$key}"; + $promise = $combinatorFunction($promises); + + goto return_struct; + } + + immediately: { + if (is_callable($yielded)) { + $watcherId = $this->immediately($yielded); + $promise = new Success($watcherId); + } else { + $promise = new Failure(new \DomainException( + sprintf( + '"%s" yield command requires callable; %s provided', + $key, + gettype($yielded) + ) + )); + } + + goto return_struct; + } + + schedule: { + if (is_array($yielded) && isset($yielded[0], $yielded[1]) && is_callable($yielded[0])) { + list($func, $msDelay) = $yielded; + $watcherId = $this->{$key}($func, $msDelay); + $promise = new Success($watcherId); + } else { + $promise = new Failure(new \DomainException( + sprintf( + '"%s" yield command requires [callable $func, int $msDelay]; %s provided', + $key, + gettype($yielded) + ) + )); + } + + goto return_struct; + } + + io_watcher: { + if (is_array($yielded) && isset($yielded[0], $yielded[1]) && is_callable($yielded[1])) { + list($stream, $func, $enableNow) = $yielded; + $watcherId = $this->{$ioWatchMethod}($stream, $func, $enableNow); + $promise = new Success($watcherId); + } else { + $promise = new Failure(new \DomainException( + sprintf( + '"%s" yield command requires [resource $stream, callable $func, bool $enableNow]; %s provided', + $key, + gettype($yielded) + ) + )); + } + + goto return_struct; + } + + pause: { + $promise = new Future; + $this->once(function() use ($promise) { + $promise->succeed(); + }, (int) $yielded); + + goto return_struct; + } + + bind: { + if (is_callable($yielded)) { + $promise = new Success(function() use ($yielded) { + $result = call_user_func_array($yielded, func_get_args()); + return $result instanceof \Generator + ? $this->coroutine($result) + : $result; + }); + } else { + $promise = new Failure(new \DomainException( + sprintf('"bind" yield command requires callable; %s provided', gettype($yielded)) + )); + } + + goto return_struct; + } + + watcher_control: { + $this->{$key}($yielded); + $promise = new Success; + + goto return_struct; + } + + return_struct: { + return [$promise, $noWait, $return]; + } + } +} diff --git a/lib/GeneratorResolver.php b/lib/GeneratorResolver.php deleted file mode 100644 index 7d06ccd..0000000 --- a/lib/GeneratorResolver.php +++ /dev/null @@ -1,243 +0,0 @@ -advanceGenerator($gen, $promisor); - - return $promisor; - } - - private function advanceGenerator(\Generator $gen, Promisor $promisor, $previous = null) { - try { - if ($gen->valid()) { - $key = $gen->key(); - $current = $gen->current(); - $promiseStruct = $this->promisifyGeneratorYield($key, $current); - $this->immediately(function() use ($gen, $promisor, $promiseStruct) { - list($promise, $noWait) = $promiseStruct; - if ($noWait) { - $this->sendToGenerator($gen, $promisor); - } else { - $promise->when(function($error, $result) use ($gen, $promisor) { - $this->sendToGenerator($gen, $promisor, $error, $result); - }); - } - }); - } else { - $promisor->succeed($previous); - } - } catch (\Exception $error) { - $promisor->fail($error); - } - } - - private function promisifyGeneratorYield($key, $current) { - $noWait = false; - - if ($key === (string) $key) { - goto explicit_key; - } else { - goto implicit_key; - } - - explicit_key: { - $key = strtolower($key); - if ($key[0] === YieldCommands::NOWAIT_PREFIX) { - $noWait = true; - $key = substr($key, 1); - } - - switch ($key) { - case YieldCommands::ALL: - // fallthrough - case YieldCommands::ANY: - // fallthrough - case YieldCommands::SOME: - if (is_array($current)) { - goto combinator; - } else { - $promise = new Failure(new \DomainException( - sprintf('"%s" yield command expects array; %s yielded', $key, gettype($current)) - )); - goto return_struct; - } - case YieldCommands::PAUSE: - goto pause; - case YieldCommands::BIND: - goto bind; - case YieldCommands::IMMEDIATELY: - goto immediately; - case YieldCommands::ONCE: - // fallthrough - case YieldCommands::REPEAT: - goto schedule; - case YieldCommands::ON_READABLE: - $ioWatchMethod = 'onReadable'; - goto io_watcher; - case YieldCommands::ON_WRITABLE: - $ioWatchMethod = 'onWritable'; - goto io_watcher; - case YieldCommands::ENABLE: - // fallthrough - case YieldCommands::DISABLE: - // fallthrough - case YieldCommands::CANCEL: - goto watcher_control; - case YieldCommands::NOWAIT: - $noWait = true; - goto implicit_key; - default: - if ($noWait) { - goto implicit_key; - } - $promise = new Failure(new \DomainException( - sprintf('Unknown or invalid yield key: "%s"', $key) - )); - goto return_struct; - } - } - - implicit_key: { - if ($current instanceof Promise) { - $promise = $current; - } elseif ($current instanceof \Generator) { - $promise = $this->resolveGenerator($current); - } elseif (is_array($current)) { - $key = YieldCommands::ALL; - goto combinator; - } else { - $promise = new Success($current); - } - - goto return_struct; - } - - combinator: { - $promises = []; - foreach ($current as $index => $element) { - if ($element instanceof Promise) { - $promise = $element; - } elseif ($element instanceof \Generator) { - $promise = $this->resolveGenerator($element); - } else { - $promise = new Success($element); - } - - $promises[$index] = $promise; - } - - $combinatorFunction = __NAMESPACE__ . "\\{$key}"; - $promise = $combinatorFunction($promises); - - goto return_struct; - } - - immediately: { - if (is_callable($current)) { - $watcherId = $this->immediately($current); - $promise = new Success($watcherId); - } else { - $promise = new Failure(new \DomainException( - sprintf( - '"%s" yield command requires callable; %s provided', - $key, - gettype($current) - ) - )); - } - - goto return_struct; - } - - schedule: { - if (is_array($current) && isset($current[0], $current[1]) && is_callable($current[0])) { - list($func, $msDelay) = $current; - $watcherId = $this->{$key}($func, $msDelay); - $promise = new Success($watcherId); - } else { - $promise = new Failure(new \DomainException( - sprintf( - '"%s" yield command requires [callable $func, int $msDelay]; %s provided', - $key, - gettype($current) - ) - )); - } - - goto return_struct; - } - - io_watcher: { - if (is_array($current) && isset($current[0], $current[1]) && is_callable($current[1])) { - list($stream, $func, $enableNow) = $current; - $watcherId = $this->{$ioWatchMethod}($stream, $func, $enableNow); - $promise = new Success($watcherId); - } else { - $promise = new Failure(new \DomainException( - sprintf( - '"%s" yield command requires [resource $stream, callable $func, bool $enableNow]; %s provided', - $key, - gettype($current) - ) - )); - } - - goto return_struct; - } - - pause: { - $promisor = new Future; - $this->once(function() use ($promisor) { - $promisor->succeed(); - }, (int) $current); - - $promise = $promisor; - - goto return_struct; - } - - bind: { - if (is_callable($current)) { - $promise = new Success(function() use ($current) { - $result = call_user_func_array($current, func_get_args()); - return $result instanceof \Generator - ? $this->resolveGenerator($result) - : $result; - }); - } else { - $promise = new Failure(new \DomainException( - sprintf('"bind" yield command requires callable; %s provided', gettype($current)) - )); - } - - goto return_struct; - } - - watcher_control: { - $this->{$key}($current); - $promise = new Success; - - goto return_struct; - } - - return_struct: { - return [$promise, $noWait]; - } - } - - private function sendToGenerator(\Generator $gen, Promisor $promisor, \Exception $error = null, $result = null) { - try { - if ($error) { - $gen->throw($error); - } else { - $gen->send($result); - } - $this->advanceGenerator($gen, $promisor, $result); - } catch (\Exception $error) { - $promisor->fail($error); - } - } -} diff --git a/lib/LibeventReactor.php b/lib/LibeventReactor.php index 3737368..2bbde14 100644 --- a/lib/LibeventReactor.php +++ b/lib/LibeventReactor.php @@ -2,9 +2,7 @@ namespace Amp; -class LibeventReactor implements SignalReactor { - use GeneratorResolver; - +class LibeventReactor extends CoroutineResolver implements SignalReactor { private $base; private $watchers = []; private $immediates = []; @@ -87,7 +85,7 @@ class LibeventReactor implements SignalReactor { ); $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); @@ -224,7 +222,7 @@ class LibeventReactor implements SignalReactor { $watcherId = $watcher->id; $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } $this->cancel($watcherId); } catch (\Exception $e) { @@ -289,7 +287,7 @@ class LibeventReactor implements SignalReactor { try { $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } // If the watcher cancelled itself this will no longer be set @@ -363,7 +361,7 @@ class LibeventReactor implements SignalReactor { try { $result = $callback($this, $watcherId, $stream); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); @@ -411,7 +409,7 @@ class LibeventReactor implements SignalReactor { try { $result = $callback($this, $watcherId, $signo); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); diff --git a/lib/NativeReactor.php b/lib/NativeReactor.php index 49a99ab..42d6eeb 100644 --- a/lib/NativeReactor.php +++ b/lib/NativeReactor.php @@ -2,9 +2,7 @@ namespace Amp; -class NativeReactor implements Reactor { - use GeneratorResolver; - +class NativeReactor extends CoroutineResolver implements Reactor { private $alarms = []; private $immediates = []; private $alarmOrder = []; @@ -123,7 +121,7 @@ class NativeReactor implements Reactor { foreach ($immediates as $watcherId => $callback) { $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } } @@ -179,7 +177,7 @@ class NativeReactor implements Reactor { foreach ($this->readCallbacks[$streamId] as $watcherId => $callback) { $result = $callback($this, $watcherId, $readableStream); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } } @@ -188,7 +186,7 @@ class NativeReactor implements Reactor { foreach ($this->writeCallbacks[$streamId] as $watcherId => $callback) { $result = $callback($this, $watcherId, $writableStream); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } } @@ -219,7 +217,7 @@ class NativeReactor implements Reactor { $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } } diff --git a/lib/Reactor.php b/lib/Reactor.php index 234f0b9..5944237 100644 --- a/lib/Reactor.php +++ b/lib/Reactor.php @@ -3,6 +3,25 @@ namespace Amp; interface Reactor { + const ALL = 'all'; + const ANY = 'any'; + const SOME = 'some'; + const PAUSE = 'pause'; + const BIND = 'bind'; + const IMMEDIATELY = 'immediately'; + const ONCE = 'once'; + const REPEAT = 'repeat'; + const ON_READABLE = 'onreadable'; + const ON_WRITABLE = 'onwritable'; + const ENABLE = 'enable'; + const DISABLE = 'disable'; + const CANCEL = 'cancel'; + const NOWAIT = 'nowait'; + const NOWAIT_PREFIX = '@'; + const ASYNC = 'async'; + const COROUTINE = 'coroutine'; + const CORETURN = 'return'; + /** * Start the event reactor and assume program flow control * @@ -112,6 +131,17 @@ interface Reactor { */ public function enable($watcherId); + /** + * Resolve the specified generator + * + * Upon resolution the final yielded value is used to succeed the returned promise. If an + * error occurs the returned promise is failed appropriately. + * + * @param \Generator $generator + * @return Promise + */ + public function coroutine(\Generator $generator); + /** * An optional "last-chance" exception handler for errors resulting during callback invocation * diff --git a/lib/UvReactor.php b/lib/UvReactor.php index 8e2783d..624b37e 100644 --- a/lib/UvReactor.php +++ b/lib/UvReactor.php @@ -2,9 +2,7 @@ namespace Amp; -class UvReactor implements SignalReactor { - use GeneratorResolver; - +class UvReactor extends CoroutineResolver implements SignalReactor { private $loop; private $lastWatcherId = 1; private $watchers; @@ -95,7 +93,7 @@ class UvReactor implements SignalReactor { ); $result = $callback($this, $watcherId); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); @@ -221,12 +219,11 @@ class UvReactor implements SignalReactor { try { $result = $callback($this, $watcher->id); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } // The isset() check is necessary because the "once" timer // callback may have cancelled itself when it was invoked. if ($watcher->type === Watcher::TIMER_ONCE && isset($this->watchers[$watcher->id])) { - $watcher->isEnabled = false; $this->clearWatcher($watcher->id); } } catch (\Exception $e) { @@ -381,7 +378,7 @@ class UvReactor implements SignalReactor { $callback = $watcher->callback; $result = $callback($this, $watcher->id, $watcher->stream); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); @@ -417,7 +414,7 @@ class UvReactor implements SignalReactor { try { $result = $callback($this, $watcher->id, $watcher->signo); if ($result instanceof \Generator) { - $this->resolveGenerator($result)->when($this->onCallbackResolution); + $this->coroutine($result)->when($this->onCallbackResolution); } } catch (\Exception $e) { $this->handleRunError($e); @@ -440,7 +437,6 @@ class UvReactor implements SignalReactor { private function clearWatcher($watcherId) { $watcher = $this->watchers[$watcherId]; unset($this->watchers[$watcherId]); - if ($watcher->isEnabled) { $this->enabledWatcherCount--; switch ($watcher->type) { @@ -455,8 +451,11 @@ class UvReactor implements SignalReactor { case Watcher::IMMEDIATE: unset($this->immediates[$watcherId]); break; + case Watcher::TIMER_ONCE: + // we don't have to actually stop once timers + break; default: - @uv_timer_stop($watcher->uvStruct); + uv_timer_stop($watcher->uvStruct); break; } } diff --git a/lib/YieldCommands.php b/lib/YieldCommands.php deleted file mode 100644 index 64bf18a..0000000 --- a/lib/YieldCommands.php +++ /dev/null @@ -1,21 +0,0 @@ -onWritable($stream, $func, $enableNow); } - +/** + * Resolve the specified generator + * + * Upon resolution the final yielded value is used to succeed the returned promise. If an + * error occurs the returned promise is failed appropriately. + * + * @param \Generator $generator + * @param Reactor $reactor optional reactor instance (uses global reactor if not specified) + * @return Promise + */ +function coroutine(\Generator $generator, $reactor = null) { + $reactor = $reactor ?: getReactor(); + return $reactor->coroutine($generator); +} /** * React to process control signals diff --git a/test/GeneratorResolverTest.php b/test/GeneratorResolverTest.php deleted file mode 100644 index ec03ad8..0000000 --- a/test/GeneratorResolverTest.php +++ /dev/null @@ -1,305 +0,0 @@ -run(function($reactor) { - $expected = ['r1' => 42, 'r2' => 41]; - $actual = (yield 'all' => [ - 'r1' => 42, - 'r2' => new Success(41), - ]); - $this->assertSame($expected, $actual); - }); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage zanzibar - */ - public function testAllThrowsIfAnyIndividualPromiseFails() { - (new NativeReactor)->run(function($reactor) { - $exception = new \RuntimeException('zanzibar'); - $promises = [ - 'r1' => new Success(42), - 'r2' => new Failure($exception), - 'r3' => new Success(40), - ]; - $results = (yield $promises); - }); - } - - public function testSomeReturnsArrayOfErrorsAndResults() { - (new NativeReactor)->run(function($reactor) { - $exception = new \RuntimeException('zanzibar'); - $promises = [ - 'r1' => new Success(42), - 'r2' => new Failure($exception), - 'r3' => new Success(40), - ]; - list($errors, $results) = (yield 'some' => $promises); - $this->assertSame(['r2' => $exception], $errors); - $this->assertSame(['r1' => 42, 'r3' => 40], $results); - }); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage All promises failed - */ - public function testSomeThrowsIfNoPromisesResolveSuccessfully() { - (new NativeReactor)->run(function($reactor) { - $promises = [ - 'r1' => new Failure(new \RuntimeException), - 'r2' => new Failure(new \RuntimeException), - ]; - list($errors, $results) = (yield 'some' => $promises); - }); - } - - public function testResolvedValueEqualsFinalYield() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - $a = (yield 21); - $b = (yield new Success(2)); - yield ($a * $b); - }; - - $result = (yield $gen()); - $this->assertSame(42, $result); - }); - } - - public function testFutureErrorsAreThrownIntoGenerator() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - $a = (yield 21); - $b = 1; - try { - yield new Failure(new \Exception('test')); - $this->fail('Code path should not be reached'); - } catch (\Exception $e) { - $this->assertSame('test', $e->getMessage()); - $b = 2; - } - - yield ($a * $b); - }; - - $result = (yield $gen()); - $this->assertSame(42, $result); - }); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage When in the chronicle of wasted time - */ - public function testUncaughtGeneratorExceptionFailsResolverPromise() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - yield; - throw new \Exception('When in the chronicle of wasted time'); - yield; - }; - - yield $gen(); - }); - } - - public function testImplicitAllCombinatorResolution() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - list($a, $b) = (yield [ - new Success(21), - new Success(2), - ]); - yield ($a * $b); - }; - - $result = (yield $gen()); - $this->assertSame(42, $result); - }); - } - - public function testImplicitAllCombinatorResolutionWithNonPromises() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - list($a, $b, $c) = (yield [new Success(21), new Success(2), 10]); - yield ($a * $b * $c); - }; - - $result = (yield $gen()); - $this->assertSame(420, $result); - }); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage When in the chronicle of wasted time - */ - public function testImplicitAllCombinatorResolutionThrowsIfAnyOnePromiseFails() { - $gen = function() { - list($a, $b) = (yield [ - new Success(21), - new Failure(new \Exception('When in the chronicle of wasted time')), - ]); - }; - - $reactor = new NativeReactor; - $reactor->run(function($reactor) use ($gen) { - yield $gen(); - }); - } - - public function testImplicitCombinatorResolvesGeneratorInArray() { - (new NativeReactor)->run(function($reactor) { - $gen1 = function() { - yield 21; - }; - - $gen2 = function() use ($gen1) { - list($a, $b) = (yield [ - $gen1(), - new Success(2) - ]); - yield ($a * $b); - }; - - - $result = (yield $gen2()); - $this->assertSame(42, $result); - }); - } - - public function testExplicitAllCombinatorResolution() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - list($a, $b, $c) = (yield 'all' => [ - new Success(21), - new Success(2), - 10 - ]); - yield ($a * $b * $c); - }; - - $result = (yield $gen()); - $this->assertSame(420, $result); - }); - } - - public function testExplicitAnyCombinatorResolution() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - yield 'any' => [ - 'a' => new Success(21), - 'b' => new Failure(new \Exception('test')), - ]; - }; - - list($errors, $results) = (yield $gen()); - $this->assertSame('test', $errors['b']->getMessage()); - $this->assertSame(21, $results['a']); - }); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage All promises failed - */ - public function testExplicitSomeCombinatorResolutionFailsOnError() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - yield 'some' => [ - 'r1' => new Failure(new \RuntimeException), - 'r2' => new Failure(new \RuntimeException), - ]; - }; - yield $gen(); - }); - } - - /** - * @expectedException \DomainException - * @expectedExceptionMessage "some" yield command expects array; string yielded - */ - public function testExplicitCombinatorResolutionFailsIfNonArrayYielded() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - yield 'some' => 'test'; - }; - yield $gen(); - }); - } - - public function testExplicitImmediatelyYieldResolution() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - $var = null; - yield 'immediately' => function() use (&$var) { $var = 42; }; - yield 'pause' => 100; // pause for 100ms so the immediately callback executes - yield $var; - }; - $result = (yield $gen()); - $this->assertSame(42, $result); - }); - } - - public function testExplicitOnceYieldResolution() { - (new NativeReactor)->run(function($reactor) { - $gen = function() { - $var = null; - yield 'once' => [function() use (&$var) { $var = 42; }, $msDelay = 1]; - yield 'pause' => 100; // pause for 100ms so the once callback executes - yield $var; - }; - $result = (yield $gen()); - $this->assertSame(42, $result); - }); - } - - public function testExplicitRepeatYieldResolution() { - (new NativeReactor)->run(function($reactor) { - $var = null; - $repeatFunc = function($reactor, $watcherId) use (&$var) { - $var = 1; - yield 'cancel' => $watcherId; - $var++; - }; - - $gen = function() use (&$var, $repeatFunc) { - yield 'repeat' => [$repeatFunc, $msDelay = 1]; - yield 'pause' => 100; // pause for 100ms so we can be sure the repeat callback executes - yield $var; - }; - - $result = (yield $gen()); - $this->assertSame(2, $result); - }); - } - - public function testCallableBindYield() { - (new NativeReactor)->run(function($reactor) { - // Register a repeating callback so the reactor run loop doesn't break - // without our intervention. - $repeatWatcherId = (yield 'repeat' => [function(){}, 1000]); - - $func = function() use ($repeatWatcherId) { - yield "cancel" => $repeatWatcherId; - }; - - $boundFunc = (yield "bind" => $func); - - // Because this Generator function is bound to the reactor it should be - // automatically resolved and our repeating watcher should be cancelled - // allowing the reactor to stop running. - $result = $boundFunc(); - $this->assertInstanceOf('Amp\\Promise', $result); - }); - } -} diff --git a/test/ReactorTest.php b/test/ReactorTest.php index 05884ea..92f5e48 100644 --- a/test/ReactorTest.php +++ b/test/ReactorTest.php @@ -2,6 +2,9 @@ namespace Amp\Test; +use Amp\Success; +use Amp\Failure; + abstract class ReactorTest extends \PHPUnit_Framework_TestCase { abstract protected function getReactor(); @@ -272,14 +275,12 @@ abstract class ReactorTest extends \PHPUnit_Framework_TestCase { } public function testOnStartGeneratorResolvesAutomatically() { - $reactor = $this->getReactor(); $test = ''; - $gen = function($reactor) use (&$test) { - yield; + $this->getReactor()->run(function($reactor) use (&$test) { + yield "pause" => 1; $test = "Thus Spake Zarathustra"; $reactor->once(function() use ($reactor) { $reactor->stop(); }, 50); - }; - $reactor->run($gen); + }); $this->assertSame("Thus Spake Zarathustra", $test); } @@ -287,7 +288,7 @@ abstract class ReactorTest extends \PHPUnit_Framework_TestCase { $reactor = $this->getReactor(); $test = ''; $gen = function($reactor) use (&$test) { - yield; + yield "pause" => 1; $test = "The abyss will gaze back into you"; $reactor->once(function() use ($reactor) { $reactor->stop(); }, 50); }; @@ -300,7 +301,7 @@ abstract class ReactorTest extends \PHPUnit_Framework_TestCase { $reactor = $this->getReactor(); $test = ''; $gen = function($reactor) use (&$test) { - yield; + yield "pause" => 1; $test = "There are no facts, only interpretations."; $reactor->once(function() use ($reactor) { $reactor->stop(); }, 50); }; @@ -314,7 +315,7 @@ abstract class ReactorTest extends \PHPUnit_Framework_TestCase { $test = ''; $gen = function($reactor, $watcherId) use (&$test) { $reactor->cancel($watcherId); - yield; + yield "pause" => 1; $test = "Art is the supreme task"; $reactor->stop(); }; @@ -376,4 +377,328 @@ abstract class ReactorTest extends \PHPUnit_Framework_TestCase { $this->assertSame(3, $var1); $this->assertSame(4, $var2); } + + + + + + + + + + + + + + + + + + + public function testAllResolvesWithArrayOfResults() { + $this->getReactor()->run(function($reactor) { + $expected = ['r1' => 42, 'r2' => 41]; + $actual = (yield 'all' => [ + 'r1' => 42, + 'r2' => new Success(41), + ]); + $this->assertSame($expected, $actual); + }); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage zanzibar + */ + public function testAllThrowsIfAnyIndividualPromiseFails() { + $this->getReactor()->run(function($reactor) { + $exception = new \RuntimeException('zanzibar'); + $promises = [ + 'r1' => new Success(42), + 'r2' => new Failure($exception), + 'r3' => new Success(40), + ]; + $results = (yield 'all' => $promises); + }); + } + + public function testSomeReturnsArrayOfErrorsAndResults() { + $this->getReactor()->run(function($reactor) { + $exception = new \RuntimeException('zanzibar'); + $promises = [ + 'r1' => new Success(42), + 'r2' => new Failure($exception), + 'r3' => new Success(40), + ]; + list($errors, $results) = (yield 'some' => $promises); + $this->assertSame(['r2' => $exception], $errors); + $this->assertSame(['r1' => 42, 'r3' => 40], $results); + }); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage All promises failed + */ + public function testSomeThrowsIfNoPromisesResolveSuccessfully() { + $this->getReactor()->run(function($reactor) { + $promises = [ + 'r1' => new Failure(new \RuntimeException), + 'r2' => new Failure(new \RuntimeException), + ]; + list($errors, $results) = (yield 'some' => $promises); + }); + } + + public function testResolvedValueEqualsReturnKeyYield() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + $a = (yield new Success(21)); + $b = (yield new Success(2)); + yield 'return' => ($a * $b); + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(42, $result); + }); + } + + public function testResolutionFailuresAreThrownIntoGenerator() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + $a = (yield new Success(21)); + $b = 1; + try { + yield new Failure(new \Exception('test')); + $this->fail('Code path should not be reached'); + } catch (\Exception $e) { + $this->assertSame('test', $e->getMessage()); + $b = 2; + } + + yield 'return' => ($a * $b); + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(42, $result); + }); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage When in the chronicle of wasted time + */ + public function testUncaughtGeneratorExceptionFailsResolverPromise() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + yield "pause" => 1; + throw new \Exception('When in the chronicle of wasted time'); + yield "pause" => 1; + }; + + yield 'coroutine' => $gen(); + }); + } + + public function testAllCombinatorResolution() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + list($a, $b) = (yield 'all' => [ + new Success(21), + new Success(2), + ]); + yield 'return' => ($a * $b); + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(42, $result); + }); + } + + public function testAllCombinatorResolutionWithNonPromises() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + list($a, $b, $c) = (yield 'all' => [new Success(21), new Success(2), 10]); + yield 'return' => ($a * $b * $c); + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(420, $result); + }); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage When in the chronicle of wasted time + */ + public function testAllCombinatorResolutionThrowsIfAnyOnePromiseFails() { + $gen = function() { + list($a, $b) = (yield 'all' => [ + new Success(21), + new Failure(new \Exception('When in the chronicle of wasted time')), + ]); + }; + + $this->getReactor()->run(function($reactor) use ($gen) { + yield 'coroutine' => $gen(); + }); + } + + public function testCombinatorResolvesGeneratorInArray() { + $this->getReactor()->run(function($reactor) { + $gen1 = function() { + yield 'return' => 21; + }; + + $gen2 = function() use ($gen1) { + list($a, $b) = (yield 'all' => [ + \Amp\coroutine($gen1(), $reactor), + new Success(2) + ]); + yield 'return' => ($a * $b); + }; + + $result = (yield 'coroutine' => $gen2()); + $this->assertSame(42, $result); + }); + } + + public function testExplicitAllCombinatorResolution() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + list($a, $b, $c) = (yield 'all' => [ + new Success(21), + new Success(2), + 10 + ]); + yield 'return' => ($a * $b * $c); + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(420, $result); + }); + } + + public function testExplicitAnyCombinatorResolution() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + $any = (yield 'any' => [ + 'a' => new Success(21), + 'b' => new Failure(new \Exception('test')), + ]); + + yield 'return' => $any; + }; + + list($errors, $results) = (yield 'coroutine' => $gen()); + $this->assertSame('test', $errors['b']->getMessage()); + $this->assertSame(21, $results['a']); + }); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage All promises failed + */ + public function testExplicitSomeCombinatorResolutionFailsOnError() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + yield 'some' => [ + 'r1' => new Failure(new \RuntimeException), + 'r2' => new Failure(new \RuntimeException), + ]; + }; + yield 'coroutine' => $gen(); + }); + } + + /** + * @expectedException \DomainException + * @expectedExceptionMessage "some" yield command expects array; string yielded + */ + public function testExplicitCombinatorResolutionFailsIfNonArrayYielded() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + yield 'some' => 'test'; + }; + yield 'coroutine' => $gen(); + }); + } + + public function testCallableBindYield() { + $this->getReactor()->run(function($reactor) { + // Register a repeating callback so the reactor run loop doesn't break + // without our intervention. + $repeatWatcherId = (yield 'repeat' => [function(){}, 1000]); + + $func = function() use ($repeatWatcherId) { + yield "cancel" => $repeatWatcherId; + }; + + $boundFunc = (yield "bind" => $func); + + // Because this Generator function is bound to the reactor it should be + // automatically resolved and our repeating watcher should be cancelled + // allowing the reactor to stop running. + $result = $boundFunc(); + $this->assertInstanceOf('Amp\\Promise', $result); + }); + } + + public function testExplicitImmediatelyYieldResolution() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + $var = null; + yield 'immediately' => function() use (&$var) { $var = 42; }; + yield 'pause' => 100; // pause for 100ms so the immediately callback executes + yield 'return' => $var; + }; + $result = (yield 'coroutine' => $gen()); + $this->assertSame(42, $result); + }); + } + + public function testExplicitOnceYieldResolution() { + $this->getReactor()->run(function($reactor) { + $gen = function() { + $var = null; + yield 'once' => [function() use (&$var) { $var = 42; }, $msDelay = 1]; + yield 'pause' => 100; // pause for 100ms so the once callback executes + yield 'return' => $var; + }; + $result = (yield 'coroutine' => $gen()); + $this->assertSame(42, $result); + }); + } + + public function testExplicitRepeatYieldResolution() { + $this->getReactor()->run(function($reactor) { + $var = null; + $repeatFunc = function($reactor, $watcherId) use (&$var) { + $var = 1; + yield 'cancel' => $watcherId; + $var++; + }; + + $gen = function() use (&$var, $repeatFunc) { + yield 'repeat' => [$repeatFunc, $msDelay = 1]; + yield 'pause' => 100; // pause for 100ms so we can be sure the repeat callback executes + yield 'return' => $var; + }; + + $result = (yield 'coroutine' => $gen()); + $this->assertSame(2, $result); + }); + } + + + + + + + + + + + }