diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index 7b703499e3246d8558d9a8db6523fca05669eb62..72b819fa9c184e0837ce9d84c865aaa1baa2611b 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -85,7 +85,7 @@ services: - { name: event_subscriber } automatic_updates.composer_executable_validator: class: Drupal\automatic_updates\Validator\ComposerExecutableValidator - arguments: ['@automatic_updates.exec_finder'] + arguments: ['@automatic_updates.composer_runner'] tags: - { name: event_subscriber } automatic_updates.path_locator: diff --git a/src/Validator/ComposerExecutableValidator.php b/src/Validator/ComposerExecutableValidator.php index ce892f3b612c15346aa8c36d32b11928c71dc994..80d623a0a6fa42fc643a84af9c0096eab19693f4 100644 --- a/src/Validator/ComposerExecutableValidator.php +++ b/src/Validator/ComposerExecutableValidator.php @@ -5,30 +5,42 @@ namespace Drupal\automatic_updates\Validator; use Drupal\automatic_updates\AutomaticUpdatesEvents; use Drupal\automatic_updates\Event\UpdateEvent; use Drupal\automatic_updates\Validation\ValidationResult; -use PhpTuf\ComposerStager\Exception\IOException; -use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface; +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface; +use PhpTuf\ComposerStager\Exception\ExceptionInterface; +use PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * Validates that the Composer executable can be found. + * Validates that the Composer executable can be found in the correct version. */ -class ComposerExecutableValidator implements EventSubscriberInterface { +class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOutputCallbackInterface { + + use StringTranslationTrait; + + /** + * The Composer runner. + * + * @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface + */ + protected $composer; /** - * The executable finder service. + * The detected version of Composer. * - * @var \PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface + * @var string */ - protected $executableFinder; + protected $version; /** * Constructs a ComposerExecutableValidator object. * - * @param \PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface $executable_finder - * The executable finder service. + * @param \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface $composer + * The Composer runner. */ - public function __construct(ExecutableFinderInterface $executable_finder) { - $this->executableFinder = $executable_finder; + public function __construct(ComposerRunnerInterface $composer) { + $this->composer = $composer; } /** @@ -39,13 +51,34 @@ class ComposerExecutableValidator implements EventSubscriberInterface { */ public function checkForComposerExecutable(UpdateEvent $event): void { try { - $this->executableFinder->find('composer'); + $this->composer->run(['--version'], $this); } - catch (IOException $e) { + catch (ExceptionInterface $e) { $error = ValidationResult::createError([ $e->getMessage(), ]); $event->addValidationResult($error); + return; + } + + if ($this->version) { + $major_version = ExtensionVersion::createFromVersionString($this->version) + ->getMajorVersion(); + + if ($major_version < 2) { + $error = ValidationResult::createError([ + $this->t('Composer 2 or later is required, but version @version was detected.', [ + '@version' => $this->version, + ]), + ]); + $event->addValidationResult($error); + } + } + else { + $error = ValidationResult::createError([ + $this->t('The Composer version could not be detected.'), + ]); + $event->addValidationResult($error); } } @@ -58,4 +91,15 @@ class ComposerExecutableValidator implements EventSubscriberInterface { ]; } + /** + * {@inheritdoc} + */ + public function __invoke(string $type, string $buffer): void { + $matched = []; + // Search for a semantic version number and optional stability flag. + if (preg_match('/([0-9]+\.?){3}-?((alpha|beta|rc)[0-9]*)?/i', $buffer, $matched)) { + $this->version = $matched[0]; + } + } + } diff --git a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php index 2ad27ce9ed4eaa4401507b3902a827a440aebf8d..e6fe4b76e3553267781485a275dd185a3b1efd08 100644 --- a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php +++ b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php @@ -3,10 +3,12 @@ namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; use Drupal\automatic_updates\Validation\ValidationResult; +use Drupal\automatic_updates\Validator\ComposerExecutableValidator; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; use PhpTuf\ComposerStager\Exception\IOException; use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface; +use Prophecy\Argument; /** * @covers \Drupal\automatic_updates\Validator\ComposerExecutableValidator @@ -46,4 +48,104 @@ class ComposerExecutableValidatorTest extends KernelTestBase { $this->assertValidationResultsEqual([$error], $results); } + /** + * Data provider for ::testComposerVersionValidation(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerComposerVersionValidation(): array { + // Invalid or undetectable Composer versions will always produce the same + // error. + $invalid_version = ValidationResult::createError(['The Composer version could not be detected.']); + + // Unsupported Composer versions will report the detected version number + // 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 { + return ValidationResult::createError([ + "Composer 2 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', + [], + ], + [ + '1.10.22', + [$unsupported_version('1.10.22')], + ], + [ + '1.7.3', + [$unsupported_version('1.7.3')], + ], + [ + '2.0.0-alpha3', + [], + ], + [ + '2.1.0-RC1', + [], + ], + [ + '1.0.0-RC', + [$unsupported_version('1.0.0-RC')], + ], + [ + '1.0.0-beta1', + [$unsupported_version('1.0.0-beta1')], + ], + [ + '1.9-dev', + [$invalid_version], + ], + [ + '@package_version@', + [$invalid_version], + ], + ]; + } + + /** + * Tests validation of various Composer versions. + * + * @param string $reported_version + * The version of Composer that `composer --version` should report. + * @param \Drupal\automatic_updates\Validation\ValidationResult[] $expected_results + * The expected validation results. + * + * @dataProvider providerComposerVersionValidation + */ + public function testComposerVersionValidation(string $reported_version, array $expected_results): void { + // Mock the output of `composer --version`, will be passed to the validator, + // which is itself a callback function that gets called repeatedly as + // Composer produces output. + /** @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */ + $runner = $this->prophesize('\PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface'); + + $runner->run(['--version'], Argument::type(ComposerExecutableValidator::class)) + // Whatever is passed to ::run() will be passed to this mock callback in + // $arguments, and we know exactly what that will contain: an array of + // command arguments for Composer, and the validator object. + ->will(function (array $arguments) use ($reported_version) { + /** @var \Drupal\automatic_updates\Validator\ComposerExecutableValidator $validator */ + $validator = $arguments[1]; + // Invoke the validator (which, as mentioned, is a callback function), + // with fake output from `composer --version`. It should try to tease a + // recognized, supported version number out of this output. + $validator($validator::OUT, "Composer version $reported_version"); + }); + $this->container->set('automatic_updates.composer_runner', $runner->reveal()); + + // If the validator can't find a recognized, supported version of Composer, + // it should produce errors. + $actual_results = $this->container->get('automatic_updates.readiness_validation_manager') + ->run() + ->getResults(); + $this->assertValidationResultsEqual($expected_results, $actual_results); + } + }