Dustin Wilson
1 year ago
8 changed files with 299 additions and 28 deletions
@ -0,0 +1,11 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher; |
|||
|
|||
class RangeException extends \RangeException {} |
@ -0,0 +1,194 @@ |
|||
<?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\{ |
|||
Error, |
|||
Handler, |
|||
RangeException, |
|||
ThrowableController |
|||
}; |
|||
use Psr\Log\LoggerInterface, |
|||
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 |
|||
]); |
|||
} |
|||
|
|||
/** @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(); |
|||
$this->assertEquals(0, count($r->getValue($this->handler))); |
|||
$h(); |
|||
$this->assertEquals(0, count($r->getValue($this->handler))); |
|||
} |
|||
|
|||
/** @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(); |
|||
$this->assertNotNull($o); |
|||
$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']); |
|||
} |
|||
|
|||
|
|||
public function testFatalError(): void { |
|||
$this->expectException(RangeException::class); |
|||
$this->handler->setOption('httpCode', 42); |
|||
} |
|||
|
|||
/** @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()); |
|||
} |
|||
|
|||
|
|||
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 ] ], |
|||
[ new Error('Ook!', \E_ERROR, '/dev/null', 42, new \Error('Eek!')), Handler::BUBBLES | Handler::OUTPUT | Handler::NOW, [ 'forceOutputNow' => true ] ], |
|||
[ 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 = [ |
|||
[ new Error('Ook!', \E_NOTICE, '/dev/null', 0, new \Error('Eek!')), 'notice' ], |
|||
[ new Error('Ook!', \E_USER_NOTICE, '/dev/null', 0), 'notice' ], |
|||
[ new Error('Ook!', \E_STRICT, '/dev/null', 0, new \Error('Eek!')), 'notice' ], |
|||
[ new Error('Ook!', \E_WARNING, '/dev/null', 0), 'warning' ], |
|||
[ new Error('Ook!', \E_COMPILE_WARNING, '/dev/null', 0, new \Error('Eek!')), 'warning' ], |
|||
[ new Error('Ook!', \E_USER_WARNING, '/dev/null', 0), 'warning' ], |
|||
[ new Error('Ook!', \E_DEPRECATED, '/dev/null', 0, new \Error('Eek!')), 'warning' ], |
|||
[ new Error('Ook!', \E_USER_DEPRECATED, '/dev/null', 0), 'warning' ], |
|||
[ new Error('Ook!', \E_PARSE, '/dev/null', 0, new \Error('Eek!')), 'critical' ], |
|||
[ new Error('Ook!', \E_CORE_ERROR, '/dev/null', 0), 'critical' ], |
|||
[ new Error('Ook!', \E_COMPILE_ERROR, '/dev/null', 0, new \Error('Eek!')), 'critical' ], |
|||
[ new Error('Ook!', \E_ERROR, '/dev/null', 0), 'error' ], |
|||
[ new Error('Ook!', \E_USER_ERROR, '/dev/null', 0, new \Error('Eek!')), 'error' ], |
|||
[ 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 ], |
|||
[ 'logger', Phake::mock(LoggerInterface::class) ], |
|||
[ 'logWhenSilent', false ], |
|||
[ 'outputBacktrace', true ], |
|||
[ 'outputPrevious', false ], |
|||
[ 'outputTime', false ], |
|||
[ 'outputToStderr', false ], |
|||
[ 'silent', true ], |
|||
[ 'timeFormat', 'Y-m-d' ], |
|||
[ 'varExporter', fn(mixed $value): string|bool => var_export($value, true) ] |
|||
]; |
|||
|
|||
foreach ($options as $o) { |
|||
yield $o; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
|
|||
class Error extends \Error {} |
@ -0,0 +1,31 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
|
|||
|
|||
class ErrorHandlingTestCase extends \PHPUnit\Framework\TestCase { |
|||
protected ?Error $lastError = null; |
|||
|
|||
|
|||
public function setUp(): void { |
|||
set_error_handler([ $this, 'handleError' ]); |
|||
} |
|||
|
|||
public function tearDown(): void { |
|||
restore_error_handler(); |
|||
} |
|||
|
|||
public function handleError(int $code, string $message, string $file, int $line): void { |
|||
$e = new Error($message, $code); |
|||
$this->lastError = $e; |
|||
if (in_array($code, [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR, \E_RECOVERABLE_ERROR ])) { |
|||
throw $e; |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue