Skip to content
Snippets Groups Projects
Commit 85b969d9 authored by Adam G-H's avatar Adam G-H Committed by Ted Bowman
Browse files

Issue #3268363 by phenaproxima, tedbow: Load new services after staged changes are applied

parent 91310036
No related branches found
No related tags found
1 merge request!229Issue #3268363: Create a build test to confirm that things are reloaded after the stage is applied
Showing
with 247 additions and 16 deletions
...@@ -331,6 +331,15 @@ class Stage { ...@@ -331,6 +331,15 @@ class Stage {
$this->committer->commit($stage_dir, $active_dir, $event->getExcludedPaths()); $this->committer->commit($stage_dir, $active_dir, $event->getExcludedPaths());
$this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY); $this->tempStore->delete(self::TEMPSTORE_APPLY_TIME_KEY);
// Rebuild the container and clear all caches, to ensure that new services
// are picked up.
drupal_flush_all_caches();
// Refresh the event dispatcher so that new or changed event subscribers
// will be called. The other services we depend on are either stateless or
// unlikely to call newly added code during the current request.
$this->eventDispatcher = \Drupal::service('event_dispatcher');
$this->dispatch(new PostApplyEvent($this)); $this->dispatch(new PostApplyEvent($this));
} }
......
{
"name": "drupal/alpha",
"type": "drupal-module",
"version": "1.0.0"
}
{
"name": "drupal/alpha",
"type": "drupal-module",
"version": "1.1.0"
}
{
"name": "drupal/updated_module",
"type": "drupal-module",
"version": "1.0.0"
}
name: 'Updated module'
description: 'A module which will change during an update, to ensure that the changes are picked up.'
type: module
package: Testing
{
"name": "drupal/updated_module",
"type": "drupal-module",
"version": "1.1.0"
}
<?php
namespace Drupal\updated_module;
use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Writes a file after staged changes are applied to the active directory.
*
* This event subscriber doesn't exist in version 1.0.0 of this module, so we
* use it to test that new event subscribers are picked up after staged changes
* have been applied.
*/
class PostApplySubscriber implements EventSubscriberInterface {
/**
* The path locator service.
*
* @var \Drupal\package_manager\PathLocator
*/
private $pathLocator;
/**
* Constructs a PostApplySubscriber.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
/**
* Writes a file when staged changes are applied to the active directory.
*/
public function postApply(): void {
$dir = $this->pathLocator->getProjectRoot();
file_put_contents("$dir/bravo.txt", 'Bravo!');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PostApplyEvent::class => 'postApply',
];
}
}
name: 'Updated module'
description: 'A module which will change during an update, to ensure that the changes are picked up.'
type: module
package: Testing
services:
updated_module.post_apply_subscriber:
class: Drupal\updated_module\PostApplySubscriber
arguments:
- '@package_manager.path_locator'
tags:
- { name: event_subscriber }
package_manager_test_api.require: package_manager_test_api:
path: '/package-manager-test-api/require' path: '/package-manager-test-api'
defaults: defaults:
_controller: 'Drupal\package_manager_test_api\ApiController::require' _controller: 'Drupal\package_manager_test_api\ApiController::run'
requirements: requirements:
_access: 'TRUE' _access: 'TRUE'
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\package_manager_test_api; namespace Drupal\package_manager_test_api;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Stage; use Drupal\package_manager\Stage;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
...@@ -20,14 +21,24 @@ class ApiController extends ControllerBase { ...@@ -20,14 +21,24 @@ class ApiController extends ControllerBase {
*/ */
private $stage; private $stage;
/**
* The path locator service.
*
* @var \Drupal\package_manager\PathLocator
*/
private $pathLocator;
/** /**
* Constructs an ApiController object. * Constructs an ApiController object.
* *
* @param \Drupal\package_manager\Stage $stage * @param \Drupal\package_manager\Stage $stage
* The stage. * The stage.
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/ */
public function __construct(Stage $stage) { public function __construct(Stage $stage, PathLocator $path_locator) {
$this->stage = $stage; $this->stage = $stage;
$this->pathLocator = $path_locator;
} }
/** /**
...@@ -45,39 +56,45 @@ class ApiController extends ControllerBase { ...@@ -45,39 +56,45 @@ class ApiController extends ControllerBase {
$container->get('tempstore.shared'), $container->get('tempstore.shared'),
$container->get('datetime.time') $container->get('datetime.time')
); );
return new static($stage); return new static(
$stage,
$container->get('package_manager.path_locator')
);
} }
/** /**
* Creates a staging area and requires packages into it. * Runs a complete stage life cycle.
*
* Creates a staging area, requires packages into it, applies changes to the
* active directory, and destroys the stage.
* *
* @param \Symfony\Component\HttpFoundation\Request $request * @param \Symfony\Component\HttpFoundation\Request $request
* The request. The runtime and dev dependencies are expected to be in * The request. The runtime and dev dependencies are expected to be in
* either the query string or request body, under the 'runtime' and 'dev' * either the query string or request body, under the 'runtime' and 'dev'
* keys, respectively. There may also be a 'files_to_return' key, which * keys, respectively. There may also be a 'files_to_return' key, which
* contains an array of file paths, relative to the stage directory, whose * contains an array of file paths, relative to the project root, whose
* contents should be returned in the response. * contents should be returned in the response.
* *
* @return \Symfony\Component\HttpFoundation\JsonResponse * @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing an associative array of the contents of the * 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 * files listed in the 'files_to_return' request key. The array will be
* be keyed by path, relative to the stage directory. * keyed by path, relative to the project root.
*/ */
public function require(Request $request): JsonResponse { public function run(Request $request): JsonResponse {
$this->stage->create(); $this->stage->create();
$this->stage->require( $this->stage->require(
$request->get('runtime', []), $request->get('runtime', []),
$request->get('dev', []) $request->get('dev', [])
); );
$this->stage->apply();
$this->stage->destroy();
$stage_dir = $this->stage->getStageDirectory(); $dir = $this->pathLocator->getProjectRoot();
$staged_file_contents = []; $file_contents = [];
foreach ($request->get('files_to_return', []) as $path) { foreach ($request->get('files_to_return', []) as $path) {
$staged_file_contents[$path] = file_get_contents($stage_dir . '/' . $path); $file_contents[$path] = file_get_contents($dir . '/' . $path);
} }
$this->stage->destroy(); return new JsonResponse($file_contents);
return new JsonResponse($staged_file_contents);
} }
} }
...@@ -7,76 +7,80 @@ namespace Drupal\Tests\package_manager\Build; ...@@ -7,76 +7,80 @@ namespace Drupal\Tests\package_manager\Build;
* *
* @group package_manager * @group package_manager
*/ */
class StagedUpdateTest extends TemplateProjectTestBase { class PackageUpdateTest extends TemplateProjectTestBase {
/** /**
* Tests that a stage only updates packages with changed constraints. * Tests updating packages in a staging area.
*/ */
public function testStagedUpdate(): void { public function testPackageUpdate(): void {
$this->createTestProject('RecommendedProject'); $this->createTestProject('RecommendedProject');
$this->createModule('alpha'); $this->addRepository('alpha', __DIR__ . '/../../fixtures/alpha/1.0.0');
$this->createModule('bravo'); $this->addRepository('updated_module', __DIR__ . '/../../fixtures/updated_module/1.0.0');
$this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha drupal/bravo --update-with-all-dependencies', 'project'); $this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha drupal/updated_module --update-with-all-dependencies', 'project');
$this->installQuickStart('minimal'); $this->installQuickStart('minimal');
$this->formLogin($this->adminUsername, $this->adminPassword); $this->formLogin($this->adminUsername, $this->adminPassword);
$this->installModules(['package_manager_test_api']); // The updated_module provides actual Drupal-facing functionality that we're
// testing as well, so we need to install it.
$this->installModules(['package_manager_test_api', 'updated_module']);
// Change both modules' upstream version. // Change both modules' upstream version.
$this->runComposer('composer config version 1.1.0', 'alpha'); $this->addRepository('alpha', __DIR__ . '/../../fixtures/alpha/1.1.0');
$this->runComposer('composer config version 1.1.0', 'bravo'); $this->addRepository('updated_module', __DIR__ . '/../../fixtures/updated_module/1.1.0');
// Use the API endpoint to create a stage and update bravo to 1.1.0. Even // Use the API endpoint to create a stage and update updated_module to
// though both modules are at version 1.1.0, only bravo should be updated. // 1.1.0. Even though both modules have version 1.1.0 available, only
// We ask the API to return the contents of both modules' staged // updated_module should be updated. We ask the API to return the contents
// composer.json files, so we can assert that the staged versions are what // of both modules' composer.json files, so we can assert that they were
// we expect. // updated to the versions we expect.
// @see \Drupal\package_manager_test_api\ApiController::require() // @see \Drupal\package_manager_test_api\ApiController::run()
$query = http_build_query([ $query = http_build_query([
'runtime' => [ 'runtime' => [
'drupal/bravo:1.1.0', 'drupal/updated_module:1.1.0',
], ],
'files_to_return' => [ 'files_to_return' => [
'web/modules/contrib/alpha/composer.json', 'web/modules/contrib/alpha/composer.json',
'web/modules/contrib/bravo/composer.json', 'web/modules/contrib/updated_module/composer.json',
'bravo.txt',
], ],
]); ]);
$this->visit("/package-manager-test-api/require?$query"); $this->visit("/package-manager-test-api?$query");
$mink = $this->getMink(); $mink = $this->getMink();
$mink->assertSession()->statusCodeEquals(200); $mink->assertSession()->statusCodeEquals(200);
$staged_file_contents = $mink->getSession()->getPage()->getContent(); $file_contents = $mink->getSession()->getPage()->getContent();
$staged_file_contents = json_decode($staged_file_contents, TRUE); $file_contents = json_decode($file_contents, TRUE);
$expected_versions = [ $expected_versions = [
'alpha' => '1.0.0', 'alpha' => '1.0.0',
'bravo' => '1.1.0', 'updated_module' => '1.1.0',
]; ];
foreach ($expected_versions as $module_name => $expected_version) { foreach ($expected_versions as $module_name => $expected_version) {
$path = "web/modules/contrib/$module_name/composer.json"; $path = "web/modules/contrib/$module_name/composer.json";
$staged_composer_json = json_decode($staged_file_contents[$path]); $module_composer_json = json_decode($file_contents[$path]);
$this->assertSame($expected_version, $staged_composer_json->version); $this->assertSame($expected_version, $module_composer_json->version);
} }
// The post-apply event subscriber in updated_module 1.1.0 should have
// created this file.
// @see \Drupal\updated_module\PostApplySubscriber::postApply()
$this->assertSame('Bravo!', $file_contents['bravo.txt']);
} }
/** /**
* Creates an empty module for testing purposes. * Adds a path repository to the test site.
* *
* @param string $name * @param string $name
* The machine name of the module, which can be added to the test site as * An arbitrary name for the repository.
* 'drupal/$name'. * @param string $path
* The path of the repository. Must exist in the file system.
*/ */
private function createModule(string $name): void { private function addRepository(string $name, string $path): void {
$dir = $this->getWorkspaceDirectory() . '/' . $name; $this->assertDirectoryExists($path);
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([ $repository = json_encode([
'type' => 'path', 'type' => 'path',
'url' => $dir, 'url' => $path,
'options' => [ 'options' => [
'symlink' => FALSE, 'symlink' => FALSE,
], ],
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace Drupal\Tests\package_manager\Kernel; namespace Drupal\Tests\package_manager\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\package_manager\Event\PostApplyEvent; use Drupal\package_manager\Event\PostApplyEvent;
use Drupal\package_manager\Event\PostCreateEvent; use Drupal\package_manager\Event\PostCreateEvent;
use Drupal\package_manager\Event\PostDestroyEvent; use Drupal\package_manager\Event\PostDestroyEvent;
...@@ -46,6 +47,18 @@ class StageEventsTest extends PackageManagerKernelTestBase implements EventSubsc ...@@ -46,6 +47,18 @@ class StageEventsTest extends PackageManagerKernelTestBase implements EventSubsc
$this->stage = $this->createStage(); $this->stage = $this->createStage();
} }
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
// Since this test adds arbitrary event listeners that aren't services, we
// need to ensure they will persist even if the container is rebuilt when
// staged changes are applied.
$container->getDefinition('event_dispatcher')->addTag('persist');
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -4,6 +4,7 @@ namespace Drupal\Tests\automatic_updates\Kernel; ...@@ -4,6 +4,7 @@ namespace Drupal\Tests\automatic_updates\Kernel;
use Drupal\automatic_updates\CronUpdater; use Drupal\automatic_updates\CronUpdater;
use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1; use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormState;
use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Logger\RfcLogLevel;
use Drupal\package_manager\Event\PostApplyEvent; use Drupal\package_manager\Event\PostApplyEvent;
...@@ -63,6 +64,19 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase { ...@@ -63,6 +64,19 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
->addLogger($this->logger); ->addLogger($this->logger);
} }
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
// Since this test dynamically adds additional loggers to certain channels,
// we need to ensure they will persist even if the container is rebuilt when
// staged changes are applied.
// @see ::testStageDestroyedOnError()
$container->getDefinition('logger.factory')->addTag('persist');
}
/** /**
* Data provider for ::testUpdaterCalled(). * Data provider for ::testUpdaterCalled().
* *
......
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