Browse Source

Testing supplemental error handling in Handler

2.1.0
Dustin Wilson 10 months ago
parent
commit
92714e58d8
  1. 3
      composer.json
  2. 136
      composer.lock
  3. 21
      lib/Catcher/Handler.php
  4. 26
      lib/Catcher/PlainTextHandler.php
  5. 38
      lib/Catcher/ThrowableController.php
  6. 57
      tests/cases/TestHandler.php
  7. 23
      tests/lib/FailLogger.php
  8. 12
      tests/lib/TestingHandler.php

3
composer.json

@ -21,7 +21,8 @@
],
"require": {
"php": ">=8.1",
"psr/log": "^3.0"
"psr/log": "^3.0",
"mikey179/vfsstream": "^1.6"
},
"require-dev": {
"phpunit/phpunit": "^10",

136
composer.lock

@ -4,8 +4,59 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "264b81873cadf8449e400b7902f0ef98",
"content-hash": "8098556523bac180121bef4ca07349e6",
"packages": [
{
"name": "mikey179/vfsstream",
"version": "v1.6.11",
"source": {
"type": "git",
"url": "https://github.com/bovigo/vfsStream.git",
"reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f",
"reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.5|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
}
},
"autoload": {
"psr-0": {
"org\\bovigo\\vfs\\": "src/main/php"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Frank Kleine",
"homepage": "http://frankkleine.de/",
"role": "Developer"
}
],
"description": "Virtual file system to mock the real file system in unit tests.",
"homepage": "http://vfs.bovigo.org/",
"support": {
"issues": "https://github.com/bovigo/vfsStream/issues",
"source": "https://github.com/bovigo/vfsStream/tree/master",
"wiki": "https://github.com/bovigo/vfsStream/wiki"
},
"time": "2022-02-23T02:02:42+00:00"
},
{
"name": "psr/log",
"version": "3.0.0",
@ -189,16 +240,16 @@
},
{
"name": "nikic/php-parser",
"version": "v4.15.4",
"version": "v4.16.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290"
"reference": "19526a33fb561ef417e822e85f08a00db4059c17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
"reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17",
"shasum": ""
},
"require": {
@ -239,9 +290,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
},
"time": "2023-03-05T19:49:14+00:00"
"time": "2023-06-25T14:52:30+00:00"
},
{
"name": "phake/phake",
@ -426,16 +477,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "10.0.2",
"version": "10.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "20800e84296ea4732f9a125e08ce86b4004ae3e4"
"reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/20800e84296ea4732f9a125e08ce86b4004ae3e4",
"reference": "20800e84296ea4732f9a125e08ce86b4004ae3e4",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e",
"reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e",
"shasum": ""
},
"require": {
@ -454,7 +505,7 @@
"theseer/tokenizer": "^1.2.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
"phpunit/phpunit": "^10.1"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@ -463,7 +514,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "10.0-dev"
"dev-main": "10.1-dev"
}
},
"autoload": {
@ -491,7 +542,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.0.2"
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2"
},
"funding": [
{
@ -499,20 +551,20 @@
"type": "github"
}
],
"time": "2023-03-06T13:00:19+00:00"
"time": "2023-05-22T09:04:27+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "4.0.1",
"version": "4.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd"
"reference": "5647d65443818959172645e7ed999217360654b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/fd9329ab3368f59fe1fe808a189c51086bd4b6bd",
"reference": "fd9329ab3368f59fe1fe808a189c51086bd4b6bd",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6",
"reference": "5647d65443818959172645e7ed999217360654b6",
"shasum": ""
},
"require": {
@ -551,7 +603,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.1"
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2"
},
"funding": [
{
@ -559,7 +612,7 @@
"type": "github"
}
],
"time": "2023-02-10T16:53:14+00:00"
"time": "2023-05-07T09:13:23+00:00"
},
{
"name": "phpunit/php-invoker",
@ -744,16 +797,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.0.19",
"version": "10.2.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "20c23e85c86e5c06d63538ba464e8054f4744e62"
"reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20c23e85c86e5c06d63538ba464e8054f4744e62",
"reference": "20c23e85c86e5c06d63538ba464e8054f4744e62",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/35c8cac1734ede2ae354a6644f7088356ff5b08e",
"reference": "35c8cac1734ede2ae354a6644f7088356ff5b08e",
"shasum": ""
},
"require": {
@ -767,7 +820,7 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=8.1",
"phpunit/php-code-coverage": "^10.0",
"phpunit/php-code-coverage": "^10.1.1",
"phpunit/php-file-iterator": "^4.0",
"phpunit/php-invoker": "^4.0",
"phpunit/php-text-template": "^3.0",
@ -793,7 +846,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "10.0-dev"
"dev-main": "10.2-dev"
}
},
"autoload": {
@ -825,7 +878,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.19"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.3"
},
"funding": [
{
@ -841,7 +894,7 @@
"type": "tidelift"
}
],
"time": "2023-03-27T11:46:33+00:00"
"time": "2023-06-30T06:17:38+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1145,16 +1198,16 @@
},
{
"name": "sebastian/diff",
"version": "5.0.1",
"version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02"
"reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/aae9a0a43bff37bd5d8d0311426c87bf36153f02",
"reference": "aae9a0a43bff37bd5d8d0311426c87bf36153f02",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b",
"reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b",
"shasum": ""
},
"require": {
@ -1200,7 +1253,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"security": "https://github.com/sebastianbergmann/diff/security/policy",
"source": "https://github.com/sebastianbergmann/diff/tree/5.0.1"
"source": "https://github.com/sebastianbergmann/diff/tree/5.0.3"
},
"funding": [
{
@ -1208,20 +1261,20 @@
"type": "github"
}
],
"time": "2023-03-23T05:12:41+00:00"
"time": "2023-05-01T07:48:21+00:00"
},
{
"name": "sebastian/environment",
"version": "6.0.0",
"version": "6.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc"
"reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b6f3694c6386c7959915a0037652e0c40f6f69cc",
"reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951",
"reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951",
"shasum": ""
},
"require": {
@ -1263,7 +1316,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/6.0.0"
"security": "https://github.com/sebastianbergmann/environment/security/policy",
"source": "https://github.com/sebastianbergmann/environment/tree/6.0.1"
},
"funding": [
{
@ -1271,7 +1325,7 @@
"type": "github"
}
],
"time": "2023-02-03T07:03:04+00:00"
"time": "2023-04-11T05:39:26+00:00"
},
{
"name": "sebastian/exporter",

21
lib/Catcher/Handler.php

@ -159,7 +159,7 @@ abstract class Handler {
set_exception_handler($exceptionHandler);
// If the current exception handler happens to not be Catcher use PHP's handler
// instead; this shouldn't happen but is here just in case
// instead; this shouldn't happen in normal operation but is here just in case
if (!is_array($exceptionHandler) || !$exceptionHandler[0] instanceof Catcher) {
return false;
}
@ -168,18 +168,23 @@ abstract class Handler {
// varExporter to prevent infinite looping of error handlers
$catcher = $exceptionHandler[0];
$handlers = $catcher->getHandlers();
$silent = false;
$handlersCount = count($handlers);
$silentCount = 0;
foreach ($handlers as $h) {
$h->setOption('logger', null);
$h->setOption('varExporter', null);
$silent = (!$silent) ? $h->getOption('silent') : $silent;
if ($h->getOption('silent')) {
$silentCount++;
}
}
// If all of the handlers are silent then use PHP's handler instead; this is
// because a valid use for Catcher is to have it be silent but instead have the
// logger print the errors to stderr/stdout; if there is an error in the logger
// then it wouldn't print.
if ($silent) {
if ($silentCount === $handlersCount) {
// TODO: Output an error here to state that Catcher failed?
return false;
}
@ -310,13 +315,7 @@ abstract class Handler {
protected function varExporter(mixed $value): string|bool {
$exporter = $this->_varExporter;
set_error_handler([ $this, 'handleError' ]);
if ($exporter === null) {
$value = print_r($value, true);
restore_error_handler();
return $value;
}
$value = $exporter($value);
$value = ($exporter === null) ? print_r($value, true) : $exporter($value);
restore_error_handler();
return $value;
}

26
lib/Catcher/PlainTextHandler.php

@ -49,7 +49,7 @@ class PlainTextHandler extends Handler {
$outputThrowable['line']
);
if (!empty($outputThrowable['previous'])) {
if (isset($outputThrowable['previous']) && $outputThrowable['previous'] instanceof \Throwable) {
if ($previous) {
$output .= ' ';
}
@ -57,24 +57,19 @@ class PlainTextHandler extends Handler {
}
if (!$previous) {
if (isset($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) {
if (isset($outputThrowable['frames']) && is_array($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) {
$output .= \PHP_EOL . 'Stack trace:' . \PHP_EOL;
$maxDigits = strlen((string)count($outputThrowable['frames']));
$indent = str_repeat(' ', $maxDigits);
foreach ($outputThrowable['frames'] as $key => $frame) {
$method = null;
if (!empty($frame['class'])) {
if (!empty($frame['errorType'])) {
$method = $frame['class'] ?? "{$frame['function']}()" ?? null;
if (isset($frame['class']) && $method === $frame['class']) {
if (isset($frame['errorType'])) {
$method = "{$frame['errorType']} ({$frame['class']})";
} else {
$method = $frame['class'];
if (!empty($frame['function'])) {
$ref = new \ReflectionMethod($frame['class'], $frame['function']);
$method .= (($ref->isStatic()) ? '::' : '->') . $frame['function'];
}
} elseif (isset($frame['function'])) {
$ref = new \ReflectionMethod($frame['class'], $frame['function']);
$method .= (($ref->isStatic()) ? '::' : '->') . $frame['function'] . '()';
}
} elseif (!empty($frame['function'])) {
$method = $frame['function'];
}
$output .= sprintf("%{$maxDigits}d. %s %s:%d" . \PHP_EOL,
@ -84,8 +79,7 @@ class PlainTextHandler extends Handler {
$frame['line']
);
if (!empty($frame['args']) && $this->_backtraceArgFrameLimit > $key) {
$varExporter = $this->_varExporter;
if (isset($frame['args']) && $this->_backtraceArgFrameLimit > $key) {
$output .= preg_replace('/^/m', "$indent| ", $this->varExporter($frame['args'])) . \PHP_EOL;
}
}
@ -100,7 +94,7 @@ class PlainTextHandler extends Handler {
if (!empty($outputThrowable['time'])) {
$timestamp = $outputThrowable['time']->format($this->_timeFormat) . ' ';
$output = ltrim(preg_replace('/^/m', str_repeat(' ', strlen($timestamp)), "$timestamp$output"));
$output = ltrim(preg_replace('/^(?=\h*\S)/m', str_repeat(' ', strlen($timestamp)), "$timestamp$output"));
}
}

38
lib/Catcher/ThrowableController.php

@ -151,25 +151,25 @@ class ThrowableController {
}
// Add a frame for the throwable to the beginning of the array
$f = [
'file' => $this->throwable->getFile() ?: '[UNKNOWN]',
'line' => (int)$this->throwable->getLine(),
'class' => $this->throwable::class,
'args' => [
$this->throwable->getMessage()
]
];
// Add the error code and type if it is an Error.
if ($this->throwable instanceof \Error) {
$errorType = $this->getErrorType();
if ($errorType !== null) {
$f['code'] = $this->throwable->getCode();
$f['errorType'] = $errorType;
}
}
array_unshift($frames, $f);
// $f = [
// 'file' => $this->throwable->getFile() ?: '[UNKNOWN]',
// 'line' => (int)$this->throwable->getLine(),
// 'class' => $this->throwable::class,
// 'args' => [
// $this->throwable->getMessage()
// ]
// ];
// // Add the error code and type if it is an Error.
// if ($this->throwable instanceof \Error) {
// $errorType = $this->getErrorType();
// if ($errorType !== null) {
// $f['code'] = $this->throwable->getCode();
// $f['errorType'] = $errorType;
// }
// }
// array_unshift($frames, $f);
// Go through previous throwables and merge in their frames
if ($prev = $this->getPrevious()) {

57
tests/cases/TestHandler.php

@ -13,7 +13,8 @@ use MensBeam\Catcher\{
RangeException,
ThrowableController
};
use Psr\Log\LoggerInterface,
use MensBeam\Catcher,
Psr\Log\LoggerInterface,
Phake;
@ -78,7 +79,7 @@ class TestHandler extends ErrorHandlingTestCase {
ob_start();
$h();
$o = ob_get_clean();
$this->assertNotNull($o);
$this->assertNotEmpty($o);
$o = json_decode($o, true);
$this->assertSame(\Exception::class, $o['class']);
$this->assertSame(__FILE__, $o['file']);
@ -86,6 +87,31 @@ class TestHandler extends ErrorHandlingTestCase {
$this->assertSame('Ook!', $o['message']);
}
/** @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);
}
}
public function testFatalError(): void {
$this->expectException(RangeException::class);
@ -191,4 +217,31 @@ class TestHandler extends ErrorHandlingTestCase {
yield $o;
}
}
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;
}
}
}

23
tests/lib/FailLogger.php

@ -0,0 +1,23 @@
<?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 org\bovigo\vfs\vfsStream,
Psr\Log\LoggerInterface,
Psr\Log\LoggerTrait;
class FailLogger implements LoggerInterface {
use LoggerTrait;
public function log($level, string|\Stringable $message, array $context = []): void {
$v = vfsStream::setup('ook');
$d = vfsStream::newDirectory('ook', 0777)->at($v);
file_put_contents($d->url(), $message);
}
}

12
tests/lib/TestingHandler.php

@ -16,6 +16,8 @@ class TestingHandler extends Handler {
protected ?string $_name = null;
// Could just use silent option instead, but we need to test Handler::SILENT
protected bool $_print = false;
// When printing output, print it as JSON
protected bool $_printJSON = true;
protected function handleCallback(array $output): array {
@ -25,10 +27,6 @@ class TestingHandler extends Handler {
protected function invokeCallback(): void {
foreach ($this->outputBuffer as $o) {
if (($o['code'] & self::OUTPUT) === 0) {
continue;
}
if ($o['code'] & self::LOG) {
$this->log($o['controller']->getThrowable(), json_encode([
'class' => $o['class'],
@ -39,10 +37,14 @@ class TestingHandler extends Handler {
]));
}
if (($o['code'] & self::OUTPUT) === 0) {
continue;
}
$o = $this->cleanOutputThrowable($o);
if ($this->_print) {
$this->print(json_encode($o, \JSON_THROW_ON_ERROR));
$this->print(($this->_printJSON) ? json_encode($o, \JSON_THROW_ON_ERROR) : var_dump($o) ?? 'FAIL');
}
$this->output[] = $o;

Loading…
Cancel
Save