DebuggerTest.php 33 KB

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