From 6d2748a59a271f7bbc913a236400cac1e595c68d Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 06:19:04 -0700 Subject: [PATCH 1/8] Inject options from rsync alias parameters. --- src/Commands/core/RsyncCommands.php | 60 +++++++++++++++++++++++++---- src/Runtime/DependencyInjection.php | 6 +-- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/Commands/core/RsyncCommands.php b/src/Commands/core/RsyncCommands.php index d20be80f69..7a02d6a10f 100644 --- a/src/Commands/core/RsyncCommands.php +++ b/src/Commands/core/RsyncCommands.php @@ -9,10 +9,15 @@ use Drush\SiteAlias\SiteAliasManagerAwareInterface; use Drush\SiteAlias\SiteAliasManagerAwareTrait; use Drush\Backend\BackendPathEvaluator; +use Robo\Contract\ConfigAwareInterface; +use Robo\Common\ConfigAwareTrait; +use Drush\Config\ConfigLocator; +use Symfony\Component\Console\Event\ConsoleCommandEvent; -class RsyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface +class RsyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface, ConfigAwareInterface { use SiteAliasManagerAwareTrait; + use ConfigAwareTrait; /** * These are arguments after the aliases and paths have been evaluated. @@ -105,17 +110,22 @@ public function rsyncOptions($options) } /** - * Validate that passed aliases are valid. + * Evaluate the path aliases in the source and destination + * parameters. We do this in the pre-command-event so that + * we can set up the configuration object to include options + * from the source and target aliases, if any, so that these + * values may participate in configuration injection. * - * @hook validate core-rsync - * @param \Consolidation\AnnotatedCommand\CommandData $commandData + * @hook command-event core:rsync + * @param ConsoleCommandEvent $event * @throws \Exception * @return void */ - public function validate(CommandData $commandData) + public function preCommandEvent(ConsoleCommandEvent $event) { - $destination = $commandData->input()->getArgument('destination'); - $source = $commandData->input()->getArgument('source'); + $input = $event->getInput(); + $destination = $input->getArgument('destination'); + $source = $input->getArgument('source'); $manager = $this->siteAliasManager(); $this->sourceEvaluatedPath = HostPath::create($manager, $source); @@ -124,6 +134,42 @@ public function validate(CommandData $commandData) $this->pathEvaluator->evaluate($this->sourceEvaluatedPath); $this->pathEvaluator->evaluate($this->destinationEvaluatedPath); + // The Drush configuration object is a ConfigOverlay; fetch the alias + // context, that already has the options et. al. from the + // site-selection alias ('drush @site rsync ...'), @self. + $aliasConfigContext = $this->getConfig()->getContext(ConfigLocator::ALIAS_CONTEXT); + + $this->injectAliasOptions($aliasConfigContext, $this->sourceEvaluatedPath->getAliasRecord(), 'source'); + $this->injectAliasOptions($aliasConfigContext, $this->destinationEvaluatedPath->getAliasRecord(), 'target'); + } + + /** + * Copy options from the source and destination aliases into the + * alias context. + */ + protected function injectAliasOptions($aliasConfigContext, $aliasRecord, $parameterSpecificOptions) + { + if (empty($aliasRecord)) { + return; + } + $aliasOptions = $aliasRecord->export(); + if (isset($aliasOptions[$parameterSpecificOptions])) { + $aliasOptions = array_merge($aliasOptions, $aliasOptions[$parameterSpecificOptions]); + unset($aliasOptions[$parameterSpecificOptions]); + } + $aliasConfigContext->import($aliasOptions + $aliasConfigContext->export()); + } + + /** + * Validate that passed aliases are valid. + * + * @hook validate core-rsync + * @param \Consolidation\AnnotatedCommand\CommandData $commandData + * @throws \Exception + * @return void + */ + public function validate(CommandData $commandData) + { if ($this->sourceEvaluatedPath->isRemote() && $this->destinationEvaluatedPath->isRemote()) { $msg = dt("Cannot specify two remote aliases. Instead, use one of the following alternate options:\n\n `drush {source} rsync @self {target}`\n `drush {source} rsync @self {fulltarget}\n\nUse the second form if the site alias definitions are not available at {source}.", array('source' => $source, 'target' => $destination, 'fulltarget' => $this->destinationEvaluatedPath->fullyQualifiedPath())); throw new \Exception($msg); diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 7b59990aad..36a176b3c9 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -128,11 +128,6 @@ protected static function alterServicesForDrush(ContainerInterface $container, A $factory = $container->get('commandFactory'); $factory->setIncludeAllPublicMethods(false); $factory->setDataStore($commandCacheDataStore); - - // It is necessary to set the dispatcher when using configureContainer - $eventDispatcher = $container->get('eventDispatcher'); - $eventDispatcher->addSubscriber($hookManager); - $application->setDispatcher($eventDispatcher); } protected static function injectApplicationServices(ContainerInterface $container, Application $application) @@ -142,5 +137,6 @@ protected static function injectApplicationServices(ContainerInterface $containe $application->setAliasManager($container->get('site.alias.manager')); $application->setRedispatchHook($container->get('redispatch.hook')); $application->setTildeExpansionHook($container->get('tildeExpansion.hook')); + $application->setDispatcher($container->get('eventDispatcher')); } } From 62eddb6d974b9edd3b88c1d8f5875ad134285df9 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 08:38:22 -0700 Subject: [PATCH 2/8] Fix a couple of bugs and add tests for rsync alias option injection. --- src/Commands/core/RsyncCommands.php | 56 +++++++++++++++++-- .../alias-fixtures/example.alias.yml | 24 ++++++++ tests/rsyncTest.php | 17 +++--- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/src/Commands/core/RsyncCommands.php b/src/Commands/core/RsyncCommands.php index 7a02d6a10f..ec8fbf94ff 100644 --- a/src/Commands/core/RsyncCommands.php +++ b/src/Commands/core/RsyncCommands.php @@ -95,7 +95,7 @@ public function rsyncOptions($options) $inc_ex_path = explode(PATH_SEPARATOR, @$options[$include_exclude . '-paths']); foreach ($inc_ex_path as $one_path_to_inc_ex) { if (!empty($one_path_to_inc_ex)) { - $paths = ' --' . $include_exclude . '="' . $one_path_to_inc_ex . '"'; + $paths .= ' --' . $include_exclude . '="' . $one_path_to_inc_ex . '"'; } } } @@ -152,12 +152,21 @@ protected function injectAliasOptions($aliasConfigContext, $aliasRecord, $parame if (empty($aliasRecord)) { return; } - $aliasOptions = $aliasRecord->export(); - if (isset($aliasOptions[$parameterSpecificOptions])) { - $aliasOptions = array_merge($aliasOptions, $aliasOptions[$parameterSpecificOptions]); - unset($aliasOptions[$parameterSpecificOptions]); + $aliasData = $aliasRecord->export(); + $aliasOptions = [ + 'options' => $aliasData['options'], + 'command' => $aliasData['command'], + ]; + if (isset($aliasData[$parameterSpecificOptions])) { + $aliasOptions = self::arrayMergeRecursiveDistinct($aliasOptions, $aliasData[$parameterSpecificOptions]); } - $aliasConfigContext->import($aliasOptions + $aliasConfigContext->export()); + // 'import' is supposed to merge, but in fact it overwrites. + // We will therefore manually merge as a workaround. + // print "starting alias values: " . var_export($aliasConfigContext->export(), true) . "\n"; + // print "merge into $parameterSpecificOptions: " . var_export($aliasOptions, true) . "\n"; + $merged = self::arrayMergeRecursiveDistinct($aliasOptions, $aliasConfigContext->export()); + $aliasConfigContext->import($merged); + // print "Result: " . var_export($aliasConfigContext, true) . "\n"; } /** @@ -175,4 +184,39 @@ public function validate(CommandData $commandData) throw new \Exception($msg); } } + + /** + * Merges arrays recursively while preserving. TODO: Factor this into a reusable utility class + * + * @param array $array1 + * @param array $array2 + * + * @return array + * + * @see http://php.net/manual/en/function.array-merge-recursive.php#92195 + * @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22 + */ + protected static function arrayMergeRecursiveDistinct( + array &$array1, + array &$array2 + ) { + $merged = $array1; + foreach ($array2 as $key => &$value) { + $merged[$key] = self::mergeRecursiveValue($merged, $key, $value); + } + return $merged; + } + + /** + * Process the value in an arrayMergeRecursiveDistinct - make a recursive + * call if needed. + */ + private static function mergeRecursiveValue(&$merged, $key, $value) + { + if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { + return self::arrayMergeRecursiveDistinct($merged[$key], $value); + } + return $value; + } + } diff --git a/tests/resources/alias-fixtures/example.alias.yml b/tests/resources/alias-fixtures/example.alias.yml index 554de31ee2..b33c7fd9b8 100644 --- a/tests/resources/alias-fixtures/example.alias.yml +++ b/tests/resources/alias-fixtures/example.alias.yml @@ -1,9 +1,33 @@ dev: root: /path/to/dev uri: dev + source: + command: + core: + rsync: + options: + include-paths: dev-source + target: + command: + core: + rsync: + options: + exclude-paths: dev-target stage: root: /path/to/stage uri: stage + source: + command: + core: + rsync: + options: + include-paths: stage-source + target: + command: + core: + rsync: + options: + exclude-paths: stage-target live: user: www-admin host: service-provider.com diff --git a/tests/rsyncTest.php b/tests/rsyncTest.php index 0784236792..ec27b8a3ef 100644 --- a/tests/rsyncTest.php +++ b/tests/rsyncTest.php @@ -23,19 +23,20 @@ public function testSimulated() { 'alias-path' => __DIR__ . '/resources/alias-fixtures', ]; - // Test simulated backend invoke - $this->drush('rsync', ['@example.dev', '@example.stage'], $options, 'user@server/path/to/drupal#sitename', NULL, self::EXIT_SUCCESS, '2>&1'); - $expected = "Simulating backend invoke: ssh -o PasswordAuthentication=no user@server 'drush --alias-path=__DIR__/resources/alias-fixtures --root=/path/to/drupal --uri=sitename --no-ansi rsync '\''@example.dev'\'' '\''@example.stage'\'' 2>&1' 2>&1"; - $this->assertOutputEquals($expected); - // Test simulated simple rsync with two local sites - $this->drush('rsync', ['@example.dev', '@example.stage'], $options, NULL, NULL, self::EXIT_SUCCESS, '2>&1'); - $expected = "Calling system(rsync -e 'ssh ' -akz /path/to/dev /path/to/stage);"; + $this->drush('rsync', ['@example.stage', '@example.dev'], $options, NULL, NULL, self::EXIT_SUCCESS, '2>&1'); + $expected = "Calling system(rsync -e 'ssh ' -akz --include=\"stage-source\" --exclude=\"dev-target\" /path/to/stage /path/to/dev);"; $this->assertOutputEquals($expected); // Test simulated rsync with relative paths $this->drush('rsync', ['@example.dev:files', '@example.stage:files'], $options, NULL, NULL, self::EXIT_SUCCESS, '2>&1'); - $expected = "Calling system(rsync -e 'ssh ' -akz /path/to/dev/files /path/to/stage/files);"; + $expected = "Calling system(rsync -e 'ssh ' -akz --include=\"dev-source\" --exclude=\"stage-target\" /path/to/dev/files /path/to/stage/files);"; + $this->assertOutputEquals($expected); + + // Test simulated backend invoke + // TODO: Note that command-specific options are not processed. Is this needed? Does Drush 8 support this? + $this->drush('rsync', ['@example.dev', '@example.stage'], $options, 'user@server/path/to/drupal#sitename', NULL, self::EXIT_SUCCESS, '2>&1'); + $expected = "Simulating backend invoke: ssh -o PasswordAuthentication=no user@server 'drush --alias-path=__DIR__/resources/alias-fixtures --root=/path/to/drupal --uri=sitename --no-ansi rsync '\''@example.dev'\'' '\''@example.stage'\'' 2>&1' 2>&1"; $this->assertOutputEquals($expected); } From 74255f4ebb48e93a25f43271fa6e68e1dcc4e474 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 11:15:23 -0700 Subject: [PATCH 3/8] Refactor alias parameter option injection code for better reusability. --- composer.json | 2 +- isolation/composer.json | 2 +- src/Commands/core/RsyncCommands.php | 102 ++++-------------- src/SiteAlias/AliasRecord.php | 53 ++++++++- .../alias-fixtures/example.alias.yml | 50 ++++----- tests/rsyncTest.php | 7 +- 6 files changed, 105 insertions(+), 111 deletions(-) diff --git a/composer.json b/composer.json index b7612be6f0..6b59cc2794 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/log": "~1.0", "psy/psysh": "~0.6", "league/container": "~2", - "consolidation/config": "^1.0.3", + "consolidation/config": "^1.0.5", "consolidation/robo": "^1.1.4", "symfony/config": "~2.2|^3", "chi-teck/drupal-code-generator": "^1.17.3", diff --git a/isolation/composer.json b/isolation/composer.json index c6f0a7423a..f547d47303 100644 --- a/isolation/composer.json +++ b/isolation/composer.json @@ -9,7 +9,7 @@ "php": ">=5.6.0", "ext-dom": "*", "psr/log": "~1.0", - "consolidation/config": "dev-master", + "consolidation/config": "^1.0.5", "grasmash/yaml-expander": "^1.1.1", "symfony/yaml": "~2.3|^3", "symfony/var-dumper": "~2.7|^3", diff --git a/src/Commands/core/RsyncCommands.php b/src/Commands/core/RsyncCommands.php index ec8fbf94ff..5fcc502d32 100644 --- a/src/Commands/core/RsyncCommands.php +++ b/src/Commands/core/RsyncCommands.php @@ -26,7 +26,7 @@ class RsyncCommands extends DrushCommands implements SiteAliasManagerAwareInterf /** @var HostPath */ public $sourceEvaluatedPath; /** @var HostPath */ - public $destinationEvaluatedPath; + public $targetEvaluatedPath; /** @var BackendPathEvaluator */ protected $pathEvaluator; @@ -42,7 +42,7 @@ public function __construct() * * @command core:rsync * @param $source A site alias and optional path. See rsync documentation and example.aliases.yml. - * @param $destination A site alias and optional path. See rsync documentation and example.aliases.config.yml.', + * @param $target A site alias and optional path. See rsync documentation and example.aliases.config.yml.', * @param $extra Additional parameters after the ssh statement. * @optionset_ssh * @option exclude-paths List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows). @@ -59,11 +59,11 @@ public function __construct() * @aliases rsync,core-rsync * @topics docs:aliases */ - public function rsync($source, $destination, array $extra, $options = ['exclude-paths' => self::REQ, 'include-paths' => self::REQ, 'mode' => 'akz']) + public function rsync($source, $target, array $extra, $options = ['exclude-paths' => self::REQ, 'include-paths' => self::REQ, 'mode' => 'akz']) { // Prompt for confirmation. This is destructive. if (!\Drush\Drush::simulate()) { - $this->output()->writeln(dt("You will delete files in !target and replace with data from !source", array('!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!target' => $this->destinationEvaluatedPath->fullyQualifiedPath()))); + $this->output()->writeln(dt("You will delete files in !target and replace with data from !source", array('!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!target' => $this->targetEvaluatedPath->fullyQualifiedPath()))); if (!$this->io()->confirm(dt('Do you want to continue?'))) { throw new UserAbortException(); } @@ -72,16 +72,16 @@ public function rsync($source, $destination, array $extra, $options = ['exclude- $rsync_options = $this->rsyncOptions($options); $parameters = array_merge([$rsync_options], $extra); $parameters[] = $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(); - $parameters[] = $this->destinationEvaluatedPath->fullyQualifiedPath(); + $parameters[] = $this->targetEvaluatedPath->fullyQualifiedPath(); $ssh_options = Drush::config()->get('ssh.options', ''); $exec = "rsync -e 'ssh $ssh_options'". ' '. implode(' ', array_filter($parameters)); $exec_result = drush_op_system($exec); if ($exec_result == 0) { - drush_backend_set_result($this->destinationEvaluatedPath->fullyQualifiedPath()); + drush_backend_set_result($this->targetEvaluatedPath->fullyQualifiedPath()); } else { - throw new \Exception(dt("Could not rsync from !source to !dest", array('!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!dest' => $this->destinationEvaluatedPath->fullyQualifiedPath()))); + throw new \Exception(dt("Could not rsync from !source to !dest", array('!source' => $this->sourceEvaluatedPath->fullyQualifiedPathPreservingTrailingSlash(), '!dest' => $this->targetEvaluatedPath->fullyQualifiedPath()))); } } @@ -124,49 +124,26 @@ public function rsyncOptions($options) public function preCommandEvent(ConsoleCommandEvent $event) { $input = $event->getInput(); - $destination = $input->getArgument('destination'); - $source = $input->getArgument('source'); - - $manager = $this->siteAliasManager(); - $this->sourceEvaluatedPath = HostPath::create($manager, $source); - $this->destinationEvaluatedPath = HostPath::create($manager, $destination); - - $this->pathEvaluator->evaluate($this->sourceEvaluatedPath); - $this->pathEvaluator->evaluate($this->destinationEvaluatedPath); + $this->sourceEvaluatedPath = $this->injectAliasPathParameterOptions($input, 'source'); + $this->targetEvaluatedPath = $this->injectAliasPathParameterOptions($input, 'target'); + } + protected function injectAliasPathParameterOptions($input, $parameterName) + { // The Drush configuration object is a ConfigOverlay; fetch the alias // context, that already has the options et. al. from the // site-selection alias ('drush @site rsync ...'), @self. $aliasConfigContext = $this->getConfig()->getContext(ConfigLocator::ALIAS_CONTEXT); + $manager = $this->siteAliasManager(); - $this->injectAliasOptions($aliasConfigContext, $this->sourceEvaluatedPath->getAliasRecord(), 'source'); - $this->injectAliasOptions($aliasConfigContext, $this->destinationEvaluatedPath->getAliasRecord(), 'target'); - } + $aliasName = $input->getArgument($parameterName); + $evaluatedPath = HostPath::create($manager, $aliasName); + $this->pathEvaluator->evaluate($evaluatedPath); - /** - * Copy options from the source and destination aliases into the - * alias context. - */ - protected function injectAliasOptions($aliasConfigContext, $aliasRecord, $parameterSpecificOptions) - { - if (empty($aliasRecord)) { - return; - } - $aliasData = $aliasRecord->export(); - $aliasOptions = [ - 'options' => $aliasData['options'], - 'command' => $aliasData['command'], - ]; - if (isset($aliasData[$parameterSpecificOptions])) { - $aliasOptions = self::arrayMergeRecursiveDistinct($aliasOptions, $aliasData[$parameterSpecificOptions]); - } - // 'import' is supposed to merge, but in fact it overwrites. - // We will therefore manually merge as a workaround. - // print "starting alias values: " . var_export($aliasConfigContext->export(), true) . "\n"; - // print "merge into $parameterSpecificOptions: " . var_export($aliasOptions, true) . "\n"; - $merged = self::arrayMergeRecursiveDistinct($aliasOptions, $aliasConfigContext->export()); - $aliasConfigContext->import($merged); - // print "Result: " . var_export($aliasConfigContext, true) . "\n"; + // Inject the source and target alias records into the alias config context. + $evaluatedPath->getAliasRecord()->injectIntoConfig($aliasConfigContext, $parameterName); + + return $evaluatedPath; } /** @@ -179,44 +156,9 @@ protected function injectAliasOptions($aliasConfigContext, $aliasRecord, $parame */ public function validate(CommandData $commandData) { - if ($this->sourceEvaluatedPath->isRemote() && $this->destinationEvaluatedPath->isRemote()) { - $msg = dt("Cannot specify two remote aliases. Instead, use one of the following alternate options:\n\n `drush {source} rsync @self {target}`\n `drush {source} rsync @self {fulltarget}\n\nUse the second form if the site alias definitions are not available at {source}.", array('source' => $source, 'target' => $destination, 'fulltarget' => $this->destinationEvaluatedPath->fullyQualifiedPath())); + if ($this->sourceEvaluatedPath->isRemote() && $this->targetEvaluatedPath->isRemote()) { + $msg = dt("Cannot specify two remote aliases. Instead, use one of the following alternate options:\n\n `drush {source} rsync @self {target}`\n `drush {source} rsync @self {fulltarget}\n\nUse the second form if the site alias definitions are not available at {source}.", array('source' => $source, 'target' => $target, 'fulltarget' => $this->targetEvaluatedPath->fullyQualifiedPath())); throw new \Exception($msg); } } - - /** - * Merges arrays recursively while preserving. TODO: Factor this into a reusable utility class - * - * @param array $array1 - * @param array $array2 - * - * @return array - * - * @see http://php.net/manual/en/function.array-merge-recursive.php#92195 - * @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22 - */ - protected static function arrayMergeRecursiveDistinct( - array &$array1, - array &$array2 - ) { - $merged = $array1; - foreach ($array2 as $key => &$value) { - $merged[$key] = self::mergeRecursiveValue($merged, $key, $value); - } - return $merged; - } - - /** - * Process the value in an arrayMergeRecursiveDistinct - make a recursive - * call if needed. - */ - private static function mergeRecursiveValue(&$merged, $key, $value) - { - if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { - return self::arrayMergeRecursiveDistinct($merged[$key], $value); - } - return $value; - } - } diff --git a/src/SiteAlias/AliasRecord.php b/src/SiteAlias/AliasRecord.php index 5340fb74b5..37dcce34c6 100644 --- a/src/SiteAlias/AliasRecord.php +++ b/src/SiteAlias/AliasRecord.php @@ -3,6 +3,7 @@ use Consolidation\Config\Config; use Consolidation\Config\ConfigInterface; +use Consolidation\Config\Util\ArrayUtil; /** * An alias record is a configuration record containing well-known items. @@ -229,9 +230,17 @@ public function localRoot() */ public function exportConfig() { - $data = $this->export(); + return $this->remap($this->export()); + } - foreach ($this->remapOptions() as $from => $to) { + /** + * Reconfigure data exported from the form it is expected to be in + * inside an alias record to the form it is expected to be in when + * inside a configuration file. + */ + protected function remap($data) + { + foreach ($this->remapOptionTable() as $from => $to) { if (isset($data[$from])) { unset($data[$from]); } @@ -244,6 +253,44 @@ public function exportConfig() return new Config($data); } + /** + * Copy options from the source and destination aliases into the + * alias context. + * + * This is essentially an `export` followed by `$config->combine()`. + * Parameter-specific options from the alias-parameters are also included. + * + * @param Config $config + * @param string $parameterName + * @return $this + */ + public function injectIntoConfig($config, $parameterName = '') + { + $aliasData = $this->export(); + $aliasOptions = $aliasData; + unset($aliasOptions['alias-parameters']); + $parameterSpecificData = $this->getParameterSpecificOptions($aliasData, $parameterName); + if (!empty($parameterSpecificData)) { + $aliasOptions = ArrayUtil::mergeRecursiveDistinct($aliasOptions, $parameterSpecificData); + } + // Combine the data from the parameter-specific + $config->combine($aliasOptions); + return $this; + } + + /** + * Fetch the parameter-specific options from the 'alias-parameters' section of the alias. + * @param string $parameterName + * @return array + */ + protected function getParameterSpecificOptions($aliasData, $parameterName) + { + if (!empty($parameterName) && $this->has("alias-parameters.{$parameterName}")) { + return $this->get("alias-parameters.{$parameterName}"); + } + return []; + } + /** * Convert the data in this record to the layout that was used * in the legacy code, for backwards compatiblity. @@ -257,7 +304,7 @@ public function legacyRecord() * Conversion table from old to new option names. These all implicitly * go in `options`, although they can come from different locations. */ - protected function remapOptions() + protected function remapOptionTable() { return [ 'user' => 'remote-user', diff --git a/tests/resources/alias-fixtures/example.alias.yml b/tests/resources/alias-fixtures/example.alias.yml index b33c7fd9b8..34f3660757 100644 --- a/tests/resources/alias-fixtures/example.alias.yml +++ b/tests/resources/alias-fixtures/example.alias.yml @@ -1,33 +1,35 @@ dev: root: /path/to/dev uri: dev - source: - command: - core: - rsync: - options: - include-paths: dev-source - target: - command: - core: - rsync: - options: - exclude-paths: dev-target + alias-parameters: + source: + command: + core: + rsync: + options: + include-paths: dev-source + target: + command: + core: + rsync: + options: + exclude-paths: dev-target stage: root: /path/to/stage uri: stage - source: - command: - core: - rsync: - options: - include-paths: stage-source - target: - command: - core: - rsync: - options: - exclude-paths: stage-target + alias-parameters: + source: + command: + core: + rsync: + options: + include-paths: stage-source + target: + command: + core: + rsync: + options: + exclude-paths: stage-target live: user: www-admin host: service-provider.com diff --git a/tests/rsyncTest.php b/tests/rsyncTest.php index ec27b8a3ef..23b744de80 100644 --- a/tests/rsyncTest.php +++ b/tests/rsyncTest.php @@ -33,8 +33,11 @@ public function testSimulated() { $expected = "Calling system(rsync -e 'ssh ' -akz --include=\"dev-source\" --exclude=\"stage-target\" /path/to/dev/files /path/to/stage/files);"; $this->assertOutputEquals($expected); - // Test simulated backend invoke - // TODO: Note that command-specific options are not processed. Is this needed? Does Drush 8 support this? + // Test simulated backend invoke. + // Note that command-specific options are not processed for remote + // targets. The aliases are not interpreted at all until they recach + // the remote side, at which point they will be evaluated & any needed + // injection will be done. $this->drush('rsync', ['@example.dev', '@example.stage'], $options, 'user@server/path/to/drupal#sitename', NULL, self::EXIT_SUCCESS, '2>&1'); $expected = "Simulating backend invoke: ssh -o PasswordAuthentication=no user@server 'drush --alias-path=__DIR__/resources/alias-fixtures --root=/path/to/drupal --uri=sitename --no-ansi rsync '\''@example.dev'\'' '\''@example.stage'\'' 2>&1' 2>&1"; $this->assertOutputEquals($expected); From b88e47b7656062558947df22bc130b74ffe4dc67 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 11:35:58 -0700 Subject: [PATCH 4/8] Inject data from source and target aliases in sql:sync command. --- src/Commands/sql/SqlSyncCommands.php | 100 ++++++++++++++++++--------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/src/Commands/sql/SqlSyncCommands.php b/src/Commands/sql/SqlSyncCommands.php index 2bb139c859..8de16335d0 100644 --- a/src/Commands/sql/SqlSyncCommands.php +++ b/src/Commands/sql/SqlSyncCommands.php @@ -10,10 +10,15 @@ use Drush\SiteAlias\SiteAliasManagerAwareTrait; use Symfony\Component\Config\Definition\Exception\Exception; use Webmozart\PathUtil\Path; +use Robo\Contract\ConfigAwareInterface; +use Robo\Common\ConfigAwareTrait; +use Drush\Config\ConfigLocator; +use Symfony\Component\Console\Event\ConsoleCommandEvent; -class SqlSyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface +class SqlSyncCommands extends DrushCommands implements SiteAliasManagerAwareInterface, ConfigAwareInterface { use SiteAliasManagerAwareTrait; + use ConfigAwareTrait; /** * Copy DB data from a source site to a target site. Transfers data via rsync. @@ -21,27 +26,27 @@ class SqlSyncCommands extends DrushCommands implements SiteAliasManagerAwareInte * @command sql:sync * @aliases sql-sync * @param $source A site-alias or the name of a subdirectory within /sites whose database you want to copy from. - * @param $destination A site-alias or the name of a subdirectory within /sites whose database you want to replace. + * @param $target A site-alias or the name of a subdirectory within /sites whose database you want to replace. * @optionset_table_selection * @option no-dump Do not dump the sql database; always use an existing dump file. * @option no-sync Do not rsync the database dump file from source to target. - * @option runner Where to run the rsync command; defaults to the local site. Can also be 'source' or 'destination'. + * @option runner Where to run the rsync command; defaults to the local site. Can also be 'source' or 'target'. * @option create-db Create a new database before importing the database dump on the target machine. * @option db-su Account to use when creating a new database (e.g. root). * @option db-su-pw Password for the db-su account. * @option source-dump The path for retrieving the sql-dump on source machine. - * @option target-dump The path for storing the sql-dump on destination machine. + * @option target-dump The path for storing the sql-dump on target machine. * @usage drush sql:sync @source @target * Copy the database from the site with the alias 'source' to the site with the alias 'target'. * @usage drush sql:sync #prod #dev * Copy the database from the site in /sites/prod to the site in /sites/dev (multisite installation). * @topics docs:aliases,docs:policy,docs:example-sync-via-http */ - public function sqlsync($source, $destination, $options = ['no-dump' => false, 'no-sync' => false, 'runner' => self::REQ, 'create-db' => false, 'db-su' => self::REQ, 'db-su-pw' => self::REQ, 'target-dump' => self::REQ, 'source-dump' => self::OPT]) + public function sqlsync($source, $target, $options = ['no-dump' => false, 'no-sync' => false, 'runner' => self::REQ, 'create-db' => false, 'db-su' => self::REQ, 'db-su-pw' => self::REQ, 'target-dump' => self::REQ, 'source-dump' => self::OPT]) { $manager = $this->siteAliasManager(); $sourceRecord = $manager->get($source); - $destinationRecord = $manager->get($destination); + $targetRecord = $manager->get($target); $backend_options = []; $global_options = Drush::redispatchOptions() + ['strict' => 0]; @@ -50,10 +55,10 @@ public function sqlsync($source, $destination, $options = ['no-dump' => false, ' $backend_options['backend-simulate'] = true; } - // Create destination DB if needed. + // Create target DB if needed. if ($options['create-db']) { - $this->logger()->notice(dt('Starting to create database on Destination.')); - $return = drush_invoke_process($destination, 'sql-create', array(), $global_options, $backend_options); + $this->logger()->notice(dt('Starting to create database on target.')); + $return = drush_invoke_process($target, 'sql-create', array(), $global_options, $backend_options); if ($return['error_status']) { throw new \Exception(dt('sql-create failed.')); } @@ -65,7 +70,7 @@ public function sqlsync($source, $destination, $options = ['no-dump' => false, ' 'result-file' => $options['source-dump'] ?: true, ); if (!$options['no-dump']) { - $this->logger()->notice(dt('Starting to dump database on Source.')); + $this->logger()->notice(dt('Starting to dump database on source.')); $return = drush_invoke_process($source, 'sql-dump', array(), $dump_options, $backend_options); if ($return['error_status']) { throw new \Exception(dt('sql-dump failed.')); @@ -80,22 +85,22 @@ public function sqlsync($source, $destination, $options = ['no-dump' => false, ' } $do_rsync = !$options['no-sync']; - // Determine path/to/dump on destination. + // Determine path/to/dump on target. if ($options['target-dump']) { - $destination_dump_path = $options['target-dump']; + $target_dump_path = $options['target-dump']; $backend_options['interactive'] = false; // @temporary: See https://github.com/drush-ops/drush/pull/555 - } elseif (!$sourceRecord->isRemote() && !$destinationRecord->isRemote()) { - $destination_dump_path = $source_dump_path; + } elseif (!$sourceRecord->isRemote() && !$targetRecord->isRemote()) { + $target_dump_path = $source_dump_path; $do_rsync = false; } else { $tmp = '/tmp'; // Our fallback plan. - $this->logger()->notice(dt('Starting to discover temporary files directory on Destination.')); - $return = drush_invoke_process($destination, 'core-status', array(), array(), array('integrate' => false, 'override-simulated' => true)); + $this->logger()->notice(dt('Starting to discover temporary files directory on target.')); + $return = drush_invoke_process($target, 'core-status', array(), array(), array('integrate' => false, 'override-simulated' => true)); if (!$return['error_status'] && isset($return['object']['drush-temp'])) { $tmp = $return['object']['drush-temp']; } - $destination_dump_path = Path::join($tmp, basename($source_dump_path)); - $backend_options['interactive'] = false; // No need to prompt as destination is a tmp file. + $target_dump_path = Path::join($tmp, basename($source_dump_path)); + $backend_options['interactive'] = false; // No need to prompt as target is a tmp file. } if ($do_rsync) { @@ -105,52 +110,79 @@ public function sqlsync($source, $destination, $options = ['no-dump' => false, ' $rsync_options[] = '--remove-source-files'; } if (!$runner = $options['runner']) { - $runner = $sourceRecord->isRemote() && $destinationRecord->isRemote() ? $destination : '@self'; + $runner = $sourceRecord->isRemote() && $targetRecord->isRemote() ? $target : '@self'; } // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync. // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync). - $return = drush_invoke_process($runner, 'core-rsync', array_merge(["$source:$source_dump_path", "$destination:$destination_dump_path", '--'], $rsync_options), [], $backend_options); - $this->logger()->notice(dt('Copying dump file from Source to Destination.')); + $return = drush_invoke_process($runner, 'core-rsync', array_merge(["$source:$source_dump_path", "$target:$target_dump_path", '--'], $rsync_options), [], $backend_options); + $this->logger()->notice(dt('Copying dump file from source to target.')); if ($return['error_status']) { throw new \Exception(dt('core-rsync failed.')); } } - // Import file into destination. - $this->logger()->notice(dt('Starting to import dump file onto Destination database.')); + // Import file into target. + $this->logger()->notice(dt('Starting to import dump file onto target database.')); $query_options = $global_options + array( - 'file' => $destination_dump_path, + 'file' => $target_dump_path, 'file-delete' => true, ); - $return = drush_invoke_process($destination, 'sql-query', array(), $query_options, $backend_options); + $return = drush_invoke_process($target, 'sql-query', array(), $query_options, $backend_options); if ($return['error_status']) { - throw new Exception('Failed to rsync the database dump from source to destination.'); + throw new Exception('Failed to rsync the database dump from source to target.'); } } + /** + * Inject options from source and target alias parameters. + * + * @hook command-event sql:sync + * @param ConsoleCommandEvent $event + */ + public function preCommandEvent(ConsoleCommandEvent $event) + { + $input = $event->getInput(); + $this->injectAliasPathParameterOptions($input, 'source'); + $this->injectAliasPathParameterOptions($input, 'target'); + } + + protected function injectAliasPathParameterOptions($input, $parameterName) + { + // The Drush configuration object is a ConfigOverlay; fetch the alias + // context, that already has the options et. al. from the + // site-selection alias ('drush @site rsync ...'), @self. + $aliasConfigContext = $this->getConfig()->getContext(ConfigLocator::ALIAS_CONTEXT); + $manager = $this->siteAliasManager(); + + $aliasName = $input->getArgument($parameterName); + + // Inject the source and target alias records into the alias config context. + $manager->get($aliasName)->injectIntoConfig($aliasConfigContext, $parameterName); + } + /** * @hook validate sql-sync */ public function validate(CommandData $commandData) { $source = $commandData->input()->getArgument('source'); - $destination = $commandData->input()->getArgument('destination'); - // Get destination info for confirmation prompt. + $target = $commandData->input()->getArgument('target'); + // Get target info for confirmation prompt. $manager = $this->siteAliasManager(); if (!$sourceRecord = $manager->get($source)) { throw new \Exception(dt('Error: no alias record could be found for source !source', array('!source' => $source))); } - if (!$destinationRecord = $manager->get($destination)) { - throw new \Exception(dt('Error: no alias record could be found for target !destination', array('!destination' => $destination))); + if (!$targetRecord = $manager->get($target)) { + throw new \Exception(dt('Error: no alias record could be found for target !target', array('!target' => $target))); } if (!$source_db_name = $this->databaseName($sourceRecord)) { throw new \Exception(dt('Error: no database record could be found for source !source', array('!source' => $source))); } - if (!$target_db_name = $this->databaseName($destinationRecord)) { - throw new \Exception(dt('Error: no database record could be found for target !destination', array('!destination' => $destination))); + if (!$target_db_name = $this->databaseName($targetRecord)) { + throw new \Exception(dt('Error: no database record could be found for target !target', array('!target' => $target))); } $txt_source = ($sourceRecord->remoteHost() ? $sourceRecord->remoteHost() . '/' : '') . $source_db_name; - $txt_destination = ($destinationRecord->remoteHost() ? $destinationRecord->remoteHost() . '/' : '') . $target_db_name; + $txt_target = ($targetRecord->remoteHost() ? $targetRecord->remoteHost() . '/' : '') . $target_db_name; if ($commandData->input()->getOption('no-dump') && !$commandData->input()->getOption('source-dump')) { throw new \Exception(dt('The --source-dump option must be supplied when --no-dump is specified.')); @@ -163,7 +195,7 @@ public function validate(CommandData $commandData) if (!Drush::simulate()) { $this->output()->writeln(dt("You will destroy data in !target and replace with data from !source.", array( '!source' => $txt_source, - '!target' => $txt_destination + '!target' => $txt_target ))); if (!$this->io()->confirm(dt('Do you really want to continue?'))) { throw new UserAbortException(); From 0b6660e4e610bc69f2fb1794094966c8e40fe776 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 13:12:39 -0700 Subject: [PATCH 5/8] Translate 'runner' values of 'source' or 'target' or 'destination' to the appropriate alias parameter. --- src/Commands/sql/SqlSyncCommands.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Commands/sql/SqlSyncCommands.php b/src/Commands/sql/SqlSyncCommands.php index 8de16335d0..1483c78a11 100644 --- a/src/Commands/sql/SqlSyncCommands.php +++ b/src/Commands/sql/SqlSyncCommands.php @@ -112,6 +112,12 @@ public function sqlsync($source, $target, $options = ['no-dump' => false, 'no-sy if (!$runner = $options['runner']) { $runner = $sourceRecord->isRemote() && $targetRecord->isRemote() ? $target : '@self'; } + if ($runner == 'source') { + $runner = $source; + } + if (($runner == 'target') || ($runner == 'destination')) { + $runner = $target; + } // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync. // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync). $return = drush_invoke_process($runner, 'core-rsync', array_merge(["$source:$source_dump_path", "$target:$target_dump_path", '--'], $rsync_options), [], $backend_options); From f8dba3f49be2efd3f5ceab843bfdadd2231337cc Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 20:45:28 -0700 Subject: [PATCH 6/8] Ensure that drush-script is passed on to backend-invoke. Add a method to set up test sites with only a settings.php + database entries (no installed site) and alias records for testing in --simulate mode. --- examples/example.aliases.yml | 38 ++++++++++++-- src/Commands/sql/SqlSyncCommands.php | 16 +++--- src/SiteAlias/AliasRecord.php | 6 ++- tests/CommandUnishTestCase.php | 28 ++++++++++ tests/UnishTestCase.php | 77 +++++++++++++++++++++++----- tests/rsyncTest.php | 16 ------ tests/sqlSyncTest.php | 38 ++++++++++++++ 7 files changed, 179 insertions(+), 40 deletions(-) diff --git a/examples/example.aliases.yml b/examples/example.aliases.yml index d55926677f..0a9fd2835a 100644 --- a/examples/example.aliases.yml +++ b/examples/example.aliases.yml @@ -160,14 +160,16 @@ # if you set a 'remote-host' value, and your remote OS is Windows, if you # do not set the 'OS' value, it will default to 'Linux' and could cause # unintended consequences, particularly when running 'drush sql-sync'. -# - 'ssh-options': If the target requires special options, such as a non- +# - 'ssh': If the target requires special options, such as a non- # standard port, alternative identity file, or alternative -# authentication method, ssh-options can contain a string of extra -# options that are used with the ssh command, eg "-p 100" +# authentication method, the `option` entry under the `ssh` item may +# contain a string of extra options that are used with the ssh command, +# e.g. "-p 100" # - 'paths': An array of aliases for common rsync targets. # Relative aliases are always taken from the Drupal root. # - 'files': Path to 'files' directory. This will be looked up if not # specified. +# - 'drush-script': Path to the remot Drush command. # - 'command': These options will only be set if the alias # is used with the specified command. In the example below, the option # `--no-dump` will be selected whenever the @stage alias @@ -177,6 +179,36 @@ # - `drush sql-sync @live @stage` # NOTE: Setting boolean options broke with Symfony 3. This will be fixed # in a future release. See: https://github.com/drush-ops/drush/issues/2956 +# - 'alias-parameters': These options will only be set if the alias is +# used as the specified parameter. `sql:sync` and `core:rsync` are the two +# core commands that use this entry. These commands both have `source` +# and `target` parameters. +# +# Complex example: +# +# @code +# # File: remote.alias.yml +# live: +# host: server.domain.com +# user: www-admin +# root: /other/path/to/drupal +# uri: http://example.com +# ssh: +# options: '-p 100' +# paths: +# drush-script: '/path/to/drush' +# command: +# site: +# install: +# options: +# admin-password: 'secret-secret' +# alias-parameters: +# target: +# core: +# rsnyc: +# options: +# excplude-paths: sites/default/files/private +# @endcode # # Altering aliases: # diff --git a/src/Commands/sql/SqlSyncCommands.php b/src/Commands/sql/SqlSyncCommands.php index 1483c78a11..a22ed6434a 100644 --- a/src/Commands/sql/SqlSyncCommands.php +++ b/src/Commands/sql/SqlSyncCommands.php @@ -51,10 +51,6 @@ public function sqlsync($source, $target, $options = ['no-dump' => false, 'no-sy $backend_options = []; $global_options = Drush::redispatchOptions() + ['strict' => 0]; - if (Drush::simulate()) { - $backend_options['backend-simulate'] = true; - } - // Create target DB if needed. if ($options['create-db']) { $this->logger()->notice(dt('Starting to create database on target.')); @@ -71,9 +67,11 @@ public function sqlsync($source, $target, $options = ['no-dump' => false, 'no-sy ); if (!$options['no-dump']) { $this->logger()->notice(dt('Starting to dump database on source.')); - $return = drush_invoke_process($source, 'sql-dump', array(), $dump_options, $backend_options); + $return = drush_invoke_process($sourceRecord, 'sql-dump', array(), $dump_options, $backend_options); if ($return['error_status']) { throw new \Exception(dt('sql-dump failed.')); + } elseif (Drush::simulate()) { + $source_dump_path = '/simulated/path/to/dump.tgz'; } else { $source_dump_path = $return['object']; if (!is_string($source_dump_path)) { @@ -163,7 +161,10 @@ protected function injectAliasPathParameterOptions($input, $parameterName) $aliasName = $input->getArgument($parameterName); // Inject the source and target alias records into the alias config context. - $manager->get($aliasName)->injectIntoConfig($aliasConfigContext, $parameterName); + $aliasRecord = $manager->get($aliasName); + if (!empty($aliasRecord)) { + $manager->get($aliasName)->injectIntoConfig($aliasConfigContext, $parameterName); + } } /** @@ -211,6 +212,9 @@ public function validate(CommandData $commandData) public function databaseName(AliasRecord $record) { + if ($record->isRemote() && preg_match('#\.simulated$#', $record->remoteHost())) { + return 'simulated_db'; + } $values = drush_invoke_process($record, "core-status", array(), array(), array('integrate' => false, 'override-simulated' => true)); if (is_array($values) && ($values['error_status'] == 0)) { return $values['object']['db-name']; diff --git a/src/SiteAlias/AliasRecord.php b/src/SiteAlias/AliasRecord.php index 37dcce34c6..e758f69a41 100644 --- a/src/SiteAlias/AliasRecord.php +++ b/src/SiteAlias/AliasRecord.php @@ -297,7 +297,11 @@ protected function getParameterSpecificOptions($aliasData, $parameterName) */ public function legacyRecord() { - return $this->exportConfig()->get('options', []); + $result = $this->exportConfig()->get('options', []); + if ($this->has('paths.drush-script')) { + $result['path-aliases']['%drush-script'] = $this->get('paths.drush-script'); + } + return $result; } /** diff --git a/tests/CommandUnishTestCase.php b/tests/CommandUnishTestCase.php index b1095c0472..92e4816f2b 100644 --- a/tests/CommandUnishTestCase.php +++ b/tests/CommandUnishTestCase.php @@ -59,6 +59,28 @@ abstract class CommandUnishTestCase extends UnishTestCase { */ protected $idleTimeout = 15; + /** + * Get command output and simiplify away things like full paths and extra + * whitespace. + */ + protected function getSimplifiedOutput() + { + $output = $this->getOutput(); + // We do not care if Drush inserts a -t or not in the string. Depends on whether there is a tty. + $output = preg_replace('# -t #', ' ', $output); + // Remove double spaces from output to help protect test from false negatives if spacing changes subtlely + $output = preg_replace('# *#', ' ', $output); + // Debug flags may be added to command strings if we are in debug mode. Take those out so that tests in phpunit --debug mode work + $output = preg_replace('# --debug #', ' ', $output); + $output = preg_replace('# --verbose #', ' ', $output); + // Get rid of any full paths in the output + $output = str_replace(__DIR__, '__DIR__', $output); + $output = str_replace(self::getSandbox(), '__SANDBOX__', $output); + $output = str_replace(self::getSut(), '__SUT__', $output); + + return $output; + } + /** * Accessor for the last output, trimmed. * @@ -436,4 +458,10 @@ function drush_major_version() { } return (int)$major; } + + protected function assertOutputEquals($expected) + { + $output = $this->getSimplifiedOutput(); + $this->assertEquals($expected, $output); + } } diff --git a/tests/UnishTestCase.php b/tests/UnishTestCase.php index 23db050463..dc3eed0e0b 100644 --- a/tests/UnishTestCase.php +++ b/tests/UnishTestCase.php @@ -457,6 +457,46 @@ function db_driver($db_url = NULL) { return parse_url($db_url ?: self::getDbUrl(), PHP_URL_SCHEME); } + /** + * Create some fixture sites that only have a 'settings.php' file + * with a database record. + * + * @param array $sites key=site_subder value=array of extra alias data + * @param string $aliasGroup Write aliases into a file named group.alias.yml + */ + function setUpSettings(array $sites, $aliasGroup = 'fixture') { + foreach ($sites as $subdir => $extra) { + $this->createSettings($subdir); + } + // Create basic site alias data with root and uri + $siteAliasData = $this->createAliasFileData(array_keys($sites), $aliasGroup); + // Add in caller-provided site alias data + $siteAliasData = array_merge_recursive($siteAliasData, $sites); + $this->writeSiteAliases($siteAliasData, $aliasGroup); + } + + function createSettings($subdir) { + $settingsContents = << 'unish_$subdir', + 'username' => 'root', + 'password' => '', + 'prefix' => '', + 'host' => '127.0.0.1', + 'port' => '', + 'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql', + 'driver' => 'mysql', +); +\$settings['install_profile'] = 'testing'; +EOT; + + $root = $this->webroot(); + $settingsPath = "$root/sites/$subdir/settings.php"; + self::mkdir(dirname($settingsPath)); + file_put_contents($settingsPath, $settingsContents); + } /** * Assemble (and optionally install) one or more Drupal sites using a single codebase. * @@ -477,18 +517,33 @@ function setUpDrupal($num_sites = 1, $install = FALSE) { copy($root . '/sites/example.sites.php', $root . '/sites/sites.php'); } + $siteData = $this->createAliasFile($sites_subdirs, 'unish'); + self::$sites = []; + foreach ($siteData as $key => $data) { + self::$sites["unish.$key"] = $data; + } + } + + function createAliasFileData($sites_subdirs, $aliasGroup = 'unish') { + $root = $this->webroot(); // Stash details about each site. + $sites = []; foreach ($sites_subdirs as $subdir) { - self::$sites['unish.' . $subdir] = array( + $sites[$subdir] = array( 'root' => $root, 'uri' => $subdir, 'db_url' => $this->db_url($subdir), ); } + return $sites; + } + + function createAliasFile($sites_subdirs, $aliasGroup = 'unish') { // Make an alias group for the sites. - $this->writeSiteAliases(self::$sites); + $sites = $this->createAliasFileData($sites_subdirs, $aliasGroup); + $this->writeSiteAliases($sites, $aliasGroup); - return self::$sites; + return $sites; } /** @@ -525,22 +580,16 @@ function installDrupal($env = 'dev', $install = FALSE) { * * @param $sites */ - function writeSiteAliases($sites) { - foreach ($sites as $name => $site) { - $groups[str_replace('unish.', '', $name)] = [ - 'root' => $site['root'], - 'uri' => $site['uri'] - ]; - } - $this->writeUnishConfig($groups); + function writeSiteAliases($sites, $aliasGroup = 'unish') { + $this->writeUnishConfig($sites, [], $aliasGroup); } - function writeUnishConfig($unishAliases, $config = []) + function writeUnishConfig($unishAliases, $config = [], $aliasGroup = 'unish') { $etc = self::getSandbox() . '/etc/drush'; - file_put_contents(Path::join($etc, 'unish.alias.yml'), Yaml::dump($unishAliases)); + file_put_contents(Path::join($etc, $aliasGroup . '.alias.yml'), Yaml::dump($unishAliases, PHP_INT_MAX, 2)); $config['drush']['paths']['alias-path'][] = $etc; - file_put_contents(Path::join($etc, 'drush.yml'), Yaml::dump($config, 3)); + file_put_contents(Path::join($etc, 'drush.yml'), Yaml::dump($config, PHP_INT_MAX, 2)); } /** diff --git a/tests/rsyncTest.php b/tests/rsyncTest.php index 23b744de80..f710629b9d 100644 --- a/tests/rsyncTest.php +++ b/tests/rsyncTest.php @@ -86,22 +86,6 @@ public function testRsyncPathAliases() { $this->assertEquals($test_data, $actual); } - /** - * Test to see if the output is what we expected. - */ - protected function assertOutputEquals($expected) - { - $output = $this->getOutput(); - // We do not care if Drush inserts a -t or not in the string. Depends on whether there is a tty. - $output = preg_replace('# -t #', ' ', $output); - // Remove double spaces from output to help protect test from false negatives if spacing changes subtlely - $output = preg_replace('# *#', ' ', $output); - // Get rid of any full paths in the output - $output = str_replace(__DIR__, '__DIR__', $output); - $output = str_replace(self::getSandbox(), '__SANDBOX__', $output); - $this->assertEquals($expected, $output); - } - /** * Test to see if rsync @site:%files calculates the %files path correctly. * This tests the non-optimized code path. The optimized code path (direct diff --git a/tests/sqlSyncTest.php b/tests/sqlSyncTest.php index 30af7353bd..e66ce03b1e 100644 --- a/tests/sqlSyncTest.php +++ b/tests/sqlSyncTest.php @@ -17,6 +17,44 @@ */ class sqlSyncTest extends CommandUnishTestCase { + public function testSimulatedSqlSync() { + $fixtureSites = [ + 'remote' => [ + 'host' => 'server.isp.simulated', + 'user' => 'www-admin', + 'ssh' => [ + 'options' => '-o PasswordAuthentication=whatever' + ], + 'paths' => [ + 'drush-script' => '/path/to/drush', + ], + ], + 'local' => [ + ], + ]; + $this->setUpSettings($fixtureSites, 'synctest'); + $options = [ + 'simulate' => NULL, + 'alias-path' => __DIR__ . '/resources/alias-fixtures', + ]; + + // Test simulated simple rsync with two local sites + $this->drush('sql:sync', ['@synctest.remote', '@synctest.local'], $options, NULL, NULL, self::EXIT_SUCCESS, '2>&1'); + $output = $this->getSimplifiedOutput(); + $this->assertContains("Simulating backend invoke: ssh -o PasswordAuthentication=whatever www-admin@server.isp.simulated '/path/to/drush --backend=2 --strict=0 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=__SUT__/web --uri=remote sql-dump --no-ansi --gzip --result-file", $output); + $this->assertContains("Simulating backend invoke: __SUT__/vendor/drush/drush/drush --backend=2 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --uri=default core-rsync '@synctest.remote:/simulated/path/to/dump.tgz' '@synctest.local:__SANDBOX__/drush-tmp/dump.tgz' -- --remove-source-files", $output); + $this->assertContains("Simulating backend invoke: __SUT__/vendor/drush/drush/drush --backend=2 --strict=0 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=__SUT__/web --uri=local sql-query --no-ansi --file=__SANDBOX__/drush-tmp/dump.tgz --file-delete", $output); + + // Test simulated backend invoke. + // Note that command-specific options are not processed for remote + // targets. The aliases are not interpreted at all until they recach + // the remote side, at which point they will be evaluated & any needed + // injection will be done. + $this->drush('sql:sync', ['@synctest.remote', '@synctest.local'], $options, 'user@server/path/to/drupal#sitename', NULL, self::EXIT_SUCCESS, '2>&1'); + $output = $this->getSimplifiedOutput(); + $this->assertContains("Simulating backend invoke: ssh -o PasswordAuthentication=whatever user@server '/path/to/drush --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=/path/to/drupal --uri=sitename --no-ansi sql:sync '\''@synctest.remote'\'' '\''@synctest.local'\''", $output); + } + /** * Covers the following responsibilities. * - A user created on the source site is copied to the destination site. From a446e2ba585166537fc92f62938be8fbae3128ef Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 21:05:57 -0700 Subject: [PATCH 7/8] Restore 'return sites' at end of setUpDrupal --- tests/UnishTestCase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/UnishTestCase.php b/tests/UnishTestCase.php index dc3eed0e0b..70a00a5d18 100644 --- a/tests/UnishTestCase.php +++ b/tests/UnishTestCase.php @@ -522,6 +522,7 @@ function setUpDrupal($num_sites = 1, $install = FALSE) { foreach ($siteData as $key => $data) { self::$sites["unish.$key"] = $data; } + return self::$sites; } function createAliasFileData($sites_subdirs, $aliasGroup = 'unish') { From a80ac19ffcf9665fabab0f0c8d6647c4aa33e952 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Tue, 17 Oct 2017 22:20:36 -0700 Subject: [PATCH 8/8] Refine sql-sync test. --- src/Application.php | 3 +++ tests/sqlSyncTest.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Application.php b/src/Application.php index f0c853e26e..9a165c0f6a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -149,6 +149,9 @@ public function refineUriSelection($cwd) return; } $selfAliasRecord = $this->aliasManager->getSelf(); + if (!$selfAliasRecord->hasRoot()) { + return; + } $uri = $selfAliasRecord->uri(); if (empty($uri)) { diff --git a/tests/sqlSyncTest.php b/tests/sqlSyncTest.php index e66ce03b1e..a753e28635 100644 --- a/tests/sqlSyncTest.php +++ b/tests/sqlSyncTest.php @@ -39,10 +39,10 @@ public function testSimulatedSqlSync() { ]; // Test simulated simple rsync with two local sites - $this->drush('sql:sync', ['@synctest.remote', '@synctest.local'], $options, NULL, NULL, self::EXIT_SUCCESS, '2>&1'); + $this->drush('sql:sync', ['@synctest.remote', '@synctest.local'], $options, '@synctest.local', NULL, self::EXIT_SUCCESS, '2>&1'); $output = $this->getSimplifiedOutput(); $this->assertContains("Simulating backend invoke: ssh -o PasswordAuthentication=whatever www-admin@server.isp.simulated '/path/to/drush --backend=2 --strict=0 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=__SUT__/web --uri=remote sql-dump --no-ansi --gzip --result-file", $output); - $this->assertContains("Simulating backend invoke: __SUT__/vendor/drush/drush/drush --backend=2 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --uri=default core-rsync '@synctest.remote:/simulated/path/to/dump.tgz' '@synctest.local:__SANDBOX__/drush-tmp/dump.tgz' -- --remove-source-files", $output); + $this->assertContains("Simulating backend invoke: __SUT__/vendor/drush/drush/drush --backend=2 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=__SUT__/web --uri=local core-rsync '@synctest.remote:/simulated/path/to/dump.tgz' '@synctest.local:__SANDBOX__/drush-tmp/dump.tgz' -- --remove-source-files", $output); $this->assertContains("Simulating backend invoke: __SUT__/vendor/drush/drush/drush --backend=2 --strict=0 --alias-path=__DIR__/resources/alias-fixtures:__SANDBOX__/etc/drush --root=__SUT__/web --uri=local sql-query --no-ansi --file=__SANDBOX__/drush-tmp/dump.tgz --file-delete", $output); // Test simulated backend invoke.