DebuggerTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  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\Http\ServerRequest;
  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. * testTrimPath method
  122. */
  123. public function testTrimPath(): void
  124. {
  125. $this->assertSame('APP/', Debugger::trimPath(APP));
  126. $this->assertSame('CORE' . DS . 'src' . DS, Debugger::trimPath(CAKE));
  127. $this->assertSame('Some/Other/Path', Debugger::trimPath('Some/Other/Path'));
  128. }
  129. /**
  130. * testExportVar method
  131. */
  132. public function testExportVar(): void
  133. {
  134. $std = new stdClass();
  135. $std->int = 2;
  136. $std->float = 1.333;
  137. $std->string = ' ';
  138. $result = Debugger::exportVar($std);
  139. $expected = <<<TEXT
  140. object(stdClass) id:0 {
  141. int => (int) 2
  142. float => (float) 1.333
  143. string => ' '
  144. }
  145. TEXT;
  146. $this->assertTextEquals($expected, $result);
  147. $Controller = new Controller(new ServerRequest());
  148. $Controller->viewBuilder()->setHelpers(['Html', 'Form']);
  149. $View = $Controller->createView();
  150. $result = Debugger::exportVar($View);
  151. $expected = <<<TEXT
  152. object(Cake\View\View) id:0 {
  153. [protected] _helpers => object(Cake\View\HelperRegistry) id:1 {}
  154. [protected] Blocks => object(Cake\View\ViewBlock) id:2 {}
  155. [protected] plugin => null
  156. [protected] name => ''
  157. [protected] helpers => [
  158. 'Html' => [
  159. '' => [maximum depth reached]
  160. ],
  161. 'Form' => [
  162. '' => [maximum depth reached]
  163. ]
  164. ]
  165. [protected] templatePath => ''
  166. [protected] template => ''
  167. [protected] layout => 'default'
  168. [protected] layoutPath => ''
  169. [protected] autoLayout => true
  170. [protected] viewVars => []
  171. [protected] _ext => '.php'
  172. [protected] subDir => ''
  173. [protected] theme => null
  174. [protected] request => object(Cake\Http\ServerRequest) id:3 {}
  175. [protected] response => object(Cake\Http\Response) id:4 {}
  176. [protected] elementCache => 'default'
  177. [protected] _passedVars => [
  178. (int) 0 => 'viewVars',
  179. (int) 1 => 'autoLayout',
  180. (int) 2 => 'helpers',
  181. (int) 3 => 'template',
  182. (int) 4 => 'layout',
  183. (int) 5 => 'name',
  184. (int) 6 => 'theme',
  185. (int) 7 => 'layoutPath',
  186. (int) 8 => 'templatePath',
  187. (int) 9 => 'plugin'
  188. ]
  189. [protected] _defaultConfig => []
  190. [protected] _paths => []
  191. [protected] _pathsForPlugin => []
  192. [protected] _parents => []
  193. [protected] _current => ''
  194. [protected] _currentType => ''
  195. [protected] _stack => []
  196. [protected] _viewBlockClass => 'Cake\View\ViewBlock'
  197. [protected] _eventManager => object(Cake\Event\EventManager) id:5 {}
  198. [protected] _eventClass => 'Cake\Event\Event'
  199. [protected] _config => []
  200. [protected] _configInitialized => true
  201. }
  202. TEXT;
  203. $this->assertTextEquals($expected, $result);
  204. $data = [
  205. 1 => 'Index one',
  206. 5 => 'Index five',
  207. ];
  208. $result = Debugger::exportVar($data);
  209. $expected = <<<TEXT
  210. [
  211. (int) 1 => 'Index one',
  212. (int) 5 => 'Index five'
  213. ]
  214. TEXT;
  215. $this->assertTextEquals($expected, $result);
  216. $data = [
  217. 'key' => [
  218. 'value',
  219. ],
  220. ];
  221. $result = Debugger::exportVar($data, 1);
  222. $expected = <<<TEXT
  223. [
  224. 'key' => [
  225. '' => [maximum depth reached]
  226. ]
  227. ]
  228. TEXT;
  229. $this->assertTextEquals($expected, $result);
  230. $data = false;
  231. $result = Debugger::exportVar($data);
  232. $expected = <<<TEXT
  233. false
  234. TEXT;
  235. $this->assertTextEquals($expected, $result);
  236. $file = fopen('php://output', 'w');
  237. fclose($file);
  238. $result = Debugger::exportVar($file);
  239. $this->assertStringContainsString('(resource (closed)) Resource id #', $result);
  240. }
  241. public function testExportVarTypedProperty(): void
  242. {
  243. // This is gross but was simpler than adding a fixture file.
  244. // phpcs:ignore
  245. eval('class MyClass { private string $field; }');
  246. /** @phpstan-ignore-next-line */
  247. $obj = new MyClass();
  248. $out = Debugger::exportVar($obj);
  249. $this->assertTextContains('field => [uninitialized]', $out);
  250. }
  251. /**
  252. * Test exporting various kinds of false.
  253. */
  254. public function testExportVarZero(): void
  255. {
  256. $data = [
  257. 'nothing' => '',
  258. 'null' => null,
  259. 'false' => false,
  260. 'szero' => '0',
  261. 'zero' => 0,
  262. ];
  263. $result = Debugger::exportVar($data);
  264. $expected = <<<TEXT
  265. [
  266. 'nothing' => '',
  267. 'null' => null,
  268. 'false' => false,
  269. 'szero' => '0',
  270. 'zero' => (int) 0
  271. ]
  272. TEXT;
  273. $this->assertTextEquals($expected, $result);
  274. }
  275. /**
  276. * test exportVar with cyclic objects.
  277. */
  278. public function testExportVarCyclicRef(): void
  279. {
  280. $parent = new stdClass();
  281. $parent->name = 'cake';
  282. $middle = new stdClass();
  283. $parent->child = $middle;
  284. $middle->name = 'php';
  285. $middle->child = $parent;
  286. $result = Debugger::exportVar($parent, 6);
  287. $expected = <<<TEXT
  288. object(stdClass) id:0 {
  289. name => 'cake'
  290. child => object(stdClass) id:1 {
  291. name => 'php'
  292. child => object(stdClass) id:0 {}
  293. }
  294. }
  295. TEXT;
  296. $this->assertTextEquals($expected, $result);
  297. }
  298. /**
  299. * test exportVar with array objects
  300. */
  301. public function testExportVarSplFixedArray(): void
  302. {
  303. $this->skipIf(
  304. version_compare(PHP_VERSION, '8.3', '>='),
  305. 'Due to different get_object_vars() function behavior used in Debugger::exportObject()' // see. https://3v4l.org/DWpRl
  306. );
  307. $subject = new SplFixedArray(2);
  308. $subject[0] = 'red';
  309. $subject[1] = 'blue';
  310. $result = Debugger::exportVar($subject, 6);
  311. $this->assertStringContainsString('object(SplFixedArray) id:0 {', $result);
  312. }
  313. /**
  314. * Tests plain text variable export.
  315. */
  316. public function testExportVarAsPlainText(): void
  317. {
  318. Debugger::configInstance('exportFormatter', null);
  319. $result = Debugger::exportVarAsPlainText(123);
  320. $this->assertSame('(int) 123', $result);
  321. Debugger::configInstance('exportFormatter', ConsoleFormatter::class);
  322. $result = Debugger::exportVarAsPlainText(123);
  323. $this->assertSame('(int) 123', $result);
  324. }
  325. /**
  326. * test exportVar with cyclic objects.
  327. */
  328. public function testExportVarDebugInfo(): void
  329. {
  330. $form = new Form();
  331. $result = Debugger::exportVar($form, 6);
  332. $this->assertStringContainsString("'_schema' => [", $result, 'Has debuginfo keys');
  333. $this->assertStringContainsString("'_validator' => [", $result);
  334. }
  335. /**
  336. * Test exportVar with an exception during __debugInfo()
  337. */
  338. public function testExportVarInvalidDebugInfo(): void
  339. {
  340. $this->skipIf(extension_loaded('xdebug'), 'Throwing exceptions inside __debugInfo breaks with xDebug');
  341. $result = Debugger::exportVar(new ThrowsDebugInfo());
  342. $expected = '(unable to export object: from __debugInfo)';
  343. $this->assertTextEquals($expected, $result);
  344. }
  345. /**
  346. * Test exportVar with a mock
  347. */
  348. public function testExportVarMockObject(): void
  349. {
  350. $result = Debugger::exportVar($this->getMockBuilder(Table::class)->getMock());
  351. $this->assertStringStartsWith('object(Mock', $result);
  352. $this->assertStringContainsString('_Table_', $result);
  353. }
  354. /**
  355. * Text exportVarAsNodes()
  356. */
  357. public function testExportVarAsNodes(): void
  358. {
  359. $data = [
  360. 1 => 'Index one',
  361. 5 => 'Index five',
  362. ];
  363. $result = Debugger::exportVarAsNodes($data);
  364. $this->assertInstanceOf(NodeInterface::class, $result);
  365. $this->assertCount(2, $result->getChildren());
  366. /** @var \Cake\Error\Debug\ArrayItemNode $item */
  367. $item = $result->getChildren()[0];
  368. $key = new ScalarNode('int', 1);
  369. $this->assertEquals($key, $item->getKey());
  370. $value = new ScalarNode('string', 'Index one');
  371. $this->assertEquals($value, $item->getValue());
  372. $data = [
  373. 'key' => [
  374. 'value',
  375. ],
  376. ];
  377. $result = Debugger::exportVarAsNodes($data, 1);
  378. $item = $result->getChildren()[0];
  379. $nestedItem = $item->getValue()->getChildren()[0];
  380. $expected = new SpecialNode('[maximum depth reached]');
  381. $this->assertEquals($expected, $nestedItem->getValue());
  382. }
  383. /**
  384. * testLog method
  385. */
  386. public function testLog(): void
  387. {
  388. Log::setConfig('test', [
  389. 'className' => 'Array',
  390. ]);
  391. Debugger::log('cool');
  392. Debugger::log(['whatever', 'here']);
  393. $messages = Log::engine('test')->read();
  394. $this->assertCount(2, $messages);
  395. $this->assertStringContainsString('DebuggerTest->testLog', $messages[0]);
  396. $this->assertStringContainsString('cool', $messages[0]);
  397. $this->assertStringContainsString('DebuggerTest->testLog', $messages[1]);
  398. $this->assertStringContainsString('[main]', $messages[1]);
  399. $this->assertStringContainsString("'whatever'", $messages[1]);
  400. $this->assertStringContainsString("'here'", $messages[1]);
  401. Log::drop('test');
  402. }
  403. /**
  404. * Tests that logging does not apply formatting.
  405. */
  406. public function testLogShouldNotApplyFormatting(): void
  407. {
  408. Log::setConfig('test', [
  409. 'className' => 'Array',
  410. ]);
  411. Debugger::configInstance('exportFormatter', null);
  412. Debugger::log(123);
  413. $messages = implode('', Log::engine('test')->read());
  414. Log::engine('test')->clear();
  415. $this->assertStringContainsString('(int) 123', $messages);
  416. $this->assertStringNotContainsString("\033[0m", $messages);
  417. Debugger::configInstance('exportFormatter', HtmlFormatter::class);
  418. Debugger::log(123);
  419. $messages = implode('', Log::engine('test')->read());
  420. Log::engine('test')->clear();
  421. $this->assertStringContainsString('(int) 123', $messages);
  422. $this->assertStringNotContainsString('<style', $messages);
  423. Debugger::configInstance('exportFormatter', ConsoleFormatter::class);
  424. Debugger::log(123);
  425. $messages = implode('', Log::engine('test')->read());
  426. Log::engine('test')->clear();
  427. $this->assertStringContainsString('(int) 123', $messages);
  428. $this->assertStringNotContainsString("\033[0m", $messages);
  429. Log::drop('test');
  430. }
  431. /**
  432. * test log() depth
  433. */
  434. public function testLogDepth(): void
  435. {
  436. Log::setConfig('test', [
  437. 'className' => 'Array',
  438. ]);
  439. $veryRandomName = [
  440. 'test' => ['key' => 'val'],
  441. ];
  442. Debugger::log($veryRandomName, 'debug', 0);
  443. $messages = Log::engine('test')->read();
  444. $this->assertStringContainsString('DebuggerTest->testLogDepth', $messages[0]);
  445. $this->assertStringContainsString('test', $messages[0]);
  446. $this->assertStringNotContainsString('veryRandomName', $messages[0]);
  447. }
  448. /**
  449. * testDump method
  450. */
  451. public function testDump(): void
  452. {
  453. $var = ['People' => [
  454. [
  455. 'name' => 'joeseph',
  456. 'coat' => 'technicolor',
  457. 'hair_color' => 'brown',
  458. ],
  459. [
  460. 'name' => 'Shaft',
  461. 'coat' => 'black',
  462. 'hair' => 'black',
  463. ],
  464. ]];
  465. ob_start();
  466. Debugger::dump($var);
  467. $result = ob_get_clean();
  468. $open = "\n";
  469. $close = "\n\n";
  470. $expected = <<<TEXT
  471. {$open}[
  472. 'People' => [
  473. (int) 0 => [
  474. 'name' => 'joeseph',
  475. 'coat' => 'technicolor',
  476. 'hair_color' => 'brown'
  477. ],
  478. (int) 1 => [
  479. 'name' => 'Shaft',
  480. 'coat' => 'black',
  481. 'hair' => 'black'
  482. ]
  483. ]
  484. ]{$close}
  485. TEXT;
  486. $this->assertTextEquals($expected, $result);
  487. ob_start();
  488. Debugger::dump($var, 1);
  489. $result = ob_get_clean();
  490. $expected = <<<TEXT
  491. {$open}[
  492. 'People' => [
  493. '' => [maximum depth reached]
  494. ]
  495. ]{$close}
  496. TEXT;
  497. $this->assertTextEquals($expected, $result);
  498. }
  499. /**
  500. * test getInstance.
  501. */
  502. public function testGetInstance(): void
  503. {
  504. $result = Debugger::getInstance();
  505. $exporter = $result->getConfig('exportFormatter');
  506. $this->assertInstanceOf(Debugger::class, $result);
  507. $result = Debugger::getInstance(TestDebugger::class);
  508. $this->assertInstanceOf(TestDebugger::class, $result);
  509. $result = Debugger::getInstance();
  510. $this->assertInstanceOf(TestDebugger::class, $result);
  511. $result = Debugger::getInstance(Debugger::class);
  512. $this->assertInstanceOf(Debugger::class, $result);
  513. $result->setConfig('exportFormatter', $exporter);
  514. }
  515. /**
  516. * Test that exportVar() will stop traversing recursive arrays.
  517. */
  518. public function testExportVarRecursion(): void
  519. {
  520. $array = [];
  521. $array['foo'] = &$array;
  522. $output = Debugger::exportVar($array);
  523. $this->assertMatchesRegularExpression("/'foo' => \[\s+'' \=\> \[maximum depth reached\]/", $output);
  524. }
  525. /**
  526. * test trace exclude
  527. */
  528. public function testTraceExclude(): void
  529. {
  530. $result = Debugger::trace();
  531. $this->assertMatchesRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  532. $result = Debugger::trace([
  533. 'exclude' => ['Cake\Test\TestCase\Error\DebuggerTest->testTraceExclude'],
  534. ]);
  535. $this->assertDoesNotMatchRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  536. }
  537. public function testTraceShortPoints(): void
  538. {
  539. $result = Debugger::trace(['format' => 'shortPoints']);
  540. $this->assertIsArray($result);
  541. $this->assertEquals(
  542. 'CORE' . DS . 'vendor' . DS . 'phpunit' . DS . 'phpunit' . DS . 'src' . DS .
  543. 'Framework' . DS . 'TestCase.php',
  544. $result[0]['file']
  545. );
  546. }
  547. protected function _makeException(): RuntimeException
  548. {
  549. return new RuntimeException('testing');
  550. }
  551. /**
  552. * Test stack frame comparisons.
  553. */
  554. public function testGetUniqueFrames(): void
  555. {
  556. $parent = new RuntimeException('parent');
  557. $child = $this->_makeException();
  558. $result = Debugger::getUniqueFrames($child, $parent);
  559. $this->assertCount(1, $result);
  560. $this->assertEquals(__LINE__ - 4, $result[0]['line']);
  561. $result = Debugger::getUniqueFrames($child, null);
  562. $this->assertGreaterThan(1, count($result));
  563. }
  564. /**
  565. * Tests that __debugInfo is used when available
  566. */
  567. public function testDebugInfo(): void
  568. {
  569. $object = new DebuggableThing();
  570. $result = Debugger::exportVar($object, 2);
  571. $expected = <<<eos
  572. object(TestApp\Error\Thing\DebuggableThing) id:0 {
  573. 'foo' => 'bar'
  574. 'inner' => object(TestApp\Error\Thing\DebuggableThing) id:1 {}
  575. }
  576. eos;
  577. $this->assertSame($expected, $result);
  578. }
  579. /**
  580. * Tests reading the output mask settings.
  581. */
  582. public function testSetOutputMask(): void
  583. {
  584. Debugger::setOutputMask(['password' => '[**********]']);
  585. $this->assertEquals(['password' => '[**********]'], Debugger::outputMask());
  586. Debugger::setOutputMask(['serial' => 'XXXXXX']);
  587. $this->assertEquals(['password' => '[**********]', 'serial' => 'XXXXXX'], Debugger::outputMask());
  588. Debugger::setOutputMask([], false);
  589. $this->assertSame([], Debugger::outputMask());
  590. }
  591. /**
  592. * Test configure based output mask configuration
  593. */
  594. public function testConfigureOutputMask(): void
  595. {
  596. Configure::write('Debugger.outputMask', ['wow' => 'xxx']);
  597. Debugger::getInstance(TestDebugger::class);
  598. Debugger::getInstance(Debugger::class);
  599. $result = Debugger::exportVar(['wow' => 'pass1234']);
  600. $this->assertStringContainsString('xxx', $result);
  601. $this->assertStringNotContainsString('pass1234', $result);
  602. }
  603. /**
  604. * Tests the masking of an array key.
  605. */
  606. public function testMaskArray(): void
  607. {
  608. Debugger::setOutputMask(['password' => '[**********]']);
  609. $result = Debugger::exportVar(['password' => 'pass1234']);
  610. $expected = "['password'=>'[**********]']";
  611. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  612. }
  613. /**
  614. * Tests the masking of an array key.
  615. */
  616. public function testMaskObject(): void
  617. {
  618. Debugger::setOutputMask(['password' => '[**********]']);
  619. $object = new SecurityThing();
  620. $result = Debugger::exportVar($object);
  621. $expected = "object(TestApp\\Error\\Thing\\SecurityThing)id:0{password=>'[**********]'}";
  622. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  623. }
  624. /**
  625. * test testPrintVar()
  626. */
  627. public function testPrintVar(): void
  628. {
  629. ob_start();
  630. Debugger::printVar('this-is-a-test', ['file' => __FILE__, 'line' => __LINE__], false);
  631. $result = ob_get_clean();
  632. $expectedText = <<<EXPECTED
  633. %s (line %d)
  634. ########## DEBUG ##########
  635. 'this-is-a-test'
  636. ###########################
  637. EXPECTED;
  638. $expected = sprintf($expectedText, Debugger::trimPath(__FILE__), __LINE__ - 9);
  639. $this->assertSame($expected, $result);
  640. ob_start();
  641. $value = '<div>this-is-a-test</div>';
  642. Debugger::printVar($value, ['file' => __FILE__, 'line' => __LINE__], true);
  643. $result = ob_get_clean();
  644. $this->assertStringContainsString('&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;', $result);
  645. ob_start();
  646. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], true);
  647. $result = ob_get_clean();
  648. $expected = <<<EXPECTED
  649. <div class="cake-debug-output cake-debug" style="direction:ltr">
  650. <span><strong>%s</strong> (line <strong>%d</strong>)</span>
  651. <div class="cake-debug"><span class="cake-debug-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  652. </div>
  653. EXPECTED;
  654. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 8);
  655. $this->assertSame($expected, $result);
  656. ob_start();
  657. Debugger::printVar('<div>this-is-a-test</div>', [], true);
  658. $result = ob_get_clean();
  659. $expected = <<<EXPECTED
  660. <div class="cake-debug-output cake-debug" style="direction:ltr">
  661. <div class="cake-debug"><span class="cake-debug-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  662. </div>
  663. EXPECTED;
  664. $this->assertSame($expected, $result);
  665. ob_start();
  666. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], false);
  667. $result = ob_get_clean();
  668. $expected = <<<EXPECTED
  669. %s (line %d)
  670. ########## DEBUG ##########
  671. '<div>this-is-a-test</div>'
  672. ###########################
  673. EXPECTED;
  674. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 9);
  675. $this->assertSame($expected, $result);
  676. ob_start();
  677. Debugger::printVar('<div>this-is-a-test</div>');
  678. $result = ob_get_clean();
  679. $expected = <<<EXPECTED
  680. ########## DEBUG ##########
  681. '<div>this-is-a-test</div>'
  682. ###########################
  683. EXPECTED;
  684. $this->assertSame($expected, $result);
  685. }
  686. /**
  687. * test formatHtmlMessage
  688. */
  689. public function testFormatHtmlMessage(): void
  690. {
  691. $output = Debugger::formatHtmlMessage('Some `code` to `replace`');
  692. $this->assertSame('Some <code>`code`</code> to <code>`replace`</code>', $output);
  693. $output = Debugger::formatHtmlMessage("Some `co\nde` to `replace`\nmore");
  694. $this->assertSame("Some <code>`co<br />\nde`</code> to <code>`replace`</code><br />\nmore", $output);
  695. $output = Debugger::formatHtmlMessage("Some `code` to <script>alert(\"test\")</script>\nmore");
  696. $this->assertSame(
  697. "Some <code>`code`</code> to &lt;script&gt;alert(&quot;test&quot;)&lt;/script&gt;<br />\nmore",
  698. $output
  699. );
  700. }
  701. /**
  702. * test choosing an unknown editor
  703. */
  704. public function testSetEditorInvalid(): void
  705. {
  706. $this->expectException(InvalidArgumentException::class);
  707. Debugger::setEditor('nope');
  708. }
  709. /**
  710. * test choosing a default editor
  711. */
  712. public function testSetEditorPredefined(): void
  713. {
  714. Debugger::setEditor('phpstorm');
  715. Debugger::setEditor('macvim');
  716. Debugger::setEditor('sublime');
  717. Debugger::setEditor('emacs');
  718. // No exceptions raised.
  719. $this->assertTrue(true);
  720. }
  721. /**
  722. * Test configure based editor setup
  723. */
  724. public function testConfigureEditor(): void
  725. {
  726. Configure::write('Debugger.editor', 'emacs');
  727. Debugger::getInstance(TestDebugger::class);
  728. Debugger::getInstance(Debugger::class);
  729. $result = Debugger::editorUrl('file.php', 123);
  730. $this->assertStringContainsString('emacs://', $result);
  731. }
  732. /**
  733. * test using a valid editor.
  734. */
  735. public function testEditorUrlValid(): void
  736. {
  737. Debugger::addEditor('open', 'open://{file}:{line}');
  738. Debugger::setEditor('open');
  739. $this->assertSame('open://test.php:123', Debugger::editorUrl('test.php', 123));
  740. }
  741. /**
  742. * test using a valid editor.
  743. */
  744. public function testEditorUrlClosure(): void
  745. {
  746. Debugger::addEditor('open', function (string $file, int $line) {
  747. return "{$file}/{$line}";
  748. });
  749. Debugger::setEditor('open');
  750. $this->assertSame('test.php/123', Debugger::editorUrl('test.php', 123));
  751. }
  752. }