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 Consolidation\TestUtils\TestTerminal;
17 use Symfony\Component\Console\Application;
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\StringInput;
21 use Symfony\Component\Console\Output\BufferedOutput;
22 use Symfony\Component\Console\Output\OutputInterface;
23 use Consolidation\TestUtils\ApplicationWithTerminalWidth;
24 use Consolidation\AnnotatedCommand\Options\PrepareTerminalWidthOption;
25 use Consolidation\AnnotatedCommand\Events\CustomEventAwareInterface;
26 use Consolidation\AnnotatedCommand\Events\CustomEventAwareTrait;
29 * Do a test of all of the classes in this project, top-to-bottom.
31 class FullStackTests extends \PHPUnit_Framework_TestCase
33 protected $application;
34 protected $commandFactory;
37 $this->application = new ApplicationWithTerminalWidth('TestApplication', '0.0.0');
38 $this->commandFactory = new AnnotatedCommandFactory();
39 $alterOptionsEventManager = new AlterOptionsCommandEvent($this->application);
40 $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
41 $eventDispatcher->addSubscriber($this->commandFactory->commandProcessor()->hookManager());
42 $this->commandFactory->commandProcessor()->hookManager()->addCommandEvent($alterOptionsEventManager);
43 $this->application->setDispatcher($eventDispatcher);
44 $this->application->setAutoExit(false);
47 function testValidFormats()
49 $formatter = new FormatterManager();
50 $formatter->addDefaultFormatters();
51 $formatter->addDefaultSimplifiers();
52 $commandInfo = CommandInfo::create('\Consolidation\TestUtils\alpha\AlphaCommandFile', 'exampleTable');
53 $this->assertEquals('example:table', $commandInfo->getName());
54 $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $commandInfo->getReturnType());
57 function testAutomaticOptions()
59 $commandFileInstance = new \Consolidation\TestUtils\alpha\AlphaCommandFile;
60 $formatter = new FormatterManager();
61 $formatter->addDefaultFormatters();
62 $formatter->addDefaultSimplifiers();
64 $this->commandFactory->commandProcessor()->setFormatterManager($formatter);
65 $this->assertAutomaticOptionsForCommand($commandFileInstance, 'exampleTable', 'example:table');
66 $this->assertAutomaticOptionsForCommand($commandFileInstance, 'exampleTableTwo', 'example:table2');
69 function assertAutomaticOptionsForCommand($commandFileInstance, $functionName, $commandName)
71 $commandInfo = $this->commandFactory->createCommandInfo($commandFileInstance, $functionName);
73 $command = $this->commandFactory->createCommand($commandInfo, $commandFileInstance);
74 $this->application->add($command);
78 '--format[=FORMAT] Format the result data. Available formats: csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml [default: "table"]',
79 '--fields[=FIELDS] Available fields: I (first), II (second), III (third) [default: ""]',
81 $this->assertRunCommandViaApplicationContains('help ' . $commandName, $containsList);
84 function testCommandsAndHooks()
86 // First, search for commandfiles in the 'alpha'
87 // directory. Note that this same functionality
88 // is tested more thoroughly in isolation in
89 // testCommandFileDiscovery.php
90 $discovery = new CommandFileDiscovery();
92 ->setSearchPattern('*CommandFile.php')
93 ->setIncludeFilesAtBase(false)
94 ->setSearchLocations(['alpha']);
97 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
99 $formatter = new FormatterManager();
100 $formatter->addDefaultFormatters();
101 $formatter->addDefaultSimplifiers();
102 $hookManager = new HookManager();
103 $terminalWidthOption = new PrepareTerminalWidthOption();
104 $terminalWidthOption->enableWrap(true);
105 $terminalWidthOption->setApplication($this->application);
106 $testTerminal = new TestTerminal(0);
107 $terminalWidthOption->setTerminal($testTerminal);
108 $commandProcessor = new CommandProcessor($hookManager);
109 $commandProcessor->setFormatterManager($formatter);
110 $commandProcessor->addPrepareFormatter($terminalWidthOption);
112 // Create a new factory, and load all of the files
114 $factory = new AnnotatedCommandFactory();
115 $factory->setCommandProcessor($commandProcessor);
116 // Add a listener to configure our command handler object
117 $factory->addListernerCallback(function($command) use($hookManager) {
118 if ($command instanceof CustomEventAwareInterface) {
119 $command->setHookManager($hookManager);
122 $factory->setIncludeAllPublicMethods(false);
123 $this->addDiscoveredCommands($factory, $commandFiles);
125 $this->assertRunCommandViaApplicationContains('list', ['example:table'], ['additional:option', 'without:annotations']);
127 $this->assertTrue($this->application->has('example:table'));
128 $this->assertFalse($this->application->has('without:annotations'));
130 // Run the use:event command that defines a custom event, my-event.
131 $this->assertRunCommandViaApplicationEquals('use:event', 'one,two');
132 // Watch as we dynamically add a custom event to the hook manager to change the command results:
133 $hookManager->add(function () { return 'three'; }, HookManager::ON_EVENT, 'my-event');
134 $this->assertRunCommandViaApplicationEquals('use:event', 'one,three,two');
136 // Fetch a reference to the 'example:table' command and test its valid format types
137 $exampleTableCommand = $this->application->find('example:table');
138 $returnType = $exampleTableCommand->getReturnType();
139 $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $returnType);
140 $validFormats = $formatter->validFormats($returnType);
141 $this->assertEquals('csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml', implode(',', $validFormats));
143 // Control: run commands without hooks.
144 $this->assertRunCommandViaApplicationEquals('always:fail', 'This command always fails.', 13);
145 $this->assertRunCommandViaApplicationEquals('simulated:status', '42');
146 $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World.');
147 $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphabet');
148 $this->assertRunCommandViaApplicationEquals('example:echo a b c', "a\tb\tc");
149 $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.');
150 $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument', 'Hello, world');
151 $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument Joe', 'Hello, Joe');
154 $factory->hookManager()->addValidator(new ExampleValidator());
155 $factory->hookManager()->addResultProcessor(new ExampleResultProcessor());
156 $factory->hookManager()->addAlterResult(new ExampleResultAlterer());
157 $factory->hookManager()->addStatusDeterminer(new ExampleStatusDeterminer());
158 $factory->hookManager()->addOutputExtractor(new ExampleOutputExtractor());
160 // Run the same commands as before, and confirm that results
161 // are different now that the hooks are in place.
162 $this->assertRunCommandViaApplicationEquals('simulated:status', '', 42);
163 $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World!');
164 $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphareplaced');
165 $this->assertRunCommandViaApplicationEquals('example:echo a b c', 'a,b,c');
166 $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.');
169 ------ ------ -------
171 ------ ------ -------
176 ------ ------ -------
178 $this->assertRunCommandViaApplicationEquals('example:table', $expected);
190 $this->assertRunCommandViaApplicationEquals('example:table --fields=III,II', $expected);
192 $expectedSingleField = <<<EOT
199 // When --field is specified (instead of --fields), then the format
200 // is forced to 'string'.
201 $this->assertRunCommandViaApplicationEquals('example:table --field=II', $expectedSingleField);
203 // Check the help for the example table command and see if the options
204 // from the alter hook were added. We expect that we should not see
205 // any of the information from the alter hook in the 'beta' folder yet.
206 $this->assertRunCommandViaApplicationContains('help example:table',
208 'Option added by @hook option example:table',
209 'example:table --french',
210 'Add a row with French numbers.'
218 $expectedOutputWithFrench = <<<EOT
219 ------ ------ -------
221 ------ ------ -------
227 ------ ------ -------
229 $this->assertRunCommandViaApplicationEquals('example:table --french', $expectedOutputWithFrench);
231 $expectedAssociativeListTable = <<<EOT
232 --------------- ----------------------------------------------------------------------------------------
233 SFTP Command sftp -o Port=2222 dev@appserver.dev.drush.in
234 Git Command git clone ssh://codeserver.dev@codeserver.dev.drush.in:2222/~/repository.git wp-update
235 MySQL Command mysql -u pantheon -p4b33cb -h dbserver.dev.drush.in -P 16191 pantheon
236 --------------- ----------------------------------------------------------------------------------------
238 $this->assertRunCommandViaApplicationEquals('example:list', $expectedAssociativeListTable);
239 $this->assertRunCommandViaApplicationEquals('example:list --field=sftp_command', 'sftp -o Port=2222 dev@appserver.dev.drush.in');
241 $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
242 $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
244 $this->assertRunCommandViaApplicationContains('help example:wrap',
246 'Test word wrapping',
247 '[default: "table"]',
251 $expectedUnwrappedOutput = <<<EOT
252 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
254 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
255 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.
256 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
258 $this->application->setWidthAndHeight(0, 0);
259 $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedUnwrappedOutput);
261 $expectedWrappedOutput = <<<EOT
262 ------------------ --------------------
264 ------------------ --------------------
265 This is a really This is the second
266 long cell that column of the same
267 contains a lot table. It is also
268 of data. When it very long, and
269 is rendered, it should be wrapped
270 should be across multiple
271 wrapped across lines, just like
272 multiple lines. the first column.
273 ------------------ --------------------
275 $this->application->setWidthAndHeight(42, 24);
276 $testTerminal->setWidth(42);
277 $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedWrappedOutput);
280 function testCommandsAndHooksIncludeAllPublicMethods()
282 // First, search for commandfiles in the 'alpha'
283 // directory. Note that this same functionality
284 // is tested more thoroughly in isolation in
285 // testCommandFileDiscovery.php
286 $discovery = new CommandFileDiscovery();
288 ->setSearchPattern('*CommandFile.php')
289 ->setIncludeFilesAtBase(false)
290 ->setSearchLocations(['alpha']);
293 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
295 $formatter = new FormatterManager();
296 $formatter->addDefaultFormatters();
297 $formatter->addDefaultSimplifiers();
298 $hookManager = new HookManager();
299 $commandProcessor = new CommandProcessor($hookManager);
300 $commandProcessor->setFormatterManager($formatter);
302 // Create a new factory, and load all of the files
303 // discovered above. The command factory class is
304 // tested in isolation in testAnnotatedCommandFactory.php,
305 // but this is the only place where
306 $factory = new AnnotatedCommandFactory();
307 $factory->setCommandProcessor($commandProcessor);
308 // $factory->addListener(...);
310 // Now we will once again add all commands, this time including all
311 // public methods. The command 'withoutAnnotations' should now be found.
312 $factory->setIncludeAllPublicMethods(true);
313 $this->addDiscoveredCommands($factory, $commandFiles);
314 $this->assertTrue($this->application->has('without:annotations'));
316 $this->assertRunCommandViaApplicationContains('list', ['example:table', 'without:annotations'], ['alter:formatters']);
318 $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
319 $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
322 function testCommandsAndHooksWithBetaFolder()
324 // First, search for commandfiles in the 'alpha'
325 // directory. Note that this same functionality
326 // is tested more thoroughly in isolation in
327 // testCommandFileDiscovery.php
328 $discovery = new CommandFileDiscovery();
330 ->setSearchPattern('*CommandFile.php')
331 ->setIncludeFilesAtBase(false)
332 ->setSearchLocations(['alpha', 'beta']);
335 $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
337 $formatter = new FormatterManager();
338 $formatter->addDefaultFormatters();
339 $formatter->addDefaultSimplifiers();
340 $hookManager = new HookManager();
341 $commandProcessor = new CommandProcessor($hookManager);
342 $commandProcessor->setFormatterManager($formatter);
344 // Create a new factory, and load all of the files
345 // discovered above. The command factory class is
346 // tested in isolation in testAnnotatedCommandFactory.php,
347 // but this is the only place where
348 $factory = new AnnotatedCommandFactory();
349 $factory->setCommandProcessor($commandProcessor);
350 // $factory->addListener(...);
351 $factory->setIncludeAllPublicMethods(true);
352 $this->addDiscoveredCommands($factory, $commandFiles);
354 // A few asserts, to make sure that our hooks all get registered.
355 $allRegisteredHooks = $hookManager->getAllHooks();
356 $registeredHookNames = array_keys($allRegisteredHooks);
357 sort($registeredHookNames);
358 $this->assertEquals('*,example:table,my-event', implode(',', $registeredHookNames));
359 $allHooksForExampleTable = $allRegisteredHooks['example:table'];
360 $allHookPhasesForExampleTable = array_keys($allHooksForExampleTable);
361 sort($allHookPhasesForExampleTable);
362 $this->assertEquals('alter,option', implode(',', $allHookPhasesForExampleTable));
364 $this->assertContains('alterFormattersChinese', var_export($allHooksForExampleTable, true));
366 $alterHooksForExampleTable = $this->callProtected($hookManager, 'getHooks', [['example:table'], 'alter']);
367 $this->assertContains('alterFormattersKanji', var_export($alterHooksForExampleTable, true));
369 $allHooksForAnyCommand = $allRegisteredHooks['*'];
370 $allHookPhasesForAnyCommand = array_keys($allHooksForAnyCommand);
371 sort($allHookPhasesForAnyCommand);
372 $this->assertEquals('alter', implode(',', $allHookPhasesForAnyCommand));
374 $this->assertContains('alterFormattersKanji', var_export($allHooksForAnyCommand, true));
376 // Help should have the information from the hooks in the 'beta' folder
377 $this->assertRunCommandViaApplicationContains('help example:table',
379 'Option added by @hook option example:table',
380 'example:table --french',
381 'Add a row with French numbers.',
387 // Confirm that the "unavailable" command is now available
388 $this->assertTrue($this->application->has('unavailable:command'));
390 $expectedOutputWithChinese = <<<EOT
391 ------ ------ -------
393 ------ ------ -------
399 ------ ------ -------
401 $this->assertRunCommandViaApplicationEquals('example:table --chinese', $expectedOutputWithChinese);
403 $expectedOutputWithKanji = <<<EOT
404 ------ ------ -------
406 ------ ------ -------
412 ------ ------ -------
414 $this->assertRunCommandViaApplicationEquals('example:table --kanji', $expectedOutputWithKanji);
417 public function addDiscoveredCommands($factory, $commandFiles) {
418 foreach ($commandFiles as $path => $commandClass) {
419 $this->assertFileExists($path);
420 if (!class_exists($commandClass)) {
423 $commandInstance = new $commandClass();
424 $commandList = $factory->createCommandsFromClass($commandInstance);
425 foreach ($commandList as $command) {
426 $this->application->add($command);
431 function assertRunCommandViaApplicationEquals($cmd, $expectedOutput, $expectedStatusCode = 0)
433 $input = new StringInput($cmd);
434 $output = new BufferedOutput();
436 $statusCode = $this->application->run($input, $output);
437 $commandOutput = trim($output->fetch());
439 $expectedOutput = $this->simplifyWhitespace($expectedOutput);
440 $commandOutput = $this->simplifyWhitespace($commandOutput);
442 $this->assertEquals($expectedOutput, $commandOutput);
443 $this->assertEquals($expectedStatusCode, $statusCode);
446 function assertRunCommandViaApplicationContains($cmd, $containsList, $doesNotContainList = [], $expectedStatusCode = 0)
448 $input = new StringInput($cmd);
449 $output = new BufferedOutput();
450 $containsList = (array) $containsList;
452 $statusCode = $this->application->run($input, $output);
453 $commandOutput = trim($output->fetch());
455 $commandOutput = $this->simplifyWhitespace($commandOutput);
457 foreach ($containsList as $expectedToContain) {
458 $this->assertContains($this->simplifyWhitespace($expectedToContain), $commandOutput);
460 foreach ($doesNotContainList as $expectedToNotContain) {
461 $this->assertNotContains($this->simplifyWhitespace($expectedToNotContain), $commandOutput);
463 $this->assertEquals($expectedStatusCode, $statusCode);
466 function simplifyWhitespace($data)
468 return trim(preg_replace('#\s+$#m', '', $data));
471 function callProtected($object, $method, $args = [])
473 $r = new \ReflectionMethod($object, $method);
474 $r->setAccessible(true);
475 return $r->invokeArgs($object, $args);
480 class ExampleValidator implements ValidatorInterface
482 public function validate(CommandData $commandData)
484 $args = $commandData->arguments();
485 if (isset($args['one']) && ($args['one'] == 'bet')) {
486 $commandData->input()->setArgument('one', 'replaced');
492 class ExampleResultProcessor implements ProcessResultInterface
494 public function process($result, CommandData $commandData)
496 if (is_array($result) && array_key_exists('item-list', $result)) {
497 return implode(',', $result['item-list']);
502 class ExampleResultAlterer implements AlterResultInterface
504 public function process($result, CommandData $commandData)
506 if (is_string($result) && ($result == 'Hello, World.')) {
507 return 'Hello, World!';
512 class ExampleStatusDeterminer implements StatusDeterminerInterface
514 public function determineStatusCode($result)
516 if (is_array($result) && array_key_exists('status-code', $result)) {
517 return $result['status-code'];
522 class ExampleOutputExtractor implements ExtractOutputInterface
524 public function extractOutput($result)
526 if (is_array($result) && array_key_exists('message', $result)) {
527 return $result['message'];