Skip to content

Commit

Permalink
Implement a state system for task collections. (#580)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-1-anderson committed May 30, 2017
1 parent be7c149 commit 0e5feb0
Show file tree
Hide file tree
Showing 21 changed files with 798 additions and 156 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -57,7 +57,8 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-master": "1.x-dev",
"dev-state": "1.x-dev"
}
},
"suggest": {
Expand Down
69 changes: 69 additions & 0 deletions docs/collections.md
Expand Up @@ -215,6 +215,75 @@ For example, the implementation of taskTmpFile() looks like this:

The `complete()` method of the task will be called once the Collection the temporary object is attached to finishes running. If the temporary is not added to a collection, then its `complete()` method will be called when the script terminates.

## Chained State

When using a collection builder, it is possible to pass state from one task to another. State is generated during the `run()` method of each task, and returned in a `Robo\Result` object. Each result has a "message" and a key/value data store that contains the task's state. This state can be made available to later tasks in the builder.

### Implicitly Passing State

Sometimes it may be desirable to process the files produced by one task using a following task that alters the result.

For example, if you have one task that takes a set of source files and generates destination files, and another task that encrypts a set of files, you could encrypt the results from the first task by running both of the tasks independently:
``` php
<?php
$result = $this->taskGenerate()
->files($sources)
->run();

$result = $this->taskEncrypt()
->files($result['files'])
->run();
?>
```
If the Encrypt task implements `\Robo\State\Consumer` and accepts 'files' from the current state, then these tasks may be chained together as follows:
``` php
<?php
$collection = $this->collectionBuilder();
$collection
->taskGenerate()
->files($sources)
->taskEncrypt()
->run();
?>
```
Tasks that do not implement the `Consumer` interface may still be chained together by explicitly connecting the state from one task with the task configuration methods, as explained in the following section:

### Explicitly Passing State

State from the key/value data store, if set, is automatically stored in the collection's state. The `storeState()` method can be used to store the result "message".

To pass state from one task to another, the `deferTaskConfiguration()` method may be used. This method defers initialization until immediately before the task's `run()` method is called. It then calls a single named setter method, passing it the value of some state variable.

For example, the builder below will create a new directory named after the output of the `uname -n` command returned by taskExec. Note that it is necessary to call `printOutput(false)` in order to make the output of taskExec available to the state system.
``` php
<?php
$this->collectionBuilder()
->taskExec('uname -n')
->printOutput(false)
->storeState('system-name')
->taskFilesystemStack()
->deferTaskConfiguration('mkdir', 'system-name')
->run();
?>
```
More complex task configuration may be done via the `defer()` method. `defer()` works like `deferTaskConfiguration()`, except that it will run an arbitrary `callable` immediately prior to the execution of the task. The example below works exactly the same as the previous example, but is implemented using `defer()` instead of `deferTaskConfiguration()`.
``` php
<?php
$this->collectionBuilder()
->taskExec('uname -n')
->printOutput(false)
->storeState('system-name')
->taskFilesystemStack()
->defer(
function ($task, $state) {
$task->mkdir($state['system-name']);
}
)
->run();
?>
```
In general, it is preferable to collect all of the information needed first, and then use that data to configure the necessary tasks. For example, the previous example could be implemented more simply by calling `$system_name = exec('uname -n');` and `taskFilesystemStack->mkdir($system_name);`. Chained state can be helpful in instances where there is a more complex relationship between the tasks.

## Named Tasks

It is also possible to provide names for the tasks added to a collection. This has two primary benefits:
Expand Down
11 changes: 11 additions & 0 deletions examples/RoboFile.php
Expand Up @@ -306,6 +306,17 @@ public function tryBuilder()
->run();
}

public function tryState()
{
return $this->collectionBuilder()
->taskExec('uname -n')
->printOutput(false)
->storeState('system-name')
->taskFilesystemStack()
->deferTaskConfiguration('mkdir', 'system-name')
->run();
}

