Skip to content
Snippets Groups Projects
Commit c72c1bd6 authored by Lucas Hedding's avatar Lucas Hedding Committed by Lucas Hedding
Browse files

Issue #3055872 by heddn, catch, eiriksm: In place updates (from quasi-patch zip)

parent b36fb251
No related branches found
No related tags found
No related merge requests found
Showing
with 945 additions and 4 deletions
......@@ -108,3 +108,12 @@ services:
class: Drupal\automatic_updates\EventSubscriber\CronOverride
tags:
- { name: config.factory.override }
automatic_updates.update:
class: Drupal\automatic_updates\Services\InPlaceUpdate
arguments:
- '@logger.channel.automatic_updates'
- '@plugin.manager.archiver'
- '@config.factory'
- '@file_system'
- '@http_client'
- '@automatic_updates.drupal_finder'
......@@ -12,9 +12,10 @@
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"composer/semver": "^1.0",
"ocramius/package-versions": "^1.4",
"webflo/drupal-finder": "^1.1"
"webflo/drupal-finder": "^1.2"
},
"require-dev": {
"drupal/ctools": "3.2.0"
......
......@@ -5,3 +5,4 @@ check_frequency: 43200
enable_readiness_checks: true
hashes_uri: 'https://updates.drupal.org/release-hashes'
ignored_paths: "modules/custom/*\nthemes/custom/*\nprofiles/custom/*"
download_uri: 'https://www.drupal.org/in-place-updates'
......@@ -23,3 +23,6 @@ automatic_updates.settings:
ignored_paths:
type: string
label: 'List of files paths to ignore when running readiness checks'
download_uri:
type: string
label: 'URI for downloading in-place update assets'
......@@ -4,19 +4,23 @@ build:
assessment:
validate_codebase:
phplint:
container_composer:
phpcs:
# phpcs will use core's specified version of Coder.
sniff-all-files: true
halt-on-fail: true
testing:
container_command:
commands: "cd ${SOURCE_DIR} && sudo -u www-data composer require drupal/ctools:3.2.0 --prefer-source --optimize-autoloader"
commands:
- cd ${SOURCE_DIR} && sudo -u www-data composer require drupal/ctools:3.2.0 --prefer-source --optimize-autoloader
- cd ${SOURCE_DIR} && sudo -u www-data curl https://www.drupal.org/files/issues/2019-10-07/3085728.patch | git apply -
- cd ${SOURCE_DIR} && sudo -u www-data curl https://www.drupal.org/files/issues/2019-10-07/3086373.patch | git apply -
# run_tests task is executed several times in order of performance speeds.
# halt-on-fail can be set on the run_tests tasks in order to fail fast.
# suppress-deprecations is false in order to be alerted to usages of
# deprecated code.
run_tests.standard:
types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
types: 'PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional,PHPUnit-Build'
testgroups: '--all'
suppress-deprecations: false
halt-on-fail: false
<?php
namespace Drupal\automatic_updates\Services;
use Drupal\Core\Archiver\ArchiverInterface;
use Drupal\Core\Archiver\ArchiverManager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Url;
use DrupalFinder\DrupalFinder;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerInterface;
/**
* Class to apply in-place updates.
*/
class InPlaceUpdate implements UpdateInterface {
/**
* The manifest file that lists all file deletions.
*/
const DELETION_MANIFEST = 'DELETION_MANIFEST.txt';
/**
* The checksum file with hashes of archive file contents.
*/
const CHECKSUM_LIST = 'checksumlist.csig';
/**
* The directory inside the archive for file additions and modifications.
*/
const ARCHIVE_DIRECTORY = 'files/';
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The archive manager.
*
* @var \Drupal\Core\Archiver\ArchiverManager
*/
protected $archiveManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The HTTP client service.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The root file path.
*
* @var string
*/
protected $rootPath;
/**
* The vendor file path.
*
* @var string
*/
protected $vendorPath;
/**
* The folder where files are backed up.
*
* @var string
*/
protected $backup;
/**
* The temporary extract directory.
*
* @var string
*/
protected $tempDirectory;
/**
* Constructs an InPlaceUpdate.
*
* @param \Psr\Log\LoggerInterface $logger
* The logger.
* @param \Drupal\Core\Archiver\ArchiverManager $archive_manager
* The archive manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The filesystem service.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client service.
* @param \DrupalFinder\DrupalFinder $drupal_finder
* The Drupal finder service.
*/
public function __construct(LoggerInterface $logger, ArchiverManager $archive_manager, ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, ClientInterface $http_client, DrupalFinder $drupal_finder) {
$this->logger = $logger;
$this->archiveManager = $archive_manager;
$this->configFactory = $config_factory;
$this->fileSystem = $file_system;
$this->httpClient = $http_client;
$drupal_finder->locateRoot(getcwd());
$this->rootPath = $drupal_finder->getDrupalRoot();
$this->vendorPath = rtrim($drupal_finder->getVendorDir(), '/\\') . DIRECTORY_SEPARATOR;
}
/**
* {@inheritdoc}
*/
public function update($project_name, $project_type, $from_version, $to_version) {
$success = FALSE;
if ($project_name === 'drupal') {
$project_root = $this->rootPath;
}
else {
$project_root = drupal_get_path($project_type, $project_name);
}
if ($archive = $this->getArchive($project_name, $from_version, $to_version)) {
if ($this->backup($archive, $project_root)) {
$success = $this->processUpdate($archive, $project_root);
}
if (!$success) {
$this->rollback($project_root);
}
}
return $success;
}
/**
* Get an archive with the quasi-patch contents.
*
* @param string $project_name
* The project name.
* @param string $from_version
* The current project version.
* @param string $to_version
* The desired next project version.
*
* @return \Drupal\Core\Archiver\ArchiverInterface|null
* The archive or NULL if download fails.
*/
protected function getArchive($project_name, $from_version, $to_version) {
$url = $this->buildUrl($project_name, $this->getQuasiPatchFileName($project_name, $from_version, $to_version));
$destination = $this->fileSystem->realpath($this->fileSystem->getDestinationFilename("temporary://$project_name.zip", FileSystemInterface::EXISTS_RENAME));
try {
$this->httpClient->get($url, ['sink' => $destination]);
/** @var \Drupal\Core\Archiver\ArchiverInterface $archive */
return $this->archiveManager->getInstance(['filepath' => $destination]);
}
catch (RequestException $exception) {
$this->logger->error('Update for @project to version @version failed for reason @message', [
'@project' => $project_name,
'@version' => $to_version,
'@message' => $exception->getMessage(),
]);
}
}
/**
* Process update.
*
* @param \Drupal\Core\Archiver\ArchiverInterface $archive
* The archive.
* @param string $project_root
* The project root directory.
*
* @return bool
* Return TRUE if update succeeds, FALSE otherwise.
*/
protected function processUpdate(ArchiverInterface $archive, $project_root) {
$archive->extract($this->getTempDirectory());
foreach ($this->getFilesList($this->getTempDirectory()) as $file) {
$file_real_path = $this->getFileRealPath($file);
$file_path = substr($file_real_path, strlen($this->getTempDirectory() . self::ARCHIVE_DIRECTORY));
$project_real_path = $this->getProjectRealPath($file_path, $project_root);
try {
$directory = dirname($project_real_path);
$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
$this->fileSystem->copy($file_real_path, $project_real_path, FileSystemInterface::EXISTS_REPLACE);
}
catch (FileException $exception) {
return FALSE;
}
}
foreach ($this->getDeletions() as $deletion) {
try {
$this->fileSystem->delete($this->getProjectRealPath($deletion, $project_root));
}
catch (FileException $exception) {
return FALSE;
}
}
return TRUE;
}
/**
* Backup before an update.
*
* @param \Drupal\Core\Archiver\ArchiverInterface $archive
* The archive.
* @param string $project_root
* The project root directory.
*
* @return bool
* Return TRUE if backup succeeds, FALSE otherwise.
*/
protected function backup(ArchiverInterface $archive, $project_root) {
$backup = $this->fileSystem->createFilename('automatic_updates-backup', 'temporary://');
$this->fileSystem->prepareDirectory($backup);
$this->backup = $this->fileSystem->realpath($backup) . DIRECTORY_SEPARATOR;
if (!$this->backup) {
return FALSE;
}
foreach ($archive->listContents() as $file) {
// Ignore files that aren't in the files directory.
if (!$this->stripFileDirectoryPath($file)) {
continue;
}
$success = $this->doBackup($file, $project_root);
if (!$success) {
return FALSE;
}
}
$archive->extract($this->getTempDirectory(), [self::DELETION_MANIFEST]);
foreach ($this->getDeletions() as $deletion) {
$success = $this->doBackup($deletion, $project_root);
if (!$success) {
return FALSE;
}
}
return TRUE;
}
/**
* Remove the files directory path from files from the archive.
*
* @param string $file
* The file path.
*
* @return bool
* TRUE if path was removed, else FALSE.
*/
protected function stripFileDirectoryPath(&$file) {
if (substr($file, 0, 6) === self::ARCHIVE_DIRECTORY) {
$file = substr($file, 6);
return TRUE;
}
return FALSE;
}
/**
* Execute file backup.
*
* @param string $file
* The file to backup.
* @param string $project_root
* The project root directory.
*
* @return bool
* Return TRUE if backup succeeds, FALSE otherwise.
*/
protected function doBackup($file, $project_root) {
$directory = $this->backup . dirname($file);
if (!file_exists($directory) && !$this->fileSystem->mkdir($directory, NULL, TRUE)) {
return FALSE;
}
$project_real_path = $this->getProjectRealPath($file, $project_root);
if (file_exists($project_real_path) && !is_dir($project_real_path)) {
try {
$this->fileSystem->copy($project_real_path, $this->backup . $file, FileSystemInterface::EXISTS_REPLACE);
}
catch (FileException $exception) {
return FALSE;
}
}
return TRUE;
}
/**
* Rollback after a failed update.
*
* @param string $project_root
* The project root directory.
*/
protected function rollback($project_root) {
if (!$this->backup) {
return;
}
foreach ($this->getFilesList($this->backup) as $file) {
$file_real_path = $this->getFileRealPath($file);
$file_path = substr($file_real_path, strlen($this->backup));
try {
$this->fileSystem->copy($file_real_path, $this->getProjectRealPath($file_path, $project_root), FileSystemInterface::EXISTS_REPLACE);
}
catch (FileException $exception) {
$this->logger->error('@file was not rolled back successfully.', ['@file' => $file_real_path]);
}
}
}
/**
* Provide a recursive list of files, excluding directories.
*
* @param string $directory
* The directory to recurse for files.
*
* @return \RecursiveIteratorIterator|\SplFileInfo[]
* The iterator of SplFileInfos.
*/
protected function getFilesList($directory) {
$filter = function ($file, $file_name, $iterator) {
/** @var \SplFileInfo $file */
/** @var string $file_name */
/** @var \RecursiveDirectoryIterator $iterator */
if ($iterator->hasChildren() && !in_array($file->getFilename(), ['.git'], TRUE)) {
return TRUE;
}
return $file->isFile() && !in_array($file->getFilename(), [self::DELETION_MANIFEST], TRUE);
};
$innerIterator = new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS);
return new \RecursiveIteratorIterator(new \RecursiveCallbackFilterIterator($innerIterator, $filter));
}
/**
* Build a project quasi-patch download URL.
*
* @param string $project_name
* The project name.
* @param string $file_name
* The file name.
*
* @return string
* The URL endpoint with for an extension.
*/
protected function buildUrl($project_name, $file_name) {
$uri = $this->configFactory->get('automatic_updates.settings')->get('download_uri');
return Url::fromUri($uri . "/$project_name/" . $file_name)->toString();
}
/**
* Get the quasi-patch file name.
*
* @param string $project_name
* The project name.
* @param string $from_version
* The current project version.
* @param string $to_version
* The desired next project version.
*
* @return string
* The quasi-patch file name.
*/
protected function getQuasiPatchFileName($project_name, $from_version, $to_version) {
return "$project_name-$from_version-to-$to_version.zip";
}
/**
* Get file real path.
*
* @param \SplFileInfo $file
* The file to retrieve the real path.
*
* @return string
* The file real path.
*/
protected function getFileRealPath(\SplFileInfo $file) {
$real_path = $file->getRealPath();
if (!$real_path) {
throw new FileException(sprintf('Could not get real path for "%s"', $file->getFilename()));
}
return $real_path;
}
/**
* Get the real path of a file.
*
* @param string $file_path
* The file path.
* @param string $project_root
* The project root directory.
*
* @return string
* The real path of a file.
*/
protected function getProjectRealPath($file_path, $project_root) {
if (substr($file_path, 0, 6) === 'vendor/') {
return $this->vendorPath . substr($file_path, 7);
}
return rtrim($project_root, '/\\') . DIRECTORY_SEPARATOR . $file_path;
}
/**
* Provides the temporary extraction directory.
*
* @return string
* The temporary directory.
*/
protected function getTempDirectory() {
if (!$this->tempDirectory) {
$this->tempDirectory = $this->fileSystem->createFilename('automatic_updates-update', 'temporary://');
$this->fileSystem->prepareDirectory($this->tempDirectory);
$this->tempDirectory = $this->fileSystem->realpath($this->tempDirectory) . DIRECTORY_SEPARATOR;
}
return $this->tempDirectory;
}
/**
* Get an iterator of files to delete.
*
* @return \ArrayIterator
* Iterator of files to delete.
*/
protected function getDeletions() {
$deletions = [];
if (!file_exists($this->getTempDirectory() . self::DELETION_MANIFEST)) {
return new \ArrayIterator($deletions);
}
$handle = fopen($this->getTempDirectory() . self::DELETION_MANIFEST, 'r');
if ($handle) {
while (($deletion = fgets($handle)) !== FALSE) {
if ($result = trim($deletion)) {
$deletions[] = $result;
}
}
fclose($handle);
}
return new \ArrayIterator($deletions);
}
}
<?php
namespace Drupal\automatic_updates\Services;
/**
* Interface UpdateInterface.
*/
interface UpdateInterface {
/**
* Update a project to the next release.
*
* @param string $project_name
* The project name.
* @param string $project_type
* The project type.
* @param string $from_version
* The current project version.
* @param string $to_version
* The desired next project version.
*
* @return bool
* TRUE if project was successfully updated, FALSE otherwise.
*/
public function update($project_name, $project_type, $from_version, $to_version);
}
<?php
namespace Drupal\test_automatic_updates\Controller;
use Drupal\automatic_updates\Services\UpdateInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for Test Automatic Updates routes.
*/
class InPlaceUpdateController extends ControllerBase {
/**
* Updater service.
*
* @var \Drupal\automatic_updates\Services\UpdateInterface
*/
protected $updater;
/**
* InPlaceUpdateController constructor.
*
* @param \Drupal\automatic_updates\Services\UpdateInterface $updater
* The updater service.
*/
public function __construct(UpdateInterface $updater) {
$this->updater = $updater;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('automatic_updates.update')
);
}
/**
* Builds the response.
*/
public function update($project, $type, $from, $to) {
$updated = $this->updater->update($project, $type, $from, $to);
return [
'#markup' => $updated ? $this->t('Update successful') : $this->t('Update Failed'),
];
}
}
<?php
namespace Drupal\test_automatic_updates\EventSubscriber;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Disable theme CSRF route subscriber.
*/
class DisableThemeCsrfRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// Disable CSRF so we can easily enable themes in tests.
if ($route = $collection->get('system.theme_set_default')) {
$route->setRequirements(['_permission' => 'administer themes']);
}
}
}
......@@ -3,3 +3,5 @@ type: module
description: 'Tests for Automatic Updates'
package: Testing
core: 8.x
dependencies:
- automatic_updates:automatic_updates
......@@ -19,3 +19,12 @@ test_automatic_updates.hashes_endpoint:
_title: 'SHA512SUMS'
requirements:
_access: 'TRUE'
test_automatic_updates.inplace-update:
path: '/automatic_updates/in-place-update/{project}/{type}/{from}/{to}'
defaults:
_title: 'Update'
_controller: '\Drupal\test_automatic_updates\Controller\InPlaceUpdateController::update'
requirements:
_access: 'TRUE'
options:
no_cache: 'TRUE'
services:
test_automatic_updates.route_subscriber:
class: Drupal\test_automatic_updates\EventSubscriber\DisableThemeCsrfRouteSubscriber
tags:
- { name: event_subscriber }
<?php
namespace Drupal\Tests\automatic_updates\Build;
use Drupal\automatic_updates\Services\InPlaceUpdate;
use Drupal\Component\FileSystem\FileSystem as DrupalFilesystem;
use Drupal\Component\Utility\Html;
use Drupal\Tests\automatic_updates\Build\QuickStart\QuickStartTestBase;
use GuzzleHttp\Client;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
use Symfony\Component\Finder\Finder;
/**
* @coversDefaultClass \Drupal\automatic_updates\Services\InPlaceUpdate
*
* @group Build
* @group Update
*
* @requires externalCommand composer
* @requires externalCommand curl
* @requires externalCommand git
* @requires externalCommand tar
*/
class InPlaceUpdateTest extends QuickStartTestBase {
/**
* The files which are candidates for deletion during an upgrade.
*
* @var string[]
*/
protected $deletions;
/**
* The directory where the deletion manifest is extracted.
*
* @var string
*/
protected $deletionsDestination;
/**
* {@inheritdoc}
*/
protected function tearDown() {
parent::tearDown();
$fs = new SymfonyFilesystem();
$fs->remove($this->deletionsDestination);
}
/**
* @covers ::update
* @dataProvider coreVersionsProvider
*/
public function testCoreUpdate($from_version, $to_version) {
$this->copyCodebase();
// We have to fetch the tags for this shallow repo. It might not be a
// shallow clone, therefore we use executeCommand instead of assertCommand.
$this->executeCommand('git fetch --unshallow --tags');
$this->executeCommand("git checkout $from_version -f");
$this->assertCommandSuccessful();
$fs = new SymfonyFilesystem();
$fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer require ocramius/package-versions:^1.4 webflo/drupal-finder:^1.1 composer/semver:^1.0 --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
$this->installQuickStart('minimal');
// Currently, this test has to use extension_discovery_scan_tests so we can
// enable test modules.
$fs->chmod($this->getWorkspaceDirectory() . '/sites/default/settings.php', 0640, 0000);
file_put_contents($this->getWorkspaceDirectory() . '/sites/default/settings.php', '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL, FILE_APPEND);
// Log in so that we can install modules.
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->moduleEnable('automatic_updates');
$this->moduleEnable('test_automatic_updates');
// Confirm we are running correct Drupal version.
$finder = new Finder();
$finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
$finder->contains("/const VERSION = '$from_version'/");
$this->assertTrue($finder->hasResults());
// Assert files slated for deletion still exist.
foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
$this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
// Assert that the site is functional before updating.
$this->visit();
$this->assertDrupalVisit();
// Update the site.
$this->visit("/automatic_updates/in-place-update/drupal/core/$from_version/$to_version");
$this->assertDrupalVisit();
// Assert that the update worked.
$assert = $this->getMink()->assertSession();
$assert->pageTextContains('Update successful');
$finder = new Finder();
$finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
$finder->contains("/const VERSION = '$to_version'/");
$this->assertTrue($finder->hasResults());
$this->visit('/admin/reports/status');
$assert->pageTextContains("Drupal Version $to_version");
// Assert files slated for deletion are now gone.
foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
$this->assertFileNotExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
}
/**
* @covers ::update
* @dataProvider contribProjectsProvider
*/
public function testContribUpdate($project, $project_type, $from_version, $to_version) {
$this->copyCodebase();
$fs = new SymfonyFilesystem();
$fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
$this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer require ocramius/package-versions:^1.4 webflo/drupal-finder:^1.1 composer/semver:^1.0 --no-interaction');
$this->assertErrorOutputContains('Generating autoload files');
$this->installQuickStart('standard');
// Download the project.
$fs->mkdir($this->getWorkspaceDirectory() . "/{$project_type}s/contrib/$project");
$this->executeCommand("curl -fsSL https://ftp.drupal.org/files/projects/$project-$from_version.tar.gz | tar xvz -C {$project_type}s/contrib/$project --strip 1");
$this->assertCommandSuccessful();
$finder = new Finder();
$finder->files()->in($this->getWorkspaceDirectory())->path("{$project_type}s/contrib/$project/$project.info.yml");
$finder->contains("/version: '$from_version'/");
$this->assertTrue($finder->hasResults());
// Assert files slated for deletion still exist.
foreach ($this->getDeletions($project, $from_version, $to_version) as $deletion) {
$this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
// Currently, this test has to use extension_discovery_scan_tests so we can
// enable test modules.
$fs->chmod($this->getWorkspaceDirectory() . '/sites/default/settings.php', 0640, 0000);
file_put_contents($this->getWorkspaceDirectory() . '/sites/default/settings.php', '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL, FILE_APPEND);
// Log in so that we can install projects.
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->moduleEnable('automatic_updates');
$this->moduleEnable('test_automatic_updates');
if (is_callable([$this, "{$project_type}Enable"])) {
call_user_func([$this, "{$project_type}Enable"], $project);
}
// Assert that the site is functional before updating.
$this->visit();
$this->assertDrupalVisit();
// Update the contrib project.
$this->visit("/automatic_updates/in-place-update/$project/$project_type/$from_version/$to_version");
$this->assertDrupalVisit();
// Assert that the update worked.
$assert = $this->getMink()->assertSession();
$assert->pageTextContains('Update successful');
$finder = new Finder();
$finder->files()->in($this->getWorkspaceDirectory())->path("{$project_type}s/contrib/$project/$project.info.yml");
$finder->contains("/version: '$to_version'/");
$this->assertTrue($finder->hasResults());
$this->assertDrupalVisit();
// Assert files slated for deletion are now gone.
foreach ($this->getDeletions($project, $from_version, $to_version) as $deletion) {
$this->assertFileNotExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
}
/**
* Core versions data provider.
*/
public function coreVersionsProvider() {
$datum[] = [
'from' => '8.7.0',
'to' => '8.7.1',
];
$datum[] = [
'from' => '8.7.1',
'to' => '8.7.2',
];
$datum[] = [
'from' => '8.7.2',
'to' => '8.7.3',
];
$datum[] = [
'from' => '8.7.3',
'to' => '8.7.4',
];
$datum[] = [
'from' => '8.7.4',
'to' => '8.7.5',
];
$datum[] = [
'from' => '8.7.5',
'to' => '8.7.6',
];
$datum[] = [
'from' => '8.7.6',
'to' => '8.7.7',
];
return $datum;
}
/**
* Contrib project data provider.
*/
public function contribProjectsProvider() {
$datum[] = [
'project' => 'bootstrap',
'type' => 'theme',
'from' => '8.x-3.19',
'to' => '8.x-3.20',
];
$datum[] = [
'project' => 'token',
'type' => 'module',
'from' => '8.x-1.4',
'to' => '8.x-1.5',
];
return $datum;
}
/**
* Helper method to retrieve files slated for deletion.
*/
protected function getDeletions($project, $from_version, $to_version) {
if (isset($this->deletions)) {
return $this->deletions;
}
$this->deletions = [];
$http_client = new Client();
$filesystem = new SymfonyFilesystem();
$this->deletionsDestination = DrupalFileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . "$project-" . rand(10000, 99999) . microtime(TRUE);
$filesystem->mkdir($this->deletionsDestination);
$file_name = "$project-$from_version-to-$to_version.zip";
$zip_file = $this->deletionsDestination . DIRECTORY_SEPARATOR . $file_name;
$http_client->get("https://www.drupal.org/in-place-updates/$project/$file_name", ['sink' => $zip_file]);
$zip = new \ZipArchive();
$zip->open($zip_file);
$zip->extractTo($this->deletionsDestination, [InPlaceUpdate::DELETION_MANIFEST]);
$handle = fopen($this->deletionsDestination . DIRECTORY_SEPARATOR . InPlaceUpdate::DELETION_MANIFEST, 'r');
if ($handle) {
while (($deletion = fgets($handle)) !== FALSE) {
if ($result = trim($deletion)) {
$this->deletions[] = $result;
}
}
fclose($handle);
}
return $this->deletions;
}
/**
* Helper method that uses Drupal's module page to enable a module.
*/
protected function moduleEnable($module_name) {
$this->visit('/admin/modules');
$field = Html::getClass("edit-modules $module_name enable");
// No need to enable a module if it is already enabled.
if ($this->getMink()->getSession()->getPage()->findField($field)->isChecked()) {
return;
}
$assert = $this->getMink()->assertSession();
$assert->fieldExists($field)->check();
$session = $this->getMink()->getSession();
$session->getPage()->findButton('Install')->submit();
$assert->fieldExists($field)->isChecked();
}
/**
* Helper method that uses Drupal's theme page to enable a theme.
*/
protected function themeEnable($theme_name) {
$this->moduleEnable('test_automatic_updates');
$this->visit("/admin/appearance/default?theme=$theme_name");
$assert = $this->getMink()->assertSession();
$assert->pageTextNotContains('theme was not found');
}
}
<?php
namespace Drupal\Tests\automatic_updates\Build\QuickStart;
use Drupal\BuildTests\Framework\BuildTestBase;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* Helper methods for using the quickstart feature of Drupal.
*
* @TODO: remove after https://www.drupal.org/project/drupal/issues/3082230.
*/
abstract class QuickStartTestBase extends BuildTestBase {
/**
* User name of the admin account generated during install.
*
* @var string
*/
protected $adminUsername;
/**
* Password of the admin account generated during install.
*
* @var string
*/
protected $adminPassword;
/**
* Install a Drupal site using the quick start feature.
*
* @param string $profile
* Drupal profile to install.
* @param string $working_dir
* (optional) A working directory relative to the workspace, within which to
* execute the command. Defaults to the workspace directory.
*/
public function installQuickStart($profile, $working_dir = NULL) {
$finder = new PhpExecutableFinder();
$process = $this->executeCommand($finder->find() . ' ./core/scripts/drupal install ' . $profile, $working_dir);
$this->assertCommandSuccessful();
$this->assertCommandOutputContains('Username:');
preg_match('/Username: (.+)\vPassword: (.+)/', $process->getOutput(), $matches);
$this->assertNotEmpty($this->adminUsername = $matches[1]);
$this->assertNotEmpty($this->adminPassword = $matches[2]);
}
/**
* Helper that uses Drupal's user/login form to log in.
*
* @param string $username
* Username.
* @param string $password
* Password.
* @param string $working_dir
* (optional) A working directory within which to login. Defaults to the
* workspace directory.
*/
public function formLogin($username, $password, $working_dir = NULL) {
$mink = $this->visit('/user/login', $working_dir);
$this->assertEquals(200, $mink->getSession()->getStatusCode());
$assert = $mink->assertSession();
$assert->fieldExists('edit-name')->setValue($username);
$assert->fieldExists('edit-pass')->setValue($password);
$mink->getSession()->getPage()->findButton('Log in')->submit();
}
}
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