Skip to content
Snippets Groups Projects
Commit 1443df5a authored by Ted Bowman's avatar Ted Bowman Committed by Ted Bowman
Browse files

Contrib: Issue #3259656 by phenaproxima: Require Composer 2.2.4 or later -...

Contrib: Issue #3259656 by phenaproxima: Require Composer 2.2.4 or later - project/automatic_updates@2d56faea
parent 54d3a287
No related branches found
No related tags found
No related merge requests found
Showing
with 275 additions and 88 deletions
......@@ -3,11 +3,12 @@
namespace Drupal\Tests\auto_updates\Build;
use Drupal\Component\Utility\Html;
use Drupal\Tests\package_manager\Build\TemplateProjectTestBase;
/**
* Base class for tests that perform in-place updates.
*/
abstract class UpdateTestBase extends TemplateProjectSiteTestBase {
abstract class UpdateTestBase extends TemplateProjectTestBase {
/**
* A secondary server instance, to serve XML metadata about available updates.
......@@ -32,16 +33,8 @@ protected function tearDown(): void {
protected function createTestProject(string $template): void {
parent::createTestProject($template);
// Install Drupal. Always allow test modules to be installed in the UI and,
// for easier debugging, always display errors in their dubious glory.
// Install Drupal, Automatic Updates, and other modules needed for testing.
$this->installQuickStart('minimal');
$php = <<<END
\$settings['extension_discovery_scan_tests'] = TRUE;
\$config['system.logging']['error_level'] = 'verbose';
END;
$this->writeSettings($php);
// Install Automatic Updates and other modules needed for testing.
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->installModules([
'auto_updates',
......@@ -50,27 +43,6 @@ protected function createTestProject(string $template): void {
]);
}
/**
* Appends PHP code to the test site's settings.php.
*
* @param string $php
* The PHP code to append to the test site's settings.php.
*/
protected function writeSettings(string $php): void {
// Ensure settings are writable, since this is the only way we can set
// configuration values that aren't accessible in the UI.
$file = $this->getWebRoot() . '/sites/default/settings.php';
$this->assertFileExists($file);
chmod(dirname($file), 0744);
chmod($file, 0744);
$this->assertFileIsWritable($file);
$stream = fopen($file, 'a');
$this->assertIsResource($stream);
$this->assertIsInt(fwrite($stream, $php));
$this->assertTrue(fclose($stream));
}
/**
* Prepares the test site to serve an XML feed of available release metadata.
*
......@@ -99,33 +71,6 @@ protected function setReleaseMetadata(array $xml_map): void {
$this->writeSettings($code);
}
/**
* Installs modules in the UI.
*
* Assumes that a user with the appropriate permissions is logged in.
*
* @param string[] $modules
* The machine names of the modules to install.
*/
protected function installModules(array $modules): void {
$mink = $this->getMink();
$page = $mink->getSession()->getPage();
$assert_session = $mink->assertSession();
$this->visit('/admin/modules');
foreach ($modules as $module) {
$page->checkField("modules[$module][enable]");
}
$page->pressButton('Install');
$form_id = $assert_session->elementExists('css', 'input[type="hidden"][name="form_id"]')
->getValue();
if (preg_match('/^system_modules_(experimental_|non_stable_)?confirm_form$/', $form_id)) {
$page->pressButton('Continue');
$assert_session->statusCodeEquals(200);
}
}
/**
* Checks for available updates.
*
......
......@@ -86,7 +86,9 @@ public function testCorrectVersionsStaged() {
// invocation recorder, rather than a regular mock, in order to test that
// the invocation recorder itself works.
// The production requirements are changed first, followed by the dev
// requirements. Then the installed packages are updated.
// requirements. Then the installed packages are updated. This is tested
// functionally in Package Manager.
// @see \Drupal\Tests\package_manager\Build\StagedUpdateTest
$expected_arguments = [
[
'require',
......
......@@ -87,7 +87,7 @@ public function create(array $command): Process {
*/
private function getComposerHomePath(): string {
$home_path = $this->fileSystem->getTempDirectory();
$home_path .= '/auto_updates_composer_home-';
$home_path .= '/package_manager_composer_home-';
$home_path .= $this->configFactory->get('system.site')->get('uuid');
$this->fileSystem->prepareDirectory($home_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
......
......@@ -2,9 +2,9 @@
namespace Drupal\package_manager\Validator;
use Composer\Semver\Comparator;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use PhpTuf\ComposerStager\Domain\Process\OutputCallbackInterface;
......@@ -18,6 +18,13 @@ class ComposerExecutableValidator implements PreOperationStageValidatorInterface
use StringTranslationTrait;
/**
* The minimum required version of Composer.
*
* @var string
*/
public const MINIMUM_COMPOSER_VERSION = '2.2.4';
/**
* The Composer runner.
*
......@@ -60,13 +67,11 @@ public function validateStagePreOperation(PreOperationStageEvent $event): void {
}
if ($this->version) {
$major_version = ExtensionVersion::createFromVersionString($this->version)
->getMajorVersion();
if ($major_version < 2) {
if (Comparator::lessThan($this->version, static::MINIMUM_COMPOSER_VERSION)) {
$event->addError([
$this->t('Composer 2 or later is required, but version @version was detected.', [
'@version' => $this->version,
$this->t('Composer @minimum_version or later is required, but version @detected_version was detected.', [
'@minimum_version' => static::MINIMUM_COMPOSER_VERSION,
'@detected_version' => $this->version,
]),
]);
}
......
name: 'Package Manager Test API'
description: 'Provides API endpoints for doing stage operations in functional tests.'
type: module
package: Testing
dependencies:
- auto_updates:package_manager
package_manager_test_api.require:
path: '/package-manager-test-api/require'
defaults:
_controller: 'Drupal\package_manager_test_api\ApiController::require'
requirements:
_access: 'TRUE'
<?php
namespace Drupal\package_manager_test_api;
use Drupal\Core\Controller\ControllerBase;
use Drupal\package_manager\Stage;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides API endpoints to interact with a staging area in functional tests.
*/
class ApiController extends ControllerBase {
/**
* The stage.
*
* @var \Drupal\package_manager\Stage
*/
private $stage;
/**
* Constructs an ApiController object.
*
* @param \Drupal\package_manager\Stage $stage
* The stage.
*/
public function __construct(Stage $stage) {
$this->stage = $stage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$stage = new Stage(
$container->get('package_manager.path_locator'),
$container->get('package_manager.beginner'),
$container->get('package_manager.stager'),
$container->get('package_manager.committer'),
$container->get('file_system'),
$container->get('event_dispatcher'),
$container->get('tempstore.shared'),
);
return new static($stage);
}
/**
* Creates a staging area and requires packages into it.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request. The runtime and dev dependencies are expected to be in
* either the query string or request body, under the 'runtime' and 'dev'
* keys, respectively. There may also be a 'files_to_return' key, which
* contains an array of file paths, relative to the stage directory, whose
* contents should be returned in the response.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing an associative array of the contents of the
* staged files listed in the 'files_to_return' request key. The array will
* be keyed by path, relative to the stage directory.
*/
public function require(Request $request): JsonResponse {
$this->stage->create();
$this->stage->require(
$request->get('runtime', []),
$request->get('dev', [])
);
$stage_dir = $this->stage->getStageDirectory();
$staged_file_contents = [];
foreach ($request->get('files_to_return', []) as $path) {
$staged_file_contents[$path] = file_get_contents($stage_dir . '/' . $path);
}
$this->stage->destroy();
return new JsonResponse($staged_file_contents);
}
}
<?php
namespace Drupal\Tests\package_manager\Build;
/**
* Tests updating packages in a staging area.
*
* @group package_manager
*/
class StagedUpdateTest extends TemplateProjectTestBase {
/**
* Tests that a stage only updates packages with changed constraints.
*/
public function testStagedUpdate(): void {
$this->createTestProject('RecommendedProject');
$this->createModule('alpha');
$this->createModule('bravo');
$this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha drupal/bravo --update-with-all-dependencies', 'project');
$this->installQuickStart('minimal');
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->installModules(['package_manager_test_api']);
// Change both modules' upstream version.
$this->runComposer('composer config version 1.1.0', 'alpha');
$this->runComposer('composer config version 1.1.0', 'bravo');
// Use the API endpoint to create a stage and update bravo to 1.1.0. Even
// though both modules are at version 1.1.0, only bravo should be updated.
// We ask the API to return the contents of both modules' staged
// composer.json files, so we can assert that the staged versions are what
// we expect.
// @see \Drupal\package_manager_test_api\ApiController::require()
$query = http_build_query([
'runtime' => [
'drupal/bravo:1.1.0',
],
'files_to_return' => [
'web/modules/contrib/alpha/composer.json',
'web/modules/contrib/bravo/composer.json',
],
]);
$this->visit("/package-manager-test-api/require?$query");
$mink = $this->getMink();
$mink->assertSession()->statusCodeEquals(200);
$staged_file_contents = $mink->getSession()->getPage()->getContent();
$staged_file_contents = json_decode($staged_file_contents, TRUE);
$expected_versions = [
'alpha' => '1.0.0',
'bravo' => '1.1.0',
];
foreach ($expected_versions as $module_name => $expected_version) {
$path = "web/modules/contrib/$module_name/composer.json";
$staged_composer_json = json_decode($staged_file_contents[$path]);
$this->assertSame($expected_version, $staged_composer_json->version);
}
}
/**
* Creates an empty module for testing purposes.
*
* @param string $name
* The machine name of the module, which can be added to the test site as
* 'drupal/$name'.
*/
private function createModule(string $name): void {
$dir = $this->getWorkspaceDirectory() . '/' . $name;
mkdir($dir);
$this->assertDirectoryExists($dir);
$this->runComposer("composer init --name drupal/$name --type drupal-module", $name);
$this->runComposer('composer config version 1.0.0', $name);
$repository = json_encode([
'type' => 'path',
'url' => $dir,
'options' => [
'symlink' => FALSE,
],
]);
$this->runComposer("composer config repo.$name '$repository'", 'project');
}
}
<?php
namespace Drupal\Tests\auto_updates\Build;
namespace Drupal\Tests\package_manager\Build;
use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\Composer\Composer;
/**
* Base class for tests which create a test site from a core project template.
*
* The test site will be created from one of the core Composer project templates
* (drupal/recommended-project or drupal/legacy-project) and contain complete
* copies of Drupal core and all installed dependencies, completely independent
* of the currently running code base.
*/
abstract class TemplateProjectSiteTestBase extends QuickStartTestBase {
abstract class TemplateProjectTestBase extends QuickStartTestBase {
/**
* The web root of the test site, relative to the workspace directory.
......@@ -30,20 +35,6 @@ public function providerTemplate(): array {
];
}
/**
* {@inheritdoc}
*/
public function copyCodebase(\Iterator $iterator = NULL, $working_dir = NULL) {
parent::copyCodebase($iterator, $working_dir);
// In certain situations, like Drupal CI, auto_updates might be
// required into the code base by Composer. This may cause it to be added to
// the drupal/core-recommended metapackage, which can prevent the test site
// from being built correctly, among other deleterious effects. To prevent
// such shenanigans, always remove drupal/auto_updates from
// drupal/core-recommended.
$this->runComposer('composer remove --no-update drupal/auto_updates', 'composer/Metapackage/CoreRecommended');
}
/**
* {@inheritdoc}
......@@ -77,6 +68,14 @@ protected function instantiateServer($port, $working_dir = NULL) {
*/
public function installQuickStart($profile, $working_dir = NULL) {
parent::installQuickStart($profile, $working_dir ?: $this->webRoot);
// Always allow test modules to be installed in the UI and, for easier
// debugging, always display errors in their dubious glory.
$php = <<<END
\$settings['extension_discovery_scan_tests'] = TRUE;
\$config['system.logging']['error_level'] = 'verbose';
END;
$this->writeSettings($php);
}
/**
......@@ -197,6 +196,7 @@ protected function createTestProject(string $template): void {
// Now that we know the project was created successfully, we can set the
// web root with confidence.
$this->webRoot = 'project/' . $this->runComposer('composer config extra.drupal-scaffold.locations.web-root', 'project');
}
/**
......@@ -292,4 +292,54 @@ protected function runComposer(string $command, string $working_dir = NULL, bool
return $output;
}
/**
* Appends PHP code to the test site's settings.php.
*
* @param string $php
* The PHP code to append to the test site's settings.php.
*/
protected function writeSettings(string $php): void {
// Ensure settings are writable, since this is the only way we can set
// configuration values that aren't accessible in the UI.
$file = $this->getWebRoot() . '/sites/default/settings.php';
$this->assertFileExists($file);
chmod(dirname($file), 0744);
chmod($file, 0744);
$this->assertFileIsWritable($file);
$stream = fopen($file, 'a');
$this->assertIsResource($stream);
$this->assertIsInt(fwrite($stream, $php));
$this->assertTrue(fclose($stream));
}
/**
* Installs modules in the UI.
*
* Assumes that a user with the appropriate permissions is logged in.
*
* @param string[] $modules
* The machine names of the modules to install.
*/
protected function installModules(array $modules): void {
$mink = $this->getMink();
$page = $mink->getSession()->getPage();
$assert_session = $mink->assertSession();
$this->visit('/admin/modules');
foreach ($modules as $module) {
$page->checkField("modules[$module][enable]");
}
$page->pressButton('Install');
// If there is a confirmation form warning about additional dependencies
// or non-stable modules, submit it.
$form_id = $assert_session->elementExists('css', 'input[type="hidden"][name="form_id"]')
->getValue();
if (preg_match('/^system_modules_(experimental_|non_stable_)?confirm_form$/', $form_id)) {
$page->pressButton('Continue');
$assert_session->statusCodeEquals(200);
}
}
}
......@@ -52,17 +52,22 @@ public function providerComposerVersionValidation(): array {
// in the validation result, so we need a function to churn out those fake
// results for the test method.
$unsupported_version = function (string $version): ValidationResult {
$minimum_version = ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION;
return ValidationResult::createError([
"Composer 2 or later is required, but version $version was detected.",
"Composer $minimum_version or later is required, but version $version was detected.",
]);
};
return [
// A valid 2.x version of Composer should not produce any errors.
[
'2.1.6',
ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION,
[],
],
[
'2.1.6',
[$unsupported_version('2.1.6')],
],
[
'1.10.22',
[$unsupported_version('1.10.22')],
......@@ -73,11 +78,11 @@ public function providerComposerVersionValidation(): array {
],
[
'2.0.0-alpha3',
[],
[$unsupported_version('2.0.0-alpha3')],
],
[
'2.1.0-RC1',
[],
[$unsupported_version('2.1.0-RC1')],
],
[
'1.0.0-RC',
......
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