2023-04-23 14:12:21 -04:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* @license MIT
|
|
|
|
* Copyright 2022 Dustin Wilson, et al.
|
|
|
|
* See LICENSE and AUTHORS files for details
|
|
|
|
*/
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MensBeam\Catcher\Test;
|
|
|
|
use MensBeam\Catcher\{
|
2023-08-09 00:17:01 -04:00
|
|
|
Error as CatcherError,
|
2023-04-23 14:12:21 -04:00
|
|
|
Handler,
|
2023-11-08 23:25:54 -05:00
|
|
|
InvalidArgumentException,
|
2023-04-23 14:12:21 -04:00
|
|
|
RangeException,
|
|
|
|
ThrowableController
|
|
|
|
};
|
2023-07-04 12:19:44 -04:00
|
|
|
use MensBeam\Catcher,
|
|
|
|
Psr\Log\LoggerInterface,
|
2023-04-23 14:12:21 -04:00
|
|
|
Phake;
|
|
|
|
|
|
|
|
|
|
|
|
/** @covers \MensBeam\Catcher\Handler */
|
|
|
|
class TestHandler extends ErrorHandlingTestCase {
|
|
|
|
protected ?Handler $handler = null;
|
|
|
|
|
|
|
|
|
|
|
|
public function setUp(): void {
|
|
|
|
parent::setUp();
|
|
|
|
|
|
|
|
$this->handler = new TestingHandler([
|
|
|
|
'outputBacktrace' => true
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-07-05 16:24:47 -04:00
|
|
|
/** @dataProvider provideArgumentSerializationTests */
|
|
|
|
public function testArgumentSerialization(mixed $arg): void {
|
|
|
|
// This looks silly because the argument is never used, but the handler will
|
|
|
|
// pick it up and print it anyway which is what we're testing here
|
|
|
|
$this->handler->setOption('print', true);
|
|
|
|
$this->handler->setOption('printJSON', false);
|
|
|
|
$this->handler->setOption('outputToStderr', false);
|
|
|
|
$this->handler->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$h = $this->handler;
|
|
|
|
ob_start();
|
|
|
|
$h();
|
|
|
|
$o = ob_get_clean();
|
|
|
|
$this->assertNotEmpty($o);
|
|
|
|
}
|
|
|
|
|
2023-04-23 14:12:21 -04:00
|
|
|
/** @dataProvider provideHandlingTests */
|
|
|
|
public function testHandling(\Throwable $throwable, int $expectedCode, array $options = []): void {
|
|
|
|
foreach ($options as $k => $v) {
|
|
|
|
$this->handler->setOption($k, $v);
|
|
|
|
}
|
|
|
|
|
|
|
|
$o = $this->handler->handle(new ThrowableController($throwable));
|
|
|
|
$this->assertSame($throwable::class, $o['controller']->getThrowable()::class);
|
|
|
|
$this->assertEquals($expectedCode, $o['code']);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testInvocation(): void {
|
|
|
|
$this->handler->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$r = new \ReflectionProperty($this->handler::class, 'outputBuffer');
|
|
|
|
$r->setAccessible(true);
|
|
|
|
$this->assertEquals(1, count($r->getValue($this->handler)));
|
|
|
|
|
|
|
|
$h = $this->handler;
|
|
|
|
$h();
|
2023-11-08 23:25:54 -05:00
|
|
|
$this->assertEquals(0, count($r->getValue($this->handler)));
|
2023-04-23 14:12:21 -04:00
|
|
|
$h();
|
2023-11-08 23:25:54 -05:00
|
|
|
$this->assertEquals(0, count($r->getValue($this->handler)));
|
2023-04-23 14:12:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @dataProvider provideLogTests */
|
|
|
|
public function testLog(\Throwable $throwable, string $methodName): void {
|
|
|
|
$l = Phake::mock(LoggerInterface::class);
|
|
|
|
$this->handler->setOption('logger', $l);
|
|
|
|
$this->handler->handle(new ThrowableController($throwable));
|
|
|
|
$h = $this->handler;
|
|
|
|
$h();
|
|
|
|
Phake::verify($l, Phake::times(1))->$methodName;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @dataProvider provideOptionsTests */
|
|
|
|
public function testOptions(string $option, mixed $value): void {
|
|
|
|
$this->handler->setOption($option, $value);
|
|
|
|
$this->assertSame($value, $this->handler->getOption($option));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testPrinting(): void {
|
|
|
|
$this->handler->setOption('print', true);
|
|
|
|
$this->handler->setOption('outputToStderr', false);
|
|
|
|
$this->handler->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$h = $this->handler;
|
|
|
|
ob_start();
|
|
|
|
$h();
|
|
|
|
$o = ob_get_clean();
|
2023-07-04 12:19:44 -04:00
|
|
|
$this->assertNotEmpty($o);
|
2023-04-23 14:12:21 -04:00
|
|
|
$o = json_decode($o, true);
|
|
|
|
$this->assertSame(\Exception::class, $o['class']);
|
|
|
|
$this->assertSame(__FILE__, $o['file']);
|
|
|
|
$this->assertSame(__LINE__ - 9, $o['line']);
|
|
|
|
$this->assertSame('Ook!', $o['message']);
|
|
|
|
}
|
|
|
|
|
2023-07-04 12:19:44 -04:00
|
|
|
/** @dataProvider provideSupplementalErrorHandlingTests */
|
|
|
|
public function testSupplementalErrorHandling(\Closure $closure, bool $useCatcher, bool $silent): void {
|
|
|
|
if (!$silent) {
|
|
|
|
$this->handler->setOption('print', true);
|
|
|
|
$this->handler->setOption('printJSON', false);
|
|
|
|
$this->handler->setOption('outputToStderr', false);
|
|
|
|
} else {
|
|
|
|
$this->handler->setOption('silent', true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($useCatcher) {
|
|
|
|
$c = new Catcher($this->handler);
|
|
|
|
}
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
$closure($this->handler);
|
|
|
|
$o = ob_get_clean();
|
|
|
|
$this->assertNotEmpty($o);
|
|
|
|
|
|
|
|
if ($useCatcher) {
|
|
|
|
$c->unregister();
|
|
|
|
unset($c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-23 14:12:21 -04:00
|
|
|
|
2023-08-09 00:17:01 -04:00
|
|
|
/** @dataProvider provideFatalErrorTests */
|
|
|
|
public function testFatalErrors(string $throwableClassName, \Closure $closure): void {
|
|
|
|
$this->expectException($throwableClassName);
|
|
|
|
$closure($this->handler);
|
2023-04-23 14:12:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @dataProvider provideNonFatalErrorTests */
|
|
|
|
public function testNonFatalErrors(int $code, string $message, \Closure $closure): void {
|
|
|
|
$closure($this->handler);
|
|
|
|
$this->assertEquals($code, $this->lastError?->getCode());
|
|
|
|
$this->assertSame($message, $this->lastError?->getMessage());
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-09 00:17:01 -04:00
|
|
|
public static function provideFatalErrorTests(): iterable {
|
|
|
|
$iterable = [
|
|
|
|
[
|
|
|
|
RangeException::class,
|
|
|
|
function (Handler $h): void {
|
|
|
|
$h->setOption('httpCode', 42);
|
|
|
|
}
|
|
|
|
],
|
|
|
|
[
|
|
|
|
CatcherError::class,
|
|
|
|
function (Handler $h): void {
|
|
|
|
$c = new Catcher();
|
|
|
|
$l = new FailLogger();
|
|
|
|
$l->error('Ook!');
|
|
|
|
}
|
2023-11-08 23:25:54 -05:00
|
|
|
],
|
|
|
|
[
|
|
|
|
InvalidArgumentException::class,
|
|
|
|
function (Handler $h): void {
|
|
|
|
$h->setOption('ignore', [ \M_PI ]);
|
|
|
|
}
|
2023-08-09 00:17:01 -04:00
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($iterable as $i) {
|
|
|
|
yield $i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-05 16:24:47 -04:00
|
|
|
public static function provideArgumentSerializationTests(): iterable {
|
|
|
|
$options = [
|
|
|
|
[ fn() => true ],
|
|
|
|
[ new \stdClass() ],
|
|
|
|
[ new class{} ],
|
|
|
|
[ (object)[] ],
|
|
|
|
[ 'ook' ],
|
|
|
|
[ 42 ],
|
|
|
|
[ \M_PI ]
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($options as $o) {
|
|
|
|
yield $o;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-23 14:12:21 -04:00
|
|
|
public static function provideHandlingTests(): iterable {
|
|
|
|
$options = [
|
|
|
|
[ new \Exception('Ook!'), Handler::BUBBLES | Handler::OUTPUT | Handler::EXIT, [ 'forceExit' => true ] ],
|
|
|
|
[ new \Error('Ook!'), Handler::BUBBLES | Handler::OUTPUT ],
|
|
|
|
[ new \Exception('Ook!'), Handler::BUBBLES, [ 'silent' => true ] ],
|
2023-08-09 00:17:01 -04:00
|
|
|
[ new CatcherError('Ook!', \E_ERROR, '/dev/null', 42, new \Error('Eek!')), Handler::BUBBLES | Handler::OUTPUT | Handler::NOW, [ 'forceOutputNow' => true ] ],
|
2023-04-23 14:12:21 -04:00
|
|
|
[ new \Exception('Ook!'), Handler::BUBBLES, [ 'silent' => true, 'logger' => Phake::mock(LoggerInterface::class), 'logWhenSilent' => false ] ],
|
|
|
|
[ new \Error('Ook!'), Handler::BUBBLES | Handler::OUTPUT | Handler::LOG, [ 'forceOutputNow' => true, 'logger' => Phake::mock(LoggerInterface::class) ] ]
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($options as $o) {
|
|
|
|
// TestingHandler adds Handler::NOW to the output code to make
|
|
|
|
// testing with it in TestCatcher less of a pain, so it needs to be
|
|
|
|
// added here
|
|
|
|
$o[1] |= Handler::NOW;
|
|
|
|
yield $o;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function provideLogTests(): iterable {
|
|
|
|
$options = [
|
2023-08-09 00:17:01 -04:00
|
|
|
[ new CatcherError('Ook!', \E_NOTICE, '/dev/null', 0, new \Error('Eek!')), 'notice' ],
|
|
|
|
[ new CatcherError('Ook!', \E_USER_NOTICE, '/dev/null', 0), 'notice' ],
|
|
|
|
[ new CatcherError('Ook!', \E_STRICT, '/dev/null', 0, new \Error('Eek!')), 'notice' ],
|
|
|
|
[ new CatcherError('Ook!', \E_WARNING, '/dev/null', 0), 'warning' ],
|
|
|
|
[ new CatcherError('Ook!', \E_COMPILE_WARNING, '/dev/null', 0, new \Error('Eek!')), 'warning' ],
|
|
|
|
[ new CatcherError('Ook!', \E_USER_WARNING, '/dev/null', 0), 'warning' ],
|
|
|
|
[ new CatcherError('Ook!', \E_DEPRECATED, '/dev/null', 0, new \Error('Eek!')), 'warning' ],
|
|
|
|
[ new CatcherError('Ook!', \E_USER_DEPRECATED, '/dev/null', 0), 'warning' ],
|
|
|
|
[ new CatcherError('Ook!', \E_PARSE, '/dev/null', 0, new \Error('Eek!')), 'critical' ],
|
|
|
|
[ new CatcherError('Ook!', \E_CORE_ERROR, '/dev/null', 0), 'critical' ],
|
|
|
|
[ new CatcherError('Ook!', \E_COMPILE_ERROR, '/dev/null', 0, new \Error('Eek!')), 'critical' ],
|
|
|
|
[ new CatcherError('Ook!', \E_ERROR, '/dev/null', 0), 'error' ],
|
|
|
|
[ new CatcherError('Ook!', \E_USER_ERROR, '/dev/null', 0, new \Error('Eek!')), 'error' ],
|
2023-04-23 14:12:21 -04:00
|
|
|
[ new \PharException('Ook!'), 'alert' ],
|
|
|
|
[ new \Exception('Ook!'), 'critical' ],
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($options as $o) {
|
|
|
|
yield $o;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function provideNonFatalErrorTests(): iterable {
|
|
|
|
$iterable = [
|
|
|
|
[
|
|
|
|
\E_USER_WARNING,
|
|
|
|
'Undefined option in ' . TestingHandler::class . ': ook',
|
|
|
|
function (Handler $h): void {
|
|
|
|
$h->getOption('ook');
|
|
|
|
}
|
|
|
|
],
|
|
|
|
[
|
|
|
|
\E_USER_WARNING,
|
|
|
|
'Undefined option in ' . TestingHandler::class . ': ook',
|
|
|
|
function (Handler $h): void {
|
|
|
|
$h->setOption('ook', 'eek');
|
|
|
|
}
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($iterable as $i) {
|
|
|
|
yield $i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function provideOptionsTests(): iterable {
|
|
|
|
$options = [
|
|
|
|
[ 'backtraceArgFrameLimit', 42 ],
|
|
|
|
[ 'bubbles', false ],
|
|
|
|
[ 'charset', 'UTF-16' ],
|
|
|
|
[ 'forceExit', true ],
|
|
|
|
[ 'forceOutputNow', true ],
|
|
|
|
[ 'httpCode', 200 ],
|
|
|
|
[ 'httpCode', 400 ],
|
|
|
|
[ 'httpCode', 502 ],
|
2023-11-08 23:25:54 -05:00
|
|
|
[ 'ignore', [ \Exception::class, \E_USER_ERROR ] ],
|
2023-04-23 14:12:21 -04:00
|
|
|
[ 'logger', Phake::mock(LoggerInterface::class) ],
|
|
|
|
[ 'logWhenSilent', false ],
|
|
|
|
[ 'outputBacktrace', true ],
|
|
|
|
[ 'outputPrevious', false ],
|
|
|
|
[ 'outputTime', false ],
|
|
|
|
[ 'outputToStderr', false ],
|
|
|
|
[ 'silent', true ],
|
2023-07-05 16:24:47 -04:00
|
|
|
[ 'timeFormat', 'Y-m-d' ]
|
2023-04-23 14:12:21 -04:00
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($options as $o) {
|
|
|
|
yield $o;
|
|
|
|
}
|
|
|
|
}
|
2023-07-04 12:19:44 -04:00
|
|
|
|
|
|
|
public static function provideSupplementalErrorHandlingTests(): iterable {
|
|
|
|
$iterable = [
|
|
|
|
// Test with a logger that errors without a Catcher
|
|
|
|
[ function (Handler $h): void {
|
|
|
|
$h->setOption('logger', new FailLogger());
|
|
|
|
$h->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$h();
|
|
|
|
}, false, false ],
|
|
|
|
// Test with a logger that errors with a Catcher
|
|
|
|
[ function (Handler $h): void {
|
|
|
|
$h->setOption('logger', new FailLogger());
|
|
|
|
$h->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$h();
|
|
|
|
}, true, false ],
|
|
|
|
// Test with a logger that errors with a Catcher but silent
|
|
|
|
[ function (Handler $h): void {
|
|
|
|
$h->setOption('logger', new FailLogger());
|
|
|
|
$h->handle(new ThrowableController(new \Exception('Ook!')));
|
|
|
|
$h();
|
|
|
|
}, true, true ]
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($iterable as $i) {
|
|
|
|
yield $i;
|
|
|
|
}
|
|
|
|
}
|
2023-04-23 14:12:21 -04:00
|
|
|
}
|