From 6d50846ac6420f4c00cd1ad386e4ecce87c54c35 Mon Sep 17 00:00:00 2001
From: Kunal Sachdev <57170-kunal.sachdev@users.noreply.drupalcode.org>
Date: Tue, 27 Jun 2023 19:25:44 +0000
Subject: [PATCH] Issue #3354914 by kunal.sachdev, phenaproxima, Wim Leers,
 tedbow: Package Manager should have status report entry to inform user of
 Composer version being used

---
 package_manager/package_manager.install       | 30 +++++++
 package_manager/src/ComposerInspector.php     |  2 +-
 .../Functional/ComposerRequirementTest.php    | 55 +++++++++++++
 .../src/Kernel/ComposerInspectorTest.php      | 79 +++++++++++++------
 4 files changed, 143 insertions(+), 23 deletions(-)
 create mode 100644 package_manager/tests/src/Functional/ComposerRequirementTest.php

diff --git a/package_manager/package_manager.install b/package_manager/package_manager.install
index 3d9fe0c606..5d59d0a8a3 100644
--- a/package_manager/package_manager.install
+++ b/package_manager/package_manager.install
@@ -7,7 +7,9 @@
 
 declare(strict_types = 1);
 
+use Drupal\package_manager\ComposerInspector;
 use Drupal\package_manager\Exception\StageFailureMarkerException;
+use PhpTuf\ComposerStager\Infrastructure\Service\Finder\ExecutableFinderInterface;
 
 /**
  * Implements hook_requirements().
@@ -39,6 +41,34 @@ function package_manager_requirements(string $phase) {
       ];
     }
   }
+
+  if ($phase !== 'runtime') {
+    return $requirements;
+  }
+  /** @var \PhpTuf\ComposerStager\Infrastructure\Service\Finder\ExecutableFinderInterface $executable_finder */
+  $executable_finder = \Drupal::service(ExecutableFinderInterface::class);
+
+  // Report the Composer version in use, as well as its path.
+  $title = t('Composer version');
+  try {
+    $requirements['package_manager_composer'] = [
+      'title' => $title,
+      'description' => t('@version (<code>@path</code>)', [
+        '@version' => \Drupal::service(ComposerInspector::class)->getVersion(),
+        '@path' => $executable_finder->find('composer'),
+      ]),
+      'severity' => REQUIREMENT_INFO,
+    ];
+  }
+  catch (\Throwable $e) {
+    $requirements['package_manager_composer'] = [
+      'title' => $title,
+      'description' => t('Composer was not found. The error message was: @message', [
+        '@message' => $e->getMessage(),
+      ]),
+      'severity' => REQUIREMENT_ERROR,
+    ];
+  }
   return $requirements;
 }
 
diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php
index 1a4fc0725a..013d0ca7da 100644
--- a/package_manager/src/ComposerInspector.php
+++ b/package_manager/src/ComposerInspector.php
@@ -279,7 +279,7 @@ class ComposerInspector implements LoggerAwareInterface {
    * @throws \UnexpectedValueException
    *   Thrown if the Composer version cannot be determined.
    */
-  private function getVersion(): string {
+  public function getVersion(): string {
     $this->runner->run(['--format=json'], $this->processCallback->reset());
     $data = $this->processCallback->parseJsonOutput();
     if (isset($data['application']['name'])
diff --git a/package_manager/tests/src/Functional/ComposerRequirementTest.php b/package_manager/tests/src/Functional/ComposerRequirementTest.php
new file mode 100644
index 0000000000..8eb7729b7e
--- /dev/null
+++ b/package_manager/tests/src/Functional/ComposerRequirementTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\package_manager\Functional;
+
+use Drupal\package_manager\ComposerInspector;
+use Drupal\Tests\BrowserTestBase;
+use PhpTuf\ComposerStager\Infrastructure\Service\Finder\ExecutableFinderInterface;
+
+/**
+ * Tests that Package Manager shows the Composer version on the status report.
+ *
+ * @group package_manager
+ * @internal
+ */
+class ComposerRequirementTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['package_manager'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Tests that Composer and file syncer info is listed on the status report.
+   */
+  public function testComposerInfoShown(): void {
+    /** @var \PhpTuf\ComposerStager\Infrastructure\Service\Finder\ExecutableFinderInterface $executable_finder */
+    $executable_finder = $this->container->get(ExecutableFinderInterface::class);
+    $composer_path = $executable_finder->find('composer');
+    $composer_version = $this->container->get(ComposerInspector::class)->getVersion();
+
+    $account = $this->drupalCreateUser(['administer site configuration']);
+    $this->drupalLogin($account);
+    $this->drupalGet('/admin/reports/status');
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Composer version');
+    $assert_session->responseContains("$composer_version (<code>$composer_path</code>)");
+
+    // If the path to Composer is invalid, we should see the error message
+    // that gets raised when we try to get its version.
+    $this->config('package_manager.settings')
+      ->set('executables.composer', '/path/to/composer')
+      ->save();
+    $this->getSession()->reload();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains('Composer was not found. The error message was: The command "\'/path/to/composer\' \'--format=json\'" failed.');
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/package_manager/tests/src/Kernel/ComposerInspectorTest.php
index b956182b96..02cc502af1 100644
--- a/package_manager/tests/src/Kernel/ComposerInspectorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerInspectorTest.php
@@ -20,6 +20,7 @@ use PhpTuf\ComposerStager\Domain\Service\Precondition\ComposerIsAvailableInterfa
 use PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ComposerRunnerInterface;
 use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactory;
 use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
 
 /**
  * @coversDefaultClass \Drupal\package_manager\ComposerInspector
@@ -240,29 +241,12 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
    *   [null, "Unable to determine Composer version"]
    */
   public function testVersionCheck(?string $reported_version, ?string $expected_message): void {
-    $runner = $this->prophesize(ComposerRunnerInterface::class);
-
-    $pass_version_to_output_callback = function (array $arguments_passed_to_runner) use ($reported_version): void {
-      $command_output = Json::encode([
-        'application' => [
-          'name' => 'Composer',
-          'version' => $reported_version,
-        ],
-      ]);
-
-      /** @var \Drupal\package_manager\ProcessOutputCallback $callback */
-      [, $callback] = $arguments_passed_to_runner;
-      $callback($callback::OUT, $command_output);
-    };
+    $runner = $this->mockComposerRunner($reported_version);
 
-    // We expect the runner to be called with two arguments: an array whose
-    // first item is `--format=json`, and an output callback. The result of the
-    // version check is statically cached, so the runner should only be called
-    // once, even though we call validate() twice in this test.
-    $runner->run(
-      Argument::withEntry(0, '--format=json'),
-      Argument::type(ProcessOutputCallback::class)
-    )->will($pass_version_to_output_callback)->shouldBeCalledOnce();
+    // The result of the version check is statically cached, so the runner
+    // should only be called once, even though we call validate() twice in this
+    // test.
+    $runner->getMethodProphecies('run')[0]->shouldBeCalledOnce();
     // The runner should be called with `validate` as the first argument, but
     // it won't affect the outcome of this test.
     $runner->run(Argument::withEntry(0, 'validate'));
@@ -294,6 +278,22 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
     $inspector->validate($project_root);
   }
 
+  /**
+   * @covers ::getVersion
+   *
+   * @testWith ["2.5.6"]
+   *   [null]
+   */
+  public function testGetVersion(?string $reported_version): void {
+    $this->container->set(ComposerRunnerInterface::class, $this->mockComposerRunner($reported_version)->reveal());
+
+    if (empty($reported_version)) {
+      $this->expectException(\UnexpectedValueException::class);
+      $this->expectExceptionMessage('Unable to determine Composer version');
+    }
+    $this->assertSame($reported_version, $this->container->get(ComposerInspector::class)->getVersion());
+  }
+
   /**
    * @covers ::validate
    */
@@ -474,4 +474,39 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
     $this->assertSame($expected_value, $actual_value);
   }
 
+  /**
+   * Mocks the Composer runner service to return a particular version string.
+   *
+   * @param string|null $reported_version
+   *   The version number that `composer --format=json` should return.
+   *
+   * @return \Prophecy\Prophecy\ObjectProphecy
+   *   The configurator for the mocked Composer runner.
+   */
+  private function mockComposerRunner(?string $reported_version): ObjectProphecy {
+    $runner = $this->prophesize(ComposerRunnerInterface::class);
+
+    $pass_version_to_output_callback = function (array $arguments_passed_to_runner) use ($reported_version): void {
+      $command_output = Json::encode([
+        'application' => [
+          'name' => 'Composer',
+          'version' => $reported_version,
+        ],
+      ]);
+
+      /** @var \Drupal\package_manager\ProcessOutputCallback $callback */
+      [, $callback] = $arguments_passed_to_runner;
+      $callback($callback::OUT, $command_output);
+    };
+
+    // We expect the runner to be called with two arguments: an array whose
+    // first item is `--format=json`, and an output callback.
+    $runner->run(
+      Argument::withEntry(0, '--format=json'),
+      Argument::type(ProcessOutputCallback::class)
+    )->will($pass_version_to_output_callback);
+
+    return $runner;
+  }
+
 }
-- 
GitLab