ExceptionTrapTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP Project
  13. * @since 4.4.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Console\TestSuite\StubConsoleOutput;
  18. use Cake\Error\ErrorLogger;
  19. use Cake\Error\ExceptionTrap;
  20. use Cake\Error\Renderer\ConsoleExceptionRenderer;
  21. use Cake\Error\Renderer\TextExceptionRenderer;
  22. use Cake\Error\Renderer\WebExceptionRenderer;
  23. use Cake\Http\Exception\MissingControllerException;
  24. use Cake\Log\Log;
  25. use Cake\TestSuite\TestCase;
  26. use Cake\Utility\Text;
  27. use InvalidArgumentException;
  28. use RuntimeException;
  29. use Throwable;
  30. class ExceptionTrapTest extends TestCase
  31. {
  32. /**
  33. * @var string
  34. */
  35. private $memoryLimit;
  36. private $triggered = false;
  37. public function setUp(): void
  38. {
  39. parent::setUp();
  40. $this->memoryLimit = ini_get('memory_limit');
  41. }
  42. public function tearDown(): void
  43. {
  44. parent::tearDown();
  45. Log::reset();
  46. ini_set('memory_limit', $this->memoryLimit);
  47. }
  48. public function testConfigExceptionRendererFallback()
  49. {
  50. $output = new StubConsoleOutput();
  51. $trap = new ExceptionTrap(['exceptionRenderer' => null, 'stderr' => $output]);
  52. $error = new InvalidArgumentException('nope');
  53. $this->assertInstanceOf(ConsoleExceptionRenderer::class, $trap->renderer($error));
  54. }
  55. public function testConfigExceptionRenderer()
  56. {
  57. $trap = new ExceptionTrap(['exceptionRenderer' => WebExceptionRenderer::class]);
  58. $error = new InvalidArgumentException('nope');
  59. $this->assertInstanceOf(WebExceptionRenderer::class, $trap->renderer($error));
  60. }
  61. public function testConfigExceptionRendererFactory()
  62. {
  63. $trap = new ExceptionTrap(['exceptionRenderer' => function ($err, $req) {
  64. return new WebExceptionRenderer($err, $req);
  65. }]);
  66. $error = new InvalidArgumentException('nope');
  67. $this->assertInstanceOf(WebExceptionRenderer::class, $trap->renderer($error));
  68. }
  69. public function testConfigRendererHandleUnsafeOverwrite()
  70. {
  71. $output = new StubConsoleOutput();
  72. $trap = new ExceptionTrap(['stderr' => $output]);
  73. $trap->setConfig('exceptionRenderer', null);
  74. $error = new InvalidArgumentException('nope');
  75. $this->assertInstanceOf(ConsoleExceptionRenderer::class, $trap->renderer($error));
  76. }
  77. public function testLoggerConfig()
  78. {
  79. $trap = new ExceptionTrap(['logger' => ErrorLogger::class]);
  80. $this->assertInstanceOf(ErrorLogger::class, $trap->logger());
  81. }
  82. public function testLoggerHandleUnsafeOverwrite()
  83. {
  84. $trap = new ExceptionTrap();
  85. $trap->setConfig('logger', null);
  86. $this->assertInstanceOf(ErrorLogger::class, $trap->logger());
  87. }
  88. public function testHandleExceptionText()
  89. {
  90. $trap = new ExceptionTrap([
  91. 'exceptionRenderer' => TextExceptionRenderer::class,
  92. ]);
  93. $error = new InvalidArgumentException('nope');
  94. ob_start();
  95. $trap->handleException($error);
  96. $out = ob_get_clean();
  97. $this->assertStringContainsString('nope', $out);
  98. $this->assertStringContainsString('ExceptionTrapTest', $out);
  99. }
  100. public function testHandleExceptionConsoleRenderingNoStack()
  101. {
  102. $output = new StubConsoleOutput();
  103. $trap = new ExceptionTrap([
  104. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  105. 'stderr' => $output,
  106. ]);
  107. $error = new InvalidArgumentException('nope');
  108. $trap->handleException($error);
  109. $out = $output->messages();
  110. $this->assertStringContainsString('nope', $out[0]);
  111. $this->assertStringNotContainsString('Stack', $out[0]);
  112. }
  113. public function testHandleExceptionConsoleRenderingWithStack()
  114. {
  115. $output = new StubConsoleOutput();
  116. $trap = new ExceptionTrap([
  117. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  118. 'stderr' => $output,
  119. 'trace' => true,
  120. ]);
  121. $error = new InvalidArgumentException('nope');
  122. $trap->handleException($error);
  123. $out = $output->messages();
  124. $this->assertStringContainsString('nope', $out[0]);
  125. $this->assertStringContainsString('Stack', $out[0]);
  126. $this->assertStringContainsString('->testHandleExceptionConsoleRenderingWithStack', $out[0]);
  127. }
  128. public function testHandleExceptionConsoleRenderingWithPrevious()
  129. {
  130. $output = new StubConsoleOutput();
  131. $trap = new ExceptionTrap([
  132. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  133. 'stderr' => $output,
  134. 'trace' => true,
  135. ]);
  136. $previous = new RuntimeException('underlying error');
  137. $error = new InvalidArgumentException('nope', 0, $previous);
  138. $trap->handleException($error);
  139. $out = $output->messages();
  140. $this->assertStringContainsString('nope', $out[0]);
  141. $this->assertStringContainsString('Caused by [RuntimeException] underlying error', $out[0]);
  142. $this->assertEquals(2, substr_count($out[0], 'Stack Trace'));
  143. }
  144. public function testHandleExceptionConsoleWithAttributes()
  145. {
  146. $output = new StubConsoleOutput();
  147. $trap = new ExceptionTrap([
  148. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  149. 'stderr' => $output,
  150. ]);
  151. $error = new MissingControllerException(['name' => 'Articles']);
  152. $trap->handleException($error);
  153. $out = $output->messages();
  154. $this->assertStringContainsString('Controller class Articles', $out[0]);
  155. $this->assertStringContainsString('Exception Attributes', $out[0]);
  156. $this->assertStringContainsString('Articles', $out[0]);
  157. }
  158. /**
  159. * Test integration with HTML exception rendering
  160. *
  161. * Run in a separate process because HTML output writes headers.
  162. *
  163. * @preserveGlobalState disabled
  164. * @runInSeparateProcess
  165. */
  166. public function testHandleExceptionHtmlRendering()
  167. {
  168. $trap = new ExceptionTrap([
  169. 'exceptionRenderer' => WebExceptionRenderer::class,
  170. ]);
  171. $error = new InvalidArgumentException('nope');
  172. ob_start();
  173. $trap->handleException($error);
  174. $out = ob_get_clean();
  175. $this->assertStringContainsString('<!DOCTYPE', $out);
  176. $this->assertStringContainsString('<html', $out);
  177. $this->assertStringContainsString('nope', $out);
  178. $this->assertStringContainsString('class="stack-frame-header"', $out);
  179. $this->assertStringContainsString('Toggle Arguments', $out);
  180. }
  181. public function testLogException()
  182. {
  183. Log::setConfig('test_error', [
  184. 'className' => 'Array',
  185. ]);
  186. $trap = new ExceptionTrap();
  187. $error = new InvalidArgumentException('nope');
  188. $trap->logException($error);
  189. $logs = Log::engine('test_error')->read();
  190. $this->assertStringContainsString('nope', $logs[0]);
  191. }
  192. public function testLogExceptionConfigOff()
  193. {
  194. Log::setConfig('test_error', [
  195. 'className' => 'Array',
  196. ]);
  197. $trap = new ExceptionTrap(['log' => false]);
  198. $error = new InvalidArgumentException('nope');
  199. $trap->logException($error);
  200. $logs = Log::engine('test_error')->read();
  201. $this->assertEmpty($logs);
  202. }
  203. /**
  204. * @preserveGlobalState disabled
  205. * @runInSeparateProcess
  206. */
  207. public function testSkipLogException(): void
  208. {
  209. Log::setConfig('test_error', [
  210. 'className' => 'Array',
  211. ]);
  212. $trap = new ExceptionTrap([
  213. 'exceptionRenderer' => WebExceptionRenderer::class,
  214. 'skipLog' => [InvalidArgumentException::class],
  215. ]);
  216. $trap->getEventManager()->on('Exception.beforeRender', function () {
  217. $this->triggered = true;
  218. });
  219. ob_start();
  220. $trap->handleException(new InvalidArgumentException('nope'));
  221. ob_get_clean();
  222. $logs = Log::engine('test_error')->read();
  223. $this->assertEmpty($logs);
  224. $this->assertTrue($this->triggered, 'Should have triggered event when skipping logging.');
  225. }
  226. public function testEventTriggered()
  227. {
  228. $trap = new ExceptionTrap(['exceptionRenderer' => TextExceptionRenderer::class]);
  229. $trap->getEventManager()->on('Exception.beforeRender', function ($event, Throwable $error) {
  230. $this->assertEquals(100, $error->getCode());
  231. $this->assertStringContainsString('nope', $error->getMessage());
  232. $event->stopPropagation();
  233. });
  234. $error = new InvalidArgumentException('nope', 100);
  235. ob_start();
  236. $trap->handleException($error);
  237. $out = ob_get_clean();
  238. $this->assertNotEmpty($out);
  239. }
  240. public function testHandleShutdownNoOp()
  241. {
  242. $trap = new ExceptionTrap([
  243. 'exceptionRenderer' => TextExceptionRenderer::class,
  244. ]);
  245. ob_start();
  246. $trap->handleShutdown();
  247. $out = ob_get_clean();
  248. $this->assertEmpty($out);
  249. }
  250. public function testHandleFatalShutdownNoError()
  251. {
  252. $trap = new ExceptionTrap([
  253. 'exceptionRenderer' => TextExceptionRenderer::class,
  254. ]);
  255. error_clear_last();
  256. ob_start();
  257. $trap->handleShutdown();
  258. $out = ob_get_clean();
  259. $this->assertSame('', $out);
  260. }
  261. public function testHandleFatalErrorText()
  262. {
  263. $trap = new ExceptionTrap([
  264. 'exceptionRenderer' => TextExceptionRenderer::class,
  265. ]);
  266. ob_start();
  267. $trap->handleFatalError(E_USER_ERROR, 'Something bad', __FILE__, __LINE__);
  268. $out = ob_get_clean();
  269. $this->assertStringContainsString('500 : Fatal Error', $out);
  270. $this->assertStringContainsString('Something bad', $out);
  271. $this->assertStringContainsString(__FILE__, $out);
  272. }
  273. /**
  274. * Test integration with HTML rendering for fatal errors
  275. *
  276. * Run in a separate process because HTML output writes headers.
  277. *
  278. * @preserveGlobalState disabled
  279. * @runInSeparateProcess
  280. */
  281. public function testHandleFatalErrorHtmlRendering()
  282. {
  283. $trap = new ExceptionTrap([
  284. 'exceptionRenderer' => WebExceptionRenderer::class,
  285. ]);
  286. ob_start();
  287. $trap->handleFatalError(E_USER_ERROR, 'Something bad', __FILE__, __LINE__);
  288. $out = ob_get_clean();
  289. $this->assertStringContainsString('<!DOCTYPE', $out);
  290. $this->assertStringContainsString('<html', $out);
  291. $this->assertStringContainsString('Something bad', $out);
  292. $this->assertStringContainsString(__FILE__, $out);
  293. }
  294. /**
  295. * Data provider for memory limit increase
  296. */
  297. public static function initialMemoryProvider(): array
  298. {
  299. return [
  300. ['256M'],
  301. ['1G'],
  302. ];
  303. }
  304. /**
  305. * @dataProvider initialMemoryProvider
  306. */
  307. public function testIncreaseMemoryLimit($initial)
  308. {
  309. ini_set('memory_limit', $initial);
  310. $this->assertEquals($initial, ini_get('memory_limit'));
  311. $trap = new ExceptionTrap([
  312. 'exceptionRenderer' => TextExceptionRenderer::class,
  313. ]);
  314. $trap->increaseMemoryLimit(4 * 1024);
  315. $initialBytes = Text::parseFileSize($initial, false);
  316. $result = Text::parseFileSize(ini_get('memory_limit'), false);
  317. $this->assertWithinRange($initialBytes + (4 * 1024 * 1024), $result, 1024);
  318. }
  319. public function testSingleton()
  320. {
  321. $trap = new ExceptionTrap();
  322. $trap->register();
  323. $this->assertSame($trap, ExceptionTrap::instance());
  324. $trap->unregister();
  325. $this->assertNull(ExceptionTrap::instance());
  326. }
  327. }