Skip to content

Commit

Permalink
FU report unhandled rejections
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Jan 16, 2019
1 parent 43896aa commit 0feb517
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 24 deletions.
38 changes: 38 additions & 0 deletions src/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
class RejectedPromise implements ExtendedPromiseInterface, CancellablePromiseInterface
{
private $reason;
private $handled = false;

public function __construct($reason = null)
{
Expand All @@ -15,12 +16,47 @@ public function __construct($reason = null)
$this->reason = $reason;
}

public function __destruct()
{
if ($this->handled) {
return;
}

$message = 'Unhandled promise rejection with ';

if ($this->reason instanceof \Throwable || $this->reason instanceof \Exception) {
$message .= get_class($this->reason) . ': ' . $this->reason->getMessage();
$message .= ' raised in ' . $this->reason->getFile() . ' on line ' . $this->reason->getLine();
$message .= PHP_EOL . $this->reason->getTraceAsString();
} else {
if ($this->reason === null) {
$message .= 'null';
} else {
$message .= (is_object($this->reason) ? get_class($this->reason) : gettype($this->reason));
}

$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if (isset($trace[0]['file'], $trace[0]['line'])) {
$message .= ' detected in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'];
}

ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$message .= PHP_EOL . ob_get_clean();
}

$message .= PHP_EOL;
echo $message;
}

public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
{
if (null === $onRejected) {
return $this;
}

$this->handled = true;

try {
return resolve($onRejected($this->reason));
} catch (\Throwable $exception) {
Expand All @@ -32,6 +68,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,

public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
{
$this->handled = true;

if (null === $onRejected) {
throw UnhandledRejectionException::resolve($this->reason);
}
Expand Down
4 changes: 3 additions & 1 deletion tests/DeferredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
$deferred = new Deferred(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$deferred->promise()->then(null, function () { });
$deferred->promise()->cancel();
unset($deferred);

Expand All @@ -60,7 +61,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
$deferred = new Deferred(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$deferred->promise()->then()->cancel();
$deferred->promise()->then(null, function () { })->cancel();
unset($deferred);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -72,6 +73,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
gc_collect_cycles();
$deferred = new Deferred(function () use (&$deferred) { });
$deferred->reject(new \Exception('foo'));
$deferred->promise()->then(null, function () { });
unset($deferred);

$this->assertSame(0, gc_collect_cycles());
Expand Down
6 changes: 4 additions & 2 deletions tests/FulfilledPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToFulfilledP
{
gc_collect_cycles();
$promise = new FulfilledPromise(1);
$promise->always(function () {
$ret = $promise->always(function () {
throw new \RuntimeException();
});
unset($promise);
$ret->then(null, function () { });
unset($ret, $promise);

$this->assertSame(0, gc_collect_cycles());
}
Expand All @@ -69,6 +70,7 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToFulfilledP
$promise = $promise->then(function () {
throw new \RuntimeException();
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand Down
2 changes: 1 addition & 1 deletion tests/FunctionRaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,6 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects
->expects($this->never())
->method('cancel');

race([$deferred->promise(), $mock2])->cancel();
race([$deferred->promise(), $mock2])->then(null, function () { })->cancel();
}
}
4 changes: 3 additions & 1 deletion tests/FunctionSomeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesRej
->expects($this->never())
->method('cancel');

some([$deferred->promise(), $mock2], 2);
$ret = some([$deferred->promise(), $mock2], 2);

$ret->then(null, function () { });
}
}
11 changes: 9 additions & 2 deletions tests/PromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function () {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -79,6 +80,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
$promise = new Promise(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -91,6 +93,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$promise->then(null, function () { });
$promise->cancel();
unset($promise);

Expand All @@ -104,7 +107,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$promise->then()->then()->then()->cancel();
$promise->then()->then()->then(null, function () { })->cancel();
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -117,6 +120,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function ($resolve, $reject) {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -142,6 +146,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
throw new \Exception('foo');
});
$promise->cancel();
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -158,6 +163,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
$promise = new Promise(function () use (&$promise) {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -174,6 +180,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
$promise = new Promise(function () {
throw new \Exception('foo');
}, function () use (&$promise) { });
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -187,7 +194,7 @@ public function shouldIgnoreNotifyAfterReject()
$notify(42);
});

$promise->then(null, null, $this->expectCallableNever());
$promise->then(null, function () { }, $this->expectCallableNever());
$promise->cancel();
}

Expand Down
14 changes: 6 additions & 8 deletions tests/PromiseTest/CancelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,13 @@ public function cancelShouldProgressPromiseIfCancellerNotifies()
/** @test */
public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves()
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->will($this->returnCallback(function ($resolve) {
$resolve();
}));
$once = $this->expectCallableOnce();
$canceller = function ($resolve) use ($once) {
$resolve();
$once();
};

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

$adapter->promise()->cancel();
$adapter->promise()->cancel();
Expand Down
4 changes: 4 additions & 0 deletions tests/PromiseTest/NotifyTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ public function notifyShouldAllowRejectAfterProgress()

$adapter->notify(1);
$adapter->reject(2);

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -289,6 +291,8 @@ public function notifyShouldReturnSilentlyOnProgressWhenAlreadyRejected()
$adapter->reject(1);

$this->assertNull($adapter->notify());

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand Down
23 changes: 20 additions & 3 deletions tests/PromiseTest/PromiseRejectedTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ public function doneShouldInvokeRejectionHandlerForRejectedPromise()

$adapter->reject(1);
$this->assertNull($adapter->promise()->done(null, $mock));

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand Down Expand Up @@ -251,7 +253,11 @@ public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRe

$adapter->reject(1);
$this->assertNull($adapter->promise()->done(null, function () {
return \React\Promise\reject();
$reject = \React\Promise\reject();

$reject->then(null, function () { });

return $reject;
}));
}

Expand All @@ -264,7 +270,11 @@ public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWith

$adapter->reject(1);
$this->assertNull($adapter->promise()->done(null, function () {
return \React\Promise\reject(new \Exception('UnhandledRejectionException'));
$reject = \React\Promise\reject(new \Exception('UnhandledRejectionException'));

$reject->then(null, function () { });

return $reject;
}));
}

Expand Down Expand Up @@ -379,10 +389,13 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
$mock = $this->expectCallableNever();

$adapter->reject($exception);
$adapter->promise()
$ret = $adapter->promise()
->otherwise(function (\InvalidArgumentException $reason) use ($mock) {
$mock($reason);
});

$ret->then(null, function () { });
$adapter->promise()->then(null, function () { });
}

/** @test */
Expand Down Expand Up @@ -498,6 +511,8 @@ public function cancelShouldReturnNullForRejectedPromise()
$adapter->reject();

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

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -508,5 +523,7 @@ public function cancelShouldHaveNoEffectForRejectedPromise()
$adapter->reject();

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

$adapter->promise()->then(null, function () { });
}
}
14 changes: 13 additions & 1 deletion tests/PromiseTest/PromiseSettledTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public function thenShouldReturnAPromiseForSettledPromise()

