DebuggerTest.php 25 KB

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