From 1a75de3524859fb0c15679960af4b53df215251f Mon Sep 17 00:00:00 2001
From: Ted Bowman <41201-tedbow@users.noreply.drupalcode.org>
Date: Thu, 26 Jan 2023 12:43:03 +0000
Subject: [PATCH] Issue #3336255 by tedbow, Wim Leers: Explore simplifying our
 fixture manipulation classes

---
 .../src/Functional/DisplayUpdatesTest.php     |   9 +-
 .../fixture_manipulator.services.yml          |   5 +
 .../src/FixtureManipulator.php                |  43 +++++-
 .../src/StageFixtureManipulator.php           | 122 +++++++++++-------
 .../package_manager_bypass/src/Beginner.php   |  45 -------
 .../src/Kernel/FixtureManipulatorTest.php     | 105 ++++-----------
 ...OverwriteExistingPackagesValidatorTest.php |   4 +-
 .../Kernel/PackageManagerKernelTestBase.php   |  10 ++
 .../Kernel/SupportedReleaseValidatorTest.php  |   4 +-
 .../src/Traits/FixtureManipulatorTrait.php    |  20 +++
 .../AutomaticUpdatesFunctionalTestBase.php    |   8 +-
 .../Functional/DeleteExistingUpdateTest.php   |  23 +---
 .../Functional/StagedDatabaseUpdateTest.php   |   5 +-
 tests/src/Functional/StatusCheckTest.php      |   5 +-
 .../StatusCheckerRunAfterUpdateTest.php       |   5 +-
 tests/src/Functional/SuccessfulUpdateTest.php |   5 +-
 .../Functional/UpdateCompleteMessageTest.php  |   5 +-
 tests/src/Functional/UpdateFailedTest.php     |   9 +-
 tests/src/Functional/UpdateLockTest.php       |   6 +-
 tests/src/Kernel/CronUpdaterTest.php          |  25 +---
 .../StatusCheck/CronServerValidatorTest.php   |   9 +-
 .../RequestedUpdateValidatorTest.php          |  10 +-
 .../ScaffoldFilePermissionsValidatorTest.php  |   7 +-
 .../StagedDatabaseUpdateValidatorTest.php     |  17 +--
 .../StagedProjectsValidatorTest.php           |  21 ++-
 .../Kernel/StatusCheck/StatusCheckerTest.php  |   5 +-
 tests/src/Kernel/UpdaterTest.php              |   5 +-
 27 files changed, 222 insertions(+), 315 deletions(-)
 create mode 100644 package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml
 create mode 100644 package_manager/tests/src/Traits/FixtureManipulatorTrait.php

diff --git a/automatic_updates_extensions/tests/src/Functional/DisplayUpdatesTest.php b/automatic_updates_extensions/tests/src/Functional/DisplayUpdatesTest.php
index 4d0007a081..c4d214a5bf 100644
--- a/automatic_updates_extensions/tests/src/Functional/DisplayUpdatesTest.php
+++ b/automatic_updates_extensions/tests/src/Functional/DisplayUpdatesTest.php
@@ -4,8 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates_extensions\Functional;
 
