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: }