Skip to content

Commit

Permalink
Merge pull request #119 from openeuropa/MULTISITE-23486
Browse files Browse the repository at this point in the history
MULTISITE-23486: Allow dynamic commands to override all code provided commands
  • Loading branch information
idimopoulos authored May 19, 2020
2 parents 73be0c1 + 446459e commit f4fa798
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 26 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ The Task Runner comes with the following built-in commands:

Run `./vendor/bin/run help [command-name]` for more information about each command's capabilities.

## Expose custom commands as YAML configuration
## Expose "dynamic" commands as YAML configuration

The Task Runner allows you to expose new commands by just listing its [tasks](http://robo.li/getting-started/#tasks)
under the `commands:` property in `runner.yml.dist`/`runner.yml`.

For example, the following YAML portion will expose two commands, `drupal:site-setup` and `setup:behat`:
For example, the following YAML portion will expose two dynamic commands, `drupal:site-setup` and `setup:behat`:

```yaml
commands:
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"consolidation/robo": "^1.4",
"gitonomy/gitlib": "^1.0",
"jakeasmith/http_build_url": "^1.0.1",
"nuvoleweb/robo-config": "^0.2.1"
"nuvoleweb/robo-config": "^0.2.1",
"symfony/console": "^3.4.21|^4|^5"
},
"require-dev": {
"openeuropa/code-review": "~1.0.0-beta3",
Expand Down
4 changes: 3 additions & 1 deletion src/Commands/AbstractDrupalCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ public function setRuntimeConfig(ConsoleCommandEvent $event)
*
* @param CommandData $commandData
* @throws \Exception
* Thrown when the settings file or its containing folder does not exist
* or is not writeable.
*/
public function validateSiteInstall(CommandData $commandData)
{
$input = $commandData->input();

// Validate if permissions will be set up.
if (!$input->getOption('skip-permissions-setup')) {
if (!$input->hasOption('skip-permissions-setup') || !$input->getOption('skip-permissions-setup')) {
return;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Commands/DrupalCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
use Symfony\Component\Yaml\Yaml;

/**
* Class DrupalCommands.
* Base class for commands that interact with a Drupal installation.
*
* @package OpenEuropa\TaskRunner\Commands
* This contains shared code that can be used in commands regardless of the
* Drupal version they target.
*/
class DrupalCommands extends AbstractDrupalCommands
{
Expand Down
13 changes: 9 additions & 4 deletions src/Commands/DynamicCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
namespace OpenEuropa\TaskRunner\Commands;

use OpenEuropa\TaskRunner\Tasks as TaskRunnerTasks;
use Robo\Robo;

/**
* Class DynamicCommands
* Command class for dynamic commands.
*
* @package OpenEuropa\TaskRunner\Commands
* Dynamic commands are defined in YAML and have no dedicated command class.
* A command is comprised of an array of tasks with their configuration.
* See the section in the README on dynamic commands for more information.
*/
class DynamicCommands extends AbstractCommands
{
Expand All @@ -18,8 +21,10 @@ class DynamicCommands extends AbstractCommands
*/
public function runTasks()
{
$command = $this->input()->getArgument('command');
$tasks = $this->getConfig()->get("commands.{$command}");
$commandName = $this->input()->getArgument('command');
/** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */
$command = Robo::application()->get($commandName);
$tasks = $command->getAnnotationData()['tasks'];

return $this->taskCollectionFactory($tasks);
}
Expand Down
91 changes: 80 additions & 11 deletions src/TaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ class TaskRunner
private $defaultCommandClasses = [
ChangelogCommands::class,
DrupalCommands::class,
DynamicCommands::class,
ReleaseCommands::class,
];

Expand All @@ -97,22 +96,40 @@ public function __construct(InputInterface $input, OutputInterface $output, Clas

// Create and initialize runner.
$this->runner = new RoboRunner();
$this->runner->setRelativePluginNamespace('TaskRunner');
$this->runner->setContainer($this->container);
}

/**
* Executes the command that has been provided on the command line input.
*
* A command consists of multiple tasks and is defined either as a Command
* class in the `vendor\TaskRunner\Commands` subnamespace, or a dynamic
* command defined in "runner.yml".
*
* Robo is not architected in a way that makes it easily extensible. It has
* no events that we can hook into to allow it to discover our two custom
* types of commands. We work around this by registering our own commands on
* the container in the same way as is done by Robo, and then delegating to
* `\Robo\Runner::run()`.
*
* @return int
* The exit code returned by the command.
*/
public function run()
{
// Discover early the commands to allow dynamic command overrides.
$commandClasses = $this->discoverCommandClasses();
$commandClasses = array_merge($this->defaultCommandClasses, $commandClasses);

// Register command classes.
$this->runner->registerCommandClasses($this->application, $this->defaultCommandClasses);
$this->runner->registerCommandClasses($this->application, $commandClasses);

// Register commands defined in runner.yml file.
// Register commands defined in runner.yml file. These are registered
// after the command classes so that dynamic commands can override
// commands defined in classes.
$this->registerDynamicCommands($this->application);

// Run command.
// Run the command entered by the user in the CLI.
return $this->runner->run($this->input, $this->output, $this->application);
}

Expand Down Expand Up @@ -235,18 +252,70 @@ private function getWorkingDir(InputInterface $input)
}

/**
* Registers dynamic commands in the container so Robo can find them.
*
* The standard class defined commands have already been registered at this
* point. If a dynamic command has the same identifier or alias as a class
* defined command it will replace it. This allows users to override
* existing commands in their runner.yml file.
*
* @param \Robo\Application $application
* The Robo Symfony application.
*/
private function registerDynamicCommands(Application $application)
{
foreach ($this->getConfig()->get('commands', []) as $name => $tasks) {
/** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */
$commandFileName = DynamicCommands::class."Commands";
$commandClass = $this->container->get($commandFileName);
$commandFactory = $this->container->get('commandFactory');
if (!$commands = $this->getConfig()->get('commands')) {
return;
}

/** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */
$commandFactory = $this->container->get('commandFactory');

// Robo registers command classes in the container using the qualified
// namespace with "Commands" appended to it. This results in identifiers
// like "OpenEuropa\TaskRunner\Commands\DrupalCommandsCommands".
// @see \Robo\Runner::instantiateCommandClass()
$commandFileName = DynamicCommands::class."Commands";
$this->runner->registerCommandClass($this->application, DynamicCommands::class);
$commandClass = $this->container->get($commandFileName);

foreach ($commands as $name => $tasks) {
$aliases = [];
// This command has been already registered as an annotated command.
if ($application->has($name)) {
$registeredCommand = $application->get($name);
$aliases = $registeredCommand->getAliases();
// The dynamic command overrides an alias rather than a
// registered command main name. Get the command main name.
if (in_array($name, $aliases, true)) {
$name = $registeredCommand->getName();
}
}

$commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
$command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name);
$commandInfo->addAnnotation('tasks', $tasks);
$command = $commandFactory->createCommand($commandInfo, $commandClass)
->setName($name)
->setAliases($aliases);
$application->add($command);
}
}

/**
* Discovers task runner commands that are provided by various packages.
*
* This traverses the namespace tree and returns all classes that are
* located in the source tree in the folder "TaskRunner/Commands/" and have
* a filename that ends with "*Command.php" or "*Commands.php".
*
* @return string[]
*/
protected function discoverCommandClasses()
{
/** @var \Robo\ClassDiscovery\RelativeNamespaceDiscovery $discovery */
$discovery = Robo::service('relativeNamespaceDiscovery');
$discovery->setRelativeNamespace('TaskRunner\Commands')
->setSearchPattern('/.*Commands?\.php$/');
return $discovery->getClasses();
}
}
23 changes: 20 additions & 3 deletions src/Tasks/CollectionFactory/CollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,34 @@ public function getHelp()
}

/**
* Returns the Robo task for a given task definition.
*
* For the moment this is a hardcoded mapping of supported tasks.
*
* @param array|string $task
* A task definition array consisting of the task name and optionally a
* number of configuration options. Can also be a string representing a
* shell command.
*
* @return \Robo\Contract\TaskInterface
* The Robo task.
*
* @throws \Robo\Exception\TaskException
*
* @SuppressWarnings(PHPMD)
*
* @todo: Tuner this into a proper plugin system.
* @todo: Turn this into a proper plugin system.
*/
protected function taskFactory($task)
{
if (is_string($task)) {
return $this->taskExec($task);
return $this->taskExec($task)->interactive($this->isTtySupported());
}

// Set a number of options to safe defaults if they have not been given
// a different value in the task definition.
// @todo Not all of these options apply to all available tasks. Only
// set defaults for this task's options.
$this->secureOption($task, 'force', false);
$this->secureOption($task, 'umask', 0000);
$this->secureOption($task, 'recursive', false);
Expand Down Expand Up @@ -196,11 +208,16 @@ protected function taskFactory($task)
}

/**
* Secure option value.
* Sets the given safe default value for the option with the given name.
*
* If the option is already set it will not be overwritten.
*
* @param array $task
* The task array containing the task name and configuration.
* @param string $name
* The name of the option for which to provide a safe default value.
* @param mixed $default
* The default value.
*/
protected function secureOption(array &$task, $name, $default)
{
Expand Down
8 changes: 6 additions & 2 deletions src/Tasks/ProcessConfigFile/loadTasks.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
trait loadTasks
{
/**
* @param $source
* @param $destination
* Replaces placeholders with actual values.
*
* @param string $source
* The path to the file to process.
* @param string $destination
* The path where to store the processed file.
*
* @return \OpenEuropa\TaskRunner\Tasks\ProcessConfigFile\ProcessConfigFile
*/
Expand Down
53 changes: 53 additions & 0 deletions tests/CommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,40 @@ public function testUserConfigFile()
$this->assertNotEquals($drupalRoot, $runner->getConfig()->get('drupal.root'));
}

/**
* Tests that existing commands can be overridden in the runner config.
*
* @dataProvider overrideCommandDataProvider
*
* @param string $command
* A command that will be executed by the task runner.
* @param array $runnerConfig
* An array of task runner configuration data, equivalent to what would be
* written in a "runner.yml" file. This contains the overridden commands.
* @param array $expected
* An array of strings which are expected to be output to the terminal
* during execution of the command.
*/
public function testOverrideCommand($command, array $runnerConfig, array $expected)
{
$runnerConfigFile = $this->getSandboxFilepath('runner.yml');
file_put_contents($runnerConfigFile, Yaml::dump($runnerConfig));

$input = new StringInput("{$command} --working-dir=".$this->getSandboxRoot());
$output = new BufferedOutput();
$runner = new TaskRunner($input, $output, $this->getClassLoader());
$exit_code = $runner->run();

// Check that the command succeeded, i.e. has exit code 0.
$this->assertEquals(0, $exit_code);

// Check that the output is as expected.
$text = $output->fetch();
foreach ($expected as $row) {
$this->assertContains($row, $text);
}
}

/**
* @return array
*/
Expand Down Expand Up @@ -425,6 +459,25 @@ public function changelogDataProvider()
return $this->getFixtureContent('changelog.yml');
}

/**
* Provides test cases for ::testOverrideCommand().
*
* @return array
* An array of test cases, each one an array with the following keys:
* - 'command': A string representing a command that will be executed by
* the task runner.
* - 'runnerConfig': An array of task runner configuration data,
* equivalent to what would be written in a "runner.yml" file.
* - 'expected': An array of strings which are expected to be output to
* the terminal during execution of the command.
*
* @see \OpenEuropa\TaskRunner\Tests\Commands\CommandsTest::testOverrideCommand()
*/
public function overrideCommandDataProvider(): array
{
return $this->getFixtureContent('override.yml');
}

/**
* @param string $content
* @param array $expected
Expand Down
46 changes: 46 additions & 0 deletions tests/fixtures/override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Override a default command by its main name.
# Run command by its main name.
- command: drupal:site-install
runnerConfig:
commands:
drupal:site-install:
- echo "Hello world!"
- echo "Hey you!"
expected:
- '[Exec] Running echo "Hello world!"'
- '[Exec] Running echo "Hey you!"'

# Override a default command by its main name.
# Run command by one of its aliases.
- command: drupal:site-install
runnerConfig:
commands:
dsi:
- echo "Hello world!"
- echo "Hey you!"
expected:
- '[Exec] Running echo "Hello world!"'
- '[Exec] Running echo "Hey you!"'

# Override a default command by one of its aliases.
# Run command by other alias.
- command: drupal:si
runnerConfig:
commands:
dsi:
- echo "Hello world!"
- echo "Hey you!"
expected:
- '[Exec] Running echo "Hello world!"'
- '[Exec] Running echo "Hey you!"'

# Override a custom command.
- command: custom:command-two
runnerConfig:
commands:
custom:command-two:
- echo "Hello world!"
- echo "Hey you!"
expected:
- '[Exec] Running echo "Hello world!"'
- '[Exec] Running echo "Hey you!"'

0 comments on commit f4fa798

Please sign in to comment.