Skip to content
Snippets Groups Projects
Commit 2da91c69 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3259228 by phenaproxima, tedbow: Add functional test coverage of...

Issue #3259228 by phenaproxima, tedbow: Add functional test coverage of Stage::require() to ensure only the correct dependencies are updated
parent 6a3f2d7c
No related branches found
No related tags found
No related merge requests found
......@@ -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.
......
......@@ -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);
......
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
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\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);
}
}
}
......@@ -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.
*
......
......@@ -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',
......
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