-use Drupal\fixture_manipulator\StageFixtureManipulator;
-
 /**
  * @covers \Drupal\automatic_updates_extensions\Form\UpdaterForm
  * @group automatic_updates_extensions
@@ -67,10 +65,9 @@ class DisplayUpdatesTest extends UpdaterFormTestBase {
       $page->checkField('projects[aaa_update_test]');
     }
     $page->checkField('projects[semver_test]');
-    $stage_manipulator = new StageFixtureManipulator();
-    $stage_manipulator->setVersion('drupal/aaa_update_test', '2.1.0')
-      ->setVersion('drupal/semver_test', '8.1.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()
+      ->setVersion('drupal/aaa_update_test', '2.1.0')
+      ->setVersion('drupal/semver_test', '8.1.1');
     $page->pressButton('Update');
     $this->checkForMetaRefresh();
     $this->assertUpdateStagedTimes(1);
diff --git a/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml b/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml
new file mode 100644
index 0000000000..862187ca84
--- /dev/null
+++ b/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml
@@ -0,0 +1,5 @@
+services:
+  fixture_manipulator.stage_manipulator:
+    class: Drupal\fixture_manipulator\StageFixtureManipulator
+    arguments: ['@state', '@fixture_manipulator.stage_manipulator.inner']
+    decorates: 'package_manager.beginner'
diff --git a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
index 50791685f5..ddd9ab635a 100644
--- a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
+++ b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
@@ -59,7 +59,7 @@ class FixtureManipulator {
    */
   public function addPackage(array $package, bool $is_dev_requirement = FALSE, bool $create_project = TRUE): self {
     if (!$this->committingChanges) {
-      $this->manipulatorArguments['addPackage'][] = func_get_args();
+      $this->queueManipulation('addPackage', func_get_args());
       return $this;
     }
     foreach (['name', 'type'] as $required_key) {
@@ -102,7 +102,7 @@ class FixtureManipulator {
    */
   public function modifyPackage(string $name, array $package): self {
     if (!$this->committingChanges) {
-      $this->manipulatorArguments['modifyPackage'][] = func_get_args();
+      $this->queueManipulation('modifyPackage', func_get_args());
       return $this;
     }
     $this->setPackage($name, $package, TRUE);
@@ -131,7 +131,7 @@ class FixtureManipulator {
    */
   public function removePackage(string $name): self {
     if (!$this->committingChanges) {
-      $this->manipulatorArguments['removePackage'][] = func_get_args();
+      $this->queueManipulation('removePackage', func_get_args());
       return $this;
     }
     $this->setPackage($name, NULL, TRUE);
@@ -267,7 +267,7 @@ class FixtureManipulator {
    */
   public function addProjectAtPath(string $path, ?string $project_name = NULL, ?string $file_name = NULL): self {
     if (!$this->committingChanges) {
-      $this->manipulatorArguments['addProjectAtPath'][] = func_get_args();
+      $this->queueManipulation('addProjectAtPath', func_get_args());
       return $this;
     }
     $path = $this->dir . "/$path";
@@ -319,8 +319,8 @@ class FixtureManipulator {
     }
     $this->dir = $dir;
     $this->committingChanges = TRUE;
-    $manipulator_arguments = $this->manipulatorArguments;
-    $this->manipulatorArguments = [];
+    $manipulator_arguments = $this->getQueuedManipulationItems();
+    $this->clearQueuedManipulationItems();
     foreach ($manipulator_arguments as $method => $argument_sets) {
       foreach ($argument_sets as $argument_set) {
         $this->{$method}(...$argument_set);
@@ -356,7 +356,7 @@ class FixtureManipulator {
    */
   public function addDotGitFolder(string $path): self {
     if (!$this->committingChanges) {
-      $this->manipulatorArguments['addDotGitFolder'][] = func_get_args();
+      $this->queueManipulation('addDotGitFolder', func_get_args());
       return $this;
     }
     $fs = new Filesystem();
@@ -370,4 +370,33 @@ class FixtureManipulator {
     return $this;
   }
 
+  /**
+   * Queues manipulation arguments to be called in ::doCommitChanges().
+   *
+   * @param string $method
+   *   The method name.
+   * @param array $arguments
+   *   The arguments.
+   */
+  protected function queueManipulation(string $method, array $arguments): void {
+    $this->manipulatorArguments[$method][] = $arguments;
+  }
+
+  /**
+   * Clears all queued manipulation items.
+   */
+  protected function clearQueuedManipulationItems(): void {
+    $this->manipulatorArguments = [];
+  }
+
+  /**
+   * Gets all queued manipulation items.
+   *
+   * @return array
+   *   The queued manipulation items as set by calls to ::queueManipulation().
+   */
+  protected function getQueuedManipulationItems(): array {
+    return $this->manipulatorArguments;
+  }
+
 }
diff --git a/package_manager/tests/modules/fixture_manipulator/src/StageFixtureManipulator.php b/package_manager/tests/modules/fixture_manipulator/src/StageFixtureManipulator.php
index 74e1fb20f8..03667d4dac 100644
--- a/package_manager/tests/modules/fixture_manipulator/src/StageFixtureManipulator.php
+++ b/package_manager/tests/modules/fixture_manipulator/src/StageFixtureManipulator.php
@@ -4,78 +4,106 @@ declare(strict_types = 1);
 
 namespace Drupal\fixture_manipulator;
 
-use Drupal\package_manager_bypass\Beginner;
+use Drupal\Core\State\StateInterface;
+use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface;
+use PhpTuf\ComposerStager\Domain\Service\ProcessOutputCallback\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ProcessRunnerInterface;
+use PhpTuf\ComposerStager\Domain\Value\Path\PathInterface;
+use PhpTuf\ComposerStager\Domain\Value\PathList\PathListInterface;
 
 /**
- * A fixture manipulator for the stage directory.
+ * A fixture manipulator service that commits changes after begin.
  */
-final class StageFixtureManipulator extends FixtureManipulator {
+final class StageFixtureManipulator extends FixtureManipulator implements BeginnerInterface {
 
   /**
-   * Whether the fixture is ready to commit.
+   * The state key to use.
+   */
+  private const STATE_KEY = __CLASS__ . 'MANIPULATOR_ARGUMENTS';
+
+  /**
+   * The state service.
    *
-   * @var bool
+   * @var \Drupal\Core\State\StateInterface
    */
-  private $ready = FALSE;
+  private StateInterface $state;
+
+  /**
+   * The decorated service.
+   *
+   * @var \PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface
+   */
+  private BeginnerInterface $inner;
+
+  /**
+   * Constructions a StageFixtureManipulator object.
+   *
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
+   * @param \PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface $inner
+   *   The decorated beginner service.
+   */
+  public function __construct(StateInterface $state, BeginnerInterface $inner) {
+    $this->state = $state;
+    $this->inner = $inner;
+  }
 
   /**
    * {@inheritdoc}
    */
-  public function commitChanges(string $dir = NULL): void {
-    if (!$this->ready) {
-      throw new \LogicException("::setReadyToCommit must be called before ::commitChanges");
-    }
-    if (!$dir) {
-      throw new \UnexpectedValueException("$dir must be specific for a StageFixtureManipulator");
-    }
-    parent::doCommitChanges($dir);
-    $this->committed = TRUE;
-
-    // In a kernel test, the Beginner runs in the same PHP process as the test,
-    // so there's no need for extra logic to inform the test runner that the
-    // queued stage fixture manipulations have been committed. In functional
-    // tests, however, we do need to pass information back from the system under
-    // test to the test runner.
-    // @see \Drupal\Core\CoreServiceProvider::registerTest()
-    $in_functional_test = defined('DRUPAL_TEST_IN_CHILD_SITE');
-    if ($in_functional_test) {
-      // Relay "committed" state to the test runner by re-serializing to state.
-      Beginner::setStageManipulator($this);
+  public function begin(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = ProcessRunnerInterface::DEFAULT_TIMEOUT): void {
+    $this->inner->begin($activeDir, $stagingDir, $exclusions, $callback, $timeout);
+    if ($this->getQueuedManipulationItems()) {
+      $this->doCommitChanges($stagingDir->resolve());
     }
   }
 
   /**
-   * Sets the manipulator as ready to commit.
+   * {@inheritdoc}
    */
-  public function setReadyToCommit(): void {
-    $this->ready = TRUE;
-    Beginner::setStageManipulator($this);
+  public function commitChanges(string $dir): void {
+    throw new \BadMethodCallException('::commitChanges() should not be called directly in StageFixtureManipulator().');
   }
 
   /**
    * {@inheritdoc}
    */
   public function __destruct() {
-    if (!$this->ready) {
-      throw new \LogicException('This fixture manipulator was not yet ready to commit! Please call setReadyToCommit() to signal all necessary changes are queued.');
-    }
+    // Overrides `__destruct` because the staged fixture manipulator service
+    // will be destroyed after every request.
+    // @see \Drupal\fixture_manipulator\StageFixtureManipulator::handleTearDown()
+  }
 
-    // Update the state to match reality, because ::commitChanges() *should*
-    // have been called by \Drupal\package_manager_bypass\Beginner::begin(). The
-    // "committed" flag will already be set in kernel tests, because there the
-    // test runner and the system under test live in the same PHP process.
-    // Note that this will never run for the system under test, because
-    // $this->committed will always be set. Ensure we do this only
-    // functional tests by checking for the presence of a container.
-    if (!$this->committed && \Drupal::hasContainer()) {
-      $sut = \Drupal::state()->get(Beginner::class . '-stage-manipulator', NULL);
-      if ($sut) {
-        $this->committed = $sut->committed;
-      }
+  /**
+   * Handles test tear down to ensure all changes were committed.
+   */
+  public static function handleTearDown() {
+    if (!empty(\Drupal::state()->get(self::STATE_KEY))) {
+      throw new \LogicException('The StageFixtureManipulator has arguments that were not cleared. This likely means that the PostCreateEvent was never fired.');
     }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function queueManipulation(string $method, array $arguments): void {
+    $stored_arguments = $this->getQueuedManipulationItems();
+    $stored_arguments[$method][] = $arguments;
+    $this->state->set(self::STATE_KEY, $stored_arguments);
+  }
 
-    // Proceed regular destruction (which will complain if it's not committed).
-    parent::__destruct();
+  /**
+   * {@inheritdoc}
+   */
+  protected function clearQueuedManipulationItems(): void {
+    $this->state->delete(self::STATE_KEY);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getQueuedManipulationItems(): array {
+    return $this->state->get(self::STATE_KEY, []);
   }
 
 }
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 5c980230bc..d19ba474b7 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\package_manager_bypass;
 
 use Drupal\Core\State\StateInterface;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface;
 use PhpTuf\ComposerStager\Domain\Service\ProcessOutputCallback\ProcessOutputCallbackInterface;
 use PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ProcessRunnerInterface;
@@ -37,56 +36,12 @@ class Beginner extends BypassedStagerServiceBase implements BeginnerInterface {
     $this->inner = $inner;
   }
 
-  /**
-   * A reference to the stage fixture manipulator, if any.
-   *
-   * Without this, StageFixtureManipulator::__destruct() would run too early:
-   * before the test has finished running,
-   *
-   * @var \Drupal\fixture_manipulator\StageFixtureManipulator|null
-   */
-  private static $manipulatorReference;
-
   /**
    * {@inheritdoc}
    */
   public function begin(PathInterface $activeDir, PathInterface $stagingDir, ?PathListInterface $exclusions = NULL, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = ProcessRunnerInterface::DEFAULT_TIMEOUT): void {
     $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions, $timeout);
     $this->inner->begin($activeDir, $stagingDir, $exclusions, $callback, $timeout);
-
-    /** @var \Drupal\fixture_manipulator\StageFixtureManipulator|null $stageManipulator */
-    $stageManipulator = $this->state->get(__CLASS__ . '-stage-manipulator', NULL);
-    if ($stageManipulator) {
-      $stageManipulator->commitChanges($stagingDir->resolve());
-    }
-  }
-
-  /**
-   * Sets the manipulator for the stage.
-   *
-   * @param \Drupal\fixture_manipulator\StageFixtureManipulator $manipulator
-   *   The manipulator.
-   */
-  public static function setStageManipulator(StageFixtureManipulator &$manipulator): void {
-    if (isset(self::$manipulatorReference)) {
-      throw new \Exception('Stage manipulator already set.');
-    }
-    // Keep a reference to the stage fixture manipulator.
-    self::$manipulatorReference = $manipulator;
-    \Drupal::state()->set(__CLASS__ . '-stage-manipulator', $manipulator);
-  }
-
-  /**
-   * Destroys references to the tracked manipulator.
-   *
-   * Without this, StageFixtureManipulator::__destruct() would run too late:
-   * after the database connection is destroyed, and hence it would fail.
-   *
-   * @see \Drupal\Tests\automatic_updates\Functional\AutomaticUpdatesFunctionalTestBase::tearDown()
-   * @see \Drupal\fixture_manipulator\StageFixtureManipulator::__destruct()
-   */
-  public function destroy() {
-    self::$manipulatorReference = NULL;
   }
 
 }
diff --git a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
index 4153fc4468..5e5549f06b 100644
--- a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
+++ b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
@@ -6,9 +6,6 @@ namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\fixture_manipulator\FixtureManipulator;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
-use Drupal\package_manager_bypass\Beginner;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactory;
 use Symfony\Component\Filesystem\Filesystem;
 
 /**
@@ -25,6 +22,13 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
    */
   private string $dir;
 
+  /**
+   * The exception expected in ::tearDown() of this test.
+   *
+   * @var \Exception
+   */
+  private \Exception $expectedTearDownException;
+
   /**
    * The existing packages in the fixture.
    *
@@ -320,76 +324,6 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       ->setVersion('drupal/core', '1.2.3');
   }
 
-  /**
-   * Test that an exception is thrown if ::readyToCommit() is not called.
-   */
-  public function testStageManipulatorSetReadyToCommitExpected(): void {
-    $this->expectException(\LogicException::class);
-    $this->expectExceptionMessage('This fixture manipulator was not yet ready to commit! Please call setReadyToCommit() to signal all necessary changes are queued.');
-
-    $manipulator = new StageFixtureManipulator();
-    $manipulator->setVersion('drupal/core', '1.2.3');
-  }
-
-  /**
-   * Test that an exception is thrown if ::commitChanges() is not called.
-   *
-   * @dataProvider stageManipulatorUsageStyles
-   */
-  public function testStageManipulatorNoCommitError(bool $immediately_destroyed): void {
-    $this->expectException(\LogicException::class);
-    $this->expectExceptionMessage('commitChanges() must be called.');
-
-    if ($immediately_destroyed) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
-    }
-    else {
-      $manipulator = new StageFixtureManipulator();
-      $manipulator->setVersion('drupal/core', '1.2.3');
-      $manipulator->setReadyToCommit();
-    }
-
-    // Ensure \Drupal\fixture_manipulator\StageFixtureManipulator::__destruct()
-    // is triggered.
-    $this->container->get('state')->delete(Beginner::class . '-stage-manipulator');
-    $this->container->get('package_manager.beginner')->destroy();
-  }
-
-  /**
-   * Test no exceptions are thrown if StageFixtureManipulator is used correctly.
-   *
-   * @dataProvider stageManipulatorUsageStyles
-   */
-  public function testStageManipulatorSatisfied(bool $immediately_destroyed): void {
-    if ($immediately_destroyed) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
-    }
-    else {
-      $manipulator = new StageFixtureManipulator();
-      $manipulator->setVersion('drupal/core', '1.2.3');
-      $manipulator->setReadyToCommit();
-    }
-
-    $path_locator = $this->container->get('package_manager.path_locator');
-    // We need to set the vendor directory's permissions first because, in the
-    // test project, it's located inside the project root.
-    $this->assertTrue(chmod($path_locator->getVendorDirectory(), 0777));
-    $this->assertTrue(chmod($path_locator->getProjectRoot(), 0777));
-    $stage_path = $path_locator->getStagingRoot() . '/stage' . $this->databasePrefix;
-
-    // Simulate a stage beginning, which would commit the changes.
-    // @see \Drupal\package_manager_bypass\Beginner::begin()
-    $path_factory = new PathFactory();
-    $this->container->get('package_manager.beginner')->begin(
-      $path_factory->create($path_locator->getProjectRoot()),
-      $path_factory->create($stage_path),
-    );
-  }
-
   /**
    * @covers ::addDotGitFolder
    */
@@ -419,11 +353,26 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     }
   }
 
-  public function stageManipulatorUsageStyles() {
-    return [
-      'immediately destroyed' => [TRUE],
-      'variable' => [FALSE],
-    ];
+  /**
+   * Tests that the stage manipulator throws an exception if not committed.
+   */
+  public function testStagedFixtureNotCommitted(): void {
+    $this->expectedTearDownException = new \LogicException('The StageFixtureManipulator has arguments that were not cleared. This likely means that the PostCreateEvent was never fired.');
+    $this->getStageFixtureManipulator()->setVersion('any-org/any-package', '3.2.1');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function tearDown(): void {
+    try {
+      parent::tearDown();
+    }
+    catch (\Exception $exception) {
+      if (!(get_class($exception) === get_class($this->expectedTearDownException) && $exception->getMessage() === $this->expectedTearDownException->getMessage())) {
+        throw $exception;
+      }
+    }
   }
 
 }
diff --git a/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php b/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
index 40b4224795..cb54dc937c 100644
--- a/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
@@ -43,7 +42,7 @@ class OverwriteExistingPackagesValidatorTest extends PackageManagerKernelTestBas
       ->addProjectAtPath('modules/module_2')
       ->addProjectAtPath('modules/module_5')
       ->commitChanges();
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
 
     // module_1 and module_2 will raise errors because they would overwrite
     // non-Composer managed paths in the active directory.
@@ -109,7 +108,6 @@ class OverwriteExistingPackagesValidatorTest extends PackageManagerKernelTestBas
       'version' => '1.3.0',
       'type' => 'metapackage',
     ]);
-    $stage_manipulator->setReadyToCommit();
 
     $expected_results = [
       ValidationResult::createError([
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 8e7be4213d..c4e46d9473 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\package_manager\Kernel;
 use Drupal\Component\FileSystem\FileSystem as DrupalFileSystem;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Site\Settings;
+use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\StageEvent;
@@ -16,6 +17,7 @@ use Drupal\package_manager\Validator\DiskSpaceValidator;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\Stage;
 use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
+use Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
 use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
 use GuzzleHttp\Client;
@@ -35,6 +37,7 @@ use Symfony\Component\Filesystem\Filesystem;
 abstract class PackageManagerKernelTestBase extends KernelTestBase {
 
   use AssertPreconditionsTrait;
+  use FixtureManipulatorTrait;
   use FixtureUtilityTrait;
   use StatusCheckTrait;
   use ValidationTestTrait;
@@ -374,6 +377,13 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
     }, $event_class, $priority - 1);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function tearDown(): void {
+    StageFixtureManipulator::handleTearDown();
+  }
+
 }
 
 /**
diff --git a/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php b/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
index 9830ab0ff0..f2f1e61c27 100644
--- a/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
+++ b/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
@@ -213,7 +212,7 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
   public function testException(array $release_metadata, bool $project_in_active, array $package, array $expected_results): void {
     $this->setReleaseMetadata(['drupal' => __DIR__ . '/../../fixtures/release-history/drupal.9.8.2.xml'] + $release_metadata);
 
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
     if ($project_in_active) {
       $stage_manipulator->setVersion($package['name'], $package['version']);
     }
@@ -224,7 +223,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
     // module as it's of type 'drupal-library'.
     // @see \Drupal\package_manager\Validator\SupportedReleaseValidator::checkStagedReleases()
     $stage_manipulator->setVersion('drupal/dependency', '9.8.1');
-    $stage_manipulator->setReadyToCommit();
     $this->assertResults($expected_results, PreApplyEvent::class);
   }
 
diff --git a/package_manager/tests/src/Traits/FixtureManipulatorTrait.php b/package_manager/tests/src/Traits/FixtureManipulatorTrait.php
new file mode 100644
index 0000000000..f8428cc5dc
--- /dev/null
+++ b/package_manager/tests/src/Traits/FixtureManipulatorTrait.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Traits;
+
+/**
+ * A trait for common fixture manipulator functions.
+ */
+trait FixtureManipulatorTrait {
+
+  /**
+   * Gets the stage fixture manipulator service.
+   *
+   * @return \Drupal\fixture_manipulator\StageFixtureManipulator|object|null
+   *   The stage fixture manipulator service.
+   */
+  protected function getStageFixtureManipulator() {
+    return $this->container->get('fixture_manipulator.stage_manipulator');
+  }
+
+}
diff --git a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
index c123e14354..46858ebdd4 100644
--- a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
+++ b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
@@ -6,8 +6,10 @@ namespace Drupal\Tests\automatic_updates\Functional;
 
 use Drupal\automatic_updates\CronUpdater;
 use Drupal\Core\Site\Settings;
+use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
+use Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -19,6 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase {
 
   use AssertPreconditionsTrait;
+  use FixtureManipulatorTrait;
   use FixtureUtilityTrait;
 
   /**
@@ -76,14 +79,11 @@ abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase {
    * {@inheritdoc}
    */
   protected function tearDown(): void {
+    StageFixtureManipulator::handleTearDown();
     $service_ids = [
       // If automatic_updates is installed, ensure any stage directory created
       // during the test is cleaned up.
       'automatic_updates.updater',
-      // Ensure that \Drupal\package_manager_bypass\Beginner's ::commitChanges()
-      // call makes its way back to the test.
-      // @see \Drupal\fixture_manipulator\StageFixtureManipulator::__destruct()
-      'package_manager.beginner',
     ];
     foreach ($service_ids as $service_id) {
       if ($this->container->has($service_id)) {
diff --git a/tests/src/Functional/DeleteExistingUpdateTest.php b/tests/src/Functional/DeleteExistingUpdateTest.php
index 1c335a5d5d..83cf1dbbe0 100644
--- a/tests/src/Functional/DeleteExistingUpdateTest.php
+++ b/tests/src/Functional/DeleteExistingUpdateTest.php
@@ -6,7 +6,6 @@ namespace Drupal\Tests\automatic_updates\Functional;
 
 use Drupal\automatic_updates_test\Datetime\TestTime;
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\system\SystemManager;
 
@@ -21,9 +20,7 @@ class DeleteExistingUpdateTest extends UpdaterFormTestBase {
    * Tests deleting an existing update.
    */
   public function testDeleteExistingUpdate(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $conflict_message = 'Cannot begin an update because another Composer operation is currently in progress.';
     $cancelled_message = 'The update was successfully cancelled.';
 
@@ -51,13 +48,9 @@ class DeleteExistingUpdateTest extends UpdaterFormTestBase {
     $assert_session->addressEquals('/admin/reports/updates/update');
     $assert_session->pageTextContains($cancelled_message);
     $assert_session->pageTextNotContains($conflict_message);
-    // Ensure that all traces of the manipulator for the first stage are erased.
-    $this->container->get('package_manager.beginner')->destroy();
 
     // Ensure we can start another update after deleting the existing one.
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
 
@@ -77,12 +70,8 @@ class DeleteExistingUpdateTest extends UpdaterFormTestBase {
     $page->pressButton('Delete existing update');
     $assert_session->pageTextContains('Staged update deleted');
     $assert_session->pageTextNotContains($conflict_message);
-    // Ensure that all traces of the manipulator for the 2nd stage are erased.
-    $this->container->get('package_manager.beginner')->destroy();
     // Before pressing the button, create a new stage fixture manipulator.
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateReady('9.8.1');
@@ -123,17 +112,13 @@ class DeleteExistingUpdateTest extends UpdaterFormTestBase {
     $page->pressButton('Delete existing update');
     $assert_session->statusCodeEquals(200);
     $assert_session->pageTextContains('Staged update deleted');
-    // Ensure that all traces of the manipulator for the 3rd stage are erased.
-    $this->container->get('package_manager.beginner')->destroy();
 
     // If a legitimate error is raised during pre-apply, we should be able to
     // delete the staged update right away.
     $results = [$this->createValidationResult(SystemManager::REQUIREMENT_ERROR)];
     TestSubscriber1::setTestResult($results, PreApplyEvent::class);
     // Before pressing the button, create a new stage fixture manipulator.
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $page->pressButton('Update to 9.8.1');
     $this->checkForMetaRefresh();
     $this->assertUpdateReady('9.8.1');
diff --git a/tests/src/Functional/StagedDatabaseUpdateTest.php b/tests/src/Functional/StagedDatabaseUpdateTest.php
index c88b8193e0..837d3beb74 100644
--- a/tests/src/Functional/StagedDatabaseUpdateTest.php
+++ b/tests/src/Functional/StagedDatabaseUpdateTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Functional;
 
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Drupal\package_manager_test_validation\StagedDatabaseUpdateValidator;
@@ -43,9 +42,7 @@ class StagedDatabaseUpdateTest extends UpdaterFormTestBase {
    * @requires PHP >= 8.0
    */
   public function testStagedDatabaseUpdates(bool $maintenance_mode_on): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $this->mockActiveCoreVersion('9.8.0');
     $this->checkForUpdates();
     $this->container->get('theme_installer')
diff --git a/tests/src/Functional/StatusCheckTest.php b/tests/src/Functional/StatusCheckTest.php
index d915ae10e0..8fa61c0cab 100644
--- a/tests/src/Functional/StatusCheckTest.php
+++ b/tests/src/Functional/StatusCheckTest.php
@@ -11,7 +11,6 @@ use Drupal\automatic_updates_test\Datetime\TestTime;
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
 use Drupal\automatic_updates_test2\EventSubscriber\TestSubscriber2;
 use Drupal\Core\Url;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber;
 use Drupal\system\SystemManager;
@@ -470,9 +469,7 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase {
    * Tests that stored validation results are deleted after an update.
    */
   public function testStoredResultsClearedAfterUpdate(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
     $this->drupalLogin($this->checkerRunnerUser);
diff --git a/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php b/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php
index 17f28df5cf..79e76e8841 100644
--- a/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php
+++ b/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Functional;
 
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Drupal\package_manager\ValidationResult;
 
@@ -40,9 +39,7 @@ class StatusCheckerRunAfterUpdateTest extends UpdaterFormTestBase {
    * @requires PHP >= 8.0
    */
   public function testStatusCheckerRunAfterUpdate(bool $has_database_updates) {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $assert_session = $this->assertSession();
     $this->mockActiveCoreVersion('9.8.0');
     $this->checkForUpdates();
diff --git a/tests/src/Functional/SuccessfulUpdateTest.php b/tests/src/Functional/SuccessfulUpdateTest.php
index 86bf86f94f..eefbbfc226 100644
--- a/tests/src/Functional/SuccessfulUpdateTest.php
+++ b/tests/src/Functional/SuccessfulUpdateTest.php
@@ -4,7 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 
 /**
@@ -52,9 +51,7 @@ class SuccessfulUpdateTest extends UpdaterFormTestBase {
    * @dataProvider providerSuccessfulUpdate
    */
   public function testSuccessfulUpdate(string $update_form_url, bool $maintenance_mode_on): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $assert_session = $this->assertSession();
     $this->mockActiveCoreVersion('9.8.0');
     $this->checkForUpdates();
diff --git a/tests/src/Functional/UpdateCompleteMessageTest.php b/tests/src/Functional/UpdateCompleteMessageTest.php
index 2ff8b99a0a..332516f59d 100644
--- a/tests/src/Functional/UpdateCompleteMessageTest.php
+++ b/tests/src/Functional/UpdateCompleteMessageTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Functional;
 
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PostApplyEvent;
 
 /**
@@ -37,9 +36,7 @@ class UpdateCompleteMessageTest extends UpdaterFormTestBase {
    * @dataProvider providerUpdateCompleteMessage
    */
   public function testUpdateCompleteMessage(bool $maintenance_mode_on): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $assert_session = $this->assertSession();
     $this->mockActiveCoreVersion('9.8.0');
     $this->checkForUpdates();
diff --git a/tests/src/Functional/UpdateFailedTest.php b/tests/src/Functional/UpdateFailedTest.php
index 73ee85f34e..90f8db4747 100644
--- a/tests/src/Functional/UpdateFailedTest.php
+++ b/tests/src/Functional/UpdateFailedTest.php
@@ -4,7 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager_bypass\Committer;
 
 /**
@@ -18,9 +17,7 @@ class UpdateFailedTest extends UpdaterFormTestBase {
    * Tests that an exception is thrown if a previous apply failed.
    */
   public function testMarkerFileFailure(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $session = $this->getSession();
     $assert_session = $this->assertSession();
     $page = $session->getPage();
@@ -58,9 +55,7 @@ class UpdateFailedTest extends UpdaterFormTestBase {
    * Tests what happens when a staged update is deleted without being destroyed.
    */
   public function testStagedUpdateDeletedImproperly(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $this->mockActiveCoreVersion('9.8.0');
     $this->checkForUpdates();
 
diff --git a/tests/src/Functional/UpdateLockTest.php b/tests/src/Functional/UpdateLockTest.php
index 165746ad62..bffca33080 100644
--- a/tests/src/Functional/UpdateLockTest.php
+++ b/tests/src/Functional/UpdateLockTest.php
@@ -4,8 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
-use Drupal\fixture_manipulator\StageFixtureManipulator;
-
 /**
  * Tests that only one Automatic Update operation can be performed at a time.
  *
@@ -44,9 +42,7 @@ class UpdateLockTest extends AutomaticUpdatesFunctionalTestBase {
    * Tests that only user who started an update can continue through it.
    */
   public function testLock(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $page = $this->getSession()->getPage();
     $assert_session = $this->assertSession();
     $this->mockActiveCoreVersion('9.8.0');
diff --git a/tests/src/Kernel/CronUpdaterTest.php b/tests/src/Kernel/CronUpdaterTest.php
index 4938c65cd9..bc257e02bf 100644
--- a/tests/src/Kernel/CronUpdaterTest.php
+++ b/tests/src/Kernel/CronUpdaterTest.php
@@ -9,7 +9,6 @@ use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Url;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PostApplyEvent;
 use Drupal\package_manager\Event\PostCreateEvent;
 use Drupal\package_manager\Event\PostDestroyEvent;
@@ -147,9 +146,7 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
   public function testUpdaterCalled(string $setting, array $release_data, bool $will_update): void {
     $version = strpos($release_data['drupal'], '9.8.2') ? '9.8.2' : '9.8.1';
     if ($will_update) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion($version)
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion($version);
     }
     // Our form alter does not refresh information on available updates, so
     // ensure that the appropriate update data is loaded beforehand.
@@ -256,9 +253,7 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
     // If the failure happens before the stage is even created, the stage
     // fixture need not be manipulated.
     if ($event_class !== PreCreateEvent::class) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     }
     $this->installConfig('automatic_updates');
     // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443
@@ -448,9 +443,7 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
    * Tests that email is sent when an unattended update succeeds.
    */
   public function testEmailOnSuccess(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $this->container->get('cron')->run();
 
     // Ensure we sent a success message to all recipients.
@@ -498,9 +491,7 @@ END;
     // If the failure happens before the stage is even created, the stage
     // fixture need not be manipulated.
     if ($event_class !== PreCreateEvent::class) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.2')
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.2');
     }
     $this->setReleaseMetadata([
       'drupal' => __DIR__ . '/../../../package_manager/tests/fixtures/release-history/drupal.9.8.2.xml',
@@ -546,9 +537,7 @@ END;
     // If the failure happens before the stage is even created, the stage
     // fixture need not be manipulated.
     if ($event_class !== PreCreateEvent::class) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     }
     $results = [
       ValidationResult::createError([t('Error while updating!')]),
@@ -579,9 +568,7 @@ END;
    * Tests the failure e-mail when an unattended update fails to apply.
    */
   public function testApplyFailureEmail(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $error = new \Exception('I drink your milkshake!');
     Committer::setException($error);
 
diff --git a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
index b6360b1d0c..8d7870d2e4 100644
--- a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
@@ -8,7 +8,6 @@ use Drupal\automatic_updates\CronUpdater;
 use Drupal\automatic_updates\Validator\CronServerValidator;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Url;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
@@ -102,9 +101,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
     // validation results happen before the stage is even created: in either
     // case the stage fixture need not be manipulated.
     if ($cron_mode !== CronUpdater::DISABLED && empty($expected_results)) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     }
     $request = $this->container->get('request_stack')->getCurrentRequest();
     $this->assertNotEmpty($request);
@@ -161,9 +158,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
     // If CronUpdater is disabled, a stage will never be created, hence
     // stage fixture need not be manipulated.
     if ($cron_mode !== CronUpdater::DISABLED) {
-      (new StageFixtureManipulator())
-        ->setCorePackageVersion('9.8.1')
-        ->setReadyToCommit();
+      $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     }
     $request = $this->container->get('request_stack')->getCurrentRequest();
     $this->assertNotEmpty($request);
diff --git a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
index 88d83c99b2..e4066a0534 100644
--- a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
@@ -4,7 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
@@ -33,9 +32,7 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
     // are expected to be updated when updating Drupal core.
     // @see \Drupal\automatic_updates\Updater::begin()
     // @see \Drupal\package_manager\ComposerUtility::getCorePackages()
-    (new StageFixtureManipulator())
-      ->setVersion('drupal/core-recommended', '9.8.2')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setVersion('drupal/core-recommended', '9.8.2');
     $this->setCoreVersion('9.8.0');
     $this->setReleaseMetadata([
       'drupal' => __DIR__ . '/../../../../package_manager/tests/fixtures/release-history/drupal.9.8.1-security.xml',
@@ -64,11 +61,10 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
    * Tests error message is shown if there are no core packages in stage.
    */
   public function testErrorMessageOnEmptyCorePackages(): void {
-    (new StageFixtureManipulator())
+    $this->getStageFixtureManipulator()
       ->removePackage('drupal/core')
       ->removePackage('drupal/core-recommended')
-      ->removePackage('drupal/core-dev')
-      ->setReadyToCommit();
+      ->removePackage('drupal/core-dev');
 
     $this->setCoreVersion('9.8.0');
     $this->setReleaseMetadata([
diff --git a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
index 9392224e59..5fa1700026 100644
--- a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\ValidationResult;
@@ -310,7 +309,7 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas
         ],
       ])
       ->commitChanges();
-    (new StageFixtureManipulator())
+    $this->getStageFixtureManipulator()
       ->setCorePackageVersion('9.8.1')
       ->modifyPackage('drupal/core', [
         'extra' => [
@@ -318,8 +317,8 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas
             'file-mapping' => $staged_scaffold_files,
           ],
         ],
-      ])
-      ->setReadyToCommit();
+      ]);
+
     // Create fake scaffold files so we can test scenarios in which a scaffold
     // file that exists in the active directory is deleted in the stage
     // directory.
diff --git a/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
index ad4a764d09..0ad80d316b 100644
--- a/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\Core\Logger\RfcLogLevel;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 use ColinODell\PsrTestLogger\TestLogger;
@@ -94,9 +93,7 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
    * Tests that no errors are raised if the stage has no DB updates.
    */
   public function testNoUpdates(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $this->container->get('cron')->run();
     $this->assertFalse($this->logger->hasRecords((string) RfcLogLevel::ERROR));
   }
@@ -110,9 +107,7 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
    * @dataProvider providerSuffixes
    */
   public function testFileDeleted(string $suffix): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $listener = function (PreApplyEvent $event) use ($suffix): void {
       $stage_dir = $event->getStage()->getStageDirectory();
       foreach ($this->extensions as $name => $path) {
@@ -135,9 +130,7 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
    * @dataProvider providerSuffixes
    */
   public function testFileChanged(string $suffix): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $listener = function (PreApplyEvent $event) use ($suffix): void {
       $stage_dir = $event->getStage()->getStageDirectory();
       foreach ($this->extensions as $name => $path) {
@@ -160,9 +153,7 @@ class StagedDatabaseUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
    * @dataProvider providerSuffixes
    */
   public function testFileAdded(string $suffix): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $listener = function () use ($suffix): void {
       $active_dir = $this->container->get('package_manager.path_locator')
         ->getProjectRoot();
diff --git a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
index 5ccbc1da0e..26b70642cf 100644
--- a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
@@ -103,7 +102,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       )
       ->commitChanges();
 
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
     $stage_manipulator
       ->setCorePackageVersion('9.8.1')
       ->addPackage([
@@ -139,8 +138,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
         TRUE
       )
       ->removePackage('other/removed')
-      ->removePackage('other/dev-removed')
-      ->setReadyToCommit();
+      ->removePackage('other/dev-removed');
 
     $messages = [
       t("module 'drupal/test_module2' installed."),
@@ -211,15 +209,14 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       )
       ->commitChanges();
 
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
     $stage_manipulator->removePackage('drupal/test_theme')
       ->removePackage('drupal/dev-test_theme')
     // The validator shouldn't complain about these packages being removed,
     // since it only cares about Drupal modules and themes.
       ->removePackage('other/removed')
       ->removePackage('other/dev-removed')
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+      ->setCorePackageVersion('9.8.1');
 
     $messages = [
       t("theme 'drupal/test_theme' removed."),
@@ -274,15 +271,14 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       )
       ->commitChanges();
 
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
     $stage_manipulator->setVersion('drupal/test_module', '1.3.1')
       ->setVersion('drupal/dev-test_module', '1.3.1')
     // The validator shouldn't complain about these packages being updated,
     // because it only cares about Drupal modules and themes.
       ->setVersion('other/changed', '1.3.2')
       ->setVersion('other/dev-changed', '1.3.2')
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+      ->setCorePackageVersion('9.8.1');
 
     $messages = [
       t("module 'drupal/test_module' from 1.3.0 to 1.3.1."),
@@ -351,7 +347,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       )
       ->commitChanges();
 
-    $stage_manipulator = new StageFixtureManipulator();
+    $stage_manipulator = $this->getStageFixtureManipulator();
     $stage_manipulator->setCorePackageVersion('9.8.1')
     // The validator shouldn't care what happens to these packages, since it
     // only concerns itself with Drupal modules and themes.
@@ -373,8 +369,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->setVersion('other/changed', '1.3.2')
       ->setVersion('other/dev-changed', '1.3.2')
       ->removePackage('other/removed')
-      ->removePackage('other/dev-removed')
-      ->setReadyToCommit();
+      ->removePackage('other/dev-removed');
 
     $updater = $this->container->get('automatic_updates.updater');
     $updater->begin(['drupal' => '9.8.1']);
diff --git a/tests/src/Kernel/StatusCheck/StatusCheckerTest.php b/tests/src/Kernel/StatusCheck/StatusCheckerTest.php
index da97efb329..9890d223c9 100644
--- a/tests/src/Kernel/StatusCheck/StatusCheckerTest.php
+++ b/tests/src/Kernel/StatusCheck/StatusCheckerTest.php
@@ -8,7 +8,6 @@ use Drupal\automatic_updates\CronUpdater;
 use Drupal\automatic_updates\Updater;
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
 use Drupal\automatic_updates_test2\EventSubscriber\TestSubscriber2;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Drupal\system\SystemManager;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
@@ -220,9 +219,7 @@ class StatusCheckerTest extends AutomaticUpdatesKernelTestBase {
    * Tests that stored validation results are deleted after an update.
    */
   public function testStoredResultsDeletedPostApply(): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     $this->setCoreVersion('9.8.0');
     $this->setReleaseMetadata([
       'drupal' => __DIR__ . '/../../../../package_manager/tests/fixtures/release-history/drupal.9.8.1-security.xml',
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 7c24da9ca3..75b3999254 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -6,7 +6,6 @@ namespace Drupal\Tests\automatic_updates\Kernel;
 
 use Drupal\automatic_updates\Exception\UpdateException;
 use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
@@ -181,9 +180,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
    * @dataProvider providerCommitException
    */
   public function testCommitException(string $thrown_class, string $expected_class = NULL): void {
-    (new StageFixtureManipulator())
-      ->setCorePackageVersion('9.8.1')
-      ->setReadyToCommit();
+    $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
 
     $updater = $this->container->get('automatic_updates.updater');
     $updater->begin([
-- 
GitLab