PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/web/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php

https://gitlab.com/andecode/theme-spark
PHP | 324 lines | 197 code | 46 blank | 81 comment | 2 complexity | e663b3d1c63b3fb78612487420bb237d MD5 | raw file
  1. <?php
  2. namespace Drupal\FunctionalTests\Bootstrap;
  3. use Drupal\Component\Render\FormattableMarkup;
  4. use Drupal\Tests\BrowserTestBase;
  5. /**
  6. * Tests kernel panic when things are really messed up.
  7. *
  8. * @group system
  9. */
  10. class UncaughtExceptionTest extends BrowserTestBase {
  11. /**
  12. * Exceptions thrown by site under test that contain this text are ignored.
  13. *
  14. * @var string
  15. */
  16. protected $expectedExceptionMessage;
  17. /**
  18. * Modules to enable.
  19. *
  20. * @var array
  21. */
  22. protected static $modules = ['error_service_test', 'error_test'];
  23. /**
  24. * {@inheritdoc}
  25. */
  26. protected $defaultTheme = 'stark';
  27. /**
  28. * {@inheritdoc}
  29. */
  30. protected function setUp(): void {
  31. parent::setUp();
  32. $settings_filename = $this->siteDirectory . '/settings.php';
  33. chmod($settings_filename, 0777);
  34. $settings_php = file_get_contents($settings_filename);
  35. $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ErrorContainer.php';\n";
  36. $settings_php .= "\ninclude_once 'core/tests/Drupal/FunctionalTests/Bootstrap/ExceptionContainer.php';\n";
  37. // Ensure we can test errors rather than being caught in
  38. // \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware.
  39. $settings_php .= "\ndefine('SIMPLETEST_COLLECT_ERRORS', FALSE);\n";
  40. file_put_contents($settings_filename, $settings_php);
  41. $settings = [];
  42. $settings['config']['system.logging']['error_level'] = (object) [
  43. 'value' => ERROR_REPORTING_DISPLAY_VERBOSE,
  44. 'required' => TRUE,
  45. ];
  46. $this->writeSettings($settings);
  47. }
  48. /**
  49. * Tests uncaught exception handling when system is in a bad state.
  50. */
  51. public function testUncaughtException() {
  52. $this->expectedExceptionMessage = 'Oh oh, bananas in the instruments.';
  53. \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
  54. $settings = [];
  55. $settings['config']['system.logging']['error_level'] = (object) [
  56. 'value' => ERROR_REPORTING_HIDE,
  57. 'required' => TRUE,
  58. ];
  59. $this->writeSettings($settings);
  60. $this->drupalGet('');
  61. $this->assertSession()->statusCodeEquals(500);
  62. $this->assertSession()->pageTextContains('The website encountered an unexpected error. Please try again later.');
  63. $this->assertSession()->pageTextNotContains($this->expectedExceptionMessage);
  64. $settings = [];
  65. $settings['config']['system.logging']['error_level'] = (object) [
  66. 'value' => ERROR_REPORTING_DISPLAY_ALL,
  67. 'required' => TRUE,
  68. ];
  69. $this->writeSettings($settings);
  70. $this->drupalGet('');
  71. $this->assertSession()->statusCodeEquals(500);
  72. $this->assertSession()->pageTextContains('The website encountered an unexpected error. Please try again later.');
  73. $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
  74. $this->assertErrorLogged($this->expectedExceptionMessage);
  75. }
  76. /**
  77. * Tests displaying an uncaught fatal error.
  78. */
  79. public function testUncaughtFatalError() {
  80. $fatal_error = [
  81. '%type' => 'TypeError',
  82. '@message' => PHP_VERSION_ID >= 80000 ?
  83. 'Drupal\error_test\Controller\ErrorTestController::Drupal\error_test\Controller\{closure}(): Argument #1 ($test) must be of type array, string given, called in ' . \Drupal::root() . '/core/modules/system/tests/modules/error_test/src/Controller/ErrorTestController.php on line 65' :
  84. '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 65',
  85. '%function' => 'Drupal\error_test\Controller\ErrorTestController->Drupal\error_test\Controller\{closure}()',
  86. ];
  87. $this->drupalGet('error-test/generate-fatals');
  88. $this->assertSession()->statusCodeEquals(500);
  89. $message = new FormattableMarkup('%type: @message in %function (line ', $fatal_error);
  90. $this->assertSession()->responseContains((string) $message);
  91. $this->assertSession()->responseContains('<pre class="backtrace">');
  92. // Ensure we are escaping but not double escaping.
  93. $this->assertSession()->responseContains('&#039;');
  94. $this->assertSession()->responseNotContains('&amp;#039;');
  95. }
  96. /**
  97. * Tests uncaught exception handling with custom exception handler.
  98. */
  99. public function testUncaughtExceptionCustomExceptionHandler() {
  100. $settings_filename = $this->siteDirectory . '/settings.php';
  101. chmod($settings_filename, 0777);
  102. $settings_php = file_get_contents($settings_filename);
  103. $settings_php .= "\n";
  104. $settings_php .= "set_exception_handler(function() {\n";
  105. $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
  106. $settings_php .= " print('Oh oh, flying teapots');\n";
  107. $settings_php .= "});\n";
  108. file_put_contents($settings_filename, $settings_php);
  109. \Drupal::state()->set('error_service_test.break_bare_html_renderer', TRUE);
  110. $this->drupalGet('');
  111. $this->assertSession()->statusCodeEquals(418);
  112. $this->assertSession()->pageTextNotContains('The website encountered an unexpected error. Please try again later.');
  113. $this->assertSession()->pageTextNotContains('Oh oh, bananas in the instruments');
  114. $this->assertSession()->pageTextContains('Oh oh, flying teapots');
  115. }
  116. /**
  117. * Tests a missing dependency on a service.
  118. */
  119. public function testMissingDependency() {
  120. $this->expectedExceptionMessage = 'Too few arguments to function Drupal\error_service_test\LonelyMonkeyClass::__construct(), 0 passed';
  121. $this->drupalGet('broken-service-class');
  122. $this->assertSession()->statusCodeEquals(500);
  123. $this->assertSession()->pageTextContains('The website encountered an unexpected error.');
  124. $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
  125. $this->assertErrorLogged($this->expectedExceptionMessage);
  126. }
  127. /**
  128. * Tests a missing dependency on a service with a custom error handler.
  129. */
  130. public function testMissingDependencyCustomErrorHandler() {
  131. $settings_filename = $this->siteDirectory . '/settings.php';
  132. chmod($settings_filename, 0777);
  133. $settings_php = file_get_contents($settings_filename);
  134. $settings_php .= "\n";
  135. $settings_php .= "set_error_handler(function() {\n";
  136. $settings_php .= " header('HTTP/1.1 418 I\'m a teapot');\n";
  137. $settings_php .= " print('Oh oh, flying teapots');\n";
  138. $settings_php .= " exit();\n";
  139. $settings_php .= "});\n";
  140. $settings_php .= "\$settings['teapots'] = TRUE;\n";
  141. file_put_contents($settings_filename, $settings_php);
  142. $this->drupalGet('broken-service-class');
  143. $this->assertSession()->statusCodeEquals(418);
  144. $this->assertSession()->responseContains('Oh oh, flying teapots');
  145. }
  146. /**
  147. * Tests a container which has an error.
  148. */
  149. public function testErrorContainer() {
  150. $settings = [];
  151. $settings['settings']['container_base_class'] = (object) [
  152. 'value' => '\Drupal\FunctionalTests\Bootstrap\ErrorContainer',
  153. 'required' => TRUE,
  154. ];
  155. $this->writeSettings($settings);
  156. \Drupal::service('kernel')->invalidateContainer();
  157. $this->expectedExceptionMessage = PHP_VERSION_ID >= 80000 ?
  158. 'Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closure}(): Argument #1 ($container) must be of type Drupal\FunctionalTests\Bootstrap\ErrorContainer' :
  159. 'Argument 1 passed to Drupal\FunctionalTests\Bootstrap\ErrorContainer::Drupal\FunctionalTests\Bootstrap\{closur';
  160. $this->drupalGet('');
  161. $this->assertSession()->statusCodeEquals(500);
  162. $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
  163. $this->assertErrorLogged($this->expectedExceptionMessage);
  164. }
  165. /**
  166. * Tests a container which has an exception really early.
  167. */
  168. public function testExceptionContainer() {
  169. $settings = [];
  170. $settings['settings']['container_base_class'] = (object) [
  171. 'value' => '\Drupal\FunctionalTests\Bootstrap\ExceptionContainer',
  172. 'required' => TRUE,
  173. ];
  174. $this->writeSettings($settings);
  175. \Drupal::service('kernel')->invalidateContainer();
  176. $this->expectedExceptionMessage = 'Thrown exception during Container::get';
  177. $this->drupalGet('');
  178. $this->assertSession()->statusCodeEquals(500);
  179. $this->assertSession()->pageTextContains('The website encountered an unexpected error');
  180. $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
  181. $this->assertErrorLogged($this->expectedExceptionMessage);
  182. }
  183. /**
  184. * Tests the case when the database connection is gone.
  185. */
  186. public function testLostDatabaseConnection() {
  187. $incorrect_username = $this->randomMachineName(16);
  188. switch ($this->container->get('database')->driver()) {
  189. case 'pgsql':
  190. case 'mysql':
  191. $this->expectedExceptionMessage = $incorrect_username;
  192. break;
  193. default:
  194. // We can not carry out this test.
  195. $this->markTestSkipped('Unable to run \Drupal\system\Tests\System\UncaughtExceptionTest::testLostDatabaseConnection for this database type.');
  196. }
  197. // We simulate a broken database connection by rewrite settings.php to no
  198. // longer have the proper data.
  199. $settings['databases']['default']['default']['username'] = (object) [
  200. 'value' => $incorrect_username,
  201. 'required' => TRUE,
  202. ];
  203. $settings['databases']['default']['default']['password'] = (object) [
  204. 'value' => $this->randomMachineName(16),
  205. 'required' => TRUE,
  206. ];
  207. $this->writeSettings($settings);
  208. $this->drupalGet('');
  209. $this->assertSession()->statusCodeEquals(500);
  210. $this->assertSession()->pageTextContains('DatabaseAccessDeniedException');
  211. $this->assertErrorLogged($this->expectedExceptionMessage);
  212. }
  213. /**
  214. * Tests fallback to PHP error log when an exception is thrown while logging.
  215. */
  216. public function testLoggerException() {
  217. // Ensure the test error log is empty before these tests.
  218. $this->assertNoErrorsLogged();
  219. $this->expectedExceptionMessage = 'Deforestation';
  220. \Drupal::state()->set('error_service_test.break_logger', TRUE);
  221. $this->drupalGet('');
  222. $this->assertSession()->statusCodeEquals(500);
  223. $this->assertSession()->pageTextContains('The website encountered an unexpected error. Please try again later.');
  224. $this->assertSession()->pageTextContains($this->expectedExceptionMessage);
  225. // Find fatal error logged to the error.log
  226. $errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
  227. $this->assertCount(8, $errors, 'The error + the error that the logging service is broken has been written to the error log.');
  228. $this->assertStringContainsString('Failed to log error', $errors[0], 'The error handling logs when an error could not be logged to the logger.');
  229. $expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php';
  230. $expected_line = 62;
  231. $expected_entry = "Failed to log error: Exception: Deforestation in Drupal\\error_service_test\\MonkeysInTheControlRoom->handle() (line ${expected_line} of ${expected_path})";
  232. $this->assertStringContainsString($expected_entry, $errors[0], 'Original error logged to the PHP error log when an exception is thrown by a logger');
  233. // The exception is expected. Do not interpret it as a test failure. Not
  234. // using File API; a potential error must trigger a PHP warning.
  235. unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
  236. }
  237. /**
  238. * Asserts that a specific error has been logged to the PHP error log.
  239. *
  240. * @param string $error_message
  241. * The expected error message.
  242. *
  243. * @see \Drupal\simpletest\TestBase::prepareEnvironment()
  244. * @see \Drupal\Core\DrupalKernel::bootConfiguration()
  245. *
  246. * @internal
  247. */
  248. protected function assertErrorLogged(string $error_message): void {
  249. $error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log';
  250. $this->assertFileExists($error_log_filename);
  251. $content = file_get_contents($error_log_filename);
  252. $rows = explode(PHP_EOL, $content);
  253. // We iterate over the rows in order to be able to remove the logged error
  254. // afterwards.
  255. $found = FALSE;
  256. foreach ($rows as $row_index => $row) {
  257. if (strpos($content, $error_message) !== FALSE) {
  258. $found = TRUE;
  259. unset($rows[$row_index]);
  260. }
  261. }
  262. file_put_contents($error_log_filename, implode("\n", $rows));
  263. $this->assertTrue($found, sprintf('The %s error message was logged.', $error_message));
  264. }
  265. /**
  266. * Asserts that no errors have been logged to the PHP error.log thus far.
  267. *
  268. * @see \Drupal\simpletest\TestBase::prepareEnvironment()
  269. * @see \Drupal\Core\DrupalKernel::bootConfiguration()
  270. *
  271. * @internal
  272. */
  273. protected function assertNoErrorsLogged(): void {
  274. // Since PHP only creates the error.log file when an actual error is
  275. // triggered, it is sufficient to check whether the file exists.
  276. $this->assertFileDoesNotExist(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
  277. }
  278. }