diff --git a/composer.json b/composer.json index 679490c..cc8baae 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "ext-dom": "For HTMLHandler" }, "require-dev": { + "ext-dom": "*", "mensbeam/html-dom": "^1.0", "phpunit/phpunit": "^9.5", "nikic/php-parser": "^4.15", diff --git a/composer.lock b/composer.lock index 576c6d8..4bd0b56 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "276b1ae35ddd23e0ebc736f079ecf98f", + "content-hash": "a72b4696660d27f574ba5142eb76d7dd", "packages": [ { "name": "psr/log", @@ -2368,6 +2368,80 @@ ], "time": "2022-05-10T07:21:04+00:00" }, + { + "name": "symfony/var-exporter", + "version": "v6.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "8a3f442d48567a5447e984ce9e86875ed768304a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/8a3f442d48567a5447e984ce9e86875ed768304a", + "reference": "8a3f442d48567a5447e984ce9e86875ed768304a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-12-03T22:32:58+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.1", diff --git a/lib/Catcher.php b/lib/Catcher.php index d8f430e..11c1b8f 100644 --- a/lib/Catcher.php +++ b/lib/Catcher.php @@ -17,7 +17,9 @@ use MensBeam\Foundation\Catcher\{ class Catcher { /** When set to true Catcher won't exit when instructed */ - public static $preventExit = false; + public bool $preventExit = false; + /** When set to true Catcher will throw errors as throwables */ + public bool $throwErrors = true; /** * Array of handlers the exceptions are passed to @@ -154,12 +156,18 @@ class Catcher { */ public function handleError(int $code, string $message, ?string $file = null, ?int $line = null): bool { if ($code !== 0 && error_reporting()) { - $this->handleThrowable(new Error($message, $code, $file, $line)); + $error = new Error($message, $code, $file, $line); + if ($this->throwErrors) { + throw $error; + } else { + $this->handleThrowable($error); + } + return true; } // If preventing exit we don't want a false here to halt processing - return (self::$preventExit); + return ($this->preventExit); } /** @@ -196,7 +204,7 @@ class Catcher { // Don't want to exit here when shutting down so any shutdown functions further // down the stack still run. - if (!self::$preventExit && !$this->isShuttingDown) { + if (!$this->preventExit && !$this->isShuttingDown) { $this->exit($throwable->getCode()); } } @@ -214,6 +222,7 @@ class Catcher { return; } + $this->throwErrors = false; $this->isShuttingDown = true; if ($error = $this->getLastError()) { if (in_array($error['type'], [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_CORE_WARNING, \E_COMPILE_ERROR, \E_COMPILE_WARNING ])) { diff --git a/lib/Catcher/HTMLHandler.php b/lib/Catcher/HTMLHandler.php index bfc4865..70aa60e 100644 --- a/lib/Catcher/HTMLHandler.php +++ b/lib/Catcher/HTMLHandler.php @@ -12,14 +12,21 @@ namespace MensBeam\Foundation\Catcher; class HTMLHandler extends Handler { public const CONTENT_TYPE = 'text/html'; + + /** The DOMDocument errors should be inserted into */ protected ?\DOMDocument $_document = null; + /** The XPath path to the element where the errors should be inserted */ + protected string $_errorPath = '/html/body'; /** 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'; + protected \DOMXPath $xpath; + protected \DOMElement $errorLocation; + public function __construct(array $options = []) { parent::__construct($options); @@ -34,48 +41,50 @@ class HTMLHandler extends Handler { HTML); } + + $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'); + } + $this->errorLocation = $location->item(0); } - protected function buildThrowable(ThrowableController $controller): \DOMElement { + protected function buildThrowable(ThrowableController $controller): \DOMDocumentFragment { $throwable = $controller->getThrowable(); - $p = $this->_document->createElement('p'); + $frag = $this->_document->createDocumentFragment(); + $b = $this->_document->createElement('b'); + $type = $controller->getErrorType(); $class = $throwable::class; - if ($throwable instanceof \Error) { - $type = $controller->getErrorType(); - if ($type !== null) { - $b = $this->_document->createElement('b'); - $b->appendChild($this->_document->createTextNode($type)); - $p->appendChild($b); - $p->appendChild($this->_document->createTextNode(' (')); - $code = $this->_document->createElement('code'); - $code->appendChild($this->_document->createTextNode($throwable::class)); - $p->appendChild($code); - $p->appendChild($this->_document->createTextNode(')')); - } else { - $code = $this->_document->createElement('code'); - $code->appendChild($this->_document->createTextNode($throwable::class)); - $p->appendChild($code); - } + $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); } + $frag->appendChild($b); - $p->appendChild($this->_document->createTextNode(': ')); + $frag->appendChild($this->_document->createTextNode(': ')); $i = $this->_document->createElement('i'); $i->appendChild($this->_document->createTextNode($throwable->getMessage())); - $p->appendChild($i); - $p->appendChild($this->_document->createTextNode(' in file ')); + $frag->appendChild($i); + $frag->appendChild($this->_document->createTextNode(' in file ')); $code = $this->_document->createElement('code'); $code->appendChild($this->_document->createTextNode($throwable->getFile())); - $p->appendChild($code); - $p->appendChild($this->_document->createTextNode(' on line ' . $throwable->getLine())); - return $p; + $frag->appendChild($code); + $frag->appendChild($this->_document->createTextNode(' on line ' . $throwable->getLine())); + return $frag; } protected function dispatchCallback(): void { - $body = $this->_document->getElementsByTagName('body')[0]; + $ul = $this->_document->createElement('ul'); + $this->errorLocation->appendChild($ul); + $allSilent = true; foreach ($this->outputBuffer as $o) { if ($o->outputCode & self::SILENT) { @@ -83,7 +92,9 @@ class HTMLHandler extends Handler { } $allSilent = false; - $body->appendChild($o->output); + $li = $this->_document->createElement('li'); + $li->appendChild($o->output); + $ul->appendChild($li); } if (!$allSilent) { @@ -97,81 +108,102 @@ class HTMLHandler extends Handler { if ($this->_outputTime && $this->_timeFormat !== '') { $p = $this->_document->createElement('p'); $time = $this->_document->createElement('time'); - $time->appendChild($this->_document->createTextNode((new \DateTime())->format($this->_timeFormat))); + $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')); + } + $time->appendChild($this->_document->createTextNode($now->format($this->_timeFormat))); $p->appendChild($time); $frag->appendChild($p); + + $ip = $this->_document->createElement('div'); + $frag->appendChild($ip); + } else { + $ip = $frag; } - $frag->appendChild($this->buildThrowable($controller)); + $p = $this->_document->createElement('p'); + $p->appendChild($this->buildThrowable($controller)); + $ip->appendChild($p); + if ($this->_outputPrevious) { - $prevController = $controller->getPrevious(); - while ($prevController) { - $p = $this->_document->createElement('p'); - $small = $this->_document->createElement('small'); - $small->appendChild($this->_document->createTextNode('Caused by ↴')); - $p->appendChild($small); - $frag->appendChild($p); - $frag->appendChild($this->buildThrowable($prevController)); - $prevController = $prevController->getPrevious(); + $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'); + $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)); + + $prev = $prev->getPrevious(); + } + + $li->appendChild($f); } } if ($this->_outputBacktrace) { $frames = $controller->getFrames(); - $p = $this->_document->createElement('p'); - $p->appendChild($this->_document->createTextNode('Stack trace:')); - $frag->appendChild($p); - if (count($frames) > 0) { + $p = $this->_document->createElement('p'); + $p->appendChild($this->_document->createTextNode('Stack trace:')); + $ip->appendChild($p); + $ol = $this->_document->createElement('ol'); - $frag->appendChild($ol); - $num = 1; + $ip->appendChild($ol); + + $num = 0; foreach ($frames as $frame) { $li = $this->_document->createElement('li'); - $args = (!empty($frame['args']) && $this->_backtraceArgFrameLimit >= $num); - $t = ($args) ? $this->_document->createElement('p') : $li; - - if (!empty($frame['error'])) { - $b = $this->_document->createElement('b'); - $b->appendChild($this->_document->createTextNode($frame['error'])); - $t->appendChild($b); - $t->appendChild($this->_document->createTextNode(' (')); - $code = $this->_document->createElement('code'); - $code->appendChild($this->_document->createTextNode($frame['class'])); - $t->appendChild($code); - $t->appendChild($this->_document->createTextNode(')')); - } elseif (!empty($frame['class'])) { - $code = $this->_document->createElement('code'); - $code->appendChild($this->_document->createTextNode($frame['class'])); - $t->appendChild($code); + $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); - $class = $frame['class'] ?? ''; - $function = $frame['function'] ?? ''; - if ($function) { - if ($class) { - $code->firstChild->textContent .= "::{$function}()"; - } else { - $code = $this->_document->createElement('code'); - $code->appendChild($this->_document->createTextNode("{$function}()")); - $t->appendChild($code); - } + $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(' ')); - $i = $this->_document->createElement('i'); - $i->appendChild($this->_document->createTextNode($frame['file'])); - $t->appendChild($i); + $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) { - $li->appendChild($t); $pre = $this->_document->createElement('pre'); - $pre->appendChild($this->_document->createTextNode(var_export($frame['args'], true))); + $pre->appendChild($this->_document->createTextNode(print_r($frame['args'], true))); $li->appendChild($pre); } - - $ol->appendChild($li); } } } diff --git a/lib/Catcher/Handler.php b/lib/Catcher/Handler.php index 1076069..2930c1c 100644 --- a/lib/Catcher/Handler.php +++ b/lib/Catcher/Handler.php @@ -99,12 +99,33 @@ abstract class Handler { $this->outputBuffer = []; } + public function getOption(string $name): mixed { + $class = get_class($this); + if (!property_exists($class, "_$name")) { + trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING); + return null; + } + + $name = "_$name"; + return $this->$name; + } + public function handle(ThrowableController $controller): HandlerOutput { $output = $this->handleCallback($controller); $this->outputBuffer[] = $output; return $output; } + public function setOption(string $name, mixed $value): void { + $class = get_class($this); + if (!property_exists($class, "_$name")) { + trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING); + } + + $name = "_$name"; + $this->$name = $value; + } + abstract protected function dispatchCallback(): void; diff --git a/lib/Catcher/PlainTextHandler.php b/lib/Catcher/PlainTextHandler.php index fe4a373..f7a84a7 100644 --- a/lib/Catcher/PlainTextHandler.php +++ b/lib/Catcher/PlainTextHandler.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace MensBeam\Foundation\Catcher; -use \Psr\Log\LoggerInterface; +use Psr\Log\LoggerInterface; class PlainTextHandler extends Handler { @@ -37,15 +37,17 @@ class PlainTextHandler extends Handler { $output = $this->serializeThrowable($controller); if ($this->_outputPrevious) { $prevController = $controller->getPrevious(); + $indent = ''; while ($prevController) { - $output .= sprintf("\n\nCaused by ↴\n%s", $this->serializeThrowable($prevController)); + $output .= sprintf("\n%s↳ %s", $indent, $this->serializeThrowable($prevController)); $prevController = $prevController->getPrevious(); + $indent .= ' '; } } if ($this->_outputBacktrace) { $frames = $controller->getFrames(); - $output .= "\nStack trace:"; + $output .= "\n\nStack trace:"; $num = 1; $maxDigits = strlen((string)count($frames)); @@ -75,6 +77,8 @@ class PlainTextHandler extends Handler { $args ); } + + $output = rtrim($output, "\n"); } // The logger will handle timestamps itself. diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 0c649f0..fa4047b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,6 +22,4 @@ if (function_exists('xdebug_set_filter')) { } else { xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [ "$cwd/lib/" ]); } -} - -Catcher::$preventExit = true; \ No newline at end of file +} \ No newline at end of file diff --git a/tests/cases/TestCatcher.php b/tests/cases/TestCatcher.php index 6a14e7b..53192d9 100644 --- a/tests/cases/TestCatcher.php +++ b/tests/cases/TestCatcher.php @@ -16,7 +16,10 @@ use MensBeam\Foundation\Catcher\{ }; use Eloquent\Phony\Phpunit\Phony; - +/** + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ class TestCatcher extends \PHPUnit\Framework\TestCase { /** * @covers \MensBeam\Foundation\Catcher::__construct @@ -30,6 +33,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod___construct(): void { $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); @@ -40,6 +45,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { new HTMLHandler(), new JSONHandler() ); + $c->preventExit = true; + $c->throwErrors = false; $this->assertSame('MensBeam\Foundation\Catcher', $c::class); $this->assertSame(3, count($c->getHandlers())); $h = $c->getHandlers(); @@ -74,6 +81,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_getLastThrowable(): void { $c = new Catcher(new PlainTextHandler([ 'silent' => true ])); + $c->preventExit = true; + $c->throwErrors = false; trigger_error('Ook!', \E_USER_WARNING); $this->assertSame(\E_USER_WARNING, $c->getLastThrowable()->getCode()); $c->unregister(); @@ -95,11 +104,15 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { $h = new PlainTextHandler(); $c = new Catcher($h, $h); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $this->assertSame(\E_USER_WARNING, $e); $e = null; $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $c->pushHandler($h, $h); $this->assertSame(\E_USER_WARNING, $e); @@ -136,6 +149,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { new JSONHandler() ]; $c = new Catcher(...$h); + $c->preventExit = true; + $c->throwErrors = false; $hh = $c->popHandler(); $this->assertSame($h[2], $hh); $hh = $c->popHandler(); @@ -163,6 +178,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_register(): void { $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $this->assertTrue($c->isRegistered()); $this->assertFalse($c->register()); $c->unregister(); @@ -181,6 +198,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_setHandlers(): void { $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->setHandlers(new PlainTextHandler()); $h = $c->getHandlers(); $this->assertSame(1, count($h)); @@ -205,6 +224,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { new JSONHandler() ]; $c = new Catcher(...$h); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $hh = $c->shiftHandler(); $this->assertSame($h[0], $hh); @@ -231,6 +252,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_unregister(): void { $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $this->assertFalse($c->unregister()); } @@ -248,6 +271,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_unshiftHandler(): void { $c = new Catcher(new PlainTextHandler()); + $c->preventExit = true; + $c->throwErrors = false; $c->unshiftHandler(new JSONHandler(), new HTMLHandler(), new PlainTextHandler()); $h = $c->getHandlers(); $this->assertSame(4, count($h)); @@ -272,6 +297,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { $c->unregister(); $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $e = null; @@ -310,6 +337,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_handleError(): void { $c = new Catcher(new PlainTextHandler([ 'silent' => true ])); + $c->preventExit = true; + $c->throwErrors = false; trigger_error('Ook!', \E_USER_NOTICE); $t = $c->getLastThrowable(); @@ -348,6 +377,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { $h3->get() ]); $c = $h->get(); + $c->preventExit = true; + $c->throwErrors = false; trigger_error('Ook!', \E_USER_ERROR); @@ -355,7 +386,16 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { $h2->dispatch->called(); $h3->dispatch->called(); + $c->throwErrors = true; + try { + trigger_error('Ook!', \E_USER_ERROR); + } catch (\Throwable $t) { + $this->assertInstanceOf(Error::class, $t); + $this->assertSame(\E_USER_ERROR, $t->getCode()); + } + $c->unregister(); + $c->throwErrors = false; } /** @@ -383,22 +423,25 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_handleThrowable(): void { $c = new Catcher(new PlainTextHandler([ 'silent' => true, 'forceBreak' => true ])); + $c->preventExit = true; + $c->throwErrors = false; trigger_error('Ook!', \E_USER_ERROR); $t = $c->getLastThrowable(); $this->assertSame(Error::class, $t::class); $this->assertSame(\E_USER_ERROR, $t->getCode()); $c->unregister(); - Catcher::$preventExit = false; $h = Phony::partialMock(Catcher::class, [ new PlainTextHandler([ 'silent' => true ]) ]); $h->exit->returns(); $c = $h->get(); + $c->preventExit = false; + $c->throwErrors = false; + trigger_error('Ook!', \E_USER_ERROR); $t = $c->getLastThrowable(); $this->assertSame(Error::class, $t::class); $this->assertSame(\E_USER_ERROR, $t->getCode()); $c->unregister(); - Catcher::$preventExit = true; } /** @@ -427,6 +470,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { */ public function testMethod_handleShutdown(): void { $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->handleShutdown(); $p = new \ReflectionProperty($c, 'isShuttingDown'); $p->setAccessible(true); @@ -434,6 +479,8 @@ class TestCatcher extends \PHPUnit\Framework\TestCase { $c->unregister(); $c = new Catcher(); + $c->preventExit = true; + $c->throwErrors = false; $c->unregister(); $c->handleShutdown(); $p = new \ReflectionProperty($c, 'isShuttingDown'); diff --git a/tests/cases/TestHTMLHandler.php b/tests/cases/TestHTMLHandler.php new file mode 100644 index 0000000..05c3b8a --- /dev/null +++ b/tests/cases/TestHTMLHandler.php @@ -0,0 +1,97 @@ +expectException(\InvalidArgumentException::class); + new HTMLHandler([ 'errorPath' => '/html/body/fail' ]); + } + + /** + * @covers \MensBeam\Foundation\Catcher\HTMLHandler::buildThrowable + * + * @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::handle + * @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct + * @covers \MensBeam\Foundation\Catcher\HTMLHandler::__construct + * @covers \MensBeam\Foundation\Catcher\HTMLHandler::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_buildThrowable(): 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([ + 'outputBacktrace' => true, + 'outputTime' => false + ]); + $o = $h->handle($c); + $this->assertSame(Handler::CONTINUE, $o->controlCode); + $this->assertInstanceOf(\DOMDocumentFragment::class, $o->output); + } + + /** + * @covers \MensBeam\Foundation\Catcher\HTMLHandler::dispatchCallback + * + * @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\Handler::setOption + * @covers \MensBeam\Foundation\Catcher\HandlerOutput::__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::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!')))); + $h = new HTMLHandler([ + 'outputToStderr' => false + ]); + $h->handle($c); + + ob_start(); + $h->dispatch(); + $o = ob_get_clean(); + $this->assertNotNull($o); + + $h->setOption('silent', true); + $h->handle($c); + $h->dispatch(); + } +} \ No newline at end of file diff --git a/tests/cases/TestHandler.php b/tests/cases/TestHandler.php index b88c9d0..9d3bfff 100644 --- a/tests/cases/TestHandler.php +++ b/tests/cases/TestHandler.php @@ -14,15 +14,19 @@ use MensBeam\Foundation\Catcher\{ JSONHandler, PlainTextHandler }; +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); - $c = new Catcher(new PlainTextHandler([ 'httpCode' => 42 ])); + new PlainTextHandler([ 'httpCode' => 42 ]); } /** @@ -51,11 +55,69 @@ class TestHandler extends \PHPUnit\Framework\TestCase { 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(); } + /** + * @covers \MensBeam\Foundation\Catcher\Handler::getOption + * + * @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::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 + */ + public function testMethod__getOption(): void { + $h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]); + $this->assertTrue($h->getOption('forceExit')); + + $c = new Catcher($h); + $c->preventExit = true; + $c->throwErrors = false; + $this->assertNull($h->getOption('ook')); + $c->unregister(); + } + + public function testMethod__setOption(): void { + $h = new PlainTextHandler([ 'forceExit' => true, 'silent' => true ]); + $h->setOption('forceExit', false); + $r = new \ReflectionProperty($h, '_forceExit'); + $r->setAccessible(true); + $this->assertFalse($r->getValue($h)); + + //$h = Phony::partialMock(PlainTextHandler::class, [ [ 'silent' => true ] ]); + $m = Phony::partialMock(Catcher::class, [ + $h + ]); + $c = $m->get(); + $c->preventExit = true; + $c->throwErrors = false; + + $h->setOption('ook', 'FAIL'); + $m->handleError->called(); + + $c->unregister(); + } + /** * @covers \MensBeam\Foundation\Catcher\Handler::getOutputCode * @@ -82,6 +144,8 @@ class TestHandler extends \PHPUnit\Framework\TestCase { 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(); @@ -114,6 +178,8 @@ class TestHandler extends \PHPUnit\Framework\TestCase { public function testMethod__print(): void { // Just need to test forceOutputNow for coverage purposes $c = new Catcher(new PlainTextHandler([ 'forceOutputNow' => true, 'outputToStderr' => false ])); + $c->preventExit = true; + $c->throwErrors = false; ob_start(); trigger_error('Ook!', \E_USER_NOTICE); ob_end_clean(); diff --git a/tests/cases/TestPlainTextHandler.php b/tests/cases/TestPlainTextHandler.php index 0eff2cd..98dd7c9 100644 --- a/tests/cases/TestPlainTextHandler.php +++ b/tests/cases/TestPlainTextHandler.php @@ -17,7 +17,10 @@ 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 @@ -45,13 +48,26 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase { $o = $h->handle($c); $this->assertSame(Handler::CONTINUE, $o->controlCode); $this->assertSame(Handler::OUTPUT | Handler::NOW, $o->outputCode); - $this->assertStringContainsString('Caused by ↴', $o->output); + $this->assertStringContainsString('↳', $o->output); $l->critical->called(); } /** * @covers \MensBeam\Foundation\Catcher\PlainTextHandler::log + * + * @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::handle + * @covers \MensBeam\Foundation\Catcher\HandlerOutput::__construct + * @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_log(): void { $l = Phony::mock(LoggerInterface::class); diff --git a/tests/cases/TestThrowableController.php b/tests/cases/TestThrowableController.php index d0f506e..9f7f126 100644 --- a/tests/cases/TestThrowableController.php +++ b/tests/cases/TestThrowableController.php @@ -14,7 +14,10 @@ use MensBeam\Foundation\Catcher\{ ThrowableController }; - +/** + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ class TestThrowableController extends \PHPUnit\Framework\TestCase { /** * @covers \MensBeam\Foundation\Catcher\ThrowableController::getErrorType @@ -43,6 +46,8 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase { */ public function testMethod_getErrorType(): void { $c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ])); + $c->preventExit = true; + $c->throwErrors = false; ob_start(); trigger_error('Ook!', \E_USER_DEPRECATED); ob_end_clean(); @@ -50,6 +55,8 @@ class TestThrowableController extends \PHPUnit\Framework\TestCase { $c->unregister(); $c = new Catcher(new PlainTextHandler([ 'outputToStderr' => false ])); + $c->preventExit = true; + $c->throwErrors = false; ob_start(); trigger_error('Ook!', \E_USER_WARNING); ob_end_clean(); diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 1fc334b..2d675df 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -19,6 +19,7 @@ cases/TestCatcher.php cases/TestHandler.php + cases/TestHTMLHandler.php cases/TestPlainTextHandler.php cases/TestThrowableController.php