Skip to content

[WIP][RFC] Throw when end of chain has been reached #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
name: PHPUnit (PHP ${{ matrix.php }})
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
php:
- 8.2
Expand Down
14 changes: 14 additions & 0 deletions src/Internal/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@
final class RejectedPromise implements PromiseInterface
{
private $reason;
private $endOfChain = true;

public function __construct(\Throwable $reason)
{
$this->reason = $reason;
}

public function __destruct()
{
if ($this->endOfChain === true) {
throw $this->reason;
}
}

public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
{
$this->endOfChain = false;

if (null === $onRejected) {
return $this;
}
Expand All @@ -33,6 +43,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):

public function catch(callable $onRejected): PromiseInterface
{
$this->endOfChain = false;

if (!_checkTypehint($onRejected, $this->reason)) {
return $this;
}
Expand All @@ -42,6 +54,8 @@ public function catch(callable $onRejected): PromiseInterface

public function finally(callable $onFulfilledOrRejected): PromiseInterface
{
$this->endOfChain = false;

return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
return resolve($onFulfilledOrRejected())->then(function () use ($reason): PromiseInterface {
return new RejectedPromise($reason);
Expand Down
13 changes: 10 additions & 3 deletions tests/DeferredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public function getPromiseTestAdapter(callable $canceller = null)
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
{
$this->expectException(\Exception::class);

gc_collect_cycles();
$deferred = new Deferred(function ($resolve, $reject) {
$reject(new \Exception('foo'));
Expand All @@ -36,6 +38,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
{
$this->expectException(\Exception::class);

gc_collect_cycles();
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on

Expand All @@ -54,9 +58,12 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
gc_collect_cycles();
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on

$deferred = new Deferred(function () use (&$deferred) { });
$deferred->reject(new \Exception('foo'));
unset($deferred);
try {
$deferred = new Deferred(function () use (&$deferred) {
});
$deferred->reject(new \Exception('foo'));
unset($deferred);
} catch (\Throwable $throwable) {}

$this->assertSame(0, gc_collect_cycles());
}
Expand Down
8 changes: 5 additions & 3 deletions tests/FunctionAnyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,18 @@ public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDefer
/** @test */
public function shouldResolveWhenFirstInputPromiseResolves()
{
$exception2 = new Exception();
$exception3 = new Exception();
$this->expectException(\Exception::class);

$rejectedPromise2 = reject(new Exception());
$rejectedPromise3 = reject(new Exception());

$mock = $this->createCallableMock();
$mock
->expects(self::once())
->method('__invoke')
->with(self::identicalTo(1));

any([resolve(1), reject($exception2), reject($exception3)])
any([resolve(1), $rejectedPromise2, $rejectedPromise3])
->then($mock);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/FunctionRaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfill
/** @test */
public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects()
{
$this->expectException(Exception::class);

$deferred = new Deferred($this->expectCallableNever());
$deferred->reject(new Exception());

Expand Down
1 change: 0 additions & 1 deletion tests/Internal/CancellationQueueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public function doesNotCallCancelTwiceWhenStartedTwice()
*/
public function rethrowsExceptionsThrownFromCancel()
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('test');
$mock = $this->createCallableMock();
$mock
Expand Down
43 changes: 34 additions & 9 deletions tests/PromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithE
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function () {
throw new \Exception('foo');
Expand All @@ -74,6 +76,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function ($resolve, $reject) {
$reject(new \Exception('foo'));
Expand All @@ -86,6 +90,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
$reject(new \Exception('foo'));
Expand All @@ -99,6 +105,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
$reject(new \Exception('foo'));
Expand All @@ -112,6 +120,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function ($resolve, $reject) {
throw new \Exception('foo');
Expand All @@ -136,6 +146,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
*/
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function () {}, function () use (&$promise) {
throw new \Exception('foo');
Expand All @@ -153,10 +165,14 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
*/
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function () use (&$promise) {
throw new \Exception('foo');
});
try {
$promise = new Promise(function () use (&$promise) {
throw new \Exception('foo');
});
} catch (\Throwable $throwable) {}
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -169,10 +185,15 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
*/
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function () {
throw new \Exception('foo');
}, function () use (&$promise) { });
try {
$promise = new Promise(function () {
throw new \Exception('foo');
}, function () use (&$promise) {
});
} catch (\Throwable $throwable) {}
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand Down Expand Up @@ -263,10 +284,14 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPro
/** @test */
public function shouldFulfillIfFullfilledWithSimplePromise()
{
$this->expectException(Exception::class);

gc_collect_cycles();
$promise = new Promise(function () {
throw new Exception('foo');
});
try {
$promise = new Promise(function () {
throw new Exception('foo');
});
} catch (\Throwable $throwable) {}
unset($promise);

self::assertSame(0, gc_collect_cycles());
Expand Down
14 changes: 7 additions & 7 deletions tests/PromiseTest/CancelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,18 @@ public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows()
/** @test */
public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves()
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->will($this->returnCallback(function ($resolve) {
$resolve(null);
}));
$count = 0;
$mock = static function ($resolve) use (&$count) {
$resolve(null);
$count++;
};

$adapter = $this->getPromiseTestAdapter($mock);

$adapter->promise()->cancel();
$adapter->promise()->cancel();

self::assertSame(1, $count);
}

/** @test */
Expand Down
3 changes: 2 additions & 1 deletion tests/PromiseTest/FullTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ trait FullTestTrait
PromiseRejectedTestTrait,
ResolveTestTrait,
RejectTestTrait,
CancelTestTrait;
CancelTestTrait,
PromiseLastInChainTestTrait;
}
84 changes: 84 additions & 0 deletions tests/PromiseTest/PromiseLastInChainTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace React\Promise\PromiseTest;

use React\Promise\PromiseAdapter\PromiseAdapterInterface;
use React\Promise\PromiseInterface;

use function React\Promise\reject;

trait PromiseLastInChainTestTrait
{
/**
* @return PromiseAdapterInterface
*/
abstract public function getPromiseTestAdapter(callable $canceller = null);

/** @test */
public function notResolvedOrNotRejectedPromiseShouldNoThrow()
{
$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->then($this->expectCallableNever(), $this->expectCallableNever());

self::assertTrue(true);
}

/** @test */
public function unresolvedOrRejectedPromiseShouldNoThrow()
{
$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->then($this->expectCallableOnce(), $this->expectCallableNever());

$adapter->resolve(true);

self::assertTrue(true);
}

/** @test */
public function throwWhenLastInChainWhenRejected()
{
$this->expectException(\Exception::class);

$adapter = $this->getPromiseTestAdapter();

$adapter->reject(new \Exception('Boom!'));
}

/** @test */
public function doNotThrowWhenLastInChainWhenRejectedAndTheRejectionIsHandled()
{
$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->then($this->expectCallableNever(), $this->expectCallableOnce());

$adapter->reject(new \Exception('Boom!'));
}

/** @test */
public function throwWhenLastInChainWhenRejectedTransformedFromResolvedPromiseIntoRejected()
{
$this->expectException(\Exception::class);

$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->then(static function (string $message): PromiseInterface {
return reject(new \Exception($message));
}, $this->expectCallableNever());

$adapter->resolve('Boom!');
}

/** @test */
public function doNotThrowWhenLastInChainWhenRejectedAndTheRejectionIsHandledTransformedFromResolvedPromiseIntoRejected()
{
$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->then(static function (string $message): PromiseInterface {
return reject(new \Exception($message));
}, $this->expectCallableNever())->then($this->expectCallableNever(), $this->expectCallableOnce());

$adapter->resolve('Boom!');
}
}
Loading