application = new ApplicationWithTerminalWidth('TestApplication', '0.0.0'); $this->commandFactory = new AnnotatedCommandFactory(); $alterOptionsEventManager = new AlterOptionsCommandEvent($this->application); $eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher(); $eventDispatcher->addSubscriber($this->commandFactory->commandProcessor()->hookManager()); $this->commandFactory->commandProcessor()->hookManager()->addCommandEvent($alterOptionsEventManager); $this->application->setDispatcher($eventDispatcher); $this->application->setAutoExit(false); } function testValidFormats() { $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $commandInfo = CommandInfo::create('\Consolidation\TestUtils\alpha\AlphaCommandFile', 'exampleTable'); $this->assertEquals('example:table', $commandInfo->getName()); $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $commandInfo->getReturnType()); } function testAutomaticOptions() { $commandFileInstance = new \Consolidation\TestUtils\alpha\AlphaCommandFile; $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $this->commandFactory->commandProcessor()->setFormatterManager($formatter); $this->assertAutomaticOptionsForCommand($commandFileInstance, 'exampleTable', 'example:table'); $this->assertAutomaticOptionsForCommand($commandFileInstance, 'exampleTableTwo', 'example:table2'); } function assertAutomaticOptionsForCommand($commandFileInstance, $functionName, $commandName) { $commandInfo = $this->commandFactory->createCommandInfo($commandFileInstance, $functionName); $command = $this->commandFactory->createCommand($commandInfo, $commandFileInstance); $this->application->add($command); $containsList = [ '--format[=FORMAT] Format the result data. Available formats: csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml [default: "table"]', '--fields[=FIELDS] Available fields: I (first), II (second), III (third) [default: ""]', ]; $this->assertRunCommandViaApplicationContains('help ' . $commandName, $containsList); } function testCommandsAndHooks() { // First, search for commandfiles in the 'alpha' // directory. Note that this same functionality // is tested more thoroughly in isolation in // testCommandFileDiscovery.php $discovery = new CommandFileDiscovery(); $discovery ->setSearchPattern('*CommandFile.php') ->setIncludeFilesAtBase(false) ->setSearchLocations(['alpha']); chdir(__DIR__); $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils'); $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $hookManager = new HookManager(); $terminalWidthOption = new PrepareTerminalWidthOption(); $terminalWidthOption->enableWrap(true); $terminalWidthOption->setApplication($this->application); $testTerminal = new TestTerminal(0); $terminalWidthOption->setTerminal($testTerminal); $commandProcessor = new CommandProcessor($hookManager); $commandProcessor->setFormatterManager($formatter); $commandProcessor->addPrepareFormatter($terminalWidthOption); // Create a new factory, and load all of the files // discovered above. $factory = new AnnotatedCommandFactory(); $factory->setCommandProcessor($commandProcessor); // Add a listener to configure our command handler object $factory->addListernerCallback(function($command) use($hookManager) { if ($command instanceof CustomEventAwareInterface) { $command->setHookManager($hookManager); } } ); $factory->setIncludeAllPublicMethods(false); $this->addDiscoveredCommands($factory, $commandFiles); $this->assertRunCommandViaApplicationContains('list', ['example:table'], ['additional:option', 'without:annotations']); $this->assertTrue($this->application->has('example:table')); $this->assertFalse($this->application->has('without:annotations')); // Run the use:event command that defines a custom event, my-event. $this->assertRunCommandViaApplicationEquals('use:event', 'one,two'); // Watch as we dynamically add a custom event to the hook manager to change the command results: $hookManager->add(function () { return 'three'; }, HookManager::ON_EVENT, 'my-event'); $this->assertRunCommandViaApplicationEquals('use:event', 'one,three,two'); // Fetch a reference to the 'example:table' command and test its valid format types $exampleTableCommand = $this->application->find('example:table'); $returnType = $exampleTableCommand->getReturnType(); $this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $returnType); $validFormats = $formatter->validFormats($returnType); $this->assertEquals('csv,json,list,php,print-r,sections,string,table,tsv,var_export,xml,yaml', implode(',', $validFormats)); // Control: run commands without hooks. $this->assertRunCommandViaApplicationEquals('always:fail', 'This command always fails.', 13); $this->assertRunCommandViaApplicationEquals('simulated:status', '42'); $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World.'); $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphabet'); $this->assertRunCommandViaApplicationEquals('example:echo a b c', "a\tb\tc"); $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.'); $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument', 'Hello, world'); $this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument Joe', 'Hello, Joe'); // Add some hooks. $factory->hookManager()->addValidator(new ExampleValidator()); $factory->hookManager()->addResultProcessor(new ExampleResultProcessor()); $factory->hookManager()->addAlterResult(new ExampleResultAlterer()); $factory->hookManager()->addStatusDeterminer(new ExampleStatusDeterminer()); $factory->hookManager()->addOutputExtractor(new ExampleOutputExtractor()); // Run the same commands as before, and confirm that results // are different now that the hooks are in place. $this->assertRunCommandViaApplicationEquals('simulated:status', '', 42); $this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World!'); $this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphareplaced'); $this->assertRunCommandViaApplicationEquals('example:echo a b c', 'a,b,c'); $this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.'); $expected = <<assertRunCommandViaApplicationEquals('example:table', $expected); $expected = <<assertRunCommandViaApplicationEquals('example:table --fields=III,II', $expected); $expectedSingleField = <<assertRunCommandViaApplicationEquals('example:table --field=II', $expectedSingleField); // Check the help for the example table command and see if the options // from the alter hook were added. We expect that we should not see // any of the information from the alter hook in the 'beta' folder yet. $this->assertRunCommandViaApplicationContains('help example:table', [ 'Option added by @hook option example:table', 'example:table --french', 'Add a row with French numbers.' ], [ 'chinese', 'kanji', ] ); $expectedOutputWithFrench = <<assertRunCommandViaApplicationEquals('example:table --french', $expectedOutputWithFrench); $expectedAssociativeListTable = <<assertRunCommandViaApplicationEquals('example:list', $expectedAssociativeListTable); $this->assertRunCommandViaApplicationEquals('example:list --field=sftp_command', 'sftp -o Port=2222 dev@appserver.dev.drush.in'); $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious'); $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1); $this->assertRunCommandViaApplicationContains('help example:wrap', [ 'Test word wrapping', '[default: "table"]', ] ); $expectedUnwrappedOutput = <<application->setWidthAndHeight(0, 0); $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedUnwrappedOutput); $expectedWrappedOutput = <<application->setWidthAndHeight(42, 24); $testTerminal->setWidth(42); $this->assertRunCommandViaApplicationEquals('example:wrap', $expectedWrappedOutput); } function testCommandsAndHooksIncludeAllPublicMethods() { // First, search for commandfiles in the 'alpha' // directory. Note that this same functionality // is tested more thoroughly in isolation in // testCommandFileDiscovery.php $discovery = new CommandFileDiscovery(); $discovery ->setSearchPattern('*CommandFile.php') ->setIncludeFilesAtBase(false) ->setSearchLocations(['alpha']); chdir(__DIR__); $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils'); $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $hookManager = new HookManager(); $commandProcessor = new CommandProcessor($hookManager); $commandProcessor->setFormatterManager($formatter); // Create a new factory, and load all of the files // discovered above. The command factory class is // tested in isolation in testAnnotatedCommandFactory.php, // but this is the only place where $factory = new AnnotatedCommandFactory(); $factory->setCommandProcessor($commandProcessor); // $factory->addListener(...); // Now we will once again add all commands, this time including all // public methods. The command 'withoutAnnotations' should now be found. $factory->setIncludeAllPublicMethods(true); $this->addDiscoveredCommands($factory, $commandFiles); $this->assertTrue($this->application->has('without:annotations')); $this->assertRunCommandViaApplicationContains('list', ['example:table', 'without:annotations'], ['alter:formatters']); $this->assertRunCommandViaApplicationEquals('get:serious', 'very serious'); $this->assertRunCommandViaApplicationContains('get:lost', 'Command "get:lost" is not defined.', [], 1); } function testCommandsAndHooksWithBetaFolder() { // First, search for commandfiles in the 'alpha' // directory. Note that this same functionality // is tested more thoroughly in isolation in // testCommandFileDiscovery.php $discovery = new CommandFileDiscovery(); $discovery ->setSearchPattern('*CommandFile.php') ->setIncludeFilesAtBase(false) ->setSearchLocations(['alpha', 'beta']); chdir(__DIR__); $commandFiles = $discovery->discover('.', '\Consolidation\TestUtils'); $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $hookManager = new HookManager(); $commandProcessor = new CommandProcessor($hookManager); $commandProcessor->setFormatterManager($formatter); // Create a new factory, and load all of the files // discovered above. The command factory class is // tested in isolation in testAnnotatedCommandFactory.php, // but this is the only place where $factory = new AnnotatedCommandFactory(); $factory->setCommandProcessor($commandProcessor); // $factory->addListener(...); $factory->setIncludeAllPublicMethods(true); $this->addDiscoveredCommands($factory, $commandFiles); // A few asserts, to make sure that our hooks all get registered. $allRegisteredHooks = $hookManager->getAllHooks(); $registeredHookNames = array_keys($allRegisteredHooks); sort($registeredHookNames); $this->assertEquals('*,example:table,my-event', implode(',', $registeredHookNames)); $allHooksForExampleTable = $allRegisteredHooks['example:table']; $allHookPhasesForExampleTable = array_keys($allHooksForExampleTable); sort($allHookPhasesForExampleTable); $this->assertEquals('alter,option', implode(',', $allHookPhasesForExampleTable)); $this->assertContains('alterFormattersChinese', var_export($allHooksForExampleTable, true)); $alterHooksForExampleTable = $this->callProtected($hookManager, 'getHooks', [['example:table'], 'alter']); $this->assertContains('alterFormattersKanji', var_export($alterHooksForExampleTable, true)); $allHooksForAnyCommand = $allRegisteredHooks['*']; $allHookPhasesForAnyCommand = array_keys($allHooksForAnyCommand); sort($allHookPhasesForAnyCommand); $this->assertEquals('alter', implode(',', $allHookPhasesForAnyCommand)); $this->assertContains('alterFormattersKanji', var_export($allHooksForAnyCommand, true)); // Help should have the information from the hooks in the 'beta' folder $this->assertRunCommandViaApplicationContains('help example:table', [ 'Option added by @hook option example:table', 'example:table --french', 'Add a row with French numbers.', 'chinese', 'kanji', ] ); // Confirm that the "unavailable" command is now available $this->assertTrue($this->application->has('unavailable:command')); $expectedOutputWithChinese = <<assertRunCommandViaApplicationEquals('example:table --chinese', $expectedOutputWithChinese); $expectedOutputWithKanji = <<assertRunCommandViaApplicationEquals('example:table --kanji', $expectedOutputWithKanji); } public function addDiscoveredCommands($factory, $commandFiles) { foreach ($commandFiles as $path => $commandClass) { $this->assertFileExists($path); if (!class_exists($commandClass)) { include $path; } $commandInstance = new $commandClass(); $commandList = $factory->createCommandsFromClass($commandInstance); foreach ($commandList as $command) { $this->application->add($command); } } } function assertRunCommandViaApplicationEquals($cmd, $expectedOutput, $expectedStatusCode = 0) { $input = new StringInput($cmd); $output = new BufferedOutput(); $statusCode = $this->application->run($input, $output); $commandOutput = trim($output->fetch()); $expectedOutput = $this->simplifyWhitespace($expectedOutput); $commandOutput = $this->simplifyWhitespace($commandOutput); $this->assertEquals($expectedOutput, $commandOutput); $this->assertEquals($expectedStatusCode, $statusCode); } function assertRunCommandViaApplicationContains($cmd, $containsList, $doesNotContainList = [], $expectedStatusCode = 0) { $input = new StringInput($cmd); $output = new BufferedOutput(); $containsList = (array) $containsList; $statusCode = $this->application->run($input, $output); $commandOutput = trim($output->fetch()); $commandOutput = $this->simplifyWhitespace($commandOutput); foreach ($containsList as $expectedToContain) { $this->assertContains($this->simplifyWhitespace($expectedToContain), $commandOutput); } foreach ($doesNotContainList as $expectedToNotContain) { $this->assertNotContains($this->simplifyWhitespace($expectedToNotContain), $commandOutput); } $this->assertEquals($expectedStatusCode, $statusCode); } function simplifyWhitespace($data) { return trim(preg_replace('#\s+$#m', '', $data)); } function callProtected($object, $method, $args = []) { $r = new \ReflectionMethod($object, $method); $r->setAccessible(true); return $r->invokeArgs($object, $args); } } class ExampleValidator implements ValidatorInterface { public function validate(CommandData $commandData) { $args = $commandData->arguments(); if (isset($args['one']) && ($args['one'] == 'bet')) { $commandData->input()->setArgument('one', 'replaced'); return $args; } } } class ExampleResultProcessor implements ProcessResultInterface { public function process($result, CommandData $commandData) { if (is_array($result) && array_key_exists('item-list', $result)) { return implode(',', $result['item-list']); } } } class ExampleResultAlterer implements AlterResultInterface { public function process($result, CommandData $commandData) { if (is_string($result) && ($result == 'Hello, World.')) { return 'Hello, World!'; } } } class ExampleStatusDeterminer implements StatusDeterminerInterface { public function determineStatusCode($result) { if (is_array($result) && array_key_exists('status-code', $result)) { return $result['status-code']; } } } class ExampleOutputExtractor implements ExtractOutputInterface { public function extractOutput($result) { if (is_array($result) && array_key_exists('message', $result)) { return $result['message']; } } }