From fb30485bbcc7d30868d5f40285ed43d9505e8703 Mon Sep 17 00:00:00 2001
From: tedbow <tedbow@240860.no-reply.drupal.org>
Date: Thu, 18 Nov 2021 20:36:15 +0000
Subject: [PATCH] Issue #3250136 by phenaproxima, tedbow: Allow
 package_manager_bypass to record method invocations for functional tests

---
 .../package_manager_bypass/src/Beginner.php   |  3 +-
 .../package_manager_bypass/src/Committer.php  |  3 +-
 .../src/InvocationRecorderBase.php            | 36 +++++++++++++++++++
 .../package_manager_bypass/src/Stager.php     |  3 +-
 tests/src/Functional/UpdaterFormTest.php      | 26 ++++++++++++++
 tests/src/Kernel/UpdaterTest.php              | 25 +++++++------
 6 files changed, 82 insertions(+), 14 deletions(-)
 create mode 100644 package_manager/tests/modules/package_manager_bypass/src/InvocationRecorderBase.php

diff --git a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
index 837efe750f..742d38bd85 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
@@ -8,12 +8,13 @@ use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
 /**
  * Defines an update beginner which doesn't do anything.
  */
-class Beginner implements BeginnerInterface {
+class Beginner extends InvocationRecorderBase implements BeginnerInterface {
 
   /**
    * {@inheritdoc}
    */
   public function begin(string $activeDir, string $stagingDir, ?array $exclusions = [], ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+    $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
   }
 
 }
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Committer.php b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
index 45ec4fb225..3518237b5b 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Committer.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
@@ -8,7 +8,7 @@ use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
 /**
  * Defines an update committer which doesn't do any actual committing.
  */
-class Committer implements CommitterInterface {
+class Committer extends InvocationRecorderBase implements CommitterInterface {
 
   /**
    * The decorated committer service.
@@ -31,6 +31,7 @@ class Committer implements CommitterInterface {
    * {@inheritdoc}
    */
   public function commit(string $stagingDir, string $activeDir, ?array $exclusions = [], ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+    $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
   }
 
   /**
diff --git a/package_manager/tests/modules/package_manager_bypass/src/InvocationRecorderBase.php b/package_manager/tests/modules/package_manager_bypass/src/InvocationRecorderBase.php
new file mode 100644
index 0000000000..feb4dc92f6
--- /dev/null
+++ b/package_manager/tests/modules/package_manager_bypass/src/InvocationRecorderBase.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\package_manager_bypass;
+
+/**
+ * Records information about method invocations.
+ *
+ * This can be used by functional tests to ensure that the bypassed Composer
+ * Stager services were called as expected. Kernel and unit tests should use
+ * regular mocks instead.
+ */
+abstract class InvocationRecorderBase {
+
+  /**
+   * Returns the arguments from every invocation of the main class method.
+   *
+   * @return array[]
+   *   The arguments from every invocation of the main class method.
+   */
+  public function getInvocationArguments(): array {
+    return \Drupal::state()->get(static::class, []);
+  }
+
+  /**
+   * Records the arguments from an invocation of the main class method.
+   *
+   * @param mixed ...$arguments
+   *   The arguments that the main class method was called with.
+   */
+  protected function saveInvocationArguments(...$arguments) {
+    $invocations = $this->getInvocationArguments();
+    $invocations[] = $arguments;
+    \Drupal::state()->set(static::class, $invocations);
+  }
+
+}
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Stager.php b/package_manager/tests/modules/package_manager_bypass/src/Stager.php
index f068b29483..bb93e47271 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Stager.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Stager.php
@@ -8,12 +8,13 @@ use PhpTuf\ComposerStager\Domain\StagerInterface;
 /**
  * Defines an update stager which doesn't actually do anything.
  */
-class Stager implements StagerInterface {
+class Stager extends InvocationRecorderBase implements StagerInterface {
 
   /**
    * {@inheritdoc}
    */
   public function stage(array $composerCommand, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+    $this->saveInvocationArguments($composerCommand, $stagingDir);
   }
 
 }
diff --git a/tests/src/Functional/UpdaterFormTest.php b/tests/src/Functional/UpdaterFormTest.php
index 0010c52fce..92371ae2aa 100644
--- a/tests/src/Functional/UpdaterFormTest.php
+++ b/tests/src/Functional/UpdaterFormTest.php
@@ -131,6 +131,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $release_notes = $assert_session->elementExists('named', ['link', 'Release notes'], $cells[2]);
     $this->assertSame('Release notes for Drupal', $release_notes->getAttribute('title'));
     $assert_session->buttonExists('Update');
+    $this->assertUpdateStagedTimes(0);
   }
 
   /**
@@ -184,8 +185,10 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $assert_session->pageTextNotContains(static::$warningsExplanation);
     $page->pressButton('Update');
     $this->checkForMetaRefresh();
+
     // If a validator flags an error, but doesn't throw, the update should still
     // be halted.
+    $this->assertUpdateStagedTimes(0);
     $assert_session->pageTextContainsOnce('An error has occurred.');
     $page->clickLink('the error page');
     $assert_session->pageTextContainsOnce((string) $expected_results[0]->getMessages()[0]);
@@ -199,6 +202,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     TestChecker1::setTestResult($expected_results, PreCreateEvent::class);
     $page->pressButton('Update');
     $this->checkForMetaRefresh();
+    $this->assertUpdateStagedTimes(0);
     $assert_session->pageTextContainsOnce('An error has occurred.');
     $page->clickLink('the error page');
     // Since there's only one message, we shouldn't see the summary.
@@ -240,6 +244,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $this->drupalGet('/admin/modules/automatic-update');
     $page->pressButton('Update');
     $this->checkForMetaRefresh();
+    $this->assertUpdateStagedTimes(1);
 
     // Confirm we are on the confirmation page.
     $assert_session->addressEquals('/admin/automatic-update-ready');
@@ -260,7 +265,28 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
 
     // Confirm we are on the confirmation page.
     $assert_session->addressEquals('/admin/automatic-update-ready');
+    $this->assertUpdateStagedTimes(2);
     $assert_session->buttonExists('Continue');
   }
 
+  /**
+   * Asserts the number of times an update was staged.
+   *
+   * @param int $attempted_times
+   *   The expected number of times an update was staged.
+   */
+  private function assertUpdateStagedTimes(int $attempted_times): void {
+    /** @var \Drupal\package_manager_bypass\InvocationRecorderBase $beginner */
+    $beginner = $this->container->get('package_manager.beginner');
+    $this->assertCount($attempted_times, $beginner->getInvocationArguments());
+
+    /** @var \Drupal\package_manager_bypass\InvocationRecorderBase $stager */
+    $stager = $this->container->get('package_manager.stager');
+    $this->assertCount($attempted_times, $stager->getInvocationArguments());
+
+    /** @var \Drupal\package_manager_bypass\InvocationRecorderBase $committer */
+    $committer = $this->container->get('package_manager.committer');
+    $this->assertEmpty($committer->getInvocationArguments());
+  }
+
 }
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 1a42cb00d3..86d6bc494d 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -4,8 +4,6 @@ namespace Drupal\Tests\automatic_updates\Kernel;
 
 use Drupal\package_manager\PathLocator;
 use Drupal\Tests\user\Traits\UserCreationTrait;
-use PhpTuf\ComposerStager\Domain\StagerInterface;
-use Prophecy\Argument;
 
 /**
  * @coversDefaultClass \Drupal\automatic_updates\Updater
@@ -71,27 +69,32 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
 
     // When we call Updater::stage(), the stored project versions should be
     // read from state and passed to Composer Stager's Stager service, in the
-    // form of a Composer command. We set up a mock here to ensure that those
-    // calls are made as expected.
-    $stager = $this->prophesize(StagerInterface::class);
+    // form of a Composer command. This is done using package_manager_bypass's
+    // invocation recorder, rather than a regular mock, in order to test that
+    // the invocation recorder itself works.
     // The production dependencies should be updated first...
-    $command = [
+    $expected_require_arguments = [
       'require',
       'drupal/core-recommended:9.8.1',
       '--update-with-all-dependencies',
     ];
-    $stager->stage($command, Argument::cetera())->shouldBeCalled();
     // ...followed by the dev dependencies.
-    $command = [
+    $expected_require_dev_arguments = [
       'require',
       'drupal/core-dev:9.8.1',
       '--update-with-all-dependencies',
       '--dev',
     ];
-    $stager->stage($command, Argument::cetera())->shouldBeCalled();
-
-    $this->container->set('package_manager.stager', $stager->reveal());
     $this->container->get('automatic_updates.updater')->stage();
+
+    /** @var \Drupal\package_manager_bypass\InvocationRecorderBase $stager */
+    $stager = $this->container->get('package_manager.stager');
+    [
+      $actual_require_arguments,
+      $actual_require_dev_arguments,
+    ] = $stager->getInvocationArguments();
+    $this->assertSame($expected_require_arguments, $actual_require_arguments[0]);
+    $this->assertSame($expected_require_dev_arguments, $actual_require_dev_arguments[0]);
   }
 
   /**
-- 
GitLab