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); $controller = new ThrowableController($throwable);
foreach ($this->handlers as $h) { foreach ($this->handlers as $h) {
$output = $h->handle($controller); $output = $h->handle($controller);
if ($output->outputCode & Handler::NOW) { if ($output['outputCode'] & Handler::NOW) {
$h->dispatch(); $h->dispatch();
} }
$controlCode = $output->controlCode; $controlCode = $output['controlCode'];
if ($controlCode & Handler::BREAK) { if ($controlCode & Handler::BREAK) {
break; break;
} }

252
lib/Catcher/HTMLHandler.php

@ -17,8 +17,6 @@ class HTMLHandler extends Handler {
protected ?\DOMDocument $_document = null; protected ?\DOMDocument $_document = null;
/** The XPath path to the element where the errors should be inserted */ /** The XPath path to the element where the errors should be inserted */
protected string $_errorPath = '/html/body'; 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 */ /** The PHP-standard date format which to use for times printed to output */
protected string $_timeFormat = 'H:i:s'; protected string $_timeFormat = 'H:i:s';
@ -44,8 +42,8 @@ class HTMLHandler extends Handler {
$this->xpath = new \DOMXPath($this->_document); $this->xpath = new \DOMXPath($this->_document);
$location = $this->xpath->query($this->_errorPath); $location = $this->xpath->query($this->_errorPath);
if (count($location) === 0 || !$location->item(0) instanceof \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'); throw new \InvalidArgumentException('Option "errorPath" must correspond to a location that is an instance of \DOMElement or \DOMDocumentFragment');
} }
$this->errorLocation = $location->item(0); $this->errorLocation = $location->item(0);
} }
@ -53,161 +51,145 @@ class HTMLHandler extends Handler {
protected function buildThrowable(ThrowableController $controller): \DOMDocumentFragment { protected function buildOutputThrowable(array $outputThrowable, bool $previous = false): \DOMDocumentFragment {
$throwable = $controller->getThrowable();
$frag = $this->_document->createDocumentFragment(); $frag = $this->_document->createDocumentFragment();
$tFrag = $this->_document->createDocumentFragment();
$ip = $frag;
$hasSiblings = false;
$b = $this->_document->createElement('b'); if ($previous === false) {
$type = $controller->getErrorType(); if (isset($outputThrowable['time'])) {
$class = $throwable::class; $p = $this->_document->createElement('p');
$b->appendChild($this->_document->createTextNode($type ?? $class)); $time = $this->_document->createElement('time');
if ($type !== null) { $time->setAttribute('datetime', $outputThrowable['time']->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.vO'));
$b->firstChild->textContent .= ' '; $time->appendChild($this->_document->createTextNode($outputThrowable['time']->format($this->_timeFormat)));
$code = $this->_document->createElement('code'); $p->appendChild($time);
$code->appendChild($this->_document->createTextNode("($class)")); $frag->appendChild($p);
$b->appendChild($code);
$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 = $this->_document->createElement('i');
$i->appendChild($this->_document->createTextNode($throwable->getMessage())); $i->appendChild($this->_document->createTextNode($outputThrowable['message']));
$frag->appendChild($i); $tFrag->appendChild($i);
$frag->appendChild($this->_document->createTextNode(' in file ')); $tFrag->appendChild($this->_document->createTextNode(' in file '));
$code = $this->_document->createElement('code'); $code = $this->_document->createElement('code');
$code->appendChild($this->_document->createTextNode($throwable->getFile())); $code->appendChild($this->_document->createTextNode($outputThrowable['file']));
$frag->appendChild($code); $tFrag->appendChild($code);
$frag->appendChild($this->_document->createTextNode(' on line ' . $throwable->getLine())); $tFrag->appendChild($this->_document->createTextNode(" on line {$outputThrowable['line']}"));
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;
}
$allSilent = false; if (isset($outputThrowable['previous'])) {
$ul = $this->_document->createElement('ul');
$li = $this->_document->createElement('li'); $li = $this->_document->createElement('li');
$li->appendChild($o->output); $li->appendChild($this->buildOutputThrowable($outputThrowable['previous'], true));
$ul->appendChild($li); $ul->appendChild($li);
$ip->appendChild($ul);
$hasSiblings = true;
} }
if (!$allSilent) { if ($previous === false && isset($outputThrowable['frames'])) {
$this->print($this->_document->saveHTML()); $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 { $t->appendChild($this->_document->createTextNode("\u{00a0}\u{00a0}"));
$frag = $this->_document->createDocumentFragment(); $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 !== '') { if (isset($frame['args'])) {
$p = $this->_document->createElement('p'); $pre = $this->_document->createElement('pre');
$time = $this->_document->createElement('time'); $pre->appendChild($this->_document->createTextNode(trim(print_r($frame['args'], true))));
$now = new \DateTimeImmutable(); $li->appendChild($pre);
$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'));
} }
$time->appendChild($this->_document->createTextNode($now->format($this->_timeFormat)));
$p->appendChild($time);
$frag->appendChild($p);
$ip = $this->_document->createElement('div'); $hasSiblings = true;
$frag->appendChild($ip); }
if ($hasSiblings) {
$p = $this->_document->createElement('p');
$p->appendChild($tFrag);
$ip->insertBefore($p, $ip->firstChild);
} else { } else {
$ip = $frag; $ip->appendChild($tFrag);
} }
$p = $this->_document->createElement('p'); return $frag;
$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);
}
$li = $this->_document->createElement('li'); protected function dispatchCallback(): void {
$ul->appendChild($li); $frag = $this->_document->createDocumentFragment();
$f = $this->_document->createDocumentFragment(); $allSilent = true;
$span = $this->_document->createElement('span'); foreach ($this->outputBuffer as $o) {
$span->appendChild($this->_document->createTextNode('Caused by:')); if ($o['outputCode'] & self::SILENT) {
$f->appendChild($span); continue;
$f->appendChild($this->_document->createTextNode(' ')); }
$f->appendChild($this->buildThrowable($prev));
$prev = $prev->getPrevious(); $li = $this->_document->createElement('li');
} $li->appendChild($this->buildOutputThrowable($o));
$frag->appendChild($li);
$li->appendChild($f); $allSilent = false;
}
} }
if ($this->_outputBacktrace) { if (!$allSilent) {
$frames = $controller->getFrames(); $ul = $this->_document->createElement('ul');
if (count($frames) > 0) { $ul->appendChild($frag);
$p = $this->_document->createElement('p'); $this->errorLocation->appendChild($ul);
$p->appendChild($this->_document->createTextNode('Stack trace:')); $this->print($this->serializeDocument());
$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);
}
}
}
} }
}
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; protected bool $_outputBacktrace = false;
/** If true the handler will output previous throwables; defaults to true */ /** If true the handler will output previous throwables; defaults to true */
protected bool $_outputPrevious = 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 */ /** When the SAPI is cli output errors to stderr; defaults to true */
protected bool $_outputToStderr = true; protected bool $_outputToStderr = true;
/** If true the handler will be silent and won't output */ /** If true the handler will be silent and won't output */
protected bool $_silent = false; 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) { foreach ($options as $key => $value) {
$key = "_$key"; $key = "_$key";
if ($key === '_httpCode' && is_int($value) && $value !== 200 && max(400, min($value, 600)) !== $value) { 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; $this->$key = $value;
@ -110,8 +114,35 @@ abstract class Handler {
return $this->$name; return $this->$name;
} }
public function handle(ThrowableController $controller): HandlerOutput { public function handle(ThrowableController $controller): array {
$output = $this->handleCallback($controller); $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; $this->outputBuffer[] = $output;
return $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 { $output = [
$code = self::CONTINUE; 'controller' => $controller,
if ($this->_forceBreak) { 'class' => $throwable::class,
$code = self::BREAK; '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 { protected function cleanOutputThrowable(array $outputThrowable): array {
$code = self::OUTPUT; unset($outputThrowable['controller']);
if ($this->_silent) { unset($outputThrowable['controlCode']);
$code = self::SILENT; unset($outputThrowable['outputCode']);
if (isset($outputThrowable['previous'])) {
$outputThrowable['previous'] = $this->cleanOutputThrowable($outputThrowable['previous']);
} }
if ($this->_forceOutputNow) { if (isset($outputThrowable['time'])) {
$code |= self::NOW; $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 { protected function print(string $string): void {
$string = "$string\n"; $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); declare(strict_types=1);
namespace MensBeam\Foundation\Catcher; namespace MensBeam\Foundation\Catcher;
use \Psr\Log\LoggerInterface;
class JSONHandler extends Handler { class JSONHandler extends Handler {
public const CONTENT_TYPE = 'application/json'; 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 { protected function dispatchCallback(): void {
$output = [ foreach ($this->outputBuffer as $key => $value) {
'status' => (string)$this->_httpCode if ($value['outputCode'] & self::SILENT) {
]; unset($this->outputBuffer[$key]);
$errors = [];
foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) {
continue; continue;
} }
$errors[] = $o->output; $this->outputBuffer[$key] = $this->cleanOutputThrowable($this->outputBuffer[$key]);
}
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);
} }
}
protected function buildThrowableArray(ThrowableController $controller): array { if (count($this->outputBuffer) > 0) {
$throwable = $controller->getThrowable(); $this->print(json_encode([
$type = $throwable::class; 'errors' => $this->outputBuffer
if ($throwable instanceof Error) { ], \JSON_INVALID_UTF8_SUBSTITUTE | \JSON_PARTIAL_OUTPUT_ON_ERROR | \JSON_UNESCAPED_SLASHES));
$t = $controller->getErrorType();
$t = ($throwable instanceof Error) ? $controller->getErrorType() : null;
$type = ($t !== null) ? "$t (" . $type . ")" : $type;
} }
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) */ /** The PSR-3 compatible logger in which to log to; defaults to null (no logging) */
protected ?LoggerInterface $_logger = null; 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 */ /** The PHP-standard date format which to use for timestamps in output */
protected string $_timeFormat = '[H:i:s]'; protected string $_timeFormat = '[H:i:s]';
protected function dispatchCallback(): void { protected function dispatchCallback(): void {
foreach ($this->outputBuffer as $o) { foreach ($this->outputBuffer as $o) {
if ($o->outputCode & self::SILENT) { if ($o['outputCode'] & self::SILENT) {
continue; continue;
} }
$this->print($o->output); $this->print($this->serializeOutputThrowable($o));
} }
} }
protected function handleCallback(ThrowableController $controller): HandlerOutput { protected function handleCallback(array $output): array {
$output = $this->serializeThrowable($controller); $output['outputCode'] = (\PHP_SAPI === 'cli') ? $output['outputCode'] | self::NOW : $output['outputCode'];
if ($this->_outputPrevious) { return $output;
$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 log(\Throwable $throwable, string $message): void { protected function log(\Throwable $throwable, string $message): void {
if ($throwable instanceof \Error) { if ($throwable instanceof \Error) {
switch ($throwable->getCode()) { switch ($throwable->getCode()) {
@ -132,21 +67,72 @@ class PlainTextHandler extends Handler {
} }
} }
protected function serializeThrowable(ThrowableController $controller): string { protected function serializeOutputThrowable(array $outputThrowable, bool $previous = false): string {
$throwable = $controller->getThrowable(); $class = $outputThrowable['class'] ?? null;
$class = $throwable::class; if ($class !== null && !empty($outputThrowable['errorType'])) {
if ($throwable instanceof Error) { $class = "{$outputThrowable['errorType']} ($class)";
$type = $controller->getErrorType();
$type = ($throwable instanceof Error) ? $controller->getErrorType() : null;
$class = ($type !== null) ? "$type (" . $throwable::class . ")" : $throwable::class;
} }
return sprintf( $output = sprintf(
'%s: %s in file %s on line %d', '%s: %s in file %s on line %d' . \PHP_EOL,
$class, $class,
$throwable->getMessage(), $outputThrowable['message'],
$throwable->getFile(), $outputThrowable['file'],
$throwable->getLine() $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 */ /** Gets backtrace frames */
public function getFrames(): array { public function getFrames(int $argFrameLimit = \PHP_INT_MAX): array {
if ($this->frames !== null) { if ($this->frames !== null) {
return $this->frames; return $this->frames;
} }
if ($argFrameLimit < 0) {
throw new \RangeException('Argument argFrameLimit cannot be less than 0');
}
if ( if (
!$this->throwable instanceof \Error || !$this->throwable instanceof \Error ||
@ -149,7 +152,7 @@ class ThrowableController {
// Add a frame for the throwable to the beginning of the array // Add a frame for the throwable to the beginning of the array
$f = [ $f = [
'file' => $this->throwable->getFile(), 'file' => $this->throwable->getFile() ?: '[UNKNOWN]',
'line' => (int)$this->throwable->getLine(), 'line' => (int)$this->throwable->getLine(),
'class' => $this->throwable::class, 'class' => $this->throwable::class,
'args' => [ 'args' => [
@ -159,10 +162,10 @@ class ThrowableController {
// Add the error code and type if it is an Error. // Add the error code and type if it is an Error.
if ($this->throwable instanceof \Error) { if ($this->throwable instanceof \Error) {
$error = $this->getErrorType(); $errorType = $this->getErrorType();
if ($error !== null) { if ($errorType !== null) {
$f['code'] = $this->throwable->getCode(); $f['code'] = $this->throwable->getCode();
$f['type'] = $error; $f['errorType'] = $errorType;
} }
} }
@ -187,6 +190,13 @@ class ThrowableController {
$frames = $temp; $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; $this->frames = $frames;
return $frames; return $frames;
} }

29
tests/cases/TestCatcher.php

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

48
tests/cases/TestHTMLHandler.php

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

122
tests/cases/TestHandler.php

@ -11,55 +11,74 @@ use MensBeam\Foundation\Catcher;
use MensBeam\Foundation\Catcher\{ use MensBeam\Foundation\Catcher\{
Error, Error,
HTMLHandler, HTMLHandler,
PlainTextHandler,
JSONHandler, JSONHandler,
PlainTextHandler ThrowableController
}; };
use Eloquent\Phony\Phpunit\Phony; use Eloquent\Phony\Phpunit\Phony;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestHandler extends \PHPUnit\Framework\TestCase { class TestHandler extends \PHPUnit\Framework\TestCase {
/** /**
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/ */
public function testMethod___construct__exception(): void { public function testMethod___construct__exception(): void {
$this->expectException(\InvalidArgumentException::class); $this->expectException(\RangeException::class);
new PlainTextHandler([ 'httpCode' => 42 ]); 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::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch * @covers \MensBeam\Foundation\Catcher\Handler::dispatch
* @covers \MensBeam\Foundation\Catcher\Handler::handle * @covers \MensBeam\Foundation\Catcher\Handler::handle
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode * @covers \MensBeam\Foundation\Catcher\Handler::handleCallback
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct * @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback * @covers \MensBeam\Foundation\Catcher\JSONHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct * @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious * @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable * @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/ */
public function testMethod__getControlCode(): void { public function testMethod__cleanOutputThrowable(): void {
// Just need to test forceExit for coverage purposes // Just need to test coverage here; TestJSONHandler covers this one thoroughly.
$c = new Catcher(new PlainTextHandler([ 'forceExit' => true, 'silent' => true ])); $c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error('Eek!')));
$c->preventExit = true; $h = new JSONHandler([
$c->throwErrors = false; 'outputBacktrace' => true,
trigger_error('Ook!', \E_USER_ERROR); 'outputToStderr' => false
$this->assertSame(\E_USER_ERROR, $c->getLastThrowable()->getCode()); ]);
$c->unregister(); $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\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch * @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::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct * @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious * @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
@ -97,6 +111,11 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$c->unregister(); $c->unregister();
} }
/**
* @covers \MensBeam\Foundation\Catcher\Handler::setOption
*
* @covers \MensBeam\Foundation\Catcher\Handler::__construct
*/
public function testMethod__setOption(): void { public function testMethod__setOption(): void {
$h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]); $h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]);
$h->setOption('forceExit', false); $h->setOption('forceExit', false);
@ -104,7 +123,6 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$r->setAccessible(true); $r->setAccessible(true);
$this->assertFalse($r->getValue($h)); $this->assertFalse($r->getValue($h));
//$h = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]);
$m = Phony::partialMock(Catcher::class, [ $m = Phony::partialMock(Catcher::class, [
$h $h
]); ]);
@ -118,39 +136,6 @@ class TestHandler extends \PHPUnit\Framework\TestCase {
$c->unregister(); $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 * @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\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch * @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::handle
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback * @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::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious * @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, use Eloquent\Phony\Phpunit\Phony,
Psr\Log\LoggerInterface; Psr\Log\LoggerInterface;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
/** /**
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback
* *
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode * @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle * @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::log
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeOutputThrowable
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct * @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames * @covers \MensBeam\Foundation\Catcher\ThrowableController::getFrames
@ -39,16 +36,22 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable * @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
*/ */
public function testMethod_handleCallback(): void { 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); $l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([ $h = new PlainTextHandler([
'logger' => $l->get(), 'logger' => $l->get(),
'outputBacktrace' => true 'outputBacktrace' => true,
'outputToStderr' => false
]); ]);
$o = $h->handle($c); $o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o->controlCode); $this->assertSame(Handler::CONTINUE, $o['controlCode']);
$this->assertSame(Handler::OUTPUT | Handler::NOW, $o->outputCode); $this->assertSame(Handler::OUTPUT | Handler::NOW, $o['outputCode']);
$this->assertStringContainsString('↳', $o->output); $this->assertTrue(isset($o['previous']));
ob_start();
$h->dispatch();
ob_end_clean();
$l->critical->called(); $l->critical->called();
} }
@ -58,12 +61,11 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
* *
* @covers \MensBeam\Foundation\Catcher\Error::__construct * @covers \MensBeam\Foundation\Catcher\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::getControlCode * @covers \MensBeam\Foundation\Catcher\Handler::buildOutputArray
* @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode
* @covers \MensBeam\Foundation\Catcher\Handler::handle * @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::handleCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::serializeThrowable * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct * @covers \MensBeam\Foundation\Catcher\ThrowableController::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious * @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
@ -71,7 +73,10 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
*/ */
public function testMethod_log(): void { public function testMethod_log(): void {
$l = Phony::mock(LoggerInterface::class); $l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([ 'logger' => $l->get() ]); $h = new PlainTextHandler([
'logger' => $l->get(),
'outputToStderr' => false
]);
$e = [ $e = [
'notice' => [ 'notice' => [
@ -99,14 +104,25 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
foreach ($e as $k => $v) { foreach ($e as $k => $v) {
foreach ($v as $vv) { foreach ($v as $vv) {
$h->handle(new ThrowableController(new Error('Ook!', $vv))); $h->handle(new ThrowableController(new Error('Ook!', $vv)));
ob_start();
$h->dispatch();
ob_end_clean();
$l->$k->called(); $l->$k->called();
} }
} }
$h->handle(new ThrowableController(new \PharException('Ook!'))); $h->handle(new ThrowableController(new \PharException('Ook!')));
ob_start();
$h->dispatch();
ob_end_clean();
$l->alert->called(); $l->alert->called();
$h->handle(new ThrowableController(new \RuntimeException('Ook!'))); $h->handle(new ThrowableController(new \RuntimeException('Ook!')));
ob_start();
$h->dispatch();
ob_end_clean();
$l->alert->called(); $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\Error::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::__construct * @covers \MensBeam\Foundation\Catcher\Handler::__construct
* @covers \MensBeam\Foundation\Catcher\Handler::dispatch * @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::handle
* @covers \MensBeam\Foundation\Catcher\Handler::print * @covers \MensBeam\Foundation\Catcher\Handler::print
* @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::dispatchCallback
* @covers \MensBeam\Foundation\Catcher\PlainTextHandler::handleCallback * @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::__construct
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious * @covers \MensBeam\Foundation\Catcher\ThrowableController::getPrevious
* @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable * @covers \MensBeam\Foundation\Catcher\ThrowableController::getThrowable
@ -63,6 +60,24 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$this->assertSame(\E_USER_WARNING, $c->getLastThrowable()->getCode()); $this->assertSame(\E_USER_WARNING, $c->getLastThrowable()->getCode());
$c->unregister(); $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 // These others will be tested by invoking the method directly
$c = new ThrowableController(new Error('Ook!', \E_ERROR)); $c = new ThrowableController(new Error('Ook!', \E_ERROR));
$this->assertSame('PHP Fatal Error', $c->getErrorType()); $this->assertSame('PHP Fatal Error', $c->getErrorType());
@ -90,6 +105,9 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$this->assertNull($c->getErrorType()); $this->assertNull($c->getErrorType());
$c = new ThrowableController(new \Exception('Ook!')); $c = new ThrowableController(new \Exception('Ook!'));
$this->assertNull($c->getErrorType()); $this->assertNull($c->getErrorType());
// For code coverage purposes.
$this->assertNull($c->getErrorType());
} }
/** /**
@ -122,7 +140,7 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase {
$f = false; $f = false;
try { 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) { } catch (\Throwable $t) {
$c = new ThrowableController($t); $c = new ThrowableController($t);
$f = $c->getFrames(); $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 // For code coverage purposes; should use the cached value instead of calculating
// the frames over again. // the frames over again.
$f = $c->getFrames(); $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/TestCatcher.php</file>
<file>cases/TestHandler.php</file> <file>cases/TestHandler.php</file>
<file>cases/TestHTMLHandler.php</file> <file>cases/TestHTMLHandler.php</file>
<file>cases/TestJSONHandler.php</file>
<file>cases/TestPlainTextHandler.php</file> <file>cases/TestPlainTextHandler.php</file>
<file>cases/TestThrowableController.php</file> <file>cases/TestThrowableController.php</file>
</testsuite> </testsuite>

Loading…
Cancel
Save