PageRenderTime 66ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/TestCase/Error/DebuggerTest.php

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