Skip to content

Commit b160d29

Browse files
committed
Throw when end of chain has been reached
1 parent 6019855 commit b160d29

File tree

12 files changed

+193
-43
lines changed

12 files changed

+193
-43
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jobs:
99
name: PHPUnit (PHP ${{ matrix.php }})
1010
runs-on: ubuntu-22.04
1111
strategy:
12+
fail-fast: false
1213
matrix:
1314
php:
1415
- 8.2

src/Internal/RejectedPromise.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
final class RejectedPromise implements PromiseInterface
1313
{
1414
private $reason;
15+
private $endOfChain = true;
1516

1617
public function __construct(\Throwable $reason)
1718
{
1819
$this->reason = $reason;
1920
}
2021

22+
public function __destruct()
23+
{
24+
if ($this->endOfChain === true) {
25+
throw $this->reason;
26+
}
27+
}
28+
2129
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
2230
{
31+
$this->endOfChain = false;
32+
2333
if (null === $onRejected) {
2434
return $this;
2535
}
@@ -33,6 +43,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
3343

3444
public function catch(callable $onRejected): PromiseInterface
3545
{
46+
$this->endOfChain = false;
47+
3648
if (!_checkTypehint($onRejected, $this->reason)) {
3749
return $this;
3850
}
@@ -42,6 +54,8 @@ public function catch(callable $onRejected): PromiseInterface
4254

4355
public function finally(callable $onFulfilledOrRejected): PromiseInterface
4456
{
57+
$this->endOfChain = false;
58+
4559
return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
4660
return resolve($onFulfilledOrRejected())->then(function () use ($reason): PromiseInterface {
4761
return new RejectedPromise($reason);

tests/DeferredTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public function getPromiseTestAdapter(callable $canceller = null)
2323
/** @test */
2424
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
2525
{
26+
$this->expectException(\Exception::class);
27+
2628
gc_collect_cycles();
2729
$deferred = new Deferred(function ($resolve, $reject) {
2830
$reject(new \Exception('foo'));
@@ -36,6 +38,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
3638
/** @test */
3739
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
3840
{
41+
$this->expectException(\Exception::class);
42+
3943
gc_collect_cycles();
4044
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
4145

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

57-
$deferred = new Deferred(function () use (&$deferred) { });
58-
$deferred->reject(new \Exception('foo'));
59-
unset($deferred);
61+
try {
62+
$deferred = new Deferred(function () use (&$deferred) {
63+
});
64+
$deferred->reject(new \Exception('foo'));
65+
unset($deferred);
66+
} catch (\Throwable $throwable) {}
6067

6168
$this->assertSame(0, gc_collect_cycles());
6269
}

tests/FunctionAnyTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,18 @@ public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDefer
170170
/** @test */
171171
public function shouldResolveWhenFirstInputPromiseResolves()
172172
{
173-
$exception2 = new Exception();
174-
$exception3 = new Exception();
173+
$this->expectException(\Exception::class);
174+
175+
$rejectedPromise2 = reject(new Exception());
176+
$rejectedPromise3 = reject(new Exception());
175177

176178
$mock = $this->createCallableMock();
177179
$mock
178180
->expects(self::once())
179181
->method('__invoke')
180182
->with(self::identicalTo(1));
181183

182-
any([resolve(1), reject($exception2), reject($exception3)])
184+
any([resolve(1), $rejectedPromise2, $rejectedPromise3])
183185
->then($mock);
184186
}
185187

tests/FunctionRaceTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfill
149149
/** @test */
150150
public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects()
151151
{
152+
$this->expectException(Exception::class);
153+
152154
$deferred = new Deferred($this->expectCallableNever());
153155
$deferred->reject(new Exception());
154156

tests/Internal/CancellationQueueTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public function doesNotCallCancelTwiceWhenStartedTwice()
8080
*/
8181
public function rethrowsExceptionsThrownFromCancel()
8282
{
83-
$this->expectException(Exception::class);
8483
$this->expectExceptionMessage('test');
8584
$mock = $this->createCallableMock();
8685
$mock

tests/PromiseTest.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithE
6262
/** @test */
6363
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver()
6464
{
65+
$this->expectException(Exception::class);
66+
6567
gc_collect_cycles();
6668
$promise = new Promise(function () {
6769
throw new \Exception('foo');
@@ -74,6 +76,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
7476
/** @test */
7577
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException()
7678
{
79+
$this->expectException(Exception::class);
80+
7781
gc_collect_cycles();
7882
$promise = new Promise(function ($resolve, $reject) {
7983
$reject(new \Exception('foo'));
@@ -86,6 +90,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
8690
/** @test */
8791
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
8892
{
93+
$this->expectException(Exception::class);
94+
8995
gc_collect_cycles();
9096
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
9197
$reject(new \Exception('foo'));
@@ -99,6 +105,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
99105
/** @test */
100106
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
101107
{
108+
$this->expectException(Exception::class);
109+
102110
gc_collect_cycles();
103111
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
104112
$reject(new \Exception('foo'));
@@ -112,6 +120,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
112120
/** @test */
113121
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
114122
{
123+
$this->expectException(Exception::class);
124+
115125
gc_collect_cycles();
116126
$promise = new Promise(function ($resolve, $reject) {
117127
throw new \Exception('foo');
@@ -136,6 +146,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
136146
*/
137147
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException()
138148
{
149+
$this->expectException(Exception::class);
150+
139151
gc_collect_cycles();
140152
$promise = new Promise(function () {}, function () use (&$promise) {
141153
throw new \Exception('foo');
@@ -153,10 +165,14 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
153165
*/
154166
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException()
155167
{
168+
$this->expectException(Exception::class);
169+
156170
gc_collect_cycles();
157-
$promise = new Promise(function () use (&$promise) {
158-
throw new \Exception('foo');
159-
});
171+
try {
172+
$promise = new Promise(function () use (&$promise) {
173+
throw new \Exception('foo');
174+
});
175+
} catch (\Throwable $throwable) {}
160176
unset($promise);
161177

162178
$this->assertSame(0, gc_collect_cycles());
@@ -169,10 +185,15 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
169185
*/
170186
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException()
171187
{
188+
$this->expectException(Exception::class);
189+
172190
gc_collect_cycles();
173-
$promise = new Promise(function () {
174-
throw new \Exception('foo');
175-
}, function () use (&$promise) { });
191+
try {
192+
$promise = new Promise(function () {
193+
throw new \Exception('foo');
194+
}, function () use (&$promise) {
195+
});
196+
} catch (\Throwable $throwable) {}
176197
unset($promise);
177198

178199
$this->assertSame(0, gc_collect_cycles());
@@ -263,10 +284,14 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPro
263284
/** @test */
264285
public function shouldFulfillIfFullfilledWithSimplePromise()
265286
{
287+
$this->expectException(Exception::class);
288+
266289
gc_collect_cycles();
267-
$promise = new Promise(function () {
268-
throw new Exception('foo');
269-
});
290+
try {
291+
$promise = new Promise(function () {
292+
throw new Exception('foo');
293+
});
294+
} catch (\Throwable $throwable) {}
270295
unset($promise);
271296

272297
self::assertSame(0, gc_collect_cycles());

tests/PromiseTest/CancelTestTrait.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,18 @@ public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows()
105105
/** @test */
106106
public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves()
107107
{
108-
$mock = $this->createCallableMock();
109-
$mock
110-
->expects($this->once())
111-
->method('__invoke')
112-
->will($this->returnCallback(function ($resolve) {
113-
$resolve(null);
114-
}));
108+
$count = 0;
109+
$mock = static function ($resolve) use (&$count) {
110+
$resolve(null);
111+
$count++;
112+
};
115113

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

118116
$adapter->promise()->cancel();
119117
$adapter->promise()->cancel();
118+
119+
self::assertSame(1, $count);
120120
}
121121

122122
/** @test */

tests/PromiseTest/FullTestTrait.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ trait FullTestTrait
1010
PromiseRejectedTestTrait,
1111
ResolveTestTrait,
1212
RejectTestTrait,
13-
CancelTestTrait;
13+
CancelTestTrait,
14+
PromiseLastInChainTestTrait;
1415
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace React\Promise\PromiseTest;
4+
5+
use React\Promise\PromiseAdapter\PromiseAdapterInterface;
6+
use React\Promise\PromiseInterface;
7+
8+
use function React\Promise\reject;
9+
10+
trait PromiseLastInChainTestTrait
11+
{
12+
/**
13+
* @return PromiseAdapterInterface
14+
*/
15+
abstract public function getPromiseTestAdapter(callable $canceller = null);
16+
17+
/** @test */
18+
public function notResolvedOrNotRejectedPromiseShouldNoThrow()
19+
{
20+
$adapter = $this->getPromiseTestAdapter();
21+
22+
$adapter->promise()->then($this->expectCallableNever(), $this->expectCallableNever());
23+
24+
self::assertTrue(true);
25+
}
26+
27+
/** @test */
28+
public function unresolvedOrRejectedPromiseShouldNoThrow()
29+
{
30+
$adapter = $this->getPromiseTestAdapter();
31+
32+
$adapter->promise()->then($this->expectCallableOnce(), $this->expectCallableNever());
33+
34+
$adapter->resolve(true);
35+
36+
self::assertTrue(true);
37+
}
38+
39+
/** @test */
40+
public function throwWhenLastInChainWhenRejected()
41+
{
42+
$this->expectException(\Exception::class);
43+
44+
$adapter = $this->getPromiseTestAdapter();
45+
46+
$adapter->reject(new \Exception('Boom!'));
47+
}
48+
49+
/** @test */
50+
public function doNotThrowWhenLastInChainWhenRejectedAndTheRejectionIsHandled()
51+
{
52+
$adapter = $this->getPromiseTestAdapter();
53+
54+
$adapter->promise()->then($this->expectCallableNever(), $this->expectCallableOnce());
55+
56+
$adapter->reject(new \Exception('Boom!'));
57+
}
58+
59+
/** @test */
60+
public function throwWhenLastInChainWhenRejectedTransformedFromResolvedPromiseIntoRejected()
61+
{
62+
$this->expectException(\Exception::class);
63+
64+
$adapter = $this->getPromiseTestAdapter();
65+
66+
$adapter->promise()->then(static function (string $message): PromiseInterface {
67+
return reject(new \Exception($message));
68+
}, $this->expectCallableNever());
69+
70+
$adapter->resolve('Boom!');
71+
}
72+
73+
/** @test */
74+
public function doNotThrowWhenLastInChainWhenRejectedAndTheRejectionIsHandledTransformedFromResolvedPromiseIntoRejected()
75+
{
76+
$adapter = $this->getPromiseTestAdapter();
77+
78+
$adapter->promise()->then(static function (string $message): PromiseInterface {
79+
return reject(new \Exception($message));
80+
}, $this->expectCallableNever())->then($this->expectCallableNever(), $this->expectCallableOnce());
81+
82+
$adapter->resolve('Boom!');
83+
}
84+
}

0 commit comments

Comments
 (0)