diff --git a/automatic_updates.install b/automatic_updates.install
index 10f6b11..d7ac9f8 100644
--- a/automatic_updates.install
+++ b/automatic_updates.install
@@ -5,15 +5,78 @@
* Automatic updates install file.
*/
+use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\Core\Url;
/**
* Requirements checks for automatic updates.
*/
function automatic_updates_requirements() {
$requirements = [];
+ _automatic_updates_checker_requirements($requirements);
+ _automatic_updates_psa_requirements($requirements);
+ return $requirements;
+}
+
+/**
+ * Display requirements from results of readiness checker.
+ *
+ * @param array $requirements
+ * The requirements array.
+ */
+function _automatic_updates_checker_requirements(array &$requirements) {
+ /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
+ $checker = \Drupal::service('automatic_updates.readiness_checker');
+ if (!$checker->isEnabled()) {
+ return;
+ }
+ $checker_results = $checker->results();
+ $last_check_timestamp = $checker->timestamp();
+ $requirements['automatic_updates_readiness'] = [
+ 'title' => t('Update readiness checks'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => t('Your site is ready to for automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']),
+ ];
+ if (!empty($checker_results)) {
+ $requirements['automatic_updates_readiness']['severity'] = REQUIREMENT_ERROR;
+ $requirements['automatic_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:');
+ $requirements['automatic_updates_readiness']['description'] = [
+ '#theme' => 'item_list',
+ '#items' => $checker_results,
+ ];
+ }
+ if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
+ $requirements['automatic_updates_readiness']['severity'] = REQUIREMENT_ERROR;
+ $requirements['automatic_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']);
+ $readiness_check = Url::fromRoute('automatic_updates.update_readiness');
+ $time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp);
+ if ($last_check_timestamp === 0) {
+ $requirements['automatic_updates_readiness']['description'] = t('Run readiness checks manually.', [
+ '@link' => $readiness_check->toString(),
+ ]);
+ }
+ elseif ($readiness_check->access()) {
+ $requirements['automatic_updates_readiness']['description'] = t('Last run @time ago. Run readiness checks manually.', [
+ '@time' => $time_ago,
+ '@link' => $readiness_check->toString(),
+ ]);
+ }
+ else {
+ $requirements['automatic_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]);
+ }
+ }
+}
+
+/**
+ * Display requirements from public service announcements.
+ *
+ * @param array $requirements
+ * The requirements array.
+ */
+function _automatic_updates_psa_requirements(array &$requirements) {
if (!\Drupal::config('automatic_updates.settings')->get('enable_psa')) {
- return $requirements;
+ return;
}
/** @var \Drupal\automatic_updates\Services\AutomaticUpdatesPsa $psa */
$psa = \Drupal::service('automatic_updates.psa');
@@ -31,5 +94,4 @@ function automatic_updates_requirements() {
'#items' => $messages,
];
}
- return $requirements;
}
diff --git a/automatic_updates.module b/automatic_updates.module
index 34ea032..ea7c5ea 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -5,6 +5,8 @@
* Contains automatic_updates.module..
*/
+use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+
/**
* Implements hook_page_top().
*/
@@ -34,5 +36,25 @@ function automatic_updates_page_top(array &$page_top) {
foreach ($psa->getPublicServiceMessages() as $psa) {
\Drupal::messenger()->addError($psa);
}
+ $last_check_timestamp = \Drupal::service('automatic_updates.readiness_checker')->timestamp();
+ if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
+ \Drupal::messenger()->addError(t('Your site has not recently run an update readiness check.'));
+ }
+ $results = \Drupal::service('automatic_updates.readiness_checker')->results();
+ if ($results) {
+ \Drupal::messenger()->addError(t('Your site is currently failing readiness checks for automatic updates. It cannot be automatically updated until further action is performed:', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']));
+ foreach ($results as $message) {
+ \Drupal::messenger()->addError($message);
+ }
+ }
}
}
+
+/**
+ * Implements hook_cron().
+ */
+function automatic_updates_cron() {
+ /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
+ $checker = \Drupal::service('automatic_updates.readiness_checker');
+ $checker->run();
+}
diff --git a/automatic_updates.routing.yml b/automatic_updates.routing.yml
index 18c0868..453aa43 100644
--- a/automatic_updates.routing.yml
+++ b/automatic_updates.routing.yml
@@ -7,3 +7,13 @@ automatic_updates.admin_form:
_permission: 'administer software updates'
options:
_admin_route: TRUE
+
+automatic_updates.update_readiness:
+ path: '/admin/config/automatic_updates/readiness'
+ defaults:
+ _controller: '\Drupal\automatic_updates\Controller\ReadinessCheckerController::run'
+ _title: 'Update readiness checking...'
+ requirements:
+ _permission: 'administer software updates'
+ options:
+ _admin_route: TRUE
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 26d646a..f3c0964 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -2,6 +2,10 @@ services:
logger.channel.automatic_updates:
parent: logger.channel_base
arguments: ['automatic_updates']
+ automatic_updates.drupal_finder:
+ class: DrupalFinder\DrupalFinder
+ automatic_updates.filesystem_cache:
+ class: League\Flysystem\Cached\Storage\Memory
automatic_updates.psa:
class: Drupal\automatic_updates\Services\AutomaticUpdatesPsa
arguments:
@@ -13,3 +17,25 @@ services:
- '@extension.list.profile'
- '@extension.list.theme'
- '@logger.channel.automatic_updates'
+ automatic_updates.readiness_checker:
+ class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager
+ arguments:
+ - '@state'
+ - '@config.factory'
+ tags:
+ - { name: service_collector, tag: readiness_checker, call: addChecker }
+ automatic_updates.readonly_checker:
+ class: Drupal\automatic_updates\ReadinessChecker\ReadOnlyFilesystem
+ arguments:
+ - '@logger.channel.automatic_updates'
+ - '@automatic_updates.drupal_finder'
+ - '@automatic_updates.filesystem_cache'
+ tags:
+ - { name: readiness_checker, priority: 100 }
+ automatic_updates.disk_space_checker:
+ class: Drupal\automatic_updates\ReadinessChecker\DiskSpace
+ arguments:
+ - '@logger.channel.automatic_updates'
+ - '@automatic_updates.drupal_finder'
+ tags:
+ - { name: readiness_checker }
diff --git a/composer.json b/composer.json
index 0e18c77..e366d15 100644
--- a/composer.json
+++ b/composer.json
@@ -12,6 +12,9 @@
},
"require": {
"ext-json": "*",
- "composer/semver": "^1.0@dev"
+ "composer/semver": "^1.0",
+ "league/flysystem": "^1.1",
+ "league/flysystem-cached-adapter": "^1.0",
+ "webflo/drupal-finder": "^1.1"
}
}
diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml
index 085f065..01c1bf1 100644
--- a/config/install/automatic_updates.settings.yml
+++ b/config/install/automatic_updates.settings.yml
@@ -3,3 +3,4 @@
# https://www.drupal.org/project/automatic_updates/issues/3045273
psa_endpoint: 'http://localhost/automatic_updates/test-json'
enable_psa: true
+enable_readiness_checks: true
diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml
index ca29403..eeeb8c8 100644
--- a/config/schema/automatic_updates.schema.yml
+++ b/config/schema/automatic_updates.schema.yml
@@ -8,3 +8,6 @@ automatic_updates.settings:
enable_psa:
type: boolean
label: 'Enable PSA notices'
+ enable_readiness_checks:
+ type: boolean
+ label: 'Enable readiness checks'
diff --git a/src/Controller/ReadinessCheckerController.php b/src/Controller/ReadinessCheckerController.php
new file mode 100644
index 0000000..e91e35a
--- /dev/null
+++ b/src/Controller/ReadinessCheckerController.php
@@ -0,0 +1,59 @@
+checker = $checker;
+ $this->stringTranslation = $string_translation;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('automatic_updates.readiness_checker'),
+ $container->get('string_translation')
+ );
+ }
+
+ /**
+ * Run the readiness checkers.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ * A redirect
+ */
+ public function run() {
+ $messages = $this->checker->run();
+ if (empty($messages)) {
+ $this->messenger()->addStatus($this->t('No issues found. Your site is ready to for automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']));
+ }
+ return $this->redirect('automatic_updates.admin_form');
+ }
+
+}
diff --git a/src/Form/AdminForm.php b/src/Form/AdminForm.php
index 78217e8..ed51f7d 100644
--- a/src/Form/AdminForm.php
+++ b/src/Form/AdminForm.php
@@ -4,12 +4,38 @@ namespace Drupal\automatic_updates\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Administration form for automatic updates.
*/
class AdminForm extends ConfigFormBase {
+ /**
+ * The readiness checker.
+ *
+ * @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface
+ */
+ protected $checker;
+
+ /**
+ * The data formatter.
+ *
+ * @var \Drupal\Core\Datetime\DateFormatterInterface
+ */
+ protected $dateFormatter;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $instance = parent::create($container);
+ $instance->checker = $container->get('automatic_updates.readiness_checker');
+ $instance->dateFormatter = $container->get('date.formatter');
+ return $instance;
+ }
+
/**
* {@inheritdoc}
*/
@@ -36,6 +62,18 @@ class AdminForm extends ConfigFormBase {
'#title' => $this->t('Enable messaging of public service alerts (PSAs)'),
'#default_value' => $config->get('enable_psa'),
];
+ $last_check_timestamp = $this->checker->timestamp();
+ $form['enable_readiness_checks'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Check the readiness of automatically updating the site.'),
+ '#default_value' => $config->get('enable_readiness_checks'),
+ ];
+ if ($this->checker->isEnabled()) {
+ $form['enable_readiness_checks']['#description'] = $this->t('Readiness checks were last run @time ago. Run the readiness checks manually.', [
+ '@time' => $this->dateFormatter->formatTimeDiffSince($last_check_timestamp),
+ '@link' => Url::fromRoute('automatic_updates.update_readiness')->toString(),
+ ]);
+ }
return parent::buildForm($form, $form_state);
}
diff --git a/src/ReadinessChecker/DiskSpace.php b/src/ReadinessChecker/DiskSpace.php
new file mode 100644
index 0000000..4809c8b
--- /dev/null
+++ b/src/ReadinessChecker/DiskSpace.php
@@ -0,0 +1,152 @@
+logger = $logger;
+ $this->drupalFinder = $drupal_finder;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run() {
+ $messages = [];
+ if (!$this->getDrupalRoot()) {
+ $messages[] = $this->t('The Drupal root directory could not be located.');
+ return $messages;
+ }
+ $this->diskSpace($messages);
+ return $messages;
+ }
+
+ /**
+ * Get the Drupal root path.
+ *
+ * @return string
+ * The Drupal root path.
+ */
+ protected function getDrupalRoot() {
+ if (!$this->drupalRoot && $this->drupalFinder->locateRoot(getcwd())) {
+ $this->drupalRoot = $this->drupalFinder->getDrupalRoot();
+ }
+ return $this->drupalRoot;
+ }
+
+ /**
+ * Get the vendor path.
+ *
+ * @return string
+ * The vendor path.
+ */
+ protected function getVendorPath() {
+ if (!$this->vendorPath && $this->drupalFinder->locateRoot(getcwd())) {
+ $this->vendorPath = $this->drupalFinder->getVendorDir();
+ }
+ return $this->vendorPath;
+ }
+
+ /**
+ * Check if the filesystem has sufficient disk space.
+ *
+ * @param array $messages
+ * The messages array of translatable strings.
+ */
+ protected function diskSpace(array &$messages) {
+ if ($this->areSameLogicalDisk($this->getDrupalRoot(), $this->getVendorPath())) {
+ if (disk_free_space($this->getDrupalRoot()) < static::MINIMUM_DISK_SPACE) {
+ $messages[] = $this->t('Logical disk "@root" has insufficient space. There must be at least @space megabytes free.', [
+ '@root' => $this->getDrupalRoot(),
+ '@space' => static::MINIMUM_DISK_SPACE / static::MEGABYTE_DIVISOR,
+ ]);
+ }
+ return;
+ }
+ if (disk_free_space($this->getDrupalRoot()) < static::MINIMUM_DISK_SPACE) {
+ $messages[] = $this->t('Drupal root filesystem "@root" has insufficient space. There must be at least @space megabytes free.', [
+ '@root' => $this->getDrupalRoot(),
+ '@space' => static::MINIMUM_DISK_SPACE / static::MEGABYTE_DIVISOR,
+ ]);
+ }
+ if (disk_free_space($this->getVendorPath()) < static::MINIMUM_DISK_SPACE) {
+ $messages[] = $this->t('Vendor filesystem "@vendor" has insufficient space. There must be at least @space megabytes free.', [
+ '@vendor' => $this->getVendorPath(),
+ '@space' => static::MINIMUM_DISK_SPACE / static::MEGABYTE_DIVISOR,
+ ]);
+ }
+ }
+
+ /**
+ * Determine if the root and vendor file system are the same logical disk.
+ *
+ * @param string $root
+ * Root file path.
+ * @param string $vendor
+ * Vendor file path.
+ *
+ * @return bool
+ * TRUE if same file system, FALSE otherwise.
+ */
+ protected function areSameLogicalDisk($root, $vendor) {
+ $root_statistics = stat($root);
+ $vendor_statistics = stat($vendor);
+ return $root_statistics['dev'] === $vendor_statistics['dev'];
+ }
+
+}
diff --git a/src/ReadinessChecker/ReadOnlyFilesystem.php b/src/ReadinessChecker/ReadOnlyFilesystem.php
new file mode 100644
index 0000000..a71c626
--- /dev/null
+++ b/src/ReadinessChecker/ReadOnlyFilesystem.php
@@ -0,0 +1,214 @@
+logger = $logger;
+ $this->drupalFinder = $drupal_finder;
+ $this->fileSystemCache = $filesystem_cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run() {
+ $messages = [];
+ if (!$this->getCoreFileSystem() || !$this->getCoreFileSystem()->has('core.services.yml')) {
+ $messages[] = $this->t('The public web directory could not be located.');
+ return $messages;
+ }
+ $this->readOnly($messages);
+ return $messages;
+ }
+
+ /**
+ * Get the core filesystem.
+ *
+ * @return \League\Flysystem\Filesystem|\League\Flysystem\FilesystemInterface
+ * The core filesystem.
+ */
+ protected function getCoreFileSystem() {
+ if (!$this->coreFileSystem && $this->drupalFinder->locateRoot(getcwd())) {
+ $this->rootPath = $this->drupalFinder->getDrupalRoot();
+ $this->coreFileSystem = new Filesystem(new CachedAdapter(new Local($this->rootPath . '/core'), $this->fileSystemCache));
+ }
+ return $this->coreFileSystem;
+ }
+
+ /**
+ * Get the vendor file system.
+ *
+ * @return \League\Flysystem\Filesystem|\League\Flysystem\FilesystemInterface
+ * The vendor filesystem.
+ */
+ protected function getVendorFileSystem() {
+ if (!$this->vendorFileSystem && $this->drupalFinder->locateRoot(getcwd())) {
+ $this->vendorPath = $this->drupalFinder->getVendorDir();
+ $this->vendorFileSystem = new Filesystem(new CachedAdapter(new Local($this->vendorPath), $this->fileSystemCache));
+ }
+ return $this->vendorFileSystem;
+ }
+
+ /**
+ * Check if the filesystem is read only.
+ *
+ * @param array $messages
+ * The messages array of translatable strings.
+ */
+ protected function readOnly(array &$messages) {
+ // Disable default error handling as rename of files on read only file
+ // systems causes PHP errors.
+ restore_error_handler();
+ try {
+ if ($this->areSameLogicalDisk($this->rootPath, $this->vendorPath)) {
+ $error = $this->t('Logical disk "@file_system" is read only. Updates to Drupal cannot be applied against a read only file system.', ['@file_system' => $this->rootPath]);
+ $this->doReadOnlyCheck($this->getCoreFileSystem(), 'core.services.yml', $messages, $error);
+ }
+ else {
+ $error = $this->t('Drupal core filesystem "@file_system" is read only. Updates to Drupal core cannot be applied against a read only file system.', ['@file_system' => $this->rootPath . '/core']);
+ $this->doReadOnlyCheck($this->getCoreFileSystem(), 'core.services.yml', $messages, $error);
+ $error = $this->t('Vendor filesystem "@file_system" is read only. Updates to the site\'s code base cannot be applied against a read only file system.', ['@file_system' => $this->vendorPath]);
+ $this->doReadOnlyCheck($this->getVendorFileSystem(), 'composer/LICENSE', $messages, $error);
+ }
+ }
+ catch (\LogicException $exception) {
+ $this->logger->error($exception->getMessage());
+ $messages[] = $this->t('During read only check, a logic exception was encountered');
+ }
+ // Re-enable the default error handling.
+ set_error_handler('_drupal_error_handler');
+ }
+
+ /**
+ * Do the read only check.
+ *
+ * @param \League\Flysystem\FilesystemInterface $filesystem
+ * The filesystem to test.
+ * @param string $file
+ * The file path.
+ * @param array $messages
+ * The messages array of translatable strings.
+ * @param \Drupal\Component\Render\MarkupInterface $message
+ * The error message to add if the file is read only.
+ */
+ protected function doReadOnlyCheck(FilesystemInterface $filesystem, $file, array &$messages, MarkupInterface $message) {
+ try {
+ if ($filesystem && $filesystem->rename($file, "$file.moved")) {
+ // Move it back after moving it.
+ $filesystem->rename("$file.moved", $file);
+ }
+ else {
+ $this->logger->error($message);
+ $messages[] = $message;
+ }
+ }
+ catch (FileExistsException $exception) {
+ $this->logger->error($exception->getMessage());
+ $messages[] = $this->t('File already exists at path: @file', ['@file' => $exception->getPath()]);
+ }
+ catch (FileNotFoundException $exception) {
+ $this->logger->error($exception->getMessage());
+ $messages[] = $this->t('File not found at path: @file', ['@file' => $exception->getPath()]);
+ }
+ // Catching throwable because rename triggers error in some cases.
+ catch (\Throwable $throwable) {
+ $this->logger->error($throwable->getMessage());
+ $messages[] = $throwable->getMessage();
+ }
+ }
+
+ /**
+ * Determine if the root and vendor file system are the same logical disk.
+ *
+ * @param string $root
+ * Root file path.
+ * @param string $vendor
+ * Vendor file path.
+ *
+ * @return bool
+ * TRUE if same file system, FALSE otherwise.
+ */
+ protected function areSameLogicalDisk($root, $vendor) {
+ $root_statistics = stat($root);
+ $vendor_statistics = stat($vendor);
+ return $root_statistics['dev'] === $vendor_statistics['dev'];
+ }
+
+}
diff --git a/src/ReadinessChecker/ReadinessCheckerInterface.php b/src/ReadinessChecker/ReadinessCheckerInterface.php
new file mode 100644
index 0000000..11fe60a
--- /dev/null
+++ b/src/ReadinessChecker/ReadinessCheckerInterface.php
@@ -0,0 +1,18 @@
+keyValue = $key_value;
+ $this->configFactory = $config_factory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addChecker(ReadinessCheckerInterface $checker, $priority = 0) {
+ $this->checkers[$priority][] = $checker;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run() {
+ $messages = [];
+ if (!$this->isEnabled()) {
+ return $messages;
+ }
+ foreach ($this->getSortedCheckers() as $checker) {
+ $messages = array_merge($messages, $checker->run());
+ }
+ $this->keyValue->set('readiness_check_results', $messages);
+ $this->keyValue->set('readiness_check_timestamp', \Drupal::time()->getCurrentTime());
+ return $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function results() {
+ return $this->keyValue->get('readiness_check_results', []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function timestamp() {
+ return $this->keyValue->get('readiness_check_timestamp', 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEnabled() {
+ return $this->configFactory->get('automatic_updates.settings')->get('enable_readiness_checks');
+ }
+
+ /**
+ * Sorts checkers according to priority.
+ *
+ * @return \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerInterface[]
+ * A sorted array of checker objects.
+ */
+ protected function getSortedCheckers() {
+ $sorted = [];
+ krsort($this->checkers);
+
+ foreach ($this->checkers as $checkers) {
+ $sorted = array_merge($sorted, $checkers);
+ }
+ return $sorted;
+ }
+
+}
diff --git a/src/ReadinessChecker/ReadinessCheckerManagerInterface.php b/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
new file mode 100644
index 0000000..05870f1
--- /dev/null
+++ b/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
@@ -0,0 +1,59 @@
+container->get('logger.channel.automatic_updates'), $this->container->get('automatic_updates.drupal_finder'));
+ $messages = $disk_space->run();
+ $this->assertEmpty($messages);
+
+ // Out of space.
+ $disk_space = new TestDiskSpace($this->container->get('logger.channel.automatic_updates'), $this->container->get('automatic_updates.drupal_finder'));
+ $messages = $disk_space->run();
+ $this->assertCount(1, $messages);
+
+ // Out of space not the same logical disk.
+ $disk_space = new TestDiskSpaceNonSameDisk($this->container->get('logger.channel.automatic_updates'), $this->container->get('automatic_updates.drupal_finder'));
+ $messages = $disk_space->run();
+ $this->assertCount(2, $messages);
+ }
+
+}
+
+/**
+ * Class TestDiskSpace.
+ */
+class TestDiskSpace extends DiskSpace {
+
+ /**
+ * Override the default free disk space minimum to an insanely high number.
+ */
+ const MINIMUM_DISK_SPACE = 99999999999999999999999999999999999999999999999999;
+
+}
+
+/**
+ * Class TestDiskSpaceNonSameDisk.
+ */
+class TestDiskSpaceNonSameDisk extends TestDiskSpace {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function areSameLogicalDisk($root, $vendor) {
+ return FALSE;
+ }
+
+}
diff --git a/tests/src/Kernel/ReadinessChecker/ReadOnlyTest.php b/tests/src/Kernel/ReadinessChecker/ReadOnlyTest.php
new file mode 100644
index 0000000..f95fc7b
--- /dev/null
+++ b/tests/src/Kernel/ReadinessChecker/ReadOnlyTest.php
@@ -0,0 +1,119 @@
+createMock(Filesystem::class);
+ $filesystem->expects($this->any())
+ ->method('has')
+ ->withAnyParameters()
+ ->will($this->onConsecutiveCalls(FALSE, TRUE, TRUE, TRUE));
+ $filesystem->expects($this->any())
+ ->method('rename')
+ ->withAnyParameters()
+ ->will($this->onConsecutiveCalls(
+ FALSE,
+ FALSE,
+ FALSE,
+ $this->throwException(new FileExistsException('core.services.yml')),
+ $this->throwException(new FileNotFoundException('composer/LICENSE'))
+ )
+ );
+
+ $readonly = $this->getMockBuilder(TestReadOnlyFilesystem::class)
+ ->setConstructorArgs([
+ $this->createMock(LoggerInterface::class),
+ new DrupalFinder(),
+ $this->createMock(CacheInterface::class),
+ ])
+ ->setMethods([
+ 'getCoreFileSystem',
+ 'getVendorFileSystem',
+ 'areSameLogicalDisk',
+ ])
+ ->getMock();
+ $readonly->method('getCoreFileSystem')
+ ->willReturn($filesystem);
+ $readonly->method('getVendorFileSystem')
+ ->willReturn($filesystem);
+ $readonly->expects($this->any())
+ ->method('areSameLogicalDisk')
+ ->withAnyParameters()
+ ->will($this->onConsecutiveCalls(
+ TRUE,
+ FALSE,
+ FALSE,
+ )
+ );
+
+ // Test can't locate drupal.
+ $messages = $readonly->run();
+ $this->assertEquals([$this->t('The public web directory could not be located.')], $messages);
+
+ // Test same logical disk.
+ $expected_messages = [];
+ $expected_messages[] = $this->t('Logical disk "/var/www/html" is read only. Updates to Drupal cannot be applied against a read only file system.');
+ $messages = $readonly->run();
+ $this->assertEquals($expected_messages, $messages);
+
+ // Test read-only.
+ $expected_messages = [];
+ $expected_messages[] = $this->t('Drupal core filesystem "/var/www/html/core" is read only. Updates to Drupal core cannot be applied against a read only file system.');
+ $expected_messages[] = $this->t('Vendor filesystem "/var/www/html/vendor" is read only. Updates to the site\'s code base cannot be applied against a read only file system.');
+ $messages = $readonly->run();
+ $this->assertEquals($expected_messages, $messages);
+
+ // Test FileExistsException and FileNotFoundException.
+ $expected_messages = [];
+ $expected_messages[] = $this->t('File already exists at path: core.services.yml');
+ $expected_messages[] = $this->t('File not found at path: composer/LICENSE');
+ $messages = $readonly->run();
+ $this->assertEquals($expected_messages, $messages);
+ }
+
+}
+
+/**
+ * Class TestReadOnlyFilesystem.
+ */
+class TestReadOnlyFilesystem extends ReadOnlyFilesystem {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $rootPath = '/var/www/html';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $vendorPath = '/var/www/html/vendor';
+
+}
diff --git a/tests/src/Kernel/ReadinessChecker/ReadinessCheckerTest.php b/tests/src/Kernel/ReadinessChecker/ReadinessCheckerTest.php
new file mode 100644
index 0000000..0e5fb95
--- /dev/null
+++ b/tests/src/Kernel/ReadinessChecker/ReadinessCheckerTest.php
@@ -0,0 +1,32 @@
+container->get('automatic_updates.readiness_checker');
+ $this->assertEmpty($checker->run());
+ }
+
+}