Chris@17: siteDirectory . '/settings.php'; Chris@17: chmod($settings_filename, 0777); Chris@17: $settings_php = file_get_contents($settings_filename); Chris@17: $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ErrorContainer.php';\n"; Chris@17: $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ExceptionContainer.php';\n"; Chris@17: file_put_contents($settings_filename, $settings_php); Chris@17: Chris@17: $settings = []; Chris@17: $settings['config']['system.logging']['error_level'] = (object) [ Chris@17: 'value' => ERROR_REPORTING_DISPLAY_VERBOSE, Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $this->writeSettings($settings); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests uncaught exception handling when system is in a bad state. Chris@17: */ Chris@17: public function testUncaughtException() { Chris@17: $this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.'; Chris@17: \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE); Chris@17: Chris@17: $this->config('system.logging') Chris@17: ->set('error_level', ERROR_REPORTING_HIDE) Chris@17: ->save(); Chris@17: $settings = []; Chris@17: $settings['config']['system.logging']['error_level'] = (object) [ Chris@17: 'value' => ERROR_REPORTING_HIDE, Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $this->writeSettings($settings); Chris@17: Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: $this->assertText('The website encountered an unexpected error. Please try again later.'); Chris@17: $this->assertNoText($this->expectedExceptionMessage); Chris@17: Chris@17: $this->config('system.logging') Chris@17: ->set('error_level', ERROR_REPORTING_DISPLAY_ALL) Chris@17: ->save(); Chris@17: $settings = []; Chris@17: $settings['config']['system.logging']['error_level'] = (object) [ Chris@17: 'value' => ERROR_REPORTING_DISPLAY_ALL, Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $this->writeSettings($settings); Chris@17: Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: $this->assertText('The website encountered an unexpected error. Please try again later.'); Chris@17: $this->assertText($this->expectedExceptionMessage); Chris@17: $this->assertErrorLogged($this->expectedExceptionMessage); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests displaying an uncaught fatal error. Chris@17: */ Chris@17: public function testUncaughtFatalError() { Chris@17: $fatal_error = [ Chris@17: '%type' => 'Recoverable fatal error', Chris@17: '@message' => 'Argument 1 passed to Drupal\error_test\Controller\ErrorTestController::Drupal\error_test\Controller\{closure}() must be of the type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 62 and defined', Chris@17: '%function' => 'Drupal\error_test\Controller\ErrorTestController->Drupal\error_test\Controller\{closure}()', Chris@17: ]; Chris@17: if (version_compare(PHP_VERSION, '7.0.0-dev') >= 0) { Chris@17: // In PHP 7, instead of a recoverable fatal error we get a TypeError. Chris@17: $fatal_error['%type'] = 'TypeError'; Chris@17: // The error message also changes in PHP 7. Chris@17: $fatal_error['@message'] = 'Argument 1 passed to Drupal\error_test\Controller\ErrorTestController::Drupal\error_test\Controller\{closure}() must be of the type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 62'; Chris@17: } Chris@17: $this->drupalGet('error-test/generate-fatals'); Chris@17: $this->assertResponse(500, 'Received expected HTTP status code.'); Chris@17: $message = new FormattableMarkup('%type: @message in %function (line ', $fatal_error); Chris@17: $this->assertRaw((string) $message); Chris@17: $this->assertRaw('
'); Chris@17: // Ensure we are escaping but not double escaping. Chris@17: $this->assertRaw('''); Chris@17: $this->assertNoRaw('''); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests uncaught exception handling with custom exception handler. Chris@17: */ Chris@17: public function testUncaughtExceptionCustomExceptionHandler() { Chris@17: $settings_filename = $this->siteDirectory . '/settings.php'; Chris@17: chmod($settings_filename, 0777); Chris@17: $settings_php = file_get_contents($settings_filename); Chris@17: $settings_php .= "\n"; Chris@17: $settings_php .= "set_exception_handler(function() {\n"; Chris@17: $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n"; Chris@17: $settings_php .= " print('Oh oh, flying teapots');\n"; Chris@17: $settings_php .= "});\n"; Chris@17: file_put_contents($settings_filename, $settings_php); Chris@17: Chris@17: \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE); Chris@17: Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(418); Chris@17: $this->assertNoText('The website encountered an unexpected error. Please try again later.'); Chris@17: $this->assertNoText('Oh oh, bananas in the instruments'); Chris@17: $this->assertText('Oh oh, flying teapots'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests a missing dependency on a service. Chris@17: */ Chris@17: public function testMissingDependency() { Chris@17: if (version_compare(PHP_VERSION, '7.1') < 0) { Chris@17: $this->expectedExceptionMessage = 'Argument 1 passed to Drupal\error_service_test\LonelyMonkeyClass::__construct() must be an instance of Drupal\Core\Database\Connection, non'; Chris@17: } Chris@17: else { Chris@17: $this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed'; Chris@17: } Chris@17: $this->drupalGet('broken-service-class'); Chris@17: $this->assertResponse(500); Chris@17: Chris@17: $this->assertRaw('The website encountered an unexpected error.'); Chris@17: $this->assertRaw($this->expectedExceptionMessage); Chris@17: $this->assertErrorLogged($this->expectedExceptionMessage); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests a missing dependency on a service with a custom error handler. Chris@17: */ Chris@17: public function testMissingDependencyCustomErrorHandler() { Chris@17: $settings_filename = $this->siteDirectory . '/settings.php'; Chris@17: chmod($settings_filename, 0777); Chris@17: $settings_php = file_get_contents($settings_filename); Chris@17: $settings_php .= "\n"; Chris@17: $settings_php .= "set_error_handler(function() {\n"; Chris@17: $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n"; Chris@17: $settings_php .= " print('Oh oh, flying teapots');\n"; Chris@17: $settings_php .= " exit();\n"; Chris@17: $settings_php .= "});\n"; Chris@17: $settings_php .= "\$settings['teapots'] = TRUE;\n"; Chris@17: file_put_contents($settings_filename, $settings_php); Chris@17: Chris@17: $this->drupalGet('broken-service-class'); Chris@17: $this->assertResponse(418); Chris@17: $this->assertSame('Oh oh, flying teapots', $this->response); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests a container which has an error. Chris@17: */ Chris@17: public function testErrorContainer() { Chris@17: $settings = []; Chris@17: $settings['settings']['container_base_class'] = (object) [ Chris@17: 'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer', Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $this->writeSettings($settings); Chris@17: \Drupal::service('kernel')->invalidateContainer(); Chris@17: Chris@17: $this->expectedExceptionMessage = 'Argument 1 passed to Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closur'; Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: Chris@17: $this->assertRaw($this->expectedExceptionMessage); Chris@17: $this->assertErrorLogged($this->expectedExceptionMessage); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests a container which has an exception really early. Chris@17: */ Chris@17: public function testExceptionContainer() { Chris@17: $settings = []; Chris@17: $settings['settings']['container_base_class'] = (object) [ Chris@17: 'value' => '\Drupal\FunctionalTests\Bootstrap\ExceptionContainer', Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $this->writeSettings($settings); Chris@17: \Drupal::service('kernel')->invalidateContainer(); Chris@17: Chris@17: $this->expectedExceptionMessage = 'Thrown exception during Container::get'; Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: Chris@17: $this->assertRaw('The website encountered an unexpected error'); Chris@17: $this->assertRaw($this->expectedExceptionMessage); Chris@17: $this->assertErrorLogged($this->expectedExceptionMessage); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests the case when the database connection is gone. Chris@17: */ Chris@17: public function testLostDatabaseConnection() { Chris@17: $incorrect_username = $this->randomMachineName(16); Chris@17: switch ($this->container->get('database')->driver()) { Chris@17: case 'pgsql': Chris@17: case 'mysql': Chris@17: $this->expectedExceptionMessage = $incorrect_username; Chris@17: break; Chris@17: default: Chris@17: // We can not carry out this test. Chris@17: $this->pass('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.'); Chris@17: return; Chris@17: } Chris@17: Chris@17: // We simulate a broken database connection by rewrite settings.php to no Chris@17: // longer have the proper data. Chris@17: $settings['databases']['default']['default']['username'] = (object) [ Chris@17: 'value' => $incorrect_username, Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: $settings['databases']['default']['default']['password'] = (object) [ Chris@17: 'value' => $this->randomMachineName(16), Chris@17: 'required' => TRUE, Chris@17: ]; Chris@17: Chris@17: $this->writeSettings($settings); Chris@17: Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: $this->assertRaw('DatabaseAccessDeniedException'); Chris@17: $this->assertErrorLogged($this->expectedExceptionMessage); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Tests fallback to PHP error log when an exception is thrown while logging. Chris@17: */ Chris@17: public function testLoggerException() { Chris@17: // Ensure the test error log is empty before these tests. Chris@17: $this->assertNoErrorsLogged(); Chris@17: Chris@17: $this->expectedExceptionMessage = 'Deforestation'; Chris@17: \Drupal::state()->set('error_service_test.break_logger', TRUE); Chris@17: Chris@17: $this->drupalGet(''); Chris@17: $this->assertResponse(500); Chris@17: $this->assertText('The website encountered an unexpected error. Please try again later.'); Chris@17: $this->assertRaw($this->expectedExceptionMessage); Chris@17: Chris@17: // Find fatal error logged to the error.log Chris@17: $errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); Chris@17: $this->assertIdentical(count($errors), 8, 'The error + the error that the logging service is broken has been written to the error log.'); Chris@17: $this->assertTrue(strpos($errors[0], 'Failed to log error') !== FALSE, 'The error handling logs when an error could not be logged to the logger.'); Chris@17: Chris@17: $expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php'; Chris@17: $expected_line = 59; Chris@17: $expected_entry = "Failed to log error: Exception: Deforestation in Drupal\\error_service_test\\MonkeysInTheControlRoom->handle() (line ${expected_line} of ${expected_path})"; Chris@17: $this->assert(strpos($errors[0], $expected_entry) !== FALSE, 'Original error logged to the PHP error log when an exception is thrown by a logger'); Chris@17: Chris@17: // The exception is expected. Do not interpret it as a test failure. Not Chris@17: // using File API; a potential error must trigger a PHP warning. Chris@17: unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Asserts that a specific error has been logged to the PHP error log. Chris@17: * Chris@17: * @param string $error_message Chris@17: * The expected error message. Chris@17: * Chris@17: * @see \Drupal\simpletest\TestBase::prepareEnvironment() Chris@17: * @see \Drupal\Core\DrupalKernel::bootConfiguration() Chris@17: */ Chris@17: protected function assertErrorLogged($error_message) { Chris@17: $error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'; Chris@17: if (!file_exists($error_log_filename)) { Chris@17: $this->fail('No error logged yet.'); Chris@17: } Chris@17: Chris@17: $content = file_get_contents($error_log_filename); Chris@17: $rows = explode(PHP_EOL, $content); Chris@17: Chris@17: // We iterate over the rows in order to be able to remove the logged error Chris@17: // afterwards. Chris@17: $found = FALSE; Chris@17: foreach ($rows as $row_index => $row) { Chris@17: if (strpos($content, $error_message) !== FALSE) { Chris@17: $found = TRUE; Chris@17: unset($rows[$row_index]); Chris@17: } Chris@17: } Chris@17: Chris@17: file_put_contents($error_log_filename, implode("\n", $rows)); Chris@17: Chris@17: $this->assertTrue($found, sprintf('The %s error message was logged.', $error_message)); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Asserts that no errors have been logged to the PHP error.log thus far. Chris@17: * Chris@17: * @see \Drupal\simpletest\TestBase::prepareEnvironment() Chris@17: * @see \Drupal\Core\DrupalKernel::bootConfiguration() Chris@17: */ Chris@17: protected function assertNoErrorsLogged() { Chris@17: // Since PHP only creates the error.log file when an actual error is Chris@17: // triggered, it is sufficient to check whether the file exists. Chris@17: $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Retrieves a Drupal path or an absolute path. Chris@17: * Chris@17: * Executes a cURL request for processing errors and exceptions. Chris@17: * Chris@17: * @param string|\Drupal\Core\Url $path Chris@17: * Request path. Chris@17: * @param array $extra_options Chris@17: * (optional) Curl options to pass to curl_setopt() Chris@17: * @param array $headers Chris@17: * (optional) Not used. Chris@17: */ Chris@17: protected function drupalGet($path, array $extra_options = [], array $headers = []) { Chris@17: $url = $this->buildUrl($path, ['absolute' => TRUE]); Chris@17: Chris@17: $ch = curl_init(); Chris@17: curl_setopt($ch, CURLOPT_URL, $url); Chris@17: curl_setopt($ch, CURLOPT_HEADER, FALSE); Chris@17: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); Chris@17: curl_setopt($ch, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix)); Chris@17: $this->response = curl_exec($ch); Chris@17: $this->info = curl_getinfo($ch); Chris@17: curl_close($ch); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function assertResponse($code) { Chris@17: $this->assertSame($code, $this->info['http_code']); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function assertText($text) { Chris@17: $this->assertContains($text, $this->response); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function assertNoText($text) { Chris@17: $this->assertNotContains($text, $this->response); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function assertRaw($text) { Chris@17: $this->assertText($text); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: protected function assertNoRaw($text) { Chris@17: $this->assertNoText($text); Chris@17: } Chris@17: Chris@17: }