From a941773cb0fad28bb4e3ea7028ea9059feb95c32 Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Fri, 17 Dec 2021 16:03:49 +0000
Subject: [PATCH] Issue #3254616 by phenaproxima, tedbow: Update Composer
 Stager to 0.3.0

---
 composer.json                                 |   2 +-
 package_manager/package_manager.services.yml  |  10 +-
 .../src/Event/ExcludedPathsTrait.php          |  17 +-
 .../ComposerExecutableValidator.php           |  10 +-
 .../ExcludedPathsSubscriber.php               | 169 ++++++++++++------
 package_manager/src/FileSyncerFactory.php     |  14 +-
 .../tests/fixtures/fake_site/vendor/.htaccess |   1 +
 .../package_manager_bypass/src/Beginner.php   |   4 +-
 .../package_manager_bypass/src/Committer.php  |   4 +-
 .../package_manager_bypass/src/Stager.php     |   4 +-
 .../src/Functional/ExcludedPathsTest.php      |  60 ++++---
 .../ComposerExecutableValidatorTest.php       |   4 +-
 .../Kernel/ComposerSettingsValidatorTest.php  |   1 +
 .../src/Kernel/DiskSpaceValidatorTest.php     |   1 +
 .../Kernel/ExcludedPathsSubscriberTest.php    |   8 +-
 .../WritableFileSystemValidatorTest.php       |   1 +
 tests/src/Build/UpdateTestBase.php            |   5 +-
 .../StagedProjectsValidatorTest.php           |   1 +
 tests/src/Kernel/UpdaterTest.php              |   1 +
 19 files changed, 197 insertions(+), 120 deletions(-)
 create mode 100644 package_manager/tests/fixtures/fake_site/vendor/.htaccess

diff --git a/composer.json b/composer.json
index 36692bf5da..39bd124972 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
   "require": {
     "ext-json": "*",
     "drupal/core": "^9.2",
-    "php-tuf/composer-stager": "0.2.3",
+    "php-tuf/composer-stager": "0.3.0",
     "composer/composer": "^2"
   },
   "config": {
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index d2b01442f1..f5e04442d0 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -4,8 +4,6 @@ services:
     class: Symfony\Component\Filesystem\Filesystem
   package_manager.symfony_executable_finder:
     class: Symfony\Component\Process\ExecutableFinder
-  package_manager.symfony_finder:
-    class: Symfony\Component\Finder\Finder
 
   # Basic infrastructure services.
   package_manager.process_factory:
@@ -45,8 +43,6 @@ services:
     class: PhpTuf\ComposerStager\Infrastructure\FileSyncer\PhpFileSyncer
     arguments:
       - '@package_manager.file_system'
-      - '@package_manager.symfony_finder'
-      - '@package_manager.symfony_finder'
   package_manager.file_syncer.factory:
     class: Drupal\package_manager\FileSyncerFactory
     arguments:
@@ -55,7 +51,7 @@ services:
       - '@package_manager.file_syncer.rsync'
       - '@config.factory'
   package_manager.file_syncer:
-    class: PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface
+    class: PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface
     factory: ['@package_manager.file_syncer.factory', 'create']
 
   # Domain services.
@@ -127,10 +123,10 @@ services:
   package_manager.excluded_paths_subscriber:
     class: Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
     arguments:
-      - '%app.root%'
       - '%site.path%'
-      - '@file_system'
+      - '@package_manager.symfony_file_system'
       - '@stream_wrapper_manager'
       - '@database'
+      - '@package_manager.path_locator'
     tags:
       - { name: event_subscriber }
diff --git a/package_manager/src/Event/ExcludedPathsTrait.php b/package_manager/src/Event/ExcludedPathsTrait.php
index 3648446b80..fc7e690440 100644
--- a/package_manager/src/Event/ExcludedPathsTrait.php
+++ b/package_manager/src/Event/ExcludedPathsTrait.php
@@ -15,12 +15,23 @@ trait ExcludedPathsTrait {
   protected $excludedPaths = [];
 
   /**
-   * Adds an absolute path to exclude from the current operation.
+   * Adds a path to exclude from the current operation.
    *
-   * @todo This should only accept paths relative to the active directory.
+   * If called on an instance of \Drupal\package_manager\Event\PreCreateEvent,
+   * excluded paths will not be copied into the staging area when the stage is
+   * created. If called on an instance of
+   * \Drupal\package_manager\Event\PreApplyEvent, excluded paths will not be
+   * deleted from the active directory when staged changes are applied. So,
+   * to ensure that a given path is never staged, but also preserved in the
+   * active directory, it should be passed to this method on both PreCreateEvent
+   * and PreApplyEvent. See
+   * \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber for an
+   * example.
    *
    * @param string $path
-   *   The path to exclude.
+   *   The path to exclude, relative to the project root.
+   *
+   * @see \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
    */
   public function excludePath(string $path): void {
     $this->excludedPaths[] = $path;
diff --git a/package_manager/src/EventSubscriber/ComposerExecutableValidator.php b/package_manager/src/EventSubscriber/ComposerExecutableValidator.php
index a4df9a3591..f91e852930 100644
--- a/package_manager/src/EventSubscriber/ComposerExecutableValidator.php
+++ b/package_manager/src/EventSubscriber/ComposerExecutableValidator.php
@@ -7,21 +7,21 @@ use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\Core\Extension\ExtensionVersion;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
-use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Process\OutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface;
 use PhpTuf\ComposerStager\Exception\ExceptionInterface;
-use PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface;
 
 /**
  * Validates that the Composer executable can be found in the correct version.
  */
-class ComposerExecutableValidator implements PreOperationStageValidatorInterface, ProcessOutputCallbackInterface {
+class ComposerExecutableValidator implements PreOperationStageValidatorInterface, OutputCallbackInterface {
 
   use StringTranslationTrait;
 
   /**
    * The Composer runner.
    *
-   * @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface
+   * @var \PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface
    */
   protected $composer;
 
@@ -35,7 +35,7 @@ class ComposerExecutableValidator implements PreOperationStageValidatorInterface
   /**
    * Constructs a ComposerExecutableValidator object.
    *
-   * @param \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface $composer
+   * @param \PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface $composer
    *   The Composer runner.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
    *   The translation service.
diff --git a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
index 4663284c79..b65ff7a709 100644
--- a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
+++ b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
@@ -3,25 +3,20 @@
 namespace Drupal\package_manager\EventSubscriber;
 
 use Drupal\Core\Database\Connection;
-use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\StreamWrapper\LocalStream;
 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Event\StageEvent;
+use Drupal\package_manager\PathLocator;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Filesystem\Filesystem;
 
 /**
  * Defines an event subscriber to exclude certain paths from staging areas.
  */
 class ExcludedPathsSubscriber implements EventSubscriberInterface {
 
-  /**
-   * The Drupal root.
-   *
-   * @var string
-   */
-  protected $appRoot;
-
   /**
    * The current site path, relative to the Drupal root.
    *
@@ -30,9 +25,9 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
   protected $sitePath;
 
   /**
-   * The file system service.
+   * The Symfony file system service.
    *
-   * @var \Drupal\Core\File\FileSystemInterface
+   * @var \Symfony\Component\Filesystem\Filesystem
    */
   protected $fileSystem;
 
@@ -50,91 +45,157 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
    */
   protected $database;
 
+  /**
+   * The path locator service.
+   *
+   * @var \Drupal\package_manager\PathLocator
+   */
+  protected $pathLocator;
+
   /**
    * Constructs an ExcludedPathsSubscriber.
    *
-   * @param string $app_root
-   *   The Drupal root.
    * @param string $site_path
    *   The current site path, relative to the Drupal root.
-   * @param \Drupal\Core\File\FileSystemInterface $file_system
-   *   The file system service.
+   * @param \Symfony\Component\Filesystem\Filesystem $file_system
+   *   The Symfony file system service.
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
    *   The stream wrapper manager service.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection.
+   * @param \Drupal\package_manager\PathLocator $path_locator
+   *   The path locator service.
    */
-  public function __construct(string $app_root, string $site_path, FileSystemInterface $file_system, StreamWrapperManagerInterface $stream_wrapper_manager, Connection $database) {
-    $this->appRoot = $app_root;
+  public function __construct(string $site_path, Filesystem $file_system, StreamWrapperManagerInterface $stream_wrapper_manager, Connection $database, PathLocator $path_locator) {
     $this->sitePath = $site_path;
     $this->fileSystem = $file_system;
     $this->streamWrapperManager = $stream_wrapper_manager;
     $this->database = $database;
+    $this->pathLocator = $path_locator;
   }
 
   /**
-   * Reacts before staged changes are committed the active directory.
+   * Flags paths to be excluded, relative to the web root.
    *
-   * @param \Drupal\package_manager\Event\PreApplyEvent $event
+   * This should only be used for paths that, if they exist at all, are
+   * *guaranteed* to exist within the web root.
+   *
+   * @param \Drupal\package_manager\Event\PreCreateEvent|\Drupal\package_manager\Event\PreApplyEvent $event
    *   The event object.
+   * @param string[] $paths
+   *   The paths to exclude. These should be relative to the web root, and will
+   *   be made relative to the project root.
    */
-  public function preApply(PreApplyEvent $event): void {
-    // Don't copy anything from the staging area's sites/default.
-    // @todo Make this a lot smarter in https://www.drupal.org/i/3228955.
-    $event->excludePath('sites/default');
+  protected function excludeInWebRoot(StageEvent $event, array $paths): void {
+    $web_root = $this->pathLocator->getWebRoot();
+    if ($web_root) {
+      $web_root .= '/';
+    }
 
-    // If the core-vendor-hardening plugin (used in the legacy-project template)
-    // is present, it may have written a web.config file into the vendor
-    // directory. We don't want to copy that.
-    $event->excludePath('web.config');
+    foreach ($paths as $path) {
+      // Make the path relative to the project root by prefixing the web root.
+      $event->excludePath($web_root . $path);
+    }
   }
 
   /**
-   * Excludes paths from a staging area before it is created.
+   * Flags paths to be excluded, relative to the project root.
    *
-   * @param \Drupal\package_manager\Event\PreCreateEvent $event
+   * @param \Drupal\package_manager\Event\PreCreateEvent|\Drupal\package_manager\Event\PreApplyEvent $event
    *   The event object.
+   * @param string[] $paths
+   *   The paths to exclude. Absolute paths will be made relative to the project
+   *   root; relative paths will be assumed to already be relative to the
+   *   project root, and excluded as given.
    */
-  public function preCreate(PreCreateEvent $event): void {
-    // Automated test site directories should never be staged.
-    $event->excludePath('sites/simpletest');
-
-    // Windows server configuration files, like web.config, should never be
-    // staged either. (These can be written in the vendor directory by the
-    // core-vendor-hardening plugin, which is used in the drupal/legacy-project
-    // template.)
-    $event->excludePath('web.config');
-
-    if ($public = $this->getFilesPath('public')) {
-      $event->excludePath($public);
+  protected function excludeInProjectRoot(StageEvent $event, array $paths): void {
+    $project_root = $this->pathLocator->getProjectRoot();
+
+    foreach ($paths as $path) {
+      // Make absolute paths relative to the project root.
+      $path = str_replace($project_root, NULL, $path);
+      $path = ltrim($path, '/');
+      $event->excludePath($path);
     }
-    if ($private = $this->getFilesPath('private')) {
-      $event->excludePath($private);
+  }
+
+  /**
+   * Excludes common paths from staging operations.
+   *
+   * @param \Drupal\package_manager\Event\PreApplyEvent|\Drupal\package_manager\Event\PreCreateEvent $event
+   *   The event object.
+   *
+   * @see \Drupal\package_manager\Event\ExcludedPathsTrait::excludePath()
+   */
+  public function ignoreCommonPaths(StageEvent $event): void {
+    // Compile two lists of paths to exclude: paths that are relative to the
+    // project root, and paths that are relative to the web root.
+    $web = $project = [];
+
+    // Always ignore automated test directories. If they exist, they will be in
+    // the web root.
+    $web[] = 'sites/simpletest';
+
+    // If the core-vendor-hardening plugin (used in the legacy-project template)
+    // is present, it may have written security hardening files in the vendor
+    // directory. They should always be ignored.
+    $vendor_dir = $this->pathLocator->getVendorDirectory();
+    $project[] = $vendor_dir . '/web.config';
+    $project[] = $vendor_dir . '/.htaccess';
+
+    // Ignore public and private files. These paths could be either absolute or
+    // relative, depending on site settings. If they are absolute, treat them
+    // as relative to the project root. Otherwise, treat them as relative to
+    // the web root.
+    $files = array_filter([
+      $this->getFilesPath('public'),
+      $this->getFilesPath('private'),
+    ]);
+    foreach ($files as $path) {
+      if ($this->fileSystem->isAbsolutePath($path)) {
+        $project[] = $path;
+      }
+      else {
+        $web[] = $path;
+      }
     }
 
-    // Exclude site-specific settings files.
+    // Ignore site-specific settings files, which are always in the web root.
     $settings_files = [
       'settings.php',
       'settings.local.php',
       'services.yml',
     ];
-    $default_site = 'sites' . DIRECTORY_SEPARATOR . 'default';
-
     foreach ($settings_files as $settings_file) {
-      $event->excludePath($this->sitePath . DIRECTORY_SEPARATOR . $settings_file);
-      $event->excludePath($default_site . DIRECTORY_SEPARATOR . $settings_file);
+      $web[] = $this->sitePath . '/' . $settings_file;
+      $web[] = 'sites/default/' . $settings_file;
     }
 
     // If the database is SQLite, it might be located in the active directory
-    // and we should not stage it.
+    // and we should ignore it. Always treat it as relative to the project root.
     if ($this->database->driver() === 'sqlite') {
       $options = $this->database->getConnectionOptions();
-      $database = str_replace($this->appRoot, NULL, $options['database']);
-      $database = ltrim($database, '/');
-      $event->excludePath($database);
-      $event->excludePath("$database-shm");
-      $event->excludePath("$database-wal");
+      $project[] = $options['database'];
+      $project[] = $options['database'] . '-shm';
+      $project[] = $options['database'] . '-wal';
     }
+
+    $this->excludeInWebRoot($event, $web);
+    $this->excludeInProjectRoot($event, $project);
+  }
+
+  /**
+   * Reacts before staged changes are committed the active directory.
+   *
+   * @param \Drupal\package_manager\Event\PreApplyEvent $event
+   *   The event object.
+   */
+  public function preApply(PreApplyEvent $event): void {
+    // Don't copy anything from the staging area's sites/default.
+    // @todo Make this a lot smarter in https://www.drupal.org/i/3228955.
+    $this->excludeInWebRoot($event, ['sites/default']);
+
+    $this->ignoreCommonPaths($event);
   }
 
   /**
@@ -165,7 +226,7 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
    */
   public static function getSubscribedEvents() {
     return [
-      PreCreateEvent::class => 'preCreate',
+      PreCreateEvent::class => 'ignoreCommonPaths',
       PreApplyEvent::class => 'preApply',
     ];
   }
diff --git a/package_manager/src/FileSyncerFactory.php b/package_manager/src/FileSyncerFactory.php
index 727620a248..ee96aa1e57 100644
--- a/package_manager/src/FileSyncerFactory.php
+++ b/package_manager/src/FileSyncerFactory.php
@@ -3,9 +3,9 @@
 namespace Drupal\package_manager;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
-use PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerFactoryInterface;
+use PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerFactoryInterface;
+use PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface;
 use PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerFactory as StagerFileSyncerFactory;
-use PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface;
 use Symfony\Component\Process\ExecutableFinder;
 
 /**
@@ -16,21 +16,21 @@ class FileSyncerFactory implements FileSyncerFactoryInterface {
   /**
    * The decorated file syncer factory.
    *
-   * @var \PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerFactoryInterface
+   * @var \PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerFactoryInterface
    */
   protected $decorated;
 
   /**
    * The PHP file syncer service.
    *
-   * @var \PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface
+   * @var \PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface
    */
   protected $phpFileSyncer;
 
   /**
    * The rsync file syncer service.
    *
-   * @var \PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface
+   * @var \PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface
    */
   protected $rsyncFileSyncer;
 
@@ -46,9 +46,9 @@ class FileSyncerFactory implements FileSyncerFactoryInterface {
    *
    * @param \Symfony\Component\Process\ExecutableFinder $executable_finder
    *   The Symfony executable finder.
-   * @param \PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface $php_file_syncer
+   * @param \PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface $php_file_syncer
    *   The PHP file syncer service.
-   * @param \PhpTuf\ComposerStager\Infrastructure\FileSyncer\FileSyncerInterface $rsync_file_syncer
+   * @param \PhpTuf\ComposerStager\Domain\FileSyncer\FileSyncerInterface $rsync_file_syncer
    *   The rsync file syncer service.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory service.
diff --git a/package_manager/tests/fixtures/fake_site/vendor/.htaccess b/package_manager/tests/fixtures/fake_site/vendor/.htaccess
new file mode 100644
index 0000000000..e11552b41d
--- /dev/null
+++ b/package_manager/tests/fixtures/fake_site/vendor/.htaccess
@@ -0,0 +1 @@
+# This file should never be staged.
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 742d38bd85..e9ff25509f 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
@@ -3,7 +3,7 @@
 namespace Drupal\package_manager_bypass;
 
 use PhpTuf\ComposerStager\Domain\BeginnerInterface;
-use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Process\OutputCallbackInterface;
 
 /**
  * Defines an update beginner which doesn't do anything.
@@ -13,7 +13,7 @@ class Beginner extends InvocationRecorderBase implements BeginnerInterface {
   /**
    * {@inheritdoc}
    */
-  public function begin(string $activeDir, string $stagingDir, ?array $exclusions = [], ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+  public function begin(string $activeDir, string $stagingDir, ?array $exclusions = [], ?OutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
     $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
   }
 
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Committer.php b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
index 3518237b5b..2feb0d0f70 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Committer.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
@@ -3,7 +3,7 @@
 namespace Drupal\package_manager_bypass;
 
 use PhpTuf\ComposerStager\Domain\CommitterInterface;
-use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Process\OutputCallbackInterface;
 
 /**
  * Defines an update committer which doesn't do any actual committing.
@@ -30,7 +30,7 @@ class Committer extends InvocationRecorderBase implements CommitterInterface {
   /**
    * {@inheritdoc}
    */
-  public function commit(string $stagingDir, string $activeDir, ?array $exclusions = [], ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+  public function commit(string $stagingDir, string $activeDir, ?array $exclusions = [], ?OutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
     $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
   }
 
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Stager.php b/package_manager/tests/modules/package_manager_bypass/src/Stager.php
index bb93e47271..237eccab38 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Stager.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Stager.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\package_manager_bypass;
 
-use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
+use PhpTuf\ComposerStager\Domain\Process\OutputCallbackInterface;
 use PhpTuf\ComposerStager\Domain\StagerInterface;
 
 /**
@@ -13,7 +13,7 @@ class Stager extends InvocationRecorderBase implements StagerInterface {
   /**
    * {@inheritdoc}
    */
-  public function stage(array $composerCommand, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
+  public function stage(array $composerCommand, string $stagingDir, ?OutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
     $this->saveInvocationArguments($composerCommand, $stagingDir);
   }
 
diff --git a/package_manager/tests/src/Functional/ExcludedPathsTest.php b/package_manager/tests/src/Functional/ExcludedPathsTest.php
index 2870806996..dbc3dab973 100644
--- a/package_manager/tests/src/Functional/ExcludedPathsTest.php
+++ b/package_manager/tests/src/Functional/ExcludedPathsTest.php
@@ -117,38 +117,44 @@ class ExcludedPathsTest extends BrowserTestBase {
     };
     $stage::$stagingRoot = $this->siteDirectory . '/stage';
     $stage_dir = $stage::$stagingRoot . DIRECTORY_SEPARATOR . $stage->create();
-
     $this->assertDirectoryExists($stage_dir);
-    $this->assertDirectoryDoesNotExist("$stage_dir/sites/simpletest");
-    $this->assertFileDoesNotExist("$stage_dir/vendor/web.config");
-    $this->assertDirectoryDoesNotExist("$stage_dir/$site_path/files");
-    $this->assertDirectoryDoesNotExist("$stage_dir/private");
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/settings.php");
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/settings.local.php");
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/services.yml");
-    // SQLite databases and their support files should never be staged.
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/db.sqlite");
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/db.sqlite-shm");
-    $this->assertFileDoesNotExist("$stage_dir/$site_path/db.sqlite-wal");
-    // Default site-specific settings files should never be staged.
-    $this->assertFileDoesNotExist("$stage_dir/sites/default/settings.php");
-    $this->assertFileDoesNotExist("$stage_dir/sites/default/settings.local.php");
-    $this->assertFileDoesNotExist("$stage_dir/sites/default/services.yml");
-    // A non-excluded file in the default site directory should be staged.
-    $this->assertFileExists("$stage_dir/sites/default/stage.txt");
 
-    $files = [
-      'sites/default/no-copy.txt',
-      'web.config',
+    $ignore = [
+      'sites/simpletest',
+      'vendor/.htaccess',
+      'vendor/web.config',
+      "$site_path/files/ignore.txt",
+      'private/ignore.txt',
+      "$site_path/settings.php",
+      "$site_path/settings.local.php",
+      "$site_path/services.yml",
+      // SQLite databases and their support files should always be ignored.
+      "$site_path/db.sqlite",
+      "$site_path/db.sqlite-shm",
+      "$site_path/db.sqlite-wal",
+      // Default site-specific settings files should be ignored.
+      'sites/default/settings.php',
+      'sites/default/settings.local.php',
+      'sites/default/services.yml',
     ];
-    foreach ($files as $file) {
-      $file = "$stage_dir/$file";
-      touch($file);
-      $this->assertFileExists($file);
+    foreach ($ignore as $path) {
+      $this->assertFileExists("$active_dir/$path");
+      $this->assertFileDoesNotExist("$stage_dir/$path");
     }
+    // A non-excluded file in the default site directory should be staged.
+    $this->assertFileExists("$stage_dir/sites/default/stage.txt");
+
+    // A new file added to the staging area in an excluded directory, should not
+    // be copied to the active directory.
+    $file = "$stage_dir/sites/default/no-copy.txt";
+    touch($file);
+    $this->assertFileExists($file);
     $stage->apply();
-    foreach ($files as $file) {
-      $this->assertFileDoesNotExist("$active_dir/$file");
+    $this->assertFileDoesNotExist("$active_dir/sites/default/no-copy.txt");
+
+    // The ignored files should still be in the active directory.
+    foreach ($ignore as $path) {
+      $this->assertFileExists("$active_dir/$path");
     }
   }
 
diff --git a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
index 8905f788ed..bc57b4fa75 100644
--- a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
@@ -112,8 +112,8 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
     // Mock the output of `composer --version`, will be passed to the validator,
     // which is itself a callback function that gets called repeatedly as
     // Composer produces output.
-    /** @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */
-    $runner = $this->prophesize('\PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface');
+    /** @var \PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */
+    $runner = $this->prophesize('\PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface');
 
     $runner->run(['--version'], Argument::type(ComposerExecutableValidator::class))
       // Whatever is passed to ::run() will be passed to this mock callback in
diff --git a/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php b/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
index fae703f6e0..ff09f97428 100644
--- a/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
@@ -85,6 +85,7 @@ class ComposerSettingsValidatorTest extends PackageManagerKernelTestBase {
     $locator = $this->prophesize(PathLocator::class);
     $locator->getActiveDirectory()->willReturn($active_dir);
     $locator->getProjectRoot()->willReturn($active_dir);
+    $locator->getWebRoot()->willReturn('');
     $locator->getVendorDirectory()->willReturn($active_dir);
     $this->container->set('package_manager.path_locator', $locator->reveal());
 
diff --git a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
index ea15bb086e..3195b51d3a 100644
--- a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
+++ b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
@@ -168,6 +168,7 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
   public function testDiskSpaceValidation(bool $shared_disk, array $free_space, array $expected_results): void {
     $path_locator = $this->prophesize('\Drupal\package_manager\PathLocator');
     $path_locator->getProjectRoot()->willReturn('root');
+    $path_locator->getWebRoot()->willReturn('');
     $path_locator->getActiveDirectory()->willReturn('root');
     $path_locator->getVendorDirectory()->willReturn('vendor');
     $this->container->set('package_manager.path_locator', $path_locator->reveal());
diff --git a/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php b/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
index b44f7811d6..538232d5b5 100644
--- a/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
+++ b/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
@@ -83,15 +83,15 @@ class ExcludedPathsSubscriberTest extends PackageManagerKernelTestBase {
     $connection->getConnectionOptions()->willReturn(['database' => $database]);
 
     $subscriber = new ExcludedPathsSubscriber(
-      $this->getDrupalRoot(),
       'sites/default',
-      $this->container->get('file_system'),
+      $this->container->get('package_manager.symfony_file_system'),
       $this->container->get('stream_wrapper_manager'),
-      $connection->reveal()
+      $connection->reveal(),
+      $this->container->get('package_manager.path_locator')
     );
 
     $event = new PreCreateEvent($this->createStage());
-    $subscriber->preCreate($event);
+    $subscriber->ignoreCommonPaths($event);
     // All of the expected exclusions should be flagged.
     $this->assertEmpty(array_diff($expected_exclusions, $event->getExcludedPaths()));
   }
diff --git a/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php b/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
index 136d6b37cb..871d42c4aa 100644
--- a/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
+++ b/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
@@ -113,6 +113,7 @@ class WritableFileSystemValidatorTest extends PackageManagerKernelTestBase {
 
     $path_locator = $this->prophesize(PathLocator::class);
     $path_locator->getActiveDirectory()->willReturn($root->url());
+    $path_locator->getProjectRoot()->willReturn($root->url());
     $path_locator->getWebRoot()->willReturn('');
     $path_locator->getVendorDirectory()->willReturn($vendor->url());
     $this->container->set('package_manager.path_locator', $path_locator->reveal());
diff --git a/tests/src/Build/UpdateTestBase.php b/tests/src/Build/UpdateTestBase.php
index e33f5ed6a3..367e0f8241 100644
--- a/tests/src/Build/UpdateTestBase.php
+++ b/tests/src/Build/UpdateTestBase.php
@@ -220,10 +220,7 @@ END;
     // Packagist.
     $repositories['packagist.org'] = FALSE;
 
-    $repositories['drupal/automatic_updates'] = [
-      'type' => 'path',
-      'url' => __DIR__ . '/../../..',
-    ];
+    $repositories['drupal/automatic_updates'] = $this->createPathRepository(__DIR__ . '/../../..');
     // Use whatever the current branch of automatic_updates is.
     $data['require']['drupal/automatic_updates'] = '*';
 
diff --git a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
index 1b21d13605..6a2dc5aebf 100644
--- a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
@@ -64,6 +64,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
 
     $locator->getActiveDirectory()->willReturn($active_dir);
     $locator->getProjectRoot()->willReturn($active_dir);
+    $locator->getWebRoot()->willReturn('');
     $locator->getVendorDirectory()->willReturn($active_dir);
 
     $stage_dir_exists = is_dir($stage_dir);
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 034514a3b9..5881d85144 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -51,6 +51,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
     $locator = $this->prophesize(PathLocator::class);
     $locator->getActiveDirectory()->willReturn($fixture_dir);
     $locator->getProjectRoot()->willReturn($fixture_dir);
+    $locator->getWebRoot()->willReturn('');
     $locator->getVendorDirectory()->willReturn($fixture_dir);
     $this->container->set('package_manager.path_locator', $locator->reveal());
 
-- 
GitLab