resetState(); } public function setProgressBarAutoDisplayInterval($interval) { if (!$this->progressIndicator) { return; } return $this->progressIndicator->setProgressBarAutoDisplayInterval($interval); } /** * {@inheritdoc} */ public function add(TaskInterface $task, $name = self::UNNAMEDTASK) { $task = new CompletionWrapper($this, $task); $this->addToTaskList($name, $task); return $this; } /** * {@inheritdoc} */ public function addCode(callable $code, $name = self::UNNAMEDTASK) { return $this->add(new CallableTask($code, $this), $name); } /** * {@inheritdoc} */ public function addIterable($iterable, callable $code) { $callbackTask = (new IterationTask($iterable, $code, $this))->inflect($this); return $this->add($callbackTask); } /** * {@inheritdoc} */ public function rollback(TaskInterface $rollbackTask) { // Rollback tasks always try as hard as they can, and never report failures. $rollbackTask = $this->ignoreErrorsTaskWrapper($rollbackTask); return $this->wrapAndRegisterRollback($rollbackTask); } /** * {@inheritdoc} */ public function rollbackCode(callable $rollbackCode) { // Rollback tasks always try as hard as they can, and never report failures. $rollbackTask = $this->ignoreErrorsCodeWrapper($rollbackCode); return $this->wrapAndRegisterRollback($rollbackTask); } /** * {@inheritdoc} */ public function completion(TaskInterface $completionTask) { $collection = $this; $completionRegistrationTask = new CallableTask( function () use ($collection, $completionTask) { $collection->registerCompletion($completionTask); }, $this ); $this->addToTaskList(self::UNNAMEDTASK, $completionRegistrationTask); return $this; } /** * {@inheritdoc} */ public function completionCode(callable $completionTask) { $completionTask = new CallableTask($completionTask, $this); return $this->completion($completionTask); } /** * {@inheritdoc} */ public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK) { return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd); } /** * {@inheritdoc} */ public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK) { return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd); } /** * {@inheritdoc} */ public function progressMessage($text, $context = [], $level = LogLevel::NOTICE) { $context += ['name' => 'Progress']; $context += TaskInfo::getTaskContext($this); return $this->addCode( function () use ($level, $text, $context) { $context += $this->getState()->getData(); $this->printTaskOutput($level, $text, $context); } ); } /** * @param \Robo\Contract\TaskInterface $rollbackTask * * @return $this */ protected function wrapAndRegisterRollback(TaskInterface $rollbackTask) { $collection = $this; $rollbackRegistrationTask = new CallableTask( function () use ($collection, $rollbackTask) { $collection->registerRollback($rollbackTask); }, $this ); $this->addToTaskList(self::UNNAMEDTASK, $rollbackRegistrationTask); return $this; } /** * Add either a 'before' or 'after' function or task. * * @param string $method * @param string $name * @param callable|TaskInterface $task * @param string $nameOfTaskToAdd * * @return $this */ protected function addBeforeOrAfter($method, $name, $task, $nameOfTaskToAdd) { if (is_callable($task)) { $task = new CallableTask($task, $this); } $existingTask = $this->namedTask($name); $fn = [$existingTask, $method]; call_user_func($fn, $task, $nameOfTaskToAdd); return $this; } /** * Wrap the provided task in a wrapper that will ignore * any errors or exceptions that may be produced. This * is useful, for example, in adding optional cleanup tasks * at the beginning of a task collection, to remove previous * results which may or may not exist. * * TODO: Provide some way to specify which sort of errors * are ignored, so that 'file not found' may be ignored, * but 'permission denied' reported? * * @param \Robo\Contract\TaskInterface $task * * @return \Robo\Collection\CallableTask */ public function ignoreErrorsTaskWrapper(TaskInterface $task) { // If the task is a stack-based task, then tell it // to try to run all of its operations, even if some // of them fail. if ($task instanceof StackBasedTask) { $task->stopOnFail(false); } $ignoreErrorsInTask = function () use ($task) { $data = []; try { $result = $this->runSubtask($task); $message = $result->getMessage(); $data = $result->getData(); $data['exitcode'] = $result->getExitCode(); } catch (\Exception $e) { $message = $e->getMessage(); } return Result::success($task, $message, $data); }; // Wrap our ignore errors callable in a task. return new CallableTask($ignoreErrorsInTask, $this); } /** * @param callable $task * * @return \Robo\Collection\CallableTask */ public function ignoreErrorsCodeWrapper(callable $task) { return $this->ignoreErrorsTaskWrapper(new CallableTask($task, $this)); } /** * Return the list of task names added to this collection. * * @return array */ public function taskNames() { return array_keys($this->taskList); } /** * Test to see if a specified task name exists. * n.b. before() and after() require that the named * task exist; use this function to test first, if * unsure. * * @param string $name * * @return bool */ public function hasTask($name) { return array_key_exists($name, $this->taskList); } /** * Find an existing named task. * * @param string $name * The name of the task to insert before. The named task MUST exist. * * @return Element * The task group for the named task. Generally this is only * used to call 'before()' and 'after()'. */ protected function namedTask($name) { if (!$this->hasTask($name)) { throw new \RuntimeException("Could not find task named $name"); } return $this->taskList[$name]; } /** * Add a list of tasks to our task collection. * * @param TaskInterface[] $tasks * An array of tasks to run with rollback protection * * @return $this */ public function addTaskList(array $tasks) { foreach ($tasks as $name => $task) { $this->add($task, $name); } return $this; } /** * Add the provided task to our task list. * * @param string $name * @param \Robo\Contract\TaskInterface $task * * @return \Robo\Collection\Collection */ protected function addToTaskList($name, TaskInterface $task) { // All tasks are stored in a task group so that we have a place // to hang 'before' and 'after' tasks. $taskGroup = new Element($task); return $this->addCollectionElementToTaskList($name, $taskGroup); } /** * @param int|string $name * @param \Robo\Collection\Element $taskGroup * * @return $this */ protected function addCollectionElementToTaskList($name, Element $taskGroup) { // If a task name is not provided, then we'll let php pick // the array index. if (Result::isUnnamed($name)) { $this->taskList[] = $taskGroup; return $this; } // If we are replacing an existing task with the // same name, ensure that our new task is added to // the end. $this->taskList[$name] = $taskGroup; return $this; } /** * Set the parent collection. This is necessary so that nested * collections' rollback and completion tasks can be added to the * top-level collection, ensuring that the rollbacks for a collection * will run if any later task fails. * * @param \Robo\Collection\NestedCollectionInterface $parentCollection * * @return $this */ public function setParentCollection(NestedCollectionInterface $parentCollection) { $this->parentCollection = $parentCollection; return $this; } /** * Get the appropriate parent collection to use * * @return CollectionInterface */ public function getParentCollection() { return $this->parentCollection ? $this->parentCollection : $this; } /** * Register a rollback task to run if there is any failure. * * Clients are free to add tasks to the rollback stack as * desired; however, usually it is preferable to call * Collection::rollback() instead. With that function, * the rollback function will only be called if all of the * tasks added before it complete successfully, AND some later * task fails. * * One example of a good use-case for registering a callback * function directly is to add a task that sends notification * when a task fails. * * @param TaskInterface $rollbackTask * The rollback task to run on failure. */ public function registerRollback(TaskInterface $rollbackTask) { if ($this->parentCollection) { return $this->parentCollection->registerRollback($rollbackTask); } if ($rollbackTask) { $this->rollbackStack[] = $rollbackTask; } } /** * Register a completion task to run once all other tasks finish. * Completion tasks run whether or not a rollback operation was * triggered. They do not trigger rollbacks if they fail. * * The typical use-case for a completion function is to clean up * temporary objects (e.g. temporary folders). The preferred * way to do that, though, is to use Temporary::wrap(). * * On failures, completion tasks will run after all rollback tasks. * If one task collection is nested inside another task collection, * then the nested collection's completion tasks will run as soon as * the nested task completes; they are not deferred to the end of * the containing collection's execution. * * @param TaskInterface $completionTask * The completion task to run at the end of all other operations. */ public function registerCompletion(TaskInterface $completionTask) { if ($this->parentCollection) { return $this->parentCollection->registerCompletion($completionTask); } if ($completionTask) { // Completion tasks always try as hard as they can, and never report failures. $completionTask = $this->ignoreErrorsTaskWrapper($completionTask); $this->completionStack[] = $completionTask; } } /** * Return the count of steps in this collection * * @return int */ public function progressIndicatorSteps() { $steps = 0; foreach ($this->taskList as $name => $taskGroup) { $steps += $taskGroup->progressIndicatorSteps(); } return $steps; } /** * A Collection of tasks can provide a command via `getCommand()` * if it contains a single task, and that task implements CommandInterface. * * @return string * * @throws \Robo\Exception\TaskException */ public function getCommand() { if (empty($this->taskList)) { return ''; } if (count($this->taskList) > 1) { // TODO: We could potentially iterate over the items in the collection // and concatenate the result of getCommand() from each one, and fail // only if we encounter a command that is not a CommandInterface. throw new TaskException($this, "getCommand() does not work on arbitrary collections of tasks."); } $taskElement = reset($this->taskList); $task = $taskElement->getTask(); $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task; if ($task instanceof CommandInterface) { return $task->getCommand(); } throw new TaskException($task, get_class($task) . " does not implement CommandInterface, so can't be used to provide a command"); } /** * Run our tasks, and roll back if necessary. * * @return \Robo\Result */ public function run() { $result = $this->runWithoutCompletion(); $this->complete(); return $result; } /** * @return \Robo\Result */ private function runWithoutCompletion() { $result = Result::success($this); if (empty($this->taskList)) { return $result; } $this->startProgressIndicator(); if ($result->wasSuccessful()) { foreach ($this->taskList as $name => $taskGroup) { $taskList = $taskGroup->getTaskList(); $result = $this->runTaskList($name, $taskList, $result); if (!$result->wasSuccessful()) { $this->fail(); return $result; } } $this->taskList = []; } $this->stopProgressIndicator(); $result['time'] = $this->getExecutionTime(); return $result; } /** * Run every task in a list, but only up to the first failure. * Return the failing result, or success if all tasks run. * * @param string $name * @param TaskInterface[] $taskList * @param \Robo\Result $result * * @return \Robo\Result * * @throws \Robo\Exception\TaskExitException */ private function runTaskList($name, array $taskList, Result $result) { try { foreach ($taskList as $taskName => $task) { $taskResult = $this->runSubtask($task); $this->advanceProgressIndicator(); // If the current task returns an error code, then stop // execution and signal a rollback. if (!$taskResult->wasSuccessful()) { return $taskResult; } // We accumulate our results into a field so that tasks that // have a reference to the collection may examine and modify // the incremental results, if they wish. $key = Result::isUnnamed($taskName) ? $name : $taskName; $result->accumulate($key, $taskResult); // The result message will be the message of the last task executed. $result->setMessage($taskResult->getMessage()); } } catch (TaskExitException $exitException) { $this->fail(); throw $exitException; } catch (\Exception $e) { // Tasks typically should not throw, but if one does, we will // convert it into an error and roll back. return Result::fromException($task, $e, $result->getData()); } return $result; } /** * Force the rollback functions to run * * @return $this */ public function fail() { $this->disableProgressIndicator(); $this->runRollbackTasks(); $this->complete(); return $this; } /** * Force the completion functions to run * * @return $this */ public function complete() { $this->detatchProgressIndicator(); $this->runTaskListIgnoringFailures($this->completionStack); $this->reset(); return $this; } /** * Reset this collection, removing all tasks. * * @return $this */ public function reset() { $this->taskList = []; $this->completionStack = []; $this->rollbackStack = []; return $this; } /** * Run all of our rollback tasks. * * Note that Collection does not implement RollbackInterface, but * it may still be used as a task inside another task collection * (i.e. you can nest task collections, if desired). */ protected function runRollbackTasks() { $this->runTaskListIgnoringFailures($this->rollbackStack); // Erase our rollback stack once we have finished rolling // everything back. This will allow us to potentially use // a command collection more than once (e.g. to retry a // failed operation after doing some error recovery). $this->rollbackStack = []; } /** * @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task * * @return \Robo\Result */ protected function runSubtask($task) { $original = ($task instanceof WrappedTaskInterface) ? $task->original() : $task; $this->setParentCollectionForTask($original, $this->getParentCollection()); if ($original instanceof InflectionInterface) { $original->inflect($this); } if ($original instanceof StateAwareInterface) { $original->setState($this->getState()); } $this->doDeferredInitialization($original); $taskResult = $task->run(); $taskResult = Result::ensureResult($task, $taskResult); $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 */ protected function setParentCollectionForTask($task, $parentCollection) { if ($task instanceof NestedCollectionInterface) { $task->setParentCollection($parentCollection); } } /** * Run all of the tasks in a provided list, ignoring failures. * This is used to roll back or complete. * * @param TaskInterface[] $taskList */ protected function runTaskListIgnoringFailures(array $taskList) { foreach ($taskList as $task) { try { $this->runSubtask($task); } catch (\Exception $e) { // Ignore rollback failures. } } } /** * Give all of our tasks to the provided collection builder. * * @param CollectionBuilder $builder */ public function transferTasks($builder) { foreach ($this->taskList as $name => $taskGroup) { // TODO: We are abandoning all of our before and after tasks here. // At the moment, transferTasks is only called under conditions where // there will be none of these, but care should be taken if that changes. $task = $taskGroup->getTask(); $builder->addTaskToCollection($task); } $this->reset(); } }