From d2a2705c9f55ff5f7b74e3e3cac486efc25b3fbc Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Tue, 12 Oct 2021 16:09:12 +0000
Subject: [PATCH] Issue #3238647 by phenaproxima, tedbow: Create an API for
 easily accessing relevant Composer information

---
 automatic_updates.services.yml                |  21 ++-
 composer.json                                 |   3 +-
 .../core_packages.json                        |   0
 package_manager/src/ComposerUtility.php       | 147 ++++++++++++++++++
 .../src/Kernel/CorePackageManifestTest.php    |   4 +-
 src/Event/PackagesAwareTrait.php              |  11 --
 src/Event/PreCommitEvent.php                  |  32 ++++
 src/Event/PreStartEvent.php                   |  16 ++
 src/Event/ReadinessCheckEvent.php             |  16 ++
 src/Event/UpdateEvent.php                     |  28 ++++
 src/Updater.php                               |  65 ++------
 src/Validation/ReadinessValidationManager.php |  23 +--
 src/Validator/StagedProjectsValidator.php     |  94 +++--------
 src/Validator/UpdateVersionValidator.php      |  20 +--
 tests/fixtures/fake-site/composer.json        |   5 +
 tests/fixtures/fake-site/composer.lock        |   1 +
 .../new_project_added/active/composer.json    |   1 +
 .../new_project_added/staged/composer.json    |   1 +
 .../no_errors/active/composer.json            |   1 +
 .../no_errors/composer.json                   |   1 +
 .../no_errors/staged/composer.json            |   1 +
 .../project_removed/active/composer.json      |   1 +
 .../project_removed/staged/composer.json      |   1 +
 .../version_changed/active/composer.json      |   1 +
 .../version_changed/staged/composer.json      |   1 +
 .../Functional/ReadinessValidationTest.php    |   2 +-
 .../DiskSpaceValidatorTest.php                |   3 +-
 .../PendingUpdatesValidatorTest.php           |  25 +--
 tests/src/Traits/ValidationTestTrait.php      |   4 +-
 .../src/Unit/StagedProjectsValidatorTest.php  |  66 ++++----
 30 files changed, 380 insertions(+), 215 deletions(-)
 rename core_packages.json => package_manager/core_packages.json (100%)
 create mode 100644 package_manager/src/ComposerUtility.php
 rename {tests => package_manager/tests}/src/Kernel/CorePackageManifestTest.php (95%)
 create mode 100644 tests/fixtures/fake-site/composer.json
 create mode 100644 tests/fixtures/fake-site/composer.lock
 create mode 100644 tests/fixtures/project_staged_validation/new_project_added/active/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/new_project_added/staged/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/no_errors/active/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/no_errors/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/no_errors/staged/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/project_removed/active/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/project_removed/staged/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/version_changed/active/composer.json
 create mode 100644 tests/fixtures/project_staged_validation/version_changed/staged/composer.json

diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index f831d0741a..51d5714ee9 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -1,10 +1,23 @@
 services:
   automatic_updates.readiness_validation_manager:
     class: Drupal\automatic_updates\Validation\ReadinessValidationManager
-    arguments: ['@keyvalue.expirable', '@datetime.time', '@automatic_updates.updater', '@event_dispatcher', 24]
+    arguments:
+      - '@keyvalue.expirable'
+      - '@datetime.time'
+      - '@automatic_updates.path_locator'
+      - '@event_dispatcher'
+      - 24
   automatic_updates.updater:
     class: Drupal\automatic_updates\Updater
-    arguments: ['@state', '@string_translation','@package_manager.beginner', '@package_manager.stager', '@package_manager.cleaner', '@package_manager.committer', '@event_dispatcher', '@automatic_updates.path_locator']
+    arguments:
+      - '@state'
+      - '@string_translation'
+      - '@package_manager.beginner'
+      - '@package_manager.stager'
+      - '@package_manager.cleaner'
+      - '@package_manager.committer'
+      - '@event_dispatcher'
+      - '@automatic_updates.path_locator'
   automatic_updates.cleaner:
     class: Drupal\automatic_updates\ComposerStager\Cleaner
     decorates: package_manager.cleaner
@@ -27,12 +40,12 @@ services:
       - { name: event_subscriber }
   automatic_updates.staged_projects_validator:
     class: Drupal\automatic_updates\Validator\StagedProjectsValidator
-    arguments: ['@string_translation', '@automatic_updates.path_locator']
+    arguments:
+      - '@string_translation'
     tags:
       - { name: event_subscriber }
   automatic_updates.update_version_validator:
     class: Drupal\automatic_updates\Validator\UpdateVersionValidator
-    arguments: ['@automatic_updates.updater']
     tags:
       - { name: event_subscriber }
   automatic_updates.composer_executable_validator:
diff --git a/composer.json b/composer.json
index 27ef06bb1f..388dfabb0e 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,8 @@
   },
   "require": {
     "ext-json": "*",
-    "php-tuf/composer-stager": "0.2.1"
+    "php-tuf/composer-stager": "0.2.1",
+    "composer/composer": "^2"
   },
   "config": {
     "platform": {
diff --git a/core_packages.json b/package_manager/core_packages.json
similarity index 100%
rename from core_packages.json
rename to package_manager/core_packages.json
diff --git a/package_manager/src/ComposerUtility.php b/package_manager/src/ComposerUtility.php
new file mode 100644
index 0000000000..9404cbc11f
--- /dev/null
+++ b/package_manager/src/ComposerUtility.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\package_manager;
+
+use Composer\Composer;
+use Composer\Factory;
+use Composer\IO\NullIO;
+use Drupal\Component\Serialization\Json;
+
+/**
+ * Defines a utility object to get information from Composer's API.
+ */
+class ComposerUtility {
+
+  /**
+   * The Composer instance.
+   *
+   * @var \Composer\Composer
+   */
+  protected $composer;
+
+  /**
+   * The statically cached names of the Drupal core packages.
+   *
+   * @var string[]
+   */
+  private static $corePackages;
+
+  /**
+   * Constructs a new ComposerUtility object.
+   *
+   * @param \Composer\Composer $composer
+   *   The Composer instance.
+   */
+  public function __construct(Composer $composer) {
+    $this->composer = $composer;
+  }
+
+  /**
+   * Creates a utility object using the files in a given directory.
+   *
+   * @param string $dir
+   *   The directory that contains composer.json and composer.lock.
+   *
+   * @return \Drupal\package_manager\ComposerUtility
+   *   The utility object.
+   */
+  public static function createForDirectory(string $dir): self {
+    $io = new NullIO();
+    $configuration = $dir . DIRECTORY_SEPARATOR . 'composer.json';
+
+    // The Composer factory requires that either the HOME or COMPOSER_HOME
+    // environment variables be set, so momentarily set the COMPOSER_HOME
+    // variable to the directory we're trying to create a Composer instance for.
+    // We have to do this because the Composer factory doesn't give us a way to
+    // pass the home directory in.
+    // @see \Composer\Factory::getHomeDir()
+    $home = getenv('COMPOSER_HOME');
+    putenv("COMPOSER_HOME=$dir");
+    $composer = Factory::create($io, $configuration);
+    putenv("COMPOSER_HOME=$home");
+
+    return new static($composer);
+  }
+
+  /**
+   * Returns the canonical names of the supported core packages.
+   *
+   * @return string[]
+   *   The canonical list of supported core package names, as listed in
+   *   ../core_packages.json.
+   */
+  protected static function getCorePackageList(): array {
+    if (self::$corePackages === NULL) {
+      $file = __DIR__ . '/../core_packages.json';
+      assert(file_exists($file), "$file does not exist.");
+
+      $core_packages = file_get_contents($file);
+      $core_packages = Json::decode($core_packages);
+
+      assert(is_array($core_packages), "$file did not contain a list of core packages.");
+      self::$corePackages = $core_packages;
+    }
+    return self::$corePackages;
+  }
+
+  /**
+   * Returns the names of the core packages required in composer.json.
+   *
+   * All packages listed in ../core_packages.json are considered core packages.
+   *
+   * @return string[]
+   *   The names of the required core packages.
+   *
+   * @throws \LogicException
+   *   If neither drupal/core or drupal/core-recommended are required.
+   *
+   * @todo Make this return a keyed array of packages, not just names.
+   */
+  public function getCorePackageNames(): array {
+    $requirements = array_keys($this->composer->getPackage()->getRequires());
+
+    // Ensure that either drupal/core or drupal/core-recommended are required.
+    // If neither is, then core cannot be updated, which we consider an error
+    // condition.
+    // @todo Move this check to an update validator as part of
+    //   https://www.drupal.org/project/automatic_updates/issues/3241105
+    $core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements);
+    if (empty($core_requirements)) {
+      $file = $this->composer->getConfig()->getConfigSource()->getName();
+      throw new \LogicException("Drupal core does not appear to be required in $file.");
+    }
+
+    return array_intersect(static::getCorePackageList(), $requirements);
+  }
+
+  /**
+   * Returns all Drupal extension packages in the lock file.
+   *
+   * The following package types are considered Drupal extension packages:
+   * drupal-module, drupal-theme, drupal-custom-module, and drupal-custom-theme.
+   *
+   * @return \Composer\Package\PackageInterface[]
+   *   All Drupal extension packages in the lock file, keyed by name.
+   */
+  public function getDrupalExtensionPackages(): array {
+    $locked_packages = $this->composer->getLocker()
+      ->getLockedRepository(TRUE)
+      ->getPackages();
+
+    $drupal_package_types = [
+      'drupal-module',
+      'drupal-theme',
+      'drupal-custom-module',
+      'drupal-custom-theme',
+    ];
+    $drupal_packages = [];
+    foreach ($locked_packages as $package) {
+      if (in_array($package->getType(), $drupal_package_types, TRUE)) {
+        $key = $package->getName();
+        $drupal_packages[$key] = $package;
+      }
+    }
+    return $drupal_packages;
+  }
+
+}
diff --git a/tests/src/Kernel/CorePackageManifestTest.php b/package_manager/tests/src/Kernel/CorePackageManifestTest.php
similarity index 95%
rename from tests/src/Kernel/CorePackageManifestTest.php
rename to package_manager/tests/src/Kernel/CorePackageManifestTest.php
index eaa0080b42..5d82264e8a 100644
--- a/tests/src/Kernel/CorePackageManifestTest.php
+++ b/package_manager/tests/src/Kernel/CorePackageManifestTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\Tests\automatic_updates\Kernel;
+namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\KernelTests\KernelTestBase;
@@ -18,7 +18,7 @@ use Symfony\Component\Finder\Finder;
  *   For example, the list could live in core/assets, and this test could live
  *   in the Drupal\Tests\Composer namespace.
  *
- * @group automatic_updates
+ * @group package_manager
  */
 class CorePackageManifestTest extends KernelTestBase {
 
diff --git a/src/Event/PackagesAwareTrait.php b/src/Event/PackagesAwareTrait.php
index 2462b217ef..49ead03861 100644
--- a/src/Event/PackagesAwareTrait.php
+++ b/src/Event/PackagesAwareTrait.php
@@ -14,17 +14,6 @@ trait PackagesAwareTrait {
    */
   protected $packageVersions;
 
-  /**
-   * Constructs a PreStartEvent.
-   *
-   * @param string[] $package_versions
-   *   (optional) The desired package versions to update to, keyed by package
-   *   name.
-   */
-  public function __construct(array $package_versions = []) {
-    $this->packageVersions = $package_versions;
-  }
-
   /**
    * Returns the desired package versions to update to.
    *
diff --git a/src/Event/PreCommitEvent.php b/src/Event/PreCommitEvent.php
index 1f9ed03833..1bffb85f5b 100644
--- a/src/Event/PreCommitEvent.php
+++ b/src/Event/PreCommitEvent.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\automatic_updates\Event;
 
+use Drupal\package_manager\ComposerUtility;
+
 /**
  * Event fired before staged changes are copied into the active site.
  */
@@ -9,4 +11,34 @@ class PreCommitEvent extends UpdateEvent {
 
   use ExcludedPathsTrait;
 
+  /**
+   * The Composer utility object for the stage directory.
+   *
+   * @var \Drupal\package_manager\ComposerUtility
+   */
+  protected $stageComposer;
+
+  /**
+   * Constructs a new PreCommitEvent object.
+   *
+   * @param \Drupal\package_manager\ComposerUtility $active_composer
+   *   A Composer utility object for the active directory.
+   * @param \Drupal\package_manager\ComposerUtility $stage_composer
+   *   A Composer utility object for the stage directory.
+   */
+  public function __construct(ComposerUtility $active_composer, ComposerUtility $stage_composer) {
+    parent::__construct($active_composer);
+    $this->stageComposer = $stage_composer;
+  }
+
+  /**
+   * Returns a Composer utility object for the stage directory.
+   *
+   * @return \Drupal\package_manager\ComposerUtility
+   *   The Composer utility object for the stage directory.
+   */
+  public function getStageComposer(): ComposerUtility {
+    return $this->stageComposer;
+  }
+
 }
diff --git a/src/Event/PreStartEvent.php b/src/Event/PreStartEvent.php
index a58b422eda..d8ce9ec7be 100644
--- a/src/Event/PreStartEvent.php
+++ b/src/Event/PreStartEvent.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\automatic_updates\Event;
 
+use Drupal\package_manager\ComposerUtility;
+
 /**
  * Event fired before an update begins.
  */
@@ -10,4 +12,18 @@ class PreStartEvent extends UpdateEvent {
   use ExcludedPathsTrait;
   use PackagesAwareTrait;
 
+  /**
+   * Constructs a PreStartEvent object.
+   *
+   * @param \Drupal\package_manager\ComposerUtility $active_composer
+   *   A Composer utility object for the active directory.
+   * @param string[] $package_versions
+   *   (optional) The desired package versions to update to, keyed by package
+   *   name.
+   */
+  public function __construct(ComposerUtility $active_composer, array $package_versions = []) {
+    parent::__construct($active_composer);
+    $this->packageVersions = $package_versions;
+  }
+
 }
diff --git a/src/Event/ReadinessCheckEvent.php b/src/Event/ReadinessCheckEvent.php
index 862a4581ca..4c165cd972 100644
--- a/src/Event/ReadinessCheckEvent.php
+++ b/src/Event/ReadinessCheckEvent.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\automatic_updates\Event;
 
+use Drupal\package_manager\ComposerUtility;
+
 /**
  * Event fired when checking if the site could perform an update.
  */
@@ -9,4 +11,18 @@ class ReadinessCheckEvent extends UpdateEvent {
 
   use PackagesAwareTrait;
 
+  /**
+   * Constructs a ReadinessCheckEvent object.
+   *
+   * @param \Drupal\package_manager\ComposerUtility $active_composer
+   *   A Composer utility object for the active directory.
+   * @param string[] $package_versions
+   *   (optional) The desired package versions to update to, keyed by package
+   *   name.
+   */
+  public function __construct(ComposerUtility $active_composer, array $package_versions = []) {
+    parent::__construct($active_composer);
+    $this->packageVersions = $package_versions;
+  }
+
 }
diff --git a/src/Event/UpdateEvent.php b/src/Event/UpdateEvent.php
index be7eace54d..0fb3f30fbd 100644
--- a/src/Event/UpdateEvent.php
+++ b/src/Event/UpdateEvent.php
@@ -4,6 +4,7 @@ namespace Drupal\automatic_updates\Event;
 
 use Drupal\automatic_updates\Validation\ValidationResult;
 use Drupal\Component\EventDispatcher\Event;
+use Drupal\package_manager\ComposerUtility;
 
 /**
  * Event fired when a site is updating.
@@ -21,6 +22,33 @@ class UpdateEvent extends Event {
    */
   protected $results = [];
 
+  /**
+   * The Composer utility object for the active directory.
+   *
+   * @var \Drupal\package_manager\ComposerUtility
+   */
+  protected $activeComposer;
+
+  /**
+   * Constructs a new UpdateEvent object.
+   *
+   * @param \Drupal\package_manager\ComposerUtility $active_composer
+   *   A Composer utility object for the active directory.
+   */
+  public function __construct(ComposerUtility $active_composer) {
+    $this->activeComposer = $active_composer;
+  }
+
+  /**
+   * Returns a Composer utility object for the active directory.
+   *
+   * @return \Drupal\package_manager\ComposerUtility
+   *   The Composer utility object for the active directory.
+   */
+  public function getActiveComposer(): ComposerUtility {
+    return $this->activeComposer;
+  }
+
   /**
    * Adds a validation result.
    *
diff --git a/src/Updater.php b/src/Updater.php
index 05d1d4f6a2..4cb0b90a64 100644
--- a/src/Updater.php
+++ b/src/Updater.php
@@ -6,10 +6,10 @@ use Drupal\automatic_updates\Event\PreCommitEvent;
 use Drupal\automatic_updates\Event\PreStartEvent;
 use Drupal\automatic_updates\Event\UpdateEvent;
 use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\Component\Serialization\Json;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\package_manager\ComposerUtility;
 use Drupal\system\SystemManager;
 use PhpTuf\ComposerStager\Domain\BeginnerInterface;
 use PhpTuf\ComposerStager\Domain\CleanerInterface;
@@ -141,62 +141,19 @@ class Updater {
     if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) {
       throw new \InvalidArgumentException("Currently only updates to Drupal core are supported.");
     }
+
+    $composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory());
     $packages = [];
-    foreach ($this->getCorePackageNames() as $package) {
+    foreach ($composer->getCorePackageNames() as $package) {
       $packages[$package] = $project_versions['drupal'];
     }
     $stage_key = $this->createActiveStage($packages);
     /** @var \Drupal\automatic_updates\Event\PreStartEvent $event */
-    $event = $this->dispatchUpdateEvent(new PreStartEvent($packages), AutomaticUpdatesEvents::PRE_START);
+    $event = $this->dispatchUpdateEvent(new PreStartEvent($composer, $packages), AutomaticUpdatesEvents::PRE_START);
     $this->beginner->begin($this->pathLocator->getActiveDirectory(), $this->pathLocator->getStageDirectory(), $this->getExclusions($event));
     return $stage_key;
   }
 
-  /**
-   * Returns the names of the core packages in the project composer.json.
-   *
-   * The following packages are considered core packages:
-   * - drupal/core;
-   * - drupal/core-recommended;
-   * - drupal/core-vendor-hardening;
-   * - drupal/core-composer-scaffold; and
-   * - drupal/core-project-message.
-   *
-   * @return string[]
-   *   The names of the core packages.
-   *
-   * @throws \RuntimeException
-   *   If the project composer.json is not found.
-   * @throws \LogicException
-   *   If the project composer.json does not contain drupal/core or
-   *   drupal/core-recommended.
-   *
-   * @todo Move this to an update validator, or use a more robust method of
-   *   detecting the core packages.
-   */
-  public function getCorePackageNames(): array {
-    $composer = realpath($this->pathLocator->getProjectRoot() . '/composer.json');
-
-    if (empty($composer) || !file_exists($composer)) {
-      throw new \RuntimeException("Could not find project-level composer.json");
-    }
-
-    $data = file_get_contents($composer);
-    $data = Json::decode($data);
-
-    // Ensure that either drupal/core or drupal/core-recommended are required
-    // by the project. If neither is, then core will not be updated, and we
-    // consider that an error condition.
-    $requirements = array_keys($data['require']);
-    $core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements);
-    if (empty($core_requirements)) {
-      throw new \LogicException("Drupal core does not appear to be required in $composer.");
-    }
-
-    $list = file_get_contents(__DIR__ . '/../core_packages.json');
-    return array_intersect(Json::decode($list), $requirements);
-  }
-
   /**
    * Gets the excluded paths collected by an event object.
    *
@@ -237,10 +194,16 @@ class Updater {
    * Commits the current update.
    */
   public function commit(): void {
+    $active_dir = $this->pathLocator->getActiveDirectory();
+    $active_composer = ComposerUtility::createForDirectory($active_dir);
+
+    $stage_dir = $this->pathLocator->getStageDirectory();
+    $stage_composer = ComposerUtility::createForDirectory($stage_dir);
+
     /** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */
-    $event = $this->dispatchUpdateEvent(new PreCommitEvent(), AutomaticUpdatesEvents::PRE_COMMIT);
-    $this->committer->commit($this->pathLocator->getStageDirectory(), $this->pathLocator->getActiveDirectory(), $this->getExclusions($event));
-    $this->dispatchUpdateEvent(new UpdateEvent(), AutomaticUpdatesEvents::POST_COMMIT);
+    $event = $this->dispatchUpdateEvent(new PreCommitEvent($active_composer, $stage_composer), AutomaticUpdatesEvents::PRE_COMMIT);
+    $this->committer->commit($stage_dir, $active_dir, $this->getExclusions($event));
+    $this->dispatchUpdateEvent(new UpdateEvent($active_composer), AutomaticUpdatesEvents::POST_COMMIT);
   }
 
   /**
diff --git a/src/Validation/ReadinessValidationManager.php b/src/Validation/ReadinessValidationManager.php
index 7a69520eda..23ab82eefc 100644
--- a/src/Validation/ReadinessValidationManager.php
+++ b/src/Validation/ReadinessValidationManager.php
@@ -4,10 +4,11 @@ namespace Drupal\automatic_updates\Validation;
 
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
 use Drupal\automatic_updates\Event\ReadinessCheckEvent;
-use Drupal\automatic_updates\Updater;
+use Drupal\automatic_updates\PathLocator;
 use Drupal\automatic_updates\UpdateRecommender;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
+use Drupal\package_manager\ComposerUtility;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
@@ -44,11 +45,11 @@ class ReadinessValidationManager {
   protected $resultsTimeToLive;
 
   /**
-   * The updater service.
+   * The path locator service.
    *
-   * @var \Drupal\automatic_updates\Updater
+   * @var \Drupal\automatic_updates\PathLocator
    */
-  protected $updater;
+  protected $pathLocator;
 
   /**
    * Constructs a ReadinessValidationManager.
@@ -57,17 +58,17 @@ class ReadinessValidationManager {
    *   The key/value expirable factory.
    * @param \Drupal\Component\Datetime\TimeInterface $time
    *   The time service.
-   * @param \Drupal\automatic_updates\Updater $updater
-   *   The updater service.
+   * @param \Drupal\automatic_updates\PathLocator $path_locator
+   *   The path locator service.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
    *   The event dispatcher service.
    * @param int $results_time_to_live
    *   The number of hours to store results.
    */
-  public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, Updater $updater, EventDispatcherInterface $dispatcher, int $results_time_to_live) {
+  public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, PathLocator $path_locator, EventDispatcherInterface $dispatcher, int $results_time_to_live) {
     $this->keyValueExpirable = $key_value_expirable_factory->get('automatic_updates');
     $this->time = $time;
-    $this->updater = $updater;
+    $this->pathLocator = $path_locator;
     $this->eventDispatcher = $dispatcher;
     $this->resultsTimeToLive = $results_time_to_live;
   }
