Version 1
[yaffs-website] / vendor / consolidation / annotated-command / tests / testFullStack.php
1 <?php
2 namespace Consolidation\AnnotatedCommand;
3
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;
26
27 /**
28  * Do a test of all of the classes in this project, top-to-bottom.
29  */
30 class FullStackTests extends \PHPUnit_Framework_TestCase
31 {
32     protected $application;
33     protected $commandFactory;
34
35     function setup() {
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);
44     }
45
46     function testValidFormats()
47     {
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());
54     }
55
56     function testAutomaticOptions()
57     {
58         $commandFileInstance = new \Consolidation\TestUtils\alpha\AlphaCommandFile;
59         $formatter = new FormatterManager();
60         $formatter->addDefaultFormatters();
61         $formatter->addDefaultSimplifiers();
62
63         $this->commandFactory->commandProcessor()->setFormatterManager($formatter);
64         $commandInfo = $this->commandFactory->createCommandInfo($commandFileInstance, 'exampleTable');
65
66         $command = $this->commandFactory->createCommand($commandInfo, $commandFileInstance);
67         $this->application->add($command);
68
69         $containsList =
70         [
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: ""]',
73         ];
74         $this->assertRunCommandViaApplicationContains('help example:table', $containsList);
75     }
76
77     function testCommandsAndHooks()
78     {
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();
84         $discovery
85           ->setSearchPattern('*CommandFile.php')
86           ->setIncludeFilesAtBase(false)
87           ->setSearchLocations(['alpha']);
88
89         chdir(__DIR__);
90         $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
91
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);
101
102         // Create a new factory, and load all of the files
103         // discovered above.
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);
110             }
111         } );
112         $factory->setIncludeAllPublicMethods(false);
113         $this->addDiscoveredCommands($factory, $commandFiles);
114
115         $this->assertRunCommandViaApplicationContains('list', ['example:table'], ['additional:option', 'without:annotations']);
116
117         $this->assertTrue($this->application->has('example:table'));
118         $this->assertFalse($this->application->has('without:annotations'));
119
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');
125
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));
132
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');
142
143         // Add some hooks.
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());
149
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.');
157
158         $expected = <<<EOT
159  ------ ------ -------
160   I      II     III
161  ------ ------ -------
162   One    Two    Three
163   Eins   Zwei   Drei
164   Ichi   Ni     San
165   Uno    Dos    Tres
166  ------ ------ -------
167 EOT;
168         $this->assertRunCommandViaApplicationEquals('example:table', $expected);
169
170         $expected = <<<EOT
171  ------- ------
172   III     II
173  ------- ------
174   Three   Two
175   Drei    Zwei
176   San     Ni
177   Tres    Dos
178  ------- ------
179 EOT;
180         $this->assertRunCommandViaApplicationEquals('example:table --fields=III,II', $expected);
181
182         $expectedSingleField = <<<EOT
183 Two
184 Zwei
185 Ni
186 Dos
187 EOT;
188
189         // When --field is specified (instead of --fields), then the format
190         // is forced to 'string'.
191         $this->assertRunCommandViaApplicationEquals('example:table --field=II', $expectedSingleField);
192
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',
197             [
198                 'Option added by @hook option example:table',
199                 'example:table --french',
200                 'Add a row with French numbers.'
201             ],
202             [
203                 'chinese',
204                 'kanji',
205             ]
206         );
207
208         $expectedOutputWithFrench = <<<EOT
209  ------ ------ -------
210   I      II     III
211  ------ ------ -------
212   One    Two    Three
213   Eins   Zwei   Drei
214   Ichi   Ni     San
215   Uno    Dos    Tres
216   Un     Deux   Trois
217  ------ ------ -------
218 EOT;
219         $this->assertRunCommandViaApplicationEquals('example:table --french', $expectedOutputWithFrench);
220
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  --------------- ----------------------------------------------------------------------------------------
227 EOT;
228         $this->assertRunCommandViaApplicationEquals('example:list', $expectedAssociativeListTable);
229         $this->assertRunCommandViaApplicationEquals('example:list --field=sftp_command', 'sftp -o Port=2222 dev@appserver.dev.drush.in');
230
231         $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
232         $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
233
234         $this->assertRunCommandViaApplicationContains('help example:wrap',
235             [
236                 'Test word wrapping',
237                 '[default: "table"]',
238             ]
239         );
240
241         $expectedUnwrappedOutput = <<<EOT
242 -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
243   First                                                                                                                      Second
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  -------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------
247 EOT;
248         $this->application->setWidthAndHeight(0, 0);
249         $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedUnwrappedOutput);
250
251         $expectedWrappedOutput = <<<EOT
252  ------------------- --------------------
253   First               Second
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  ------------------- --------------------
264 EOT;
265         $this->application->setWidthAndHeight(42, 24);
266         $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedWrappedOutput);
267     }
268
269     function testCommandsAndHooksIncludeAllPublicMethods()
270     {
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();
276         $discovery
277           ->setSearchPattern('*CommandFile.php')
278           ->setIncludeFilesAtBase(false)
279           ->setSearchLocations(['alpha']);
280
281         chdir(__DIR__);
282         $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
283
284         $formatter = new FormatterManager();
285         $formatter->addDefaultFormatters();
286         $formatter->addDefaultSimplifiers();
287         $hookManager = new HookManager();
288         $commandProcessor = new CommandProcessor($hookManager);
289         $commandProcessor->setFormatterManager($formatter);
290
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(...);
298
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'));
304
305         $this->assertRunCommandViaApplicationContains('list', ['example:table', 'without:annotations'], ['alter:formatters']);
306
307         $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious');
308         $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1);
309     }
310
311     function testCommandsAndHooksWithBetaFolder()
312     {
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();
318         $discovery
319           ->setSearchPattern('*CommandFile.php')
320           ->setIncludeFilesAtBase(false)
321           ->setSearchLocations(['alpha', 'beta']);
322
323         chdir(__DIR__);
324         $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
325
326         $formatter = new FormatterManager();
327         $formatter->addDefaultFormatters();
328         $formatter->addDefaultSimplifiers();
329         $hookManager = new HookManager();
330         $commandProcessor = new CommandProcessor($hookManager);
331         $commandProcessor->setFormatterManager($formatter);
332
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);
342
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));
352
353         $this->assertContains('alterFormattersChinese', var_export($allHooksForExampleTable, true));
354
355         $alterHooksForExampleTable = $this->callProtected($hookManager, 'getHooks', [['example:table'], 'alter']);
356         $this->assertContains('alterFormattersKanji', var_export($alterHooksForExampleTable, true));
357
358         $allHooksForAnyCommand = $allRegisteredHooks['*'];
359         $allHookPhasesForAnyCommand = array_keys($allHooksForAnyCommand);
360         sort($allHookPhasesForAnyCommand);
361         $this->assertEquals('alter', implode(',', $allHookPhasesForAnyCommand));
362
363         $this->assertContains('alterFormattersKanji', var_export($allHooksForAnyCommand, true));
364
365         // Help should have the information from the hooks in the 'beta' folder
366         $this->assertRunCommandViaApplicationContains('help example:table',
367             [
368                 'Option added by @hook option example:table',
369                 'example:table --french',
370                 'Add a row with French numbers.',
371                 'chinese',
372                 'kanji',
373             ]
374         );
375
376         // Confirm that the "unavailable" command is now available
377         $this->assertTrue($this->application->has('unavailable:command'));
378
379         $expectedOutputWithChinese = <<<EOT
380  ------ ------ -------
381   I      II     III
382  ------ ------ -------
383   One    Two    Three
384   Eins   Zwei   Drei
385   Ichi   Ni     San
386   Uno    Dos    Tres
387   壹     貳     叁
388  ------ ------ -------
389 EOT;
390         $this->assertRunCommandViaApplicationEquals('example:table --chinese', $expectedOutputWithChinese);
391
392         $expectedOutputWithKanji = <<<EOT
393  ------ ------ -------
394   I      II     III
395  ------ ------ -------
396   One    Two    Three
397   Eins   Zwei   Drei
398   Ichi   Ni     San
399   Uno    Dos    Tres
400   一     二     三
401  ------ ------ -------
402 EOT;
403         $this->assertRunCommandViaApplicationEquals('example:table --kanji', $expectedOutputWithKanji);
404     }
405
406     public function addDiscoveredCommands($factory, $commandFiles) {
407         foreach ($commandFiles as $path => $commandClass) {
408             $this->assertFileExists($path);
409             if (!class_exists($commandClass)) {
410                 include $path;
411             }
412             $commandInstance = new $commandClass();
413             $commandList = $factory->createCommandsFromClass($commandInstance);
414             foreach ($commandList as $command) {
415                 $this->application->add($command);
416             }
417         }
418     }
419
420     function assertRunCommandViaApplicationEquals($cmd, $expectedOutput, $expectedStatusCode = 0)
421     {
422         $input = new StringInput($cmd);
423         $output = new BufferedOutput();
424
425         $statusCode = $this->application->run($input, $output);
426         $commandOutput = trim($output->fetch());
427
428         $expectedOutput = $this->simplifyWhitespace($expectedOutput);
429         $commandOutput = $this->simplifyWhitespace($commandOutput);
430
431         $this->assertEquals($expectedOutput, $commandOutput);
432         $this->assertEquals($expectedStatusCode, $statusCode);
433     }
434
435     function assertRunCommandViaApplicationContains($cmd, $containsList, $doesNotContainList = [], $expectedStatusCode = 0)
436     {
437         $input = new StringInput($cmd);
438         $output = new BufferedOutput();
439         $containsList = (array) $containsList;
440
441         $statusCode = $this->application->run($input, $output);
442         $commandOutput = trim($output->fetch());
443
444         $commandOutput = $this->simplifyWhitespace($commandOutput);
445
446         foreach ($containsList as $expectedToContain) {
447             $this->assertContains($this->simplifyWhitespace($expectedToContain), $commandOutput);
448         }
449         foreach ($doesNotContainList as $expectedToNotContain) {
450             $this->assertNotContains($this->simplifyWhitespace($expectedToNotContain), $commandOutput);
451         }
452         $this->assertEquals($expectedStatusCode, $statusCode);
453     }
454
455     function simplifyWhitespace($data)
456     {
457         return trim(preg_replace('#[ \t]+$#m', '', $data));
458     }
459
460     function callProtected($object, $method, $args = [])
461     {
462         $r = new \ReflectionMethod($object, $method);
463         $r->setAccessible(true);
464         return $r->invokeArgs($object, $args);
465     }
466
467 }
468
469 class ExampleValidator implements ValidatorInterface
470 {
471     public function validate(CommandData $commandData)
472     {
473         $args = $commandData->arguments();
474         if (isset($args['one']) && ($args['one'] == 'bet')) {
475             $commandData->input()->setArgument('one', 'replaced');
476             return $args;
477         }
478     }
479 }
480
481 class ExampleResultProcessor implements ProcessResultInterface
482 {
483     public function process($result, CommandData $commandData)
484     {
485         if (is_array($result) && array_key_exists('item-list', $result)) {
486             return implode(',', $result['item-list']);
487         }
488     }
489 }
490
491 class ExampleResultAlterer implements AlterResultInterface
492 {
493     public function process($result, CommandData $commandData)
494     {
495         if (is_string($result) && ($result == 'Hello, World.')) {
496             return 'Hello, World!';
497         }
498     }
499 }
500
501 class ExampleStatusDeterminer implements StatusDeterminerInterface
502 {
503     public function determineStatusCode($result)
504     {
505         if (is_array($result) && array_key_exists('status-code', $result)) {
506             return $result['status-code'];
507         }
508     }
509 }
510
511 class ExampleOutputExtractor implements ExtractOutputInterface
512 {
513     public function extractOutput($result)
514     {
515         if (is_array($result) && array_key_exists('message', $result)) {
516             return $result['message'];
517         }
518     }
519 }