DebuggerTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  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. $this->assertStringContainsString('object(SplFixedArray) id:0 {', $result);
  305. }
  306. /**
  307. * Tests plain text variable export.
  308. */
  309. public function testExportVarAsPlainText(): void
  310. {
  311. Debugger::configInstance('exportFormatter', null);
  312. $result = Debugger::exportVarAsPlainText(123);
  313. $this->assertSame('(int) 123', $result);
  314. Debugger::configInstance('exportFormatter', ConsoleFormatter::class);
  315. $result = Debugger::exportVarAsPlainText(123);
  316. $this->assertSame('(int) 123', $result);
  317. }
  318. /**
  319. * test exportVar with cyclic objects.
  320. */
  321. public function testExportVarDebugInfo(): void
  322. {
  323. $form = new Form();
  324. $result = Debugger::exportVar($form, 6);
  325. $this->assertStringContainsString("'_schema' => [", $result, 'Has debuginfo keys');
  326. $this->assertStringContainsString("'_validator' => [", $result);
  327. }
  328. /**
  329. * Test exportVar with an exception during __debugInfo()
  330. */
  331. public function testExportVarInvalidDebugInfo(): void
  332. {
  333. $this->skipIf(extension_loaded('xdebug'), 'Throwing exceptions inside __debugInfo breaks with xDebug');
  334. $result = Debugger::exportVar(new ThrowsDebugInfo());
  335. $expected = '(unable to export object: from __debugInfo)';
  336. $this->assertTextEquals($expected, $result);
  337. }
  338. /**
  339. * Test exportVar with a mock
  340. */
  341. public function testExportVarMockObject(): void
  342. {
  343. $result = Debugger::exportVar($this->getMockBuilder(Table::class)->getMock());
  344. $this->assertStringContainsString('object(Mock_Table', $result);
  345. }
  346. /**
  347. * Text exportVarAsNodes()
  348. */
  349. public function testExportVarAsNodes(): void
  350. {
  351. $data = [
  352. 1 => 'Index one',
  353. 5 => 'Index five',
  354. ];
  355. $result = Debugger::exportVarAsNodes($data);
  356. $this->assertInstanceOf(NodeInterface::class, $result);
  357. $this->assertCount(2, $result->getChildren());
  358. /** @var \Cake\Error\Debug\ArrayItemNode $item */
  359. $item = $result->getChildren()[0];
  360. $key = new ScalarNode('int', 1);
  361. $this->assertEquals($key, $item->getKey());
  362. $value = new ScalarNode('string', 'Index one');
  363. $this->assertEquals($value, $item->getValue());
  364. $data = [
  365. 'key' => [
  366. 'value',
  367. ],
  368. ];
  369. $result = Debugger::exportVarAsNodes($data, 1);
  370. $item = $result->getChildren()[0];
  371. $nestedItem = $item->getValue()->getChildren()[0];
  372. $expected = new SpecialNode('[maximum depth reached]');
  373. $this->assertEquals($expected, $nestedItem->getValue());
  374. }
  375. /**
  376. * testLog method
  377. */
  378. public function testLog(): void
  379. {
  380. Log::setConfig('test', [
  381. 'className' => 'Array',
  382. ]);
  383. Debugger::log('cool');
  384. Debugger::log(['whatever', 'here']);
  385. $messages = Log::engine('test')->read();
  386. $this->assertCount(2, $messages);
  387. $this->assertStringContainsString('DebuggerTest->testLog', $messages[0]);
  388. $this->assertStringContainsString('cool', $messages[0]);
  389. $this->assertStringContainsString('DebuggerTest->testLog', $messages[1]);
  390. $this->assertStringContainsString('[main]', $messages[1]);
  391. $this->assertStringContainsString("'whatever'", $messages[1]);
  392. $this->assertStringContainsString("'here'", $messages[1]);
  393. Log::drop('test');
  394. }
  395. /**
  396. * Tests that logging does not apply formatting.
  397. */
  398. public function testLogShouldNotApplyFormatting(): void
  399. {
  400. Log::setConfig('test', [
  401. 'className' => 'Array',
  402. ]);
  403. Debugger::configInstance('exportFormatter', null);
  404. Debugger::log(123);
  405. $messages = implode('', Log::engine('test')->read());
  406. Log::engine('test')->clear();
  407. $this->assertStringContainsString('(int) 123', $messages);
  408. $this->assertStringNotContainsString("\033[0m", $messages);
  409. Debugger::configInstance('exportFormatter', HtmlFormatter::class);
  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('<style', $messages);
  415. Debugger::configInstance('exportFormatter', ConsoleFormatter::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("\033[0m", $messages);
  421. Log::drop('test');
  422. }
  423. /**
  424. * test log() depth
  425. */
  426. public function testLogDepth(): void
  427. {
  428. Log::setConfig('test', [
  429. 'className' => 'Array',
  430. ]);
  431. $veryRandomName = [
  432. 'test' => ['key' => 'val'],
  433. ];
  434. Debugger::log($veryRandomName, 'debug', 0);
  435. $messages = Log::engine('test')->read();
  436. $this->assertStringContainsString('DebuggerTest->testLogDepth', $messages[0]);
  437. $this->assertStringContainsString('test', $messages[0]);
  438. $this->assertStringNotContainsString('veryRandomName', $messages[0]);
  439. }
  440. /**
  441. * testDump method
  442. */
  443. public function testDump(): void
  444. {
  445. $var = ['People' => [
  446. [
  447. 'name' => 'joeseph',
  448. 'coat' => 'technicolor',
  449. 'hair_color' => 'brown',
  450. ],
  451. [
  452. 'name' => 'Shaft',
  453. 'coat' => 'black',
  454. 'hair' => 'black',
  455. ],
  456. ]];
  457. ob_start();
  458. Debugger::dump($var);
  459. $result = ob_get_clean();
  460. $open = "\n";
  461. $close = "\n\n";
  462. $expected = <<<TEXT
  463. {$open}[
  464. 'People' => [
  465. (int) 0 => [
  466. 'name' => 'joeseph',
  467. 'coat' => 'technicolor',
  468. 'hair_color' => 'brown'
  469. ],
  470. (int) 1 => [
  471. 'name' => 'Shaft',
  472. 'coat' => 'black',
  473. 'hair' => 'black'
  474. ]
  475. ]
  476. ]{$close}
  477. TEXT;
  478. $this->assertTextEquals($expected, $result);
  479. ob_start();
  480. Debugger::dump($var, 1);
  481. $result = ob_get_clean();
  482. $expected = <<<TEXT
  483. {$open}[
  484. 'People' => [
  485. '' => [maximum depth reached]
  486. ]
  487. ]{$close}
  488. TEXT;
  489. $this->assertTextEquals($expected, $result);
  490. }
  491. /**
  492. * test getInstance.
  493. */
  494. public function testGetInstance(): void
  495. {
  496. $result = Debugger::getInstance();
  497. $exporter = $result->getConfig('exportFormatter');
  498. $this->assertInstanceOf(Debugger::class, $result);
  499. $result = Debugger::getInstance(TestDebugger::class);
  500. $this->assertInstanceOf(TestDebugger::class, $result);
  501. $result = Debugger::getInstance();
  502. $this->assertInstanceOf(TestDebugger::class, $result);
  503. $result = Debugger::getInstance(Debugger::class);
  504. $this->assertInstanceOf(Debugger::class, $result);
  505. $result->setConfig('exportFormatter', $exporter);
  506. }
  507. /**
  508. * Test that exportVar() will stop traversing recursive arrays.
  509. */
  510. public function testExportVarRecursion(): void
  511. {
  512. $array = [];
  513. $array['foo'] = &$array;
  514. $output = Debugger::exportVar($array);
  515. $this->assertMatchesRegularExpression("/'foo' => \[\s+'' \=\> \[maximum depth reached\]/", $output);
  516. }
  517. /**
  518. * test trace exclude
  519. */
  520. public function testTraceExclude(): void
  521. {
  522. $result = Debugger::trace();
  523. $this->assertMatchesRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  524. $result = Debugger::trace([
  525. 'exclude' => ['Cake\Test\TestCase\Error\DebuggerTest->testTraceExclude'],
  526. ]);
  527. $this->assertDoesNotMatchRegularExpression('/^Cake\\\Test\\\TestCase\\\Error\\\DebuggerTest..testTraceExclude/m', $result);
  528. }
  529. protected function _makeException()
  530. {
  531. return new RuntimeException('testing');
  532. }
  533. /**
  534. * Test stack frame comparisons.
  535. */
  536. public function testGetUniqueFrames()
  537. {
  538. $parent = new RuntimeException('parent');
  539. $child = $this->_makeException();
  540. $result = Debugger::getUniqueFrames($child, $parent);
  541. $this->assertCount(1, $result);
  542. $this->assertEquals(__LINE__ - 4, $result[0]['line']);
  543. $result = Debugger::getUniqueFrames($child, null);
  544. $this->assertGreaterThan(1, count($result));
  545. }
  546. /**
  547. * Tests that __debugInfo is used when available
  548. */
  549. public function testDebugInfo(): void
  550. {
  551. $object = new DebuggableThing();
  552. $result = Debugger::exportVar($object, 2);
  553. $expected = <<<eos
  554. object(TestApp\Error\Thing\DebuggableThing) id:0 {
  555. 'foo' => 'bar'
  556. 'inner' => object(TestApp\Error\Thing\DebuggableThing) id:1 {}
  557. }
  558. eos;
  559. $this->assertSame($expected, $result);
  560. }
  561. /**
  562. * Tests reading the output mask settings.
  563. */
  564. public function testSetOutputMask(): void
  565. {
  566. Debugger::setOutputMask(['password' => '[**********]']);
  567. $this->assertEquals(['password' => '[**********]'], Debugger::outputMask());
  568. Debugger::setOutputMask(['serial' => 'XXXXXX']);
  569. $this->assertEquals(['password' => '[**********]', 'serial' => 'XXXXXX'], Debugger::outputMask());
  570. Debugger::setOutputMask([], false);
  571. $this->assertSame([], Debugger::outputMask());
  572. }
  573. /**
  574. * Test configure based output mask configuration
  575. */
  576. public function testConfigureOutputMask(): void
  577. {
  578. Configure::write('Debugger.outputMask', ['wow' => 'xxx']);
  579. Debugger::getInstance(TestDebugger::class);
  580. Debugger::getInstance(Debugger::class);
  581. $result = Debugger::exportVar(['wow' => 'pass1234']);
  582. $this->assertStringContainsString('xxx', $result);
  583. $this->assertStringNotContainsString('pass1234', $result);
  584. }
  585. /**
  586. * Tests the masking of an array key.
  587. */
  588. public function testMaskArray(): void
  589. {
  590. Debugger::setOutputMask(['password' => '[**********]']);
  591. $result = Debugger::exportVar(['password' => 'pass1234']);
  592. $expected = "['password'=>'[**********]']";
  593. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  594. }
  595. /**
  596. * Tests the masking of an array key.
  597. */
  598. public function testMaskObject(): void
  599. {
  600. Debugger::setOutputMask(['password' => '[**********]']);
  601. $object = new SecurityThing();
  602. $result = Debugger::exportVar($object);
  603. $expected = "object(TestApp\\Error\\Thing\\SecurityThing)id:0{password=>'[**********]'}";
  604. $this->assertSame($expected, preg_replace('/\s+/', '', $result));
  605. }
  606. /**
  607. * test testPrintVar()
  608. */
  609. public function testPrintVar(): void
  610. {
  611. ob_start();
  612. Debugger::printVar('this-is-a-test', ['file' => __FILE__, 'line' => __LINE__], false);
  613. $result = ob_get_clean();
  614. $expectedText = <<<EXPECTED
  615. %s (line %d)
  616. ########## DEBUG ##########
  617. 'this-is-a-test'
  618. ###########################
  619. EXPECTED;
  620. $expected = sprintf($expectedText, Debugger::trimPath(__FILE__), __LINE__ - 9);
  621. $this->assertSame($expected, $result);
  622. ob_start();
  623. $value = '<div>this-is-a-test</div>';
  624. Debugger::printVar($value, ['file' => __FILE__, 'line' => __LINE__], true);
  625. $result = ob_get_clean();
  626. $this->assertStringContainsString('&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;', $result);
  627. ob_start();
  628. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], true);
  629. $result = ob_get_clean();
  630. $expected = <<<EXPECTED
  631. <div class="cake-debug-output cake-debug" style="direction:ltr">
  632. <span><strong>%s</strong> (line <strong>%d</strong>)</span>
  633. <div class="cake-debug"><span class="cake-debug-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  634. </div>
  635. EXPECTED;
  636. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 8);
  637. $this->assertSame($expected, $result);
  638. ob_start();
  639. Debugger::printVar('<div>this-is-a-test</div>', [], true);
  640. $result = ob_get_clean();
  641. $expected = <<<EXPECTED
  642. <div class="cake-debug-output cake-debug" style="direction:ltr">
  643. <div class="cake-debug"><span class="cake-debug-string">&#039;&lt;div&gt;this-is-a-test&lt;/div&gt;&#039;</span></div>
  644. </div>
  645. EXPECTED;
  646. $this->assertSame($expected, $result);
  647. ob_start();
  648. Debugger::printVar('<div>this-is-a-test</div>', ['file' => __FILE__, 'line' => __LINE__], false);
  649. $result = ob_get_clean();
  650. $expected = <<<EXPECTED
  651. %s (line %d)
  652. ########## DEBUG ##########
  653. '<div>this-is-a-test</div>'
  654. ###########################
  655. EXPECTED;
  656. $expected = sprintf($expected, Debugger::trimPath(__FILE__), __LINE__ - 9);
  657. $this->assertSame($expected, $result);
  658. ob_start();
  659. Debugger::printVar('<div>this-is-a-test</div>');
  660. $result = ob_get_clean();
  661. $expected = <<<EXPECTED
  662. ########## DEBUG ##########
  663. '<div>this-is-a-test</div>'
  664. ###########################
  665. EXPECTED;
  666. $this->assertSame($expected, $result);
  667. }
  668. /**
  669. * test formatHtmlMessage
  670. */
  671. public function testFormatHtmlMessage(): void
  672. {
  673. $output = Debugger::formatHtmlMessage('Some `code` to `replace`');
  674. $this->assertSame('Some <code>`code`</code> to <code>`replace`</code>', $output);
  675. $output = Debugger::formatHtmlMessage("Some `co\nde` to `replace`\nmore");
  676. $this->assertSame("Some <code>`co<br />\nde`</code> to <code>`replace`</code><br />\nmore", $output);
  677. $output = Debugger::formatHtmlMessage("Some `code` to <script>alert(\"test\")</script>\nmore");
  678. $this->assertSame(
  679. "Some <code>`code`</code> to &lt;script&gt;alert(&quot;test&quot;)&lt;/script&gt;<br />\nmore",
  680. $output
  681. );
  682. }
  683. /**
  684. * test choosing an unknown editor
  685. */
  686. public function testSetEditorInvalid(): void
  687. {
  688. $this->expectException(InvalidArgumentException::class);
  689. Debugger::setEditor('nope');
  690. }
  691. /**
  692. * test choosing a default editor
  693. */
  694. public function testSetEditorPredefined(): void
  695. {
  696. Debugger::setEditor('phpstorm');
  697. Debugger::setEditor('macvim');
  698. Debugger::setEditor('sublime');
  699. Debugger::setEditor('emacs');
  700. // No exceptions raised.
  701. $this->assertTrue(true);
  702. }
  703. /**
  704. * Test configure based editor setup
  705. */
  706. public function testConfigureEditor(): void
  707. {
  708. Configure::write('Debugger.editor', 'emacs');
  709. Debugger::getInstance(TestDebugger::class);
  710. Debugger::getInstance(Debugger::class);
  711. $result = Debugger::editorUrl('file.php', 123);
  712. $this->assertStringContainsString('emacs://', $result);
  713. }
  714. /**
  715. * test using a valid editor.
  716. */
  717. public function testEditorUrlValid(): void
  718. {
  719. Debugger::addEditor('open', 'open://{file}:{line}');
  720. Debugger::setEditor('open');
  721. $this->assertSame('open://test.php:123', Debugger::editorUrl('test.php', 123));
  722. }
  723. /**
  724. * test using a valid editor.
  725. */
  726. public function testEditorUrlClosure(): void
  727. {
  728. Debugger::addEditor('open', function (string $file, int $line) {
  729. return "{$file}/{$line}";
  730. });
  731. Debugger::setEditor('open');
  732. $this->assertSame('test.php/123', Debugger::editorUrl('test.php', 123));
  733. }
  734. }