@@ -78,10 +79,12 @@ class ReadinessValidationManager {
    * @return $this
    */
   public function run(): self {
+    $composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory());
+
     $recommender = new UpdateRecommender();
     $release = $recommender->getRecommendedRelease(TRUE);
     if ($release) {
-      $core_packages = $this->updater->getCorePackageNames();
+      $core_packages = $composer->getCorePackageNames();
       // Update all core packages to the same version.
       $package_versions = array_fill(0, count($core_packages), $release->getVersion());
       $package_versions = array_combine($core_packages, $package_versions);
@@ -89,7 +92,7 @@ class ReadinessValidationManager {
     else {
       $package_versions = [];
     }
-    $event = new ReadinessCheckEvent($package_versions);
+    $event = new ReadinessCheckEvent($composer, $package_versions);
     $this->eventDispatcher->dispatch($event, AutomaticUpdatesEvents::READINESS_CHECK);
     $results = $event->getResults();
     $this->keyValueExpirable->setWithExpire(
diff --git a/src/Validator/StagedProjectsValidator.php b/src/Validator/StagedProjectsValidator.php
index 72fa7519cb..455c965764 100644
--- a/src/Validator/StagedProjectsValidator.php
+++ b/src/Validator/StagedProjectsValidator.php
@@ -3,11 +3,8 @@
 namespace Drupal\automatic_updates\Validator;
 
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
-use Drupal\automatic_updates\Event\UpdateEvent;
-use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\automatic_updates\PathLocator;
+use Drupal\automatic_updates\Event\PreCommitEvent;
 use Drupal\automatic_updates\Validation\ValidationResult;
-use Drupal\Component\Serialization\Json;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -19,81 +16,32 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
-  /**
-   * The path locator service.
-   *
-   * @var \Drupal\automatic_updates\PathLocator
-   */
-  protected $pathLocator;
-
   /**
    * Constructs a StagedProjectsValidation object.
    *
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
    *   The translation service.
-   * @param \Drupal\automatic_updates\PathLocator $path_locator
-   *   The path locator service.
    */
-  public function __construct(TranslationInterface $translation, PathLocator $path_locator) {
+  public function __construct(TranslationInterface $translation) {
     $this->setStringTranslation($translation);
-    $this->pathLocator = $path_locator;
-  }
-
-  /**
-   * Gets the Drupal packages in a composer.lock file.
-   *
-   * @param string $composer_lock_file
-   *   The composer.lock file location.
-   *
-   * @return array[]
-   *   The Drupal packages' information, as stored in the lock file, keyed by
-   *   package name.
-   */
-  private function getDrupalPackagesFromLockFile(string $composer_lock_file): array {
-    if (!file_exists($composer_lock_file)) {
-      $result = ValidationResult::createError([
-        $this->t("composer.lock file '@lock_file' not found.", ['@lock_file' => $composer_lock_file]),
-      ]);
-      throw new UpdateException(
-        [$result],
-        'The staged packages could not be evaluated because composer.lock file not found.'
-      );
-    }
-    $composer_lock = file_get_contents($composer_lock_file);
-    $drupal_packages = [];
-    $data = Json::decode($composer_lock);
-    $drupal_package_types = [
-      'drupal-module',
-      'drupal-theme',
-      'drupal-custom-module',
-      'drupal-custom-theme',
-    ];
-    $packages = $data['packages'] ?? [];
-    $packages = array_merge($packages, $data['packages-dev'] ?? []);
-    foreach ($packages as $package) {
-      if (in_array($package['type'], $drupal_package_types, TRUE)) {
-        $drupal_packages[$package['name']] = $package;
-      }
-    }
-
-    return $drupal_packages;
   }
 
   /**
    * Validates the staged packages.
    *
-   * @param \Drupal\automatic_updates\Event\UpdateEvent $event
-   *   The update event.
+   * @param \Drupal\automatic_updates\Event\PreCommitEvent $event
+   *   The event object.
    */
-  public function validateStagedProjects(UpdateEvent $event): void {
+  public function validateStagedProjects(PreCommitEvent $event): void {
     try {
-      $active_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getActiveDirectory() . "/composer.lock");
-      $staged_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getStageDirectory() . "/composer.lock");
+      $active_packages = $event->getActiveComposer()->getDrupalExtensionPackages();
+      $staged_packages = $event->getStageComposer()->getDrupalExtensionPackages();
     }
-    catch (UpdateException $e) {
-      foreach ($e->getValidationResults() as $result) {
-        $event->addValidationResult($result);
-      }
+    catch (\Throwable $e) {
+      $result = ValidationResult::createError([
+        $e->getMessage(),
+      ]);
+      $event->addValidationResult($result);
       return;
     }
 
@@ -111,8 +59,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
         $new_packages_messages[] = $this->t(
           "@type '@name' installed.",
           [
-            '@type' => $type_map[$new_package['type']],
-            '@name' => $new_package['name'],
+            '@type' => $type_map[$new_package->getType()],
+            '@name' => $new_package->getName(),
           ]
         );
       }
@@ -131,8 +79,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
         $removed_packages_messages[] = $this->t(
           "@type '@name' removed.",
           [
-            '@type' => $type_map[$removed_package['type']],
-            '@name' => $removed_package['name'],
+            '@type' => $type_map[$removed_package->getType()],
+            '@name' => $removed_package->getName(),
           ]
         );
       }
@@ -149,14 +97,14 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
     if ($pre_existing_packages = array_diff_key($staged_packages, $removed_packages, $new_packages)) {
       foreach ($pre_existing_packages as $package_name => $staged_existing_package) {
         $active_package = $active_packages[$package_name];
-        if ($staged_existing_package['version'] !== $active_package['version']) {
+        if ($staged_existing_package->getVersion() !== $active_package->getVersion()) {
           $version_change_messages[] = $this->t(
             "@type '@name' from @active_version to  @staged_version.",
             [
-              '@type' => $type_map[$active_package['type']],
-              '@name' => $active_package['name'],
-              '@staged_version' => $staged_existing_package['version'],
-              '@active_version' => $active_package['version'],
+              '@type' => $type_map[$active_package->getType()],
+              '@name' => $active_package->getName(),
+              '@staged_version' => $staged_existing_package->getPrettyVersion(),
+              '@active_version' => $active_package->getPrettyVersion(),
             ]
           );
         }
diff --git a/src/Validator/UpdateVersionValidator.php b/src/Validator/UpdateVersionValidator.php
index 5a4f7d3638..df3521bf9b 100644
--- a/src/Validator/UpdateVersionValidator.php
+++ b/src/Validator/UpdateVersionValidator.php
@@ -5,7 +5,6 @@ namespace Drupal\automatic_updates\Validator;
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
 use Drupal\automatic_updates\Event\ReadinessCheckEvent;
 use Drupal\automatic_updates\Event\UpdateEvent;
-use Drupal\automatic_updates\Updater;
 use Drupal\automatic_updates\Validation\ValidationResult;
 use Drupal\Core\Extension\ExtensionVersion;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -18,23 +17,6 @@ class UpdateVersionValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
-  /**
-   * The updater service.
-   *
-   * @var \Drupal\automatic_updates\Updater
-   */
-  protected $updater;
-
-  /**
-   * Constructs an UpdateVersionSubscriber.
-   *
-   * @param \Drupal\automatic_updates\Updater $updater
-   *   The updater service.
-   */
-  public function __construct(Updater $updater) {
-    $this->updater = $updater;
-  }
-
   /**
    * Returns the running core version, according to the Update module.
    *
@@ -58,7 +40,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
    */
   public function checkUpdateVersion(UpdateEvent $event): void {
     $from_version = ExtensionVersion::createFromVersionString($this->getCoreVersion());
-    $core_package_names = $this->updater->getCorePackageNames();
+    $core_package_names = $event->getActiveComposer()->getCorePackageNames();
     // All the core packages will be updated to the same version, so it doesn't
     // matter which specific package we're looking at.
     $core_package_name = reset($core_package_names);
diff --git a/tests/fixtures/fake-site/composer.json b/tests/fixtures/fake-site/composer.json
new file mode 100644
index 0000000000..74d8204d88
--- /dev/null
+++ b/tests/fixtures/fake-site/composer.json
@@ -0,0 +1,5 @@
+{
+    "require": {
+        "drupal/core": "*"
+    }
+}
diff --git a/tests/fixtures/fake-site/composer.lock b/tests/fixtures/fake-site/composer.lock
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/fake-site/composer.lock
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/new_project_added/active/composer.json b/tests/fixtures/project_staged_validation/new_project_added/active/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/new_project_added/active/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json b/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/active/composer.json b/tests/fixtures/project_staged_validation/no_errors/active/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/no_errors/active/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/composer.json b/tests/fixtures/project_staged_validation/no_errors/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/no_errors/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/no_errors/staged/composer.json b/tests/fixtures/project_staged_validation/no_errors/staged/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/no_errors/staged/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/project_removed/active/composer.json b/tests/fixtures/project_staged_validation/project_removed/active/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/project_removed/active/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/project_removed/staged/composer.json b/tests/fixtures/project_staged_validation/project_removed/staged/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/project_removed/staged/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/version_changed/active/composer.json b/tests/fixtures/project_staged_validation/version_changed/active/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/version_changed/active/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/fixtures/project_staged_validation/version_changed/staged/composer.json b/tests/fixtures/project_staged_validation/version_changed/staged/composer.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/version_changed/staged/composer.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/src/Functional/ReadinessValidationTest.php b/tests/src/Functional/ReadinessValidationTest.php
index 999067ce79..4db87cdf04 100644
--- a/tests/src/Functional/ReadinessValidationTest.php
+++ b/tests/src/Functional/ReadinessValidationTest.php
@@ -382,7 +382,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase {
 
     $text = $this->getSession()->getPage()->find(
       'css',
-      "h3#$section ~ details.system-status-report__entry:contains('Update readiness checks')",
+      "h3#$section ~ details.system-status-report__entry:contains('Update readiness checks')"
     )->getText();
     $this->assertStringMatchesFormat($format, $text);
   }
diff --git a/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php b/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php
index 90ce7ab3a5..fa860f115c 100644
--- a/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php
@@ -208,7 +208,8 @@ class DiskSpaceValidatorTest extends KernelTestBase {
     $this->validator->sharedDisk = $shared_disk;
     $this->validator->freeSpace = array_map([Bytes::class, 'toNumber'], $free_space);
 
-    $event = new UpdateEvent();
+    $composer = $this->createMock('\Drupal\package_manager\ComposerUtility');
+    $event = new UpdateEvent($composer);
     $this->validator->checkDiskSpace($event);
     $this->assertValidationResultsEqual($expected_results, $event->getResults());
   }
diff --git a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
index a304b7aad7..7254942eee 100644
--- a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
@@ -26,6 +26,13 @@ class PendingUpdatesValidatorTest extends KernelTestBase {
     'update',
   ];
 
+  /**
+   * The update event object that will be dispatched.
+   *
+   * @var \Drupal\automatic_updates\Event\UpdateEvent
+   */
+  private $event;
+
   /**
    * {@inheritdoc}
    */
@@ -42,16 +49,18 @@ class PendingUpdatesValidatorTest extends KernelTestBase {
     $this->container->get('keyvalue')
       ->get('post_update')
       ->set('existing_updates', $updates);
+
+    $composer = $this->createMock('\Drupal\package_manager\ComposerUtility');
+    $this->event = new UpdateEvent($composer);
   }
 
   /**
    * Tests that no error is raised if there are no pending updates.
    */
   public function testNoPendingUpdates(): void {
-    $event = new UpdateEvent();
     $this->container->get('automatic_updates.pending_updates_validator')
-      ->checkPendingUpdates($event);
-    $this->assertEmpty($event->getResults());
+      ->checkPendingUpdates($this->event);
+    $this->assertEmpty($this->event->getResults());
   }
 
   /**
@@ -66,10 +75,9 @@ class PendingUpdatesValidatorTest extends KernelTestBase {
 
     $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']);
 
-    $event = new UpdateEvent();
     $this->container->get('automatic_updates.pending_updates_validator')
-      ->checkPendingUpdates($event);
-    $this->assertValidationResultsEqual([$result], $event->getResults());
+      ->checkPendingUpdates($this->event);
+    $this->assertValidationResultsEqual([$result], $this->event->getResults());
   }
 
   /**
@@ -80,10 +88,9 @@ class PendingUpdatesValidatorTest extends KernelTestBase {
 
     $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']);
 
-    $event = new UpdateEvent();
     $this->container->get('automatic_updates.pending_updates_validator')
-      ->checkPendingUpdates($event);
-    $this->assertValidationResultsEqual([$result], $event->getResults());
+      ->checkPendingUpdates($this->event);
+    $this->assertValidationResultsEqual([$result], $this->event->getResults());
   }
 
 }
diff --git a/tests/src/Traits/ValidationTestTrait.php b/tests/src/Traits/ValidationTestTrait.php
index 7f313f23c9..70e97bc594 100644
--- a/tests/src/Traits/ValidationTestTrait.php
+++ b/tests/src/Traits/ValidationTestTrait.php
@@ -46,7 +46,7 @@ trait ValidationTestTrait {
       $this->testResults["checker_$listener_number"]['1 error 1 warning'] = [
         "$listener_number:error" => ValidationResult::createError(
           [t("$listener_number:OMG 🔌. Some one unplugged the server! How is this site even running?")],
-          t("$listener_number:Summary: 🔥"),
+          t("$listener_number:Summary: 🔥")
         ),
         "$listener_number:warning" => ValidationResult::createWarning(
           [t("$listener_number:It looks like it going to rain and your server is outside.")],
@@ -82,7 +82,7 @@ trait ValidationTestTrait {
       $this->testResults["checker_$listener_number"]['1 warning'] = [
         ValidationResult::createWarning(
           [t("$listener_number:This is your one and only warning. You have been warned.")],
-          t("$listener_number:No need for this summary with only 1 warning."),
+          t("$listener_number:No need for this summary with only 1 warning.")
         ),
       ];
     }
diff --git a/tests/src/Unit/StagedProjectsValidatorTest.php b/tests/src/Unit/StagedProjectsValidatorTest.php
index 6180fd9941..3c26777206 100644
--- a/tests/src/Unit/StagedProjectsValidatorTest.php
+++ b/tests/src/Unit/StagedProjectsValidatorTest.php
@@ -2,13 +2,12 @@
 
 namespace Drupal\Tests\automatic_updates\Unit;
 
-use Drupal\automatic_updates\Event\UpdateEvent;
-use Drupal\automatic_updates\PathLocator;
+use Drupal\automatic_updates\Event\PreCommitEvent;
 use Drupal\automatic_updates\Validator\StagedProjectsValidator;
-use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\package_manager\ComposerUtility;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -18,26 +17,42 @@ use Drupal\Tests\UnitTestCase;
  */
 class StagedProjectsValidatorTest extends UnitTestCase {
 
+  /**
+   * Creates a pre-commit event object for testing.
+   *
+   * @param string $active_dir
+   *   The active directory.
+   * @param string $stage_dir
+   *   The stage directory.
+   *
+   * @return \Drupal\automatic_updates\Event\PreCommitEvent
+   *   The event object.
+   */
+  private function createEvent(string $active_dir, string $stage_dir): PreCommitEvent {
+    return new PreCommitEvent(
+      ComposerUtility::createForDirectory($active_dir),
+      ComposerUtility::createForDirectory($stage_dir)
+    );
+  }
+
   /**
    * Tests that if an exception is thrown, the update event will absorb it.
    */
   public function testUpdateEventConsumesExceptionResults(): void {
-    $prefix = FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR;
-    $active_dir = uniqid($prefix);
-    $stage_dir = uniqid($prefix);
+    $message = 'An exception thrown by Composer at runtime.';
 
-    $locator = $this->prophesize(PathLocator::class);
-    $locator->getActiveDirectory()->willReturn($active_dir);
-    $locator->getStageDirectory()->willReturn($stage_dir);
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
+    $composer = $this->prophesize(ComposerUtility::class);
+    $composer->getDrupalExtensionPackages()
+      ->willThrow(new \RuntimeException($message));
+    $event = new PreCommitEvent($composer->reveal(), $composer->reveal());
 
-    $event = new UpdateEvent();
+    $validator = new StagedProjectsValidator(new TestTranslationManager());
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
     $this->assertCount(1, $results);
     $messages = reset($results)->getMessages();
     $this->assertCount(1, $messages);
-    $this->assertSame("composer.lock file '$active_dir/composer.lock' not found.", (string) reset($messages));
+    $this->assertSame($message, (string) reset($messages));
   }
 
   /**
@@ -56,14 +71,11 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    * @covers ::validateStagedProjects
    */
   public function testErrors(string $fixtures_dir, string $expected_summary, array $expected_messages): void {
-    $locator = $this->prophesize(PathLocator::class);
     $this->assertNotEmpty($fixtures_dir);
     $this->assertDirectoryExists($fixtures_dir);
 
-    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $locator->getStageDirectory()->willReturn("$fixtures_dir/staged");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
-    $event = new UpdateEvent();
+    $event = $this->createEvent("$fixtures_dir/active", "$fixtures_dir/staged");
+    $validator = new StagedProjectsValidator(new TestTranslationManager());
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
     $this->assertCount(1, $results);
@@ -121,11 +133,8 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    */
   public function testNoErrors(): void {
     $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors');
-    $locator = $this->prophesize(PathLocator::class);
-    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $locator->getStageDirectory()->willReturn("$fixtures_dir/staged");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
-    $event = new UpdateEvent();
+    $event = $this->createEvent("$fixtures_dir/active", "$fixtures_dir/staged");
+    $validator = new StagedProjectsValidator(new TestTranslationManager());
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
     $this->assertIsArray($results);
@@ -137,19 +146,14 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    */
   public function testNoLockFile(): void {
     $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors');
-    $locator = $this->prophesize(PathLocator::class);
-    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $locator->getStageDirectory()->willReturn("$fixtures_dir");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
-    $event = new UpdateEvent();
+
+    $event = $this->createEvent("$fixtures_dir/active", $fixtures_dir);
+    $validator = new StagedProjectsValidator(new TestTranslationManager());
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
     $this->assertCount(1, $results);
     $result = array_pop($results);
-    $this->assertMatchesRegularExpression(
-      "/.*automatic_updates\/tests\/fixtures\/project_staged_validation\/no_errors\/composer.lock' not found/",
-        (string) $result->getMessages()[0]
-      );
+    $this->assertSame("No lockfile found. Unable to read locked packages", (string) $result->getMessages()[0]);
     $this->assertSame('', (string) $result->getSummary());
   }
 
-- 
GitLab