Skip to content
Snippets Groups Projects
Commit fcbb50e6 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3230024 by phenaproxima, tedbow: Collect exclusions from event objects

parent 648f8901
No related branches found
No related tags found
No related merge requests found
Showing
with 406 additions and 288 deletions
...@@ -4,7 +4,7 @@ services: ...@@ -4,7 +4,7 @@ services:
arguments: ['@keyvalue.expirable', '@datetime.time', 24] arguments: ['@keyvalue.expirable', '@datetime.time', 24]
automatic_updates.updater: automatic_updates.updater:
class: Drupal\automatic_updates\Updater class: Drupal\automatic_updates\Updater
arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer' , '@file_system', '@event_dispatcher', '%app.root%', '%site.path%'] arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer', '@event_dispatcher']
automatic_updates.staged_package_validator: automatic_updates.staged_package_validator:
class: Drupal\automatic_updates\Validation\StagedProjectsValidation class: Drupal\automatic_updates\Validation\StagedProjectsValidation
arguments: ['@string_translation', '@automatic_updates.updater' ] arguments: ['@string_translation', '@automatic_updates.updater' ]
...@@ -70,3 +70,8 @@ services: ...@@ -70,3 +70,8 @@ services:
['@automatic_updates.symfony_exec_finder' ] ['@automatic_updates.symfony_exec_finder' ]
automatic_updates.process_factory: automatic_updates.process_factory:
class: Drupal\automatic_updates\ComposerStager\ProcessFactory class: Drupal\automatic_updates\ComposerStager\ProcessFactory
automatic_updates.excluded_paths_subscriber:
class: Drupal\automatic_updates\Event\ExcludedPathsSubscriber
arguments: ['%app.root%', '%site.path%', '@file_system']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\automatic_updates\Event;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines an event subscriber to exclude certain paths from update operations.
*/
class ExcludedPathsSubscriber implements EventSubscriberInterface {
/**
* The Drupal root.
*
* @var string
*/
protected $appRoot;
/**
* The current site path, relative to the Drupal root.
*
* @var string
*/
protected $sitePath;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs an UpdateSubscriber.
*
* @param string $app_root
* The Drupal root.
* @param string $site_path
* The current site path, relative to the Drupal root.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct(string $app_root, string $site_path, FileSystemInterface $file_system) {
$this->appRoot = $app_root;
$this->sitePath = $site_path;
$this->fileSystem = $file_system;
}
/**
* Reacts to the beginning of an update process.
*
* @param \Drupal\automatic_updates\Event\PreStartEvent $event
* The event object.
*/
public function preStart(PreStartEvent $event): void {
if ($public = $this->fileSystem->realpath('public://')) {
$event->excludePath($public);
}
if ($private = $this->fileSystem->realpath('private://')) {
$event->excludePath($private);
}
// If this module is a git clone, exclude it.
if (is_dir(__DIR__ . '/../../.git')) {
$event->excludePath($this->fileSystem->realpath(__DIR__ . '/../..'));
}
// Exclude site-specific settings files.
$settings_files = [
'settings.php',
'settings.local.php',
'services.yml',
];
foreach ($settings_files as $settings_file) {
$file_path = implode(DIRECTORY_SEPARATOR, [
$this->appRoot,
$this->sitePath,
$settings_file,
]);
$file_path = $this->fileSystem->realpath($file_path);
if (file_exists($file_path)) {
$event->excludePath($file_path);
}
$default_file_path = implode(DIRECTORY_SEPARATOR, [
'sites',
'default',
$settings_file,
]);
$event->excludePath($default_file_path);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
AutomaticUpdatesEvents::PRE_START => 'preStart',
];
}
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Common functionality for events which can collect excluded paths.
*/
trait ExcludedPathsTrait {
/**
* Paths to exclude from the update.
*
* @var string[]
*/
protected $excludedPaths = [];
/**
* Adds an absolute path to exclude from the update operation.
*
* @todo This should only accept paths relative to the active directory.
*
* @param string $path
* The path to exclude.
*/
public function excludePath(string $path): void {
$this->excludedPaths[] = $path;
}
/**
* Returns the paths to exclude from the update operation.
*
* @return string[]
* The paths to exclude.
*/
public function getExcludedPaths(): array {
return array_unique($this->excludedPaths);
}
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Event fired before staged changes are copied into the active site.
*/
class PreCommitEvent extends UpdateEvent {
use ExcludedPathsTrait;
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Event fired before an update begins.
*/
class PreStartEvent extends UpdateEvent {
use ExcludedPathsTrait;
}
<?php
namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a base class for forms which are part of the attended update process.
*/
abstract class UpdateFormBase extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* Constructs an UpdateFormBase object.
*
* @param \Drupal\automatic_updates\Updater $updater
* The updater service.
*/
public function __construct(Updater $updater) {
$this->updater = $updater;
}
/**
* Fires an update validation event and handles any detected errors.
*
* If $form and $form_state are passed, errors will be flagged against the
* form_id element, since it's guaranteed to exist in all forms. Otherwise,
* the errors will be displayed in the messages area.
*
* @param string $event
* The name of the event to fire. Should be one of the constants from
* \Drupal\automatic_updates\AutomaticUpdatesEvents.
* @param array|null $form
* (optional) The complete form array.
* @param \Drupal\Core\Form\FormStateInterface|null $form_state
* (optional) The current form state.
*
* @return bool
* TRUE if no errors were found, FALSE otherwise.
*/
protected function validateUpdate(string $event, array &$form = NULL, FormStateInterface $form_state = NULL): bool {
$errors = FALSE;
foreach ($this->getValidationErrors($event) as $error) {
if ($form && $form_state) {
$form_state->setError($form['form_id'], $error);
}
else {
$this->messenger()->addError($error);
}
$errors = TRUE;
}
return !$errors;
}
/**
* Fires an update validation event and returns all resulting errors.
*
* @param string $event
* The name of the event to fire. Should be one of the constants from
* \Drupal\automatic_updates\AutomaticUpdatesEvents.
*
* @return \Drupal\Component\Render\MarkupInterface[]
* The validation errors, if any.
*/
protected function getValidationErrors(string $event): array {
$errors = [];
try {
$this->updater->dispatchUpdateEvent($event);
}
catch (UpdateException $e) {
foreach ($e->getValidationResults() as $result) {
$errors = array_merge($errors, $this->getMessagesFromValidationResult($result));
}
}
return $errors;
}
/**
* Extracts all relevant messages from an update validation result.
*
* @param \Drupal\automatic_updates\Validation\ValidationResult $result
* The validation result.
*
* @return \Drupal\Component\Render\MarkupInterface[]
* The messages to display from the validation result.
*/
protected function getMessagesFromValidationResult(ValidationResult $result): array {
$messages = $result->getMessages();
if (count($messages) > 1) {
array_unshift($messages, $result->getSummary());
}
return $messages;
}
}
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
namespace Drupal\automatic_updates\Form; namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\BatchProcessor; use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\Updater;
use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\State\StateInterface; use Drupal\Core\State\StateInterface;
...@@ -17,7 +17,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface; ...@@ -17,7 +17,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal * @internal
* Form classes are internal. * Form classes are internal.
*/ */
class UpdateReady extends UpdateFormBase { class UpdateReady extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/** /**
* The state service. * The state service.
...@@ -37,7 +44,7 @@ class UpdateReady extends UpdateFormBase { ...@@ -37,7 +44,7 @@ class UpdateReady extends UpdateFormBase {
* The state service. * The state service.
*/ */
public function __construct(Updater $updater, MessengerInterface $messenger, StateInterface $state) { public function __construct(Updater $updater, MessengerInterface $messenger, StateInterface $state) {
parent::__construct($updater); $this->updater = $updater;
$this->setMessenger($messenger); $this->setMessenger($messenger);
$this->state = $state; $this->state = $state;
} }
...@@ -64,10 +71,6 @@ class UpdateReady extends UpdateFormBase { ...@@ -64,10 +71,6 @@ class UpdateReady extends UpdateFormBase {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function buildForm(array $form, FormStateInterface $form_state) { public function buildForm(array $form, FormStateInterface $form_state) {
if (!$this->validateUpdate(AutomaticUpdatesEvents::PRE_COMMIT)) {
return $form;
}
$form['backup'] = [ $form['backup'] = [
'#prefix' => '<strong>', '#prefix' => '<strong>',
'#markup' => $this->t('Back up your database and site before you continue. <a href=":backup_url">Learn how</a>.', [':backup_url' => 'https://www.drupal.org/node/22281']), '#markup' => $this->t('Back up your database and site before you continue. <a href=":backup_url">Learn how</a>.', [':backup_url' => 'https://www.drupal.org/node/22281']),
...@@ -89,14 +92,6 @@ class UpdateReady extends UpdateFormBase { ...@@ -89,14 +92,6 @@ class UpdateReady extends UpdateFormBase {
return $form; return $form;
} }
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$this->validateUpdate(AutomaticUpdatesEvents::PRE_COMMIT, $form, $form_state);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
namespace Drupal\automatic_updates\Form; namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\BatchProcessor; use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates_9_3_shim\ProjectRelease; use Drupal\automatic_updates_9_3_shim\ProjectRelease;
use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link; use Drupal\Core\Link;
use Drupal\Core\State\StateInterface; use Drupal\Core\State\StateInterface;
...@@ -21,7 +21,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface; ...@@ -21,7 +21,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal * @internal
* Form classes are internal. * Form classes are internal.
*/ */
class UpdaterForm extends UpdateFormBase { class UpdaterForm extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/** /**
* The module handler. * The module handler.
...@@ -38,7 +45,7 @@ class UpdaterForm extends UpdateFormBase { ...@@ -38,7 +45,7 @@ class UpdaterForm extends UpdateFormBase {
protected $state; protected $state;
/** /**
* Constructs a new UpdateManagerUpdate object. * Constructs a new UpdaterForm object.
* *
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler. * The module handler.
...@@ -48,7 +55,7 @@ class UpdaterForm extends UpdateFormBase { ...@@ -48,7 +55,7 @@ class UpdaterForm extends UpdateFormBase {
* The updater service. * The updater service.
*/ */
public function __construct(ModuleHandlerInterface $module_handler, StateInterface $state, Updater $updater) { public function __construct(ModuleHandlerInterface $module_handler, StateInterface $state, Updater $updater) {
parent::__construct($updater); $this->updater = $updater;
$this->moduleHandler = $module_handler; $this->moduleHandler = $module_handler;
$this->state = $state; $this->state = $state;
} }
...@@ -213,14 +220,6 @@ class UpdaterForm extends UpdateFormBase { ...@@ -213,14 +220,6 @@ class UpdaterForm extends UpdateFormBase {
return $actions; return $actions;
} }
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$this->validateUpdate(AutomaticUpdatesEvents::PRE_START, $form, $form_state);
}
/** /**
* Submit function to delete an existing in-progress update. * Submit function to delete an existing in-progress update.
*/ */
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
namespace Drupal\automatic_updates; namespace Drupal\automatic_updates;
use Composer\Autoload\ClassLoader; use Composer\Autoload\ClassLoader;
use Drupal\automatic_updates\Event\PreCommitEvent;
use Drupal\automatic_updates\Event\PreStartEvent;
use Drupal\automatic_updates\Event\UpdateEvent; use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Exception\UpdateException; use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface; use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\StringTranslation\TranslationInterface;
...@@ -23,6 +24,16 @@ class Updater { ...@@ -23,6 +24,16 @@ class Updater {
use StringTranslationTrait; use StringTranslationTrait;
/**
* The event classes to dispatch for various update events.
*
* @var string[]
*/
protected const EVENT_CLASSES = [
AutomaticUpdatesEvents::PRE_START => PreStartEvent::class,
AutomaticUpdatesEvents::PRE_COMMIT => PreCommitEvent::class,
];
/** /**
* The state key in which to store the status of the update. * The state key in which to store the status of the update.
* *
...@@ -65,13 +76,6 @@ class Updater { ...@@ -65,13 +76,6 @@ class Updater {
*/ */
protected $state; protected $state;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/** /**
* The event dispatcher service. * The event dispatcher service.
* *
...@@ -79,20 +83,6 @@ class Updater { ...@@ -79,20 +83,6 @@ class Updater {
*/ */
protected $eventDispatcher; protected $eventDispatcher;
/**
* The Drupal root.
*
* @var string
*/
protected $appRoot;
/**
* The current site directory, relative to the Drupal root.
*
* @var string
*/
protected $sitePath;
/** /**
* Constructs an Updater object. * Constructs an Updater object.
* *
...@@ -108,26 +98,17 @@ class Updater { ...@@ -108,26 +98,17 @@ class Updater {
* The Composer Stager's cleaner service. * The Composer Stager's cleaner service.
* @param \PhpTuf\ComposerStager\Domain\CommitterInterface $committer * @param \PhpTuf\ComposerStager\Domain\CommitterInterface $committer
* The Composer Stager's committer service. * The Composer Stager's committer service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service. * The event dispatcher service.
* @param string $app_root
* The Drupal root.
* @param string $site_path
* The current site directory, relative to the Drupal root.
*/ */
public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher, string $app_root, string $site_path) { public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, EventDispatcherInterface $event_dispatcher) {
$this->state = $state; $this->state = $state;
$this->beginner = $beginner; $this->beginner = $beginner;
$this->stager = $stager; $this->stager = $stager;
$this->cleaner = $cleaner; $this->cleaner = $cleaner;
$this->committer = $committer; $this->committer = $committer;
$this->setStringTranslation($translation); $this->setStringTranslation($translation);
$this->fileSystem = $file_system;
$this->eventDispatcher = $event_dispatcher; $this->eventDispatcher = $event_dispatcher;
$this->appRoot = $app_root;
$this->sitePath = $site_path;
} }
/** /**
...@@ -183,51 +164,25 @@ class Updater { ...@@ -183,51 +164,25 @@ class Updater {
*/ */
public function begin(): string { public function begin(): string {
$stage_key = $this->createActiveStage(); $stage_key = $this->createActiveStage();
$this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), NULL, 120, $this->getExclusions()); $event = $this->dispatchUpdateEvent(AutomaticUpdatesEvents::PRE_START);
$this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), NULL, 120, $this->getExclusions($event));
return $stage_key; return $stage_key;
} }
/** /**
* Gets the paths that should be excluded from the staging area. * Gets the excluded paths collected by an event object.
*
* @param \Drupal\automatic_updates\Event\PreStartEvent|\Drupal\automatic_updates\Event\PreCommitEvent $event
* The event object.
* *
* @return string[] * @return string[]
* The paths to exclude, relative to the active directory. * The paths to exclude, relative to the active directory.
*/ */
protected function getExclusions(): array { private function getExclusions($event): array {
$exclusions = [];
if ($public = $this->fileSystem->realpath('public://')) {
$exclusions[] = $public;
}
if ($private = $this->fileSystem->realpath('private://')) {
$exclusions[] = $private;
}
// If this module is a git clone, exclude it.
if (is_dir(__DIR__ . '/../.git')) {
$exclusions[] = $this->fileSystem->realpath(__DIR__ . '/..');
}
// Exclude site-specific settings files.
$settings_files = [
'settings.php',
'settings.local.php',
'services.yml',
];
foreach ($settings_files as $settings_file) {
$file_path = implode(DIRECTORY_SEPARATOR, [
$this->appRoot,
$this->sitePath,
$settings_file,
]);
$file_path = $this->fileSystem->realpath($file_path);
if (file_exists($file_path)) {
$exclusions[] = $file_path;
}
}
$make_relative = function (string $path): string { $make_relative = function (string $path): string {
return str_replace($this->getActiveDirectory() . '/', '', $path); return str_replace($this->getActiveDirectory() . '/', '', $path);
}; };
return array_map($make_relative, $exclusions); return array_map($make_relative, $event->getExcludedPaths());
} }
/** /**
...@@ -272,6 +227,10 @@ class Updater { ...@@ -272,6 +227,10 @@ class Updater {
* Commits the current update. * Commits the current update.
*/ */
public function commit(): void { public function commit(): void {
$this->dispatchUpdateEvent(AutomaticUpdatesEvents::PRE_COMMIT);
// @todo Pass excluded paths into the committer once
// https://github.com/php-tuf/composer-stager/pull/14 is in a tagged release
// of Composer Stager.
$this->committer->commit($this->getStageDirectory(), static::getActiveDirectory()); $this->committer->commit($this->getStageDirectory(), static::getActiveDirectory());
} }
...@@ -316,16 +275,21 @@ class Updater { ...@@ -316,16 +275,21 @@ class Updater {
* @param string $event_name * @param string $event_name
* The name of the event to dispatch. * The name of the event to dispatch.
* *
* @return \Drupal\automatic_updates\Event\UpdateEvent
* The event object.
*
* @throws \Drupal\automatic_updates\Exception\UpdateException * @throws \Drupal\automatic_updates\Exception\UpdateException
* If any of the event subscribers adds a validation error. * If any of the event subscribers adds a validation error.
*/ */
public function dispatchUpdateEvent(string $event_name): void { public function dispatchUpdateEvent(string $event_name): UpdateEvent {
$event = new UpdateEvent(); $class = static::EVENT_CLASSES[$event_name] ?? UpdateEvent::class;
$event = new $class();
$this->eventDispatcher->dispatch($event, $event_name); $this->eventDispatcher->dispatch($event, $event_name);
if ($checker_results = $event->getResults(SystemManager::REQUIREMENT_ERROR)) { if ($checker_results = $event->getResults(SystemManager::REQUIREMENT_ERROR)) {
throw new UpdateException($checker_results, throw new UpdateException($checker_results,
"Unable to complete the update because of errors."); "Unable to complete the update because of errors.");
} }
return $event;
} }
} }
...@@ -40,13 +40,13 @@ class TestChecker1 implements EventSubscriberInterface { ...@@ -40,13 +40,13 @@ class TestChecker1 implements EventSubscriberInterface {
* This method is static to enable setting the expected messages before the * This method is static to enable setting the expected messages before the
* test module is enabled. * test module is enabled.
* *
* @param \Drupal\automatic_updates\Validation\ValidationResult[] $checker_results * @param \Drupal\automatic_updates\Validation\ValidationResult[]|\Throwable $checker_results
* The test validation result. * The test validation results, or an exception to throw.
* @param string $event_name * @param string $event_name
* (optional )The event name. Defaults to * (optional )The event name. Defaults to
* AutomaticUpdatesEvents::READINESS_CHECK. * AutomaticUpdatesEvents::READINESS_CHECK.
*/ */
public static function setTestResult(array $checker_results, string $event_name = AutomaticUpdatesEvents::READINESS_CHECK): void { public static function setTestResult($checker_results, string $event_name = AutomaticUpdatesEvents::READINESS_CHECK): void {
\Drupal::state()->set(static::STATE_KEY . ".$event_name", $checker_results); \Drupal::state()->set(static::STATE_KEY . ".$event_name", $checker_results);
} }
...@@ -60,6 +60,9 @@ class TestChecker1 implements EventSubscriberInterface { ...@@ -60,6 +60,9 @@ class TestChecker1 implements EventSubscriberInterface {
*/ */
protected function addResults(UpdateEvent $event, string $state_key): void { protected function addResults(UpdateEvent $event, string $state_key): void {
$results = $this->state->get($state_key, []); $results = $this->state->get($state_key, []);
if ($results instanceof \Throwable) {
throw $results;
}
foreach ($results as $result) { foreach ($results as $result) {
$event->addValidationResult($result); $event->addValidationResult($result);
} }
......
<?php
namespace Drupal\automatic_updates_test;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates\Updater;
/**
* A test-only updater which can throw errors during the update process.
*/
class TestUpdater extends Updater {
/**
* Sets the errors to be thrown during the begin() method.
*
* @param \Drupal\automatic_updates\Validation\ValidationResult[] $errors
* The validation errors that should be thrown.
*/
public static function setBeginErrors(array $errors): void {
\Drupal::state()->set('automatic_updates_test.updater_errors', [
'begin' => $errors,
]);
}
/**
* {@inheritdoc}
*/
public function begin(): string {
$errors = $this->state->get('automatic_updates_test.updater_errors', []);
if (isset($errors['begin'])) {
throw new UpdateException($errors['begin'], reset($errors['begin'])->getSummary());
}
return parent::begin();
}
}
name: 'Composer Stager bypass'
description: 'Mocks Composer Stager services for functional testing'
type: module
package: Testing
dependencies:
- automatic_updates:automatic_updates
services:
composer_stager_bypass.committer:
public: false
class: Drupal\composer_stager_bypass\Committer
decorates: automatic_updates.committer
arguments:
- '@composer_stager_bypass.committer.inner'
properties:
- { _serviceId: automatic_updates.committer }
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
/**
* Defines an update beginner which doesn't do anything.
*/
class Beginner implements BeginnerInterface {
/**
* {@inheritdoc}
*/
public function begin(string $activeDir, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
}
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\CommitterInterface;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
/**
* Defines an update committer which doesn't do any actual committing.
*/
class Committer implements CommitterInterface {
/**
* The decorated committer service.
*
* @var \PhpTuf\ComposerStager\Domain\CommitterInterface
*/
private $decorated;
/**
* Constructs a Committer object.
*
* @param \PhpTuf\ComposerStager\Domain\CommitterInterface $decorated
* The decorated committer service.
*/
public function __construct(CommitterInterface $decorated) {
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*/
public function commit(string $stagingDir, string $activeDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
/**
* {@inheritdoc}
*/
public function directoryExists(string $stagingDir): bool {
return $this->decorated->directoryExists($stagingDir);
}
}
<?php <?php
namespace Drupal\automatic_updates_test; namespace Drupal\composer_stager_bypass;
use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase; use Drupal\Core\DependencyInjection\ServiceProviderBase;
/** /**
* Defines a service provider for testing automatic updates. * Defines services to bypass Composer Stager's core functionality.
*/ */
class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase { class ComposerStagerBypassServiceProvider extends ServiceProviderBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -16,14 +16,10 @@ class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase { ...@@ -16,14 +16,10 @@ class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
public function alter(ContainerBuilder $container) { public function alter(ContainerBuilder $container) {
parent::alter($container); parent::alter($container);
$modules = $container->getParameter('container.modules'); $container->getDefinition('automatic_updates.beginner')
if (isset($modules['automatic_updates'])) { ->setClass(Beginner::class);
// Swap in our special updater implementation, which can be rigged to $container->getDefinition('automatic_updates.stager')
// throw errors during various points in the update process in order to ->setClass(Stager::class);
// test error handling during updates.
$container->getDefinition('automatic_updates.updater')
->setClass(TestUpdater::class);
}
} }
} }
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
use PhpTuf\ComposerStager\Domain\StagerInterface;
/**
* Defines an update stager which doesn't actually do anything.
*/
class Stager implements StagerInterface {
/**
* {@inheritdoc}
*/
public function stage(array $composerCommand, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace Drupal\Tests\automatic_updates\Functional; namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\Event\PreStartEvent;
use Drupal\Tests\BrowserTestBase; use Drupal\Tests\BrowserTestBase;
/** /**
...@@ -51,13 +52,16 @@ class ExclusionsTest extends BrowserTestBase { ...@@ -51,13 +52,16 @@ class ExclusionsTest extends BrowserTestBase {
* @covers \Drupal\automatic_updates\Updater::getExclusions * @covers \Drupal\automatic_updates\Updater::getExclusions
*/ */
public function testExclusions(): void { public function testExclusions(): void {
$event = new PreStartEvent();
$this->container->get('automatic_updates.excluded_paths_subscriber')
->preStart($event);
/** @var \Drupal\automatic_updates\Updater $updater */ /** @var \Drupal\automatic_updates\Updater $updater */
$updater = $this->container->get('automatic_updates.updater'); $updater = $this->container->get('automatic_updates.updater');
$reflector = new \ReflectionObject($updater); $reflector = new \ReflectionObject($updater);
$method = $reflector->getMethod('getExclusions'); $method = $reflector->getMethod('getExclusions');
$method->setAccessible(TRUE); $method->setAccessible(TRUE);
$exclusions = $method->invoke($updater); $exclusions = $method->invoke($updater, $event);
$this->assertContains("$this->siteDirectory/files", $exclusions); $this->assertContains("$this->siteDirectory/files", $exclusions);
$this->assertContains("$this->siteDirectory/private", $exclusions); $this->assertContains("$this->siteDirectory/private", $exclusions);
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
namespace Drupal\Tests\automatic_updates\Functional; namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\AutomaticUpdatesEvents; use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Validation\ValidationResult; use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1; use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1;
use Drupal\automatic_updates_test\TestUpdater;
use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
use Drupal\Tests\BrowserTestBase; use Drupal\Tests\BrowserTestBase;
...@@ -29,6 +28,7 @@ class UpdaterFormTest extends BrowserTestBase { ...@@ -29,6 +28,7 @@ class UpdaterFormTest extends BrowserTestBase {
protected static $modules = [ protected static $modules = [
'automatic_updates', 'automatic_updates',
'automatic_updates_test', 'automatic_updates_test',
'composer_stager_bypass',
'update_test', 'update_test',
]; ];
...@@ -102,65 +102,66 @@ class UpdaterFormTest extends BrowserTestBase { ...@@ -102,65 +102,66 @@ class UpdaterFormTest extends BrowserTestBase {
} }
/** /**
* Tests that the form runs update validators before starting the batch job. * Tests handling of errors and warnings during the update process.
*/ */
public function testValidation(): void { public function testUpdateErrors(): void {
$this->setCoreVersion('9.8.0'); $session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Ensure that one of the update validators will produce an error when we $this->setCoreVersion('9.8.0');
// try to run updates.
$this->createTestValidationResults(); $this->createTestValidationResults();
$expected_results = $this->testResults['checker_1']['1 error']; $expected_results = $this->testResults['checker_1']['1 error'];
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START); // Repackage the validation error as an exception, so we can test what
// happens if a validator throws.
$error = new UpdateException($expected_results, 'The update exploded.');
TestChecker1::setTestResult($error, AutomaticUpdatesEvents::PRE_START);
$this->drupalLogin($this->rootUser); $this->drupalLogin($this->rootUser);
$this->checkForUpdates(); $this->checkForUpdates();
$this->drupalGet('/admin/automatic-update'); $this->drupalGet('/admin/automatic-update');
$this->getSession()->getPage()->pressButton('Download these updates'); $page->pressButton('Download these updates');
$this->checkForMetaRefresh();
$assert_session->pageTextContains('An error has occurred.');
$page->clickLink('the error page');
$assert_session->pageTextContains((string) $expected_results[0]->getMessages()[0]);
// Since there's only one error message, we shouldn't see the summary...
$assert_session->pageTextNotContains($expected_results[0]->getSummary());
// ...but we should see the exception message.
$assert_session->pageTextContains('The update exploded.');
$assert_session = $this->assertSession(); // If a validator flags an error, but doesn't throw, the update should still
// We should still be on the same page, having not passed validation. // be halted.
$assert_session->addressEquals('/admin/automatic-update'); TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START);
foreach ($expected_results[0]->getMessages() as $message) { $this->deleteStagedUpdate();
$assert_session->pageTextContains($message); $page->pressButton('Download these updates');
} $this->checkForMetaRefresh();
// Since there is only one error message, we shouldn't see the summary. $assert_session->pageTextContains('An error has occurred.');
$page->clickLink('the error page');
// Since there's only one message, we shouldn't see the summary.
$assert_session->pageTextNotContains($expected_results[0]->getSummary()); $assert_session->pageTextNotContains($expected_results[0]->getSummary());
$assert_session->pageTextContains((string) $expected_results[0]->getMessages()[0]);
// Ensure the update-ready form runs pre-commit checks immediately, even // If a validator flags a warning, but doesn't throw, the update should
// before it's submitted. // continue.
$expected_results = $this->testResults['checker_1']['1 error 1 warning']; $expected_results = $this->testResults['checker_1']['1 warning'];
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_COMMIT); TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START);
$this->drupalGet('/admin/automatic-update-ready'); $session->reload();
$assert_session->pageTextContains($expected_results['1:error']->getMessages()[0]); $this->deleteStagedUpdate();
// Only show errors, not warnings. $page->pressButton('Download these updates');
$assert_session->pageTextNotContains($expected_results['1:warning']->getMessages()[0]); $this->checkForMetaRefresh();
// Since there is only one error message, we shouldn't see the summary. And $assert_session->pageTextContains('Ready to update');
// we shouldn't see the warning's summary in any case.
$assert_session->pageTextNotContains($expected_results['1:error']->getSummary());
$assert_session->pageTextNotContains($expected_results['1:warning']->getSummary());
} }
/** /**
* Tests that errors during the update process are displayed as messages. * Deletes a staged, failed update.
*/ */
public function testBatchErrorsAreForwardedToMessenger(): void { private function deleteStagedUpdate(): void {
$this->setCoreVersion('9.8.0'); $session = $this->getSession();
$session->getPage()->pressButton('Delete existing update');
$error = ValidationResult::createError([ $this->assertSession()->pageTextContains('Staged update deleted');
t('💥'), $session->reload();
], t('The update exploded.'));
TestUpdater::setBeginErrors([$error]);
$this->drupalLogin($this->rootUser);
$this->checkForUpdates();
$this->drupalGet('/admin/automatic-update');
$this->submitForm([], 'Download these updates');
$assert_session = $this->assertSession();
$assert_session->pageTextContains('An error has occurred.');
$this->getSession()->getPage()->clickLink('the error page');
$assert_session->pageTextContains('💥');
$assert_session->pageTextContains('The update exploded.');
} }
/** /**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment