UncaughtExceptionTest.php 12 KB

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