Browse Source

Should have committed something long ago... :)

2.1.0
Dustin Wilson 1 year ago
parent
commit
3c2461272f
  1. 2
      .gitignore
  2. 5
      README.md
  3. 16
      composer.json
  4. 1183
      composer.lock
  5. 103
      lib/Catcher.php
  6. 11
      lib/Catcher/ArgumentCountError.php
  7. 196
      lib/Catcher/HTMLHandler.php
  8. 88
      lib/Catcher/Handler.php
  9. 32
      lib/Catcher/JSONHandler.php
  10. 62
      lib/Catcher/PlainTextHandler.php
  11. 11
      lib/Catcher/UnderflowException.php
  12. 100
      run
  13. 37
      test
  14. 17
      tests/bootstrap.php
  15. 657
      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.*

5
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 #
@ -14,8 +15,8 @@ Catcher uses classes called _handlers_ to handle throwables sent its way. PHP is
## 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 ##

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

103
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");
}
$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;
throw new ArgumentCountError(__METHOD__ . 'expects at least 1 argument, 0 given');
}
$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,29 +143,11 @@ class Catcher {
public function unshiftHandler(Handler ...$handlers): void {
if (count($handlers) === 0) {
throw new \ArgumentCountError(__METHOD__ . "expects at least 1 argument, 0 given\n");
}
$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);
throw new ArgumentCountError(__METHOD__ . 'expects at least 1 argument, 0 given');
}
if (count($handlers) > 0) {
$this->handlers = [ ...$handlers, ...$this->handlers ];
}
}
/**
@ -181,33 +159,14 @@ 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)) {
if ($this->errorHandlingMethod > self::THROW_NO_ERRORS && ($this->errorHandlingMethod === self::THROW_ALL_ERRORS || $this->isErrorFatal($code)) && !$this->isShuttingDown) {
$this->lastThrowable = $error;
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;
}
}
} else {
$this->handleThrowable($error);
return true;
}
}
// If preventing exit we don't want a false here to halt processing
return ($this->preventExit);
@ -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();
}
}

88
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,7 +86,7 @@ abstract class Handler {
public function dispatch(): void {
public function __invoke(): void {
if (count($this->outputBuffer) === 0) {
return;
}
@ -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));
}
}
}

62
lib/Catcher/PlainTextHandler.php

@ -7,77 +7,33 @@
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 handleCallback(array $output): array {
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code'];
return $output;
}
protected function dispatchCallback(): void {
if ($this->_logger) {
protected function invokeCallback(): void {
foreach ($this->outputBuffer as $o) {
$output = $this->serializeOutputThrowable($o);
if ($o['outputCode'] & self::SILENT) {
continue;
if (($o['code'] & self::OUTPUT) === 0) {
if ($o['code'] & self::LOG) {
$this->serializeOutputThrowable($o);
}
$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'];
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);
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
$this->_logger->alert($message, $context);
} else {
$this->_logger->critical($message, $context);
}
}
protected function serializeOutputThrowable(array $outputThrowable, bool $previous = false): string {
$class = $outputThrowable['class'] ?? null;
@ -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/' ]);
}
}

657
tests/cases/TestCatcher.php

@ -7,493 +7,274 @@
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;
public function testHandlerBubbling(): void {
$this->catcher->unregister();
trigger_error('Ook!', \E_USER_NOTICE);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_NOTICE, $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;
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_ERROR);
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();
}
trigger_error('Ook!', \E_USER_WARNING);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_WARNING, $t->getCode());
public function testHandlerForceExiting(): void {
$this->catcher->setHandlers(new TestingHandler([ 'forceExit' => true ]));
$this->catcher->errorHandlingMethod = Catcher::THROW_NO_ERRORS;
$this->catcher->preventExit = true;
trigger_error('Ook!', \E_USER_ERROR);
$t = $c->getLastThrowable();
$this->assertSame(Error::class, $t::class);
$this->assertSame(\E_USER_ERROR, $t->getCode());
trigger_error('Ook', \E_USER_ERROR);
$this->assertSame(Error::class, $this->catcher->getLastThrowable()::class);
}
$er = error_reporting();
error_reporting(0);
trigger_error('Ook!', \E_USER_ERROR);
error_reporting($er);
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());
}
$c->unregister();
/** @dataProvider provideShutdownTests */
public function testShutdownHandling(\Closure $closure): void {
$this->catcher->unregister();
$h1 = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]);
$h2 = Phony::partialMock(HTMLHandler::class, [ [ 'silent' => true ] ]);
$h3 = Phony::partialMock(JSONHandler::class, [ [ 'silent' => true ] ]);
$h1 = Phake::partialMock(TestingHandler::class);
$this->catcher = $m = Phake::partialMock(Catcher::class, $h1);
$closure($m, $h1);
}
$h = Phony::partialMock(Catcher::class, [
$h1->get(),
$h2->get(),
$h3->get()
]);
$c = $h->get();
$c->preventExit = true;
$c->throwErrors = false;
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'));
trigger_error('Ook!', \E_USER_ERROR);
$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'));
$h1->dispatch->called();
$h2->dispatch->called();
$h3->dispatch->called();
$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->throwErrors = true;
try {
trigger_error('Ook!', \E_USER_WARNING);
} catch (\Throwable $t) {
$this->assertInstanceOf(Error::class, $t);
$this->assertSame(\E_USER_WARNING, $t->getCode());
$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_ERROR);
} catch (\Throwable $t) {
$this->assertInstanceOf(Error::class, $t);
$this->assertSame(\E_USER_ERROR, $t->getCode());
trigger_error('Ook!', \E_USER_WARNING);
} catch (\Throwable $t) {} finally {
$this->assertNull($t);
$this->assertNull($this->catcher->getLastThrowable());
}
}
$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([
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
]);
$c = $h->get();
$c->handleShutdown();
$h->handleError->called();
$h->handleThrowable->called();
$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