CommandRunnerTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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(tm) Project
  13. * @since 3.5.0
  14. * @license https://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Console;
  17. use Cake\Command\VersionCommand;
  18. use Cake\Console\Arguments;
  19. use Cake\Console\CommandCollection;
  20. use Cake\Console\CommandFactoryInterface;
  21. use Cake\Console\CommandInterface;
  22. use Cake\Console\CommandRunner;
  23. use Cake\Console\ConsoleIo;
  24. use Cake\Console\TestSuite\StubConsoleOutput;
  25. use Cake\Core\Configure;
  26. use Cake\Core\ConsoleApplicationInterface;
  27. use Cake\Event\EventManager;
  28. use Cake\Http\BaseApplication;
  29. use Cake\Routing\Router;
  30. use Cake\TestSuite\TestCase;
  31. use stdClass;
  32. use TestApp\Command\AbortCommand;
  33. use TestApp\Command\DemoCommand;
  34. use TestApp\Command\DependencyCommand;
  35. use TestApp\Command\SampleCommand;
  36. /**
  37. * Test case for the CommandCollection
  38. */
  39. class CommandRunnerTest extends TestCase
  40. {
  41. /**
  42. * @var string
  43. */
  44. protected $config;
  45. /**
  46. * Tracking property for event triggering
  47. *
  48. * @var bool
  49. */
  50. protected $eventTriggered = false;
  51. /**
  52. * setup
  53. */
  54. public function setUp(): void
  55. {
  56. parent::setUp();
  57. Configure::write('App.namespace', 'TestApp');
  58. $this->config = dirname(dirname(__DIR__));
  59. }
  60. /**
  61. * test event manager proxies to the application.
  62. */
  63. public function testEventManagerProxies(): void
  64. {
  65. $app = $this->getMockForAbstractClass(
  66. BaseApplication::class,
  67. [$this->config]
  68. );
  69. $runner = new CommandRunner($app);
  70. $this->assertSame($app->getEventManager(), $runner->getEventManager());
  71. }
  72. /**
  73. * test event manager cannot be set on applications without events.
  74. */
  75. public function testGetEventManagerNonEventedApplication(): void
  76. {
  77. $app = $this->createMock(ConsoleApplicationInterface::class);
  78. $runner = new CommandRunner($app);
  79. $this->assertSame(EventManager::instance(), $runner->getEventManager());
  80. }
  81. /**
  82. * Test that running an unknown command raises an error.
  83. */
  84. public function testRunInvalidCommand(): void
  85. {
  86. $app = $this->getMockBuilder(BaseApplication::class)
  87. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  88. ->setConstructorArgs([$this->config])
  89. ->getMock();
  90. $output = new StubConsoleOutput();
  91. $runner = new CommandRunner($app);
  92. $runner->run(['cake', 'nope', 'nope', 'nope'], $this->getMockIo($output));
  93. $messages = implode("\n", $output->messages());
  94. $this->assertStringContainsString(
  95. 'Unknown command `cake nope`. Run `cake --help` to get the list of commands.',
  96. $messages
  97. );
  98. }
  99. /**
  100. * Test that using special characters in an unknown command does
  101. * not cause a PHP error.
  102. */
  103. public function testRunInvalidCommandWithSpecialCharacters(): void
  104. {
  105. $app = $this->getMockBuilder(BaseApplication::class)
  106. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  107. ->setConstructorArgs([$this->config])
  108. ->getMock();
  109. $output = new StubConsoleOutput();
  110. $runner = new CommandRunner($app);
  111. $runner->run(['cake', 's/pec[ial'], $this->getMockIo($output));
  112. $messages = implode("\n", $output->messages());
  113. $this->assertStringContainsString(
  114. 'Unknown command `cake s/pec[ial`. Run `cake --help` to get the list of commands.',
  115. $messages
  116. );
  117. }
  118. /**
  119. * Test that running an unknown command gives suggestions.
  120. */
  121. public function testRunInvalidCommandSuggestion(): void
  122. {
  123. $app = $this->getMockBuilder(BaseApplication::class)
  124. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  125. ->setConstructorArgs([$this->config])
  126. ->getMock();
  127. $output = new StubConsoleOutput();
  128. $runner = new CommandRunner($app);
  129. $runner->run(['cake', 'cache'], $this->getMockIo($output));
  130. $messages = implode("\n", $output->messages());
  131. $this->assertStringContainsString(
  132. "Did you mean: `cache clear`?\n" .
  133. "\n" .
  134. "Other valid choices:\n" .
  135. "\n" .
  136. '- help',
  137. $messages
  138. );
  139. }
  140. /**
  141. * Test using `cake --help` invokes the help command
  142. */
  143. public function testRunHelpLongOption(): void
  144. {
  145. $app = $this->getMockBuilder(BaseApplication::class)
  146. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  147. ->setConstructorArgs([$this->config])
  148. ->getMock();
  149. $output = new StubConsoleOutput();
  150. $runner = new CommandRunner($app, 'cake');
  151. $result = $runner->run(['cake', '--help'], $this->getMockIo($output));
  152. $this->assertSame(0, $result);
  153. $messages = implode("\n", $output->messages());
  154. $this->assertStringContainsString('Current Paths', $messages);
  155. $this->assertStringContainsString('- i18n', $messages);
  156. $this->assertStringContainsString('Available Commands', $messages);
  157. }
  158. /**
  159. * Test using `cake -h` invokes the help command
  160. */
  161. public function testRunHelpShortOption(): void
  162. {
  163. $app = $this->getMockBuilder(BaseApplication::class)
  164. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  165. ->setConstructorArgs([$this->config])
  166. ->getMock();
  167. $output = new StubConsoleOutput();
  168. $runner = new CommandRunner($app, 'cake');
  169. $result = $runner->run(['cake', '-h'], $this->getMockIo($output));
  170. $this->assertSame(0, $result);
  171. $messages = implode("\n", $output->messages());
  172. $this->assertStringContainsString('- i18n', $messages);
  173. $this->assertStringContainsString('Available Commands', $messages);
  174. }
  175. /**
  176. * Test that no command outputs the command list
  177. */
  178. public function testRunNoCommand(): void
  179. {
  180. $app = $this->getMockBuilder(BaseApplication::class)
  181. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  182. ->setConstructorArgs([$this->config])
  183. ->getMock();
  184. $output = new StubConsoleOutput();
  185. $runner = new CommandRunner($app);
  186. $result = $runner->run(['cake'], $this->getMockIo($output));
  187. $this->assertSame(0, $result, 'help output is success.');
  188. $messages = implode("\n", $output->messages());
  189. $this->assertStringContainsString('No command provided. Choose one of the available commands', $messages);
  190. $this->assertStringContainsString('- i18n', $messages);
  191. $this->assertStringContainsString('Available Commands', $messages);
  192. }
  193. /**
  194. * Test using `cake --version` invokes the version command
  195. */
  196. public function testRunVersionAlias(): void
  197. {
  198. $app = $this->getMockBuilder(BaseApplication::class)
  199. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  200. ->setConstructorArgs([$this->config])
  201. ->getMock();
  202. $output = new StubConsoleOutput();
  203. $runner = new CommandRunner($app, 'cake');
  204. $runner->run(['cake', '--version'], $this->getMockIo($output));
  205. $this->assertStringContainsString(Configure::version(), $output->messages()[0]);
  206. }
  207. /**
  208. * Test running a valid command
  209. */
  210. public function testRunValidCommand(): void
  211. {
  212. $app = $this->getMockBuilder(BaseApplication::class)
  213. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  214. ->setConstructorArgs([$this->config])
  215. ->getMock();
  216. $output = new StubConsoleOutput();
  217. $runner = new CommandRunner($app, 'cake');
  218. $result = $runner->run(['cake', 'routes'], $this->getMockIo($output));
  219. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  220. $contents = implode("\n", $output->messages());
  221. $this->assertStringContainsString('URI template', $contents);
  222. }
  223. /**
  224. * Test running a valid command and that backwards compatible
  225. * inflection is hooked up.
  226. */
  227. public function testRunValidCommandInflection(): void
  228. {
  229. $app = $this->getMockBuilder(BaseApplication::class)
  230. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  231. ->setConstructorArgs([$this->config])
  232. ->getMock();
  233. $output = new StubConsoleOutput();
  234. $runner = new CommandRunner($app, 'cake');
  235. $result = $runner->run(['cake', 'schema_cache', 'build'], $this->getMockIo($output));
  236. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  237. $contents = implode("\n", $output->messages());
  238. $this->assertStringContainsString('Cache', $contents);
  239. }
  240. /**
  241. * Test running a valid raising an error
  242. */
  243. public function testRunValidCommandWithAbort(): void
  244. {
  245. $app = $this->makeAppWithCommands(['failure' => AbortCommand::class]);
  246. $output = new StubConsoleOutput();
  247. $runner = new CommandRunner($app, 'cake');
  248. $result = $runner->run(['cake', 'failure'], $this->getMockIo($output));
  249. $this->assertSame(127, $result);
  250. }
  251. /**
  252. * Ensure that the root command name propagates to shell help
  253. */
  254. public function testRunRootNamePropagates(): void
  255. {
  256. $app = $this->makeAppWithCommands(['sample' => SampleCommand::class]);
  257. $output = new StubConsoleOutput();
  258. $runner = new CommandRunner($app, 'widget');
  259. $runner->run(['widget', 'sample', '-h'], $this->getMockIo($output));
  260. $result = implode("\n", $output->messages());
  261. $this->assertStringContainsString('widget sample [-h]', $result);
  262. $this->assertStringNotContainsString('cake sample [-h]', $result);
  263. }
  264. /**
  265. * Test running a valid command
  266. */
  267. public function testRunValidCommandClass(): void
  268. {
  269. $app = $this->makeAppWithCommands(['ex' => DemoCommand::class]);
  270. $output = new StubConsoleOutput();
  271. $runner = new CommandRunner($app, 'cake');
  272. $result = $runner->run(['cake', 'ex'], $this->getMockIo($output));
  273. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  274. $messages = implode("\n", $output->messages());
  275. $this->assertStringContainsString('Demo Command!', $messages);
  276. }
  277. /**
  278. * Test running a valid command with spaces in the name
  279. */
  280. public function testRunValidCommandSubcommandName(): void
  281. {
  282. $app = $this->makeAppWithCommands([
  283. 'tool build' => DemoCommand::class,
  284. 'tool' => AbortCommand::class,
  285. ]);
  286. $output = new StubConsoleOutput();
  287. $runner = new CommandRunner($app, 'cake');
  288. $result = $runner->run(['cake', 'tool', 'build'], $this->getMockIo($output));
  289. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  290. $messages = implode("\n", $output->messages());
  291. $this->assertStringContainsString('Demo Command!', $messages);
  292. }
  293. /**
  294. * Test running a valid command with spaces in the name
  295. */
  296. public function testRunValidCommandNestedName(): void
  297. {
  298. $app = $this->makeAppWithCommands([
  299. 'tool build assets' => DemoCommand::class,
  300. 'tool' => AbortCommand::class,
  301. ]);
  302. $output = new StubConsoleOutput();
  303. $runner = new CommandRunner($app, 'cake');
  304. $result = $runner->run(['cake', 'tool', 'build', 'assets'], $this->getMockIo($output));
  305. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  306. $messages = implode("\n", $output->messages());
  307. $this->assertStringContainsString('Demo Command!', $messages);
  308. }
  309. /**
  310. * Test using a custom factory
  311. */
  312. public function testRunWithCustomFactory(): void
  313. {
  314. $output = new StubConsoleOutput();
  315. $io = $this->getMockIo($output);
  316. $factory = $this->createMock(CommandFactoryInterface::class);
  317. $factory->expects($this->once())
  318. ->method('create')
  319. ->with(DemoCommand::class)
  320. ->willReturn(new DemoCommand());
  321. $app = $this->makeAppWithCommands(['ex' => DemoCommand::class]);
  322. $runner = new CommandRunner($app, 'cake', $factory);
  323. $result = $runner->run(['cake', 'ex'], $io);
  324. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  325. $messages = implode("\n", $output->messages());
  326. $this->assertStringContainsString('Demo Command!', $messages);
  327. }
  328. public function testRunWithContainerDependencies(): void
  329. {
  330. $app = $this->makeAppWithCommands([
  331. 'dependency' => DependencyCommand::class,
  332. ]);
  333. $container = $app->getContainer();
  334. $container->add(stdClass::class, json_decode('{"key":"value"}'));
  335. $container->add(DependencyCommand::class)
  336. ->addArgument(stdClass::class);
  337. $output = new StubConsoleOutput();
  338. $runner = new CommandRunner($app, 'cake');
  339. $result = $runner->run(['cake', 'dependency'], $this->getMockIo($output));
  340. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  341. $messages = implode("\n", $output->messages());
  342. $this->assertStringContainsString('Dependency Command', $messages);
  343. $this->assertStringContainsString('constructor inject: {"key":"value"}', $messages);
  344. }
  345. /**
  346. * Test running a command class' help
  347. */
  348. public function testRunValidCommandClassHelp(): void
  349. {
  350. $app = $this->makeAppWithCommands(['ex' => DemoCommand::class]);
  351. $output = new StubConsoleOutput();
  352. $runner = new CommandRunner($app, 'cake');
  353. $result = $runner->run(['cake', 'ex', '-h'], $this->getMockIo($output));
  354. $this->assertSame(CommandInterface::CODE_SUCCESS, $result);
  355. $messages = implode("\n", $output->messages());
  356. $this->assertStringContainsString("\ncake ex [-h]", $messages);
  357. $this->assertStringNotContainsString('Demo Command!', $messages);
  358. }
  359. /**
  360. * Test that run() fires off the buildCommands event.
  361. */
  362. public function testRunTriggersBuildCommandsEvent(): void
  363. {
  364. $app = $this->getMockBuilder(BaseApplication::class)
  365. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  366. ->setConstructorArgs([$this->config])
  367. ->getMock();
  368. $output = new StubConsoleOutput();
  369. $runner = new CommandRunner($app, 'cake');
  370. $runner->getEventManager()->on('Console.buildCommands', function ($event, $commands): void {
  371. $this->assertInstanceOf(CommandCollection::class, $commands);
  372. $this->eventTriggered = true;
  373. });
  374. $runner->run(['cake', '--version'], $this->getMockIo($output));
  375. $this->assertTrue($this->eventTriggered, 'Should have triggered event.');
  376. }
  377. /**
  378. * Test that run() fires off the Command.started and Command.finished events.
  379. */
  380. public function testRunTriggersCommandEvents(): void
  381. {
  382. $app = $this->getMockBuilder(BaseApplication::class)
  383. ->onlyMethods(['middleware', 'bootstrap', 'routes'])
  384. ->setConstructorArgs([$this->config])
  385. ->getMock();
  386. $output = new StubConsoleOutput();
  387. $runner = new CommandRunner($app, 'cake');
  388. $startedEventTriggered = $finishedEventTriggered = false;
  389. $runner->getEventManager()->on('Command.beforeExecute', function ($event, $args) use (&$startedEventTriggered): void {
  390. $this->assertInstanceOf(VersionCommand::class, $event->getSubject());
  391. $this->assertInstanceOf(Arguments::class, $args);
  392. $startedEventTriggered = true;
  393. });
  394. $runner->getEventManager()->on('Command.afterExecute', function ($event, $args, $result) use (&$finishedEventTriggered): void {
  395. $this->assertInstanceOf(VersionCommand::class, $event->getSubject());
  396. $this->assertInstanceOf(Arguments::class, $args);
  397. $this->assertEquals(CommandInterface::CODE_SUCCESS, $result);
  398. $finishedEventTriggered = true;
  399. });
  400. $runner->run(['cake', '--version'], $this->getMockIo($output));
  401. $this->assertTrue($startedEventTriggered, 'Should have triggered Command.started event.');
  402. $this->assertTrue($finishedEventTriggered, 'Should have triggered Command.finished event.');
  403. }
  404. /**
  405. * Test that run calls plugin hook methods
  406. */
  407. public function testRunCallsPluginHookMethods(): void
  408. {
  409. $app = $this->getMockBuilder(BaseApplication::class)
  410. ->onlyMethods([
  411. 'middleware', 'bootstrap', 'routes',
  412. 'pluginBootstrap', 'pluginConsole', 'pluginRoutes',
  413. ])
  414. ->setConstructorArgs([$this->config])
  415. ->getMock();
  416. $app->expects($this->once())->method('bootstrap');
  417. $app->expects($this->once())->method('pluginBootstrap');
  418. $app->expects($this->once())
  419. ->method('pluginConsole')
  420. ->with($this->isinstanceOf(CommandCollection::class))
  421. ->willReturnCallback(function ($commands) {
  422. return $commands;
  423. });
  424. $app->expects($this->once())->method('routes');
  425. $app->expects($this->once())->method('pluginRoutes');
  426. $output = new StubConsoleOutput();
  427. $runner = new CommandRunner($app, 'cake');
  428. $runner->run(['cake', '--version'], $this->getMockIo($output));
  429. $this->assertStringContainsString(Configure::version(), $output->messages()[0]);
  430. }
  431. /**
  432. * Test that run() loads routing.
  433. */
  434. public function testRunLoadsRoutes(): void
  435. {
  436. $app = $this->getMockBuilder(BaseApplication::class)
  437. ->onlyMethods(['middleware', 'bootstrap'])
  438. ->setConstructorArgs([TEST_APP . 'config' . DS])
  439. ->getMock();
  440. $output = new StubConsoleOutput();
  441. $runner = new CommandRunner($app, 'cake');
  442. $runner->run(['cake', '--version'], $this->getMockIo($output));
  443. $this->assertGreaterThan(2, count(Router::getRouteCollection()->routes()));
  444. }
  445. protected function makeAppWithCommands(array $commands): BaseApplication
  446. {
  447. $app = $this->getMockBuilder(BaseApplication::class)
  448. ->onlyMethods(['middleware', 'bootstrap', 'console', 'routes'])
  449. ->setConstructorArgs([$this->config])
  450. ->getMock();
  451. $collection = new CommandCollection($commands);
  452. $app->method('console')->willReturn($collection);
  453. return $app;
  454. }
  455. protected function getMockIo(StubConsoleOutput $output): ConsoleIo
  456. {
  457. $io = $this->getMockBuilder(ConsoleIo::class)
  458. ->setConstructorArgs([$output, $output, null, null])
  459. ->addMethods(['in'])
  460. ->getMock();
  461. return $io;
  462. }
  463. }