Browse Source

Handler tests

2.1.0
Dustin Wilson 1 year ago
parent
commit
c2e06bb15c
  1. 50
      lib/Catcher/Handler.php
  2. 11
      lib/Catcher/RangeException.php
  3. 2
      tests/bootstrap.php
  4. 16
      tests/cases/TestCatcher.php
  5. 194
      tests/cases/TestHandler.php
  6. 11
      tests/lib/Error.php
  7. 31
      tests/lib/ErrorHandlingTestCase.php
  8. 12
      tests/lib/TestingHandler.php

50
lib/Catcher/Handler.php

@ -36,7 +36,7 @@ abstract class Handler {
* an error occurred
*/
protected string $_charset = 'UTF-8';
/** If true the handler will force an exit */
/** If true the handler will force an exit after all handlers have run */
protected bool $_forceExit = false;
/** If true the handler will output as soon as possible, unless silenced */
protected bool $_forceOutputNow = false;
@ -70,12 +70,7 @@ abstract class Handler {
public function __construct(array $options = []) {
foreach ($options as $key => $value) {
$key = "_$key";
if ($key === '_httpCode' && is_int($value) && $value !== 200 && max(400, min($value, 600)) !== $value) {
throw new \RangeException('Option "httpCode" can only be an integer of 200 or 400-599');
}
$this->$key = $value;
$this->setOption($key, $value);
}
if ($this->_varExporter === null) {
@ -93,8 +88,7 @@ abstract class Handler {
// Send the headers if possible and necessary
if (isset($_SERVER['REQUEST_URI'])) {
// Can't figure out a way to test coverage here, but the logic is tested thoroughly
// when running tests in HTTP
// Can't figure out a way to test coverage here
// @codeCoverageIgnoreStart
if (!headers_sent()) {
header_remove('location');
@ -159,6 +153,22 @@ abstract class Handler {
return;
}
if (
$name === 'httpCode' &&
is_int($value) &&
$value !== 200 &&
max(400, min($value, 418)) !== $value &&
max(421, min($value, 429)) !== $value &&
$value !== 431 &&
$value !== 451 &&
max(500, min($value, 511)) !== $value &&
// Cloudflare extensions
max(520, min($value, 527)) !== $value &&
$value !== 530
) {
throw new RangeException('Option "httpCode" can only be a valid HTTP 200, 4XX, or 5XX code');
}
$name = "_$name";
$this->$name = $value;
}
@ -192,8 +202,7 @@ abstract class Handler {
protected function cleanOutputThrowable(array $outputThrowable): array {
unset($outputThrowable['controller']);
unset($outputThrowable['controlCode']);
unset($outputThrowable['outputCode']);
unset($outputThrowable['code']);
if (isset($outputThrowable['previous'])) {
$outputThrowable['previous'] = $this->cleanOutputThrowable($outputThrowable['previous']);
@ -205,10 +214,8 @@ abstract class Handler {
return $outputThrowable;
}
protected function handleCallback(array $output): array {
return $output;
}
abstract protected function handleCallback(array $output): array;
abstract protected function invokeCallback(): void;
protected function log(\Throwable $throwable, string $message): void {
@ -227,17 +234,14 @@ abstract class Handler {
case \E_USER_DEPRECATED:
$this->_logger->warning($message, $context);
break;
case \E_RECOVERABLE_ERROR:
$this->_logger->error($message, $context);
break;
case \E_PARSE:
case \E_CORE_ERROR:
case \E_COMPILE_ERROR:
$this->_logger->alert($message, $context);
$this->_logger->critical($message, $context);
break;
default: $this->_logger->critical($message, $context);
default: $this->_logger->error($message, $context);
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
} elseif ($throwable instanceof \PharException || $throwable instanceof \RuntimeException) {
$this->_logger->alert($message, $context);
} else {
$this->_logger->critical($message, $context);
@ -245,12 +249,12 @@ abstract class Handler {
}
protected function print(string $string): void {
$string = "$string\n";
if (strtolower(\PHP_SAPI) === 'cli' && $this->_outputToStderr) {
// Can't test this in code coverage without printing errors to STDERR
fwrite(\STDERR, $string); // @codeCoverageIgnore
} else {
echo $string;
return; // @codeCoverageIgnore
}
echo $string;
}
}

11
lib/Catcher/RangeException.php

@ -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 {}

2
tests/bootstrap.php

@ -1,6 +1,6 @@
<?php
declare(strict_types=1);
namespace MensBeam\Logger\Test;
namespace MensBeam\Catcher\Test;
ini_set('memory_limit', '2G');
ini_set('zend.assertions', '1');

16
tests/cases/TestCatcher.php

@ -49,7 +49,10 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
$this->assertInstanceOf(PlainTextHandler::class, $h[0]);
}
/** @dataProvider provideErrorHandlingTests */
/**
* @dataProvider provideErrorHandlingTests
* @covers \MensBeam\Catcher\Error
*/
public function testErrorHandling(int $code): void {
$t = null;
try {
@ -62,19 +65,21 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
}
}
/** @covers \MensBeam\Catcher\Error */
public function testExit(): void {
$this->catcher->unregister();
$h = Phake::partialMock(TestingHandler::class);
$this->catcher = $m = Phake::partialMock(Catcher::class, $h);
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
Phake::when($m)->exit->thenReturn(null);
Phake::when($m)->handleShutdown()->thenReturn(null);
Phake::when($m)->handleShutdown->thenReturn(null);
trigger_error('Ook!', \E_USER_ERROR);
Phake::verify($h, Phake::times(1))->invokeCallback();
}
/** @covers \MensBeam\Catcher\Error */
public function testHandlerBubbling(): void {
$this->catcher->unregister();
@ -91,6 +96,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
Phake::verify($h2, Phake::never())->invokeCallback();
}
/** @covers \MensBeam\Catcher\Error */
public function testHandlerForceExiting(): void {
$this->catcher->setHandlers(new TestingHandler([ 'forceExit' => true ]));
$this->catcher->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
@ -108,7 +114,10 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
$this->assertFalse($this->catcher->isRegistered());
}
/** @dataProvider provideShutdownTests */
/**
* @dataProvider provideShutdownTests
* @covers \MensBeam\Catcher\Error
*/
public function testShutdownHandling(\Closure $closure): void {
$this->catcher->unregister();
@ -148,6 +157,7 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
$this->assertEquals(1, count($c->getHandlers()));
}
/** @covers \MensBeam\Catcher\Error */
public function testWeirdErrorReporting(): void {
error_reporting(\E_ERROR);
$t = null;

194
tests/cases/TestHandler.php

@ -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;
}
}
}

11
tests/lib/Error.php

@ -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 {}

31
tests/lib/ErrorHandlingTestCase.php

@ -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;
}
}
}

12
tests/lib/TestingHandler.php

@ -11,7 +11,11 @@ use MensBeam\Catcher\Handler;
class TestingHandler extends Handler {
public array $output = [];
protected ?string $_name = null;
// Could just use silent option instead, but we need to test Handler::SILENT
protected bool $_print = false;
protected function handleCallback(array $output): array {
@ -35,7 +39,13 @@ class TestingHandler extends Handler {
]));
}
//$this->print($this->serializeOutputThrowable($o));
$o = $this->cleanOutputThrowable($o);
if ($this->_print) {
$this->print(json_encode($o, \JSON_THROW_ON_ERROR));
}
$this->output[] = $o;
}
}
}
Loading…
Cancel
Save