DebuggerTest.php 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  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 1.2.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Controller\Controller;
  18. use Cake\Core\Configure;
  19. use Cake\Error\Debug\ConsoleFormatter;
  20. use Cake\Error\Debug\HtmlFormatter;
  21. use Cake\Error\Debug\NodeInterface;
  22. use Cake\Error\Debug\ScalarNode;
  23. use Cake\Error\Debug\SpecialNode;
  24. use Cake\Error\Debug\TextFormatter;
  25. use Cake\Error\Debugger;
  26. use Cake\Error\Renderer\HtmlErrorRenderer;
  27. use Cake\Form\Form;
  28. use Cake\Log\Log;
  29. use Cake\ORM\Table;
  30. use Cake\TestSuite\TestCase;
  31. use InvalidArgumentException;
  32. use MyClass;
  33. use RuntimeException;
  34. use SplFixedArray;
  35. use stdClass;
  36. use TestApp\Error\TestDebugger;
  37. use TestApp\Error\Thing\DebuggableThing;
  38. use TestApp\Error\Thing\SecurityThing;
  39. use TestApp\Utility\ThrowsDebugInfo;
  40. /**
  41. * DebuggerTest class
  42. *
  43. * !!! Be careful with changing code below as it may
  44. * !!! change line numbers which are used in the tests
  45. */
  46. class DebuggerTest extends TestCase
  47. {
  48. /**
  49. * @var bool
  50. */
  51. protected $restoreError = false;
  52. /**
  53. * setUp method
  54. */
  55. public function setUp(): void
  56. {
  57. parent::setUp();
  58. Configure::write('debug', true);
  59. Log::drop('stderr');
  60. Log::drop('stdout');
  61. Debugger::configInstance('exportFormatter', TextFormatter::class);
  62. }
  63. /**
  64. * tearDown method
  65. */
  66. public function tearDown(): void
  67. {
  68. parent::tearDown();
  69. if ($this->restoreError) {
  70. restore_error_handler();
  71. }
  72. }
  73. /**
  74. * testDocRef method
  75. */
  76. public function testDocRef(): void
  77. {
  78. ini_set('docref_root', '');
  79. $this->assertEquals(ini_get('docref_root'), '');
  80. // Force a new instance.
  81. Debugger::getInstance(TestDebugger::class);
  82. Debugger::getInstance(Debugger::class);
  83. $this->assertEquals(ini_get('docref_root'), 'https://secure.php.net/');
  84. }
  85. /**
  86. * test Excerpt writing
  87. */
  88. public function testExcerpt(): void
  89. {
  90. $result = Debugger::excerpt(__FILE__, __LINE__ - 1, 2);
  91. $this->assertIsArray($result);
  92. $this->assertCount(5, $result);
  93. $this->assertMatchesRegularExpression('/function(.+)testExcerpt/', $result[1]);
  94. $result = Debugger::excerpt(__FILE__, 2, 2);
  95. $this->assertIsArray($result);
  96. $this->assertCount(4, $result);
  97. $this->skipIf(defined('HHVM_VERSION'), 'HHVM does not highlight php code');
  98. // Due to different highlight_string() function behavior, see. https://3v4l.org/HcfBN. Since 8.3, it wraps it around <pre>
  99. $pattern = version_compare(PHP_VERSION, '8.3', '<')
  100. ? '/<code>.*?<span style\="color\: \#\d+">.*?&lt;\?php/'
  101. : '/<pre>.*?<code style\="color\: \#\d+">.*?<span style\="color\: \#[a-zA-Z0-9]+">.*?&lt;\?php/';
  102. $this->assertMatchesRegularExpression($pattern, $result[0]);
  103. $result = Debugger::excerpt(__FILE__, 11, 2);
  104. $this->assertCount(5, $result);
  105. $pattern = '/<span style\="color\: \#\d{6}">.*?<\/span>/';
  106. $this->assertMatchesRegularExpression($pattern, $result[0]);
  107. $return = Debugger::excerpt('[internal]', 2, 2);
  108. $this->assertEmpty($return);
  109. $result = Debugger::excerpt(__FILE__, __LINE__, 5);
  110. $this->assertCount(11, $result);
  111. $this->assertStringContainsString('Debugger', $result[5]);
  112. $this->assertStringContainsString('excerpt', $result[5]);
  113. $this->assertStringContainsString('__FILE__', $result[5]);
  114. $result = Debugger::excerpt(__FILE__, 1, 2);
  115. $this->assertCount(3, $result);
  116. $lastLine = count(explode("\n", file_get_contents(__FILE__)));
  117. $result = Debugger::excerpt(__FILE__, $lastLine, 2);
  118. $this->assertCount(3, $result);
  119. }
  120. /**
  121. * Test that setOutputFormat works.
  122. */
  123. public function testSetOutputFormat(): void
  124. {
  125. $this->deprecated(function () {
  126. Debugger::setOutputFormat('html');
  127. $this->assertSame('html', Debugger::getOutputFormat());
  128. });
  129. }
  130. /**
  131. * Test that getOutputFormat/setOutputFormat works.
  132. */
  133. public function testGetSetOutputFormat(): void
  134. {
  135. $this->deprecated(function () {
  136. Debugger::setOutputFormat('html');
  137. $this->assertSame('html', Debugger::getOutputFormat());
  138. });
  139. }
  140. /**
  141. * Test that choosing a nonexistent format causes an exception
  142. */
  143. public function testSetOutputAsException(): void
  144. {
  145. $this->expectException(InvalidArgumentException::class);
  146. $this->deprecated(function () {
  147. Debugger::setOutputFormat('Invalid junk');
  148. });
  149. }
  150. /**
  151. * Test outputError with description encoding
  152. */
  153. public function testOutputErrorDescriptionEncoding(): void
  154. {
  155. $this->deprecated(function () {
  156. Debugger::setOutputFormat('html');
  157. ob_start();
  158. $debugger = Debugger::getInstance();
  159. $debugger->outputError([
  160. 'error' => 'Notice',
  161. 'code' => E_NOTICE,
  162. 'level' => E_NOTICE,
  163. 'description' => 'Undefined index <script>alert(1)</script>',
  164. 'file' => __FILE__,
  165. 'line' => __LINE__,
  166. ]);
  167. });
  168. $result = ob_get_clean();
  169. $this->assertStringContainsString('&lt;script&gt;', $result);
  170. $this->assertStringNotContainsString('<script>', $result);
  171. }
  172. /**
  173. * Test invalid class and addRenderer()
  174. */
  175. public function testAddRendererInvalid(): void
  176. {
  177. $this->expectException(InvalidArgumentException::class);
  178. $this->deprecated(function () {
  179. Debugger::addRenderer('test', stdClass::class);
  180. });
  181. }
  182. /**
  183. * Test addFormat() overwriting addRenderer()
  184. */
  185. public function testAddOutputFormatOverwrite(): void
  186. {
  187. $this->deprecated(function () {
  188. Debugger::addRenderer('test', HtmlErrorRenderer::class);
  189. Debugger::addFormat('test', [
  190. 'error' => '{:description} : {:path}, line {:line}',
  191. ]);
  192. Debugger::setOutputFormat('test');
  193. ob_start();
  194. $debugger = Debugger::getInstance();
  195. $data = [
  196. 'error' => 'Notice',
  197. 'code' => E_NOTICE,
  198. 'level' => E_NOTICE,
  199. 'description' => 'Oh no!',
  200. 'file' => __FILE__,
  201. 'line' => __LINE__,
  202. ];
  203. $debugger->outputError($data);
  204. $result = ob_get_clean();
  205. $this->assertStringContainsString('Oh no! :', $result);
  206. $this->assertStringContainsString(", line {$data['line']}", $result);
  207. });
  208. }
  209. /**
  210. * Tests that the correct line is being highlighted.
  211. */
  212. public function testOutputErrorLineHighlight(): void
  213. {
  214. $this->deprecated(function () {
  215. Debugger::setOutputFormat('js');
  216. ob_start();
  217. $debugger = Debugger::getInstance();
  218. $data = [
  219. 'level' => E_NOTICE,
  220. 'code' => E_NOTICE,
  221. 'file' => __FILE__,
  222. 'line' => __LINE__,
  223. 'description' => 'Error description',
  224. 'start' => 1,
  225. ];
  226. $debugger->outputError($data);
  227. });
  228. $result = ob_get_clean();
  229. $this->assertMatchesRegularExpression('#^\<span class\="code\-highlight"\>.*__LINE__.*\</span\>$#m', $result);
  230. }
  231. /**
  232. * Test plain text output format.
  233. */
  234. public function testOutputErrorText(): void
  235. {
  236. $this->deprecated(function () {
  237. Debugger::setOutputFormat('txt');
  238. ob_start();
  239. $debugger = Debugger::getInstance();
  240. $data = [
  241. 'level' => E_NOTICE,
  242. 'code' => E_NOTICE,
  243. 'file' => __FILE__,
  244. 'line' => __LINE__,
  245. 'description' => 'Error description',
  246. 'start' => 1,
  247. ];
  248. $debugger->outputError($data);
  249. $result = ob_get_clean();
  250. $this->assertStringContainsString('notice: 8 :: Error description', $result);
  251. $this->assertStringContainsString("on line {$data['line']} of {$data['file']}", $result);
  252. $this->assertStringContainsString('Trace:', $result);
  253. $this->assertStringContainsString('Cake\Test\TestCase\Error\DebuggerTest->testOutputErrorText()', $result);
  254. $this->assertStringContainsString('[main]', $result);
  255. });
  256. }
  257. /**
  258. * Test log output format.
  259. */
  260. public function testOutputErrorLog(): void
  261. {
  262. $this->deprecated(function () {
  263. Debugger::setOutputFormat('log');
  264. Log::setConfig('array', ['engine' => 'Array']);
  265. ob_start();
  266. $debugger = Debugger::getInstance();
  267. $data = [
  268. 'level' => E_NOTICE,
  269. 'code' => E_NOTICE,
  270. 'file' => __FILE__,
  271. 'line' => __LINE__,
  272. 'description' => 'Error description',
  273. 'start' => 1,
  274. ];
  275. $debugger->outputError($data);
  276. $output = ob_get_clean();
  277. /** @var \Cake\Log\Engine\ArrayLog $logger */
  278. $logger = Log::engine('array');
  279. $logs = $logger->read();
  280. $this->assertSame('', $output);
  281. $this->assertCount(1, $logs);
  282. $this->assertStringContainsString('Cake\Error\Debugger->outputError()', $logs[0]);
  283. $this->assertStringContainsString("'file' => '{$data['file']}'", $logs[0]);
  284. $this->assertStringContainsString("'line' => (int) {$data['line']}", $logs[0]);
  285. $this->assertStringContainsString("'trace' => ", $logs[0]);
  286. $this->assertStringContainsString("'description' => 'Error description'", $logs[0]);
  287. $this->assertStringContainsString('DebuggerTest->testOutputErrorLog()', $logs[0]);
  288. });
  289. }
  290. /**
  291. * Tests that changes in output formats using Debugger::output() change the templates used.
  292. */
  293. public function testAddFormat(): void
  294. {
  295. $this->deprecated(function () {
  296. Debugger::addFormat('js', [
  297. 'traceLine' => '{:reference} - <a href="txmt://open?url=file://{:file}' .
  298. '&line={:line}">{:path}</a>, line {:line}',
  299. ]);
  300. Debugger::setOutputFormat('js');
  301. $result = Debugger::trace();
  302. $this->assertMatchesRegularExpression('/' . preg_quote('txmt://open?url=file://', '/') . '(\/|[A-Z]:\\\\)' . '/', $result);
  303. Debugger::addFormat('xml', [
  304. 'error' => '<error><code>{:code}</code><file>{:file}</file><line>{:line}</line>' .
  305. '{:description}</error>',
  306. ]);
  307. Debugger::setOutputFormat('xml');
  308. ob_start();
  309. $debugger = Debugger::getInstance();
  310. $debugger->outputError([
  311. 'level' => E_NOTICE,
  312. 'code' => E_NOTICE,
  313. 'file' => __FILE__,
  314. 'line' => __LINE__,
  315. 'description' => 'Undefined variable: foo',
  316. ]);
  317. $result = ob_get_clean();
  318. $expected = [
  319. '<error',
  320. '<code', '8', '/code',
  321. '<file', 'preg:/[^<]+/', '/file',
  322. '<line', '' . ((int)__LINE__ - 9), '/line',
  323. 'preg:/Undefined variable:\s+foo/',
  324. '/error',
  325. ];
  326. $this->assertHtml($expected, $result, true);
  327. });
  328. }
  329. /**
  330. * Test adding a format that is handled by a callback.
  331. */
  332. public function testAddFormatCallback(): void
  333. {
  334. $this->deprecated(function () {
  335. Debugger::addFormat('callback', ['callback' => [$this, 'customFormat']]);
  336. Debugger::setOutputFormat('callback');
  337. ob_start();
  338. $debugger = Debugger::getInstance();
  339. $debugger->outputError([
  340. 'error' => 'Notice',
  341. 'code' => E_NOTICE,
  342. 'level' => E_NOTICE,
  343. 'description' => 'Undefined variable $foo',
  344. 'file' => __FILE__,
  345. 'line' => __LINE__,
  346. ]);
  347. $result = ob_get_clean();
  348. $this->assertStringContainsString('Notice: I eated an error', $result);
  349. $this->assertStringContainsString('DebuggerTest.php', $result);
  350. Debugger::setOutputFormat('js');
  351. });
  352. }
  353. /**
  354. * Test method for testing addFormat with callbacks.
  355. */
  356. public function customFormat(array $error, array $strings): void
  357. {
  358. echo $error['error'] . ': I eated an error ' . $error['file'];
  359. }
  360. /**
  361. * testTrimPath method
  362. */
  363. public function testTrimPath(): void
  364. {
  365. $this->assertSame('APP/', Debugger::trimPath(APP));
  366. $this->assertSame('CORE' . DS . 'src' . DS, Debugger::trimPath(CAKE));
  367. $this->assertSame('Some/Other/Path', Debugger::trimPath('Some/Other/Path'));
  368. }
  369. /**
  370. * testExportVar method
  371. */
  372. public function testExportVar(): void
  373. {
  374. $std = new stdClass();
  375. $std->int = 2;
  376. $std->float = 1.333;
  377. $std->string = ' ';
  378. $result = Debugger::exportVar($std);
  379. $expected = <<<TEXT
  380. object(stdClass) id:0 {
  381. int => (int) 2
  382. float => (float) 1.333
  383. string => ' '
  384. }
  385. TEXT;
  386. $this->assertTextEquals($expected, $result);
  387. $Controller = new Controller();
  388. $Controller->viewBuilder()->setHelpers(['Html', 'Form'], false);
  389. $View = $Controller->createView();
  390. $result = Debugger::exportVar($View);
  391. $expected = <<<TEXT
  392. object(Cake\View\View) id:0 {
  393. Html => object(Cake\View\Helper\HtmlHelper) id:1 {}
  394. Form => object(Cake\View\Helper\FormHelper) id:2 {}
  395. [protected] _helpers => object(Cake\View\HelperRegistry) id:3 {}
  396. [protected] Blocks => object(Cake\View\ViewBlock) id:4 {}
  397. [protected] plugin => null
  398. [protected] name => ''
  399. [protected] helpers => [
  400. (int) 0 => 'Html',
  401. (int) 1 => 'Form'
  402. ]
  403. [protected] templatePath => ''
  404. [protected] template => ''
  405. [protected] layout => 'default'
  406. [protected] layoutPath => ''
  407. [protected] autoLayout => true
  408. [protected] viewVars => []
  409. [protected] _ext => '.php'
  410. [protected] subDir => ''
  411. [protected] theme => null
  412. [protected] request => object(Cake\Http\ServerRequest) id:5 {}
  413. [protected] response => object(Cake\Http\Response) id:6 {}
  414. [protected] elementCache => 'default'
  415. [protected] _passedVars => [
  416. (int) 0 => 'viewVars',
  417. (int) 1 => 'autoLayout',
  418. (int) 2 => 'helpers',
  419. (int) 3 => 'template',
  420. (int) 4 => 'layout',
  421. (int) 5 => 'name',
  422. (int) 6 => 'theme',
  423. (int) 7 => 'layoutPath',
  424. (int) 8 => 'templatePath',
  425. (int) 9 => 'plugin'
  426. ]
  427. [protected] _defaultConfig => []
  428. [protected] _paths => []
  429. [protected] _pathsForPlugin => []
  430. [protected] _parents => []
  431. [protected] _current => null
  432. [protected] _currentType => ''
  433. [protected] _stack => []
  434. [protected] _viewBlockClass => 'Cake\View\ViewBlock'
  435. [protected] _eventManager => object(Cake\Event\EventManager) id:7 {}
  436. [protected] _eventClass => 'Cake\Event\Event'
  437. [protected] _config => []
  438. [protected] _configInitialized => true
  439. }
  440. TEXT;
  441. $this->assertTextEquals($expected, $result);
  442. $data = [
  443. 1 => 'Index one',
  444. 5 => 'Index five',
  445. ];
  446. $result = Debugger::exportVar($data);
  447. $expected = <<<TEXT
  448. [
  449. (int) 1 => 'Index one',
  450. (int) 5 => 'Index five'
  451. ]
  452. TEXT;
  453. $this->assertTextEquals($expected, $result);
  454. $data = [
  455. 'key' => [
  456. 'value',
  457. ],
  458. ];
  459. $result = Debugger::exportVar($data, 1);
  460. $expected = <<<TEXT
  461. [
  462. 'key' => [
  463. '' => [maximum depth reached]
  464. ]
  465. ]
  466. TEXT;
  467. $this->assertTextEquals($expected, $result);
  468. $data = false;
  469. $result = Debugger::exportVar($data);
  470. $expected = <<<TEXT
  471. false
  472. TEXT;
  473. $this->assertTextEquals($expected, $result);
  474. $file = fopen('php://output', 'w');
  475. fclose($file);
  476. $result = Debugger::exportVar($file);
  477. $this->assertStringContainsString('(resource (closed)) Resource id #', $result);
  478. }
  479. public function testExportVarTypedProperty(): void
  480. {
  481. $this->skipIf(version_compare(PHP_VERSION, '7.4.0', '<'), 'typed properties require PHP7.4');
  482. // This is gross but was simpler than adding a fixture file.
  483. // phpcs:ignore
  484. eval('class MyClass { private string $field; }');
  485. $obj = new MyClass();
  486. $out = Debugger::exportVar($obj);
  487. $this->assertTextContains('field => [uninitialized]', $out);
  488. }
  489. /**
  490. * Test exporting various kinds of false.
  491. */
  492. public function testExportVarZero(): void
  493. {
  494. $data = [
  495. 'nothing' => '',
  496. 'null' => null,
  497. 'false' => false,
  498. 'szero' => '0',
  499. 'zero' => 0,
  500. ];
  501. $result = Debugger::exportVar($data);
  502. $expected = <<<TEXT
  503. [
  504. 'nothing' => '',
  505. 'null' => null,
  506. 'false' => false,
  507. 'szero' => '0',
  508. 'zero' => (int) 0
  509. ]
  510. TEXT;
  511. $this->assertTextEquals($expected, $result);
  512. }
  513. /**
  514. * test exportVar with cyclic objects.
  515. */
  516. public function testExportVarCyclicRef(): void
  517. {
  518. $parent = new stdClass();
  519. $parent->name = 'cake';
  520. $middle = new stdClass();
  521. $parent->child = $middle;
  522. $middle->name = 'php';
  523. $middle->child = $parent;
  524. $result = Debugger::exportVar($parent, 6);
  525. $expected = <<<TEXT
  526. object(stdClass) id:0 {
  527. name => 'cake'
  528. child => object(stdClass) id:1 {
  529. name => 'php'
  530. child => object(stdClass) id:0 {}
  531. }
  532. }
  533. TEXT;
  534. $this->assertTextEquals($expected, $result);
  535. }
  536. /**
  537. * test exportVar with array objects
  538. */
  539. public function testExportVarSplFixedArray(): void
  540. {
  541. $this->skipIf(
  542. version_compare(PHP_VERSION, '8.3', '>='),
  543. 'Due to different get_object_vars() function behavior used in Debugger::exportObject()' // see. https://3v4l.org/DWpRl
  544. );
  545. $subject = new SplFixedArray(2);
  546. $subject[0] = 'red';
  547. $subject[1] = 'blue';
  548. $result = Debugger::exportVar($subject, 6);
  549. $expected = <<<TEXT
  550. object(SplFixedArray) id:0 {
  551. 0 => 'red'
  552. 1 => 'blue'
  553. }
  554. TEXT;
  555. $this->assertTextEquals($expected, $result);
  556. }
  557. /**
  558. * Tests plain text variable export.
  559. */
  560. public function testExportVarAsPlainText(): void
  561. {
  562. Debugger::configInstance('exportFormatter', null);
  563. $result = Debugger::exportVarAsPlainText(123);
  564. $this->assertSame('(int) 123', $result);
  565. Debugger::configInstance('exportFormatter', ConsoleFormatter::class);
  566. $result = Debugger::exportVarAsPlainText(123);
  567. $this->assertSame('(int) 123', $result);
  568. }
  569. /**
  570. * test exportVar with cyclic objects.
  571. */
  572. public function testExportVarDebugInfo(): void
  573. {
  574. $form = new Form();
  575. $result = Debugger::exportVar($form, 6);
  576. $this->assertStringContainsString("'_schema' => [", $result, 'Has debuginfo keys');
  577. $this->assertStringContainsString("'_validator' => [", $result);
  578. }
  579. /**
  580. * Test exportVar with an exception during __debugInfo()
  581. */
  582. public function testExportVarInvalidDebugInfo(): void
  583. {
  584. $result = Debugger::exportVar(new ThrowsDebugInfo());
  585. $expected = '(unable to export object: from __debugInfo)';
  586. $this->assertTextEquals($expected, $result);
  587. }
  588. /**
  589. * Test exportVar with a mock
  590. */
  591. public function testExportVarMockObject(): void
  592. {
  593. $result = Debugger::exportVar($this->getMockBuilder(Table::class)->getMock());
  594. $this->assertStringContainsString('object(Mock_Table', $result);
  595. }
  596. /**
  597. * Text exportVarAsNodes()
  598. */
  599. public function testExportVarAsNodes(): void
  600. {
  601. $data = [
  602. 1 => 'Index one',
  603. 5 => 'Index five',
  604. ];
  605. $result = Debugger::exportVarAsNodes($data);
  606. $this->assertInstanceOf(NodeInterface::class, $result);
  607. $this->assertCount(2, $result->getChildren());
  608. /** @var \Cake\Error\Debug\ArrayItemNode $item */
  609. $item = $result->getChildren()[0];
  610. $key = new ScalarNode('int', 1);
  611. $this->assertEquals($key, $item->getKey());
  612. $value = new ScalarNode('string', 'Index one');
  613. $this->assertEquals($value, $item->getValue());
  614. $data = [
  615. 'key' => [
  616. 'value',
  617. ],
  618. ];
  619. $result = Debugger::exportVarAsNodes($data, 1);
  620. $item = $result->getChildren()[0];
  621. $nestedItem = $item->getValue()->getChildren()[0];
  622. $expected = new SpecialNode('[maximum depth reached]');
  623. $this->assertEquals($expected, $nestedItem->getValue());
  624. }
  625. /**
  626. * testLog method
  627. */
  628. public function testLog(): void
  629. {
  630. Log::setConfig('test', [
  631. 'className' => 'Array',
  632. ]);
  633. Debugger::log('cool');
  634. Debugger::log(['whatever', 'here']);
  635. $messages = Log::engine('test')->read();
  636. $this->assertCount(2, $messages);
  637. $this->assertStringContainsString('DebuggerTest->testLog', $messages[0]);
  638. $this->assertStringContainsString('cool', $messages[0]);
  639. $this->assertStringContainsString('DebuggerTest->testLog', $messages[1]);
  640. $this->assertStringContainsString('[main]', $messages[1]);
  641. $this->assertStringContainsString("'whatever'", $messages[1]);
  642. $this->assertStringContainsString("'here'", $messages[1]);
  643. Log::drop('test');
  644. }
  645. /**
  646. * Tests that logging does not apply formatting.
  647. */
  648. public function testLogShouldNotApplyFormatting(): void
  649. {
  650. Log::setConfig('test', [
  651. 'className' => 'Array',
  652. ]);
  653. Debugger::configInstance('exportFormatter', null);
  654. Debugger::log(123);
  655. $messages = implode('', Log::engine('test')->read());
  656. Log::engine('test')->clear();
  657. $this->assertStringContainsString('(int) 123', $messages);
  658. $this->assertStringNotContainsString("\033[0m", $messages);
  659. Debugger::configInstance('exportFormatter', HtmlFormatter::class);
  660. Debugger::log(123);
  661. $messages = implode('', Log::engine('test')->read());
  662. Log::engine('test')->clear();
  663. $this->assertStringContainsString('(int) 123', $messages);
  664. $this->assertStringNotContainsString('<style', $messages);
  665. Debugger::configInstance('exportFormatter', ConsoleFormatter::class);
  666. Debugger::log(123);
  667. $messages = implode('', Log::engine('test')->read());
  668. Log::engine('test')->clear();
  669. $this->assertStringContainsString('(int) 123', $messages);
  670. $this->assertStringNotContainsString("\033[0m", $messages);
  671. Log::drop('test');
  672. }
  673. /**
  674. * test log() depth
  675. */
  676. public function testLogDepth(): void
  677. {
  678. Log::setConfig('test', [
  679. 'className' => 'Array',
  680. ]);
  681. $veryRandomName = [
  682. 'test' => ['key' => 'val'],
  683. ];
  684. Debugger::log($veryRandomName, 'debug', 0);
  685. $messages = Log::engine('test')->read();
  686. $this->assertStringContainsString('DebuggerTest->testLogDepth', $messages[0]);
  687. $this->assertStringContainsString('test', $messages[0]);
  688. $this->assertStringNotContainsString('veryRandomName', $messages[0]);
  689. }
  690. /**
  691. * testDump method
  692. */
  693. public function testDump(): void
  694. {
  695. $var = ['People' => [
  696. [
  697. 'name' => 'joeseph',
  698. 'coat' => 'technicolor',
  699. 'hair_color' => 'brown',
  700. ],
  701. [
  702. 'name' => 'Shaft',
  703. 'coat' => 'black',
  704. 'hair' => 'black',
  705. ],
  706. ]];
  707. ob_start();
  708. Debugger::dump($var);
  709. $result = ob_get_clean();
  710. $open = "\n";
  711. $close = "\n\n";
  712. $expected = <<<TEXT
  713. {$open}[
  714. 'People' => [
  715. (int) 0 => [
  716. 'name' => 'joeseph',
  717. 'coat' => 'technicolor',
  718. 'hair_color' => 'brown'
  719. ],
  720. (int) 1 => [
  721. 'name' => 'Shaft',
  722. 'coat' => 'black',
  723. 'hair' => 'black'
  724. ]
  725. ]
  726. ]{$close}
  727. TEXT;
  728. $this->assertTextEquals($expected, $result);
  729. ob_start();
  730. Debugger::dump($var, 1);
  731. $result = ob_get_clean();
  732. $expected = <<<TEXT
  733. {$open}[
  734. 'People' => [
  735. '' => [maximum depth reached]
  736. ]
  737. ]{$close}
  738. TEXT;
  739. $this->assertTextEquals($expected, $result);
  740. }
  741. /**
  742. * test getInstance.
  743. */
  744. public function testGetInstance(): void
  745. {
  746. $result = Debugger::getInstance();
  747. $exporter = $result->getConfig('exportFormatter');
  748. $this->assertInstanceOf(Debugger::class, $result);
  749. $result = Debugger::getInstance(TestDebugger::class);
  750. $this->assertInstanceOf(TestDebugger::class, $result);
  751. $result = Debugger::getInstance();
  752. $this->assertInstanceOf(TestDebugger::class, $result);
  753. $result = Debugger::getInstance(Debugger::class);
  754. $this->assertInstanceOf(Debugger::class, $result);
  755. $result->setConfig('exportFormatter', $exporter);
  756. }
  757. /**
  758. * Test that exportVar() will stop traversing recursive arrays.
  759. */
  760. public function testExportVarRecursion(): void
  761. {
  762. $array = [];
  763. $array['foo'] = &$array;
  764. $output = Debugger::exportVar($array);
  765. $this->assertMatchesRegularExpression("/'foo' => \[\s+'' \=\> \[maximum depth reached\]/", $output);
  766. }
  767. /**
  768. * test trace exclude
  769. */
  770. public function testTraceExclude(): void
  771. {
  772. $result = Debugger::trace();
  773. $this->assertMatchesRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  774. $result = Debugger::trace([
  775. 'exclude' => ['Cake\Test\TestCase\Error\DebuggerTest->testTraceExclude'],
  776. ]);
  777. $this->assertDoesNotMatchRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  778. }
  779. protected function _makeException()
  780. {
  781. return new RuntimeException('testing');
  782. }
  783. /**
  784. * Test stack frame comparisons.
  785. */
  786. public function testGetUniqueFrames()
  787. {
  788. $parent = new RuntimeException('parent');
  789. $child = $this->_makeException();
  790. $result = Debugger::getUniqueFrames($child, $parent);
  791. $this->assertCount(1, $result);
  792. $this->assertEquals(__LINE__ - 4, $result[0]['line']);
  793. $result = Debugger::getUniqueFrames($child, null);
  794. $this->assertGreaterThan(1, count($result));
  795. }
  796. /**
  797. * Tests that __debugInfo is used when available
  798. */
  799. public function testDebugInfo(): void
  800. {
  801. $object = new DebuggableThing();
  802. $result = Debugger::exportVar($object, 2);
  803. $expected = <<<eos
  804. object(TestApp\Error\Thing\DebuggableThing) id:0 {
  805. 'foo' => 'bar'
  806. 'inner' => object(TestApp\Error\Thing\DebuggableThing) id:1 {}
  807. }
  808. eos;
  809. $this->assertSame($expected, $result);
  810. }
  811. /**
  812. * Tests reading the output mask settings.
  813. */
  814. public function testSetOutputMask(): void
  815. {
  816. Debugger::setOutputMask(['password' => '[**********]']);
  817. $this->assertEquals(['password' => '[**********]'], Debugger::outputMask());
  818. Debugger::setOutputMask(['serial' => 'XXXXXX']);
  819. $this->assertEquals(['password' => '[**********]', 'serial' => 'XXXXXX'], Debugger::outputMask());
  820. Debugger::setOutputMask([], false);
  821. $this->assertSame([], Debugger::outputMask());
  822. }
  823. /**
  824. * Test configure based output mask configuration
  825. */
  826. public function testConfigureOutputMask(): void
  827. {
  828. Configure::write('Debugger.outputMask', ['wow' => 'xxx']);
  829. Debugger::getInstance(TestDebugger::class);
  830. Debugger::getInstance(Debugger::class);
  831. $result = Debugger::exportVar(['wow' => 'pass1234']);
  832. $this->assertStringContainsString('xxx', $result);
  833. $this->assertStringNotContainsString('pass1234', $result);
  834. }
  835. /**
  836. * Tests the masking of an array key.
  837. */
  838. public function testMaskArray(): void
  839. {
  840. Debugger::setOutputMask(['password' => '[**********]']);
  841. $result = Debugger::exportVar(['password' => 'pass1234']);
  842. $expected = "['password'=>'[**********]']";
  843. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  844. }
  845. /**
  846. * Tests the masking of an array key.
  847. */
  848. public function testMaskObject(): void
  849. {
  850. Debugger::setOutputMask(['password' => '[**********]']);
  851. $object = new SecurityThing();
  852. $result = Debugger::exportVar($object);
  853. $expected = "object(TestApp\\Error\\Thing\\SecurityThing)id:0{password=>'[**********]'}";
  854. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  855. }
  856. /**
  857. * test testPrintVar()
  858. */
  859. public function testPrintVar(): void
  860. {
  861. ob_start();
  862. Debugger::printVar('this-is-a-test', ['file' => __FILE__, 'line' => __LINE__], false);
  863. $result = ob_get_clean();
  864. $expectedText = <<<EXPECTED
  865. %s (line %d)
  866. ########## DEBUG ##########
  867. 'this-is-a-test'
  868. ###########################
  869. EXPECTED;
  870. $expected = sprintf($expectedText, Debugger::trimPath(__FILE__), __LINE__ - 9);
  871. $this->assertSame($expected, $result);
  872. ob_start();
  873. $value = '<div>this-is-a-test</div>';
  874. Debugger::printVar($value, ['file' => __FILE__, 'line' => __LINE__], true);
  875. $result = ob_get_clean();
  876. $this->assertStringContainsString('&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;', $result);
  877. ob_start();
  878. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], true);
  879. $result = ob_get_clean();
  880. $expected = <<<EXPECTED
  881. <div class="cake-debug-output cake-debug" style="direction:ltr">
  882. <span><strong>%s</strong> (line <strong>%d</strong>)</span>
  883. <div class="cake-dbg"><span class="cake-dbg-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  884. </div>
  885. EXPECTED;
  886. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 8);
  887. $this->assertSame($expected, $result);
  888. ob_start();
  889. Debugger::printVar('<div>this-is-a-test</div>', [], true);
  890. $result = ob_get_clean();
  891. $expected = <<<EXPECTED
  892. <div class="cake-debug-output cake-debug" style="direction:ltr">
  893. <div class="cake-dbg"><span class="cake-dbg-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  894. </div>
  895. EXPECTED;
  896. $this->assertSame($expected, $result);
  897. ob_start();
  898. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], false);
  899. $result = ob_get_clean();
  900. $expected = <<<EXPECTED
  901. %s (line %d)
  902. ########## DEBUG ##########
  903. '<div>this-is-a-test</div>'
  904. ###########################
  905. EXPECTED;
  906. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 9);
  907. $this->assertSame($expected, $result);
  908. ob_start();
  909. Debugger::printVar('<div>this-is-a-test</div>');
  910. $result = ob_get_clean();
  911. $expected = <<<EXPECTED
  912. ########## DEBUG ##########
  913. '<div>this-is-a-test</div>'
  914. ###########################
  915. EXPECTED;
  916. $this->assertSame($expected, $result);
  917. }
  918. /**
  919. * test formatHtmlMessage
  920. */
  921. public function testFormatHtmlMessage(): void
  922. {
  923. $output = Debugger::formatHtmlMessage('Some `code` to `replace`');
  924. $this->assertSame('Some <code>code</code> to <code>replace</code>', $output);
  925. $output = Debugger::formatHtmlMessage("Some `co\nde` to `replace`\nmore");
  926. $this->assertSame("Some <code>co<br />\nde</code> to <code>replace</code><br />\nmore", $output);
  927. $output = Debugger::formatHtmlMessage("Some `code` to <script>alert(\"test\")</script>\nmore");
  928. $this->assertSame(
  929. "Some <code>code</code> to &lt;script&gt;alert(&quot;test&quot;)&lt;/script&gt;<br />\nmore",
  930. $output
  931. );
  932. }
  933. /**
  934. * test adding invalid editor
  935. */
  936. public function testAddEditorInvalid(): void
  937. {
  938. $this->expectException(RuntimeException::class);
  939. Debugger::addEditor('nope', ['invalid']);
  940. }
  941. /**
  942. * test choosing an unknown editor
  943. */
  944. public function testSetEditorInvalid(): void
  945. {
  946. $this->expectException(RuntimeException::class);
  947. Debugger::setEditor('nope');
  948. }
  949. /**
  950. * test choosing a default editor
  951. */
  952. public function testSetEditorPredefined(): void
  953. {
  954. Debugger::setEditor('phpstorm');
  955. Debugger::setEditor('macvim');
  956. Debugger::setEditor('sublime');
  957. Debugger::setEditor('emacs');
  958. // No exceptions raised.
  959. $this->assertTrue(true);
  960. }
  961. /**
  962. * Test configure based editor setup
  963. */
  964. public function testConfigureEditor(): void
  965. {
  966. Configure::write('Debugger.editor', 'emacs');
  967. Debugger::getInstance(TestDebugger::class);
  968. Debugger::getInstance(Debugger::class);
  969. $result = Debugger::editorUrl('file.php', 123);
  970. $this->assertStringContainsString('emacs://', $result);
  971. }
  972. /**
  973. * test using a valid editor.
  974. */
  975. public function testEditorUrlValid(): void
  976. {
  977. Debugger::addEditor('open', 'open://{file}:{line}');
  978. Debugger::setEditor('open');
  979. $this->assertSame('open://test.php:123', Debugger::editorUrl('test.php', 123));
  980. }
  981. /**
  982. * test using a valid editor.
  983. */
  984. public function testEditorUrlClosure(): void
  985. {
  986. Debugger::addEditor('open', function (string $file, int $line) {
  987. return "{$file}/{$line}";
  988. });
  989. Debugger::setEditor('open');
  990. $this->assertSame('test.php/123', Debugger::editorUrl('test.php', 123));
  991. }
  992. }