$adapter->settle();
$this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then());

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -25,6 +27,8 @@ public function thenShouldReturnAllowNullForSettledPromise()

$adapter->settle();
$this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then(null, null, null));

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -35,6 +39,8 @@ public function cancelShouldReturnNullForSettledPromise()
$adapter->settle();

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

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -45,6 +51,8 @@ public function cancelShouldHaveNoEffectForSettledPromise()
$adapter->settle();

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

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand Down Expand Up @@ -73,6 +81,8 @@ public function progressShouldNotInvokeProgressHandlerForSettledPromise()
$adapter->settle();
$adapter->promise()->progress($this->expectCallableNever());
$adapter->notify();

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -81,6 +91,8 @@ public function alwaysShouldReturnAPromiseForSettledPromise()
$adapter = $this->getPromiseTestAdapter();

$adapter->settle();
$this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->always(function () {}));
$this->assertInstanceOf('React\\Promise\\PromiseInterface', $ret = $adapter->promise()->always(function () {}));

$ret->then(null, function () { });
}
}
12 changes: 10 additions & 2 deletions tests/PromiseTest/RejectTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRe
$this->setExpectedException('React\\Promise\\UnhandledRejectionException');

$this->assertNull($adapter->promise()->done(null, function () {
return \React\Promise\reject();
$reject = \React\Promise\reject();

$reject->then(null, function () { });

return $reject;
}));
$adapter->reject(1);
}
Expand All @@ -189,7 +193,11 @@ public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWith
$this->setExpectedException('\Exception', 'UnhandledRejectionException');

$this->assertNull($adapter->promise()->done(null, function () {
return \React\Promise\reject(new \Exception('UnhandledRejectionException'));
$reject = \React\Promise\reject(new \Exception('UnhandledRejectionException'));

$reject->then(null, function () { });

return $reject;
}));
$adapter->reject(1);
}
Expand Down
Loading

0 comments on commit 0feb517

Please sign in to comment.