public function tryBuilderRollback()
{
// This example will create two builders, and add
Expand Down
12 changes: 11 additions & 1 deletion src/Collection/CallableTask.php
Expand Up @@ -3,6 +3,8 @@

use Robo\Result;
use Robo\Contract\TaskInterface;
use Robo\State\StateAwareInterface;
use Robo\State\Data;

/**
* Creates a task wrapper that converts any Callable into an
Expand Down Expand Up @@ -34,7 +36,7 @@ public function __construct(callable $fn, TaskInterface $reference)
*/
public function run()
{
$result = call_user_func($this->fn);
$result = call_user_func($this->fn, $this->getState());
// If the function returns no result, then count it
// as a success.
if (!isset($result)) {
Expand All @@ -49,4 +51,12 @@ public function run()

return $result;
}

public function getState()
{
if ($this->reference instanceof StateAwareInterface) {
return $this->reference->getState();
}
return new Data();
}
}
87 changes: 86 additions & 1 deletion src/Collection/Collection.php
Expand Up @@ -2,6 +2,7 @@
namespace Robo\Collection;

use Robo\Result;
use Robo\State\Data;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
use Robo\Task\StackBasedTask;
Expand All @@ -13,6 +14,8 @@
use Robo\Contract\CommandInterface;

use Robo\Contract\InflectionInterface;
use Robo\State\StateAwareInterface;
use Robo\State\StateAwareTrait;

/**
* Group tasks into a collection that run together. Supports
Expand All @@ -28,8 +31,10 @@
* called. Here, taskDeleteDir is used to remove partial results
* of an unfinished task.
*/
class Collection extends BaseTask implements CollectionInterface, CommandInterface
class Collection extends BaseTask implements CollectionInterface, CommandInterface, StateAwareInterface
{
use StateAwareTrait;

/**
* @var \Robo\Collection\Element[]
*/
Expand All @@ -50,11 +55,22 @@ class Collection extends BaseTask implements CollectionInterface, CommandInterfa
*/
protected $parentCollection;

/**
* @var callable[]
*/
protected $deferredCallbacks = [];

/**
* @var string[]
*/
protected $messageStoreKeys = [];

/**
* Constructor.
*/
public function __construct()
{
$this->resetState();
}

public function setProgressBarAutoDisplayInterval($interval)
Expand Down Expand Up @@ -163,6 +179,7 @@ public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
$context += TaskInfo::getTaskContext($this);
return $this->addCode(
function () use ($level, $text, $context) {
$context += $this->getState()->getData();
$this->printTaskOutput($level, $text, $context);
}
);
Expand Down Expand Up @@ -630,10 +647,78 @@ protected function runSubtask($task)
if ($original instanceof InflectionInterface) {
$original->inflect($this);
}
if ($original instanceof StateAwareInterface) {
$original->setState($this->getState());
}
$this->doDeferredInitialization($original);
$taskResult = $task->run();
$this->doStateUpdates($original, $taskResult);
return $taskResult;
}

protected function doStateUpdates($task, Data $taskResult)
{
$this->updateState($taskResult);
$key = spl_object_hash($task);
if (array_key_exists($key, $this->messageStoreKeys)) {
$state = $this->getState();
list($stateKey, $sourceKey) = $this->messageStoreKeys[$key];
$value = empty($sourceKey) ? $taskResult->getMessage() : $taskResult[$sourceKey];
$state[$stateKey] = $value;
}
}

public function storeState($task, $key, $source = '')
{
$this->messageStoreKeys[spl_object_hash($task)] = [$key, $source];

return $this;
}

public function deferTaskConfiguration($task, $functionName, $stateKey)
{
return $this->defer(
$task,
function ($task, $state) use ($functionName, $stateKey) {
$fn = [$task, $functionName];
$value = $state[$stateKey];
$fn($value);
}
);
}

/**
* Defer execution of a callback function until just before a task
* runs. Use this time to provide more settings for the task, e.g. from
* the collection's shared state, which is populated with the results
* of previous test runs.
*/
public function defer($task, $callback)
{
$this->deferredCallbacks[spl_object_hash($task)][] = $callback;

return $this;
}

protected function doDeferredInitialization($task)
{
// If the task is a state consumer, then call its receiveState method
if ($task instanceof \Robo\State\Consumer) {
$task->receiveState($this->getState());
}

// Check and see if there are any deferred callbacks for this task.
$key = spl_object_hash($task);
if (!array_key_exists($key, $this->deferredCallbacks)) {
return;
}

// Call all of the deferred callbacks
foreach ($this->deferredCallbacks[$key] as $fn) {
$fn($task, $this->getState());
}
}

/**
* @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
* @param $parentCollection
Expand Down
42 changes: 41 additions & 1 deletion src/Collection/CollectionBuilder.php
Expand Up @@ -13,6 +13,8 @@
use Robo\Contract\BuilderAwareInterface;
use Robo\Contract\CommandInterface;
use Robo\Contract\VerbosityThresholdInterface;
use Robo\State\StateAwareInterface;
use Robo\State\StateAwareTrait;

/**
* Creates a collection, and adds tasks to it. The collection builder
Expand Down Expand Up @@ -41,8 +43,9 @@
* In the example above, the `taskDeleteDir` will be called if
* ```
*/
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface, StateAwareInterface
{
use StateAwareTrait;

/**
* @var \Robo\Tasks
Expand Down Expand Up @@ -70,6 +73,7 @@ class CollectionBuilder extends BaseTask implements NestedCollectionInterface, W
public function __construct($commandFile)
{
$this->commandFile = $commandFile;
$this->resetState();
}

public static function create($container, $commandFile)
Expand Down Expand Up @@ -259,6 +263,39 @@ public function addTaskToCollection($task)
return $this;
}

public function getState()
{
$collection = $this->getCollection();
return $collection->getState();
}

public function storeState($key, $source = '')
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}

public function deferTaskConfiguration($functionName, $stateKey)
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}

public function defer($callback)
{
return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
}

protected function callCollectionStateFuntion($functionName, $args)
{
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;

array_unshift($args, $currentTask);
$collection = $this->getCollection();
$fn = [$collection, $functionName];

call_user_func_array($fn, $args);
return $this;
}

public function setVerbosityThreshold($verbosityThreshold)
{
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
Expand Down Expand Up @@ -293,6 +330,7 @@ public function newBuilder()
$collectionBuilder->inflect($this);
$collectionBuilder->simulated($this->isSimulated());
$collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
$collectionBuilder->setState($this->getState());

return $collectionBuilder;
}
Expand Down Expand Up @@ -480,6 +518,7 @@ public function run()
$result = $this->runTasks();
$this->stopTimer();
$result['time'] = $this->getExecutionTime();
$result->mergeData($this->getState()->getData());
return $result;
}

Expand Down Expand Up @@ -531,6 +570,7 @@ public function getCollection()
if (!isset($this->collection)) {
$this->collection = new Collection();
$this->collection->inflect($this);
$this->collection->setState($this->getState());
$this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));

if (isset($this->currentTask)) {
Expand Down
3 changes: 2 additions & 1 deletion src/Collection/CollectionInterface.php
Expand Up @@ -141,7 +141,8 @@ public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK);
* message will not be printed.
*
* @param string $text Message to print.
* @param array $context Extra context data for use by the logger.
* @param array $context Extra context data for use by the logger. Note
* that the data from the collection state is merged with the provided context.
* @param \Psr\Log\LogLevel|string $level The log level to print the information at. Default is NOTICE.
*
* @return $this
Expand Down
5 changes: 3 additions & 2 deletions src/Common/ExecTrait.php
Expand Up @@ -305,7 +305,7 @@ protected function execute($process, $output_callback = null)
$this->process->setInput($this->input);
}

if ($this->interactive) {
if ($this->interactive && $this->isPrinted) {
$this->process->setTty(true);
}

Expand All @@ -317,9 +317,10 @@ protected function execute($process, $output_callback = null)
$this->startTimer();
$this->process->run();
$this->stopTimer();
$output = rtrim($this->process->getOutput());
return new ResultData(
$this->process->getExitCode(),
$this->process->getOutput(),
$output,
$this->getResultData()
);
}
Expand Down

0 comments on commit 0e5feb0

Please sign in to comment.