From a5b89f86aeac9d45356ae2355e2ecadbf922a6b8 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 2 Oct 2026 16:11:20 +0200 Subject: [PATCH] Upstream filp/whoops state of the touched files before 5805bc7620 --- src/Whoops/Run.php | 607 ++++++++++++++++++++++++ src/Whoops/Util/SystemFacade.php | 144 ++++++ tests/Whoops/RunTest.php | 609 +++++++++++++++++++++++++ tests/Whoops/Util/SystemFacadeTest.php | 221 +++++++++ 4 files changed, 1581 insertions(+) create mode 100644 src/Whoops/Run.php create mode 100644 src/Whoops/Util/SystemFacade.php create mode 100644 tests/Whoops/RunTest.php create mode 100644 tests/Whoops/Util/SystemFacadeTest.php diff --git a/src/Whoops/Run.php b/src/Whoops/Run.php new file mode 100644 index 0000000..b6eee28 --- /dev/null +++ b/src/Whoops/Run.php @@ -0,0 +1,607 @@ + + */ + +namespace Whoops; + +use InvalidArgumentException; +use Throwable; +use Whoops\Exception\ErrorException; +use Whoops\Handler\CallbackHandler; +use Whoops\Handler\Handler; +use Whoops\Handler\HandlerInterface; +use Whoops\Inspector\CallableInspectorFactory; +use Whoops\Inspector\InspectorFactory; +use Whoops\Inspector\InspectorFactoryInterface; +use Whoops\Inspector\InspectorInterface; +use Whoops\Util\Misc; +use Whoops\Util\SystemFacade; + +final class Run implements RunInterface +{ + /** + * @var bool + */ + private $isRegistered; + + /** + * @var bool + */ + private $allowQuit = true; + + /** + * @var bool + */ + private $sendOutput = true; + + /** + * @var integer|false + */ + private $sendHttpCode = 500; + + /** + * @var integer|false + */ + private $sendExitCode = 1; + + /** + * @var HandlerInterface[] + */ + private $handlerStack = []; + + /** + * @var array + * @psalm-var list + */ + private $silencedPatterns = []; + + /** + * @var SystemFacade + */ + private $system; + + /** + * In certain scenarios, like in shutdown handler, we can not throw exceptions. + * + * @var bool + */ + private $canThrowExceptions = true; + + /** + * The inspector factory to create inspectors. + * + * @var InspectorFactoryInterface + */ + private $inspectorFactory; + + /** + * @var array + */ + private $frameFilters = []; + + public function __construct(?SystemFacade $system = null) + { + $this->system = $system ?: new SystemFacade; + $this->inspectorFactory = new InspectorFactory(); + } + + public function __destruct() + { + $this->unregister(); + } + + /** + * Explicitly request your handler runs as the last of all currently registered handlers. + * + * @param callable|HandlerInterface $handler + * + * @return Run + */ + public function appendHandler($handler) + { + array_unshift($this->handlerStack, $this->resolveHandler($handler)); + return $this; + } + + /** + * Explicitly request your handler runs as the first of all currently registered handlers. + * + * @param callable|HandlerInterface $handler + * + * @return Run + */ + public function prependHandler($handler) + { + return $this->pushHandler($handler); + } + + /** + * Register your handler as the last of all currently registered handlers (to be executed first). + * Prefer using appendHandler and prependHandler for clarity. + * + * @param callable|HandlerInterface $handler + * + * @return Run + * + * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface. + */ + public function pushHandler($handler) + { + $this->handlerStack[] = $this->resolveHandler($handler); + return $this; + } + + /** + * Removes and returns the last handler pushed to the handler stack. + * + * @see Run::removeFirstHandler(), Run::removeLastHandler() + * + * @return HandlerInterface|null + */ + public function popHandler() + { + return array_pop($this->handlerStack); + } + + /** + * Removes the first handler. + * + * @return void + */ + public function removeFirstHandler() + { + array_pop($this->handlerStack); + } + + /** + * Removes the last handler. + * + * @return void + */ + public function removeLastHandler() + { + array_shift($this->handlerStack); + } + + /** + * Returns an array with all handlers, in the order they were added to the stack. + * + * @return array + */ + public function getHandlers() + { + return $this->handlerStack; + } + + /** + * Clears all handlers in the handlerStack, including the default PrettyPage handler. + * + * @return Run + */ + public function clearHandlers() + { + $this->handlerStack = []; + return $this; + } + + public function getFrameFilters() + { + return $this->frameFilters; + } + + public function clearFrameFilters() + { + $this->frameFilters = []; + return $this; + } + + /** + * Registers this instance as an error handler. + * + * @return Run + */ + public function register() + { + if (!$this->isRegistered) { + // Workaround PHP bug 42098 + // https://bugs.php.net/bug.php?id=42098 + class_exists("\\Whoops\\Exception\\ErrorException"); + class_exists("\\Whoops\\Exception\\FrameCollection"); + class_exists("\\Whoops\\Exception\\Frame"); + class_exists("\\Whoops\\Exception\\Inspector"); + class_exists("\\Whoops\\Inspector\\InspectorFactory"); + + $this->system->setErrorHandler([$this, self::ERROR_HANDLER]); + $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]); + $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]); + + $this->isRegistered = true; + } + + return $this; + } + + /** + * Unregisters all handlers registered by this Whoops\Run instance. + * + * @return Run + */ + public function unregister() + { + if ($this->isRegistered) { + $this->system->restoreExceptionHandler(); + $this->system->restoreErrorHandler(); + + $this->isRegistered = false; + } + + return $this; + } + + /** + * Should Whoops allow Handlers to force the script to quit? + * + * @param bool|int $exit + * + * @return bool + */ + public function allowQuit($exit = null) + { + if (func_num_args() == 0) { + return $this->allowQuit; + } + + return $this->allowQuit = (bool) $exit; + } + + /** + * Silence particular errors in particular files. + * + * @param array|string $patterns List or a single regex pattern to match. + * @param int $levels Defaults to E_STRICT | E_DEPRECATED. + * + * @return Run + */ + public function silenceErrorsInPaths($patterns, $levels = 10240) + { + $this->silencedPatterns = array_merge( + $this->silencedPatterns, + array_map( + function ($pattern) use ($levels) { + return [ + "pattern" => $pattern, + "levels" => $levels, + ]; + }, + (array) $patterns + ) + ); + + return $this; + } + + /** + * Returns an array with silent errors in path configuration. + * + * @return array + */ + public function getSilenceErrorsInPaths() + { + return $this->silencedPatterns; + } + + /** + * Should Whoops send HTTP error code to the browser if possible? + * Whoops will by default send HTTP code 500, but you may wish to + * use 502, 503, or another 5xx family code. + * + * @param bool|int $code + * + * @return int|false + * + * @throws InvalidArgumentException + */ + public function sendHttpCode($code = null) + { + if (func_num_args() == 0) { + return $this->sendHttpCode; + } + + if (!$code) { + return $this->sendHttpCode = false; + } + + if ($code === true) { + $code = 500; + } + + if ($code < 400 || 600 <= $code) { + throw new InvalidArgumentException( + "Invalid status code '$code', must be 4xx or 5xx" + ); + } + + return $this->sendHttpCode = $code; + } + + /** + * Should Whoops exit with a specific code on the CLI if possible? + * Whoops will exit with 1 by default, but you can specify something else. + * + * @param int $code + * + * @return int + * + * @throws InvalidArgumentException + */ + public function sendExitCode($code = null) + { + if (func_num_args() == 0) { + return $this->sendExitCode; + } + + if ($code < 0 || 255 <= $code) { + throw new InvalidArgumentException( + "Invalid status code '$code', must be between 0 and 254" + ); + } + + return $this->sendExitCode = (int) $code; + } + + /** + * Should Whoops push output directly to the client? + * If this is false, output will be returned by handleException. + * + * @param bool|int $send + * + * @return bool + */ + public function writeToOutput($send = null) + { + if (func_num_args() == 0) { + return $this->sendOutput; + } + + return $this->sendOutput = (bool) $send; + } + + /** + * Handles an exception, ultimately generating a Whoops error page. + * + * @param Throwable $exception + * + * @return string Output generated by handlers. + */ + public function handleException($exception) + { + // Walk the registered handlers in the reverse order + // they were registered, and pass off the exception + $inspector = $this->getInspector($exception); + + // Capture output produced while handling the exception, + // we might want to send it straight away to the client, + // or return it silently. + $this->system->startOutputBuffering(); + + // Just in case there are no handlers: + $handlerResponse = null; + $handlerContentType = null; + + try { + foreach (array_reverse($this->handlerStack) as $handler) { + $handler->setRun($this); + $handler->setInspector($inspector); + $handler->setException($exception); + + // The HandlerInterface does not require an Exception passed to handle() + // and neither of our bundled handlers use it. + // However, 3rd party handlers may have already relied on this parameter, + // and removing it would be possibly breaking for users. + $handlerResponse = $handler->handle($exception); + + // Collect the content type for possible sending in the headers. + $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null; + + if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) { + // The Handler has handled the exception in some way, and + // wishes to quit execution (Handler::QUIT), or skip any + // other handlers (Handler::LAST_HANDLER). If $this->allowQuit + // is false, Handler::QUIT behaves like Handler::LAST_HANDLER + break; + } + } + + $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit(); + } finally { + $output = $this->system->cleanOutputBuffer(); + } + + // If we're allowed to, send output generated by handlers directly + // to the output, otherwise, and if the script doesn't quit, return + // it so that it may be used by the caller + if ($this->writeToOutput()) { + // @todo Might be able to clean this up a bit better + if ($willQuit) { + // Cleanup all other output buffers before sending our output: + while ($this->system->getOutputBufferLevel() > 0) { + $this->system->endOutputBuffering(); + } + + // Send any headers if needed: + if (Misc::canSendHeaders() && $handlerContentType) { + header("Content-Type: {$handlerContentType}"); + } + } + + $this->writeToOutputNow($output); + } + + if ($willQuit) { + // HHVM fix for https://github.com/facebook/hhvm/issues/4055 + $this->system->flushOutputBuffer(); + + $this->system->stopExecution( + $this->sendExitCode() + ); + } + + return $output; + } + + /** + * Converts generic PHP errors to \ErrorException instances, before passing them off to be handled. + * + * This method MUST be compatible with set_error_handler. + * + * @param int $level + * @param string $message + * @param string|null $file + * @param int|null $line + * + * @return bool + * + * @throws ErrorException + */ + public function handleError($level, $message, $file = null, $line = null) + { + if ($level & $this->system->getErrorReportingLevel()) { + foreach ($this->silencedPatterns as $entry) { + $pathMatches = (bool) preg_match($entry["pattern"], $file); + $levelMatches = $level & $entry["levels"]; + if ($pathMatches && $levelMatches) { + // Ignore the error, abort handling + // See https://github.com/filp/whoops/issues/418 + return true; + } + } + + // XXX we pass $level for the "code" param only for BC reasons. + // see https://github.com/filp/whoops/issues/267 + $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line); + if ($this->canThrowExceptions) { + throw $exception; + } else { + $this->handleException($exception); + } + // Do not propagate errors which were already handled by Whoops. + return true; + } + + // Propagate error to the next handler, allows error_get_last() to + // work on silenced errors. + return false; + } + + /** + * Special case to deal with Fatal errors and the like. + * + * @return void + */ + public function handleShutdown() + { + // If we reached this step, we are in shutdown handler. + // An exception thrown in a shutdown handler will not be propagated + // to the exception handler. Pass that information along. + $this->canThrowExceptions = false; + + // If we are not currently registered, we should not do anything + if (!$this->isRegistered) { + return; + } + + $error = $this->system->getLastError(); + if ($error && Misc::isLevelFatal($error['type'])) { + // If there was a fatal error, + // it was not handled in handleError yet. + $this->allowQuit = false; + $this->handleError( + $error['type'], + $error['message'], + $error['file'], + $error['line'] + ); + } + } + + + /** + * @param InspectorFactoryInterface $factory + * + * @return void + */ + public function setInspectorFactory(InspectorFactoryInterface $factory) + { + $this->inspectorFactory = $factory; + } + + public function addFrameFilter($filterCallback) + { + if (!is_callable($filterCallback)) { + throw new \InvalidArgumentException(sprintf( + "A frame filter must be of type callable, %s type given.", + gettype($filterCallback) + )); + } + + $this->frameFilters[] = $filterCallback; + return $this; + } + + /** + * @param Throwable $exception + * + * @return InspectorInterface + */ + private function getInspector($exception) + { + return $this->inspectorFactory->create($exception); + } + + /** + * Resolves the giving handler. + * + * @param callable|HandlerInterface $handler + * + * @return HandlerInterface + * + * @throws InvalidArgumentException + */ + private function resolveHandler($handler) + { + if (is_callable($handler)) { + $handler = new CallbackHandler($handler); + } + + if (!$handler instanceof HandlerInterface) { + throw new InvalidArgumentException( + "Handler must be a callable, or instance of " + . "Whoops\\Handler\\HandlerInterface" + ); + } + + return $handler; + } + + /** + * Echo something to the browser. + * + * @param string $output + * + * @return Run + */ + private function writeToOutputNow($output) + { + if ($this->sendHttpCode() && Misc::canSendHeaders()) { + $this->system->setHttpResponseCode( + $this->sendHttpCode() + ); + } + + echo $output; + + return $this; + } +} diff --git a/src/Whoops/Util/SystemFacade.php b/src/Whoops/Util/SystemFacade.php new file mode 100644 index 0000000..9eb0acf --- /dev/null +++ b/src/Whoops/Util/SystemFacade.php @@ -0,0 +1,144 @@ + + */ + +namespace Whoops\Util; + +class SystemFacade +{ + /** + * Turns on output buffering. + * + * @return bool + */ + public function startOutputBuffering() + { + return ob_start(); + } + + /** + * @param callable $handler + * @param int $types + * + * @return callable|null + */ + public function setErrorHandler(callable $handler, $types = 'use-php-defaults') + { + // Since PHP 5.4 the constant E_ALL contains all errors (even E_STRICT) + if ($types === 'use-php-defaults') { + $types = E_ALL; + } + return set_error_handler($handler, $types); + } + + /** + * @param callable $handler + * + * @return callable|null + */ + public function setExceptionHandler(callable $handler) + { + return set_exception_handler($handler); + } + + /** + * @return void + */ + public function restoreExceptionHandler() + { + restore_exception_handler(); + } + + /** + * @return void + */ + public function restoreErrorHandler() + { + restore_error_handler(); + } + + /** + * @param callable $function + * + * @return void + */ + public function registerShutdownFunction(callable $function) + { + register_shutdown_function($function); + } + + /** + * @return string|false + */ + public function cleanOutputBuffer() + { + return ob_get_clean(); + } + + /** + * @return int + */ + public function getOutputBufferLevel() + { + return ob_get_level(); + } + + /** + * @return bool + */ + public function endOutputBuffering() + { + return ob_end_clean(); + } + + /** + * @return void + */ + public function flushOutputBuffer() + { + flush(); + } + + /** + * @return int + */ + public function getErrorReportingLevel() + { + return error_reporting(); + } + + /** + * @return array|null + */ + public function getLastError() + { + return error_get_last(); + } + + /** + * @param int $httpCode + * + * @return int + */ + public function setHttpResponseCode($httpCode) + { + if (!headers_sent()) { + // Ensure that no 'location' header is present as otherwise this + // will override the HTTP code being set here, and mask the + // expected error page. + header_remove('location'); + } + + return http_response_code($httpCode); + } + + /** + * @param int $exitStatus + */ + public function stopExecution($exitStatus) + { + exit($exitStatus); + } +} diff --git a/tests/Whoops/RunTest.php b/tests/Whoops/RunTest.php new file mode 100644 index 0000000..708fa21 --- /dev/null +++ b/tests/Whoops/RunTest.php @@ -0,0 +1,609 @@ + + */ + +namespace Whoops; + +use ArrayObject; +use Exception; +use InvalidArgumentException; +use Mockery as m; +use RuntimeException; +use Whoops\Handler\Handler; +use Whoops\Handler\HandlerInterface; +use Whoops\Handler\PrettyPageHandler; +use Whoops\Exception\Frame; +use Whoops\Util\SystemFacade; + +class RunTest extends TestCase +{ + public function testImplementsRunInterface() + { + $this->assertNotFalse(class_implements('Whoops\\Run', 'Whoops\\RunInterface')); + } + + public function testConstantsAreAccessibleFromTheClass() + { + $this->assertEquals(RunInterface::ERROR_HANDLER, Run::ERROR_HANDLER); + $this->assertEquals(RunInterface::EXCEPTION_HANDLER, Run::EXCEPTION_HANDLER); + $this->assertEquals(RunInterface::SHUTDOWN_HANDLER, Run::SHUTDOWN_HANDLER); + } + + /** + * @param string $message + * @return Exception + */ + protected function getException($message = "") + { + // HHVM does not support mocking exceptions + // Since we do not use any additional features of Mockery for exceptions, + // we can just use native Exceptions instead. + return new \Exception($message); + } + + /** + * @return Handler + */ + protected function getHandler() + { + return m::mock('Whoops\\Handler\\Handler') + ->shouldReceive('setRun') + ->andReturn(null) + ->mock() + + ->shouldReceive('setInspector') + ->andReturn(null) + ->mock() + + ->shouldReceive('setException') + ->andReturn(null) + ->mock(); + } + + /** + * @covers Whoops\Run::clearHandlers + */ + public function testClearHandlers() + { + $run = $this->getRunInstance(); + $run->clearHandlers(); + + $handlers = $run->getHandlers(); + + $this->assertEmpty($handlers); + } + + /** + * @covers Whoops\Run::pushHandler + */ + public function testPushHandler() + { + $run = $this->getRunInstance(); + $run->clearHandlers(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + + $run->pushHandler($handlerOne); + $run->pushHandler($handlerTwo); + + $handlers = $run->getHandlers(); + + $this->assertCount(2, $handlers); + $this->assertContains($handlerOne, $handlers); + $this->assertContains($handlerTwo, $handlers); + } + + /** + * @covers Whoops\Run::pushHandler + */ + public function testPushInvalidHandler() + { + $run = $this->getRunInstance(); + + $this->expectExceptionOfType('InvalidArgumentException'); + + $run->pushHandler('actually turnip'); + } + + /** + * @covers Whoops\Run::pushHandler + */ + public function testPushClosureBecomesHandler() + { + $run = $this->getRunInstance(); + $run->pushHandler(function () {}); + $this->assertInstanceOf('Whoops\\Handler\\CallbackHandler', $run->popHandler()); + } + + /** + * @covers Whoops\Run::popHandler + * @covers Whoops\Run::getHandlers + */ + public function testPopHandler() + { + $run = $this->getRunInstance(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + $handlerThree = $this->getHandler(); + + $run->pushHandler($handlerOne); + $run->pushHandler($handlerTwo); + $run->pushHandler($handlerThree); + + $this->assertSame($handlerThree, $run->popHandler()); + $this->assertSame($handlerTwo, $run->popHandler()); + $this->assertSame($handlerOne, $run->popHandler()); + + // Should return null if there's nothing else in + // the stack + $this->assertNull($run->popHandler()); + + // Should be empty since we popped everything off + // the stack: + $this->assertEmpty($run->getHandlers()); + } + + /** + * @covers Whoops\Run::removeFirstHandler + * @covers Whoops\Run::removeLastHandler + * @covers Whoops\Run::getHandlers + */ + public function testRemoveHandler() + { + $run = $this->getRunInstance(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + $handlerThree = $this->getHandler(); + + $run->pushHandler($handlerOne); + $run->pushHandler($handlerTwo); + $run->pushHandler($handlerThree); + + $run->removeLastHandler(); + $this->assertSame($handlerTwo, $run->getHandlers()[0]); + $run->removeFirstHandler(); + $this->assertSame($handlerTwo, $run->getHandlers()[0]); + $this->assertCount(1, $run->getHandlers()); + } + + /** + * @covers Whoops\Run::register + */ + public function testRegisterHandler() + { + // It is impossible to test the Run::register method using phpunit, + // as given how every test is always inside a giant try/catch block, + // any thrown exception will never hit a global exception handler. + // On the other hand, there is not much need in testing + // a call to a native PHP function. + $this->assertTrue(true); + } + + /** + * @covers Whoops\Run::unregister + */ + public function testUnregisterHandler() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + $run->unregister(); + + $this->expectExceptionOfType('Exception'); + + throw $this->getException("I'm not supposed to be caught!"); + } + + /** + * @covers Whoops\Run::pushHandler + * @covers Whoops\Run::getHandlers + */ + public function testHandlerHoldsOrder() + { + $run = $this->getRunInstance(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + $handlerThree = $this->getHandler(); + $handlerFour = $this->getHandler(); + + $run->pushHandler($handlerOne); + $run->prependHandler($handlerTwo); + $run->appendHandler($handlerThree); + $run->appendHandler($handlerFour); + + $handlers = $run->getHandlers(); + + $this->assertSame($handlers[0], $handlerFour); + $this->assertSame($handlers[1], $handlerThree); + $this->assertSame($handlers[2], $handlerOne); + $this->assertSame($handlers[3], $handlerTwo); + } + + /** + * @todo possibly split this up a bit and move + * some of this test to Handler unit tests? + * @covers Whoops\Run::handleException + */ + public function testHandlersGonnaHandle() + { + $run = $this->getRunInstance(); + $exception = $this->getException(); + $order = new ArrayObject(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + $handlerThree = $this->getHandler(); + + $handlerOne->shouldReceive('handle') + ->andReturnUsing(function () use ($order) { $order[] = 1; }); + $handlerTwo->shouldReceive('handle') + ->andReturnUsing(function () use ($order) { $order[] = 2; }); + $handlerThree->shouldReceive('handle') + ->andReturnUsing(function () use ($order) { $order[] = 3; }); + + $run->pushHandler($handlerOne); + $run->pushHandler($handlerTwo); + $run->pushHandler($handlerThree); + + // Get an exception to be handled, and verify that the handlers + // are given the handler, and in the inverse order they were + // registered. + $run->handleException($exception); + $this->assertEquals((array) $order, [3, 2, 1]); + } + + /** + * @covers Whoops\Run::handleException + */ + public function testLastHandler() + { + $run = $this->getRunInstance(); + + $handlerOne = $this->getHandler(); + $handlerTwo = $this->getHandler(); + $handlerThree = $this->getHandler(); + $handlerFour = $this->getHandler(); + + $run->pushHandler($handlerOne); + $run->prependHandler($handlerTwo); + $run->appendHandler($handlerThree); + $run->appendHandler($handlerFour); + + $test = $this; + $handlerFour + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test) { + $test->fail('$handlerFour should not be called'); + }); + + $handlerThree + ->shouldReceive('handle') + ->andReturn(Handler::LAST_HANDLER); + + $twoRan = false; + + $handlerOne + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test, &$twoRan) { + $test->assertTrue($twoRan); + }); + + $handlerTwo + ->shouldReceive('handle') + ->andReturnUsing(function () use (&$twoRan) { + $twoRan = true; + }); + + $run->handleException($this->getException()); + + // Reached the end without errors + $this->assertTrue(true); + } + + /** + * Test error suppression using @ operator. + */ + public function testErrorSuppression() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + $test = $this; + $handler + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test) { + $test->fail('$handler should not be called, error not suppressed'); + }); + + @trigger_error("Test error suppression"); + + // Reached the end without errors + $this->assertTrue(true); + } + + public function testErrorCatching() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + $test = $this; + $handler + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test) { + $test->fail('$handler should not be called error should be caught'); + }); + + try { + trigger_error('foo', E_USER_NOTICE); + $this->fail('Should not continue after error thrown'); + } catch (\ErrorException $e) { + // Do nothing + $this->assertTrue(true); + return; + } + $this->fail('Should not continue here, should have been caught.'); + } + + /** + * Test to make sure that error_reporting is respected. + */ + public function testErrorReporting() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + $test = $this; + $handler + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test) { + $test->fail('$handler should not be called, error_reporting not respected'); + }); + + $oldLevel = error_reporting(E_ALL ^ E_USER_NOTICE); + trigger_error("Test error reporting", E_USER_NOTICE); + error_reporting($oldLevel); + + // Reached the end without errors + $this->assertTrue(true); + } + + /** + * @covers Whoops\Run::silenceErrorsInPaths + */ + public function testSilenceErrorsInPaths() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + $test = $this; + $handler + ->shouldReceive('handle') + ->andReturnUsing(function () use ($test) { + $test->fail('$handler should not be called, silenceErrorsInPaths not respected'); + }); + + $run->silenceErrorsInPaths('@^'.preg_quote(__FILE__, '@').'$@', E_USER_NOTICE); + trigger_error('Test', E_USER_NOTICE); + $this->assertTrue(true); + } + + /** + * @covers Whoops\Run::handleError + * @requires PHP < 8 + */ + public function testGetSilencedError() + { + $run = $this->getRunInstance(); + $run->register(); + + $handler = $this->getHandler(); + $run->pushHandler($handler); + + @strpos(); + + $error = error_get_last(); + + $this->assertTrue($error && strpos($error['message'], 'strpos()') !== false); + } + + /** + * @covers Whoops\Run::handleError + * @see https://github.com/filp/whoops/issues/267 + */ + public function testErrorWrappedInException() + { + try { + $run = $this->getRunInstance(); + $run->handleError(E_WARNING, 'my message', 'my file', 99); + $this->fail("missing expected exception"); + } catch (\ErrorException $e) { + $this->assertSame(E_WARNING, $e->getSeverity()); + $this->assertSame(E_WARNING, $e->getCode(), "For BC reasons getCode() should match getSeverity()"); + $this->assertSame('my message', $e->getMessage()); + $this->assertSame('my file', $e->getFile()); + $this->assertSame(99, $e->getLine()); + } + } + + /** + * @covers Whoops\Run::handleException + * @covers Whoops\Run::writeToOutput + */ + public function testOutputIsSent() + { + $run = $this->getRunInstance(); + $run->pushHandler(function () { + echo "hello there"; + }); + + ob_start(); + $run->handleException(new RuntimeException()); + $this->assertEquals("hello there", ob_get_clean()); + } + + /** + * @covers Whoops\Run::handleException + * @covers Whoops\Run::writeToOutput + */ + public function testOutputIsNotSent() + { + $run = $this->getRunInstance(); + $run->writeToOutput(false); + $run->pushHandler(function () { + echo "hello there"; + }); + + ob_start(); + $this->assertEquals("hello there", $run->handleException(new RuntimeException())); + $this->assertEquals("", ob_get_clean()); + } + + /** + * @covers Whoops\Run::sendHttpCode + */ + public function testSendHttpCode() + { + $run = $this->getRunInstance(); + $run->sendHttpCode(true); + $this->assertEquals(500, $run->sendHttpCode()); + } + + /** + * @covers Whoops\Run::sendHttpCode + */ + public function testSendHttpCodeNullCode() + { + $run = $this->getRunInstance(); + $this->assertEquals(false, $run->sendHttpCode(null)); + } + + /** + * @covers Whoops\Run::sendHttpCode + */ + public function testSendHttpCodeWrongCode() + { + $this->expectExceptionOfType('InvalidArgumentException'); + + $this->getRunInstance()->sendHttpCode(1337); + } + + /** + * @covers Whoops\Run::sendHttpCode + */ + public function testSendExitCode() + { + $run = $this->getRunInstance(); + $run->sendExitCode(42); + $this->assertEquals(42, $run->sendExitCode()); + } + + /** + * @covers Whoops\Run::sendExitCode + */ + public function testSendExitCodeDefaultCode() + { + $run = $this->getRunInstance(); + $this->assertEquals(1, $run->sendExitCode()); + } + + /** + * @covers Whoops\Run::sendExitCode + */ + public function testSendExitCodeWrongCode() + { + $this->expectExceptionOfType('InvalidArgumentException'); + + $this->getRunInstance()->sendExitCode(255); + } + + /** + * @covers Whoops\Run::addFrameFilter + * @covers Whoops\Run::getFrameFilters + */ + public function testAddFrameFilter() + { + $run = $this->getRunInstance(); + + $filterCallbackOne = function(Frame $frame) {}; + $filterCallbackTwo = function(Frame $frame) {}; + + $run + ->addFrameFilter($filterCallbackOne) + ->addFrameFilter($filterCallbackTwo); + + $frameFilters = $run->getFrameFilters(); + + $this->assertCount(2, $frameFilters); + $this->assertContains($filterCallbackOne, $frameFilters); + $this->assertContains($filterCallbackTwo, $frameFilters); + $this->assertInstanceOf("Whoops\\RunInterface", $run); + } + + /** + * @covers Whoops\Run::clearFrameFilters + * @covers Whoops\Run::getFrameFilters + */ + public function testClearFrameFilters() + { + $run = $this->getRunInstance(); + $run->addFrameFilter(function(Frame $frame) {}); + + $run = $run->clearFrameFilters(); + + $this->assertEmpty($run->getFrameFilters()); + $this->assertInstanceOf("Whoops\\RunInterface", $run); + } + + public function testShutdownHandlerDoesNotRunIfUnregistered() + { + $system = m::mock('Whoops\Util\SystemFacade'); + + $run = new Run($system); + $run->writeToOutput(false); + $run->allowQuit(false); + + $fatalError = [ + 'type' => E_ERROR, + 'message' => 'Simulated fatal error for shutdown', + 'file' => 'somefile.php', + 'line' => 10, + ]; + $system->shouldReceive('getLastError')->andReturn($fatalError)->byDefault(); + + $mockHandler = m::mock('Whoops\Handler\HandlerInterface'); + $mockHandler->shouldNotReceive('handle'); + $mockHandler->shouldNotReceive('setRun'); + $mockHandler->shouldNotReceive('setInspector'); + $mockHandler->shouldNotReceive('setException'); + $run->pushHandler($mockHandler); + + $run->unregister(); + + ob_start(); + $run->handleShutdown(); + $output = ob_get_clean(); + + $this->assertEquals('', $output, "Output buffer should be empty."); + } +} diff --git a/tests/Whoops/Util/SystemFacadeTest.php b/tests/Whoops/Util/SystemFacadeTest.php new file mode 100644 index 0000000..554bbbf --- /dev/null +++ b/tests/Whoops/Util/SystemFacadeTest.php @@ -0,0 +1,221 @@ + true]); + $this->facade = new SystemFacade(); + } + + /** + * @after + */ + public function finishUp() + { + self::$runtime = null; + \Mockery::close(); + } + + public function test_it_delegates_output_buffering_to_the_native_implementation() + { + self::$runtime->shouldReceive('ob_start')->once(); + + $this->facade->startOutputBuffering(); + } + + public function test_it_delegates_cleaning_output_buffering_to_the_native_implementation() + { + self::$runtime->shouldReceive('ob_get_clean')->once(); + + $this->facade->cleanOutputBuffer(); + } + + public function test_it_delegates_getting_the_current_buffer_level_to_the_native_implementation() + { + self::$runtime->shouldReceive('ob_get_level')->once(); + + $this->facade->getOutputBufferLevel(); + } + + public function test_it_delegates_ending_the_current_buffer_to_the_native_implementation() + { + self::$runtime->shouldReceive('ob_end_clean')->once(); + + $this->facade->endOutputBuffering(); + } + + public function test_it_delegates_flushing_the_current_buffer_to_the_native_implementation() + { + self::$runtime->shouldReceive('flush')->once(); + + $this->facade->flushOutputBuffer(); + } + + public function test_it_delegates_error_handling_to_the_native_implementation() + { + self::$runtime->shouldReceive('set_error_handler')->once(); + + $this->facade->setErrorHandler(function(){}); + } + + public function test_it_delegates_error_handling_with_level_to_the_native_implementation() + { + self::$runtime->shouldReceive('set_error_handler')->once(); + + $this->facade->setErrorHandler(function(){}, E_CORE_ERROR); + } + + public function test_it_delegates_exception_handling_to_the_native_implementation() + { + self::$runtime->shouldReceive('set_exception_handler')->once(); + + $this->facade->setExceptionHandler(function(){}); + } + + public function test_it_delegates_restoring_the_exception_handler_to_the_native_implementation() + { + self::$runtime->shouldReceive('restore_exception_handler')->once(); + + $this->facade->restoreExceptionHandler(); + } + + public function test_it_delegates_restoring_the_error_handler_to_the_native_implementation() + { + self::$runtime->shouldReceive('restore_error_handler')->once(); + + $this->facade->restoreErrorHandler(); + } + + public function test_it_delegates_registering_a_shutdown_function_to_the_native_implementation() + { + self::$runtime->shouldReceive('register_shutdown_function')->once(); + + $this->facade->registerShutdownFunction(function(){}); + } + + public function test_it_delegates_error_reporting_to_the_native_implementation() + { + self::$runtime->shouldReceive('error_reporting')->once()->withNoArgs(); + + $this->facade->getErrorReportingLevel(); + } + + public function test_it_delegates_getting_the_last_error_to_the_native_implementation() + { + self::$runtime->shouldReceive('error_get_last')->once()->withNoArgs(); + + $this->facade->getLastError(); + } + + public function test_it_delegates_sending_an_http_response_code_to_the_native_implementation() + { + self::$runtime->shouldReceive('headers_sent')->once()->withNoArgs(); + self::$runtime->shouldReceive('header_remove')->once()->with('location'); + self::$runtime->shouldReceive('http_response_code')->once()->with(230); + + $this->facade->setHttpResponseCode(230); + } +} + +function ob_start() +{ + return SystemFacadeTest::delegate('ob_start'); +} + +function ob_get_clean() +{ + return SystemFacadeTest::delegate('ob_get_clean'); +} + +function ob_get_level() +{ + return SystemFacadeTest::delegate('ob_get_level'); +} + +function ob_end_clean() +{ + return SystemFacadeTest::delegate('ob_end_clean'); +} + +function flush() +{ + return SystemFacadeTest::delegate('flush'); +} + +function set_error_handler(callable $handler, $types = 'use-php-defaults') +{ + // Workaround for PHP 5.5 + if ($types === 'use-php-defaults') { + $types = E_ALL | E_STRICT; + } + return SystemFacadeTest::delegate('set_error_handler', func_get_args()); +} + +function set_exception_handler(callable $handler) +{ + return SystemFacadeTest::delegate('set_exception_handler', func_get_args()); +} + +function restore_exception_handler() +{ + return SystemFacadeTest::delegate('restore_exception_handler'); +} + +function restore_error_handler() +{ + return SystemFacadeTest::delegate('restore_error_handler'); +} + +function register_shutdown_function() +{ + return SystemFacadeTest::delegate('register_shutdown_function', func_get_args()); +} + +function error_reporting($level = null) +{ + return SystemFacadeTest::delegate('error_reporting', func_get_args()); +} + +function error_get_last() +{ + return SystemFacadeTest::delegate('error_get_last', func_get_args()); +} + +function header_remove($header = null) +{ + return SystemFacadeTest::delegate('header_remove', func_get_args()); +} + +function headers_sent(&$filename = null, &$line = null) +{ + return SystemFacadeTest::delegate('headers_sent', func_get_args()); +} + +function http_response_code($code = null) +{ + return SystemFacadeTest::delegate('http_response_code', func_get_args()); +}