annotate core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@17 1 <?php
Chris@17 2
Chris@17 3 namespace Drupal\FunctionalTests\Bootstrap;
Chris@17 4
Chris@17 5 use Drupal\Component\Render\FormattableMarkup;
Chris@17 6 use Drupal\Tests\BrowserTestBase;
Chris@17 7
Chris@17 8 /**
Chris@17 9 * Tests kernel panic when things are really messed up.
Chris@17 10 *
Chris@17 11 * @group system
Chris@17 12 */
Chris@17 13 class UncaughtExceptionTest extends BrowserTestBase {
Chris@17 14
Chris@17 15 /**
Chris@17 16 * Last cURL response.
Chris@17 17 *
Chris@17 18 * @var string
Chris@17 19 */
Chris@17 20 protected $response = '';
Chris@17 21
Chris@17 22 /**
Chris@17 23 * Last cURL info.
Chris@17 24 *
Chris@17 25 * @var array
Chris@17 26 */
Chris@17 27 protected $info = [];
Chris@17 28
Chris@17 29 /**
Chris@17 30 * Exceptions thrown by site under test that contain this text are ignored.
Chris@17 31 *
Chris@17 32 * @var string
Chris@17 33 */
Chris@17 34 protected $expectedExceptionMessage;
Chris@17 35
Chris@17 36 /**
Chris@17 37 * Modules to enable.
Chris@17 38 *
Chris@17 39 * @var array
Chris@17 40 */
Chris@17 41 public static $modules = ['error_service_test', 'error_test'];
Chris@17 42
Chris@17 43 /**
Chris@17 44 * {@inheritdoc}
Chris@17 45 */
Chris@17 46 protected function setUp() {
Chris@17 47 parent::setUp();
Chris@17 48
Chris@17 49 $settings_filename = $this->siteDirectory . '/settings.php';
Chris@17 50 chmod($settings_filename, 0777);
Chris@17 51 $settings_php = file_get_contents($settings_filename);
Chris@17 52 $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ErrorContainer.php';\n";
Chris@17 53 $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ExceptionContainer.php';\n";
Chris@17 54 file_put_contents($settings_filename, $settings_php);
Chris@17 55
Chris@17 56 $settings = [];
Chris@17 57 $settings['config']['system.logging']['error_level'] = (object) [
Chris@17 58 'value' => ERROR_REPORTING_DISPLAY_VERBOSE,
Chris@17 59 'required' => TRUE,
Chris@17 60 ];
Chris@17 61 $this->writeSettings($settings);
Chris@17 62 }
Chris@17 63
Chris@17 64 /**
Chris@17 65 * Tests uncaught exception handling when system is in a bad state.
Chris@17 66 */
Chris@17 67 public function testUncaughtException() {
Chris@17 68 $this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.';
Chris@17 69 \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
Chris@17 70
Chris@17 71 $this->config('system.logging')
Chris@17 72 ->set('error_level', ERROR_REPORTING_HIDE)
Chris@17 73 ->save();
Chris@17 74 $settings = [];
Chris@17 75 $settings['config']['system.logging']['error_level'] = (object) [
Chris@17 76 'value' => ERROR_REPORTING_HIDE,
Chris@17 77 'required' => TRUE,
Chris@17 78 ];
Chris@17 79 $this->writeSettings($settings);
Chris@17 80
Chris@17 81 $this->drupalGet('');
Chris@17 82 $this->assertResponse(500);
Chris@17 83 $this->assertText('The website encountered an unexpected error. Please try again later.');
Chris@17 84 $this->assertNoText($this->expectedExceptionMessage);
Chris@17 85
Chris@17 86 $this->config('system.logging')
Chris@17 87 ->set('error_level', ERROR_REPORTING_DISPLAY_ALL)
Chris@17 88 ->save();
Chris@17 89 $settings = [];
Chris@17 90 $settings['config']['system.logging']['error_level'] = (object) [
Chris@17 91 'value' => ERROR_REPORTING_DISPLAY_ALL,
Chris@17 92 'required' => TRUE,
Chris@17 93 ];
Chris@17 94 $this->writeSettings($settings);
Chris@17 95
Chris@17 96 $this->drupalGet('');
Chris@17 97 $this->assertResponse(500);
Chris@17 98 $this->assertText('The website encountered an unexpected error. Please try again later.');
Chris@17 99 $this->assertText($this->expectedExceptionMessage);
Chris@17 100 $this->assertErrorLogged($this->expectedExceptionMessage);
Chris@17 101 }
Chris@17 102
Chris@17 103 /**
Chris@17 104 * Tests displaying an uncaught fatal error.
Chris@17 105 */
Chris@17 106 public function testUncaughtFatalError() {
Chris@17 107 $fatal_error = [
Chris@17 108 '%type' => 'Recoverable fatal error',
Chris@17 109 '@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 110 '%function' => 'Drupal\error_test\Controller\ErrorTestController->Drupal\error_test\Controller\{closure}()',
Chris@17 111 ];
Chris@17 112 if (version_compare(PHP_VERSION, '7.0.0-dev') >= 0) {
Chris@17 113 // In PHP 7, instead of a recoverable fatal error we get a TypeError.
Chris@17 114 $fatal_error['%type'] = 'TypeError';
Chris@17 115 // The error message also changes in PHP 7.
Chris@17 116 $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 117 }
Chris@17 118 $this->drupalGet('error-test/generate-fatals');
Chris@17 119 $this->assertResponse(500, 'Received expected HTTP status code.');
Chris@17 120 $message = new FormattableMarkup('%type: @message in %function (line ', $fatal_error);
Chris@17 121 $this->assertRaw((string) $message);
Chris@17 122 $this->assertRaw('<pre class="backtrace">');
Chris@17 123 // Ensure we are escaping but not double escaping.
Chris@17 124 $this->assertRaw('&#039;');
Chris@17 125 $this->assertNoRaw('&amp;#039;');
Chris@17 126 }
Chris@17 127
Chris@17 128 /**
Chris@17 129 * Tests uncaught exception handling with custom exception handler.
Chris@17 130 */
Chris@17 131 public function testUncaughtExceptionCustomExceptionHandler() {
Chris@17 132 $settings_filename = $this->siteDirectory . '/settings.php';
Chris@17 133 chmod($settings_filename, 0777);
Chris@17 134 $settings_php = file_get_contents($settings_filename);
Chris@17 135 $settings_php .= "\n";
Chris@17 136 $settings_php .= "set_exception_handler(function() {\n";
Chris@17 137 $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
Chris@17 138 $settings_php .= " print('Oh oh, flying teapots');\n";
Chris@17 139 $settings_php .= "});\n";
Chris@17 140 file_put_contents($settings_filename, $settings_php);
Chris@17 141
Chris@17 142 \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
Chris@17 143
Chris@17 144 $this->drupalGet('');
Chris@17 145 $this->assertResponse(418);
Chris@17 146 $this->assertNoText('The website encountered an unexpected error. Please try again later.');
Chris@17 147 $this->assertNoText('Oh oh, bananas in the instruments');
Chris@17 148 $this->assertText('Oh oh, flying teapots');
Chris@17 149 }
Chris@17 150
Chris@17 151 /**
Chris@17 152 * Tests a missing dependency on a service.
Chris@17 153 */
Chris@17 154 public function testMissingDependency() {
Chris@17 155 if (version_compare(PHP_VERSION, '7.1') < 0) {
Chris@17 156 $this->expectedExceptionMessage = 'Argument 1 passed to Drupal\error_service_test\LonelyMonkeyClass::__construct() must be an instance of Drupal\Core\Database\Connection, non';
Chris@17 157 }
Chris@17 158 else {
Chris@17 159 $this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed';
Chris@17 160 }
Chris@17 161 $this->drupalGet('broken-service-class');
Chris@17 162 $this->assertResponse(500);
Chris@17 163
Chris@17 164 $this->assertRaw('The website encountered an unexpected error.');
Chris@17 165 $this->assertRaw($this->expectedExceptionMessage);
Chris@17 166 $this->assertErrorLogged($this->expectedExceptionMessage);
Chris@17 167 }
Chris@17 168
Chris@17 169 /**
Chris@17 170 * Tests a missing dependency on a service with a custom error handler.
Chris@17 171 */
Chris@17 172 public function testMissingDependencyCustomErrorHandler() {
Chris@17 173 $settings_filename = $this->siteDirectory . '/settings.php';
Chris@17 174 chmod($settings_filename, 0777);
Chris@17 175 $settings_php = file_get_contents($settings_filename);
Chris@17 176 $settings_php .= "\n";
Chris@17 177 $settings_php .= "set_error_handler(function() {\n";
Chris@17 178 $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
Chris@17 179 $settings_php .= " print('Oh oh, flying teapots');\n";
Chris@17 180 $settings_php .= " exit();\n";
Chris@17 181 $settings_php .= "});\n";
Chris@17 182 $settings_php .= "\$settings['teapots'] = TRUE;\n";
Chris@17 183 file_put_contents($settings_filename, $settings_php);
Chris@17 184
Chris@17 185 $this->drupalGet('broken-service-class');
Chris@17 186 $this->assertResponse(418);
Chris@17 187 $this->assertSame('Oh oh, flying teapots', $this->response);
Chris@17 188 }
Chris@17 189
Chris@17 190 /**
Chris@17 191 * Tests a container which has an error.
Chris@17 192 */
Chris@17 193 public function testErrorContainer() {
Chris@17 194 $settings = [];
Chris@17 195 $settings['settings']['container_base_class'] = (object) [
Chris@17 196 'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer',
Chris@17 197 'required' => TRUE,
Chris@17 198 ];
Chris@17 199 $this->writeSettings($settings);
Chris@17 200 \Drupal::service('kernel')->invalidateContainer();
Chris@17 201
Chris@17 202 $this->expectedExceptionMessage = 'Argument 1 passed to Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closur';
Chris@17 203 $this->drupalGet('');
Chris@17 204 $this->assertResponse(500);
Chris@17 205
Chris@17 206 $this->assertRaw($this->expectedExceptionMessage);
Chris@17 207 $this->assertErrorLogged($this->expectedExceptionMessage);
Chris@17 208 }
Chris@17 209
Chris@17 210 /**
Chris@17 211 * Tests a container which has an exception really early.
Chris@17 212 */
Chris@17 213 public function testExceptionContainer() {
Chris@17 214 $settings = [];
Chris@17 215 $settings['settings']['container_base_class'] = (object) [
Chris@17 216 'value' => '\Drupal\FunctionalTests\Bootstrap\ExceptionContainer',
Chris@17 217 'required' => TRUE,
Chris@17 218 ];
Chris@17 219 $this->writeSettings($settings);
Chris@17 220 \Drupal::service('kernel')->invalidateContainer();
Chris@17 221
Chris@17 222 $this->expectedExceptionMessage = 'Thrown exception during Container::get';
Chris@17 223 $this->drupalGet('');
Chris@17 224 $this->assertResponse(500);
Chris@17 225
Chris@17 226 $this->assertRaw('The website encountered an unexpected error');
Chris@17 227 $this->assertRaw($this->expectedExceptionMessage);
Chris@17 228 $this->assertErrorLogged($this->expectedExceptionMessage);
Chris@17 229 }
Chris@17 230
Chris@17 231 /**
Chris@17 232 * Tests the case when the database connection is gone.
Chris@17 233 */
Chris@17 234 public function testLostDatabaseConnection() {
Chris@17 235 $incorrect_username = $this->randomMachineName(16);
Chris@17 236 switch ($this->container->get('database')->driver()) {
Chris@17 237 case 'pgsql':
Chris@17 238 case 'mysql':
Chris@17 239 $this->expectedExceptionMessage = $incorrect_username;
Chris@17 240 break;
Chris@17 241 default:
Chris@17 242 // We can not carry out this test.
Chris@17 243 $this->pass('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.');
Chris@17 244 return;
Chris@17 245 }
Chris@17 246
Chris@17 247 // We simulate a broken database connection by rewrite settings.php to no
Chris@17 248 // longer have the proper data.
Chris@17 249 $settings['databases']['default']['default']['username'] = (object) [
Chris@17 250 'value' => $incorrect_username,
Chris@17 251 'required' => TRUE,
Chris@17 252 ];
Chris@17 253 $settings['databases']['default']['default']['password'] = (object) [
Chris@17 254 'value' => $this->randomMachineName(16),
Chris@17 255 'required' => TRUE,
Chris@17 256 ];
Chris@17 257
Chris@17 258 $this->writeSettings($settings);
Chris@17 259
Chris@17 260 $this->drupalGet('');
Chris@17 261 $this->assertResponse(500);
Chris@17 262 $this->assertRaw('DatabaseAccessDeniedException');
Chris@17 263 $this->assertErrorLogged($this->expectedExceptionMessage);
Chris@17 264 }
Chris@17 265
Chris@17 266 /**
Chris@17 267 * Tests fallback to PHP error log when an exception is thrown while logging.
Chris@17 268 */
Chris@17 269 public function testLoggerException() {
Chris@17 270 // Ensure the test error log is empty before these tests.
Chris@17 271 $this->assertNoErrorsLogged();
Chris@17 272
Chris@17 273 $this->expectedExceptionMessage = 'Deforestation';
Chris@17 274 \Drupal::state()->set('error_service_test.break_logger', TRUE);
Chris@17 275
Chris@17 276 $this->drupalGet('');
Chris@17 277 $this->assertResponse(500);
Chris@17 278 $this->assertText('The website encountered an unexpected error. Please try again later.');
Chris@17 279 $this->assertRaw($this->expectedExceptionMessage);
Chris@17 280
Chris@17 281 // Find fatal error logged to the error.log
Chris@17 282 $errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
Chris@17 283 $this->assertIdentical(count($errors), 8, 'The error + the error that the logging service is broken has been written to the error log.');
Chris@17 284 $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 285
Chris@17 286 $expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php';
Chris@17 287 $expected_line = 59;
Chris@17 288 $expected_entry = "Failed to log error: Exception: Deforestation in Drupal\\error_service_test\\MonkeysInTheControlRoom->handle() (line ${expected_line} of ${expected_path})";
Chris@17 289 $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 290
Chris@17 291 // The exception is expected. Do not interpret it as a test failure. Not
Chris@17 292 // using File API; a potential error must trigger a PHP warning.
Chris@17 293 unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
Chris@17 294 }
Chris@17 295
Chris@17 296 /**
Chris@17 297 * Asserts that a specific error has been logged to the PHP error log.
Chris@17 298 *
Chris@17 299 * @param string $error_message
Chris@17 300 * The expected error message.
Chris@17 301 *
Chris@17 302 * @see \Drupal\simpletest\TestBase::prepareEnvironment()
Chris@17 303 * @see \Drupal\Core\DrupalKernel::bootConfiguration()
Chris@17 304 */
Chris@17 305 protected function assertErrorLogged($error_message) {
Chris@17 306 $error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log';
Chris@17 307 if (!file_exists($error_log_filename)) {
Chris@17 308 $this->fail('No error logged yet.');
Chris@17 309 }
Chris@17 310
Chris@17 311 $content = file_get_contents($error_log_filename);
Chris@17 312 $rows = explode(PHP_EOL, $content);
Chris@17 313
Chris@17 314 // We iterate over the rows in order to be able to remove the logged error
Chris@17 315 // afterwards.
Chris@17 316 $found = FALSE;
Chris@17 317 foreach ($rows as $row_index => $row) {
Chris@17 318 if (strpos($content, $error_message) !== FALSE) {
Chris@17 319 $found = TRUE;
Chris@17 320 unset($rows[$row_index]);
Chris@17 321 }
Chris@17 322 }
Chris@17 323
Chris@17 324 file_put_contents($error_log_filename, implode("\n", $rows));
Chris@17 325
Chris@17 326 $this->assertTrue($found, sprintf('The %s error message was logged.', $error_message));
Chris@17 327 }
Chris@17 328
Chris@17 329 /**
Chris@17 330 * Asserts that no errors have been logged to the PHP error.log thus far.
Chris@17 331 *
Chris@17 332 * @see \Drupal\simpletest\TestBase::prepareEnvironment()
Chris@17 333 * @see \Drupal\Core\DrupalKernel::bootConfiguration()
Chris@17 334 */
Chris@17 335 protected function assertNoErrorsLogged() {
Chris@17 336 // Since PHP only creates the error.log file when an actual error is
Chris@17 337 // triggered, it is sufficient to check whether the file exists.
Chris@17 338 $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.');
Chris@17 339 }
Chris@17 340
Chris@17 341 /**
Chris@17 342 * Retrieves a Drupal path or an absolute path.
Chris@17 343 *
Chris@17 344 * Executes a cURL request for processing errors and exceptions.
Chris@17 345 *
Chris@17 346 * @param string|\Drupal\Core\Url $path
Chris@17 347 * Request path.
Chris@17 348 * @param array $extra_options
Chris@17 349 * (optional) Curl options to pass to curl_setopt()
Chris@17 350 * @param array $headers
Chris@17 351 * (optional) Not used.
Chris@17 352 */
Chris@17 353 protected function drupalGet($path, array $extra_options = [], array $headers = []) {
Chris@17 354 $url = $this->buildUrl($path, ['absolute' => TRUE]);
Chris@17 355
Chris@17 356 $ch = curl_init();
Chris@17 357 curl_setopt($ch, CURLOPT_URL, $url);
Chris@17 358 curl_setopt($ch, CURLOPT_HEADER, FALSE);
Chris@17 359 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
Chris@17 360 curl_setopt($ch, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
Chris@17 361 $this->response = curl_exec($ch);
Chris@17 362 $this->info = curl_getinfo($ch);
Chris@17 363 curl_close($ch);
Chris@17 364 }
Chris@17 365
Chris@17 366 /**
Chris@17 367 * {@inheritdoc}
Chris@17 368 */
Chris@17 369 protected function assertResponse($code) {
Chris@17 370 $this->assertSame($code, $this->info['http_code']);
Chris@17 371 }
Chris@17 372
Chris@17 373 /**
Chris@17 374 * {@inheritdoc}
Chris@17 375 */
Chris@17 376 protected function assertText($text) {
Chris@17 377 $this->assertContains($text, $this->response);
Chris@17 378 }
Chris@17 379
Chris@17 380 /**
Chris@17 381 * {@inheritdoc}
Chris@17 382 */
Chris@17 383 protected function assertNoText($text) {
Chris@17 384 $this->assertNotContains($text, $this->response);
Chris@17 385 }
Chris@17 386
Chris@17 387 /**
Chris@17 388 * {@inheritdoc}
Chris@17 389 */
Chris@17 390 protected function assertRaw($text) {
Chris@17 391 $this->assertText($text);
Chris@17 392 }
Chris@17 393
Chris@17 394 /**
Chris@17 395 * {@inheritdoc}
Chris@17 396 */
Chris@17 397 protected function assertNoRaw($text) {
Chris@17 398 $this->assertNoText($text);
Chris@17 399 }
Chris@17 400
Chris@17 401 }