Browse Source

100% Coverage

2.1.0
Dustin Wilson 1 year ago
parent
commit
17b8857480
  1. 4
      lib/Catcher.php
  2. 252
      lib/Catcher/HTMLHandler.php
  3. 90
      lib/Catcher/Handler.php
  4. 23
      lib/Catcher/HandlerOutput.php
  5. 102
      lib/Catcher/JSONHandler.php
  6. 152
      lib/Catcher/PlainTextHandler.php
  7. 20
      lib/Catcher/ThrowableController.php
  8. 29
      tests/cases/TestCatcher.php
  9. 48
      tests/cases/TestHTMLHandler.php
  10. 122
      tests/cases/TestHandler.php
  11. 36
      tests/cases/TestJSONHandler.php
  12. 52
      tests/cases/TestPlainTextHandler.php
  13. 33
      tests/cases/TestThrowableController.php
  14. 1
      tests/phpunit.dist.xml

4
lib/Catcher.php

@ -179,11 +179,11 @@ class Catcher {
$controller = new ThrowableController($throwable);
foreach ($this->handlers as $h) {
$output = $h->handle($controller);
if ($output->outputCode & Handler::NOW) {
if ($output['outputCode'] & Handler::NOW) {
$h->dispatch();
}
$controlCode = $output->controlCode;
$controlCode = $output['controlCode'];
if ($controlCode & Handler::BREAK) {
break;
}

252
lib/Catcher/HTMLHandler.php

@ -17,8 +17,6 @@ class HTMLHandler extends Handler {
protected ?\DOMDocument $_document = null;
/** The XPath path to the element where the errors should be inserted */
protected string $_errorPath = '/html/body';
/** If true the handler will output times to the output; defaults to true */
protected bool $_outputTime = true;
/** The PHP-standard date format which to use for times printed to output */
protected string $_timeFormat = 'H:i:s';
@ -44,8 +42,8 @@ class HTMLHandler extends Handler {
$this->xpath = new \DOMXPath($this->_document);
$location = $this->xpath->query($this->_errorPath);
if (count($location) === 0 || !$location->item(0) instanceof \DOMElement) {
throw new \InvalidArgumentException('Option "errorPath" must correspond to a location that is an instance of \DOMElement');
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);
}
@ -53,161 +51,145 @@ class HTMLHandler extends Handler {
protected function buildThrowable(ThrowableController $controller): \DOMDocumentFragment {
$throwable = $controller->getThrowable();
protected function buildOutputThrowable(array $outputThrowable, bool $previous = false): \DOMDocumentFragment {
$frag = $this->_document->createDocumentFragment();
$tFrag = $this->_document->createDocumentFragment();
$ip = $frag;
$hasSiblings = false;
$b = $this->_document->createElement('b');
$type = $controller->getErrorType();
$class = $throwable::class;
$b->appendChild($this->_document->createTextNode($type ?? $class));
if ($type !== null) {
$b->firstChild->textContent .= ' ';
$code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode("($class)"));
$b->appendChild($code);
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(' '));
}
$frag->appendChild($b);
$frag->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($throwable->getMessage()));
$frag->appendChild($i);
$frag->appendChild($this->_document->createTextNode(' in file '));
$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($throwable->getFile()));
$frag->appendChild($code);
$frag->appendChild($this->_document->createTextNode(' on line ' . $throwable->getLine()));
return $frag;
}
protected function dispatchCallback(): void {
$ul = $this->_document->createElement('ul');
$this->errorLocation->appendChild($ul);
$allSilent = true;
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
continue;
}
$code->appendChild($this->_document->createTextNode($outputThrowable['file']));
$tFrag->appendChild($code);
$tFrag->appendChild($this->_document->createTextNode(" on line {$outputThrowable['line']}"));
$allSilent = false;
if (isset($outputThrowable['previous'])) {
$ul = $this->_document->createElement('ul');
$li = $this->_document->createElement('li');
$li->appendChild($o->output);
$li->appendChild($this->buildOutputThrowable($outputThrowable['previous'], true));
$ul->appendChild($li);
$ip->appendChild($ul);
$hasSiblings = true;
}
if (!$allSilent) {
$this->print($this->_document->saveHTML());
}
}
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']));
}
protected function handleCallback(ThrowableController $controller): HandlerOutput {
$frag = $this->_document->createDocumentFragment();
$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 ($this->_outputTime && $this->_timeFormat !== '') {
$p = $this->_document->createElement('p');
$time = $this->_document->createElement('time');
$now = new \DateTimeImmutable();
$tz = $now->getTimezone()->getName();
if ($tz !== 'UTC' || !in_array($this->_timeFormat, [ 'c', 'Y-m-d\TH:i:sO', 'Y-m-d\TH:i:sP', 'Y-m-d\TH:i:s\Z' ])) {
$n = ($tz !== 'UTC') ? $now->setTimezone(new \DateTimeZone('UTC')) : $now;
$time->setAttribute('datetime', $n->format('Y-m-d\TH:i:s\Z'));
if (isset($frame['args'])) {
$pre = $this->_document->createElement('pre');
$pre->appendChild($this->_document->createTextNode(trim(print_r($frame['args'], true))));
$li->appendChild($pre);
}
}
$time->appendChild($this->_document->createTextNode($now->format($this->_timeFormat)));
$p->appendChild($time);
$frag->appendChild($p);
$ip = $this->_document->createElement('div');
$frag->appendChild($ip);
$hasSiblings = true;
}
if ($hasSiblings) {
$p = $this->_document->createElement('p');
$p->appendChild($tFrag);
$ip->insertBefore($p, $ip->firstChild);
} else {
$ip = $frag;
$ip->appendChild($tFrag);
}
$p = $this->_document->createElement('p');
$p->appendChild($this->buildThrowable($controller));
$ip->appendChild($p);
if ($this->_outputPrevious) {
$prev = $controller->getPrevious();
if ($prev !== null) {
$ul = $this->_document->createElement('ul');
$ip->appendChild($ul);
$f = null;
while ($prev) {
if ($f !== null) {
$p = $this->_document->createElement('p');
$p->appendChild($f);
$li->appendChild($p);
$ul = $this->_document->createElement('ul');
$li->appendChild($ul);
}
return $frag;
}
$li = $this->_document->createElement('li');
$ul->appendChild($li);
$f = $this->_document->createDocumentFragment();
$span = $this->_document->createElement('span');
$span->appendChild($this->_document->createTextNode('Caused by:'));
$f->appendChild($span);
$f->appendChild($this->_document->createTextNode(' '));
$f->appendChild($this->buildThrowable($prev));
protected function dispatchCallback(): void {
$frag = $this->_document->createDocumentFragment();
$allSilent = true;
foreach ($this->outputBuffer as $o) {
if ($o['outputCode'] & self::SILENT) {
continue;
}
$prev = $prev->getPrevious();
}
$li = $this->_document->createElement('li');
$li->appendChild($this->buildOutputThrowable($o));
$frag->appendChild($li);
$li->appendChild($f);
}
$allSilent = false;
}
if ($this->_outputBacktrace) {
$frames = $controller->getFrames();
if (count($frames) > 0) {
$p = $this->_document->createElement('p');
$p->appendChild($this->_document->createTextNode('Stack trace:'));
$ip->appendChild($p);
$ol = $this->_document->createElement('ol');
$ip->appendChild($ol);
$num = 0;
foreach ($frames as $frame) {
$li = $this->_document->createElement('li');
$ol->appendChild($li);
$args = (isset($frame['args']) && $this->_backtraceArgFrameLimit >= ++$num);
if ($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);
$text = $frame['error'] ?? $frame['class'] ?? '';
if (isset($frame['function'])) {
$text = ((isset($frame['class'])) ? '::' : '') . "{$frame['function']}()";
}
$code->appendChild($this->_document->createTextNode($text));
$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 ($args) {
$pre = $this->_document->createElement('pre');
$pre->appendChild($this->_document->createTextNode(print_r($frame['args'], true)));
$li->appendChild($pre);
}
}
}
if (!$allSilent) {
$ul = $this->_document->createElement('ul');
$ul->appendChild($frag);
$this->errorLocation->appendChild($ul);
$this->print($this->serializeDocument());
}
}
return new HandlerOutput($this->getControlCode(), $this->getOutputCode(), $frag);
protected function serializeDocument() {
return $this->_document->saveHTML();
}
}

90
lib/Catcher/Handler.php

@ -55,10 +55,14 @@ abstract class Handler {
protected bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */
protected bool $_outputPrevious = true;
/** If true the handler will output times to the output; defaults to true */
protected bool $_outputTime = true;
/** When the SAPI is cli output errors to stderr; defaults to true */
protected bool $_outputToStderr = true;
/** If true the handler will be silent and won't output */
protected bool $_silent = false;
/** The PHP-standard date format which to use for timestamps in output */
protected string $_timeFormat = 'Y-m-d\TH:i:s.vO';
@ -67,7 +71,7 @@ abstract class Handler {
foreach ($options as $key => $value) {
$key = "_$key";
if ($key === '_httpCode' && is_int($value) && $value !== 200 && max(400, min($value, 600)) !== $value) {
throw new \InvalidArgumentException('Option "httpCode" can only be an integer of 200 or 400-599');
throw new \RangeException('Option "httpCode" can only be an integer of 200 or 400-599');
}
$this->$key = $value;
@ -110,8 +114,35 @@ abstract class Handler {
return $this->$name;
}
public function handle(ThrowableController $controller): HandlerOutput {
$output = $this->handleCallback($controller);
public function handle(ThrowableController $controller): array {
$output = $this->buildOutputArray($controller);
if ($this->_outputBacktrace) {
$output['frames'] = $controller->getFrames(argFrameLimit: $this->_backtraceArgFrameLimit);
}
if ($this->_outputTime && $this->_timeFormat !== '') {
$output['time'] = new \DateTimeImmutable();
}
$code = self::CONTINUE;
if ($this->_forceBreak) {
$code = self::BREAK;
}
if ($this->_forceExit) {
$code |= self::EXIT;
}
$output['controlCode'] = $code;
$code = self::OUTPUT;
if ($this->_silent) {
$code = self::SILENT;
}
if ($this->_forceOutputNow) {
$code |= self::NOW;
}
$output['outputCode'] = $code;
$output = $this->handleCallback($output);
$this->outputBuffer[] = $output;
return $output;
}
@ -127,33 +158,52 @@ abstract class Handler {
}
abstract protected function dispatchCallback(): void;
protected function buildOutputArray(ThrowableController $controller): array {
$throwable = $controller->getThrowable();
protected function getControlCode(): int {
$code = self::CONTINUE;
if ($this->_forceBreak) {
$code = self::BREAK;
$output = [
'controller' => $controller,
'class' => $throwable::class,
'code' => $throwable->getCode(),
'file' => $throwable->getFile() ?: '[UNKNOWN]',
'line' => $throwable->getLine(),
'message' => $throwable->getMessage()
];
if ($throwable instanceof Error) {
$output['errorType'] = $controller->getErrorType();
}
if ($this->_forceExit) {
$code |= self::EXIT;
if ($this->_outputPrevious) {
$prevController = $controller->getPrevious();
if ($prevController) {
$output['previous'] = $this->buildOutputArray($prevController);
}
}
return $code;
return $output;
}
protected function getOutputCode(): int {
$code = self::OUTPUT;
if ($this->_silent) {
$code = self::SILENT;
protected function cleanOutputThrowable(array $outputThrowable): array {
unset($outputThrowable['controller']);
unset($outputThrowable['controlCode']);
unset($outputThrowable['outputCode']);
if (isset($outputThrowable['previous'])) {
$outputThrowable['previous'] = $this->cleanOutputThrowable($outputThrowable['previous']);
}
if ($this->_forceOutputNow) {
$code |= self::NOW;
if (isset($outputThrowable['time'])) {
$outputThrowable['time'] = $outputThrowable['time']->format($this->_timeFormat);
}
return $code;
return $outputThrowable;
}
abstract protected function handleCallback(ThrowableController $controller): HandlerOutput;
abstract protected function dispatchCallback(): void;
protected function handleCallback(array $output): array {
return $output;
}
protected function print(string $string): void {
$string = "$string\n";

23
lib/Catcher/HandlerOutput.php

@ -1,23 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher;
class HandlerOutput {
public readonly int $controlCode;
public readonly mixed $output;
public readonly int $outputCode;
public function __construct(int $controlCode, int $outputCode, mixed $output) {
$this->controlCode = $controlCode;
$this->outputCode = $outputCode;
$this->output = $output;
}
}

102
lib/Catcher/JSONHandler.php

@ -7,112 +7,26 @@
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher;
use \Psr\Log\LoggerInterface;
class JSONHandler extends Handler {
public const CONTENT_TYPE = 'application/json';
/** If true the handler will output times to the output; defaults to true */
protected bool $_outputTime = true;
/** The PHP-standard date format which to use for timestamps in output */
protected string $_timeFormat = 'c';
protected function dispatchCallback(): void {
$output = [
'status' => (string)$this->_httpCode
];
$errors = [];
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
foreach ($this->outputBuffer as $key => $value) {
if ($value['outputCode'] & self::SILENT) {
unset($this->outputBuffer[$key]);
continue;
}
$errors[] = $o->output;
}
if (count($errors) === 0) {
return;
}
$output['errors'] = $errors;
$this->print(json_encode($output, \JSON_PARTIAL_OUTPUT_ON_ERROR));
}
protected function handleCallback(ThrowableController $controller): HandlerOutput {
$output = $this->buildThrowableArray($controller);
if ($this->_outputPrevious) {
$target = $output;
$prevController = $controller->getPrevious();
while ($prevController) {
$prev = $this->buildThrowableArray($prevController);
$target['previous'] = $prev;
$target = $prev;
$prevController = $prevController->getPrevious();
}
}
if ($this->_outputBacktrace) {
$output['frames'] = $controller->getFrames();
}
if ($this->_outputTime && $this->_timeFormat !== '') {
$output['timestamp'] = (new \DateTime())->format($this->_timeFormat);
}
return new HandlerOutput($this->getControlCode(), $this->getOutputCode(), $output);
}
protected function log(\Throwable $throwable, string $message): void {
if ($throwable instanceof \Error) {
switch ($throwable->getCode()) {
case \E_NOTICE:
case \E_USER_NOTICE:
case \E_STRICT:
$this->_logger->notice($message);
break;
case \E_WARNING:
case \E_COMPILE_WARNING:
case \E_USER_WARNING:
case \E_DEPRECATED:
case \E_USER_DEPRECATED:
$this->_logger->warning($message);
break;
case \E_RECOVERABLE_ERROR:
$this->_logger->error($message);
break;
case \E_PARSE:
case \E_CORE_ERROR:
case \E_COMPILE_ERROR:
$this->_logger->alert($message);
break;
default: $this->_logger->critical($message);
}
} elseif ($throwable instanceof \Exception && ($throwable instanceof \PharException || $throwable instanceof \RuntimeException)) {
$this->_logger->alert($message);
} else {
$this->_logger->critical($message);
$this->outputBuffer[$key] = $this->cleanOutputThrowable($this->outputBuffer[$key]);
}
}
protected function buildThrowableArray(ThrowableController $controller): array {
$throwable = $controller->getThrowable();
$type = $throwable::class;
if ($throwable instanceof Error) {
$t = $controller->getErrorType();
$t = ($throwable instanceof Error) ? $controller->getErrorType() : null;
$type = ($t !== null) ? "$t (" . $type . ")" : $type;
if (count($this->outputBuffer) > 0) {
$this->print(json_encode([
'errors' => $this->outputBuffer
], \JSON_INVALID_UTF8_SUBSTITUTE | \JSON_PARTIAL_OUTPUT_ON_ERROR | \JSON_UNESCAPED_SLASHES));
}
return [
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'message' => $throwable->getMessage(),
'type' => $type
];
}
}

152
lib/Catcher/PlainTextHandler.php

@ -15,91 +15,26 @@ class PlainTextHandler extends Handler {
/** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */
protected ?LoggerInterface $_logger = null;
/** If true the handler will output times to the output; defaults to true */
protected bool $_outputTime = true;
/** The PHP-standard date format which to use for timestamps in output */
protected string $_timeFormat = '[H:i:s]';
protected function dispatchCallback(): void {
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
if ($o['outputCode'] & self::SILENT) {
continue;
}
$this->print($o->output);
$this->print($this->serializeOutputThrowable($o));
}
}
protected function handleCallback(ThrowableController $controller): HandlerOutput {
$output = $this->serializeThrowable($controller);
if ($this->_outputPrevious) {
$prevController = $controller->getPrevious();
$indent = '';
while ($prevController) {
$output .= sprintf("\n%s↳ %s", $indent, $this->serializeThrowable($prevController));
$prevController = $prevController->getPrevious();
$indent .= ' ';
}
}
if ($this->_outputBacktrace) {
$frames = $controller->getFrames();
$output .= "\n\nStack trace:";
$num = 1;
$maxDigits = strlen((string)count($frames));
foreach ($frames as $frame) {
$class = (!empty($frame['error'])) ? "{$frame['error']} ({$frame['class']})" : $frame['class'] ?? '';
$function = $frame['function'] ?? '';
$args = '';
if (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num) {
$args = "\n" . preg_replace('/^/m', str_repeat(' ', $maxDigits) . '| ', print_r($frame['args'], true));
}
$template = "\n%{$maxDigits}d. %s";
if ($class && $function) {
$template .= '::';
}
$template .= ($function) ? '%s()' : '%s';
$template .= ' %s:%d%s';
$output .= sprintf(
"$template\n",
$num++,
$class,
$function,
$frame['file'],
$frame['line'],
$args
);
}
$output = rtrim($output, "\n");
}
// The logger will handle timestamps itself.
if ($this->_logger !== null) {
$this->log($controller->getThrowable(), $output);
}
if (!$this->_silent && $this->_outputTime && $this->_timeFormat !== '') {
$time = (new \DateTime())->format($this->_timeFormat) . ' ';
$timeStrlen = strlen($time);
$output = preg_replace('/^/m', str_repeat(' ', $timeStrlen), $output);
$output = preg_replace('/^ {' . $timeStrlen . '}/', $time, $output);
}
$outputCode = $this->getOutputCode();
return new HandlerOutput($this->getControlCode(), (\PHP_SAPI === 'cli') ? $outputCode | self::NOW : $outputCode, $output);
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 {
if ($throwable instanceof \Error) {
switch ($throwable->getCode()) {
@ -132,21 +67,72 @@ class PlainTextHandler extends Handler {
}
}
protected function serializeThrowable(ThrowableController $controller): string {
$throwable = $controller->getThrowable();
$class = $throwable::class;
if ($throwable instanceof Error) {
$type = $controller->getErrorType();
$type = ($throwable instanceof Error) ? $controller->getErrorType() : null;
$class = ($type !== null) ? "$type (" . $throwable::class . ")" : $throwable::class;
protected function serializeOutputThrowable(array $outputThrowable, bool $previous = false): string {
$class = $outputThrowable['class'] ?? null;
if ($class !== null && !empty($outputThrowable['errorType'])) {
$class = "{$outputThrowable['errorType']} ($class)";
}
return sprintf(
'%s: %s in file %s on line %d',
$output = sprintf(
'%s: %s in file %s on line %d' . \PHP_EOL,
$class,
$throwable->getMessage(),
$throwable->getFile(),
$throwable->getLine()
$outputThrowable['message'],
$outputThrowable['file'],
$outputThrowable['line']
);
if (!empty($outputThrowable['previous'])) {
if ($previous) {
$output .= ' ';
}
$output .= '↳ ' . $this->serializeOutputThrowable($outputThrowable['previous'], true);
}
if (!$previous) {
if (isset($outputThrowable['frames']) && count($outputThrowable['frames']) > 0) {
$output .= \PHP_EOL . 'Stack trace:' . \PHP_EOL;
$maxDigits = strlen((string)count($outputThrowable['frames']));
$indent = str_repeat(' ', $maxDigits);
foreach ($outputThrowable['frames'] as $key => $frame) {
$method = null;
if (!empty($frame['class'])) {
if (!empty($frame['errorType'])) {
$method = "{$frame['errorType']} ({$frame['class']})";
} else {
$method = $frame['class'];
if (!empty($frame['function'])) {
$method .= "::{$frame['function']}";
}
}
} elseif (!empty($frame['function'])) {
$method = $frame['function'];
}
$output .= sprintf("%{$maxDigits}d. %s %s:%d" . \PHP_EOL,
$key + 1,
$method,
$frame['file'],
$frame['line']
);
if (!empty($frame['args']) && $this->_backtraceArgFrameLimit > $key) {
$output .= preg_replace('/^/m', "$indent| ", print_r($frame['args'], true)) . \PHP_EOL;
}
}
$output = rtrim($output) . \PHP_EOL;
}
if (!empty($this->_logger)) {
$this->log($outputThrowable['controller']->getThrowable(), $output);
}
if (!empty($outputThrowable['time'])) {
$timestamp = $outputThrowable['time']->format($this->_timeFormat) . ' ';
$output = ltrim(preg_replace('/^/m', str_repeat(' ', strlen($timestamp)), "$timestamp$output"));
}
}
return $output;
}
}

20
lib/Catcher/ThrowableController.php

@ -88,10 +88,13 @@ class ThrowableController {
}
/** Gets backtrace frames */
public function getFrames(): array {
public function getFrames(int $argFrameLimit = \PHP_INT_MAX): array {
if ($this->frames !== null) {
return $this->frames;
}
if ($argFrameLimit < 0) {
throw new \RangeException('Argument argFrameLimit cannot be less than 0');
}
if (
!$this->throwable instanceof \Error ||
@ -149,7 +152,7 @@ class ThrowableController {
// Add a frame for the throwable to the beginning of the array
$f = [
'file' => $this->throwable->getFile(),
'file' => $this->throwable->getFile() ?: '[UNKNOWN]',
'line' => (int)$this->throwable->getLine(),
'class' => $this->throwable::class,
'args' => [
@ -159,10 +162,10 @@ class ThrowableController {
// Add the error code and type if it is an Error.
if ($this->throwable instanceof \Error) {
$error = $this->getErrorType();
if ($error !== null) {
$errorType = $this->getErrorType();
if ($errorType !== null) {
$f['code'] = $this->throwable->getCode();
$f['type'] = $error;
$f['errorType'] = $errorType;
}
}
@ -187,6 +190,13 @@ class ThrowableController {
$frames = $temp;
}
// Lastly, remove all args past the specified limit.
if ($argFrameLimit !== \PHP_INT_MAX) {
for ($i = $argFrameLimit, $frameCount = count($frames); $i < $frameCount; $i++) {
unset($frames[$i]['args']);
}
}
$this->frames = $frames;
return $frames;
}

29
tests/cases/TestCatcher.php

@ -16,10 +16,7 @@ use MensBeam\Foundation\Catcher\{
};
use Eloquent\Phony\Phpunit\Phony;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestCatcher extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher::__construct
@ -35,7 +32,6 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
$c = new Catcher();
$c->preventExit = true;
$c->throwErrors = false;
$this->assertSame('MensBeam\Foundation\Catcher', $c::class);
$this->assertSame(1, count($c->getHandlers()));
$this->assertSame(PlainTextHandler::class, $c->getHandlers()[0]::class);
$c->unregister();
@ -67,15 +63,13 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
@ -316,22 +310,19 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::getLastThrowable
* @covers \MensBeam\Foundation\Catcher::exit
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
@ -409,14 +400,11 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
@ -456,14 +444,11 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable

48
tests/cases/TestHTMLHandler.php

@ -15,10 +15,7 @@ use MensBeam\Foundation\Catcher\{
ThrowableController
};
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestHTMLHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::__construct
@ -31,67 +28,62 @@ class TestHTMLHandler extends \PHPUnit\Framework\TestCase {
}
/**
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::buildThrowable
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::buildOutputThrowable
*
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::__construct
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::serializeDocument
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod_buildThrowable(): void {
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,
'outputTime' => false
'outputToStderr' => false
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o->controlCode);
$this->assertInstanceOf(\DOMDocumentFragment::class, $o->output);
$this->assertSame(Handler::CONTINUE, $o['controlCode']);
ob_start();
$h->dispatch();
ob_end_clean();
}
/**
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::dispatchCallback
*
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\Handler::setOption
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::handleCallback
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::__construct
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::buildThrowable
* @covers \MensBeam\Foundation\Catcher\HTMLHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod_dispatchCallback(): void {
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new Error(message: 'Eek!', code: \E_USER_ERROR, previous: new \Error(message: 'Ack!'))));
$c = new ThrowableController(new \Exception(message: 'Ook!'));
$h = new HTMLHandler([
'outputToStderr' => false
'backtraceArgFrameLimit' => 1,
'outputToStderr' => false,
'silent' => true
]);
$h->handle($c);
ob_start();
$h->dispatch();
$o = ob_get_clean();
$this->assertNotNull($o);
$h->setOption('silent', true);
$h->handle($c);
$h->dispatch();
$this->assertEmpty($o);
}
}

122
tests/cases/TestHandler.php

@ -11,55 +11,74 @@ use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
Error,
HTMLHandler,
PlainTextHandler,
JSONHandler,
PlainTextHandler
ThrowableController
};
use Eloquent\Phony\Phpunit\Phony;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/
public function testMethod___construct__exception(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectException(\RangeException::class);
new PlainTextHandler([ 'httpCode' => 42 ]);
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::cleanOutputThrowable
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\Handler::handleCallback
* @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\JSONHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__getControlCode(): void {
// Just need to test forceExit for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]));
$c->preventExit = true;
$c->throwErrors = false;
trigger_error('Ook!', \E_USER_ERROR);
$this->assertSame(\E_USER_ERROR, $c->getLastThrowable()->getCode());
$c->unregister();
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\Foundation\Catcher\Handler::handle
*
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::handleCallback
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\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']));
}
/**
@ -74,13 +93,8 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
@ -97,6 +111,11 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$c->unregister();
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::setOption
*
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/
public function testMethod__setOption(): void {
$h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]);
$h->setOption('forceExit', false);
@ -104,7 +123,6 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$r->setAccessible(true);
$this->assertFalse($r->getValue($h));
//$h = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]);
$m = Phony::partialMock(Catcher::class, [
$h
]);
@ -118,39 +136,6 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$c->unregister();
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
*
* @covers \MensBeam\Foundation\Catcher::__construct
* @covers \MensBeam\Foundation\Catcher::handleError
* @covers \MensBeam\Foundation\Catcher::handleThrowable
* @covers \MensBeam\Foundation\Catcher::pushHandler
* @covers \MensBeam\Foundation\Catcher::register
* @covers \MensBeam\Foundation\Catcher::unregister
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod__getOutputCode(): void {
// Just need to test forceOutputNow for coverage purposes
$c = new Catcher(new PlainTextHandler([ 'forceOutputNow' => true, 'silent' => true ]));
$c->preventExit = true;
$c->throwErrors = false;
trigger_error('Ook!', \E_USER_ERROR);
$this->assertSame(\E_USER_ERROR, $c->getLastThrowable()->getCode());
$c->unregister();
}
/**
* @covers \MensBeam\Foundation\Catcher\Handler::print
*
@ -163,13 +148,10 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious

36
tests/cases/TestJSONHandler.php

@ -0,0 +1,36 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Foundation\Catcher\Test;
use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{
Error,
Handler,
JSONHandler,
ThrowableController
};
class TestJSONHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher\JSONHandler::dispatchCallback
*/
public function testMethod_dispatchCallback(): void {
$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);
}
}

52
tests/cases/TestPlainTextHandler.php

@ -17,21 +17,18 @@ use MensBeam\Foundation\Catcher\{
use Eloquent\Phony\Phpunit\Phony,
Psr\Log\LoggerInterface;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
*
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::log
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
@ -39,16 +36,22 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/
public function testMethod_handleCallback(): void {
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error('Eek!')));
$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
'outputBacktrace' => true,
'outputToStderr' => false
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o->controlCode);
$this->assertSame(Handler::OUTPUT | Handler::NOW, $o->outputCode);
$this->assertStringContainsString('↳', $o->output);
$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();
}
@ -58,12 +61,11 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
*
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
@ -71,7 +73,10 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
*/
public function testMethod_log(): void {
$l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([ 'logger' => $l->get() ]);
$h = new PlainTextHandler([
'logger' => $l->get(),
'outputToStderr' => false
]);
$e = [
'notice' => [
@ -99,14 +104,25 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
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();
}
}

33
tests/cases/TestThrowableController.php

@ -32,14 +32,11 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
@ -63,6 +60,24 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$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());
@ -90,6 +105,9 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$this->assertNull($c->getErrorType());
$c = new ThrowableController(new \Exception('Ook!'));
$this->assertNull($c->getErrorType());
// For code coverage purposes.
$this->assertNull($c->getErrorType());
}
/**
@ -122,7 +140,7 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$f = false;
try {
throw new \Exception(message: 'Ook!', previous: new Error('Ook!', \E_ERROR));
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();
@ -164,5 +182,10 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
// 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);
}
}

1
tests/phpunit.dist.xml

@ -20,6 +20,7 @@
<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>

Loading…
Cancel
Save