From 17b8857480cdc88848150ed909fe71218c71934b Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Mon, 26 Dec 2022 22:10:56 -0600 Subject: [PATCH] 100% Coverage --- lib/Catcher.php | 4 +- lib/Catcher/HTMLHandler.php | 252 +++++++++++------------- lib/Catcher/Handler.php | 90 +++++++-- lib/Catcher/HandlerOutput.php | 23 --- lib/Catcher/JSONHandler.php | 102 +--------- lib/Catcher/PlainTextHandler.php | 152 +++++++------- lib/Catcher/ThrowableController.php | 20 +- tests/cases/TestCatcher.php | 29 +-- tests/cases/TestHTMLHandler.php | 48 ++--- tests/cases/TestHandler.php | 122 +++++------- tests/cases/TestJSONHandler.php | 36 ++++ tests/cases/TestPlainTextHandler.php | 52 +++-- tests/cases/TestThrowableController.php | 33 +++- tests/phpunit.dist.xml | 1 + 14 files changed, 459 insertions(+), 505 deletions(-) delete mode 100644 lib/Catcher/HandlerOutput.php create mode 100644 tests/cases/TestJSONHandler.php diff --git a/lib/Catcher.php b/lib/Catcher.php index 11c1b8f..ebecaad 100644 --- a/lib/Catcher.php +++ b/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; } diff --git a/lib/Catcher/HTMLHandler.php b/lib/Catcher/HTMLHandler.php index 70aa60e..aac314d 100644 --- a/lib/Catcher/HTMLHandler.php +++ b/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(); } } \ No newline at end of file diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php index 2930c1c..0ad9ba6 100644 --- a/lib/Catcher/Handler.php +++ b/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"; diff --git a/lib/Catcher/HandlerOutput.php b/lib/Catcher/HandlerOutput.php deleted file mode 100644 index 68399fe..0000000 --- a/lib/Catcher/HandlerOutput.php +++ /dev/null @@ -1,23 +0,0 @@ -controlCode = $controlCode; - $this->outputCode = $outputCode; - $this->output = $output; - } -} \ No newline at end of file diff --git a/lib/Catcher/JSONHandler.php b/lib/Catcher/JSONHandler.php index de56e8f..2fef98c 100644 --- a/lib/Catcher/JSONHandler.php +++ b/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 - ]; } } \ No newline at end of file diff --git a/lib/Catcher/PlainTextHandler.php b/lib/Catcher/PlainTextHandler.php index f7a84a7..02178c5 100644 --- a/lib/Catcher/PlainTextHandler.php +++ b/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; } } \ No newline at end of file diff --git a/lib/Catcher/ThrowableController.php b/lib/Catcher/ThrowableController.php index 688775c..fa985be 100644 --- a/lib/Catcher/ThrowableController.php +++ b/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; } diff --git a/tests/cases/TestCatcher.php b/tests/cases/TestCatcher.php index 53192d9..55644a5 100644 --- a/tests/cases/TestCatcher.php +++ b/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 diff --git a/tests/cases/TestHTMLHandler.php b/tests/cases/TestHTMLHandler.php index 05c3b8a..915b99b 100644 --- a/tests/cases/TestHTMLHandler.php +++ b/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); } } \ No newline at end of file diff --git a/tests/cases/TestHandler.php b/tests/cases/TestHandler.php index 9d3bfff..c7c38ac 100644 --- a/tests/cases/TestHandler.php +++ b/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 diff --git a/tests/cases/TestJSONHandler.php b/tests/cases/TestJSONHandler.php new file mode 100644 index 0000000..dd06bf8 --- /dev/null +++ b/tests/cases/TestJSONHandler.php @@ -0,0 +1,36 @@ + true, + 'outputToStderr' => false + ]); + $o = $h->handle($c); + + ob_start(); + $h->dispatch(); + $o = ob_get_clean(); + $this->assertEmpty($o); + } +} \ No newline at end of file diff --git a/tests/cases/TestPlainTextHandler.php b/tests/cases/TestPlainTextHandler.php index 98dd7c9..1e16626 100644 --- a/tests/cases/TestPlainTextHandler.php +++ b/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(); } } \ No newline at end of file diff --git a/tests/cases/TestThrowableController.php b/tests/cases/TestThrowableController.php index 9f7f126..7a6707f 100644 --- a/tests/cases/TestThrowableController.php +++ b/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); } } \ No newline at end of file diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 2d675df..672fd0c 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -20,6 +20,7 @@ cases/TestCatcher.php cases/TestHandler.php cases/TestHTMLHandler.php + cases/TestJSONHandler.php cases/TestPlainTextHandler.php cases/TestThrowableController.php