Browse Source

Should have committed something long ago... :)

2.1.0
Dustin Wilson 1 year ago
parent
commit
3c2461272f
  1. 2
      .gitignore
  2. 17
      README.md
  3. 16
      composer.json
  4. 1183
      composer.lock
  5. 109
      lib/Catcher.php
  6. 11
      lib/Catcher/ArgumentCountError.php
  7. 196
      lib/Catcher/HTMLHandler.php
  8. 90
      lib/Catcher/Handler.php
  9. 32
      lib/Catcher/JSONHandler.php
  10. 68
      lib/Catcher/PlainTextHandler.php
  11. 11
      lib/Catcher/UnderflowException.php
  12. 100
      run
  13. 37
      test
  14. 17
      tests/bootstrap.php
  15. 677
      tests/cases/TestCatcher.php
  16. 89
      tests/cases/TestHTMLHandler.php
  17. 173
      tests/cases/TestHandler.php
  18. 48
      tests/cases/TestJSONHandler.php
  19. 145
      tests/cases/TestPlainTextHandler.php
  20. 189
      tests/cases/TestThrowableController.php
  21. 41
      tests/lib/TestingHandler.php
  22. 28
      tests/phpunit.dist.xml
  23. 21
      tests/phpunit.xml

2
.gitignore

