Skip to content

Commit

Permalink
Corrections any type of function call in constructor, move all failin…
Browse files Browse the repository at this point in the history
…g, mark as skiped for now.

The difference in Guzzle promise implementations mainly lay in the construction.

According to promises-aplus/constructor-spec#18 it's not valid.
The constructor starts the fate of the promise. Which has to been delayed, under Guzzle.

The wait method and constructor setup in Guzzle is necessary in certain callable functions situations.
The constructor will fail it's execution when trying to access member method that's null.
It will happen when passing an promise object not fully created, itself. The cause for some failing tests.

Normally an promise is attached to an external running event loop, no need to start the process.
The wait function/method both starts and stops it, internally.
  • Loading branch information
TheTechsTech committed Dec 17, 2018
1 parent 0106923 commit 207b011
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 90 deletions.
34 changes: 16 additions & 18 deletions lib/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,20 @@ public function __construct( ...$executor)
$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']
);
}
$promiseFunction = function () use($callExecutor) {
if (is_callable($callExecutor)) {
$callExecutor([$this, 'fulfill'], [$this, 'reject']);
}
};

try {
$promiseFunction();
} catch (\Throwable $e) {
$this->isWaitRequired = true;
$this->implement($promiseFunction);
} catch (\Exception $exception) {
$this->isWaitRequired = true;
$this->implement($promiseFunction);
}
}

Expand Down Expand Up @@ -263,12 +266,7 @@ public function wait($unwrap = true)
$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']);
}

} elseif (method_exists($loop, 'tick')) {
$hasEvents = true;
while (self::PENDING === $this->state) {
if (!$hasEvents) {
Expand All @@ -293,22 +291,22 @@ public function wait($unwrap = true)
}

$result = $this->value instanceof Promise
? $this->value->wait()
? $this->value->wait($unwrap)
: $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 {
} elseif ($unwrap) {
// 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) {
} elseif (is_scalar($result)) {
throw new \Exception($result);
} elseif ($unwrap) {
} else {
$type = is_object($result) ? get_class($result) : gettype($result);
throw new \Exception('Promise was rejected with reason of type: ' . $type);
}
Expand Down Expand Up @@ -404,7 +402,7 @@ public function implement(callable $function, Promise $promise = null)
$othersLoop = [$loop, 'add'];

if ($othersLoop)
call_user_func_array($othersLoop, $function);
call_user_func($othersLoop, $function);
else
$loop->nextTick($function);
} else {
Expand Down
105 changes: 105 additions & 0 deletions tests/FailingInteroperabilityPromiseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace Sabre\Event\Promise;
//namespace GuzzleHttp\Promise;
//namespace React\Promise;
//namespace Async\Tests

use Exception;
use Sabre\Event\Loop;
use Sabre\Event\Promise;
use Sabre\Event\RejectionException;
use Sabre\Event\CancellationException;
use Sabre\Event\PromiseAlreadyResolvedException;
//use GuzzleHttp\Promise;
//use GuzzleHttp\Promise\TaskQueue;
//use GuzzleHttp\Promise\PromiseInterface;
//use GuzzleHttp\Promise\RejectionException;
//use GuzzleHttp\Promise\CancellationException;
//use Async\Loop\Loop;
//use Async\Promise\Promise;
//use Async\Promise\PromiseInterface;
//use Async\Promise\RejectionException;
//use Async\Promise\CancellationException;
//use React\Promise;
//use React\EventLoop\Factory;
//use React\Promise\Internal\Queue;
use PHPUnit\Framework\TestCase;

class FailingInteroperabilityPromiseTest extends TestCase
{
const PENDING = Promise::PENDING;
const REJECTED = Promise::REJECTED;
const FULFILLED = Promise::FULFILLED;
//const PENDING = PromiseInterface::PENDING;
//const REJECTED = PromiseInterface::REJECTED;
//const FULFILLED = PromiseInterface::FULFILLED;
//const PENDING = PromiseInterface::STATE_PENDING;
//const REJECTED = PromiseInterface::STATE_REJECTED;
//const FULFILLED = PromiseInterface::STATE_RESOLVED;
//const PENDING = 'pending';
//const REJECTED = 'rejected';
//const FULFILLED = 'fulfilled';

private $loop = null;

protected function setUp()
{
$this->markTestSkipped('These test fails in various stages, all taken from Guzzle phpunit tests.');
$this->loop = Loop\instance();
//$this->loop = new TaskQueue();
//Loop::clearInstance();
//$this->loop = Promise::getLoop(true);
//$this->loop = Factory::create();
//$this->loop = new Queue();
}

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 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 {
$p4->wait();
$this->fail();
} catch (CancellationException $e) {
$this->assertContains('cancelled', $e->getMessage());
}
$this->assertEquals(self::REJECTED, $p4->getState());
}

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());
}
}
82 changes: 10 additions & 72 deletions tests/InteroperabilityPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -627,73 +627,16 @@ function ($v) use ($p2, &$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-'; })
Expand All @@ -703,8 +646,6 @@ public function testInvokesWaitFnsForThens()

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'); });
Expand All @@ -714,18 +655,15 @@ public function testStacksThenWaitFunctions()
$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);
public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped()
{
$p2 = new Promise(function () use (&$p2) {
$p2->reject('Fail');
});
$this->assertEquals(99, $parent->wait());
$p = new Promise(function () use ($p2, &$p) {
$p->resolve($p2);
});
$p->wait(false);
$this->assertSame(self::REJECTED, $p2->getState());
}
}

0 comments on commit 207b011

Please sign in to comment.