Skip to content

Commit

Permalink
Throw when end of chain has been reached
Browse files Browse the repository at this point in the history
  • Loading branch information
WyriHaximus committed Jun 14, 2023
1 parent 6019855 commit b615618
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 43 deletions.
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;
}
56 changes: 56 additions & 0 deletions tests/PromiseTest/PromiseLastInChainTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace React\Promise\PromiseTest;

use React\Promise\PromiseAdapter\PromiseAdapterInterface;

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

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

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

self::assertTrue(true);
}

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

$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()
{
$caught = false;
$adapter = $this->getPromiseTestAdapter();

$adapter->promise()->catch(static function () use (&$caught) {
$caught = true;
});
$adapter->reject(new \Exception('Boom!'));
self::assertTrue($caught);
}
}
20 changes: 13 additions & 7 deletions tests/PromiseTest/PromiseRejectedTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public function rejectedPromiseShouldBeImmutable()

$adapter->reject($exception1);
$adapter->reject($exception2);

$adapter->promise()
->then(
$this->expectCallableNever(),
Expand All @@ -46,14 +45,13 @@ public function rejectedPromiseShouldInvokeNewlyAddedCallback()

$exception = new Exception();

$adapter->reject($exception);

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

$adapter->reject($exception);
$adapter->promise()
->then($this->expectCallableNever(), $mock);
}
Expand Down Expand Up @@ -264,7 +262,7 @@ public function catchShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehint
$adapter->promise()
->catch(function (InvalidArgumentException $reason) use ($mock) {
$mock($reason);
});
})->then(null, $this->expectCallableOnce());
}

/** @test */
Expand Down Expand Up @@ -375,19 +373,27 @@ public function finallyShouldRejectWhenHandlerRejectsForRejectedPromise()
/** @test */
public function cancelShouldReturnNullForRejectedPromise()
{
$this->expectException(Exception::class);

$adapter = $this->getPromiseTestAdapter();

$adapter->reject(new Exception());
try {
$adapter->reject(new Exception());
} catch (\Throwable $throwable) {}

self::assertNull($adapter->promise()->cancel());
}

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

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

$adapter->reject(new Exception());
try {
$adapter->reject(new Exception());
} catch (\Throwable $throwable) {}

$adapter->promise()->cancel();
}
Expand Down Expand Up @@ -474,7 +480,7 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
$adapter->promise()
->otherwise(function (InvalidArgumentException $reason) use ($mock) {
$mock($reason);
});
})->then(null, $this->expectCallableOnce());
}

/**
Expand Down
Loading

0 comments on commit b615618

Please sign in to comment.