diff --git a/drupalci.yml b/drupalci.yml index 61958f038e12097e5064529bdee7a6a1c0567f90..8fac0e8b1e16cdcfb12c81724e3d5ef746c30749 100644 --- a/drupalci.yml +++ b/drupalci.yml @@ -43,6 +43,7 @@ build: # to work correctly, and disabling it is a known workaround. # @see pcre.ini - sudo cp modules/contrib/automatic_updates/pcre.ini /usr/local/etc/php/conf.d + - composer self-update 2.2.4 halt-on-fail: true # 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. diff --git a/package_manager/src/ProcessFactory.php b/package_manager/src/ProcessFactory.php index bb053beadfb365feb98944988d7218426f036ec8..78ef902b2d909cb6e7dfb460ed3fe9612d5fa0e4 100644 --- a/package_manager/src/ProcessFactory.php +++ b/package_manager/src/ProcessFactory.php @@ -87,7 +87,7 @@ final class ProcessFactory implements ProcessFactoryInterface { */ private function getComposerHomePath(): string { $home_path = $this->fileSystem->getTempDirectory(); - $home_path .= '/automatic_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); diff --git a/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.info.yml b/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..4ad510c1f437add9d15c5dbe1a9cac7dd6f5a4f5 --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.info.yml @@ -0,0 +1,6 @@ +name: 'Package Manager Test API' +description: 'Provides API endpoints for doing stage operations in functional tests.' +type: module +package: Testing +dependencies: + - automatic_updates:package_manager diff --git a/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.routing.yml b/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..4b5cf58ca1f5f30d96cb984bb097e2bccdf6fae0 --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_api/package_manager_test_api.routing.yml @@ -0,0 +1,6 @@ +package_manager_test_api.require: + path: '/package-manager-test-api/require' + defaults: + _controller: 'Drupal\package_manager_test_api\ApiController::require' + requirements: + _access: 'TRUE' diff --git a/package_manager/tests/modules/package_manager_test_api/src/ApiController.php b/package_manager/tests/modules/package_manager_test_api/src/ApiController.php new file mode 100644 index 0000000000000000000000000000000000000000..a9f116ff2bc82ac62497304e31790a7c1953a208 --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_api/src/ApiController.php @@ -0,0 +1,81 @@ +<?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); + } + +} diff --git a/package_manager/tests/src/Build/StagedUpdateTest.php b/package_manager/tests/src/Build/StagedUpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8dbf40363380cadb19729d7d43dc343ae59bdf3e --- /dev/null +++ b/package_manager/tests/src/Build/StagedUpdateTest.php @@ -0,0 +1,87 @@ +<?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'); + } + +} diff --git a/tests/src/Build/TemplateProjectSiteTestBase.php b/package_manager/tests/src/Build/TemplateProjectTestBase.php similarity index 77% rename from tests/src/Build/TemplateProjectSiteTestBase.php rename to package_manager/tests/src/Build/TemplateProjectTestBase.php index f77f19f2b3508b67d830cb48cd417ad924d6c7ec..5b17ceed6b3659615531b99997bc1c7088fa7551 100644 --- a/tests/src/Build/TemplateProjectSiteTestBase.php +++ b/package_manager/tests/src/Build/TemplateProjectTestBase.php @@ -1,14 +1,19 @@ <?php -namespace Drupal\Tests\automatic_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,6 +35,8 @@ abstract class TemplateProjectSiteTestBase extends QuickStartTestBase { ]; } + // BEGIN: DELETE FROM CORE MERGE REQUEST + /** * {@inheritdoc} */ @@ -45,6 +52,8 @@ abstract class TemplateProjectSiteTestBase extends QuickStartTestBase { $this->runComposer('composer remove --no-update drupal/automatic_updates', 'composer/Metapackage/CoreRecommended'); } + // END: DELETE FROM CORE MERGE REQUEST + /** * {@inheritdoc} */ @@ -77,6 +86,14 @@ abstract class TemplateProjectSiteTestBase extends QuickStartTestBase { */ 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 +214,18 @@ abstract class TemplateProjectSiteTestBase extends QuickStartTestBase { // 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'); + + // BEGIN: DELETE FROM CORE MERGE REQUEST + // Install Automatic Updates into the test project and ensure it wasn't + // symlinked. + $automatic_updates_dir = realpath(__DIR__ . '/../../../..'); + if (str_contains($automatic_updates_dir, 'automatic_updates')) { + $dir = 'project'; + $this->runComposer("composer config repo.automatic_updates path $automatic_updates_dir", $dir); + $output = $this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require --update-with-all-dependencies "drupal/automatic_updates:@dev"', $dir); + $this->assertStringNotContainsString('Symlinking', $output); + } + // END: DELETE FROM CORE MERGE REQUEST } /** @@ -292,4 +321,54 @@ abstract class TemplateProjectSiteTestBase extends QuickStartTestBase { 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); + } + } + } diff --git a/tests/src/Build/UpdateTestBase.php b/tests/src/Build/UpdateTestBase.php index 278eeaa2596b1e8315bcb77c77d8b13e64e66bff..eb859abb1d52b6e3bf820d2c4a867c01a11823d8 100644 --- a/tests/src/Build/UpdateTestBase.php +++ b/tests/src/Build/UpdateTestBase.php @@ -3,11 +3,12 @@ namespace Drupal\Tests\automatic_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,27 +33,8 @@ abstract class UpdateTestBase extends TemplateProjectSiteTestBase { protected function createTestProject(string $template): void { parent::createTestProject($template); - // BEGIN: DELETE FROM CORE MERGE REQUEST - // Install Automatic Updates into the test project and ensure it wasn't - // symlinked. - if (__NAMESPACE__ === 'Drupal\Tests\automatic_updates\Build') { - $dir = 'project'; - $this->runComposer('composer config repo.automatic_updates path ' . __DIR__ . '/../../..', $dir); - $this->runComposer('composer require --no-update "drupal/automatic_updates:@dev"', $dir); - $output = $this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer update --with-all-dependencies', $dir); - $this->assertStringNotContainsString('Symlinking', $output); - } - // END: DELETE FROM CORE MERGE REQUEST - // 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([ 'automatic_updates', @@ -61,27 +43,6 @@ END; ]); } - /** - * 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. * @@ -110,33 +71,6 @@ END; $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. * diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php index a1c96278ceb624f49fcdce4a43122f27952c0166..70fc49e42e42bf465474b8449d6181a8cd904aa6 100644 --- a/tests/src/Kernel/UpdaterTest.php +++ b/tests/src/Kernel/UpdaterTest.php @@ -86,7 +86,9 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase { // 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',