class InPlaceUpdateTest in Automatic Updates 8
@coversDefaultClass \Drupal\automatic_updates\Services\InPlaceUpdate
@group Build @group Update
@requires externalCommand composer @requires externalCommand curl @requires externalCommand git @requires externalCommand tar
Hierarchy
- class \Drupal\BuildTests\Framework\BuildTestBase extends \PHPUnit\Framework\TestCase uses ExternalCommandRequirementsTrait, PhpunitCompatibilityTrait
- class \Drupal\Tests\automatic_updates\Build\QuickStart\QuickStartTestBase
- class \Drupal\Tests\automatic_updates\Build\InPlaceUpdateTest uses InstallTestTrait
- class \Drupal\Tests\automatic_updates\Build\QuickStart\QuickStartTestBase
Expanded class hierarchy of InPlaceUpdateTest
File
- tests/
src/ Build/ InPlaceUpdateTest.php, line 25
Namespace
Drupal\Tests\automatic_updates\BuildView source
class InPlaceUpdateTest extends QuickStartTestBase {
use InstallTestTrait;
/**
* 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 coreVersionsSuccessProvider
*/
public function testCoreUpdate($from_version, $to_version) {
$this
->installCore($from_version);
$this
->assertCoreUpgradeSuccess($from_version, $to_version);
}
/**
* @covers ::update
*/
public function testCoreRollbackUpdate() {
$from_version = '8.7.0';
$to_version = '8.8.5';
$this
->installCore($from_version);
// Configure module to have db updates cause a rollback.
$settings_php = $this
->getWorkspaceDirectory() . '/sites/default/settings.php';
$fs = new SymfonyFilesystem();
$fs
->chmod($this
->getWorkspaceDirectory() . '/sites/default', 0755);
$fs
->chmod($settings_php, 0640);
$fs
->appendToFile($settings_php, PHP_EOL . '$config[\'automatic_updates.settings\'][\'database_update_handling\'] = [\'rollback\'];' . PHP_EOL);
$this
->assertCoreUpgradeFailed($from_version, $to_version);
}
/**
* @covers ::update
* @dataProvider contribProjectsProvider
*/
public function testContribUpdate($project, $project_type, $from_version, $to_version) {
$this
->markTestSkipped('Contrib updates are not currently supported');
$this
->copyCodebase();
$fs = new SymfonyFilesystem();
$fs
->chmod($this
->getWorkspaceDirectory() . '/sites/default', 0700);
$this
->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --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(), "Expected version {$from_version} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
// 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
// install test modules.
$fs = new SymfonyFilesystem();
$settings_php = $this
->getWorkspaceDirectory() . '/sites/default/settings.php';
$fs
->chmod($settings_php, 0640);
$fs
->appendToFile($settings_php, '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL);
// Log in so that we can install projects.
$this
->formLogin($this->adminUsername, $this->adminPassword);
$this
->moduleInstall('update');
$this
->moduleInstall('automatic_updates');
$this
->moduleInstall('test_automatic_updates');
$this
->{"{$project_type}Install"}($project);
// Assert that the site is functional before updating.
$this
->visit();
$this
->assertDrupalVisit();
// Update the contrib project.
$assert = $this
->visit("/test_automatic_updates/in-place-update/{$project}/{$project_type}/{$from_version}/{$to_version}")
->assertSession();
$assert
->statusCodeEquals(200);
$this
->assertDrupalVisit();
// Assert that the update worked.
$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(), "Expected version {$to_version} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
$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);
}
}
/**
* Test in-place update via cron run.
*
* @covers ::update
* @see automatic_updates_cron()
*/
public function testCronCoreUpdate() {
$this
->installCore('8.8.0');
$filesystem = new SymfonyFilesystem();
$filesystem
->chmod($this
->getWorkspaceDirectory() . '/sites/default', 0750);
$settings_php = $this
->getWorkspaceDirectory() . '/sites/default/settings.php';
$filesystem
->chmod($settings_php, 0640);
$filesystem
->appendToFile($settings_php, PHP_EOL . '$config[\'automatic_updates.settings\'][\'enable_cron_updates\'] = TRUE;' . PHP_EOL);
$mink = $this
->visit('/admin/config/system/cron');
$mink
->getSession()
->getPage()
->findButton('Run cron')
->submit();
$mink
->assertSession()
->pageTextContains('Cron ran successfully.');
// Assert that the update worked.
$this
->assertDrupalVisit();
$finder = new Finder();
$finder
->files()
->in($this
->getWorkspaceDirectory())
->path('core/lib/Drupal.php');
$finder
->notContains("/const VERSION = '8.8.0'/");
$finder
->contains("/const VERSION = '8.8./");
$this
->assertTrue($finder
->hasResults(), "Expected version 8.8.{x} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
}
/**
* Core versions data provider resulting in a successful upgrade.
*/
public function coreVersionsSuccessProvider() {
$datum[] = [
'from' => '8.7.2',
'to' => '8.7.4',
];
$datum[] = [
'from' => '8.7.0',
'to' => '8.7.1',
];
$datum[] = [
'from' => '8.7.2',
'to' => '8.7.10',
];
$datum[] = [
'from' => '8.7.6',
'to' => '8.7.7',
];
$datum[] = [
'from' => '8.9.0-beta1',
'to' => '8.9.0-beta2',
];
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 = [];
$filesystem = new SymfonyFilesystem();
$this->deletionsDestination = DrupalFileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . "{$project}-" . mt_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;
$this
->doGetArchive($project, $file_name, $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;
}
/**
* Get the archive with protection against 429s.
*
* @param string $project
* The project.
* @param string $file_name
* The filename.
* @param string $zip_file
* The zip file path.
* @param int $delay
* (optional) The delay.
*/
protected function doGetArchive($project, $file_name, $zip_file, $delay = 0) {
try {
sleep($delay);
$http_client = new Client();
$http_client
->get("https://www.drupal.org/in-place-updates/{$project}/{$file_name}", [
'sink' => $zip_file,
]);
} catch (RequestException $exception) {
$response = $exception
->getResponse();
if ($response && $response
->getStatusCode() === 429) {
$this
->doGetArchive($project, $file_name, $zip_file, 10);
}
else {
throw $exception;
}
}
}
/**
* Assert an upgrade succeeded.
*
* @param string $from_version
* The version from which to upgrade.
* @param string $to_version
* The version to which to upgrade.
*
* @throws \Behat\Mink\Exception\ExpectationException
* @throws \Behat\Mink\Exception\ResponseTextException
*/
public function assertCoreUpgradeSuccess($from_version, $to_version) {
// Assert files slated for deletion still exist.
foreach ($this
->getDeletions('drupal', $from_version, $to_version) as $deletion) {
$this
->assertFileExists($this
->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
// Update the site.
$assert = $this
->visit("/test_automatic_updates/in-place-update/drupal/core/{$from_version}/{$to_version}")
->assertSession();
$assert
->statusCodeEquals(200);
$this
->assertDrupalVisit();
// Assert that the update worked.
$finder = new Finder();
$finder
->files()
->in($this
->getWorkspaceDirectory())
->path('core/lib/Drupal.php');
$finder
->contains("/const VERSION = '{$to_version}'/");
$this
->assertTrue($finder
->hasResults(), "Expected version {$to_version} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
$assert
->pageTextContains('Update successful');
$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);
}
// Validate that all DB updates are processed.
$this
->visit('/update.php/selection');
$assert
->pageTextContains('No pending updates.');
}
/**
* Assert an upgraded failed and was handle appropriately.
*
* @param string $from_version
* The version from which to upgrade.
* @param string $to_version
* The version to which to upgrade.
*
* @throws \Behat\Mink\Exception\ResponseTextException
*/
public function assertCoreUpgradeFailed($from_version, $to_version) {
// Assert files slated for deletion still exist.
foreach ($this
->getDeletions('drupal', $from_version, $to_version) as $deletion) {
$this
->assertFileExists($this
->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
// Update the site.
$assert = $this
->visit("/test_automatic_updates/in-place-update/drupal/core/{$from_version}/{$to_version}")
->assertSession();
$assert
->statusCodeEquals(200);
// Assert that the update failed.
$finder = new Finder();
$finder
->files()
->in($this
->getWorkspaceDirectory())
->path('core/lib/Drupal.php');
$finder
->contains("/const VERSION = '{$from_version}'/");
$this
->assertTrue($finder
->hasResults(), "Expected version {$from_version} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
$assert
->pageTextContains('Update Failed');
$this
->visit('/admin/reports/status');
$assert
->pageTextContains("Drupal Version {$from_version}");
// Assert files slated for deletion are restored.
foreach ($this
->getDeletions('drupal', $from_version, $to_version) as $deletion) {
$this
->assertFileExists($this
->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
BuildTestBase:: |
private | property | The most recent command process. | |
BuildTestBase:: |
protected | property | Default to destroying build artifacts after a test finishes. | |
BuildTestBase:: |
private static | property | Our native host name, used by PHP when it starts up the server. | |
BuildTestBase:: |
private | property | Port that will be tested. | |
BuildTestBase:: |
private | property | The Mink session manager. | |
BuildTestBase:: |
private | property | A list of ports used by the test. | |
BuildTestBase:: |
private | property | The docroot for the server process. | |
BuildTestBase:: |
private | property | The process that's running the HTTP server. | |
BuildTestBase:: |
private | property | The working directory where this test will manipulate files. | |
BuildTestBase:: |
public | function | Asserts that the last command returned the specified exit code. | |
BuildTestBase:: |
public | function | Assert that text is present in the output of the most recent command. | |
BuildTestBase:: |
public | function | Asserts that the last command ran without error. | |
BuildTestBase:: |
public | function | Helper function to assert that the last visit was a Drupal site. | |
BuildTestBase:: |
public | function | Assert that text is present in the error output of the most recent command. | |
BuildTestBase:: |
protected | function | Checks whether a port is available. | |
BuildTestBase:: |
public | function | Copy the current working codebase into a workspace. | |
BuildTestBase:: |
public | function | Run a command. | |
BuildTestBase:: |
protected | function | Discover an available port number. | |
BuildTestBase:: |
public | function | Get a default Finder object for a Drupal codebase. | |
BuildTestBase:: |
protected | function | Get the root path of this Drupal codebase. | |
BuildTestBase:: |
public | function | Get the Mink instance. | |
BuildTestBase:: |
protected | function | Get the port number for requests. | |
BuildTestBase:: |
protected | function | Get the working directory within the workspace, creating if necessary. | |
BuildTestBase:: |
public | function | Full path to the workspace where this test can build. | |
BuildTestBase:: |
protected | function | Set up the Mink session manager. | |
BuildTestBase:: |
protected | function | Do the work of making a server process. | |
BuildTestBase:: |
protected | function | ||
BuildTestBase:: |
public static | function | ||
BuildTestBase:: |
protected | function | Makes a local test server using PHP's internal HTTP server. | |
BuildTestBase:: |
protected | function | Stop the HTTP server, zero out all necessary variables. | |
BuildTestBase:: |
public | function | Visit a URI on the HTTP server. | |
ExternalCommandRequirementsTrait:: |
private static | property | A list of existing external commands we've already discovered. | |
ExternalCommandRequirementsTrait:: |
private static | function | Checks whether required external commands are available per test class. | |
ExternalCommandRequirementsTrait:: |
private static | function | Checks missing external command requirements. | |
ExternalCommandRequirementsTrait:: |
private static | function | Checks whether required external commands are available per method. | |
ExternalCommandRequirementsTrait:: |
private static | function | Determine if an external command is available. | 3 |
InPlaceUpdateTest:: |
protected | property | The files which are candidates for deletion during an upgrade. | |
InPlaceUpdateTest:: |
protected | property | The directory where the deletion manifest is extracted. | |
InPlaceUpdateTest:: |
public | function | Assert an upgraded failed and was handle appropriately. | |
InPlaceUpdateTest:: |
public | function | Assert an upgrade succeeded. | |
InPlaceUpdateTest:: |
public | function | Contrib project data provider. | |
InPlaceUpdateTest:: |
public | function | Core versions data provider resulting in a successful upgrade. | |
InPlaceUpdateTest:: |
protected | function | Get the archive with protection against 429s. | |
InPlaceUpdateTest:: |
protected | function | Helper method to retrieve files slated for deletion. | |
InPlaceUpdateTest:: |
protected | function |
Overrides BuildTestBase:: |
|
InPlaceUpdateTest:: |
public | function | @covers ::update @dataProvider contribProjectsProvider | |
InPlaceUpdateTest:: |
public | function | @covers ::update | |
InPlaceUpdateTest:: |
public | function | @covers ::update @dataProvider coreVersionsSuccessProvider | |
InPlaceUpdateTest:: |
public | function | Test in-place update via cron run. | |
InstallTestTrait:: |
protected | function | Helper method that uses Drupal's module page to install a module. | |
InstallTestTrait:: |
protected | function | Helper method that uses Drupal's theme page to install a theme. | |
PhpunitCompatibilityTrait:: |
public | function | Returns a mock object for the specified class using the available method. | |
PhpunitCompatibilityTrait:: |
public | function | Compatibility layer for PHPUnit 6 to support PHPUnit 4 code. | |
QuickStartTestBase:: |
protected | property | Password of the admin account generated during install. | |
QuickStartTestBase:: |
protected | property | User name of the admin account generated during install. | |
QuickStartTestBase:: |
public | function | Helper that uses Drupal's user/login form to log in. | |
QuickStartTestBase:: |
protected | function | Prepare core for testing. | |
QuickStartTestBase:: |
public | function | Install a Drupal site using the quick start feature. |