FlattenExceptionTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\ErrorHandler\Tests\Exception;
  11. use PHPUnit\Framework\TestCase;
  12. use Symfony\Component\ErrorHandler\Exception\FlattenException;
  13. use Symfony\Component\ErrorHandler\Tests\Fixtures\StringErrorCodeException;
  14. use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
  15. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  16. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  17. use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
  18. use Symfony\Component\HttpKernel\Exception\GoneHttpException;
  19. use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
  20. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  21. use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
  22. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  23. use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
  24. use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
  25. use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
  26. use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
  27. use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
  28. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  29. class FlattenExceptionTest extends TestCase
  30. {
  31. public function testStatusCode()
  32. {
  33. $flattened = FlattenException::createFromThrowable(new \RuntimeException(), 403);
  34. $this->assertEquals('403', $flattened->getStatusCode());
  35. $flattened = FlattenException::createFromThrowable(new \RuntimeException());
  36. $this->assertEquals('500', $flattened->getStatusCode());
  37. $flattened = FlattenException::createFromThrowable(new \DivisionByZeroError(), 403);
  38. $this->assertEquals('403', $flattened->getStatusCode());
  39. $flattened = FlattenException::createFromThrowable(new \DivisionByZeroError());
  40. $this->assertEquals('500', $flattened->getStatusCode());
  41. $flattened = FlattenException::createFromThrowable(new NotFoundHttpException());
  42. $this->assertEquals('404', $flattened->getStatusCode());
  43. $flattened = FlattenException::createFromThrowable(new UnauthorizedHttpException('Basic realm="My Realm"'));
  44. $this->assertEquals('401', $flattened->getStatusCode());
  45. $flattened = FlattenException::createFromThrowable(new BadRequestHttpException());
  46. $this->assertEquals('400', $flattened->getStatusCode());
  47. $flattened = FlattenException::createFromThrowable(new NotAcceptableHttpException());
  48. $this->assertEquals('406', $flattened->getStatusCode());
  49. $flattened = FlattenException::createFromThrowable(new ConflictHttpException());
  50. $this->assertEquals('409', $flattened->getStatusCode());
  51. $flattened = FlattenException::createFromThrowable(new MethodNotAllowedHttpException(['POST']));
  52. $this->assertEquals('405', $flattened->getStatusCode());
  53. $flattened = FlattenException::createFromThrowable(new AccessDeniedHttpException());
  54. $this->assertEquals('403', $flattened->getStatusCode());
  55. $flattened = FlattenException::createFromThrowable(new GoneHttpException());
  56. $this->assertEquals('410', $flattened->getStatusCode());
  57. $flattened = FlattenException::createFromThrowable(new LengthRequiredHttpException());
  58. $this->assertEquals('411', $flattened->getStatusCode());
  59. $flattened = FlattenException::createFromThrowable(new PreconditionFailedHttpException());
  60. $this->assertEquals('412', $flattened->getStatusCode());
  61. $flattened = FlattenException::createFromThrowable(new PreconditionRequiredHttpException());
  62. $this->assertEquals('428', $flattened->getStatusCode());
  63. $flattened = FlattenException::createFromThrowable(new ServiceUnavailableHttpException());
  64. $this->assertEquals('503', $flattened->getStatusCode());
  65. $flattened = FlattenException::createFromThrowable(new TooManyRequestsHttpException());
  66. $this->assertEquals('429', $flattened->getStatusCode());
  67. $flattened = FlattenException::createFromThrowable(new UnsupportedMediaTypeHttpException());
  68. $this->assertEquals('415', $flattened->getStatusCode());
  69. if (class_exists(SuspiciousOperationException::class)) {
  70. $flattened = FlattenException::createFromThrowable(new SuspiciousOperationException());
  71. $this->assertEquals('400', $flattened->getStatusCode());
  72. }
  73. }
  74. public function testHeadersForHttpException()
  75. {
  76. $flattened = FlattenException::createFromThrowable(new MethodNotAllowedHttpException(['POST']));
  77. $this->assertEquals(['Allow' => 'POST'], $flattened->getHeaders());
  78. $flattened = FlattenException::createFromThrowable(new UnauthorizedHttpException('Basic realm="My Realm"'));
  79. $this->assertEquals(['WWW-Authenticate' => 'Basic realm="My Realm"'], $flattened->getHeaders());
  80. $flattened = FlattenException::createFromThrowable(new ServiceUnavailableHttpException('Fri, 31 Dec 1999 23:59:59 GMT'));
  81. $this->assertEquals(['Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'], $flattened->getHeaders());
  82. $flattened = FlattenException::createFromThrowable(new ServiceUnavailableHttpException(120));
  83. $this->assertEquals(['Retry-After' => 120], $flattened->getHeaders());
  84. $flattened = FlattenException::createFromThrowable(new TooManyRequestsHttpException('Fri, 31 Dec 1999 23:59:59 GMT'));
  85. $this->assertEquals(['Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'], $flattened->getHeaders());
  86. $flattened = FlattenException::createFromThrowable(new TooManyRequestsHttpException(120));
  87. $this->assertEquals(['Retry-After' => 120], $flattened->getHeaders());
  88. }
  89. /**
  90. * @dataProvider flattenDataProvider
  91. */
  92. public function testFlattenHttpException(\Throwable $exception)
  93. {
  94. $flattened = FlattenException::createFromThrowable($exception);
  95. $flattened2 = FlattenException::createFromThrowable($exception);
  96. $flattened->setPrevious($flattened2);
  97. $this->assertEquals($exception->getMessage(), $flattened->getMessage(), 'The message is copied from the original exception.');
  98. $this->assertEquals($exception->getCode(), $flattened->getCode(), 'The code is copied from the original exception.');
  99. $this->assertInstanceOf($flattened->getClass(), $exception, 'The class is set to the class of the original exception');
  100. }
  101. public function testThrowable()
  102. {
  103. $error = new \DivisionByZeroError('Ouch', 42);
  104. $flattened = FlattenException::createFromThrowable($error);
  105. $this->assertSame('Ouch', $flattened->getMessage(), 'The message is copied from the original error.');
  106. $this->assertSame(42, $flattened->getCode(), 'The code is copied from the original error.');
  107. $this->assertSame('DivisionByZeroError', $flattened->getClass(), 'The class is set to the class of the original error');
  108. }
  109. /**
  110. * @dataProvider flattenDataProvider
  111. */
  112. public function testPrevious(\Throwable $exception)
  113. {
  114. $flattened = FlattenException::createFromThrowable($exception);
  115. $flattened2 = FlattenException::createFromThrowable($exception);
  116. $flattened->setPrevious($flattened2);
  117. $this->assertSame($flattened2, $flattened->getPrevious());
  118. $this->assertSame([$flattened2], $flattened->getAllPrevious());
  119. }
  120. public function testPreviousError()
  121. {
  122. $exception = new \Exception('test', 123, new \ParseError('Oh noes!', 42));
  123. $flattened = FlattenException::createFromThrowable($exception)->getPrevious();
  124. $this->assertEquals('Oh noes!', $flattened->getMessage(), 'The message is copied from the original exception.');
  125. $this->assertEquals(42, $flattened->getCode(), 'The code is copied from the original exception.');
  126. $this->assertEquals('ParseError', $flattened->getClass(), 'The class is set to the class of the original exception');
  127. }
  128. /**
  129. * @dataProvider flattenDataProvider
  130. */
  131. public function testLine(\Throwable $exception)
  132. {
  133. $flattened = FlattenException::createFromThrowable($exception);
  134. $this->assertSame($exception->getLine(), $flattened->getLine());
  135. }
  136. /**
  137. * @dataProvider flattenDataProvider
  138. */
  139. public function testFile(\Throwable $exception)
  140. {
  141. $flattened = FlattenException::createFromThrowable($exception);
  142. $this->assertSame($exception->getFile(), $flattened->getFile());
  143. }
  144. /**
  145. * @dataProvider stringAndIntDataProvider
  146. */
  147. public function testCode(\Throwable $exception)
  148. {
  149. $flattened = FlattenException::createFromThrowable($exception);
  150. $this->assertSame($exception->getCode(), $flattened->getCode());
  151. }
  152. /**
  153. * @dataProvider flattenDataProvider
  154. */
  155. public function testToArray(\Throwable $exception, string $expectedClass)
  156. {
  157. $flattened = FlattenException::createFromThrowable($exception);
  158. $flattened->setTrace([], 'foo.php', 123);
  159. $this->assertEquals([
  160. [
  161. 'message' => 'test',
  162. 'class' => $expectedClass,
  163. 'trace' => [[
  164. 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123,
  165. 'args' => [],
  166. ]],
  167. ],
  168. ], $flattened->toArray());
  169. }
  170. public function testCreate()
  171. {
  172. $exception = new NotFoundHttpException(
  173. 'test',
  174. new \RuntimeException('previous', 123)
  175. );
  176. $this->assertSame(
  177. FlattenException::createFromThrowable($exception)->toArray(),
  178. FlattenException::createFromThrowable($exception)->toArray()
  179. );
  180. }
  181. public static function flattenDataProvider(): array
  182. {
  183. return [
  184. [new \Exception('test', 123), 'Exception'],
  185. [new \Error('test', 123), 'Error'],
  186. ];
  187. }
  188. public static function stringAndIntDataProvider(): array
  189. {
  190. return [
  191. [new \Exception('test1', 123)],
  192. [new StringErrorCodeException('test2', '42S02')],
  193. ];
  194. }
  195. public function testArguments()
  196. {
  197. if (\PHP_VERSION_ID >= 70400) {
  198. $this->markTestSkipped('PHP 7.4 removes arguments from exception traces.');
  199. }
  200. $dh = opendir(__DIR__);
  201. $fh = tmpfile();
  202. $incomplete = unserialize('O:14:"BogusTestClass":0:{}');
  203. $exception = $this->createException([
  204. (object) ['foo' => 1],
  205. new NotFoundHttpException(),
  206. $incomplete,
  207. $dh,
  208. $fh,
  209. function () {},
  210. [1, 2],
  211. ['foo' => 123],
  212. null,
  213. true,
  214. false,
  215. 0,
  216. 0.0,
  217. '0',
  218. '',
  219. \INF,
  220. \NAN,
  221. ]);
  222. $flattened = FlattenException::createFromThrowable($exception);
  223. $trace = $flattened->getTrace();
  224. $args = $trace[1]['args'];
  225. $array = $args[0][1];
  226. closedir($dh);
  227. fclose($fh);
  228. $i = 0;
  229. $this->assertSame(['object', 'stdClass'], $array[$i++]);
  230. $this->assertSame(['object', 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'], $array[$i++]);
  231. $this->assertSame(['incomplete-object', 'BogusTestClass'], $array[$i++]);
  232. $this->assertSame(['resource', 'stream'], $array[$i++]);
  233. $this->assertSame(['resource', 'stream'], $array[$i++]);
  234. $args = $array[$i++];
  235. $this->assertSame('object', $args[0]);
  236. $this->assertTrue('Closure' === $args[1] || is_subclass_of($args[1], \Closure::class), 'Expect object class name to be Closure or a subclass of Closure.');
  237. $this->assertSame(['array', [['integer', 1], ['integer', 2]]], $array[$i++]);
  238. $this->assertSame(['array', ['foo' => ['integer', 123]]], $array[$i++]);
  239. $this->assertSame(['null', null], $array[$i++]);
  240. $this->assertSame(['boolean', true], $array[$i++]);
  241. $this->assertSame(['boolean', false], $array[$i++]);
  242. $this->assertSame(['integer', 0], $array[$i++]);
  243. $this->assertSame(['float', 0.0], $array[$i++]);
  244. $this->assertSame(['string', '0'], $array[$i++]);
  245. $this->assertSame(['string', ''], $array[$i++]);
  246. $this->assertSame(['float', \INF], $array[$i++]);
  247. // assertEquals() does not like NAN values.
  248. $this->assertEquals('float', $array[$i][0]);
  249. $this->assertNan($array[$i][1]);
  250. }
  251. public function testRecursionInArguments()
  252. {
  253. if (\PHP_VERSION_ID >= 70400) {
  254. $this->markTestSkipped('PHP 7.4 removes arguments from exception traces.');
  255. }
  256. $a = null;
  257. $a = ['foo', [2, &$a]];
  258. $exception = $this->createException($a);
  259. $flattened = FlattenException::createFromThrowable($exception);
  260. $trace = $flattened->getTrace();
  261. $this->assertStringContainsString('*DEEP NESTED ARRAY*', serialize($trace));
  262. }
  263. public function testTooBigArray()
  264. {
  265. if (\PHP_VERSION_ID >= 70400) {
  266. $this->markTestSkipped('PHP 7.4 removes arguments from exception traces.');
  267. }
  268. $a = [];
  269. for ($i = 0; $i < 20; ++$i) {
  270. for ($j = 0; $j < 50; ++$j) {
  271. for ($k = 0; $k < 10; ++$k) {
  272. $a[$i][$j][$k] = 'value';
  273. }
  274. }
  275. }
  276. $a[20] = 'value';
  277. $a[21] = 'value1';
  278. $exception = $this->createException($a);
  279. $flattened = FlattenException::createFromThrowable($exception);
  280. $trace = $flattened->getTrace();
  281. $this->assertSame(['array', ['array', '*SKIPPED over 10000 entries*']], $trace[1]['args'][0]);
  282. $serializeTrace = serialize($trace);
  283. $this->assertStringContainsString('*SKIPPED over 10000 entries*', $serializeTrace);
  284. $this->assertStringNotContainsString('*value1*', $serializeTrace);
  285. }
  286. public function testAnonymousClass()
  287. {
  288. $flattened = FlattenException::createFromThrowable(new class() extends \RuntimeException {
  289. });
  290. $this->assertSame('RuntimeException@anonymous', $flattened->getClass());
  291. $flattened->setClass(\get_class(new class('Oops') extends NotFoundHttpException {
  292. }));
  293. $this->assertSame('Symfony\Component\HttpKernel\Exception\NotFoundHttpException@anonymous', $flattened->getClass());
  294. $flattened = FlattenException::createFromThrowable(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException {
  295. }))));
  296. $this->assertSame('Class "RuntimeException@anonymous" blah.', $flattened->getMessage());
  297. }
  298. public function testToStringEmptyMessage()
  299. {
  300. $exception = new \RuntimeException();
  301. $flattened = FlattenException::createFromThrowable($exception);
  302. $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString());
  303. $this->assertSame($exception->__toString(), $flattened->getAsString());
  304. }
  305. public function testToString()
  306. {
  307. $test = function ($a, $b, $c, $d) {
  308. return new \RuntimeException('This is a test message');
  309. };
  310. $exception = $test('foo123', 1, null, 1.5);
  311. $flattened = FlattenException::createFromThrowable($exception);
  312. $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString());
  313. $this->assertSame($exception->__toString(), $flattened->getAsString());
  314. }
  315. public function testToStringParent()
  316. {
  317. $exception = new \LogicException('This is message 1');
  318. $exception = new \RuntimeException('This is messsage 2', 500, $exception);
  319. $flattened = FlattenException::createFromThrowable($exception);
  320. $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString());
  321. $this->assertSame($exception->__toString(), $flattened->getAsString());
  322. }
  323. private function createException($foo): \Exception
  324. {
  325. return new \Exception();
  326. }
  327. }