2 namespace Consolidation\AnnotatedCommand;
4 use Consolidation\AnnotatedCommand\AnnotationData;
5 use Consolidation\AnnotatedCommand\CommandData;
6 use Consolidation\AnnotatedCommand\CommandProcessor;
7 use Consolidation\AnnotatedCommand\Hooks\AlterResultInterface;
8 use Consolidation\AnnotatedCommand\Hooks\ExtractOutputInterface;
9 use Consolidation\AnnotatedCommand\Hooks\HookManager;
10 use Consolidation\AnnotatedCommand\Hooks\ProcessResultInterface;
11 use Consolidation\AnnotatedCommand\Hooks\StatusDeterminerInterface;
12 use Consolidation\AnnotatedCommand\Hooks\ValidatorInterface;
13 use Consolidation\AnnotatedCommand\Options\AlterOptionsCommandEvent;
14 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
15 use Consolidation\OutputFormatters\FormatterManager;
16 use Symfony\Component\Console\Application;
17 use Symfony\Component\Console\Command\Command;
18 use Symfony\Component\Console\Input\InputInterface;
19 use Symfony\Component\Console\Input\StringInput;
20 use Symfony\Component\Console\Output\BufferedOutput;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Consolidation\TestUtils\ApplicationWithTerminalWidth;
23 use Consolidation\AnnotatedCommand\Options\PrepareTerminalWidthOption;
24 use Consolidation\AnnotatedCommand\Events\CustomEventAwareInterface;
25 use Consolidation\AnnotatedCommand\Events\CustomEventAwareTrait;
28 * Do a test of all of the classes in this project, top-to-bottom.
30 class FullStackTests extends \PHPUnit_Framework_TestCase
32 protected $application;
33 protected $commandFactory;
36 $this->application = new ApplicationWithTerminalWidth('TestApplication', '0.0.0');
37 $this->commandFactory = new AnnotatedCommandFactory();
38 $alterOptionsEventManager = new AlterOptionsCommandEvent($this->application);
39 $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
40 $eventDispatcher->addSubscriber($this->commandFactory->commandProcessor()->hookManager());
41 $eventDispatcher->addSubscriber($alterOptionsEventManager);
42 $this->application->setDispatcher($eventDispatcher);
43 $this->application->setAutoExit(false);
46 function testValidFormats()
48 $formatter = new FormatterManager();
49 $formatter->addDefaultFormatters();
50 $formatter->addDefaultSimplifiers();
51 $commandInfo = CommandInfo::create('\Consolidation\TestUtils\alpha\AlphaCommandFile', 'exampleTable');
52 $this->assertEquals('example:table', $commandInfo->getName());
53 $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $commandInfo->getReturnType());
56 function testAutomaticOptions()
58 $commandFileInstance = new \Consolidation\TestUtils\alpha\AlphaCommandFile;
59 $formatter = new FormatterManager();
60 $formatter->addDefaultFormatters();
61 $formatter->addDefaultSimplifiers();
63 $this->commandFactory->commandProcessor()->setFormatterManager($formatter);
64 $commandInfo = $this->commandFactory->createCommandInfo($commandFileInstance, 'exampleTable');
66 $command = $this->commandFactory->createCommand($commandInfo, $commandFileInstance);
67 $this->application->add($command);
71 '--format[=FORMAT] Format the result data. Available formats: csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml [default: "table"]',
72 '--fields[=FIELDS] Available fields: I (first), II (second), III (third) [default: ""]',
74 $this->assertRunCommandViaApplicationContains('help example:table', $containsList);
77 function testCommandsAndHooks()
79 // First, search for commandfiles in the 'alpha'
80 // directory. Note that this same functionality
81 // is tested more thoroughly in isolation in
82 // testCommandFileDiscovery.php
83 $discovery = new CommandFileDiscovery();
85 ->setSearchPattern('*CommandFile.php')
86 ->setIncludeFilesAtBase(false)
87 ->setSearchLocations(['alpha']);
90 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
92 $formatter = new FormatterManager();
93 $formatter->addDefaultFormatters();
94 $formatter->addDefaultSimplifiers();
95 $hookManager = new HookManager();
96 $terminalWidthOption = new PrepareTerminalWidthOption();
97 $terminalWidthOption->setApplication($this->application);
98 $commandProcessor = new CommandProcessor($hookManager);
99 $commandProcessor->setFormatterManager($formatter);
100 $commandProcessor->addPrepareFormatter($terminalWidthOption);
102 // Create a new factory, and load all of the files
104 $factory = new AnnotatedCommandFactory();
105 $factory->setCommandProcessor($commandProcessor);
106 // Add a listener to configure our command handler object
107 $factory->addListernerCallback(function($command) use($hookManager) {
108 if ($command instanceof CustomEventAwareInterface) {
109 $command->setHookManager($hookManager);
112 $factory->setIncludeAllPublicMethods(false);
113 $this->addDiscoveredCommands($factory, $commandFiles);
115 $this->assertRunCommandViaApplicationContains('list', ['example:table'], ['additional:option', 'without:annotations']);
117 $this->assertTrue($this->application->has('example:table'));
118 $this->assertFalse($this->application->has('without:annotations'));
120 // Run the use:event command that defines a custom event, my-event.
121 $this->assertRunCommandViaApplicationEquals('use:event', 'one,two');
122 // Watch as we dynamically add a custom event to the hook manager to change the command results:
123 $hookManager->add(function () { return 'three'; }, HookManager::ON_EVENT, 'my-event');
124 $this->assertRunCommandViaApplicationEquals('use:event', 'one,three,two');
126 // Fetch a reference to the 'example:table' command and test its valid format types
127 $exampleTableCommand = $this->application->find('example:table');
128 $returnType = $exampleTableCommand->getReturnType();
129 $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $returnType);
130 $validFormats = $formatter->validFormats($returnType);
131 $this->assertEquals('csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml', implode(',', $validFormats));
133 // Control: run commands without hooks.
134 $this->assertRunCommandViaApplicationEquals('always:fail', 'This command always fails.', 13);
135 $this->assertRunCommandViaApplicationEquals('simulated:status', '42');
136 $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World.');
137 $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphabet');
138 $this->assertRunCommandViaApplicationEquals('example:echo a b c', "a\tb\tc");
139 $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.');
140 $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument', 'Hello, world');
141 $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument Joe', 'Hello, Joe');
144 $factory->hookManager()->addValidator(new ExampleValidator());
145 $factory->hookManager()->addResultProcessor(new ExampleResultProcessor());
146 $factory->hookManager()->addAlterResult(new ExampleResultAlterer());
147 $factory->hookManager()->addStatusDeterminer(new ExampleStatusDeterminer());
148 $factory->hookManager()->addOutputExtractor(new ExampleOutputExtractor());
150 // Run the same commands as before, and confirm that results
151 // are different now that the hooks are in place.
152 $this->assertRunCommandViaApplicationEquals('simulated:status', '', 42);
153 $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World!');
154 $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphareplaced');
155 $this->assertRunCommandViaApplicationEquals('example:echo a b c', 'a,b,c');
156 $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.');
159 ------ ------ -------
161 ------ ------ -------
166 ------ ------ -------
168 $this->assertRunCommandViaApplicationEquals('example:table', $expected);
180 $this->assertRunCommandViaApplicationEquals('example:table --fields=III,II', $expected);
182 $expectedSingleField = <<<EOT
189 // When --field is specified (instead of --fields), then the format
190 // is forced to 'string'.
191 $this->assertRunCommandViaApplicationEquals('example:table --field=II', $expectedSingleField);
193 // Check the help for the example table command and see if the options
194 // from the alter hook were added. We expect that we should not see
195 // any of the information from the alter hook in the 'beta' folder yet.
196 $this->assertRunCommandViaApplicationContains('help example:table',
198 'Option added by @hook option example:table',
199 'example:table --french',
200 'Add a row with French numbers.'
208 $expectedOutputWithFrench = <<<EOT
209 ------ ------ -------
211 ------ ------ -------
217 ------ ------ -------
219 $this->assertRunCommandViaApplicationEquals('example:table --french', $expectedOutputWithFrench);
221 $expectedAssociativeListTable = <<<EOT
222 --------------- ----------------------------------------------------------------------------------------
223 SFTP Command sftp -o Port=2222 dev@appserver.dev.drush.in
224 Git Command git clone ssh://codeserver.dev@codeserver.dev.drush.in:2222/~/repository.git wp-update
225 MySQL Command mysql -u pantheon -p4b33cb -h dbserver.dev.drush.in -P 16191 pantheon
226 --------------- ----------------------------------------------------------------------------------------
228 $this->assertRunCommandViaApplicationEquals('example:list', $expectedAssociativeListTable);
229 $this->assertRunCommandViaApplicationEquals('example:list --field=sftp_command', 'sftp -o Port=2222 dev@appserver.dev.drush.in');
231 $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
232 $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
234 $this->assertRunCommandViaApplicationContains('help example:wrap',
236 'Test word wrapping',
237 '[default: "table"]',
241 $expectedUnwrappedOutput = <<<EOT
242 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
244 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
245 This is a really long cell that contains a lot of data. When it is rendered, it should be wrapped across multiple lines. This is the second column of the same table. It is also very long, and should be wrapped across multiple lines, just like the first column.
246 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
248 $this->application->setWidthAndHeight(0, 0);
249 $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedUnwrappedOutput);
251 $expectedWrappedOutput = <<<EOT
252 ------------------- --------------------
254 ------------------- --------------------
255 This is a really This is the second
256 long cell that column of the same
257 contains a lot of table. It is also
258 data. When it is very long, and
259 rendered, it should be wrapped
260 should be wrapped across multiple
261 across multiple lines, just like
262 lines. the first column.
263 ------------------- --------------------
265 $this->application->setWidthAndHeight(42, 24);
266 $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedWrappedOutput);
269 function testCommandsAndHooksIncludeAllPublicMethods()
271 // First, search for commandfiles in the 'alpha'
272 // directory. Note that this same functionality
273 // is tested more thoroughly in isolation in
274 // testCommandFileDiscovery.php
275 $discovery = new CommandFileDiscovery();
277 ->setSearchPattern('*CommandFile.php')
278 ->setIncludeFilesAtBase(false)
279 ->setSearchLocations(['alpha']);
282 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
284 $formatter = new FormatterManager();
285 $formatter->addDefaultFormatters();
286 $formatter->addDefaultSimplifiers();
287 $hookManager = new HookManager();
288 $commandProcessor = new CommandProcessor($hookManager);
289 $commandProcessor->setFormatterManager($formatter);
291 // Create a new factory, and load all of the files
292 // discovered above. The command factory class is
293 // tested in isolation in testAnnotatedCommandFactory.php,
294 // but this is the only place where
295 $factory = new AnnotatedCommandFactory();
296 $factory->setCommandProcessor($commandProcessor);
297 // $factory->addListener(...);
299 // Now we will once again add all commands, this time including all
300 // public methods. The command 'withoutAnnotations' should now be found.
301 $factory->setIncludeAllPublicMethods(true);
302 $this->addDiscoveredCommands($factory, $commandFiles);
303 $this->assertTrue($this->application->has('without:annotations'));
305 $this->assertRunCommandViaApplicationContains('list', ['example:table', 'without:annotations'], ['alter:formatters']);
307 $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
308 $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
311 function testCommandsAndHooksWithBetaFolder()
313 // First, search for commandfiles in the 'alpha'
314 // directory. Note that this same functionality
315 // is tested more thoroughly in isolation in
316 // testCommandFileDiscovery.php
317 $discovery = new CommandFileDiscovery();
319 ->setSearchPattern('*CommandFile.php')
320 ->setIncludeFilesAtBase(false)
321 ->setSearchLocations(['alpha', 'beta']);
324 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
326 $formatter = new FormatterManager();
327 $formatter->addDefaultFormatters();
328 $formatter->addDefaultSimplifiers();
329 $hookManager = new HookManager();
330 $commandProcessor = new CommandProcessor($hookManager);
331 $commandProcessor->setFormatterManager($formatter);
333 // Create a new factory, and load all of the files
334 // discovered above. The command factory class is
335 // tested in isolation in testAnnotatedCommandFactory.php,
336 // but this is the only place where
337 $factory = new AnnotatedCommandFactory();
338 $factory->setCommandProcessor($commandProcessor);
339 // $factory->addListener(...);
340 $factory->setIncludeAllPublicMethods(true);
341 $this->addDiscoveredCommands($factory, $commandFiles);
343 // A few asserts, to make sure that our hooks all get registered.
344 $allRegisteredHooks = $hookManager->getAllHooks();
345 $registeredHookNames = array_keys($allRegisteredHooks);
346 sort($registeredHookNames);
347 $this->assertEquals('*,example:table,my-event', implode(',', $registeredHookNames));
348 $allHooksForExampleTable = $allRegisteredHooks['example:table'];
349 $allHookPhasesForExampleTable = array_keys($allHooksForExampleTable);
350 sort($allHookPhasesForExampleTable);
351 $this->assertEquals('alter,option', implode(',', $allHookPhasesForExampleTable));
353 $this->assertContains('alterFormattersChinese', var_export($allHooksForExampleTable, true));
355 $alterHooksForExampleTable = $this->callProtected($hookManager, 'getHooks', [['example:table'], 'alter']);
356 $this->assertContains('alterFormattersKanji', var_export($alterHooksForExampleTable, true));
358 $allHooksForAnyCommand = $allRegisteredHooks['*'];
359 $allHookPhasesForAnyCommand = array_keys($allHooksForAnyCommand);
360 sort($allHookPhasesForAnyCommand);
361 $this->assertEquals('alter', implode(',', $allHookPhasesForAnyCommand));
363 $this->assertContains('alterFormattersKanji', var_export($allHooksForAnyCommand, true));
365 // Help should have the information from the hooks in the 'beta' folder
366 $this->assertRunCommandViaApplicationContains('help example:table',
368 'Option added by @hook option example:table',
369 'example:table --french',
370 'Add a row with French numbers.',
376 // Confirm that the "unavailable" command is now available
377 $this->assertTrue($this->application->has('unavailable:command'));
379 $expectedOutputWithChinese = <<<EOT
380 ------ ------ -------
382 ------ ------ -------
388 ------ ------ -------
390 $this->assertRunCommandViaApplicationEquals('example:table --chinese', $expectedOutputWithChinese);
392 $expectedOutputWithKanji = <<<EOT
393 ------ ------ -------
395 ------ ------ -------
401 ------ ------ -------
403 $this->assertRunCommandViaApplicationEquals('example:table --kanji', $expectedOutputWithKanji);
406 public function addDiscoveredCommands($factory, $commandFiles) {
407 foreach ($commandFiles as $path => $commandClass) {
408 $this->assertFileExists($path);
409 if (!class_exists($commandClass)) {
412 $commandInstance = new $commandClass();
413 $commandList = $factory->createCommandsFromClass($commandInstance);
414 foreach ($commandList as $command) {
415 $this->application->add($command);
420 function assertRunCommandViaApplicationEquals($cmd, $expectedOutput, $expectedStatusCode = 0)
422 $input = new StringInput($cmd);
423 $output = new BufferedOutput();
425 $statusCode = $this->application->run($input, $output);
426 $commandOutput = trim($output->fetch());
428 $expectedOutput = $this->simplifyWhitespace($expectedOutput);
429 $commandOutput = $this->simplifyWhitespace($commandOutput);
431 $this->assertEquals($expectedOutput, $commandOutput);
432 $this->assertEquals($expectedStatusCode, $statusCode);
435 function assertRunCommandViaApplicationContains($cmd, $containsList, $doesNotContainList = [], $expectedStatusCode = 0)
437 $input = new StringInput($cmd);
438 $output = new BufferedOutput();
439 $containsList = (array) $containsList;
441 $statusCode = $this->application->run($input, $output);
442 $commandOutput = trim($output->fetch());
444 $commandOutput = $this->simplifyWhitespace($commandOutput);
446 foreach ($containsList as $expectedToContain) {
447 $this->assertContains($this->simplifyWhitespace($expectedToContain), $commandOutput);
449 foreach ($doesNotContainList as $expectedToNotContain) {
450 $this->assertNotContains($this->simplifyWhitespace($expectedToNotContain), $commandOutput);
452 $this->assertEquals($expectedStatusCode, $statusCode);
455 function simplifyWhitespace($data)
457 return trim(preg_replace('#[ \t]+$#m', '', $data));
460 function callProtected($object, $method, $args = [])
462 $r = new \ReflectionMethod($object, $method);
463 $r->setAccessible(true);
464 return $r->invokeArgs($object, $args);
469 class ExampleValidator implements ValidatorInterface
471 public function validate(CommandData $commandData)
473 $args = $commandData->arguments();
474 if (isset($args['one']) && ($args['one'] == 'bet')) {
475 $commandData->input()->setArgument('one', 'replaced');
481 class ExampleResultProcessor implements ProcessResultInterface
483 public function process($result, CommandData $commandData)
485 if (is_array($result) && array_key_exists('item-list', $result)) {
486 return implode(',', $result['item-list']);
491 class ExampleResultAlterer implements AlterResultInterface
493 public function process($result, CommandData $commandData)
495 if (is_string($result) && ($result == 'Hello, World.')) {
496 return 'Hello, World!';
501 class ExampleStatusDeterminer implements StatusDeterminerInterface
503 public function determineStatusCode($result)
505 if (is_array($result) && array_key_exists('status-code', $result)) {
506 return $result['status-code'];
511 class ExampleOutputExtractor implements ExtractOutputInterface
513 public function extractOutput($result)
515 if (is_array($result) && array_key_exists('message', $result)) {
516 return $result['message'];