@ -72,6 +72,6 @@ $RECYCLE.BIN/
/vendor/
/vendor-bin/*/vendor
/tests/html5lib-tests
/tests/.phpunit.result.cache
/tests/.phpunit.cache
/tests/coverage
cachegrind.out.*

17
README.md

@ -4,6 +4,7 @@
[d]: https://www.php.net/manual/en/function.pcntl-fork.php
[e]: https://www.php.net/manual/en/function.print-r.php
[f]: https://github.com/symfony/var-exporter
[g]: https://github.com/php-fig/log
# Catcher #
@ -11,20 +12,20 @@ Catcher is a Throwable catcher and error handling library for PHP. Error handlin
Catcher uses classes called _handlers_ to handle throwables sent its way. PHP is currently in a state of flux when it comes to errors. There are traditional PHP errors which are triggered in userland by using `trigger_error()` which can't be caught using `try`/`catch` and are generally a pain to work with. PHP has begun to remedy this problem by introducing the `\Error` class and its various child classes. However, a lot of functions and core aspects of the language itself continue to use legacy errors. This class does away with this pain point in PHP by turning all errors into throwables. When Catcher converts legacy errors into throwables it will only exit if PHP would, so warnings, notices, etc. won't cause the program to exit unless you configure it to do so. Non user-level fatal errors are picked up by Catcher using its shutdown handler. This means that simply by invoking Catcher one may now... catch (almost) any error PHP then handles.
## Requirements ##
* PHP 8.1 or newer with the following _optional_ extensions:
* [dom][a] extension (for HTMLHandler)
* PHP >= 8.1
* [psr/log][g] ^3.0
## Installation ##
```shell
composer require mensbeam/catcher
```
## Usage ##
For most use cases this library requires no configuration and little effort to integrate into non-complex environments:
@ -88,7 +89,7 @@ This is accomplished internally because of [`pcntl_fork`][d]. The throw is done
PHP by default won't allow fatal errors to be handled by error handlers. It will instead print the error and exit. However, before code execution halts any shutdown functions are run. Catcher will retrieve the last error and manually process it. This causes multiple instances of the same error to be output. Because of this Catcher alters the error reporting level by always removing `E_ERROR` from it when registering the handlers. `E_ERROR` is bitwise or'd back to the error reporting level when unregistering. If this behavior is undesirable then `E_ERROR` can be manually included back into error reporting at any time after Catcher instantiates. Keep in mind Catcher _will not_ include `E_ERROR` back into the error reporting level bitmask if the error reporting level was modified after Catcher was instantiated or if the error reporting level didn't have it when Catcher registered its handlers.
## Documentation ##
### MensBeam\Catcher ###
This is the main class in the library. Unless you have a need to configure a handler or use multiple handlers there usually isn't a need to interact with the rest of the library at all.
@ -163,7 +164,7 @@ Unregisters the Catcher instance as an error, exception and shutdown handler.
Unshifts the specified handler(s) onto the beginning of the stack
### MensBeam\Catcher\Handler ###
All handlers inherit from this abstract class. Since it is an abstract class meant for constructing handlers protected methods and properties will be documented here as well.
@ -457,7 +458,7 @@ throw new \Exception('Ook!');
Output:
```
[21:12:00] Exception: Ook! in file /home/mensbeam/super-awesome-project/ook.php on line 13
Stack trace:
1. Exception /home/mensbeam/super-awesome-project/ook.php:13
| [

16
composer.json

@ -8,6 +8,11 @@
"MensBeam\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"MensBeam\\Catcher\\Test\\": "tests/lib/"
}
},
"authors": [
{
"name": "Dustin Wilson",
@ -18,15 +23,8 @@
"php": ">=8.1",
"psr/log": "^3.0"
},
"suggest": {
"ext-dom": "For HTMLHandler",
"ext-pcntl": "For allowing catching of notices, warnings, etc."
},
"require-dev": {
"ext-dom": "*",
"mensbeam/html-dom": ">=1.0",
"phpunit/phpunit": ">=9.5",
"nikic/php-parser": ">=4.15",
"eloquent/phony-phpunit": ">=7.1"
"phpunit/phpunit": "^10",
"phake/phake": "^4.4"
}
}

1183
composer.lock

File diff suppressed because it is too large

109
lib/Catcher.php

@ -8,26 +8,30 @@
declare(strict_types=1);
namespace MensBeam;
use MensBeam\Catcher\{
ArgumentCountError,
Error,
Handler,
PlainTextHandler,
ThrowableController,
UnderflowException
};
class Catcher {
/** Fork when throwing non-exiting errors, if available */
public bool $forking = true;
public const THROW_NO_ERRORS = 0;
public const THROW_FATAL_ERRORS = 1;
public const THROW_ALL_ERRORS = 2;
/** When set to true Catcher won't exit when instructed */
public bool $preventExit = false;
/** When set to true Catcher will throw errors as throwables */
public bool $throwErrors = true;
/** Determines how errors are handled; THROW_* constants exist to control */
public int $errorHandlingMethod = self::THROW_FATAL_ERRORS;
/**
* Stores the error reporting level set by Catcher to compare against when
* unregistering
*/
protected ?int $errorReporting = null;
public ?int $errorReporting = null;
/**
* Array of handlers the exceptions are passed to
*
@ -69,7 +73,7 @@ class Catcher {
public function popHandler(): Handler {
if (count($this->handlers) === 1) {
throw new \Exception("Popping the last handler will cause the Catcher to have zero handlers; there must be at least one\n");
throw new UnderflowException('Popping the last handler will cause the Catcher to have zero handlers; there must be at least one');
}
return array_pop($this->handlers);
@ -77,19 +81,10 @@ class Catcher {
public function pushHandler(Handler ...$handlers): void {
if (count($handlers) === 0) {
throw new \ArgumentCountError(__METHOD__ . "expects at least 1 argument, 0 given\n");
throw new ArgumentCountError(__METHOD__ . 'expects at least 1 argument, 0 given');
}
$prev = [];
foreach ($handlers as $h) {
if (in_array($h, $this->handlers, true) || in_array($h, $prev, true)) {
trigger_error("Handlers must be unique; skipping\n", \E_USER_WARNING);
continue;
}
$prev[] = $h;
$this->handlers[] = $h;
}
$this->handlers = [ ...$this->handlers, ...$handlers ];
}
public function register(): bool {
@ -108,6 +103,7 @@ class Catcher {
set_error_handler([ $this, 'handleError' ]);
set_exception_handler([ $this, 'handleThrowable' ]);
register_shutdown_function([ $this, 'handleShutdown' ]);
$this->registered = true;
return true;
}
@ -119,7 +115,7 @@ class Catcher {
public function shiftHandler(): Handler {
if (count($this->handlers) === 1) {
throw new \Exception("Shifting the last handler will cause the Catcher to have zero handlers; there must be at least one\n");
throw new UnderflowException('Shifting the last handler will cause the Catcher to have zero handlers; there must be at least one');
}
return array_shift($this->handlers);
@ -147,28 +143,10 @@ class Catcher {
public function unshiftHandler(Handler ...$handlers): void {
if (count($handlers) === 0) {
throw new \ArgumentCountError(__METHOD__ . "expects at least 1 argument, 0 given\n");
throw new ArgumentCountError(__METHOD__ . 'expects at least 1 argument, 0 given');
}
$modified = false;
$prev = [];
foreach ($handlers as $v => $h) {
if (in_array($h, $this->handlers, true) || in_array($h, $prev, true)) {
trigger_error("Handlers must be unique; skipping\n", \E_USER_WARNING);
unset($handlers[$v]);
$modified = true;
continue;
}
$prev[] = $h;
}
if ($modified) {
$handlers = array_values($handlers);
}
if (count($handlers) > 0) {
$this->handlers = [ ...$handlers, ...$this->handlers ];
}
$this->handlers = [ ...$handlers, ...$this->handlers ];
}
@ -181,32 +159,13 @@ class Catcher {
public function handleError(int $code, string $message, ?string $file = null, ?int $line = null): bool {
if ($code && $code & error_reporting()) {
$error = new Error($message, $code, $file, $line);
if ($this->throwErrors) {
// The point of this library is to allow treating of errors as if they were
// exceptions but instead have things like warnings, notices, etc. not stop
// execution. You normally can't have it both ways. So, what's going on here is
// that if the error wouldn't normally stop execution the newly-created Error
// throwable is thrown in a fork instead, allowing execution to resume in the
// parent process.
if ($this->isErrorFatal($code)) {
throw $error;
} elseif ($this->forking && \PHP_SAPI === 'cli' && function_exists('pcntl_fork')) {
$pid = pcntl_fork();
if ($pid === -1) {
// This can't be covered unless it is possible to fake a misconfigured system
throw new \Exception(message: 'Could not create fork to throw Error', previous: $error); // @codeCoverageIgnore
} elseif (!$pid) {
// This can't be covered because it happens in the fork
throw $error; // @codeCoverageIgnore
}
pcntl_wait($status);
return true;
}
if ($this->errorHandlingMethod > self::THROW_NO_ERRORS && ($this->errorHandlingMethod === self::THROW_ALL_ERRORS || $this->isErrorFatal($code)) && !$this->isShuttingDown) {
$this->lastThrowable = $error;
throw $error;
} else {
$this->handleThrowable($error);
return true;
}
$this->handleThrowable($error);
return true;
}
// If preventing exit we don't want a false here to halt processing
@ -220,21 +179,24 @@ class Catcher {
*/
public function handleThrowable(\Throwable $throwable): void {
$controller = new ThrowableController($throwable);
$exit = false;
foreach ($this->handlers as $h) {
$output = $h->handle($controller);
if ($output['outputCode'] & Handler::NOW) {
$h->dispatch();
}
$controlCode = $output['controlCode'];
if ($controlCode & Handler::BREAK) {
if (!$this->isShuttingDown && $output['code'] & Handler::NOW) {
$h();
}
if ($output['code'] & Handler::EXIT) {
$exit = true;
}
if (($output['code'] & Handler::BUBBLES) === 0) {
break;
}
}
if (
$exit ||
$this->isShuttingDown ||
$controlCode & Handler::EXIT ||
$throwable instanceof \Exception ||
($throwable instanceof Error && $this->isErrorFatal($throwable->getCode())) ||
(!$throwable instanceof Error && $throwable instanceof \Error)
@ -243,11 +205,9 @@ class Catcher {
if ($this->isShuttingDown) {
$h->setOption('outputBacktrace', false);
}
$h->dispatch();
$h();
}
$this->lastThrowable = $throwable;
// Don't want to exit here when shutting down so any shutdown functions further
// down the stack still run.
if (!$this->preventExit && !$this->isShuttingDown) {
@ -268,19 +228,18 @@ class Catcher {
return;
}
$this->throwErrors = false;
$this->isShuttingDown = true;
if ($error = $this->getLastError()) {
if ($this->isErrorFatal($error['type'])) {
$errorReporting = error_reporting();
if ($this->errorReporting !== null && $this->errorReporting === $errorReporting && !($this->errorReporting & \E_ERROR)) {
if ($this->errorReporting !== null && $this->errorReporting === $errorReporting && ($this->errorReporting & \E_ERROR) === 0) {
error_reporting($errorReporting | \E_ERROR);
}
$this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
}
} else {
foreach ($this->handlers as $h) {
$h->dispatch();
$h();
}
}
}

11
lib/Catcher/ArgumentCountError.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 ArgumentCountError extends \ArgumentCountError {}

196
lib/Catcher/HTMLHandler.php

@ -1,196 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher;
class HTMLHandler extends Handler {
public const CONTENT_TYPE = 'text/html';
/** The DOMDocument errors should be inserted into */
protected ?\DOMDocument $_document = null;
/** The XPath path to the element where the errors should be inserted */
protected string $_errorPath = '/html/body';
/** The PHP-standard date format which to use for times printed to output */
protected string $_timeFormat = 'H:i:s';
protected \DOMXPath $xpath;
protected \DOMElement $errorLocation;
public function __construct(array $options = []) {
parent::__construct($options);
if ($this->_document === null) {
$this->_document = new \DOMDocument();
$this->_document->loadHTML(<<<HTML
<!DOCTYPE html>
<html>
<head><title>HTTP {$this->_httpCode}</title></head>
<body></body>
</html>
HTML);
}
$this->xpath = new \DOMXPath($this->_document);
$location = $this->xpath->query($this->_errorPath);
if (count($location) === 0 || (!$location->item(0) instanceof \DOMElement && !$location->item(0) instanceof \DOMDocumentFragment)) {
throw new \InvalidArgumentException('Option "errorPath" must correspond to a location that is an instance of \DOMElement or \DOMDocumentFragment');
}
$this->errorLocation = $location->item(0);
}
protected function buildOutputThrowable(array $outputThrowable, bool $previous = false): \DOMDocumentFragment {
$frag = $this->_document->createDocumentFragment();
$tFrag = $this->_document->createDocumentFragment();
$ip = $frag;
$hasSiblings = false;
if ($previous === false) {
if (isset($outputThrowable['time'])) {
$p = $this->_document->createElement('p');
$time = $this->_document->createElement('time');
$time->setAttribute('datetime', $outputThrowable['time']->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.vO'));
$time->appendChild($this->_document->createTextNode($outputThrowable['time']->format($this->_timeFormat)));
$p->appendChild($time);
$frag->appendChild($p);
$div = $this->_document->createElement('div');
$frag->appendChild($div);
$ip = $div;
}
} else {
$span = $this->_document->createElement('span');
$span->appendChild($this->_document->createTextNode('Caused by:'));
$tFrag->appendChild($span);
$tFrag->appendChild($this->_document->createTextNode(' '));
}
$b = $this->_document->createElement('b');
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($outputThrowable['class']));
$b->appendChild($code);
if (isset($outputThrowable['errorType'])) {
$b->insertBefore($this->_document->createTextNode("{$outputThrowable['errorType']} ("), $code);
$b->appendChild($this->_document->createTextNode(')'));
}
$tFrag->appendChild($b);
$tFrag->appendChild($this->_document->createTextNode(': '));
$i = $this->_document->createElement('i');
$i->appendChild($this->_document->createTextNode($outputThrowable['message']));
$tFrag->appendChild($i);
$tFrag->appendChild($this->_document->createTextNode(' in file '));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($outputThrowable['file']));
$tFrag->appendChild($code);
$tFrag->appendChild($this->_document->createTextNode(" on line {$outputThrowable['line']}"));
if (isset($outputThrowable['previous'])) {
$ul = $this->_document->createElement('ul');
$li = $this->_document->createElement('li');
$li->appendChild($this->buildOutputThrowable($outputThrowable['previous'], true));
$ul->appendChild($li);
$ip->appendChild($ul);
$hasSiblings = true;
}
if ($previous === false && isset($outputThrowable['frames'])) {
$p = $this->_document->createElement('p');
$p->appendChild($this->_document->createTextNode('Stack trace:'));
$ip->appendChild($p);
$ol = $this->_document->createElement('ol');
$ip->appendChild($ol);
foreach ($outputThrowable['frames'] as $frame) {
$li = $this->_document->createElement('li');
$ol->appendChild($li);
if (isset($frame['args'])) {
$t = $this->_document->createElement('p');
$li->appendChild($t);
} else {
$t = $li;
}
$b = $this->_document->createElement('b');
$code = $this->_document->createElement('code');
$b->appendChild($code);
$t->appendChild($b);
if (isset($frame['class'])) {
$code->appendChild($this->_document->createTextNode($frame['class']));
if (isset($frame['errorType'])) {
$b->insertBefore($this->_document->createTextNode("{$frame['errorType']} ("), $code);
$b->appendChild($this->_document->createTextNode(')'));
} elseif (isset($frame['function'])) {
$code->firstChild->appendData("::{$frame['function']}");
}
} elseif (!empty($frame['function'])) {
$code->appendChild($this->_document->createTextNode($frame['function']));
}
$t->appendChild($this->_document->createTextNode("\u{00a0}\u{00a0}"));
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($frame['file']));
$t->appendChild($code);
$t->appendChild($this->_document->createTextNode(":{$frame['line']}"));
if (isset($frame['args'])) {
$varExporter = $this->_varExporter;
$pre = $this->_document->createElement('pre');
$pre->appendChild($this->_document->createTextNode(trim($varExporter($frame['args']))));
$li->appendChild($pre);
}
}
$hasSiblings = true;
}
if ($hasSiblings) {
$p = $this->_document->createElement('p');
$p->appendChild($tFrag);
$ip->insertBefore($p, $ip->firstChild);
} else {
$ip->appendChild($tFrag);
}
return $frag;
}
protected function dispatchCallback(): void {
$frag = $this->_document->createDocumentFragment();
$allSilent = true;
foreach ($this->outputBuffer as $o) {
if ($o['outputCode'] & self::SILENT) {
continue;
}
$li = $this->_document->createElement('li');
$li->appendChild($this->buildOutputThrowable($o));
$frag->appendChild($li);
$allSilent = false;
}
if (!$allSilent) {
$ul = $this->_document->createElement('ul');
$ul->appendChild($frag);
$this->errorLocation->appendChild($ul);
$this->print($this->serializeDocument());
}
}
protected function serializeDocument() {
return $this->_document->saveHTML();
}
}

90
lib/Catcher/Handler.php

@ -7,21 +7,18 @@
declare(strict_types=1);
namespace MensBeam\Catcher;
use Psr\Log\LoggerInterface;
abstract class Handler {
public const CONTENT_TYPE = null;
// Control constants
public const CONTINUE = 1;
public const BREAK = 2;
public const EXIT = 4;
// Output constants
public const OUTPUT = 8;
public const SILENT = 16;
public const NOW = 32;
public const BUBBLES = 1;
public const EXIT = 2;
public const LOG = 4;
public const NOW = 8;
public const OUTPUT = 16;
/**
* Array of HandlerOutputs the handler creates
@ -32,22 +29,23 @@ abstract class Handler {
/** The number of backtrace frames in which to print arguments; defaults to 5 */
protected int $_backtraceArgFrameLimit = 5;
/** If true the handler will move onto the next item in the stack of handlers */
protected bool $_bubbles = true;
/**
* The character encoding used for errors; only used if headers weren't sent before
* an error occurred
*/
protected string $_charset = 'UTF-8';
/** If true the handler will force break the loop through the stack of handlers */
protected bool $_forceBreak = false;
/** If true the handler will force an exit */
protected bool $_forceExit = false;
/**
* If true the handler will output as soon as possible; however, if silent
* is true the handler will output nothing
*/
/** If true the handler will output as soon as possible, unless silenced */
protected bool $_forceOutputNow = false;
/** The HTTP code to be sent; possible values: 200, 400-599 */
protected int $_httpCode = 500;
/** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */
protected ?LoggerInterface $_logger = null;
/** Still send logs when silent */
protected bool $_logWhenSilent = true;
/** If true the handler will output backtraces; defaults to false */
protected bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */
@ -88,14 +86,14 @@ abstract class Handler {
public function dispatch(): void {
public function __invoke(): void {
if (count($this->outputBuffer) === 0) {
return;
}
// 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
// Can't figure out a way to test coverage here, but the logic is tested thoroughly
// when running tests in HTTP
// @codeCoverageIgnoreStart
if (!headers_sent()) {
@ -106,7 +104,7 @@ abstract class Handler {
// @codeCoverageIgnoreEnd
}
$this->dispatchCallback();
$this->invokeCallback();
$this->outputBuffer = [];
}
@ -131,23 +129,23 @@ abstract class Handler {
$output['time'] = new \DateTimeImmutable();
}
$code = self::CONTINUE;
if ($this->_forceBreak) {
$code = self::BREAK;
$code = 0;
if ($this->_bubbles) {
$code = self::BUBBLES;
}
if ($this->_forceExit) {
$code |= self::EXIT;
}
$output['controlCode'] = $code;
$code = self::OUTPUT;
if ($this->_silent) {
$code = self::SILENT;
if ($this->_logger !== null && (!$this->_silent || ($this->_silent && $this->_logWhenSilent))) {
$code |= self::LOG;
}
if ($this->_forceOutputNow) {
$code |= self::NOW;
}
$output['outputCode'] = $code;
if (!$this->_silent) {
$code |= self::OUTPUT;
}
$output['code'] = $code;
$output = $this->handleCallback($output);
$this->outputBuffer[] = $output;
@ -158,6 +156,7 @@ abstract class Handler {
$class = get_class($this);
if (!property_exists($class, "_$name")) {
trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING);
return;
}
$name = "_$name";
@ -206,12 +205,45 @@ abstract class Handler {
return $outputThrowable;
}
abstract protected function dispatchCallback(): void;
protected function handleCallback(array $output): array {
return $output;
}
abstract protected function invokeCallback(): void;
protected function log(\Throwable $throwable, string $message): void {
$context = [ 'exception' => $throwable ];
if ($throwable instanceof \Error) {
switch ($throwable->getCode()) {
case \E_NOTICE:
case \E_USER_NOTICE:
case \E_STRICT:
$this->_logger->notice($message, $context);
break;
case \E_WARNING:
case \E_COMPILE_WARNING:
case \E_USER_WARNING:
case \E_DEPRECATED:
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);
break;
default: $this->_logger->critical($message, $context);
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
$this->_logger->alert($message, $context);
} else {
$this->_logger->critical($message, $context);
}
}
protected function print(string $string): void {
$string = "$string\n";
if (strtolower(\PHP_SAPI) === 'cli' && $this->_outputToStderr) {

32
lib/Catcher/JSONHandler.php

@ -1,32 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher;
class JSONHandler extends Handler {
public const CONTENT_TYPE = 'application/json';
protected function dispatchCallback(): void {
foreach ($this->outputBuffer as $key => $value) {
if ($value['outputCode'] & self::SILENT) {
unset($this->outputBuffer[$key]);
continue;
}
$this->outputBuffer[$key] = $this->cleanOutputThrowable($this->outputBuffer[$key]);
}
if (count($this->outputBuffer) > 0) {
$this->print(json_encode([
'errors' => $this->outputBuffer
], \JSON_INVALID_UTF8_SUBSTITUTE | \JSON_PARTIAL_OUTPUT_ON_ERROR | \JSON_UNESCAPED_SLASHES));
}
}
}

68
lib/Catcher/PlainTextHandler.php

@ -7,75 +7,31 @@
declare(strict_types=1);
namespace MensBeam\Catcher;
use Psr\Log\LoggerInterface;
class PlainTextHandler extends Handler {
public const CONTENT_TYPE = 'text/plain';
/** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */
protected ?LoggerInterface $_logger = null;
/** The PHP-standard date format which to use for timestamps in output */
protected string $_timeFormat = '[H:i:s]';
protected function dispatchCallback(): void {
if ($this->_logger) {
foreach ($this->outputBuffer as $o) {
$output = $this->serializeOutputThrowable($o);
if ($o['outputCode'] & self::SILENT) {
continue;
}
$this->print($output);
}
} else {
foreach ($this->outputBuffer as $o) {
if ($o['outputCode'] & self::SILENT) {
continue;
}
$this->print($this->serializeOutputThrowable($o));
}
}
}
protected function handleCallback(array $output): array {
$output['outputCode'] = (\PHP_SAPI === 'cli') ? $output['outputCode'] | self::NOW : $output['outputCode'];
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code'];
return $output;
}
protected function log(\Throwable $throwable, string $message): void {
$context = [ 'exception' => $throwable ];
if ($throwable instanceof \Error) {
switch ($throwable->getCode()) {
case \E_NOTICE:
case \E_USER_NOTICE:
case \E_STRICT:
$this->_logger->notice($message, $context);
break;
case \E_WARNING:
case \E_COMPILE_WARNING:
case \E_USER_WARNING:
case \E_DEPRECATED:
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);
break;
default: $this->_logger->critical($message, $context);
protected function invokeCallback(): void {
foreach ($this->outputBuffer as $o) {
if (($o['code'] & self::OUTPUT) === 0) {
if ($o['code'] & self::LOG) {
$this->serializeOutputThrowable($o);
}
continue;
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
$this->_logger->alert($message, $context);
} else {
$this->_logger->critical($message, $context);
$this->print($this->serializeOutputThrowable($o));
}
}
@ -136,7 +92,7 @@ class PlainTextHandler extends Handler {
$output = rtrim($output) . \PHP_EOL;
}
if (!empty($this->_logger)) {
if ($outputThrowable['code'] & self::LOG) {
$this->log($outputThrowable['controller']->getThrowable(), $output);
}

11
lib/Catcher/UnderflowException.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 UnderflowException extends \UnderflowException {}

100
run

@ -1,100 +0,0 @@
#!/usr/bin/env php
<?php
$cwd = __DIR__;
$codeDir = "$cwd/lib";
$testDir = "$cwd/tests";
function help($error = true): void {
$help = <<<USAGE
Usage:
run test [additional_phpunit_options]
run --help
USAGE;
if ($error) {
fprintf(\STDERR, $help);
} else {
echo $help;
}
exit((int)$error);
}
function error(string $message): void {
fprintf(\STDERR, "ERROR: $message\n");
exit(1);
}
if (count($argv) === 1) {
help();
}
switch ($argv[1]) {
case 'test':
$opts = [
'--colors',
'--coverage-html='.escapeshellarg("$testDir/coverage")
];
if (isset($argv[2])) {
$opts = [ ...$opts, array_slice($argv, 2) ];
}
$opts = implode(' ', $opts);
break;
case '-h':
case '--help':
help(false);
break;
default:
help();
}
$phpunitPath = escapeshellarg("$cwd/vendor/bin/phpunit");
$confPath = "$testDir/phpunit.dist.xml";
if (!file_exists($confPath)) {
$confPath = "$testDir/phpunit.xml";
if (!file_exists($confPath)) {
error('A phpunit configuration must be present at "tests/phpunit.dist.xml" or "tests/phpunit.xml"; aborting');
}
}
$confPath = escapeshellarg($confPath);
$cmd = [
escapeshellarg(\PHP_BINARY),
'-d opcache.enable_cli=0',
'-d zend.assertions=1'
];
if (!extension_loaded('xdebug')) {
$extDir = rtrim(ini_get("extension_dir"), "/");
if (file_exists("$extDir/xdebug.so")) {
$cmd[] = '-d zend_extension=xdebug.so';
} else {
error('Xdebug is not installed on your system; aborting');
}
}
$cmd[] = '-d xdebug.mode=coverage,develop,trace';
$cmd = implode(' ', $cmd);
$process = proc_open("$cmd $phpunitPath -c $confPath $opts", [
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
], $pipes);
if ($process === false) {
error('Failed to execute phpunit');
}
$stderr = trim(stream_get_contents($pipes[2]));
$output = trim(stream_get_contents($pipes[1]));
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
echo "$output\n";
if ($stderr !== '') {
error($stderr);
}

37
test

@ -0,0 +1,37 @@
#!/usr/bin/env php
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
$dir = ini_get('extension_dir');
$php = escapeshellarg(\PHP_BINARY);
$code = escapeshellarg(__DIR__ . '/lib');
array_shift($argv);
foreach ($argv as $k => $v) {
if (in_array($v, ['--coverage', '--coverage-html'])) {
$argv[$k] = '--coverage-html tests/coverage';
}
}
$cmd = [
$php,
'-d opcache.enable_cli=0',
];
if (!extension_loaded('xdebug')) {
$cmd[] = '-d zend_extension=xdebug.so';
}
$cmd = implode(' ', [
...$cmd,
'-d xdebug.mode=coverage,develop,trace',
escapeshellarg(__DIR__ . '/vendor/bin/phpunit'),
'--configuration tests/phpunit.xml',
...$argv
]);
passthru($cmd);

17
tests/bootstrap.php

@ -1,23 +1,18 @@
<?php
/** @license MIT
* Copyright 2017 , Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace MensBeam;
namespace MensBeam\Logger\Test;
ini_set('memory_limit', '-1');
ini_set('memory_limit', '2G');
ini_set('zend.assertions', '1');
ini_set('assert.exception', 'true');
error_reporting(\E_ALL);
$cwd = dirname(__DIR__);
require_once "$cwd/vendor/autoload.php";
define('CWD', dirname(__DIR__));
require_once CWD . '/vendor/autoload.php';
if (function_exists('xdebug_set_filter')) {
if (defined('XDEBUG_PATH_INCLUDE')) {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_INCLUDE, [ "$cwd/lib/" ]);
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_INCLUDE, [ CWD . '/lib/' ]);
} else {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [ "$cwd/lib/" ]);
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [ CWD . '/lib/' ]);
}
}

677
tests/cases/TestCatcher.php

@ -1,499 +1,280 @@
<?php
/**
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
use MensBeam\Catcher;
use MensBeam\Catcher,
Phake,
Phake\IMock;
use MensBeam\Catcher\{
ArgumentCountError,
Error,
HTMLHandler,
JSONHandler,
PlainTextHandler
PlainTextHandler,
UnderflowException
};
use Eloquent\Phony\Phpunit\Phony;
/** @covers \MensBeam\Catcher */
class TestCatcher extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher::__construct
*
* @covers \MensBeam\Catcher::getHandlers
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\HTMLHandler::__construct
*/
public function testMethod___construct(): void {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$this->assertSame(1, count($c->getHandlers()));
$this->assertSame(PlainTextHandler::class, $c->getHandlers()[0]::class);
$c->unregister();
$c = new Catcher(
new PlainTextHandler(),
new HTMLHandler(),
new JSONHandler()
);
$c->preventExit = true;
$c->throwErrors = false;
$this->assertSame('MensBeam\Catcher', $c::class);
$this->assertSame(3, count($c->getHandlers()));
$h = $c->getHandlers();
$this->assertSame(PlainTextHandler::class, $h[0]::class);
$this->assertSame(HTMLHandler::class, $h[1]::class);
$this->assertSame(JSONHandler::class, $h[2]::class);
$c->unregister();
}
protected ?Catcher $catcher = null;
/**
* @covers \MensBeam\Catcher::getLastThrowable
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_getLastThrowable(): void {
$c = new Catcher(new PlainTextHandler([ 'silent' => true ]));
$c->preventExit = true;
$c->throwErrors = false;
trigger_error('Ook!', \E_USER_WARNING);
$this->assertSame(\E_USER_WARNING, $c->getLastThrowable()->getCode());
$c->unregister();
}
/**
* @covers \MensBeam\Catcher::pushHandler
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod_pushHandler(): void {
$e = null;
set_error_handler(function($errno) use (&$e) {
$e = $errno;
});
$h = new PlainTextHandler();
$c = new Catcher($h, $h);
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$this->assertSame(\E_USER_WARNING, $e);
$e = null;
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$c->pushHandler($h, $h);
$this->assertSame(\E_USER_WARNING, $e);
restore_error_handler();
$c = new Catcher();
$c->unregister();
$e = null;
try {
$c->pushHandler();
} catch (\Throwable $t) {
$e = $t::class;
} finally {
$this->assertSame(\ArgumentCountError::class, $e);
public function setUp(): void {
if ($this->catcher !== null) {
$this->catcher->unregister();
}
}
$this->catcher = new Catcher();
$this->catcher->preventExit = true;
/**
* @covers \MensBeam\Catcher::popHandler
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\HTMLHandler::__construct
*/
public function testMethod_popHandler(): void {
$h = [
new HTMLHandler(),
new PlainTextHandler(),
new JSONHandler()
];
$c = new Catcher(...$h);
$c->preventExit = true;
$c->throwErrors = false;
$hh = $c->popHandler();
$this->assertSame($h[2], $hh);
$hh = $c->popHandler();
$this->assertSame($h[1], $hh);
$e = null;
try {
$c->popHandler();
} catch (\Throwable $t) {
$e = $t::class;
} finally {
$c->unregister();
$this->assertSame(\Exception::class, $e);
}
// Do this instead of specifying the option in the constructor for coverage
// purposes...
$handlers = $this->catcher->getHandlers();
$handlers[0]->setOption('silent', true);
}
/**
* @covers \MensBeam\Catcher::isRegistered
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod_register(): void {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$this->assertTrue($c->isRegistered());
$this->assertFalse($c->register());
$c->unregister();
$this->assertFalse($c->isRegistered());
public function tearDown(): void {
$this->catcher->unregister();
$this->catcher = null;
error_reporting(\E_ALL);
}
/**
* @covers \MensBeam\Catcher::setHandlers
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getHandlers
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod_setHandlers(): void {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->setHandlers(new PlainTextHandler());
$h = $c->getHandlers();
$this->assertSame(1, count($h));
$this->assertSame(PlainTextHandler::class, $h[0]::class);
$c->unregister();
public function testConstructor(): void {
$h = $this->catcher->getHandlers();
$this->assertEquals(1, count($h));
$this->assertInstanceOf(PlainTextHandler::class, $h[0]);
}
/**
* @covers \MensBeam\Catcher::shiftHandler
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\HTMLHandler::__construct
*/
public function testMethod_shiftHandler(): void {
$h = [
new HTMLHandler(),
new PlainTextHandler(),
new JSONHandler()
];
$c = new Catcher(...$h);
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$hh = $c->shiftHandler();
$this->assertSame($h[0], $hh);
$hh = $c->shiftHandler();
$this->assertSame($h[1], $hh);
$e = null;
/** @dataProvider provideErrorHandlingTests */
public function testErrorHandling(int $code): void {
$t = null;
try {
$c->shiftHandler();
} catch (\Throwable $t) {
$e = $t::class;
} finally {
$this->assertSame(\Exception::class, $e);
trigger_error('Ook!', $code);
} catch (\Throwable $t) {} finally {
$t = ($t === null) ? $this->catcher->getLastThrowable() : $t;
$this->assertSame(Error::class, $t::class);
$this->assertSame($code, $t->getCode());
$this->assertSame($t, $this->catcher->getLastThrowable());
}
}
/**
* @covers \MensBeam\Catcher::unregister
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod_unregister(): void {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$this->assertFalse($c->unregister());
}
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);
/**
* @covers \MensBeam\Catcher::unshiftHandler
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getHandlers
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\HTMLHandler::__construct
*/
public function testMethod_unshiftHandler(): void {
$c = new Catcher(new PlainTextHandler());
$c->preventExit = true;
$c->throwErrors = false;
$c->unshiftHandler(new JSONHandler(), new HTMLHandler(), new PlainTextHandler());
$h = $c->getHandlers();
$this->assertSame(4, count($h));
$this->assertSame(JSONHandler::class, $h[0]::class);
$this->assertSame(HTMLHandler::class, $h[1]::class);
$this->assertSame(PlainTextHandler::class, $h[2]::class);
$this->assertSame(PlainTextHandler::class, $h[3]::class);
$e = null;
set_error_handler(function($errno) use (&$e) {
$e = $errno;
});
$c->unshiftHandler($h[0]);
$this->assertSame(\E_USER_WARNING, $e);
$e = null;
$h = new PlainTextHandler();
$c->unshiftHandler($h, $h);
$this->assertSame(\E_USER_WARNING, $e);
restore_error_handler();
$c->unregister();
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$e = null;
try {
$c->unshiftHandler();
} catch (\Throwable $t) {
$e = $t::class;
} finally {
$this->assertSame(\ArgumentCountError::class, $e);
}
trigger_error('Ook!', \E_USER_ERROR);
Phake::verify($h, Phake::times(1))->invokeCallback();
}
/**
* @covers \MensBeam\Catcher::handleError
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getLastThrowable
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_handleError(): void {
$c = new Catcher(new PlainTextHandler([ 'silent' => true ]));
$c->preventExit = true;
$c->throwErrors = false;
trigger_error('Ook!', \E_USER_NOTICE);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_NOTICE, $t->getCode());
trigger_error('Ook!', \E_USER_DEPRECATED);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_DEPRECATED, $t->getCode());
trigger_error('Ook!', \E_USER_WARNING);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_WARNING, $t->getCode());
public function testHandlerBubbling(): void {
$this->catcher->unregister();
trigger_error('Ook!', \E_USER_ERROR);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_ERROR, $t->getCode());
$h1 = Phake::partialMock(TestingHandler::class, [ 'bubbles' => false ]);
$h2 = Phake::partialMock(TestingHandler::class);
$this->catcher = $m = Phake::partialMock(Catcher::class, $h1, $h2);
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
$m->preventExit = true;
$er = error_reporting();
error_reporting(0);
trigger_error('Ook!', \E_USER_ERROR);
error_reporting($er);
Phake::verify($m)->handleError(\E_USER_ERROR, 'Ook!', __FILE__, __LINE__ - 1);
Phake::verify($m)->handleThrowable($m->getLastThrowable());
Phake::verify($h1)->invokeCallback();
Phake::verify($h2, Phake::never())->invokeCallback();
}
$c->unregister();
public function testHandlerForceExiting(): void {
$this->catcher->setHandlers(new TestingHandler([ 'forceExit' => true ]));
$this->catcher->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
$this->catcher->preventExit = true;
$h1 = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]);
$h2 = Phony::partialMock(HTMLHandler::class, [ [ 'silent' => true ] ]);
$h3 = Phony::partialMock(JSONHandler::class, [ [ 'silent' => true ] ]);
trigger_error('Ook', \E_USER_ERROR);
$this->assertSame(Error::class, $this->catcher->getLastThrowable()::class);
}
$h = Phony::partialMock(Catcher::class, [
$h1->get(),
$h2->get(),
$h3->get()
]);
$c = $h->get();
$c->preventExit = true;
$c->throwErrors = false;
public function testRegistration(): void {
$this->assertTrue($this->catcher->isRegistered());
$this->assertFalse($this->catcher->register());
$this->assertTrue($this->catcher->unregister());
$this->assertFalse($this->catcher->unregister());
$this->assertFalse($this->catcher->isRegistered());
}
trigger_error('Ook!', \E_USER_ERROR);
/** @dataProvider provideShutdownTests */
public function testShutdownHandling(\Closure $closure): void {
$this->catcher->unregister();
$h1->dispatch->called();
$h2->dispatch->called();
$h3->dispatch->called();
$h1 = Phake::partialMock(TestingHandler::class);
$this->catcher = $m = Phake::partialMock(Catcher::class, $h1);
$closure($m, $h1);
}
$c->throwErrors = true;
public function testStackManipulation(): void {
$c = $this->catcher;
$c->pushHandler(new TestingHandler(options: [ 'name' => 'ook' ]), new TestingHandler(options: [ 'name' => 'eek' ]));
$h = $c->getHandlers();
$this->assertEquals(3, count($h));
$this->assertSame('ook', $h[1]->getOption('name'));
$this->assertSame('eek', $h[2]->getOption('name'));
$this->assertInstanceOf(PlainTextHandler::class, $c->shiftHandler());
$h = $c->getHandlers();
$this->assertEquals(2, count($h));
$this->assertSame('ook', $h[0]->getOption('name'));
$this->assertSame('eek', $h[1]->getOption('name'));
$p = $c->popHandler();
$this->assertInstanceOf(TestingHandler::class, $p);
$h = $c->getHandlers();
$this->assertEquals(1, count($h));
$this->assertSame('eek', $p->getOption('name'));
$this->assertSame('ook', $h[0]->getOption('name'));
$c->unshiftHandler($p);
$h = $c->getHandlers();
$this->assertEquals(2, count($h));
$this->assertSame('eek', $h[0]->getOption('name'));
$this->assertSame('ook', $h[1]->getOption('name'));
$c->setHandlers(new PlainTextHandler());
$this->assertEquals(1, count($c->getHandlers()));
}
public function testWeirdErrorReporting(): void {
error_reporting(\E_ERROR);
$t = null;
try {
trigger_error('Ook!', \E_USER_WARNING);
} catch (\Throwable $t) {
$this->assertInstanceOf(Error::class, $t);
$this->assertSame(\E_USER_WARNING, $t->getCode());
} catch (\Throwable $t) {} finally {
$this->assertNull($t);
$this->assertNull($this->catcher->getLastThrowable());
}
}
try {
trigger_error('Ook!', \E_USER_ERROR);
} catch (\Throwable $t) {
$this->assertInstanceOf(Error::class, $t);
$this->assertSame(\E_USER_ERROR, $t->getCode());
}
$c->unregister();
$c->throwErrors = false;
/** @dataProvider provideFatalErrorTests */
public function testFatalErrors(string $throwableClassName, \Closure $closure): void {
$this->expectException($throwableClassName);
$closure($this->catcher);
}
/**
* @covers \MensBeam\Catcher::handleThrowable
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getLastThrowable
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_handleThrowable(): void {
$c = new Catcher(new PlainTextHandler([ 'silent' => true, 'forceBreak' => true ]));
$c->preventExit = true;
$c->throwErrors = false;
trigger_error('Ook!', \E_USER_ERROR);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_ERROR, $t->getCode());
$c->unregister();
$h = Phony::partialMock(Catcher::class, [ new PlainTextHandler([ 'silent' => true ]) ]);
$h->exit->returns();
$c = $h->get();
$c->preventExit = false;
$c->throwErrors = false;
public static function provideFatalErrorTests(): iterable {
$iterable = [
[
UnderflowException::class,
function (Catcher $c): void {
$c->popHandler();
}
],
[
ArgumentCountError::class,
function (Catcher $c): void {
$c->pushHandler();
}
],
[
UnderflowException::class,
function (Catcher $c): void {
$c->shiftHandler();
}
],
[
ArgumentCountError::class,
function (Catcher $c): void {
$c->unshiftHandler();
}
],
];
trigger_error('Ook!', \E_USER_ERROR);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_ERROR, $t->getCode());
$c->unregister();
foreach ($iterable as $i) {
yield $i;
}
}
/**
* @covers \MensBeam\Catcher::handleShutdown
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getLastError
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_handleShutdown(): void {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->handleShutdown();
$p = new \ReflectionProperty($c, 'isShuttingDown');
$p->setAccessible(true);
$this->assertTrue($p->getValue($c));
$c->unregister();
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$c->unregister();
$c->handleShutdown();
$p = new \ReflectionProperty($c, 'isShuttingDown');
$p->setAccessible(true);
$this->assertFalse($p->getValue($c));
$c->unregister();
$h = Phony::partialMock(Catcher::class, [ new PlainTextHandler([ 'silent' => true ]) ]);
$h->getLastError->returns([
'type' => \E_ERROR,
'message' => 'Ook!',
'file' => '/dev/null',
'line' => 2112
]);
$c = $h->get();
$c->handleShutdown();
$h->handleError->called();
$h->handleThrowable->called();
public static function provideErrorHandlingTests(): iterable {
foreach ([ \E_USER_NOTICE, \E_USER_DEPRECATED, \E_USER_WARNING, \E_USER_ERROR ] as $i) {
yield [ $i ];
}
}
public static function provideShutdownTests(): iterable {
$iterable = [
[
function (IMock $m, IMock $h): void {
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
Phake::when($m)->getLastError()->thenReturn([
'type' => \E_ERROR,
'message' => 'Ook!',
'file' => '/dev/null',
'line' => 2112
]);
$m->handleShutdown();
Phake::verify($m)->getLastError();
Phake::verify($m)->handleError(\E_ERROR, 'Ook!', '/dev/null', 2112);
Phake::verify($h, Phake::times(1))->invokeCallback();
}
],
[
function (IMock $m, IMock $h): void {
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
Phake::when($m)->getLastError()->thenReturn([
'type' => \E_USER_ERROR,
'message' => 'Ook!',
'file' => '/dev/null',
'line' => 2112
]);
$m->handleShutdown();
Phake::verify($m)->getLastError();
Phake::verify($m)->handleError(\E_USER_ERROR, 'Ook!', '/dev/null', 2112);
Phake::verify($h, Phake::times(1))->invokeCallback();
}
],
[
function (IMock $m, IMock $h): void {
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
Phake::when($m)->getLastError()->thenReturn([
'type' => \E_USER_ERROR,
'message' => 'Ook!',
'file' => '/dev/null',
'line' => 2112
]);
$m->handleShutdown();
Phake::verify($m)->getLastError();
Phake::verify($m)->handleError(\E_USER_ERROR, 'Ook!', '/dev/null', 2112);
Phake::verify($h, Phake::times(1))->invokeCallback();
}
],
[
function (IMock $m, IMock $h): void {
$m->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
$m->handleShutdown();
Phake::verify($m)->getLastError();
// Handler wouldn't be invoked because there aren't any errors in the output buffer.
Phake::verify($h, Phake::never())->invokeCallback();
}
],
[
function (IMock $m, IMock $h): void {
// Nothing in the shutdown handler runs if Catcher is unregistered
$m->unregister();
$m->handleShutdown();
Phake::verify($m, Phake::never())->getLastError();
Phake::verify($h, Phake::never())->invokeCallback();
}
]
];
foreach ($iterable as $i) {
yield $i;
}
}
}

89
tests/cases/TestHTMLHandler.php

@ -1,89 +0,0 @@
<?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;
use MensBeam\Catcher\{
Error,
Handler,
HTMLHandler,
ThrowableController
};
class TestHTMLHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher\HTMLHandler::__construct
*
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod___construct__exception(): void {
$this->expectException(\InvalidArgumentException::class);
new HTMLHandler([ 'errorPath' => '/html/body/fail' ]);
}
/**
* @covers \MensBeam\Catcher\HTMLHandler::buildOutputThrowable
*
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\HTMLHandler::__construct
* @covers \MensBeam\Catcher\HTMLHandler::dispatchCallback
* @covers \MensBeam\Catcher\HTMLHandler::handleCallback
* @covers \MensBeam\Catcher\HTMLHandler::serializeDocument
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_buildOutputThrowable(): void {
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new Error(message: 'Eek!', code: \E_USER_ERROR, previous: new Error(message: 'Ack!'))));
$h = new HTMLHandler([
'backtraceArgFrameLimit' => 1,
'outputBacktrace' => true,
'outputToStderr' => false
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o['controlCode']);
ob_start();
$h->dispatch();
ob_end_clean();
}
/**
* @covers \MensBeam\Catcher\HTMLHandler::dispatchCallback
*
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\Handler::handleCallback
* @covers \MensBeam\Catcher\HTMLHandler::__construct
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_dispatchCallback(): void {
$c = new ThrowableController(new \Exception(message: 'Ook!'));
$h = new HTMLHandler([
'backtraceArgFrameLimit' => 1,
'outputToStderr' => false,
'silent' => true
]);
$h->handle($c);
ob_start();
$h->dispatch();
$o = ob_get_clean();
$this->assertEmpty($o);
}
}

173
tests/cases/TestHandler.php

@ -1,173 +0,0 @@
<?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;
use MensBeam\Catcher\{
Error,
HTMLHandler,
PlainTextHandler,
JSONHandler,
ThrowableController
};
use Eloquent\Phony\Phpunit\Phony;
class TestHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod___construct__exception(): void {
$this->expectException(\RangeException::class);
new PlainTextHandler([ 'httpCode' => 42 ]);
}
/**
* @covers \MensBeam\Catcher\Handler::cleanOutputThrowable
*
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\Handler::handleCallback
* @covers \MensBeam\Catcher\Handler::print
* @covers \MensBeam\Catcher\JSONHandler::dispatchCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod__cleanOutputThrowable(): void {
// Just need to test coverage here; TestJSONHandler covers this one thoroughly.
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error('Eek!')));
$h = new JSONHandler([
'outputBacktrace' => true,
'outputToStderr' => false
]);
$o = $h->handle($c);
ob_start();
$h->dispatch();
ob_end_clean();
$this->assertTrue(isset($o['frames']));
}
/**
* @covers \MensBeam\Catcher\Handler::handle
*
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod__handle(): void {
// Just need to test backtrace handling. The rest has already been covered by prior tests.
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new Error(message: 'Eek!', code: \E_USER_ERROR))));
$h = new HTMLHandler([
'outputBacktrace' => true,
'outputToStderr' => true
]);
$this->assertTrue(isset($h->handle($c)['frames']));
}
/**
* @covers \MensBeam\Catcher\Handler::getOption
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod__getOption(): void {
$h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]);
$this->assertTrue($h->getOption('forceExit'));
$c = new Catcher($h);
$c->preventExit = true;
$c->throwErrors = false;
$this->assertNull($h->getOption('ook'));
$c->unregister();
}
/**
* @covers \MensBeam\Catcher\Handler::setOption
*
* @covers \MensBeam\Catcher\Handler::__construct
*/
public function testMethod__setOption(): void {
$h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]);
$h->setOption('forceExit', false);
$r = new \ReflectionProperty($h, '_forceExit');
$r->setAccessible(true);
$this->assertFalse($r->getValue($h));
$m = Phony::partialMock(Catcher::class, [
$h
]);
$c = $m->get();
$c->preventExit = true;
$c->throwErrors = false;
$h->setOption('ook', 'FAIL');
$m->handleError->called();
$c->unregister();
}
/**
* @covers \MensBeam\Catcher\Handler::print
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod__print(): void {
// Just need to test forceOutputNow for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceOutputNow' => true, 'outputToStderr' => false ]));
$c->preventExit = true;
$c->throwErrors = false;
ob_start();
trigger_error('Ook!', \E_USER_NOTICE);
ob_end_clean();
$this->assertSame(\E_USER_NOTICE, $c->getLastThrowable()->getCode());
$c->unregister();
}
}

48
tests/cases/TestJSONHandler.php

@ -1,48 +0,0 @@
<?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;
use MensBeam\Catcher\{
Error,
Handler,
JSONHandler,
ThrowableController
};
class TestJSONHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher\JSONHandler::dispatchCallback
*
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\Handler::handleCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_dispatchCallback(): void {
// Not much left to cover; just need to test silent output
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new Error(message: 'Eek!', code: \E_USER_ERROR, previous: new Error(message: 'Ack!'))));
$h = new JSONHandler([
'silent' => true,
'outputToStderr' => false
]);
$o = $h->handle($c);
ob_start();
$h->dispatch();
$o = ob_get_clean();
$this->assertEmpty($o);
}
}

145
tests/cases/TestPlainTextHandler.php

@ -1,145 +0,0 @@
<?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;
use MensBeam\Catcher\{
Error,
Handler,
PlainTextHandler,
ThrowableController
};
use Eloquent\Phony\Phpunit\Phony,
Psr\Log\LoggerInterface;
class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
*
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\PlainTextHandler::log
* @covers \MensBeam\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_handleCallback(): void {
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new Error(message: 'Ack!', code: \E_USER_ERROR))));
$l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([
'logger' => $l->get(),
'outputBacktrace' => true,
'outputToStderr' => false
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o['controlCode']);
$this->assertSame(Handler::OUTPUT | Handler::NOW, $o['outputCode']);
$this->assertTrue(isset($o['previous']));
ob_start();
$h->dispatch();
ob_end_clean();
$l->critical->called();
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new Error(message: 'Ack!', code: \E_USER_ERROR))));
$l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([
'logger' => $l->get(),
'silent' => true
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o['controlCode']);
$this->assertSame(Handler::SILENT | Handler::NOW, $o['outputCode']);
$this->assertTrue(isset($o['previous']));
ob_start();
$h->dispatch();
ob_end_clean();
$l->critical->called();
}
/**
* @covers \MensBeam\Catcher\PlainTextHandler::log
*
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_log(): void {
$l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([
'logger' => $l->get(),
'outputToStderr' => false
]);
$e = [
'notice' => [
\E_NOTICE,
\E_USER_NOTICE,
\E_STRICT
],
'warning' => [
\E_WARNING,
\E_COMPILE_WARNING,
\E_USER_WARNING,
\E_DEPRECATED,
\E_USER_DEPRECATED
],
'error' => [
\E_RECOVERABLE_ERROR
],
'alert' => [
\E_PARSE,
\E_CORE_ERROR,
\E_COMPILE_ERROR
]
];
foreach ($e as $k => $v) {
foreach ($v as $vv) {
$h->handle(new ThrowableController(new Error('Ook!', $vv)));
ob_start();
$h->dispatch();
ob_end_clean();
$l->$k->called();
}
}
$h->handle(new ThrowableController(new \PharException('Ook!')));
ob_start();
$h->dispatch();
ob_end_clean();
$l->alert->called();
$h->handle(new ThrowableController(new \RuntimeException('Ook!')));
ob_start();
$h->dispatch();
ob_end_clean();
$l->alert->called();
}
}

189
tests/cases/TestThrowableController.php

@ -1,189 +0,0 @@
<?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;
use MensBeam\Catcher\{
Error,
PlainTextHandler,
ThrowableController
};
class TestThrowableController extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
*
* @covers \MensBeam\Catcher::__construct
* @covers \MensBeam\Catcher::getLastThrowable
* @covers \MensBeam\Catcher::handleError
* @covers \MensBeam\Catcher::isErrorFatal
* @covers \MensBeam\Catcher::handleThrowable
* @covers \MensBeam\Catcher::pushHandler
* @covers \MensBeam\Catcher::register
* @covers \MensBeam\Catcher::unregister
* @covers \MensBeam\Catcher\Error::__construct
* @covers \MensBeam\Catcher\Handler::__construct
* @covers \MensBeam\Catcher\Handler::dispatch
* @covers \MensBeam\Catcher\Handler::handle
* @covers \MensBeam\Catcher\Handler::print
* @covers \MensBeam\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Catcher\ThrowableController::getThrowable
*/
public function testMethod_getErrorType(): void {
$c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ]));
$c->preventExit = true;
$c->throwErrors = false;
ob_start();
trigger_error('Ook!', \E_USER_DEPRECATED);
ob_end_clean();
$this->assertSame(\E_USER_DEPRECATED, $c->getLastThrowable()->getCode());
$c->unregister();
$c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ]));
$c->preventExit = true;
$c->throwErrors = false;
ob_start();
trigger_error('Ook!', \E_USER_WARNING);
ob_end_clean();
$this->assertSame(\E_USER_WARNING, $c->getLastThrowable()->getCode());
$c->unregister();
$c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ]));
$c->preventExit = true;
$c->throwErrors = false;
ob_start();
trigger_error('Ook!', \E_USER_NOTICE);
ob_end_clean();
$this->assertSame(\E_USER_NOTICE, $c->getLastThrowable()->getCode());
$c->unregister();
$c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ]));
$c->preventExit = true;
$c->throwErrors = false;
ob_start();
trigger_error('Ook!', \E_USER_ERROR);
ob_end_clean();
$this->assertSame(\E_USER_ERROR, $c->getLastThrowable()->getCode());
$c->unregister();
// These others will be tested by invoking the method directly
$c = new ThrowableController(new Error('Ook!', \E_ERROR));
$this->assertSame('PHP Fatal Error', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_WARNING));
$this->assertSame('PHP Warning', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_PARSE));
$this->assertSame('PHP Parsing Error', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_NOTICE));
$this->assertSame('PHP Notice', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_DEPRECATED));
$this->assertSame('Deprecated', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_CORE_ERROR));
$this->assertSame('PHP Core Error', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_CORE_WARNING));
$this->assertSame('PHP Core Warning', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_COMPILE_ERROR));
$this->assertSame('Compile Error', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_COMPILE_WARNING));
$this->assertSame('Compile Warning', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_STRICT));
$this->assertSame('Runtime Notice', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!', \E_RECOVERABLE_ERROR));
$this->assertSame('Recoverable Error', $c->getErrorType());
$c = new ThrowableController(new Error('Ook!'));
$this->assertNull($c->getErrorType());
$c = new ThrowableController(new \Exception('Ook!'));
$this->assertNull($c->getErrorType());
// For code coverage purposes.
$this->assertNull($c->getErrorType());
}
/**
* @covers \MensBeam\Catcher\ThrowableController::getFrames
*
* @covers \MensBeam\Catcher\ThrowableController::__construct
* @covers \MensBeam\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Catcher\ThrowableController::getPrevious
*/
public function testMethod_getFrames(): void {
$f = false;
try {
throw new \Exception('Ook!');
} catch (\Throwable $t) {
$c = new ThrowableController($t);
$f = $c->getFrames();
} finally {
$this->assertSame(\Exception::class, $f[0]['class']);
}
$f = false;
try {
throw new Error('Ook!', \E_ERROR);
} catch (\Throwable $t) {
$c = new ThrowableController($t);
$f = $c->getFrames();
} finally {
$this->assertSame(Error::class, $f[0]['class']);
}
$f = false;
try {
throw new \Exception(message: 'Ook!', previous: new Error(message: 'Ook!', code: \E_ERROR, previous: new \Exception('Ook!')));
} catch (\Throwable $t) {
$c = new ThrowableController($t);
$f = $c->getFrames();
} finally {
$this->assertSame(\Exception::class, $f[0]['class']);
$this->assertSame(Error::class, $f[count($f) - 2]['class']);
}
$f = false;
try {
call_user_func_array(function () {
throw new \Exception('Ook!');
}, []);
} catch (\Throwable $t) {
$c = new ThrowableController($t);
$f = $c->getFrames();
} finally {
$this->assertSame(\Exception::class, $f[0]['class']);
$this->assertArrayHasKey('file', $f[2]);
$this->assertMatchesRegularExpression('/TestThrowableController\.php$/', $f[2]['file']);
$this->assertSame('call_user_func_array', $f[2]['function']);
$this->assertArrayHasKey('line', $f[2]);
$this->assertNotSame(0, $f[2]['line']);
}
// This is mostly here for code coverage: to delete userland error handling from
// the backtrace
$f = false;
try {
function ook() {}
call_user_func('ook', []);
} catch (\Throwable $t) {
$c = new ThrowableController($t);
$f = $c->getFrames();
} finally {
$this->assertSame(\TypeError::class, $f[0]['class']);
}
// For code coverage purposes; should use the cached value instead of calculating
// the frames over again.
$f = $c->getFrames();
// Lastly test for a RangeException
$this->expectException(\RangeException::class);
$c = new ThrowableController(new \Exception('Ook!'));
$c->getFrames(-1);
}
}

