ShellTest.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. <?php
  2. /*
  3. * This file is part of Psy Shell.
  4. *
  5. * (c) 2012-2023 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Psy\Test;
  11. use Psy\Configuration;
  12. use Psy\Exception\BreakException;
  13. use Psy\Exception\ParseErrorException;
  14. use Psy\Shell;
  15. use Psy\TabCompletion\Matcher\ClassMethodsMatcher;
  16. use Symfony\Component\Console\Application;
  17. use Symfony\Component\Console\Input\StringInput;
  18. use Symfony\Component\Console\Output\StreamOutput;
  19. class ShellTest extends TestCase
  20. {
  21. private $streams = [];
  22. /**
  23. * @after
  24. */
  25. public function closeOpenStreams()
  26. {
  27. foreach ($this->streams as $stream) {
  28. \fclose($stream);
  29. }
  30. }
  31. public function testScopeVariables()
  32. {
  33. $one = 'banana';
  34. $two = 123;
  35. $three = new \stdClass();
  36. $__psysh__ = 'ignore this';
  37. $_ = 'ignore this';
  38. $_e = 'ignore this';
  39. $shell = new Shell($this->getConfig());
  40. $shell->setScopeVariables(\compact('one', 'two', 'three', '__psysh__', '_', '_e', 'this'));
  41. $this->assertNotContains('__psysh__', $shell->getScopeVariableNames());
  42. $this->assertSame(['one', 'two', 'three', '_'], $shell->getScopeVariableNames());
  43. $this->assertSame('banana', $shell->getScopeVariable('one'));
  44. $this->assertSame(123, $shell->getScopeVariable('two'));
  45. $this->assertSame($three, $shell->getScopeVariable('three'));
  46. $this->assertNull($shell->getScopeVariable('_'));
  47. $diff = $shell->getScopeVariablesDiff(['one' => $one, 'two' => 'not two']);
  48. $this->assertSame(['two' => $two, 'three' => $three, '_' => null], $diff);
  49. $shell->setScopeVariables([]);
  50. $this->assertSame(['_'], $shell->getScopeVariableNames());
  51. $shell->setBoundObject($this);
  52. $this->assertSame(['_', 'this'], $shell->getScopeVariableNames());
  53. $this->assertSame($this, $shell->getScopeVariable('this'));
  54. $this->assertSame(['_' => null], $shell->getScopeVariables(false));
  55. $this->assertSame(['_' => null, 'this' => $this], $shell->getScopeVariables());
  56. }
  57. public function testUnknownScopeVariablesThrowExceptions()
  58. {
  59. $this->expectException(\InvalidArgumentException::class);
  60. $shell = new Shell($this->getConfig());
  61. $shell->setScopeVariables(['foo' => 'FOO', 'bar' => 1]);
  62. $shell->getScopeVariable('baz');
  63. $this->fail();
  64. }
  65. /**
  66. * @group isolation-fail
  67. */
  68. public function testIncludesWithScopeVariables()
  69. {
  70. $one = 'banana';
  71. $two = 123;
  72. $three = new \stdClass();
  73. $__psysh__ = 'ignore this';
  74. $_ = 'ignore this';
  75. $_e = 'ignore this';
  76. $config = $this->getConfig(['usePcntl' => false]);
  77. $shell = new Shell($config);
  78. $shell->setScopeVariables(\compact('one', 'two', 'three', '__psysh__', '_', '_e', 'this'));
  79. $shell->addInput('exit', true);
  80. // This is super slow and we shouldn't do this :(
  81. $shell->run(null, $this->getOutput());
  82. $this->assertNotContains('__psysh__', $shell->getScopeVariableNames());
  83. $this->assertArrayEquals(['one', 'two', 'three', '_'], $shell->getScopeVariableNames());
  84. $this->assertSame('banana', $shell->getScopeVariable('one'));
  85. $this->assertSame(123, $shell->getScopeVariable('two'));
  86. $this->assertSame($three, $shell->getScopeVariable('three'));
  87. $this->assertNull($shell->getScopeVariable('_'));
  88. }
  89. protected function assertArrayEquals(array $expected, array $actual, $message = '')
  90. {
  91. if (\method_exists($this, 'assertSameCanonicalizing')) {
  92. return $this->assertSameCanonicalizing($expected, $actual, $message);
  93. }
  94. \sort($expected);
  95. \sort($actual);
  96. $this->assertSame($expected, $actual, $message);
  97. }
  98. /**
  99. * @group isolation-fail
  100. */
  101. public function testNonInteractiveDoesNotUpdateContext()
  102. {
  103. $config = $this->getConfig([
  104. 'usePcntl' => false,
  105. 'interactiveMode' => Configuration::INTERACTIVE_MODE_DISABLED,
  106. ]);
  107. $shell = new Shell($config);
  108. $input = $this->getInput('');
  109. $shell->addInput('$var=5;', true);
  110. $shell->addInput('exit', true);
  111. // This is still super slow and we shouldn't do this :(
  112. $shell->run($input, $this->getOutput());
  113. $this->assertNotContains('var', $shell->getScopeVariableNames());
  114. }
  115. /**
  116. * @group isolation-fail
  117. */
  118. public function testNonInteractiveRawOutput()
  119. {
  120. $config = $this->getConfig([
  121. 'usePcntl' => false,
  122. 'rawOutput' => true,
  123. 'interactiveMode' => Configuration::INTERACTIVE_MODE_DISABLED,
  124. ]);
  125. $shell = new Shell($config);
  126. $input = $this->getInput('');
  127. $output = $this->getOutput();
  128. $stream = $output->getStream();
  129. $shell->setOutput($output);
  130. $shell->addInput('$foo = "bar"', true);
  131. $shell->addInput('exit', true);
  132. // Sigh
  133. $shell->run($input, $output);
  134. \rewind($stream);
  135. $streamContents = \stream_get_contents($stream);
  136. // There shouldn't be a welcome message with raw output
  137. $this->assertStringNotContainsString('Justin Hileman', $streamContents);
  138. $this->assertStringNotContainsString(\PHP_VERSION, $streamContents);
  139. $this->assertStringNotContainsString(Shell::VERSION, $streamContents);
  140. // There shouldn't be an exit message with non-interactive input
  141. $this->assertStringNotContainsString('Goodbye', $streamContents);
  142. $this->assertStringNotContainsString('Exiting', $streamContents);
  143. }
  144. public function testIncludes()
  145. {
  146. $config = $this->getConfig(['configFile' => __DIR__.'/fixtures/empty.php']);
  147. $shell = new Shell($config);
  148. $this->assertEmpty($shell->getIncludes());
  149. $shell->setIncludes(['foo', 'bar', 'baz']);
  150. $this->assertSame(['foo', 'bar', 'baz'], $shell->getIncludes());
  151. }
  152. public function testIncludesConfig()
  153. {
  154. $config = $this->getConfig([
  155. 'defaultIncludes' => ['/file.php'],
  156. 'configFile' => __DIR__.'/fixtures/empty.php',
  157. ]);
  158. $shell = new Shell($config);
  159. $includes = $shell->getIncludes();
  160. $this->assertSame('/file.php', $includes[0]);
  161. }
  162. public function testAddMatchersViaConfig()
  163. {
  164. $shell = new FakeShell();
  165. $matcher = new ClassMethodsMatcher();
  166. $config = $this->getConfig([
  167. 'matchers' => [$matcher],
  168. ]);
  169. $config->setShell($shell);
  170. $this->assertSame([$matcher], $shell->matchers);
  171. }
  172. public function testAddMatchersViaConfigAfterShell()
  173. {
  174. $shell = new FakeShell();
  175. $matcher = new ClassMethodsMatcher();
  176. $config = $this->getConfig([]);
  177. $config->setShell($shell);
  178. $config->addMatchers([$matcher]);
  179. $this->assertSame([$matcher], $shell->matchers);
  180. }
  181. /**
  182. * @group isolation-fail
  183. */
  184. public function testRenderingExceptions()
  185. {
  186. $shell = new Shell($this->getConfig());
  187. $output = $this->getOutput();
  188. $stream = $output->getStream();
  189. $e = new ParseErrorException('message', 13);
  190. $shell->setOutput($output);
  191. $shell->addCode('code');
  192. $this->assertTrue($shell->hasCode());
  193. $this->assertNotEmpty($shell->getCodeBuffer());
  194. $shell->writeException($e);
  195. $this->assertSame($e, $shell->getScopeVariable('_e'));
  196. $this->assertFalse($shell->hasCode());
  197. $this->assertEmpty($shell->getCodeBuffer());
  198. \rewind($stream);
  199. $streamContents = \stream_get_contents($stream);
  200. $expected = 'PARSE ERROR PHP Parse error: message in test/ShellTest.php on line 236.';
  201. $this->assertSame($expected, \trim($streamContents));
  202. }
  203. /**
  204. * @dataProvider notSoBadErrors
  205. *
  206. * @group isolation-fail
  207. */
  208. public function testReportsErrors($errno, $label)
  209. {
  210. $shell = new Shell($this->getConfig());
  211. $output = $this->getOutput();
  212. $stream = $output->getStream();
  213. $shell->setOutput($output);
  214. $oldLevel = \error_reporting(\E_ALL);
  215. $shell->handleError($errno, 'wheee', null, 13);
  216. \error_reporting($oldLevel);
  217. \rewind($stream);
  218. $streamContents = \stream_get_contents($stream);
  219. $this->assertStringContainsString($label, $streamContents);
  220. $this->assertStringContainsString('wheee', $streamContents);
  221. $this->assertStringContainsString('line 13', $streamContents);
  222. }
  223. public function notSoBadErrors()
  224. {
  225. return [
  226. [\E_WARNING, 'WARNING'],
  227. [\E_NOTICE, 'NOTICE'],
  228. [\E_CORE_WARNING, 'CORE WARNING'],
  229. [\E_COMPILE_WARNING, 'COMPILE WARNING'],
  230. [\E_USER_WARNING, 'USER WARNING'],
  231. [\E_USER_NOTICE, 'USER NOTICE'],
  232. [\E_DEPRECATED, 'DEPRECATED'],
  233. [\E_USER_DEPRECATED, 'USER DEPRECATED'],
  234. ];
  235. }
  236. /**
  237. * @dataProvider badErrors
  238. */
  239. public function testThrowsBadErrors($errno)
  240. {
  241. $this->expectException(\Psy\Exception\ErrorException::class);
  242. $shell = new Shell($this->getConfig());
  243. $shell->handleError($errno, 'wheee', null, 13);
  244. $this->fail();
  245. }
  246. public function badErrors()
  247. {
  248. return [
  249. [\E_ERROR],
  250. [\E_PARSE],
  251. [\E_CORE_ERROR],
  252. [\E_COMPILE_ERROR],
  253. [\E_USER_ERROR],
  254. [\E_RECOVERABLE_ERROR],
  255. ];
  256. }
  257. /**
  258. * @group isolation-fail
  259. */
  260. public function testVersion()
  261. {
  262. $shell = new Shell($this->getConfig());
  263. $this->assertInstanceOf(Application::class, $shell);
  264. $this->assertStringContainsString(Shell::VERSION, $shell->getVersion());
  265. $this->assertStringContainsString(\PHP_VERSION, $shell->getVersion());
  266. $this->assertStringContainsString(\PHP_SAPI, $shell->getVersion());
  267. }
  268. public function testGetVersionHeader()
  269. {
  270. $header = Shell::getVersionHeader(false);
  271. $this->assertStringContainsString(Shell::VERSION, $header);
  272. $this->assertStringContainsString(\PHP_VERSION, $header);
  273. $this->assertStringContainsString(\PHP_SAPI, $header);
  274. }
  275. public function testCodeBuffer()
  276. {
  277. $shell = new Shell($this->getConfig());
  278. $shell->addCode('class');
  279. $this->assertNull($shell->flushCode());
  280. $this->assertTrue($shell->hasCode());
  281. $shell->addCode('a');
  282. $this->assertNull($shell->flushCode());
  283. $this->assertTrue($shell->hasCode());
  284. $shell->addCode('{}');
  285. $code = $shell->flushCode();
  286. $this->assertFalse($shell->hasCode());
  287. $code = \preg_replace('/\s+/', ' ', $code);
  288. $this->assertNotNull($code);
  289. $this->assertSame('class a { } return new \\Psy\\CodeCleaner\\NoReturnValue();', $code);
  290. }
  291. public function testKeepCodeBufferOpen()
  292. {
  293. $shell = new Shell($this->getConfig());
  294. $shell->addCode('1 \\');
  295. $this->assertNull($shell->flushCode());
  296. $this->assertTrue($shell->hasCode());
  297. $shell->addCode('+ 1 \\');
  298. $this->assertNull($shell->flushCode());
  299. $this->assertTrue($shell->hasCode());
  300. $shell->addCode('+ 1');
  301. $code = $shell->flushCode();
  302. $this->assertFalse($shell->hasCode());
  303. $code = \preg_replace('/\s+/', ' ', $code);
  304. $this->assertNotNull($code);
  305. $this->assertSame('return 1 + 1 + 1;', $code);
  306. }
  307. public function testCodeBufferThrowsParseExceptions()
  308. {
  309. $this->expectException(\Psy\Exception\ParseErrorException::class);
  310. $shell = new Shell($this->getConfig());
  311. $shell->addCode('this is not valid');
  312. $shell->flushCode();
  313. $this->fail();
  314. }
  315. public function testClosuresSupport()
  316. {
  317. $shell = new Shell($this->getConfig());
  318. $code = '$test = function () {}';
  319. $shell->addCode($code);
  320. $shell->flushCode();
  321. $code = '$test()';
  322. $shell->addCode($code);
  323. $this->assertSame($shell->flushCode(), 'return $test();');
  324. }
  325. /**
  326. * @group isolation-fail
  327. */
  328. public function testWriteStdout()
  329. {
  330. $output = $this->getOutput();
  331. $stream = $output->getStream();
  332. $shell = new Shell($this->getConfig());
  333. $shell->setOutput($output);
  334. $shell->writeStdout("{{stdout}}\n");
  335. \rewind($stream);
  336. $streamContents = \stream_get_contents($stream);
  337. $this->assertSame('{{stdout}}'.\PHP_EOL, $streamContents);
  338. }
  339. /**
  340. * @group isolation-fail
  341. */
  342. public function testWriteStdoutWithoutNewline()
  343. {
  344. $this->markTestSkipped('This test won\'t work on CI without overriding pipe detection');
  345. $output = $this->getOutput();
  346. $stream = $output->getStream();
  347. $shell = new Shell($this->getConfig());
  348. $shell->setOutput($output);
  349. $shell->writeStdout('{{stdout}}');
  350. \rewind($stream);
  351. $streamContents = \stream_get_contents($stream);
  352. $this->assertSame('{{stdout}}<aside>⏎</aside>'.\PHP_EOL, $streamContents);
  353. }
  354. /**
  355. * @group isolation-fail
  356. */
  357. public function testWriteStdoutRawOutputWithoutNewline()
  358. {
  359. $output = $this->getOutput();
  360. $stream = $output->getStream();
  361. $shell = new Shell($this->getConfig(['rawOutput' => true]));
  362. $shell->setOutput($output);
  363. $shell->writeStdout('{{stdout}}');
  364. \rewind($stream);
  365. $streamContents = \stream_get_contents($stream);
  366. $this->assertSame('{{stdout}}'.\PHP_EOL, $streamContents);
  367. }
  368. /**
  369. * @dataProvider getReturnValues
  370. *
  371. * @group isolation-fail
  372. */
  373. public function testWriteReturnValue($input, $expected)
  374. {
  375. $output = $this->getOutput();
  376. $stream = $output->getStream();
  377. $shell = new Shell($this->getConfig(['theme' => 'modern']));
  378. $shell->setOutput($output);
  379. $shell->writeReturnValue($input);
  380. \rewind($stream);
  381. $this->assertSame($expected, \stream_get_contents($stream));
  382. }
  383. /**
  384. * @dataProvider getReturnValues
  385. *
  386. * @group isolation-fail
  387. */
  388. public function testDoNotWriteReturnValueWhenQuiet($input, $expected)
  389. {
  390. $output = $this->getOutput();
  391. $output->setVerbosity(StreamOutput::VERBOSITY_QUIET);
  392. $stream = $output->getStream();
  393. $shell = new Shell($this->getConfig(['theme' => 'modern']));
  394. $shell->setOutput($output);
  395. $shell->writeReturnValue($input);
  396. \rewind($stream);
  397. $this->assertSame('', \stream_get_contents($stream));
  398. }
  399. public function getReturnValues()
  400. {
  401. return [
  402. ['{{return value}}', "<whisper>= </whisper>\"\033[32m{{return value}}\033[39m\"".\PHP_EOL],
  403. [1, "<whisper>= </whisper>\033[35m1\033[39m".\PHP_EOL],
  404. ];
  405. }
  406. /**
  407. * @dataProvider getRenderedExceptions
  408. *
  409. * @group isolation-fail
  410. */
  411. public function testWriteException($exception, $expected)
  412. {
  413. $output = $this->getOutput();
  414. $stream = $output->getStream();
  415. $shell = new Shell($this->getConfig(['theme' => 'compact']));
  416. $shell->setOutput($output);
  417. $shell->writeException($exception);
  418. \rewind($stream);
  419. $this->assertSame($expected, \stream_get_contents($stream));
  420. }
  421. /**
  422. * @dataProvider getRenderedExceptions
  423. *
  424. * @group isolation-fail
  425. */
  426. public function testWriteExceptionVerbose($exception, $expected)
  427. {
  428. $output = $this->getOutput();
  429. $output->setVerbosity(StreamOutput::VERBOSITY_VERBOSE);
  430. $stream = $output->getStream();
  431. $shell = new Shell($this->getConfig(['theme' => 'compact']));
  432. $shell->setOutput($output);
  433. $shell->writeException($exception);
  434. \rewind($stream);
  435. $stdout = \stream_get_contents($stream);
  436. $this->assertStringStartsWith($expected, $stdout);
  437. $this->assertStringContainsString(\basename(__FILE__), $stdout);
  438. $lineCount = \count(\explode(\PHP_EOL, $stdout));
  439. $this->assertGreaterThan(4, $lineCount); // /shrug
  440. }
  441. public function getRenderedExceptions()
  442. {
  443. return [[
  444. new \Exception('{{message}}'),
  445. " Exception {{message}}.\n",
  446. ]];
  447. }
  448. /**
  449. * @group isolation-fail
  450. */
  451. public function testWriteExceptionVerboseButNotReallyBecauseItIsABreakException()
  452. {
  453. $output = $this->getOutput();
  454. $output->setVerbosity(StreamOutput::VERBOSITY_VERBOSE);
  455. $stream = $output->getStream();
  456. $shell = new Shell($this->getConfig(['theme' => 'compact']));
  457. $shell->setOutput($output);
  458. $shell->writeException(new BreakException('yeah'));
  459. \rewind($stream);
  460. $this->assertSame(" INFO yeah.\n", \stream_get_contents($stream));
  461. }
  462. /**
  463. * @dataProvider getExceptionOutput
  464. *
  465. * @group isolation-fail
  466. */
  467. public function testCompactExceptionOutput($theme, $exception, $expected)
  468. {
  469. $output = $this->getOutput();
  470. $stream = $output->getStream();
  471. $shell = new Shell($this->getConfig(['theme' => $theme]));
  472. $shell->setOutput($output);
  473. $shell->writeException($exception);
  474. \rewind($stream);
  475. $this->assertSame($expected, \stream_get_contents($stream));
  476. }
  477. public function getExceptionOutput()
  478. {
  479. return [
  480. ['compact', new BreakException('break'), " INFO break.\n"],
  481. ['modern', new BreakException('break'), "\n INFO break.\n\n"],
  482. ['compact', new \Exception('foo'), " Exception foo.\n"],
  483. ['modern', new \Exception('bar'), "\n Exception bar.\n\n"],
  484. ];
  485. }
  486. /**
  487. * @dataProvider getExecuteValues
  488. *
  489. * @group isolation-fail
  490. */
  491. public function testShellExecute($input, $expected)
  492. {
  493. $output = $this->getOutput();
  494. $stream = $output->getStream();
  495. $shell = new Shell($this->getConfig());
  496. $shell->setOutput($output);
  497. $this->assertSame($expected, $shell->execute($input));
  498. \rewind($stream);
  499. $this->assertSame('', \stream_get_contents($stream));
  500. }
  501. public function getExecuteValues()
  502. {
  503. return [
  504. ['return 12', 12],
  505. ['"{{return value}}"', '{{return value}}'],
  506. ['1', 1],
  507. ];
  508. }
  509. /**
  510. * @dataProvider commandsToHas
  511. */
  512. public function testHasCommand($command, $has)
  513. {
  514. $shell = new Shell($this->getConfig());
  515. // :-/
  516. $refl = new \ReflectionClass(Shell::class);
  517. $method = $refl->getMethod('hasCommand');
  518. $method->setAccessible(true);
  519. $this->assertSame($method->invokeArgs($shell, [$command]), $has);
  520. }
  521. public function commandsToHas()
  522. {
  523. return [
  524. ['help', true],
  525. ['help help', true],
  526. ['"help"', false],
  527. ['"help help"', false],
  528. ['ls -al ', true],
  529. ['ls "-al" ', true],
  530. ['ls"-al"', false],
  531. [' q', true],
  532. [' q --help', true],
  533. ['"q"', false],
  534. ['"q",', false],
  535. ];
  536. }
  537. private function getInput($input)
  538. {
  539. $input = new StringInput($input);
  540. return $input;
  541. }
  542. private function getOutput()
  543. {
  544. $stream = \fopen('php://memory', 'w+');
  545. $this->streams[] = $stream;
  546. $output = new StreamOutput($stream, StreamOutput::VERBOSITY_NORMAL, false);
  547. return $output;
  548. }
  549. private function getConfig(array $config = [])
  550. {
  551. // Mebbe there's a better way than this?
  552. $dir = \tempnam(\sys_get_temp_dir(), 'psysh_shell_test_');
  553. \unlink($dir);
  554. $defaults = [
  555. 'configDir' => $dir,
  556. 'dataDir' => $dir,
  557. 'runtimeDir' => $dir,
  558. 'colorMode' => Configuration::COLOR_MODE_FORCED,
  559. ];
  560. return new Configuration(\array_merge($defaults, $config));
  561. }
  562. /**
  563. * @group isolation-fail
  564. */
  565. public function testStrictTypesExecute()
  566. {
  567. $shell = new Shell($this->getConfig(['strictTypes' => false]));
  568. $shell->setOutput($this->getOutput());
  569. $shell->execute('(function(): int { return 1.1; })()', true);
  570. $this->assertTrue(true);
  571. }
  572. /**
  573. * @group isolation-fail
  574. */
  575. public function testLaxTypesExecute()
  576. {
  577. $this->expectException(\TypeError::class);
  578. $shell = new Shell($this->getConfig(['strictTypes' => true]));
  579. $shell->setOutput($this->getOutput());
  580. $shell->execute('(function(): int { return 1.1; })()', true);
  581. }
  582. }