From a3a4a3d4688ca034b630c01876fdc550385e2cc6 Mon Sep 17 00:00:00 2001 From: techno-express Date: Tue, 4 Dec 2018 16:07:25 -0500 Subject: [PATCH 01/23] some guzzle promise tests --- .gitignore | 1 + tests/Promise/PromiseTest.php | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/.gitignore b/.gitignore index 12f5de9..8e8d8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ bin/phpunit #development stuff tests/cov .php_cs.cache +bin/phpunit.bat diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index 4ada055..9e8b3d0 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -226,4 +226,49 @@ public function testWaitRejectedScalar() $this->assertEquals('foo', $e->getMessage()); } } + + ////////////////////////////////// + public function testForwardsFulfilledDownChainBetweenGaps() + { + $p = new Promise(); + $r = $r2 = null; + $p->then(null, null) + ->then(function ($v) use (&$r) {$r = $v; return $v . '2'; }) + ->then(function ($v) use (&$r2) { $r2 = $v; }); + $p->fulfill('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertEquals('foo2', $r2); + } + + public function testForwardsHandlersToNextPromise() + { + $p = new Promise(); + $p2 = new Promise(); + $resolved = null; + $p + ->then(function ($v) use ($p2) { return $p2; }) + ->then(function ($value) use (&$resolved) { $resolved = $value; }); + $p->fulfill('a'); + $p2->fulfill('b'); + Loop\run(); + $this->assertEquals('b', $resolved); + } + + public function testForwardsHandlersWhenFulfilledPromiseIsReturned() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->fulfill('foo'); + $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); + // $res is A:foo + $p + ->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) + ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->fulfill('a'); + $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); + } } From 396e1e6966fe06196ee0b5cbb2ac65e1b1770396 Mon Sep 17 00:00:00 2001 From: techno-express Date: Wed, 5 Dec 2018 08:54:11 -0500 Subject: [PATCH 02/23] some changes to be guzzle compitable --- lib/Promise.php | 68 +++++++++++++++++++++++++++++++---- tests/Promise/PromiseTest.php | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/lib/Promise.php b/lib/Promise.php index 98d5ec3..e015176 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -56,14 +56,32 @@ class Promise * Each are callbacks that map to $this->fulfill and $this->reject. * Using the executor is optional. */ - public function __construct(callable $executor = null) + public function __construct( ...$executor) { - if ($executor) { - $executor( + $callDone = isset($executor[0]) ? $executor[0] : null; + $callFail = isset($executor[1]) ? $executor[1] : null; + + $this->waitFn = is_callable($callDone) ? $callDone : null; + $this->cancelFn = is_callable($callFail) + ? function($reason = null) use($callFail) { + try + { + return $callFail && ($result = $callFail($reason)) instanceof self ? $result : $this; + } + catch (Error $ex) + {} + catch (Exception $ex) + {} + return $this; + } + : null; + + if (is_callable($callDone)) { + $callDone( [$this, 'fulfill'], [$this, 'reject'] - ); - } + ); + } } /** @@ -90,7 +108,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): // This new subPromise will be returned from this function, and will // be fulfilled with the result of the onFulfilled or onRejected event // handlers. - $subPromise = new self(); + $subPromise = new Promise(null, [$this, 'cancel']); switch ($this->state) { case self::PENDING: @@ -124,6 +142,18 @@ public function otherwise(callable $onRejected): Promise return $this->then(null, $onRejected); } + public function resolve($value): Promise + { + if ($value instanceof Promise) { + return $value->then(); + } else { + $promise = new Promise(); + $promise->fulfill($value); + + return $promise; + } + } + /** * Marks this promise as fulfilled and sets its return value. * @@ -144,7 +174,7 @@ public function fulfill($value = null) /** * Marks this promise as rejected, and set it's rejection reason. */ - public function reject(Throwable $reason) + public function reject($reason) { if (self::PENDING !== $this->state) { throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); @@ -156,6 +186,29 @@ public function reject(Throwable $reason) } } + public function cancel() + { + if (self::PENDING !== $this->state) { + return; + } + + if ($this->cancelFn) { + $fn = $this->cancelFn; + $this->cancelFn = null; + try { + $fn($this->value); + } catch (\Throwable $e) { + $this->reject($e); + } catch (\Exception $e) { + $this->reject($e); + } + } + + // Reject the promise only if it wasn't rejected in a then callback. + if (self::PENDING === $this->state) { + $this->reject(new \Exception('Promise has been cancelled')); + } + } /** * Stops execution until this promise is resolved. * @@ -209,6 +262,7 @@ public function wait() * @var mixed */ protected $value = null; + protected $cancelFn = null; /** * This method is used to call either an onFulfilled or onRejected callback. diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index 9e8b3d0..6758cb3 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -228,6 +228,54 @@ public function testWaitRejectedScalar() } ////////////////////////////////// + public function testForwardsRejectedPromisesDownChainBetweenGaps() + { + $p = new Promise(); + $r = $r2 = null; + $p->then(null, null) + ->then(null, function ($v) use (&$r) { $r = $v; return $v . '2'; }) + ->then(function ($v) use (&$r2) { $r2 = $v; }); + $p->reject('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertEquals('foo2', $r2); + } + + public function testForwardsThrownPromisesDownChainBetweenGaps() + { + $e = new \Exception(); + $p = new Promise(); + $r = $r2 = null; + $p->then(null, null) + ->then(null, function ($v) use (&$r, $e) { + $r = $v; + throw $e; + }) + ->then( + null, + function ($v) use (&$r2) { $r2 = $v; } + ); + $p->reject('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertSame($e, $r2); + } + + public function testForwardsHandlersWhenRejectedPromiseIsReturned() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->reject('foo'); + $p2->then(null, function ($v) use (&$res) { $res[] = 'A:' . $v; }); + $p->then(null, function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) + ->then(null, function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->reject('a'); + $p->then(null, function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); + } + public function testForwardsFulfilledDownChainBetweenGaps() { $p = new Promise(); @@ -271,4 +319,19 @@ public function testForwardsHandlersWhenFulfilledPromiseIsReturned() Loop\run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } + + public function testDoesNotForwardRejectedPromise() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->cancel(); + $p2->then(function ($v) use (&$res) { $res[] = "B:$v"; return $v; }); + $p->then(function ($v) use ($p2, &$res) { $res[] = "B:$v"; return $p2; }) + ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->resolve('a'); + $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['B:a', 'D:a'], $res); + } } From dce13a226044997031c534b3d03679ded5ae4ca7 Mon Sep 17 00:00:00 2001 From: techno-express Date: Thu, 6 Dec 2018 10:12:01 -0500 Subject: [PATCH 03/23] test changes to make promise cancellable like guzzle's --- lib/CancellationException.php | 12 ++++++++ lib/Promise.php | 30 ++++++++------------ lib/RejectionException.php | 53 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 lib/CancellationException.php create mode 100644 lib/RejectionException.php diff --git a/lib/CancellationException.php b/lib/CancellationException.php new file mode 100644 index 0000000..43aa7c0 --- /dev/null +++ b/lib/CancellationException.php @@ -0,0 +1,12 @@ +waitFn = is_callable($callDone) ? $callDone : null; - $this->cancelFn = is_callable($callFail) - ? function($reason = null) use($callFail) { - try - { - return $callFail && ($result = $callFail($reason)) instanceof self ? $result : $this; - } - catch (Error $ex) - {} - catch (Exception $ex) - {} - return $this; - } - : null; + $this->cancelFn = is_callable($callFail) ? $callFail : null; if (is_callable($callDone)) { $callDone( @@ -145,9 +134,9 @@ public function otherwise(callable $onRejected): Promise public function resolve($value): Promise { if ($value instanceof Promise) { - return $value->then(); + return $value->then(null, [$this, 'cancel']); } else { - $promise = new Promise(); + $promise = new Promise(null, [$this, 'cancel']); $promise->fulfill($value); return $promise; @@ -191,22 +180,25 @@ public function cancel() if (self::PENDING !== $this->state) { return; } + + Loop\stop(); + $this->subscribers = []; if ($this->cancelFn) { $fn = $this->cancelFn; $this->cancelFn = null; try { - $fn($this->value); + $fn(); } catch (\Throwable $e) { $this->reject($e); - } catch (\Exception $e) { - $this->reject($e); + } catch (\Exception $exception) { + $this->reject($exception); } } // Reject the promise only if it wasn't rejected in a then callback. if (self::PENDING === $this->state) { - $this->reject(new \Exception('Promise has been cancelled')); + $this->reject(new CancellationException('Promise has been cancelled')); } } /** diff --git a/lib/RejectionException.php b/lib/RejectionException.php new file mode 100644 index 0000000..a886b13 --- /dev/null +++ b/lib/RejectionException.php @@ -0,0 +1,53 @@ +reason = $reason; + + $message = 'The promise was rejected'; + + if ($description) { + $message .= ' with reason: ' . $description; + } elseif (is_string($reason) + || (is_object($reason) && method_exists($reason, '__toString')) + ) { + $message .= ' with reason: ' . $this->reason; + } elseif ($reason instanceof \JsonSerializable) { + $message .= ' with reason: ' + . json_encode($this->reason, JSON_PRETTY_PRINT); + } + + parent::__construct($message); + } + + /** + * Returns the rejection reason. + * + * @return mixed + */ + public function getReason() + { + return $this->reason; + } + + public function wait($unwrap = true) + { + return $unwrap ? $this->reason : null; + } +} From 30f9b496704c84ca976aac85790af3bc4c17954a Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 6 Dec 2018 10:14:05 -0500 Subject: [PATCH 04/23] fix phpunit v7.4.5 warning --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ccd59be..8b5a5fb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,6 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" - strict="true" > tests/ From 3cacf4d47a7e4686c3db08e9a269fcb9eb02fb19 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 6 Dec 2018 10:28:22 -0500 Subject: [PATCH 05/23] promises cancellable --- lib/CancellationException.php | 12 -------- lib/Promise.php | 17 ++++------- lib/RejectionException.php | 53 ----------------------------------- 3 files changed, 6 insertions(+), 76 deletions(-) delete mode 100644 lib/CancellationException.php delete mode 100644 lib/RejectionException.php diff --git a/lib/CancellationException.php b/lib/CancellationException.php deleted file mode 100644 index 43aa7c0..0000000 --- a/lib/CancellationException.php +++ /dev/null @@ -1,12 +0,0 @@ -then(null, $onRejected); } - public function resolve($value): Promise + public function resolve($value) { if ($value instanceof Promise) { - return $value->then(null, [$this, 'cancel']); - } else { - $promise = new Promise(null, [$this, 'cancel']); - $promise->fulfill($value); - - return $promise; - } + return $value->then(); + } + + return $this->fulfill($value); } /** @@ -181,7 +177,6 @@ public function cancel() return; } - Loop\stop(); $this->subscribers = []; if ($this->cancelFn) { @@ -198,7 +193,7 @@ public function cancel() // Reject the promise only if it wasn't rejected in a then callback. if (self::PENDING === $this->state) { - $this->reject(new CancellationException('Promise has been cancelled')); + $this->reject(new \Exception('Promise has been cancelled')); } } /** diff --git a/lib/RejectionException.php b/lib/RejectionException.php deleted file mode 100644 index a886b13..0000000 --- a/lib/RejectionException.php +++ /dev/null @@ -1,53 +0,0 @@ -reason = $reason; - - $message = 'The promise was rejected'; - - if ($description) { - $message .= ' with reason: ' . $description; - } elseif (is_string($reason) - || (is_object($reason) && method_exists($reason, '__toString')) - ) { - $message .= ' with reason: ' . $this->reason; - } elseif ($reason instanceof \JsonSerializable) { - $message .= ' with reason: ' - . json_encode($this->reason, JSON_PRETTY_PRINT); - } - - parent::__construct($message); - } - - /** - * Returns the rejection reason. - * - * @return mixed - */ - public function getReason() - { - return $this->reason; - } - - public function wait($unwrap = true) - { - return $unwrap ? $this->reason : null; - } -} From a5d757e6f447e5632014b617244d75bac3b02eb8 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 6 Dec 2018 12:34:02 -0500 Subject: [PATCH 06/23] Make promise interpolatable with other event loop implementations, React, Guzzle, AMP. --- lib/Promise.php | 69 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/lib/Promise.php b/lib/Promise.php index 7d9b42d..9715fe9 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -6,6 +6,7 @@ use Exception; use Throwable; +use Sabre\Event\Loop; /** * An implementation of the Promise pattern. @@ -46,6 +47,8 @@ class Promise * @var int */ public $state = self::PENDING; + + public $loop = null; /** * Creates the promise. @@ -57,21 +60,43 @@ class Promise * Using the executor is optional. */ public function __construct( ...$executor) - { - $callDone = isset($executor[0]) ? $executor[0] : null; - $callFail = isset($executor[1]) ? $executor[1] : null; + { + $callExecutor = isset($executor[0]) ? $executor[0] : null; + $childLoop = $this->checkLoopInstance($callExecutor) ? $callExecutor : null; + $callExecutor = $this->checkLoopInstance($callExecutor) ? null : $callExecutor; + + $callCanceller = isset($executor[1]) ? $executor[1] : null; + $childLoop = $this->checkLoopInstance($callCanceller) ? $callCanceller : $childLoop; + $callCanceller = $this->checkLoopInstance($callCanceller) ? null : $callCanceller; + + $loop = isset($executor[2]) ? $executor[2] : null; + $childLoop = $this->checkLoopInstance($loop) ? $loop : $childLoop; + $this->loop = $this->checkLoopInstance($childLoop) ? $childLoop : Loop\instance(); - $this->waitFn = is_callable($callDone) ? $callDone : null; - $this->cancelFn = is_callable($callFail) ? $callFail : null; + $this->waitFn = is_callable($callExecutor) ? $callExecutor : null; + $this->cancelFn = is_callable($callCanceller) ? $callCanceller : null; - if (is_callable($callDone)) { - $callDone( + if (is_callable($callExecutor) && $this->loop) { + $callExecutor( [$this, 'fulfill'], [$this, 'reject'] ); } } + private function checkLoopInstance($instance = null): bool + { + $isInstanceiable = false; + if ($instance instanceof TaskQueueInterface) + $isInstanceiable = true; + elseif ($instance instanceof LoopInterface) + $isInstanceiable = true; + elseif ($instance instanceof Loop) + $isInstanceiable = true; + + return $isInstanceiable; + } + /** * This method allows you to specify the callback that will be called after * the promise has been fulfilled or rejected. @@ -177,6 +202,7 @@ public function cancel() return; } + $this->waitFn = null; $this->subscribers = []; if ($this->cancelFn) { @@ -250,6 +276,7 @@ public function wait() */ protected $value = null; protected $cancelFn = null; + protected $waitFn = null; /** * This method is used to call either an onFulfilled or onRejected callback. @@ -268,7 +295,7 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) // passed to 'then'. // // This makes the order of execution more predictable. - Loop\nextTick(function () use ($callBack, $subPromise) { + $promiseFunction = function() use ($callBack, $subPromise) { if (is_callable($callBack)) { try { $result = $callBack($this->value); @@ -295,6 +322,30 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) $subPromise->reject($this->value); } } - }); + }; + + $this->implement($promiseFunction, $subPromise); } + + public function implement(callable $function, Promise $promise = null) + { + if ($this->loop) { + $loop = $this->loop; + + $othersLoop = method_exists($loop, 'futureTick') ? [$loop, 'futureTick'] : null; + $othersLoop = method_exists($loop, 'addTick') ? [$loop, 'addTick'] : $othersLoop; + $othersLoop = method_exists($loop, 'onTick') ? [$loop, 'onTick'] : $othersLoop; + $othersLoop = method_exists($loop, 'enqueue') ? [$loop, 'enqueue'] : $othersLoop; + $othersLoop = method_exists($loop, 'add') ? [$loop, 'add'] : $othersLoop; + + if ($othersLoop) + call_user_func_array($othersLoop, $function); + else + $loop->nextTick($function); + } else { + return $function(); + } + + return $promise; + } } From 0c82d0776eccb0c48b39fba863ebfea9376d1da5 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 6 Dec 2018 15:52:30 -0500 Subject: [PATCH 07/23] Tests addding some Guzzle wait and cancel tests --- lib/CancellationException.php | 12 ++ lib/Promise.php | 23 ++- lib/RejectionException.php | 53 ++++++ tests/WaitPromiseTest.php | 344 ++++++++++++++++++++++++++++++++++ 4 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 lib/CancellationException.php create mode 100644 lib/RejectionException.php create mode 100644 tests/WaitPromiseTest.php diff --git a/lib/CancellationException.php b/lib/CancellationException.php new file mode 100644 index 0000000..43aa7c0 --- /dev/null +++ b/lib/CancellationException.php @@ -0,0 +1,12 @@ +state; + } + /** * This method allows you to specify the callback that will be called after * the promise has been fulfilled or rejected. @@ -155,7 +161,7 @@ public function otherwise(callable $onRejected): Promise return $this->then(null, $onRejected); } - public function resolve($value) + public function resolve($value = null) { if ($value instanceof Promise) { return $value->then(); @@ -181,6 +187,13 @@ public function fulfill($value = null) } } +public function rejector($reason = null) +{ + $promise = new Promise(); + $promise->reject($reason); + + return $promise; +} /** * Marks this promise as rejected, and set it's rejection reason. */ @@ -210,16 +223,16 @@ public function cancel() $this->cancelFn = null; try { $fn(); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->reject($e); - } catch (\Exception $exception) { + } catch (Exception $exception) { $this->reject($exception); } } // Reject the promise only if it wasn't rejected in a then callback. if (self::PENDING === $this->state) { - $this->reject(new \Exception('Promise has been cancelled')); + $this->reject(new CancellationException('Promise has been cancelled')); } } /** @@ -314,6 +327,8 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) // If the event handler threw an exception, we need to make sure that // the chained promise is rejected as well. $subPromise->reject($e); + } catch (Exception $exception) { + $subPromise->reject($exception); } } else { if (self::FULFILLED === $this->state) { diff --git a/lib/RejectionException.php b/lib/RejectionException.php new file mode 100644 index 0000000..a886b13 --- /dev/null +++ b/lib/RejectionException.php @@ -0,0 +1,53 @@ +reason = $reason; + + $message = 'The promise was rejected'; + + if ($description) { + $message .= ' with reason: ' . $description; + } elseif (is_string($reason) + || (is_object($reason) && method_exists($reason, '__toString')) + ) { + $message .= ' with reason: ' . $this->reason; + } elseif ($reason instanceof \JsonSerializable) { + $message .= ' with reason: ' + . json_encode($this->reason, JSON_PRETTY_PRINT); + } + + parent::__construct($message); + } + + /** + * Returns the rejection reason. + * + * @return mixed + */ + public function getReason() + { + return $this->reason; + } + + public function wait($unwrap = true) + { + return $unwrap ? $this->reason : null; + } +} diff --git a/tests/WaitPromiseTest.php b/tests/WaitPromiseTest.php new file mode 100644 index 0000000..095d59d --- /dev/null +++ b/tests/WaitPromiseTest.php @@ -0,0 +1,344 @@ +resolve('foo'); + $p->resolve('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->resolve('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectNonPendingPromise() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanRejectWithSameValue() + { + $p = new Promise(); + $p->reject('foo'); + $p->reject('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('foo'); + } + + public function testInvokesWaitFunction() + { + $p = new Promise(function () use (&$p) { $p->resolve('10'); }); + $this->assertEquals('10', $p->wait()); + } + + /** + * @expectedException \Exception + */ + public function testRejectsAndThrowsWhenWaitFailsToResolve() + { + $p = new Promise(function () {}); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithNonException() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithException() + { + $e = new \Exception('foo'); + $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); + $p->wait(); + } + + public function testDoesNotUnwrapExceptionsWhenDisabled() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $this->assertEquals(Promise::PENDING, $p->getState()); + $p->wait(false); + $this->assertEquals(Promise::REJECTED, $p->getState()); + } + + public function testRejectsSelfWhenWaitThrows() + { + $e = new \Exception('foo'); + $p = new Promise(function () use ($e) { throw $e; }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals(Promise::REJECTED, $p->getState()); + } + } + + public function testWaitsOnNestedPromises() + { + $p = new Promise(function () use (&$p) { $p->resolve('_'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('foo'); }); + $p3 = $p->then(function () use ($p2) { return $p2; }); + $this->assertSame('foo', $p3->wait()); + } + + /** + * @expectedException \Exception + */ + public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() + { + $p = new Promise(); + $p->wait(); + } + + public function testThrowsWaitExceptionAfterPromiseIsResolved() + { + $p = new Promise(function () use (&$p) { + $p->reject('Foo!'); + throw new \Exception('Bar?'); + }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('Bar?', $e->getMessage()); + } + } + + public function testGetsActualWaitValueFromThen() + { + $p = new Promise(function () use (&$p) { $p->reject('Foo!'); }); + $p2 = $p->then(null, function ($reason) { + return new RejectedPromise([$reason]); + }); + try { + $p2->wait(); + $this->fail('Should have thrown'); + } catch (RejectionException $e) { + $this->assertEquals(['Foo!'], $e->getReason()); + } + } + + public function testWaitBehaviorIsBasedOnLastPromiseInChain() + { + $p3 = new Promise(function () use (&$p3) { $p3->resolve('Whoop'); }); + $p2 = new Promise(function () use (&$p2, $p3) { $p2->reject($p3); }); + $p = new Promise(function () use (&$p, $p2) { $p->reject($p2); }); + $this->assertEquals('Whoop', $p->wait()); + } + + public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped() + { + $p2 = new Promise(function () use (&$p2) { + $p2->reject('Fail'); + }); + $p = new Promise(function () use ($p2, &$p) { + $p->resolve($p2); + }); + $p->wait(false); + $this->assertSame(Promise::REJECTED, $p2->getState()); + } + + public function testCannotCancelNonPending() + { + $p = new Promise(); + $p->resolve('foo'); + $p->cancel(); + $this->assertEquals(Promise::FULFILLED, $p->getState()); + } + + /** + * @expectedException \Exception + */ + public function testCancelsPromiseWhenNoCancelFunction() + { + $p = new Promise(); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $p->wait(); + } + + public function testCancelsPromiseWithCancelFunction() + { + $called = false; + $p = new Promise(null, function () use (&$called) { $called = true; }); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $this->assertTrue($called); + } + + public function testCancelsUppermostPendingPromise() + { + $called = false; + $p1 = new Promise(null, function () use (&$called) { $called = true; }); + $p2 = $p1->then(function () {}); + $p3 = $p2->then(function () {}); + $p4 = $p3->then(function () {}); + $p3->cancel(); + $this->assertEquals(Promise::REJECTED, $p1->getState()); + $this->assertEquals(Promise::REJECTED, $p2->getState()); + $this->assertEquals(Promise::REJECTED, $p3->getState()); + $this->assertEquals(Promise::PENDING, $p4->getState()); + $this->assertTrue($called); + try { + $p3->wait(); + $this->fail(); + } catch (CancellationException $e) { + $this->assertContains('cancelled', $e->getMessage()); + } + try { + $p4->wait(); + $this->fail(); + } catch (CancellationException $e) { + $this->assertContains('cancelled', $e->getMessage()); + } + $this->assertEquals(Promise::REJECTED, $p4->getState()); + } + + public function testCancelsChildPromises() + { + $called1 = $called2 = $called3 = false; + $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); + $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); + $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); + $p4 = $p2->then(function () use ($p3) { return $p3; }); + $p5 = $p4->then(function () { $this->fail(); }); + $p4->cancel(); + $this->assertEquals(Promise::PENDING, $p1->getState()); + $this->assertEquals(Promise::REJECTED, $p2->getState()); + $this->assertEquals(Promise::REJECTED, $p4->getState()); + $this->assertEquals(Promise::PENDING, $p5->getState()); + $this->assertFalse($called1); + $this->assertTrue($called2); + $this->assertFalse($called3); + } + + public function testRejectsPromiseWhenCancelFails() + { + $called = false; + $p = new Promise(null, function () use (&$called) { + $called = true; + throw new \Exception('e'); + }); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $this->assertTrue($called); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('e', $e->getMessage()); + } + } + + + public function testCreatesPromiseWhenRejectedWithNoCallback() + { + $p = new Promise(); + $p->reject('foo'); + $p2 = $p->then(); + $this->assertNotSame($p, $p2); + $this->assertInstanceOf(Promise::class, $p2); + } + + public function testInvokesWaitFnsForThens() + { + $p = new Promise(function () use (&$p) { $p->resolve('a'); }); + $p2 = $p + ->then(function ($v) { return $v . '-1-'; }) + ->then(function ($v) { return $v . '2'; }); + $this->assertEquals('a-1-2', $p2->wait()); + } + + public function testStacksThenWaitFunctions() + { + $p1 = new Promise(function () use (&$p1) { $p1->resolve('a'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); + $p3 = new Promise(function () use (&$p3) { $p3->resolve('c'); }); + $p4 = $p1 + ->then(function () use ($p2) { return $p2; }) + ->then(function () use ($p3) { return $p3; }); + $this->assertEquals('c', $p4->wait()); + } + + public function testRemovesReferenceFromChildWhenParentWaitedUpon() + { + $r = null; + $p = new Promise(function () use (&$p) { $p->resolve('a'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); + $pb = $p->then( + function ($v) use ($p2, &$r) { + $r = $v; + return $p2; + }) + ->then(function ($v) { return $v . '.'; }); + $this->assertEquals('a', $p->wait()); + $this->assertEquals('b', $p2->wait()); + $this->assertEquals('b.', $pb->wait()); + $this->assertEquals('a', $r); + } + + public function testDoesNotBlowStackWhenWaitingOnNestedThens() + { + $inner = new Promise(function () use (&$inner) { $inner->resolve(0); }); + $prev = $inner; + for ($i = 1; $i < 100; $i++) { + $prev = $prev->then(function ($i) { return $i + 1; }); + } + $parent = new Promise(function () use (&$parent, $prev) { + $parent->resolve($prev); + }); + $this->assertEquals(99, $parent->wait()); + } + +} From 54ea0d57551e2d0b9c1868789f778adf94f496f1 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Thu, 6 Dec 2018 16:55:02 -0500 Subject: [PATCH 08/23] moved not working Guzzle tests to separate file --- lib/Promise.php | 45 +++-- ...miseTest.php => NotWorkingPromiseTest.php} | 176 +----------------- tests/Promise/PromiseTest.php | 171 +++++++++++++++++ 3 files changed, 201 insertions(+), 191 deletions(-) rename tests/{WaitPromiseTest.php => NotWorkingPromiseTest.php} (52%) diff --git a/lib/Promise.php b/lib/Promise.php index 3723df4..798864e 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -250,26 +250,33 @@ public function cancel() */ public function wait() { - $hasEvents = true; - while (self::PENDING === $this->state) { - if (!$hasEvents) { - throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); - } - - // As long as the promise is not fulfilled, we tell the event loop - // to handle events, and to block. - $hasEvents = Loop\tick(true); - } + if ($this->waitFn && !$this->loop) { + $fn = $this->waitFn; + $this->waitFn = null; + $fn([$this, 'fulfill'], [$this, 'reject']); + $this->loop->run(); + } else { + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } - if (self::FULFILLED === $this->state) { - // If the state of this promise is fulfilled, we can return the value. - return $this->value; - } else { - // If we got here, it means that the asynchronous operation - // errored. Therefore we need to throw an exception. - throw $this->value; - } - } + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); + } + } + + if (self::FULFILLED === $this->state) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + throw $this->value; + } + } /** * A list of subscribers. Subscribers are the callbacks that want us to let diff --git a/tests/WaitPromiseTest.php b/tests/NotWorkingPromiseTest.php similarity index 52% rename from tests/WaitPromiseTest.php rename to tests/NotWorkingPromiseTest.php index 095d59d..46965a4 100644 --- a/tests/WaitPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -5,104 +5,18 @@ use Exception; use Sabre\Event\Loop; use Sabre\Event\Promise; +use Sabre\Event\RejectionException; use Sabre\Event\CancellationException; use Sabre\Event\PromiseAlreadyResolvedException; use PHPUnit\Framework\TestCase; -class WaitPromiseTest extends TestCase +class NotWorkingPromiseTest extends TestCase { - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotResolveNonPendingPromise() - { - $p = new Promise(); - $p->resolve('foo'); - $p->resolve('bar'); - $this->assertEquals('foo', $p->wait()); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCanResolveWithSameValue() - { - $p = new Promise(); - $p->resolve('foo'); - $p->resolve('foo'); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotRejectNonPendingPromise() - { - $p = new Promise(); - $p->resolve('foo'); - $p->reject('bar'); - $this->assertEquals('foo', $p->wait()); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCanRejectWithSameValue() - { - $p = new Promise(); - $p->reject('foo'); - $p->reject('foo'); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotRejectResolveWithSameValue() - { - $p = new Promise(); - $p->resolve('foo'); - $p->reject('foo'); - } - public function testInvokesWaitFunction() { $p = new Promise(function () use (&$p) { $p->resolve('10'); }); $this->assertEquals('10', $p->wait()); - } - - /** - * @expectedException \Exception - */ - public function testRejectsAndThrowsWhenWaitFailsToResolve() - { - $p = new Promise(function () {}); - $p->wait(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithNonException() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $p->wait(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithException() - { - $e = new \Exception('foo'); - $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); - $p->wait(); - } + } public function testDoesNotUnwrapExceptionsWhenDisabled() { @@ -132,14 +46,6 @@ public function testWaitsOnNestedPromises() $this->assertSame('foo', $p3->wait()); } - /** - * @expectedException \Exception - */ - public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() - { - $p = new Promise(); - $p->wait(); - } public function testThrowsWaitExceptionAfterPromiseIsResolved() { @@ -189,34 +95,6 @@ public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped() $this->assertSame(Promise::REJECTED, $p2->getState()); } - public function testCannotCancelNonPending() - { - $p = new Promise(); - $p->resolve('foo'); - $p->cancel(); - $this->assertEquals(Promise::FULFILLED, $p->getState()); - } - - /** - * @expectedException \Exception - */ - public function testCancelsPromiseWhenNoCancelFunction() - { - $p = new Promise(); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $p->wait(); - } - - public function testCancelsPromiseWithCancelFunction() - { - $called = false; - $p = new Promise(null, function () use (&$called) { $called = true; }); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $this->assertTrue($called); - } - public function testCancelsUppermostPendingPromise() { $called = false; @@ -243,53 +121,7 @@ public function testCancelsUppermostPendingPromise() $this->assertContains('cancelled', $e->getMessage()); } $this->assertEquals(Promise::REJECTED, $p4->getState()); - } - - public function testCancelsChildPromises() - { - $called1 = $called2 = $called3 = false; - $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); - $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); - $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); - $p4 = $p2->then(function () use ($p3) { return $p3; }); - $p5 = $p4->then(function () { $this->fail(); }); - $p4->cancel(); - $this->assertEquals(Promise::PENDING, $p1->getState()); - $this->assertEquals(Promise::REJECTED, $p2->getState()); - $this->assertEquals(Promise::REJECTED, $p4->getState()); - $this->assertEquals(Promise::PENDING, $p5->getState()); - $this->assertFalse($called1); - $this->assertTrue($called2); - $this->assertFalse($called3); - } - - public function testRejectsPromiseWhenCancelFails() - { - $called = false; - $p = new Promise(null, function () use (&$called) { - $called = true; - throw new \Exception('e'); - }); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $this->assertTrue($called); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals('e', $e->getMessage()); - } - } - - - public function testCreatesPromiseWhenRejectedWithNoCallback() - { - $p = new Promise(); - $p->reject('foo'); - $p2 = $p->then(); - $this->assertNotSame($p, $p2); - $this->assertInstanceOf(Promise::class, $p2); - } + } public function testInvokesWaitFnsForThens() { diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index 6758cb3..e0aaa60 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -334,4 +334,175 @@ public function testDoesNotForwardRejectedPromise() Loop\run(); $this->assertEquals(['B:a', 'D:a'], $res); } + /////////////////////// + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotResolveNonPendingPromise() + { + $p = new Promise(); + $p->resolve('foo'); + $p->resolve('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->resolve('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectNonPendingPromise() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanRejectWithSameValue() + { + $p = new Promise(); + $p->reject('foo'); + $p->reject('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('foo'); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage There were no more events in the loop. This promise will never be fulfilled. + */ + public function testRejectsAndThrowsWhenWaitFailsToResolve() + { + $p = new Promise(function () {}); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithNonException() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithException() + { + $e = new \Exception('foo'); + $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); + $p->wait(); + } + + /** + * @expectedException \Exception + */ + public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() + { + $p = new Promise(); + $p->wait(); + } + + public function testCannotCancelNonPending() + { + $p = new Promise(); + $p->resolve('foo'); + $p->cancel(); + $this->assertEquals(Promise::FULFILLED, $p->getState()); + } + + /** + * @expectedException \Exception + */ + public function testCancelsPromiseWhenNoCancelFunction() + { + $p = new Promise(); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $p->wait(); + } + + public function testCancelsPromiseWithCancelFunction() + { + $called = false; + $p = new Promise(null, function () use (&$called) { $called = true; }); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $this->assertTrue($called); + } + + public function testCancelsChildPromises() + { + $called1 = $called2 = $called3 = false; + $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); + $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); + $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); + $p4 = $p2->then(function () use ($p3) { return $p3; }); + $p5 = $p4->then(function () { $this->fail(); }); + $p4->cancel(); + $this->assertEquals(Promise::PENDING, $p1->getState()); + $this->assertEquals(Promise::REJECTED, $p2->getState()); + $this->assertEquals(Promise::REJECTED, $p4->getState()); + $this->assertEquals(Promise::PENDING, $p5->getState()); + $this->assertFalse($called1); + $this->assertTrue($called2); + $this->assertFalse($called3); + } + + public function testRejectsPromiseWhenCancelFails() + { + $called = false; + $p = new Promise(null, function () use (&$called) { + $called = true; + throw new \Exception('e'); + }); + $p->cancel(); + $this->assertEquals(Promise::REJECTED, $p->getState()); + $this->assertTrue($called); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('e', $e->getMessage()); + } + } + + public function testCreatesPromiseWhenRejectedWithNoCallback() + { + $p = new Promise(); + $p->reject('foo'); + $p2 = $p->then(); + $this->assertNotSame($p, $p2); + $this->assertInstanceOf(Promise::class, $p2); + } } From 348684f2bb939176f60d0449d44919fd7dd5a8d9 Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 11:05:56 -0500 Subject: [PATCH 09/23] update Guzzle advance tests not working here, all have Promises addressed by reference --- tests/NotWorkingPromiseTest.php | 37 ++++++++++++++++++++------------- tests/Promise/PromiseTest.php | 21 ------------------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php index 46965a4..bd45952 100644 --- a/tests/NotWorkingPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -12,6 +12,27 @@ class NotWorkingPromiseTest extends TestCase { + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithNonException() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithException() + { + $e = new \Exception('foo'); + $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); + $p->wait(); + } + public function testInvokesWaitFunction() { $p = new Promise(function () use (&$p) { $p->resolve('10'); }); @@ -60,21 +81,7 @@ public function testThrowsWaitExceptionAfterPromiseIsResolved() $this->assertEquals('Bar?', $e->getMessage()); } } - - public function testGetsActualWaitValueFromThen() - { - $p = new Promise(function () use (&$p) { $p->reject('Foo!'); }); - $p2 = $p->then(null, function ($reason) { - return new RejectedPromise([$reason]); - }); - try { - $p2->wait(); - $this->fail('Should have thrown'); - } catch (RejectionException $e) { - $this->assertEquals(['Foo!'], $e->getReason()); - } - } - + public function testWaitBehaviorIsBasedOnLastPromiseInChain() { $p3 = new Promise(function () use (&$p3) { $p3->resolve('Whoop'); }); diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index e0aaa60..b0f6776 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -403,27 +403,6 @@ public function testRejectsAndThrowsWhenWaitFailsToResolve() $p->wait(); } - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithNonException() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $p->wait(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithException() - { - $e = new \Exception('foo'); - $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); - $p->wait(); - } - /** * @expectedException \Exception */ From 55b6003a9215b7302ad53c4a845d5224296ee689 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Fri, 7 Dec 2018 11:28:11 -0500 Subject: [PATCH 10/23] Update for interpolatable --- lib/Promise.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Promise.php b/lib/Promise.php index 798864e..030e8a5 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -250,12 +250,12 @@ public function cancel() */ public function wait() { - if ($this->waitFn && !$this->loop) { + if ($this->waitFn && method_exists($this->loop, 'add') && method_exists($this->loop, 'run')) { $fn = $this->waitFn; $this->waitFn = null; $fn([$this, 'fulfill'], [$this, 'reject']); $this->loop->run(); - } else { + } elseif (method_exists($this->loop, 'tick')) { $hasEvents = true; while (self::PENDING === $this->state) { if (!$hasEvents) { @@ -264,7 +264,7 @@ public function wait() // As long as the promise is not fulfilled, we tell the event loop // to handle events, and to block. - $hasEvents = Loop\tick(true); + $hasEvents = $this->loop->tick(true); } } From c887c83b3e1aec233fe6be745108b07012d3d338 Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 20:23:52 -0500 Subject: [PATCH 11/23] corrections for interoperability with Guzzle promise syntax, moved what works --- lib/Promise.php | 92 ++++++++++++++++++++++++--------- tests/NotWorkingPromiseTest.php | 49 +----------------- tests/Promise/PromiseTest.php | 46 +++++++++++++++++ 3 files changed, 114 insertions(+), 73 deletions(-) diff --git a/lib/Promise.php b/lib/Promise.php index 030e8a5..b17a654 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -51,6 +51,8 @@ class Promise public $loop = null; + private $isWaitRequired = false; + /** * Creates the promise. * @@ -75,13 +77,19 @@ public function __construct( ...$executor) $this->loop = $this->checkLoopInstance($childLoop) ? $childLoop : Loop\instance(); $this->waitFn = is_callable($callExecutor) ? $callExecutor : null; - $this->cancelFn = is_callable($callCanceller) ? $callCanceller : null; - - if (is_callable($callExecutor) && $this->loop) { - $callExecutor( - [$this, 'fulfill'], - [$this, 'reject'] - ); + $this->cancelFn = is_callable($callCanceller) ? $callCanceller : null; + + try { + if (is_callable($callExecutor) && !$this->isWaitRequired) { + $callExecutor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } + } catch (\Throwable $e) { + $this->isWaitRequired = true; + } catch (\Exception $exception) { + $this->isWaitRequired = true; } } @@ -248,33 +256,67 @@ public function cancel() * * @return mixed */ - public function wait() + public function wait($unwap = true) { - if ($this->waitFn && method_exists($this->loop, 'add') && method_exists($this->loop, 'run')) { - $fn = $this->waitFn; - $this->waitFn = null; - $fn([$this, 'fulfill'], [$this, 'reject']); - $this->loop->run(); - } elseif (method_exists($this->loop, 'tick')) { - $hasEvents = true; - while (self::PENDING === $this->state) { - if (!$hasEvents) { - throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + try { + $loop = $this->loop; + $fn = $this->waitFn; + $this->waitFn = null; + if (is_callable($fn) + && method_exists($loop, 'add') + && method_exists($loop, 'run') + && $this->isWaitRequired + ) { + $this->isWaitRequired = false; + $fn([$this, 'resolve'], [$this, 'reject']); + $loop->run(); + } elseif (method_exists($loop, 'tick')) { + if (is_callable($fn) && $this->isWaitRequired) { + $this->isWaitRequired = false; + $fn([$this, 'resolve'], [$this, 'reject']); } + + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } - // As long as the promise is not fulfilled, we tell the event loop - // to handle events, and to block. - $hasEvents = $this->loop->tick(true); + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = $loop->tick(true); + } } - } + } catch (\Exception $reason) { + if ($this->state === self::PENDING) { + // The promise has not been resolved yet, so reject the promise + // with the exception. + $this->reject($reason); + } else { + // The promise was already resolved, so there's a problem in + // the application. + throw $reason; + } + } - if (self::FULFILLED === $this->state) { + $result = $this->value; + + if ($this->state === self::PENDING) { + $this->reject('Invoking the wait callback did not resolve the promise'); + } elseif (self::FULFILLED === $this->state) { // If the state of this promise is fulfilled, we can return the value. - return $this->value; + return $result; } else { // If we got here, it means that the asynchronous operation // errored. Therefore we need to throw an exception. - throw $this->value; + if ($result instanceof Exception) { + throw $result; + } elseif (is_scalar($result) && $unwap) { + throw new \Exception($result); + } elseif ($unwap) { + $type = is_object($result) ? get_class($result) : gettype($result); + throw new \Exception('Promise was rejected with reason of type: ' . $type); + } } } diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php index bd45952..4cb2e44 100644 --- a/tests/NotWorkingPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -11,54 +11,7 @@ use PHPUnit\Framework\TestCase; class NotWorkingPromiseTest extends TestCase -{ - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithNonException() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $p->wait(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithException() - { - $e = new \Exception('foo'); - $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); - $p->wait(); - } - - public function testInvokesWaitFunction() - { - $p = new Promise(function () use (&$p) { $p->resolve('10'); }); - $this->assertEquals('10', $p->wait()); - } - - public function testDoesNotUnwrapExceptionsWhenDisabled() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $this->assertEquals(Promise::PENDING, $p->getState()); - $p->wait(false); - $this->assertEquals(Promise::REJECTED, $p->getState()); - } - - public function testRejectsSelfWhenWaitThrows() - { - $e = new \Exception('foo'); - $p = new Promise(function () use ($e) { throw $e; }); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals(Promise::REJECTED, $p->getState()); - } - } - +{ public function testWaitsOnNestedPromises() { $p = new Promise(function () use (&$p) { $p->resolve('_'); }); diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index b0f6776..0882131 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -484,4 +484,50 @@ public function testCreatesPromiseWhenRejectedWithNoCallback() $this->assertNotSame($p, $p2); $this->assertInstanceOf(Promise::class, $p2); } + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithNonException() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithException() + { + $e = new \Exception('foo'); + $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); + $p->wait(); + } + + public function testInvokesWaitFunction() + { + $p = new Promise(function () use (&$p) { $p->resolve('10'); }); + $this->assertEquals('10', $p->wait()); + } + + public function testDoesNotUnwrapExceptionsWhenDisabled() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $this->assertEquals(Promise::PENDING, $p->getState()); + $p->wait(false); + $this->assertEquals(Promise::REJECTED, $p->getState()); + } + + public function testRejectsSelfWhenWaitThrows() + { + $e = new \Exception('foo'); + $p = new Promise(function () use ($e) { throw $e; }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals(Promise::REJECTED, $p->getState()); + } + } } From a1d18518c19023e38f06bc4f08d3351b9f731194 Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 20:30:38 -0500 Subject: [PATCH 12/23] moving guzzle based promise tests that now works here --- tests/NotWorkingPromiseTest.php | 34 +-------------------------------- tests/Promise/PromiseTest.php | 31 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php index 4cb2e44..c02343f 100644 --- a/tests/NotWorkingPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -18,22 +18,7 @@ public function testWaitsOnNestedPromises() $p2 = new Promise(function () use (&$p2) { $p2->resolve('foo'); }); $p3 = $p->then(function () use ($p2) { return $p2; }); $this->assertSame('foo', $p3->wait()); - } - - - public function testThrowsWaitExceptionAfterPromiseIsResolved() - { - $p = new Promise(function () use (&$p) { - $p->reject('Foo!'); - throw new \Exception('Bar?'); - }); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals('Bar?', $e->getMessage()); - } - } + } public function testWaitBehaviorIsBasedOnLastPromiseInChain() { @@ -102,23 +87,6 @@ public function testStacksThenWaitFunctions() ->then(function () use ($p3) { return $p3; }); $this->assertEquals('c', $p4->wait()); } - - public function testRemovesReferenceFromChildWhenParentWaitedUpon() - { - $r = null; - $p = new Promise(function () use (&$p) { $p->resolve('a'); }); - $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); - $pb = $p->then( - function ($v) use ($p2, &$r) { - $r = $v; - return $p2; - }) - ->then(function ($v) { return $v . '.'; }); - $this->assertEquals('a', $p->wait()); - $this->assertEquals('b', $p2->wait()); - $this->assertEquals('b.', $pb->wait()); - $this->assertEquals('a', $r); - } public function testDoesNotBlowStackWhenWaitingOnNestedThens() { diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index 0882131..c2d556f 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -530,4 +530,35 @@ public function testRejectsSelfWhenWaitThrows() $this->assertEquals(Promise::REJECTED, $p->getState()); } } + + public function testThrowsWaitExceptionAfterPromiseIsResolved() + { + $p = new Promise(function () use (&$p) { + $p->reject('Foo!'); + throw new \Exception('Bar?'); + }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('Bar?', $e->getMessage()); + } + } + + public function testRemovesReferenceFromChildWhenParentWaitedUpon() + { + $r = null; + $p = new Promise(function () use (&$p) { $p->resolve('a'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); + $pb = $p->then( + function ($v) use ($p2, &$r) { + $r = $v; + return $p2; + }) + ->then(function ($v) { return $v . '.'; }); + $this->assertEquals('a', $p->wait()); + $this->assertEquals('b', $p2->wait()); + $this->assertEquals('b.', $pb->wait()); + $this->assertEquals('a', $r); + } } From 2e31d009969d8be731f0a60ca409f09bd536d9d1 Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 23:10:13 -0500 Subject: [PATCH 13/23] some corrections, mostly functionaly compitable with Guzzle, not sure why some tests in NotWorkingPromiseTest .php still fails --- lib/Promise.php | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/Promise.php b/lib/Promise.php index b17a654..315aea3 100644 --- a/lib/Promise.php +++ b/lib/Promise.php @@ -195,13 +195,6 @@ public function fulfill($value = null) } } -public function rejector($reason = null) -{ - $promise = new Promise(); - $promise->reject($reason); - - return $promise; -} /** * Marks this promise as rejected, and set it's rejection reason. */ @@ -256,7 +249,7 @@ public function cancel() * * @return mixed */ - public function wait($unwap = true) + public function wait($unwrap = true) { try { $loop = $this->loop; @@ -268,16 +261,16 @@ public function wait($unwap = true) && $this->isWaitRequired ) { $this->isWaitRequired = false; - $fn([$this, 'resolve'], [$this, 'reject']); + $fn([$this, 'fulfill'], [$this, 'reject']); $loop->run(); - } elseif (method_exists($loop, 'tick')) { + } elseif (method_exists($loop, 'tick')) { if (is_callable($fn) && $this->isWaitRequired) { $this->isWaitRequired = false; - $fn([$this, 'resolve'], [$this, 'reject']); - } + $fn([$this, 'fulfill'], [$this, 'reject']); + } $hasEvents = true; - while (self::PENDING === $this->state) { + while (self::PENDING === $this->state) { if (!$hasEvents) { throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); } @@ -299,7 +292,9 @@ public function wait($unwap = true) } } - $result = $this->value; + $result = $this->value instanceof Promise + ? $this->value->wait() + : $this->value; if ($this->state === self::PENDING) { $this->reject('Invoking the wait callback did not resolve the promise'); @@ -311,9 +306,9 @@ public function wait($unwap = true) // errored. Therefore we need to throw an exception. if ($result instanceof Exception) { throw $result; - } elseif (is_scalar($result) && $unwap) { + } elseif (is_scalar($result) && $unwrap) { throw new \Exception($result); - } elseif ($unwap) { + } elseif ($unwrap) { $type = is_object($result) ? get_class($result) : gettype($result); throw new \Exception('Promise was rejected with reason of type: ' . $type); } From 6d78281a95485b1a680d0ccc64d6fe610d19ecd5 Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 23:28:48 -0500 Subject: [PATCH 14/23] Update NotWorkingPromiseTest.php --- tests/NotWorkingPromiseTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php index c02343f..d108703 100644 --- a/tests/NotWorkingPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -12,6 +12,12 @@ class NotWorkingPromiseTest extends TestCase { + protected function setUp() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + } + } + public function testWaitsOnNestedPromises() { $p = new Promise(function () use (&$p) { $p->resolve('_'); }); From 3a8375d0ed4f2754e9f23016e0d739393989d71b Mon Sep 17 00:00:00 2001 From: techno-express Date: Fri, 7 Dec 2018 23:30:00 -0500 Subject: [PATCH 15/23] bug/error fix --- tests/NotWorkingPromiseTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php index d108703..bea5d69 100644 --- a/tests/NotWorkingPromiseTest.php +++ b/tests/NotWorkingPromiseTest.php @@ -15,7 +15,6 @@ class NotWorkingPromiseTest extends TestCase protected function setUp() { $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); - } } public function testWaitsOnNestedPromises() From 4168d2a151585f001080646890b1c5412752b568 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Sat, 8 Dec 2018 11:26:15 -0500 Subject: [PATCH 16/23] Added phpstan/phpstan composer -dev requirement, Moved added tests to general test file for Promise compatibility, mark failing as skipped. --- .gitignore | 6 + composer.json | 3 +- tests/InteroperabilityPromiseTest.php | 455 ++++++++++++++++++++++++++ tests/NotWorkingPromiseTest.php | 109 ------ tests/Promise/PromiseTest.php | 335 ------------------- 5 files changed, 463 insertions(+), 445 deletions(-) create mode 100644 tests/InteroperabilityPromiseTest.php delete mode 100644 tests/NotWorkingPromiseTest.php diff --git a/.gitignore b/.gitignore index 8e8d8a9..d65f1e6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,10 @@ bin/phpunit #development stuff tests/cov .php_cs.cache + +#phpstan/phpstan bin/phpunit.bat +bin/phpstan.bat +bin/phpstan +bin/php-parse.bat +bin/php-parse diff --git a/composer.json b/composer.json index 70fe9ab..2be488d 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ ] }, "require-dev": { - "phpunit/phpunit" : ">=6" + "phpunit/phpunit" : ">=6", + "phpstan/phpstan": "^0.10.6" }, "config" : { "bin-dir" : "bin/" diff --git a/tests/InteroperabilityPromiseTest.php b/tests/InteroperabilityPromiseTest.php new file mode 100644 index 0000000..9fc865b --- /dev/null +++ b/tests/InteroperabilityPromiseTest.php @@ -0,0 +1,455 @@ +then(null, null) + ->then(null, function ($v) use (&$r) { $r = $v; return $v . '2'; }) + ->then(function ($v) use (&$r2) { $r2 = $v; }); + $p->reject('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertEquals('foo2', $r2); + } + + public function testForwardsThrownPromisesDownChainBetweenGaps() + { + $e = new \Exception(); + $p = new Promise(); + $r = $r2 = null; + $p->then(null, null) + ->then(null, function ($v) use (&$r, $e) { + $r = $v; + throw $e; + }) + ->then( + null, + function ($v) use (&$r2) { $r2 = $v; } + ); + $p->reject('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertSame($e, $r2); + } + + public function testForwardsHandlersWhenRejectedPromiseIsReturned() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->reject('foo'); + $p2->then(null, function ($v) use (&$res) { $res[] = 'A:' . $v; }); + $p->then(null, function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) + ->then(null, function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->reject('a'); + $p->then(null, function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); + } + + public function testForwardsFulfilledDownChainBetweenGaps() + { + $p = new Promise(); + $r = $r2 = null; + $p->then(null, null) + ->then(function ($v) use (&$r) {$r = $v; return $v . '2'; }) + ->then(function ($v) use (&$r2) { $r2 = $v; }); + $p->fulfill('foo'); + Loop\run(); + $this->assertEquals('foo', $r); + $this->assertEquals('foo2', $r2); + } + + public function testForwardsHandlersToNextPromise() + { + $p = new Promise(); + $p2 = new Promise(); + $resolved = null; + $p + ->then(function ($v) use ($p2) { return $p2; }) + ->then(function ($value) use (&$resolved) { $resolved = $value; }); + $p->fulfill('a'); + $p2->fulfill('b'); + Loop\run(); + $this->assertEquals('b', $resolved); + } + + public function testForwardsHandlersWhenFulfilledPromiseIsReturned() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->fulfill('foo'); + $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); + // $res is A:foo + $p + ->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) + ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->fulfill('a'); + $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); + } + + public function testDoesNotForwardRejectedPromise() + { + $res = []; + $p = new Promise(); + $p2 = new Promise(); + $p2->cancel(); + $p2->then(function ($v) use (&$res) { $res[] = "B:$v"; return $v; }); + $p->then(function ($v) use ($p2, &$res) { $res[] = "B:$v"; return $p2; }) + ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); + $p->resolve('a'); + $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); + Loop\run(); + $this->assertEquals(['B:a', 'D:a'], $res); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotResolveNonPendingPromise() + { + $p = new Promise(); + $p->resolve('foo'); + $p->resolve('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->resolve('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectNonPendingPromise() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('bar'); + $this->assertEquals('foo', $p->wait()); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCanRejectWithSameValue() + { + $p = new Promise(); + $p->reject('foo'); + $p->reject('foo'); + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once + */ + public function testCannotRejectResolveWithSameValue() + { + $p = new Promise(); + $p->resolve('foo'); + $p->reject('foo'); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage There were no more events in the loop. This promise will never be fulfilled. + */ + public function testRejectsAndThrowsWhenWaitFailsToResolve() + { + $p = new Promise(function () {}); + $p->wait(); + } + + /** + * @expectedException \Exception + */ + public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() + { + $p = new Promise(); + $p->wait(); + } + + public function testCannotCancelNonPending() + { + $p = new Promise(); + $p->resolve('foo'); + $p->cancel(); + $this->assertEquals(self::FULFILLED, $p->getState()); + } + + /** + * @expectedException \Exception + */ + public function testCancelsPromiseWhenNoCancelFunction() + { + $p = new Promise(); + $p->cancel(); + $this->assertEquals(self::REJECTED, $p->getState()); + $p->wait(); + } + + public function testCancelsPromiseWithCancelFunction() + { + $called = false; + $p = new Promise(null, function () use (&$called) { $called = true; }); + $p->cancel(); + $this->assertEquals(self::REJECTED, $p->getState()); + $this->assertTrue($called); + } + + public function testCancelsChildPromises() + { + $called1 = $called2 = $called3 = false; + $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); + $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); + $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); + $p4 = $p2->then(function () use ($p3) { return $p3; }); + $p5 = $p4->then(function () { $this->fail(); }); + $p4->cancel(); + $this->assertEquals(self::PENDING, $p1->getState()); + $this->assertEquals(self::REJECTED, $p2->getState()); + $this->assertEquals(self::REJECTED, $p4->getState()); + $this->assertEquals(self::PENDING, $p5->getState()); + $this->assertFalse($called1); + $this->assertTrue($called2); + $this->assertFalse($called3); + } + + public function testRejectsPromiseWhenCancelFails() + { + $called = false; + $p = new Promise(null, function () use (&$called) { + $called = true; + throw new \Exception('e'); + }); + $p->cancel(); + $this->assertEquals(self::REJECTED, $p->getState()); + $this->assertTrue($called); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('e', $e->getMessage()); + } + } + + public function testCreatesPromiseWhenRejectedWithNoCallback() + { + $p = new Promise(); + $p->reject('foo'); + $p2 = $p->then(); + $this->assertNotSame($p, $p2); + $this->assertInstanceOf(Promise::class, $p2); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithNonException() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $p->wait(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage foo + */ + public function testThrowsWhenUnwrapIsRejectedWithException() + { + $e = new \Exception('foo'); + $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); + $p->wait(); + } + + public function testInvokesWaitFunction() + { + $p = new Promise(function () use (&$p) { $p->resolve('10'); }); + $this->assertEquals('10', $p->wait()); + } + + public function testDoesNotUnwrapExceptionsWhenDisabled() + { + $p = new Promise(function () use (&$p) { $p->reject('foo'); }); + $this->assertEquals(self::PENDING, $p->getState()); + $p->wait(false); + $this->assertEquals(self::REJECTED, $p->getState()); + } + + public function testRejectsSelfWhenWaitThrows() + { + $e = new \Exception('foo'); + $p = new Promise(function () use ($e) { throw $e; }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals(self::REJECTED, $p->getState()); + } + } + + public function testThrowsWaitExceptionAfterPromiseIsResolved() + { + $p = new Promise(function () use (&$p) { + $p->reject('Foo!'); + throw new \Exception('Bar?'); + }); + try { + $p->wait(); + $this->fail(); + } catch (\Exception $e) { + $this->assertEquals('Bar?', $e->getMessage()); + } + } + + public function testRemovesReferenceFromChildWhenParentWaitedUpon() + { + $r = null; + $p = new Promise(function () use (&$p) { $p->resolve('a'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); + $pb = $p->then( + function ($v) use ($p2, &$r) { + $r = $v; + return $p2; + }) + ->then(function ($v) { return $v . '.'; }); + $this->assertEquals('a', $p->wait()); + $this->assertEquals('b', $p2->wait()); + $this->assertEquals('b.', $pb->wait()); + $this->assertEquals('a', $r); + } + + public function testWaitsOnNestedPromises() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p = new Promise(function () use (&$p) { $p->resolve('_'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('foo'); }); + $p3 = $p->then(function () use ($p2) { return $p2; }); + $this->assertSame('foo', $p3->wait()); + } + + public function testWaitBehaviorIsBasedOnLastPromiseInChain() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p3 = new Promise(function () use (&$p3) { $p3->resolve('Whoop'); }); + $p2 = new Promise(function () use (&$p2, $p3) { $p2->reject($p3); }); + $p = new Promise(function () use (&$p, $p2) { $p->reject($p2); }); + $this->assertEquals('Whoop', $p->wait()); + } + + public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p2 = new Promise(function () use (&$p2) { + $p2->reject('Fail'); + }); + $p = new Promise(function () use ($p2, &$p) { + $p->resolve($p2); + }); + $p->wait(false); + $this->assertSame(self::REJECTED, $p2->getState()); + } + + public function testCancelsUppermostPendingPromise() + { + $called = false; + $p1 = new Promise(null, function () use (&$called) { $called = true; }); + $p2 = $p1->then(function () {}); + $p3 = $p2->then(function () {}); + $p4 = $p3->then(function () {}); + $p3->cancel(); + $this->assertEquals(self::REJECTED, $p1->getState()); + $this->assertEquals(self::REJECTED, $p2->getState()); + $this->assertEquals(self::REJECTED, $p3->getState()); + $this->assertEquals(self::PENDING, $p4->getState()); + $this->assertTrue($called); + try { + $p3->wait(); + $this->fail(); + } catch (CancellationException $e) { + $this->assertContains('cancelled', $e->getMessage()); + } + try { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p4->wait(); + $this->fail(); + } catch (CancellationException $e) { + $this->assertContains('cancelled', $e->getMessage()); + } + $this->assertEquals(self::REJECTED, $p4->getState()); + } + + public function testInvokesWaitFnsForThens() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p = new Promise(function () use (&$p) { $p->resolve('a'); }); + $p2 = $p + ->then(function ($v) { return $v . '-1-'; }) + ->then(function ($v) { return $v . '2'; }); + $this->assertEquals('a-1-2', $p2->wait()); + } + + public function testStacksThenWaitFunctions() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $p1 = new Promise(function () use (&$p1) { $p1->resolve('a'); }); + $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); + $p3 = new Promise(function () use (&$p3) { $p3->resolve('c'); }); + $p4 = $p1 + ->then(function () use ($p2) { return $p2; }) + ->then(function () use ($p3) { return $p3; }); + $this->assertEquals('c', $p4->wait()); + } + + public function testDoesNotBlowStackWhenWaitingOnNestedThens() + { + $this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); + + $inner = new Promise(function () use (&$inner) { $inner->resolve(0); }); + $prev = $inner; + for ($i = 1; $i < 100; $i++) { + $prev = $prev->then(function ($i) { return $i + 1; }); + } + $parent = new Promise(function () use (&$parent, $prev) { + $parent->resolve($prev); + }); + $this->assertEquals(99, $parent->wait()); + } +} diff --git a/tests/NotWorkingPromiseTest.php b/tests/NotWorkingPromiseTest.php deleted file mode 100644 index bea5d69..0000000 --- a/tests/NotWorkingPromiseTest.php +++ /dev/null @@ -1,109 +0,0 @@ -markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.'); - } - - public function testWaitsOnNestedPromises() - { - $p = new Promise(function () use (&$p) { $p->resolve('_'); }); - $p2 = new Promise(function () use (&$p2) { $p2->resolve('foo'); }); - $p3 = $p->then(function () use ($p2) { return $p2; }); - $this->assertSame('foo', $p3->wait()); - } - - public function testWaitBehaviorIsBasedOnLastPromiseInChain() - { - $p3 = new Promise(function () use (&$p3) { $p3->resolve('Whoop'); }); - $p2 = new Promise(function () use (&$p2, $p3) { $p2->reject($p3); }); - $p = new Promise(function () use (&$p, $p2) { $p->reject($p2); }); - $this->assertEquals('Whoop', $p->wait()); - } - - public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped() - { - $p2 = new Promise(function () use (&$p2) { - $p2->reject('Fail'); - }); - $p = new Promise(function () use ($p2, &$p) { - $p->resolve($p2); - }); - $p->wait(false); - $this->assertSame(Promise::REJECTED, $p2->getState()); - } - - public function testCancelsUppermostPendingPromise() - { - $called = false; - $p1 = new Promise(null, function () use (&$called) { $called = true; }); - $p2 = $p1->then(function () {}); - $p3 = $p2->then(function () {}); - $p4 = $p3->then(function () {}); - $p3->cancel(); - $this->assertEquals(Promise::REJECTED, $p1->getState()); - $this->assertEquals(Promise::REJECTED, $p2->getState()); - $this->assertEquals(Promise::REJECTED, $p3->getState()); - $this->assertEquals(Promise::PENDING, $p4->getState()); - $this->assertTrue($called); - try { - $p3->wait(); - $this->fail(); - } catch (CancellationException $e) { - $this->assertContains('cancelled', $e->getMessage()); - } - try { - $p4->wait(); - $this->fail(); - } catch (CancellationException $e) { - $this->assertContains('cancelled', $e->getMessage()); - } - $this->assertEquals(Promise::REJECTED, $p4->getState()); - } - - public function testInvokesWaitFnsForThens() - { - $p = new Promise(function () use (&$p) { $p->resolve('a'); }); - $p2 = $p - ->then(function ($v) { return $v . '-1-'; }) - ->then(function ($v) { return $v . '2'; }); - $this->assertEquals('a-1-2', $p2->wait()); - } - - public function testStacksThenWaitFunctions() - { - $p1 = new Promise(function () use (&$p1) { $p1->resolve('a'); }); - $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); - $p3 = new Promise(function () use (&$p3) { $p3->resolve('c'); }); - $p4 = $p1 - ->then(function () use ($p2) { return $p2; }) - ->then(function () use ($p3) { return $p3; }); - $this->assertEquals('c', $p4->wait()); - } - - public function testDoesNotBlowStackWhenWaitingOnNestedThens() - { - $inner = new Promise(function () use (&$inner) { $inner->resolve(0); }); - $prev = $inner; - for ($i = 1; $i < 100; $i++) { - $prev = $prev->then(function ($i) { return $i + 1; }); - } - $parent = new Promise(function () use (&$parent, $prev) { - $parent->resolve($prev); - }); - $this->assertEquals(99, $parent->wait()); - } - -} diff --git a/tests/Promise/PromiseTest.php b/tests/Promise/PromiseTest.php index c2d556f..4ada055 100644 --- a/tests/Promise/PromiseTest.php +++ b/tests/Promise/PromiseTest.php @@ -226,339 +226,4 @@ public function testWaitRejectedScalar() $this->assertEquals('foo', $e->getMessage()); } } - - ////////////////////////////////// - public function testForwardsRejectedPromisesDownChainBetweenGaps() - { - $p = new Promise(); - $r = $r2 = null; - $p->then(null, null) - ->then(null, function ($v) use (&$r) { $r = $v; return $v . '2'; }) - ->then(function ($v) use (&$r2) { $r2 = $v; }); - $p->reject('foo'); - Loop\run(); - $this->assertEquals('foo', $r); - $this->assertEquals('foo2', $r2); - } - - public function testForwardsThrownPromisesDownChainBetweenGaps() - { - $e = new \Exception(); - $p = new Promise(); - $r = $r2 = null; - $p->then(null, null) - ->then(null, function ($v) use (&$r, $e) { - $r = $v; - throw $e; - }) - ->then( - null, - function ($v) use (&$r2) { $r2 = $v; } - ); - $p->reject('foo'); - Loop\run(); - $this->assertEquals('foo', $r); - $this->assertSame($e, $r2); - } - - public function testForwardsHandlersWhenRejectedPromiseIsReturned() - { - $res = []; - $p = new Promise(); - $p2 = new Promise(); - $p2->reject('foo'); - $p2->then(null, function ($v) use (&$res) { $res[] = 'A:' . $v; }); - $p->then(null, function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) - ->then(null, function ($v) use (&$res) { $res[] = 'C:' . $v; }); - $p->reject('a'); - $p->then(null, function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); - $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); - } - - public function testForwardsFulfilledDownChainBetweenGaps() - { - $p = new Promise(); - $r = $r2 = null; - $p->then(null, null) - ->then(function ($v) use (&$r) {$r = $v; return $v . '2'; }) - ->then(function ($v) use (&$r2) { $r2 = $v; }); - $p->fulfill('foo'); - Loop\run(); - $this->assertEquals('foo', $r); - $this->assertEquals('foo2', $r2); - } - - public function testForwardsHandlersToNextPromise() - { - $p = new Promise(); - $p2 = new Promise(); - $resolved = null; - $p - ->then(function ($v) use ($p2) { return $p2; }) - ->then(function ($value) use (&$resolved) { $resolved = $value; }); - $p->fulfill('a'); - $p2->fulfill('b'); - Loop\run(); - $this->assertEquals('b', $resolved); - } - - public function testForwardsHandlersWhenFulfilledPromiseIsReturned() - { - $res = []; - $p = new Promise(); - $p2 = new Promise(); - $p2->fulfill('foo'); - $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); - // $res is A:foo - $p - ->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) - ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); - $p->fulfill('a'); - $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); - $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); - } - - public function testDoesNotForwardRejectedPromise() - { - $res = []; - $p = new Promise(); - $p2 = new Promise(); - $p2->cancel(); - $p2->then(function ($v) use (&$res) { $res[] = "B:$v"; return $v; }); - $p->then(function ($v) use ($p2, &$res) { $res[] = "B:$v"; return $p2; }) - ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); - $p->resolve('a'); - $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); - $this->assertEquals(['B:a', 'D:a'], $res); - } - /////////////////////// - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotResolveNonPendingPromise() - { - $p = new Promise(); - $p->resolve('foo'); - $p->resolve('bar'); - $this->assertEquals('foo', $p->wait()); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCanResolveWithSameValue() - { - $p = new Promise(); - $p->resolve('foo'); - $p->resolve('foo'); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotRejectNonPendingPromise() - { - $p = new Promise(); - $p->resolve('foo'); - $p->reject('bar'); - $this->assertEquals('foo', $p->wait()); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCanRejectWithSameValue() - { - $p = new Promise(); - $p->reject('foo'); - $p->reject('foo'); - } - - /** - * @expectedException \Sabre\Event\PromiseAlreadyResolvedException - * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once - */ - public function testCannotRejectResolveWithSameValue() - { - $p = new Promise(); - $p->resolve('foo'); - $p->reject('foo'); - } - - /** - * @expectedException \LogicException - * @expectedExceptionMessage There were no more events in the loop. This promise will never be fulfilled. - */ - public function testRejectsAndThrowsWhenWaitFailsToResolve() - { - $p = new Promise(function () {}); - $p->wait(); - } - - /** - * @expectedException \Exception - */ - public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() - { - $p = new Promise(); - $p->wait(); - } - - public function testCannotCancelNonPending() - { - $p = new Promise(); - $p->resolve('foo'); - $p->cancel(); - $this->assertEquals(Promise::FULFILLED, $p->getState()); - } - - /** - * @expectedException \Exception - */ - public function testCancelsPromiseWhenNoCancelFunction() - { - $p = new Promise(); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $p->wait(); - } - - public function testCancelsPromiseWithCancelFunction() - { - $called = false; - $p = new Promise(null, function () use (&$called) { $called = true; }); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $this->assertTrue($called); - } - - public function testCancelsChildPromises() - { - $called1 = $called2 = $called3 = false; - $p1 = new Promise(null, function () use (&$called1) { $called1 = true; }); - $p2 = new Promise(null, function () use (&$called2) { $called2 = true; }); - $p3 = new Promise(null, function () use (&$called3) { $called3 = true; }); - $p4 = $p2->then(function () use ($p3) { return $p3; }); - $p5 = $p4->then(function () { $this->fail(); }); - $p4->cancel(); - $this->assertEquals(Promise::PENDING, $p1->getState()); - $this->assertEquals(Promise::REJECTED, $p2->getState()); - $this->assertEquals(Promise::REJECTED, $p4->getState()); - $this->assertEquals(Promise::PENDING, $p5->getState()); - $this->assertFalse($called1); - $this->assertTrue($called2); - $this->assertFalse($called3); - } - - public function testRejectsPromiseWhenCancelFails() - { - $called = false; - $p = new Promise(null, function () use (&$called) { - $called = true; - throw new \Exception('e'); - }); - $p->cancel(); - $this->assertEquals(Promise::REJECTED, $p->getState()); - $this->assertTrue($called); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals('e', $e->getMessage()); - } - } - - public function testCreatesPromiseWhenRejectedWithNoCallback() - { - $p = new Promise(); - $p->reject('foo'); - $p2 = $p->then(); - $this->assertNotSame($p, $p2); - $this->assertInstanceOf(Promise::class, $p2); - } - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithNonException() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $p->wait(); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage foo - */ - public function testThrowsWhenUnwrapIsRejectedWithException() - { - $e = new \Exception('foo'); - $p = new Promise(function () use (&$p, $e) { $p->reject($e); }); - $p->wait(); - } - - public function testInvokesWaitFunction() - { - $p = new Promise(function () use (&$p) { $p->resolve('10'); }); - $this->assertEquals('10', $p->wait()); - } - - public function testDoesNotUnwrapExceptionsWhenDisabled() - { - $p = new Promise(function () use (&$p) { $p->reject('foo'); }); - $this->assertEquals(Promise::PENDING, $p->getState()); - $p->wait(false); - $this->assertEquals(Promise::REJECTED, $p->getState()); - } - - public function testRejectsSelfWhenWaitThrows() - { - $e = new \Exception('foo'); - $p = new Promise(function () use ($e) { throw $e; }); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals(Promise::REJECTED, $p->getState()); - } - } - - public function testThrowsWaitExceptionAfterPromiseIsResolved() - { - $p = new Promise(function () use (&$p) { - $p->reject('Foo!'); - throw new \Exception('Bar?'); - }); - try { - $p->wait(); - $this->fail(); - } catch (\Exception $e) { - $this->assertEquals('Bar?', $e->getMessage()); - } - } - - public function testRemovesReferenceFromChildWhenParentWaitedUpon() - { - $r = null; - $p = new Promise(function () use (&$p) { $p->resolve('a'); }); - $p2 = new Promise(function () use (&$p2) { $p2->resolve('b'); }); - $pb = $p->then( - function ($v) use ($p2, &$r) { - $r = $v; - return $p2; - }) - ->then(function ($v) { return $v . '.'; }); - $this->assertEquals('a', $p->wait()); - $this->assertEquals('b', $p2->wait()); - $this->assertEquals('b.', $pb->wait()); - $this->assertEquals('a', $r); - } } From dcae72d2b2e476ba252369031dd1c7974cd7b36d Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Sat, 8 Dec 2018 11:44:00 -0500 Subject: [PATCH 17/23] Changes for easy reconfiguration to run under different environments. --- tests/InteroperabilityPromiseTest.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/InteroperabilityPromiseTest.php b/tests/InteroperabilityPromiseTest.php index 9fc865b..15c9458 100644 --- a/tests/InteroperabilityPromiseTest.php +++ b/tests/InteroperabilityPromiseTest.php @@ -14,7 +14,14 @@ class InteroperabilityPromiseTest extends TestCase { const PENDING = Promise::PENDING; const REJECTED = Promise::REJECTED; - const FULFILLED = Promise::FULFILLED; + const FULFILLED = Promise::FULFILLED; + + private $loop = null; + + protected function setUp() + { + $this->loop = Loop\instance(); + } public function testForwardsRejectedPromisesDownChainBetweenGaps() { @@ -24,7 +31,7 @@ public function testForwardsRejectedPromisesDownChainBetweenGaps() ->then(null, function ($v) use (&$r) { $r = $v; return $v . '2'; }) ->then(function ($v) use (&$r2) { $r2 = $v; }); $p->reject('foo'); - Loop\run(); + $this->loop->run(); $this->assertEquals('foo', $r); $this->assertEquals('foo2', $r2); } @@ -44,7 +51,7 @@ public function testForwardsThrownPromisesDownChainBetweenGaps() function ($v) use (&$r2) { $r2 = $v; } ); $p->reject('foo'); - Loop\run(); + $this->loop->run(); $this->assertEquals('foo', $r); $this->assertSame($e, $r2); } @@ -60,7 +67,7 @@ public function testForwardsHandlersWhenRejectedPromiseIsReturned() ->then(null, function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->reject('a'); $p->then(null, function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); + $this->loop->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } @@ -72,7 +79,7 @@ public function testForwardsFulfilledDownChainBetweenGaps() ->then(function ($v) use (&$r) {$r = $v; return $v . '2'; }) ->then(function ($v) use (&$r2) { $r2 = $v; }); $p->fulfill('foo'); - Loop\run(); + $this->loop->run(); $this->assertEquals('foo', $r); $this->assertEquals('foo2', $r2); } @@ -87,7 +94,7 @@ public function testForwardsHandlersToNextPromise() ->then(function ($value) use (&$resolved) { $resolved = $value; }); $p->fulfill('a'); $p2->fulfill('b'); - Loop\run(); + $this->loop->run(); $this->assertEquals('b', $resolved); } @@ -104,7 +111,7 @@ public function testForwardsHandlersWhenFulfilledPromiseIsReturned() ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->fulfill('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); + $this->loop->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); } @@ -119,7 +126,7 @@ public function testDoesNotForwardRejectedPromise() ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); - Loop\run(); + $this->loop->run(); $this->assertEquals(['B:a', 'D:a'], $res); } From daaf0d36e88d16a9e46abcc36703715f81497273 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Sat, 8 Dec 2018 11:59:56 -0500 Subject: [PATCH 18/23] Update InteroperabilityPromiseTest.php --- tests/InteroperabilityPromiseTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/InteroperabilityPromiseTest.php b/tests/InteroperabilityPromiseTest.php index 15c9458..fdc7958 100644 --- a/tests/InteroperabilityPromiseTest.php +++ b/tests/InteroperabilityPromiseTest.php @@ -78,7 +78,7 @@ public function testForwardsFulfilledDownChainBetweenGaps() $p->then(null, null) ->then(function ($v) use (&$r) {$r = $v; return $v . '2'; }) ->then(function ($v) use (&$r2) { $r2 = $v; }); - $p->fulfill('foo'); + $p->resolve('foo'); $this->loop->run(); $this->assertEquals('foo', $r); $this->assertEquals('foo2', $r2); @@ -92,8 +92,8 @@ public function testForwardsHandlersToNextPromise() $p ->then(function ($v) use ($p2) { return $p2; }) ->then(function ($value) use (&$resolved) { $resolved = $value; }); - $p->fulfill('a'); - $p2->fulfill('b'); + $p->resolve('a'); + $p2->resolve('b'); $this->loop->run(); $this->assertEquals('b', $resolved); } @@ -103,13 +103,13 @@ public function testForwardsHandlersWhenFulfilledPromiseIsReturned() $res = []; $p = new Promise(); $p2 = new Promise(); - $p2->fulfill('foo'); + $p2->resolve('foo'); $p2->then(function ($v) use (&$res) { $res[] = 'A:' . $v; }); // $res is A:foo $p ->then(function () use ($p2, &$res) { $res[] = 'B'; return $p2; }) ->then(function ($v) use (&$res) { $res[] = 'C:' . $v; }); - $p->fulfill('a'); + $p->resolve('a'); $p->then(function ($v) use (&$res) { $res[] = 'D:' . $v; }); $this->loop->run(); $this->assertEquals(['A:foo', 'B', 'D:a', 'C:foo'], $res); From d975307b449b97111580980021b410375b79734f Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Sat, 8 Dec 2018 12:25:45 -0500 Subject: [PATCH 19/23] Removed phpstan/phpstan composer -dev requirement --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2be488d..70fe9ab 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,7 @@ ] }, "require-dev": { - "phpunit/phpunit" : ">=6", - "phpstan/phpstan": "^0.10.6" + "phpunit/phpunit" : ">=6" }, "config" : { "bin-dir" : "bin/" From 68d906ec99574612d4a42a315eb0730c8105deca Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Sat, 8 Dec 2018 12:32:13 -0500 Subject: [PATCH 20/23] Update .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6f11b0a..6af8c64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ matrix: - name: 'PHPStan' php: 7.2 env: RUN_PHPSTAN="TRUE" + allow_failures: + - name: 'PHPStan' install: - if [ $RUN_PHPSTAN == "TRUE" ]; then wget https://github.com/phpstan/phpstan/releases/download/0.10.3/phpstan.phar; fi From eee20ee9fb989f07481e72a1fe86a9a2879a1bf2 Mon Sep 17 00:00:00 2001 From: techno-express Date: Sun, 9 Dec 2018 17:55:16 -0500 Subject: [PATCH 21/23] revert newmaster branch to master of upstream --- lib/CancellationException.php | 12 -- lib/Promise.php | 213 +++++----------------------------- lib/RejectionException.php | 53 --------- 3 files changed, 31 insertions(+), 247 deletions(-) delete mode 100644 lib/CancellationException.php delete mode 100644 lib/RejectionException.php diff --git a/lib/CancellationException.php b/lib/CancellationException.php deleted file mode 100644 index 43aa7c0..0000000 --- a/lib/CancellationException.php +++ /dev/null @@ -1,12 +0,0 @@ -fulfill and $this->reject. * Using the executor is optional. */ - public function __construct( ...$executor) - { - $callExecutor = isset($executor[0]) ? $executor[0] : null; - $childLoop = $this->checkLoopInstance($callExecutor) ? $callExecutor : null; - $callExecutor = $this->checkLoopInstance($callExecutor) ? null : $callExecutor; - - $callCanceller = isset($executor[1]) ? $executor[1] : null; - $childLoop = $this->checkLoopInstance($callCanceller) ? $callCanceller : $childLoop; - $callCanceller = $this->checkLoopInstance($callCanceller) ? null : $callCanceller; - - $loop = isset($executor[2]) ? $executor[2] : null; - $childLoop = $this->checkLoopInstance($loop) ? $loop : $childLoop; - $this->loop = $this->checkLoopInstance($childLoop) ? $childLoop : Loop\instance(); - - $this->waitFn = is_callable($callExecutor) ? $callExecutor : null; - $this->cancelFn = is_callable($callCanceller) ? $callCanceller : null; - - try { - if (is_callable($callExecutor) && !$this->isWaitRequired) { - $callExecutor( - [$this, 'fulfill'], - [$this, 'reject'] - ); - } - } catch (\Throwable $e) { - $this->isWaitRequired = true; - } catch (\Exception $exception) { - $this->isWaitRequired = true; - } - } - - private function checkLoopInstance($instance = null): bool - { - $isInstanceiable = false; - if ($instance instanceof TaskQueueInterface) - $isInstanceiable = true; - elseif ($instance instanceof LoopInterface) - $isInstanceiable = true; - elseif ($instance instanceof Loop) - $isInstanceiable = true; - - return $isInstanceiable; - } - - public function getState() + public function __construct(callable $executor = null) { - return $this->state; + if ($executor) { + $executor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } } - + /** * This method allows you to specify the callback that will be called after * the promise has been fulfilled or rejected. @@ -135,7 +90,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): // This new subPromise will be returned from this function, and will // be fulfilled with the result of the onFulfilled or onRejected event // handlers. - $subPromise = new Promise(null, [$this, 'cancel']); + $subPromise = new self(); switch ($this->state) { case self::PENDING: @@ -169,15 +124,6 @@ public function otherwise(callable $onRejected): Promise return $this->then(null, $onRejected); } - public function resolve($value = null) - { - if ($value instanceof Promise) { - return $value->then(); - } - - return $this->fulfill($value); - } - /** * Marks this promise as fulfilled and sets its return value. * @@ -198,7 +144,7 @@ public function fulfill($value = null) /** * Marks this promise as rejected, and set it's rejection reason. */ - public function reject($reason) + public function reject(Throwable $reason) { if (self::PENDING !== $this->state) { throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); @@ -210,32 +156,6 @@ public function reject($reason) } } - public function cancel() - { - if (self::PENDING !== $this->state) { - return; - } - - $this->waitFn = null; - $this->subscribers = []; - - if ($this->cancelFn) { - $fn = $this->cancelFn; - $this->cancelFn = null; - try { - $fn(); - } catch (Throwable $e) { - $this->reject($e); - } catch (Exception $exception) { - $this->reject($exception); - } - } - - // Reject the promise only if it wasn't rejected in a then callback. - if (self::PENDING === $this->state) { - $this->reject(new CancellationException('Promise has been cancelled')); - } - } /** * Stops execution until this promise is resolved. * @@ -249,71 +169,28 @@ public function cancel() * * @return mixed */ - public function wait($unwrap = true) + public function wait() { - try { - $loop = $this->loop; - $fn = $this->waitFn; - $this->waitFn = null; - if (is_callable($fn) - && method_exists($loop, 'add') - && method_exists($loop, 'run') - && $this->isWaitRequired - ) { - $this->isWaitRequired = false; - $fn([$this, 'fulfill'], [$this, 'reject']); - $loop->run(); - } elseif (method_exists($loop, 'tick')) { - if (is_callable($fn) && $this->isWaitRequired) { - $this->isWaitRequired = false; - $fn([$this, 'fulfill'], [$this, 'reject']); - } - - $hasEvents = true; - while (self::PENDING === $this->state) { - if (!$hasEvents) { - throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); - } - - // As long as the promise is not fulfilled, we tell the event loop - // to handle events, and to block. - $hasEvents = $loop->tick(true); - } - } - } catch (\Exception $reason) { - if ($this->state === self::PENDING) { - // The promise has not been resolved yet, so reject the promise - // with the exception. - $this->reject($reason); - } else { - // The promise was already resolved, so there's a problem in - // the application. - throw $reason; + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); } + + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); } - - $result = $this->value instanceof Promise - ? $this->value->wait() - : $this->value; - - if ($this->state === self::PENDING) { - $this->reject('Invoking the wait callback did not resolve the promise'); - } elseif (self::FULFILLED === $this->state) { - // If the state of this promise is fulfilled, we can return the value. - return $result; - } else { - // If we got here, it means that the asynchronous operation - // errored. Therefore we need to throw an exception. - if ($result instanceof Exception) { - throw $result; - } elseif (is_scalar($result) && $unwrap) { - throw new \Exception($result); - } elseif ($unwrap) { - $type = is_object($result) ? get_class($result) : gettype($result); - throw new \Exception('Promise was rejected with reason of type: ' . $type); - } - } - } + + if (self::FULFILLED === $this->state) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + throw $this->value; + } + } /** * A list of subscribers. Subscribers are the callbacks that want us to let @@ -332,8 +209,6 @@ public function wait($unwrap = true) * @var mixed */ protected $value = null; - protected $cancelFn = null; - protected $waitFn = null; /** * This method is used to call either an onFulfilled or onRejected callback. @@ -352,7 +227,7 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) // passed to 'then'. // // This makes the order of execution more predictable. - $promiseFunction = function() use ($callBack, $subPromise) { + Loop\nextTick(function () use ($callBack, $subPromise) { if (is_callable($callBack)) { try { $result = $callBack($this->value); @@ -371,8 +246,6 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) // If the event handler threw an exception, we need to make sure that // the chained promise is rejected as well. $subPromise->reject($e); - } catch (Exception $exception) { - $subPromise->reject($exception); } } else { if (self::FULFILLED === $this->state) { @@ -381,30 +254,6 @@ private function invokeCallback(Promise $subPromise, callable $callBack = null) $subPromise->reject($this->value); } } - }; - - $this->implement($promiseFunction, $subPromise); + }); } - - public function implement(callable $function, Promise $promise = null) - { - if ($this->loop) { - $loop = $this->loop; - - $othersLoop = method_exists($loop, 'futureTick') ? [$loop, 'futureTick'] : null; - $othersLoop = method_exists($loop, 'addTick') ? [$loop, 'addTick'] : $othersLoop; - $othersLoop = method_exists($loop, 'onTick') ? [$loop, 'onTick'] : $othersLoop; - $othersLoop = method_exists($loop, 'enqueue') ? [$loop, 'enqueue'] : $othersLoop; - $othersLoop = method_exists($loop, 'add') ? [$loop, 'add'] : $othersLoop; - - if ($othersLoop) - call_user_func_array($othersLoop, $function); - else - $loop->nextTick($function); - } else { - return $function(); - } - - return $promise; - } } diff --git a/lib/RejectionException.php b/lib/RejectionException.php deleted file mode 100644 index a886b13..0000000 --- a/lib/RejectionException.php +++ /dev/null @@ -1,53 +0,0 @@ -reason = $reason; - - $message = 'The promise was rejected'; - - if ($description) { - $message .= ' with reason: ' . $description; - } elseif (is_string($reason) - || (is_object($reason) && method_exists($reason, '__toString')) - ) { - $message .= ' with reason: ' . $this->reason; - } elseif ($reason instanceof \JsonSerializable) { - $message .= ' with reason: ' - . json_encode($this->reason, JSON_PRETTY_PRINT); - } - - parent::__construct($message); - } - - /** - * Returns the rejection reason. - * - * @return mixed - */ - public function getReason() - { - return $this->reason; - } - - public function wait($unwrap = true) - { - return $unwrap ? $this->reason : null; - } -} From 23f87f68f05bc60afdadb7cad177acd9141354a8 Mon Sep 17 00:00:00 2001 From: techno-express Date: Mon, 10 Dec 2018 09:02:22 -0500 Subject: [PATCH 22/23] Update InteroperabilityPromiseTest.php --- tests/InteroperabilityPromiseTest.php | 261 +++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 2 deletions(-) diff --git a/tests/InteroperabilityPromiseTest.php b/tests/InteroperabilityPromiseTest.php index fdc7958..7ff7908 100644 --- a/tests/InteroperabilityPromiseTest.php +++ b/tests/InteroperabilityPromiseTest.php @@ -1,13 +1,29 @@ loop = Loop\instance(); + //$this->loop = new TaskQueue(); + //Loop::clearInstance(); + //$this->loop = Promise::getLoop(true); + //$this->loop = Factory::create(); + //$this->loop = new Queue(); + } + + public function testSuccess() + { + $finalValue = 0; + $promise = new Promise(); + $promise->resolve(1); + + $promise->then(function ($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + $this->loop->run(); + + $this->assertEquals(3, $finalValue); + } + + public function testFailure() + { + $finalValue = 0; + $promise = new Promise(); + $promise->reject(new Exception('1')); + + $promise->then(null, function ($value) use (&$finalValue) { + $finalValue = $value->getMessage() + 2; + }); + $this->loop->run(); + + $this->assertEquals(3, $finalValue); + } + + public function testChaining() + { + $finalValue = 0; + $promise = new Promise(); + $promise->resolve(1); + + $promise->then(function ($value) use (&$finalValue) { + $finalValue = $value + 2; + + return $finalValue; + })->then(function ($value) use (&$finalValue) { + $finalValue = $value + 4; + + return $finalValue; + }); + $this->loop->run(); + + $this->assertEquals(7, $finalValue); + } + + public function testPendingResult() + { + $finalValue = 0; + $promise = new Promise(); + + $promise->then(function ($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + + $promise->resolve(4); + $this->loop->run(); + + $this->assertEquals(6, $finalValue); + } + + public function testPendingFail() + { + $finalValue = 0; + $promise = new Promise(); + + $promise->otherwise(function ($value) use (&$finalValue) { + $finalValue = $value->getMessage() + 2; + }); + + $promise->reject(new Exception('4')); + $this->loop->run(); + + $this->assertEquals(6, $finalValue); + } + + public function testChainingPromises() + { + $finalValue = 0; + $promise = new Promise(); + $promise->resolve(1); + + $subPromise = new Promise(); + + $promise->then(function ($value) use ($subPromise) { + return $subPromise; + })->then(function ($value) use (&$finalValue) { + $finalValue = $value + 4; + + return $finalValue; + }); + + $subPromise->resolve(2); + $this->loop->run(); + + $this->assertEquals(6, $finalValue); + } + + /** + * /expected Risky - No Tests Performed! + * /or Exception + * @expectedException \Exception + */ + public function testResolveTwice() + { + $promise = new Promise(); + $promise->resolve(1); + $promise->resolve(1); + } + + /** + * /expected Risky - No Tests Performed! + * /or Exception + * @expectedException \Exception + */ + public function testRejectTwice() + { + $promise = new Promise(); + $promise->reject(new Exception('1')); + $promise->reject(new Exception('1')); + } + + public function testConstructorCallResolve() + { + $promise = (new Promise(function ($resolve, $reject) { + $resolve('hi'); + }))->then(function ($result) use (&$realResult) { + $realResult = $result; + }); + $this->loop->run(); + + $this->assertEquals('hi', $realResult); + } + + public function testConstructorCallReject() + { + $promise = (new Promise(function ($resolve, $reject) { + $reject(new Exception('hi')); + }))->then(function ($result) use (&$realResult) { + $realResult = 'incorrect'; + })->otherwise(function ($reason) use (&$realResult) { + $realResult = $reason->getMessage(); + }); + $this->loop->run(); + + $this->assertEquals('hi', $realResult); + } + + public function testWaitResolve() + { + $promise = new Promise(); + $this->loop->nextTick(function () use ($promise) { + $promise->resolve(1); + }); + $this->assertEquals( + 1, + $promise->wait() + ); + } + + public function testFailureHandler() + { + $ok = 0; + $promise = new Promise(); + $promise->otherwise(function ($reason) { + $this->assertEquals('foo', $reason); + throw new \Exception('hi'); + })->then(function () use (&$ok) { + $ok = -1; + }, function () use (&$ok) { + $ok = 1; + }); + + $this->assertEquals(0, $ok); + $promise->reject(new Exception('foo')); + $this->loop->run(); + + $this->assertEquals(1, $ok); + } + + public function testWaitRejectedException() + { + $promise = new Promise(); + $this->loop->nextTick(function () use ($promise) { + $promise->reject(new \OutOfBoundsException('foo')); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('OutOfBoundsException', $e); + $this->assertEquals('foo', $e->getMessage()); + } + } + + public function testWaitRejectedScalar() + { + $promise = new Promise(); + $this->loop->nextTick(function () use ($promise) { + $promise->reject(new Exception('foo')); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('Exception', $e); + $this->assertEquals('foo', $e->getMessage()); + } } public function testForwardsRejectedPromisesDownChainBetweenGaps() @@ -131,6 +373,8 @@ public function testDoesNotForwardRejectedPromise() } /** + * /expectedException \LogicException + * /expectedExceptionMessage The promise is already fulfilled * @expectedException \Sabre\Event\PromiseAlreadyResolvedException * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once */ @@ -143,6 +387,7 @@ public function testCannotResolveNonPendingPromise() } /** + * /expected Risky - No Tests Performed! * @expectedException \Sabre\Event\PromiseAlreadyResolvedException * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once */ @@ -154,6 +399,8 @@ public function testCanResolveWithSameValue() } /** + * /expectedException \LogicException + * /expectedExceptionMessage Cannot change a fulfilled promise to rejected * @expectedException \Sabre\Event\PromiseAlreadyResolvedException * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once */ @@ -166,6 +413,7 @@ public function testCannotRejectNonPendingPromise() } /** + * /expected Risky - No Tests Performed! * @expectedException \Sabre\Event\PromiseAlreadyResolvedException * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once */ @@ -177,6 +425,8 @@ public function testCanRejectWithSameValue() } /** + * /expectedException \LogicException + * /expectedExceptionMessage Cannot change a fulfilled promise to rejected * @expectedException \Sabre\Event\PromiseAlreadyResolvedException * @expectedExceptionMessage This promise is already resolved, and you're not allowed to resolve a promise more than once */ @@ -188,6 +438,7 @@ public function testCannotRejectResolveWithSameValue() } /** + * /expectedException \Async\Promise\RejectionException * @expectedException \LogicException * @expectedExceptionMessage There were no more events in the loop. This promise will never be fulfilled. */ @@ -198,6 +449,7 @@ public function testRejectsAndThrowsWhenWaitFailsToResolve() } /** + * /expectedException \Async\Promise\RejectionException * @expectedException \Exception */ public function testThrowsWhenWaitingOnPromiseWithNoWaitFunction() @@ -215,6 +467,7 @@ public function testCannotCancelNonPending() } /** + * /expectedException \Async\Promise\CancellationException * @expectedException \Exception */ public function testCancelsPromiseWhenNoCancelFunction() @@ -280,6 +533,8 @@ public function testCreatesPromiseWhenRejectedWithNoCallback() } /** + * /expectedException \Async\Promise\RejectionException + * /expectedExceptionMessage The promise was rejected with reason: foo * @expectedException \Exception * @expectedExceptionMessage foo */ @@ -290,6 +545,8 @@ public function testThrowsWhenUnwrapIsRejectedWithNonException() } /** + * /expectedException \Async\Promise\RejectionException + * /expectedExceptionMessage The promise was rejected with reason: foo * @expectedException \Exception * @expectedExceptionMessage foo */ From 5734a43f91ffcf33f04a6663998347ac0b4b4315 Mon Sep 17 00:00:00 2001 From: "L. Stubbs" Date: Mon, 10 Dec 2018 10:54:15 -0500 Subject: [PATCH 23/23] Update InteroperabilityPromiseTest.php --- tests/InteroperabilityPromiseTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/InteroperabilityPromiseTest.php b/tests/InteroperabilityPromiseTest.php index 7ff7908..43ea25a 100644 --- a/tests/InteroperabilityPromiseTest.php +++ b/tests/InteroperabilityPromiseTest.php @@ -207,6 +207,10 @@ public function testWaitResolve() { $promise = new Promise(); $this->loop->nextTick(function () use ($promise) { + //$this->loop->addTick(function () use ($promise) { + //$this->loop->enqueue(function () use ($promise) { + //$this->loop->add(function () use ($promise) { + //$this->loop->futureTick(function () use ($promise) { $promise->resolve(1); }); $this->assertEquals( @@ -239,6 +243,10 @@ public function testWaitRejectedException() { $promise = new Promise(); $this->loop->nextTick(function () use ($promise) { + //$this->loop->addTick(function () use ($promise) { + //$this->loop->enqueue(function () use ($promise) { + //$this->loop->add(function () use ($promise) { + //$this->loop->futureTick(function () use ($promise) { $promise->reject(new \OutOfBoundsException('foo')); }); try { @@ -254,6 +262,10 @@ public function testWaitRejectedScalar() { $promise = new Promise(); $this->loop->nextTick(function () use ($promise) { + //$this->loop->addTick(function () use ($promise) { + //$this->loop->enqueue(function () use ($promise) { + //$this->loop->add(function () use ($promise) { + //$this->loop->futureTick(function () use ($promise) { $promise->reject(new Exception('foo')); }); try {