41
tests/lib/TestingHandler.php

@ -0,0 +1,41 @@
<?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\Handler;
class TestingHandler extends Handler {
protected ?string $_name = null;
protected function handleCallback(array $output): array {
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code'];
return $output;
}
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'],
'code' => $o['code'],
'file' => $o['file'],
'line' => $o['line'],
'message' => $o['message']
]));
}
//$this->print($this->serializeOutputThrowable($o));
}
}
}

28
tests/phpunit.dist.xml

@ -1,28 +0,0 @@
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
colors="true"
bootstrap="bootstrap.php"
convertErrorsToExceptions="false"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false"
beStrictAboutTestsThatDoNotTestAnything="true"
forceCoversAnnotation="true"
stopOnError="false">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">../lib</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Main">
<file>cases/TestCatcher.php</file>
<file>cases/TestHandler.php</file>
<file>cases/TestHTMLHandler.php</file>
<file>cases/TestJSONHandler.php</file>
<file>cases/TestPlainTextHandler.php</file>
<file>cases/TestThrowableController.php</file>
</testsuite>
</testsuites>
</phpunit>

21
tests/phpunit.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
bootstrap="bootstrap.php"
cacheDirectory=".phpunit.cache"
colors="true"
executionOrder="defects"
requireCoverageMetadata="true"
>
<testsuites>
<testsuite name="Main">
<directory prefix="Test" suffix=".php">./cases</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">../lib</directory>
</include>
</coverage>
</phpunit>
Loading…
Cancel
Save