diff --git a/package_manager/package_manager.module b/package_manager/package_manager.module index 889f3e2966f87ae936aa45540fd06c4835e675c6..e75f4f2df2e9de0c22280a966071141562296d04 100644 --- a/package_manager/package_manager.module +++ b/package_manager/package_manager.module @@ -33,7 +33,7 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) { $output .= '<ul>'; $output .= ' <li>' . t('It does not support Drupal multi-site installations.') . '</li>'; $output .= ' <li>' . t('It does not support symlinks. If you have any, see <a href="#package-manager-faq-composer-not-found">What if it says I have symlinks in my codebase?</a>.') . '</li>'; - $output .= ' <li>' . t('It only allows supported composer plugins. If you have any, see <a href="#package-manager-faq-unsupported-composer-plugin">What if it says I have unsupported composer plugins in my codebase?</a>.') . '</li>'; + $output .= ' <li>' . t('It only allows supported Composer plugins. If you have any, see <a href="#package-manager-faq-unsupported-composer-plugin">What if it says I have unsupported Composer plugins in my codebase?</a>.') . '</li>'; $output .= ' <li>' . t('It does not automatically perform version control operations, e.g., with Git. Site administrators are responsible for committing updates.') . '</li>'; $output .= ' <li>' . t('It can only maintain one copy of the site at any given time. If a copy of the site already exists, another one cannot be created until the existing copy is destroyed.') . '</li>'; $output .= ' <li>' . t('It associates the temporary copy of the site with the user or session that originally created it, and only that user or session can make changes to it.') . '</li>'; @@ -81,11 +81,22 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) { $output .= '<p>' . t('The new configuration will take effect on the next Composer install or update event. Do this to apply it immediately:') . '</p>'; $output .= '<pre><code>composer install</code></pre>'; - $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported composer plugins in my codebase?') . '</h4>'; - $output .= '<p>' . t('A fresh Drupal installation only uses supported composer plugins, but some modules or themes may depend on additional composer plugins. Please <a href=":new-issue">create a new issue</a> when you encounter this.', [ + $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>'; + $output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. Please <a href=":new-issue">create a new issue</a> when you encounter this.', [ ':new-issue' => 'https://www.drupal.org/node/add/project-issue/automatic_updates', ]) . '</p>'; - $output .= '<p>' . t('It is possible to <em>trust</em> additional composer plugins, but this requires significant expertise: understanding the code of that composer plugin, what the effects on the file system are and how it affects the Package Manager module. Some composer plugins could result in a broken site!') . '</p>'; + $output .= '<p>' . t('It is possible to <em>trust</em> additional Composer plugins, but this requires significant expertise: understanding the code of that Composer plugin, what the effects on the file system are and how it affects the Package Manager module. Some Composer plugins could result in a broken site!') . '</p>'; + + $output .= '<h4 id="package-manager-faq-composer-patches-installed-or-removed">' . t('What if it says <code>cweagans/composer-patches</code> cannot be installed/removed?') . '</h4>'; + $output .= '<p>' . t('Installation or removal of <code>cweagans/composer-patches</code> via Package Manager is not support it. You can install or remove it manually by running Composer commands in your site root.') . '</p>'; + $output .= '<p>' . t('To install it:') . '</p>'; + $output .= '<pre><code>composer require cweagans/composer-patches</code></pre>'; + $output .= '<p>' . t('To remove it:') . '</p>'; + $output .= '<pre><code>composer remove cweagans/composer-patches</code></pre>'; + + $output .= '<h4 id="package-manager-faq-composer-patches-not-a-root-dependency">' . t('What if it says <code>cweagans/composer-patches</code> must be a root dependency?') . '</h4>'; + $output .= '<p>' . t('If <code>cweagans/composer-patches</code> is installed, it must be defined as a dependency of the main project (i.e., it must be listed in the <code>require</code> or <code>require-dev</code> section of <code>composer.json</code>). You can run the following command in your site root to add it as a dependency of the main project:') . '</p>'; + $output .= "<pre><code>composer require cweagans/composer-patches</code></pre>"; $output .= '<h5>' . t('Custom code') . '</h5>'; $output .= '<p>' . t('Symlinks are seldom truly necessary and should be avoided in your own code. No solution currently exists to get around them--they must be removed in order to use Automatic Updates.') . '</p>'; diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index a165aca59d54aaa0bae8bc03abdfd424fbdae0d0..cf43ca76a27da9801fa0b17d22e9f09118b4295d 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -240,6 +240,8 @@ services: - { name: event_subscriber } package_manager.validator.patches: class: Drupal\package_manager\Validator\ComposerPatchesValidator + arguments: + - '@module_handler' tags: - { name: event_subscriber } package_manager.validator.supported_releases: diff --git a/package_manager/src/Validator/ComposerPatchesValidator.php b/package_manager/src/Validator/ComposerPatchesValidator.php index 7588095ffdbd9e03aa85fa4c200e511c50d2aea3..efcbee12b7e5420acdd1fbaf587c88154730c3d4 100644 --- a/package_manager/src/Validator/ComposerPatchesValidator.php +++ b/package_manager/src/Validator/ComposerPatchesValidator.php @@ -4,7 +4,11 @@ declare(strict_types = 1); namespace Drupal\package_manager\Validator; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\package_manager\ComposerUtility; use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\Event\PreOperationStageEvent; @@ -13,30 +17,152 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Validates the configuration of the cweagans/composer-patches plugin. + * + * To ensure that applied patches remain consistent between the active and + * stage directories, the following rules are enforced if the patcher is + * installed: + * - It must be installed in both places, or in neither of them. It can't, for + * example, be installed in the active directory but not the stage directory + * (or vice-versa). + * - It must be one of the project's direct runtime or dev dependencies. + * - It cannot be installed or removed by Package Manager. In other words, it + * must be added to the project at the command line by someone technical + * enough to install and configure it properly. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. */ -class ComposerPatchesValidator implements EventSubscriberInterface { +final class ComposerPatchesValidator implements EventSubscriberInterface { use StringTranslationTrait; /** - * {@inheritdoc} + * The name of the plugin being analyzed. + * + * @var string + */ + private const PLUGIN_NAME = 'cweagans/composer-patches'; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + private ModuleHandlerInterface $moduleHandler; + + /** + * Constructs a ComposerPatchesValidator object. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. + */ + public function __construct(ModuleHandlerInterface $module_handler) { + $this->moduleHandler = $module_handler; + } + + /** + * Validates the status of the patcher plugin. + * + * @param \Drupal\package_manager\Event\PreOperationStageEvent $event + * The event object. */ - public function validateStagePreOperation(PreOperationStageEvent $event): void { + public function validatePatcher(PreOperationStageEvent $event): void { + $messages = []; + $stage = $event->getStage(); - $composer = $stage->getActiveComposer(); - - if (array_key_exists('cweagans/composer-patches', $composer->getInstalledPackages())) { - $composer = $composer->getComposer(); - - $extra = $composer->getPackage()->getExtra(); - if (empty($extra['composer-exit-on-patch-failure'])) { - $event->addError([ - $this->t('The <code>cweagans/composer-patches</code> plugin is installed, but the <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of @file.', [ - '@file' => $composer->getConfig()->getConfigSource()->getName(), - ]), - ]); + [$plugin_installed_in_active, $is_active_root_requirement, $active_configuration_ok] = $this->computePatcherStatus($stage->getActiveComposer()); + try { + [$plugin_installed_in_stage, $is_stage_root_requirement, $stage_configuration_ok] = $this->computePatcherStatus($stage->getStageComposer()); + $has_staged_update = TRUE; + } + catch (\LogicException $e) { + // No staged update exists. + $has_staged_update = FALSE; + } + + // If there's a staged update and the patcher has been installed or removed + // in the stage directory, that's a problem. + if ($has_staged_update && $plugin_installed_in_active !== $plugin_installed_in_stage) { + if ($plugin_installed_in_stage) { + $message = $this->t('It cannot be installed by Package Manager.'); + } + else { + $message = $this->t('It cannot be removed by Package Manager.'); } + $messages[] = $this->createErrorMessage($message, 'package-manager-faq-composer-patches-installed-or-removed'); + } + + // If the patcher is not listed in the runtime or dev dependencies, that's + // an error as well. + if (($plugin_installed_in_active && !$is_active_root_requirement) || ($has_staged_update && $plugin_installed_in_stage && !$is_stage_root_requirement)) { + $messages[] = $this->createErrorMessage($this->t('It must be a root dependency.'), 'package-manager-faq-composer-patches-not-a-root-dependency'); } + + // If the plugin is misconfigured in either the active or stage directories, + // flag an error. + if (($plugin_installed_in_active && !$active_configuration_ok) || ($has_staged_update && $plugin_installed_in_stage && !$stage_configuration_ok)) { + $messages[] = $this->t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.'); + } + + if ($messages) { + $summary = $this->t("Problems detected related to the Composer plugin <code>@plugin</code>.", [ + '@plugin' => static::PLUGIN_NAME, + ]); + $event->addError($messages, $summary); + } + } + + /** + * Appends a link to online help to an error message. + * + * @param \Drupal\Core\StringTranslation\TranslatableMarkup $message + * The error message. + * @param string $fragment + * The fragment of the online help to link to. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The final, translated error message. + */ + private function createErrorMessage(TranslatableMarkup $message, string $fragment): TranslatableMarkup { + if ($this->moduleHandler->moduleExists('help')) { + $url = Url::fromRoute('help.page', ['name' => 'package_manager']) + ->setOption('fragment', $fragment) + ->toString(); + + return $this->t('@message See <a href=":url">the help page</a> for information on how to resolve the problem.', [ + '@message' => $message, + ':url' => $url, + ]); + } + return $message; + } + + /** + * Computes the status of the patcher plugin in a particular directory. + * + * @param \Drupal\package_manager\ComposerUtility $composer + * A Composer utility for a specific directory. + * + * @return bool[] + * An indexed array containing three booleans, in order: + * - Whether the patcher plugin is installed. + * - Whether the patcher plugin is a root requirement in composer.json (in + * either the runtime or dev dependencies). + * - Whether the `composer-exit-on-patch-failure` flag is set in the `extra` + * section of composer.json. + */ + private function computePatcherStatus(ComposerUtility $composer): array { + $is_installed = array_key_exists(static::PLUGIN_NAME, $composer->getInstalledPackages()); + + $root_package = $composer->getComposer()->getPackage(); + $is_root_requirement = array_key_exists(static::PLUGIN_NAME, $root_package->getRequires()) || array_key_exists(static::PLUGIN_NAME, $root_package->getDevRequires()); + + $extra = $root_package->getExtra(); + $exit_on_failure = !empty($extra['composer-exit-on-patch-failure']); + + return [$is_installed, $is_root_requirement, $exit_on_failure]; } /** @@ -44,9 +170,9 @@ class ComposerPatchesValidator implements EventSubscriberInterface { */ public static function getSubscribedEvents(): array { return [ - PreCreateEvent::class => 'validateStagePreOperation', - PreApplyEvent::class => 'validateStagePreOperation', - StatusCheckEvent::class => 'validateStagePreOperation', + PreCreateEvent::class => 'validatePatcher', + PreApplyEvent::class => 'validatePatcher', + StatusCheckEvent::class => 'validatePatcher', ]; } diff --git a/package_manager/tests/fixtures/fake_site/composer.json b/package_manager/tests/fixtures/fake_site/composer.json index 4c53c626dbd35c88502ee701ef9acf0d5f625fdb..0d3c05112027fc2a80792a1a5703ad27fc7dd9bc 100644 --- a/package_manager/tests/fixtures/fake_site/composer.json +++ b/package_manager/tests/fixtures/fake_site/composer.json @@ -45,6 +45,14 @@ "options": { "symlink": false } + }, + "cweagans/composer-patches": { + "type": "path", + "version": "24.12.1999", + "url": "../path_repos/cweagans--composer-patches", + "options": { + "symlink": false + } } }, "minimum-stability": "stable", diff --git a/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..799869295807f6a5bf36fa7053816add4e3d228e --- /dev/null +++ b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json @@ -0,0 +1,13 @@ +{ + "name": "cweagans/composer-patches", + "type": "composer-plugin", + "extra": { + "class": "\\cweagans\\Fake\\ComposerPatches" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "autoload": { + "psr-4": {"cweagans\\Fake\\": "src"} + } +} diff --git a/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/src/ComposerPatches.php b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/src/ComposerPatches.php new file mode 100644 index 0000000000000000000000000000000000000000..65f431d01ba64bde86b58b3b0de8efbc54aa5144 --- /dev/null +++ b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/src/ComposerPatches.php @@ -0,0 +1,29 @@ +<?php + +namespace cweagans\Fake; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; + +/** + * Dummy composer plugin implementation. + */ +class ComposerPatches implements PluginInterface { + + /** + * {@inheritdoc} + */ + public function activate(Composer $composer, IOInterface $io) {} + + /** + * {@inheritdoc} + */ + public function deactivate(Composer $composer, IOInterface $io) {} + + /** + * {@inheritdoc} + */ + public function uninstall(Composer $composer, IOInterface $io) {} + +} diff --git a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php index 54c307715d2c3bbd42f0b115c793c2eef6e3446f..c8fa89aacb7fa909e2efe13cc743ead5ce0e6186 100644 --- a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php +++ b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php @@ -4,10 +4,12 @@ declare(strict_types = 1); namespace Drupal\Tests\package_manager\Kernel; +use Drupal\Core\Url; use Drupal\fixture_manipulator\ActiveFixtureManipulator; -use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\Event\StatusCheckEvent; use Drupal\package_manager\ValidationResult; +use Symfony\Component\Process\Process; /** * @covers \Drupal\package_manager\Validator\ComposerPatchesValidator @@ -16,63 +18,227 @@ use Drupal\package_manager\ValidationResult; */ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase { + /** + * Data provider for testErrorDuringPreCreate(). + * + * @return mixed[][] + * The test cases. + */ + public function providerPatcherConfiguration(): array { + return [ + 'exit-on-patch-failure missing' => [ + FALSE, + [ + ValidationResult::createError([ + t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.'), + ], t('Problems detected related to the Composer plugin <code>cweagans/composer-patches</code>.')), + ], + ], + 'exit-on-patch-failure set' => [ + TRUE, + [], + ], + ]; + } + /** * Tests that the patcher configuration is validated during pre-create. + * + * @param bool $extra_key_set + * Whether to set key in extra part of root package. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * + * @dataProvider providerPatcherConfiguration() */ - public function testError(): void { - // Simulate an active directory where the patcher is installed, but there's - // no composer-exit-on-patch-failure flag. - $dir = $this->container->get('package_manager.path_locator') - ->getProjectRoot(); - - $this->installPatcherInActive($dir); - - // Because ComposerUtility reads composer.json and passes it to the Composer - // factory as an array, Composer will assume that the configuration is - // coming from a config.json file, even if one doesn't exist. - $error = ValidationResult::createError([ - t('The <code>cweagans/composer-patches</code> plugin is installed, but the <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of @dir/composer.json.', [ - '@dir' => realpath($dir), - ]), - ]); - $this->assertStatusCheckResults([$error]); - $this->assertResults([$error], PreCreateEvent::class); + public function testPatcherConfiguration(bool $extra_key_set, array $expected_results): void { + $this->addPatcherToAllowedPlugins(); + $this->setRootRequires(); + if ($extra_key_set) { + $this->setRootExtra(); + } + $this->assertStatusCheckResults($expected_results); + $this->assertResults($expected_results, PreCreateEvent::class); } /** - * Tests that the patcher configuration is validated during pre-apply. + * Data provider for testErrorDuringPreApply() and testHelpLink(). + * + * @return mixed[][] + * The test cases. */ - public function testErrorDuringPreApply(): void { - // Simulate an active directory where the patcher is installed, but there's - // no composer-exit-on-patch-failure flag. - $dir = $this->container->get('package_manager.path_locator') - ->getProjectRoot(); - - $this->addEventTestListener(function () use ($dir): void { - $this->installPatcherInActive($dir); - }); - // Because ComposerUtility reads composer.json and passes it to the Composer - // factory as an array, Composer will assume that the configuration is - // coming from a config.json file, even if one doesn't exist. - $error = ValidationResult::createError([ - "The <code>cweagans/composer-patches</code> plugin is installed, but the <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of $dir/composer.json.", - ]); - $this->assertResults([$error], PreApplyEvent::class); + public function providerErrorDuringPreApply(): array { + $summary = t('Problems detected related to the Composer plugin <code>cweagans/composer-patches</code>.'); + + return [ + 'composer-patches present in stage, but not present in active' => [ + FALSE, + TRUE, + [ + ValidationResult::createError([ + t('It cannot be installed by Package Manager.'), + t('It must be a root dependency.'), + t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.'), + ], $summary), + ], + [ + 'package-manager-faq-composer-patches-installed-or-removed', + 'package-manager-faq-composer-patches-not-a-root-dependency', + NULL, + ], + ], + 'composer-patches removed in stage, but present in active' => [ + TRUE, + FALSE, + [ + ValidationResult::createError([ + t('It cannot be removed by Package Manager.'), + ], $summary), + ], + [ + 'package-manager-faq-composer-patches-installed-or-removed', + ], + ], + 'composer-patches present in stage and active' => [ + TRUE, + TRUE, + [], + [], + ], + 'composer-patches not present in stage and active' => [ + FALSE, + FALSE, + [], + [], + ], + ]; } /** - * Simulates that the patcher is installed in the active directory. + * Tests the patcher's presence and configuration are validated on pre-apply. * - * @param string $dir - * The active directory. + * @param bool $in_active + * Whether patcher is installed in active. + * @param bool $in_stage + * Whether patcher is installed in stage. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * + * @dataProvider providerErrorDuringPreApply */ - private function installPatcherInActive(string $dir): void { + public function testErrorDuringPreApply(bool $in_active, bool $in_stage, array $expected_results): void { + if ($in_active) { + // Add patcher as a root dependency and set + // `composer-exit-on-patch-failure` to true. + $this->addPatcherToAllowedPlugins(); + $this->setRootRequires(); + $this->setRootExtra(); + } + if ($in_stage && !$in_active) { + // Simulate a stage directory where the patcher is installed. + $this->getStageFixtureManipulator() + ->addPackage([ + 'name' => 'cweagans/composer-patches', + 'version' => '24.12.1999', + 'type' => 'composer-plugin', + ]); + } + + if (!$in_stage && $in_active) { + $this->getStageFixtureManipulator() + ->removePackage('cweagans/composer-patches'); + } + + $stage = $this->createStage(); + $stage->create(); + $stage_dir = $stage->getStageDirectory(); + $stage->require(['drupal/core:9.8.1']); + $event = new StatusCheckEvent($stage, []); + $this->container->get('event_dispatcher')->dispatch($event); + $this->assertValidationResultsEqual($expected_results, $event->getResults(), NULL, $stage_dir); + + try { + $stage->apply(); + // If we didn't get an exception, ensure we didn't expect any errors + $this->assertSame([], $expected_results); + } + catch (TestStageValidationException $e) { + $this->assertNotEmpty($expected_results); + $this->assertValidationResultsEqual($expected_results, $e->getResults(), NULL, $stage_dir); + } + } + + /** + * Tests that validation errors can carry links to help. + * + * @param bool $in_active + * Whether patcher is installed in active. + * @param bool $in_stage + * Whether patcher is installed in stage. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * @param string[] $help_page_sections + * An associative array of fragments (anchors) in the online help. The keys + * should be the numeric indices of the validation result messages which + * should link to those fragments. + * + * @dataProvider providerErrorDuringPreApply + */ + public function testErrorDuringPreApplyWithHelp(bool $in_active, bool $in_stage, array $expected_results, array $help_page_sections): void { + $this->enableModules(['help']); + + foreach ($expected_results as $result_index => $result) { + $messages = $result->getMessages(); + + foreach ($messages as $message_index => $message) { + if ($help_page_sections[$message_index]) { + // Get the link to the online documentation for the error message. + $url = Url::fromRoute('help.page', ['name' => 'package_manager']) + ->setOption('fragment', $help_page_sections[$message_index]) + ->toString(); + // Reformat the provided results so that they all have the link to the + // online documentation appended to them. + $messages[$message_index] = $message . ' See <a href="' . $url . '">the help page</a> for information on how to resolve the problem.'; + } + } + $expected_results[$result_index] = ValidationResult::createError($messages, $result->getSummary()); + } + $this->testErrorDuringPreApply($in_active, $in_stage, $expected_results); + } + + /** + * Add the installed patcher to allowed plugins. + */ + private function addPatcherToAllowedPlugins(): void { (new ActiveFixtureManipulator()) - ->addPackage([ - 'name' => 'cweagans/composer-patches', - 'version' => '1.0.0', - 'type' => 'composer-plugin', - ])->commitChanges(); + ->addConfig([ + 'allow-plugins' => [ + 'cweagans/composer-patches' => TRUE, + ], + ]) + ->commitChanges(); + } + + /** + * Sets the cweagans/composer-patches as required package for root package. + */ + private function setRootRequires(): void { + $process = new Process( + ['composer', 'require', "cweagans/composer-patches:@dev"], + $this->container->get('package_manager.path_locator')->getProjectRoot() + ); + $process->mustRun(); + } + + /** + * Sets the composer-exit-on-patch-failure key in extra part of root package. + */ + private function setRootExtra(): void { + $process = new Process( + ['composer', 'config', 'extra.composer-exit-on-patch-failure', 'true'], + $this->container->get('package_manager.path_locator')->getProjectRoot() + ); + $process->mustRun(); } } diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php index 499ab51fb9b84d003492daa43dffd50c4b854636..e277e02ebae3f48ac0a40cc48cc88dfeba1b0371 100644 --- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php +++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php @@ -245,6 +245,12 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase { $this->assertTrue(mkdir($active_dir)); static::copyFixtureFilesTo($source_dir, $active_dir); + // Make sure that the path repositories exist in the test project too. + (new Filesystem())->mirror(__DIR__ . '/../../fixtures/path_repos', $root . DIRECTORY_SEPARATOR . 'path_repos', NULL, [ + 'override' => TRUE, + 'delete' => FALSE, + ]); + // Removing 'vfs://root/' from site path set in // \Drupal\KernelTests\KernelTestBase::setUpFilesystem as we don't use vfs. $test_site_path = str_replace('vfs://root/', '', $this->siteDirectory); diff --git a/package_manager/tests/src/Traits/ValidationTestTrait.php b/package_manager/tests/src/Traits/ValidationTestTrait.php index 440b963f19dee42c5a08b40f0a24a4b1dc2a4081..035c08f7e6bcf287f82cdb2859424b218a6ecee8 100644 --- a/package_manager/tests/src/Traits/ValidationTestTrait.php +++ b/package_manager/tests/src/Traits/ValidationTestTrait.php @@ -28,14 +28,16 @@ trait ValidationTestTrait { * The actual validation results. * @param \Drupal\package_manager\PathLocator|null $path_locator * (optional) The path locator (when this trait is used in unit tests). + * @param string|null $stage_dir + * (optional) The stage directory. */ - protected function assertValidationResultsEqual(array $expected_results, array $actual_results, ?PathLocator $path_locator = NULL): void { + protected function assertValidationResultsEqual(array $expected_results, array $actual_results, ?PathLocator $path_locator = NULL, ?string $stage_dir = NULL): void { if ($path_locator) { assert(is_a(get_called_class(), UnitTestCase::class, TRUE)); } $expected_results = array_map( - function (array $result) use ($path_locator): array { - $result['messages'] = $this->resolvePlaceholdersInArrayValuesWithRealPaths($result['messages'], $path_locator); + function (array $result) use ($path_locator, $stage_dir): array { + $result['messages'] = $this->resolvePlaceholdersInArrayValuesWithRealPaths($result['messages'], $path_locator, $stage_dir); return $result; }, $this->getValidationResultsAsArray($expected_results) @@ -54,19 +56,30 @@ trait ValidationTestTrait { * <STAGE_ROOT_PARENT>. * @param \Drupal\package_manager\PathLocator|null $path_locator * (optional) The path locator (when this trait is used in unit tests). + * @param string|null $stage_dir + * (optional) The stage directory. * * @return array * The same array, with unchanged keys, and with the placeholders resolved. */ - protected function resolvePlaceholdersInArrayValuesWithRealPaths(array $subject, ?PathLocator $path_locator = NULL): array { + protected function resolvePlaceholdersInArrayValuesWithRealPaths(array $subject, ?PathLocator $path_locator = NULL, ?string $stage_dir = NULL): array { if (!$path_locator) { $path_locator = $this->container->get('package_manager.path_locator'); } - return str_replace( + $subject = str_replace( ['<PROJECT_ROOT>', '<VENDOR_DIR>', '<STAGE_ROOT>', '<STAGE_ROOT_PARENT>'], [$path_locator->getProjectRoot(), $path_locator->getVendorDirectory(), $path_locator->getStagingRoot(), dirname($path_locator->getStagingRoot())], $subject ); + if ($stage_dir) { + $subject = str_replace(['<STAGE_DIR>'], [$stage_dir], $subject); + } + foreach ($subject as $message) { + if (str_contains($message, '<STAGE_DIR>')) { + throw new \LogicException("No stage directory passed to replace '<STAGE_DIR>' in message '$message'"); + } + } + return $subject; } /** diff --git a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php index 5fa17000269abaac316447cdfd08e058305b5eb7..a7413bb61229aafe1fd33080874a3a57785e9dc4 100644 --- a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php @@ -42,7 +42,7 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas /** * {@inheritdoc} */ - protected function assertValidationResultsEqual(array $expected_results, array $actual_results, ?PathLocator $path_locator = NULL): void { + protected function assertValidationResultsEqual(array $expected_results, array $actual_results, ?PathLocator $path_locator = NULL, ?string $stage_dir = NULL): void { $map = function (string $path): string { return $this->activeDir . '/' . $path; };