From bceaf42c1c19b618384dba30ee6daec37bbe6396 Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Thu, 6 Jan 2022 20:50:56 +0000
Subject: [PATCH] Issue #3255011 by phenaproxima: Don't stage git directories

---
 .../ExcludedPathsSubscriber.php               |  15 ++
 .../tests/fixtures/fake_site/composer.json    |   1 -
 .../fixtures/fake_site/private/ignore.txt     |   1 -
 .../fake_site/sites/default/services.yml      |   2 -
 .../sites/default/settings.local.php          |   6 -
 .../fake_site/sites/default/settings.php      |   6 -
 .../fake_site/sites/default/stage.txt         |   1 -
 .../fake_site/sites/example.com/db.sqlite     |   1 -
 .../fake_site/sites/example.com/db.sqlite-shm |   1 -
 .../fake_site/sites/example.com/db.sqlite-wal |   1 -
 .../sites/example.com/files/ignore.txt        |   1 -
 .../fake_site/sites/example.com/services.yml  |   2 -
 .../sites/example.com/settings.local.php      |   6 -
 .../fake_site/sites/example.com/settings.php  |   6 -
 .../fake_site/sites/simpletest/ignore.txt     |   1 -
 .../tests/fixtures/fake_site/vendor/.htaccess |   1 -
 .../fixtures/fake_site/vendor/web.config      |   1 -
 .../Kernel/ComposerSettingsValidatorTest.php  |  33 +--
 .../src/Kernel/DiskSpaceValidatorTest.php     | 128 ++-------
 .../Kernel/ExcludedPathsSubscriberTest.php    |  99 -------
 .../tests/src/Kernel/ExcludedPathsTest.php    | 247 ++++++++----------
 .../src/Kernel/LockFileValidatorTest.php      |  60 +----
 .../Kernel/PackageManagerKernelTestBase.php   | 170 ++++++++++++
 .../tests/src/Kernel/StageEventsTest.php      |  12 +-
 .../WritableFileSystemValidatorTest.php       |  37 +--
 .../StagedProjectsValidatorTest.php           |  12 +-
 26 files changed, 367 insertions(+), 484 deletions(-)
 delete mode 100644 package_manager/tests/fixtures/fake_site/composer.json
 delete mode 100644 package_manager/tests/fixtures/fake_site/private/ignore.txt
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/default/services.yml
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/default/settings.local.php
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/default/settings.php
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/default/stage.txt
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/files/ignore.txt
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/services.yml
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/settings.local.php
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/example.com/settings.php
 delete mode 100644 package_manager/tests/fixtures/fake_site/sites/simpletest/ignore.txt
 delete mode 100644 package_manager/tests/fixtures/fake_site/vendor/.htaccess
 delete mode 100644 package_manager/tests/fixtures/fake_site/vendor/web.config
 delete mode 100644 package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php

diff --git a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
index b65ff7a709..a54a4e2816 100644
--- a/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
+++ b/package_manager/src/EventSubscriber/ExcludedPathsSubscriber.php
@@ -11,6 +11,7 @@ use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\PathLocator;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Finder\Finder;
 
 /**
  * Defines an event subscriber to exclude certain paths from staging areas.
@@ -180,6 +181,20 @@ class ExcludedPathsSubscriber implements EventSubscriberInterface {
       $project[] = $options['database'] . '-wal';
     }
 
+    // Find all .git directories in the project and exclude them. We cannot do
+    // this with FileSystemInterface::scanDirectory() because it unconditionally
+    // excludes anything starting with a dot.
+    $finder = Finder::create()
+      ->in($this->pathLocator->getProjectRoot())
+      ->directories()
+      ->name('.git')
+      ->ignoreVCS(FALSE)
+      ->ignoreDotFiles(FALSE);
+
+    foreach ($finder as $git_directory) {
+      $project[] = $git_directory->getPathname();
+    }
+
     $this->excludeInWebRoot($event, $web);
     $this->excludeInProjectRoot($event, $project);
   }
diff --git a/package_manager/tests/fixtures/fake_site/composer.json b/package_manager/tests/fixtures/fake_site/composer.json
deleted file mode 100644
index 0967ef424b..0000000000
--- a/package_manager/tests/fixtures/fake_site/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/package_manager/tests/fixtures/fake_site/private/ignore.txt b/package_manager/tests/fixtures/fake_site/private/ignore.txt
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/private/ignore.txt
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/default/services.yml b/package_manager/tests/fixtures/fake_site/sites/default/services.yml
deleted file mode 100644
index cbc4434e8f..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/default/services.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-# This file should never be staged.
-must_not_be: 'empty'
diff --git a/package_manager/tests/fixtures/fake_site/sites/default/settings.local.php b/package_manager/tests/fixtures/fake_site/sites/default/settings.local.php
deleted file mode 100644
index 15b43d2812..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/default/settings.local.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
diff --git a/package_manager/tests/fixtures/fake_site/sites/default/settings.php b/package_manager/tests/fixtures/fake_site/sites/default/settings.php
deleted file mode 100644
index 15b43d2812..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/default/settings.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
diff --git a/package_manager/tests/fixtures/fake_site/sites/default/stage.txt b/package_manager/tests/fixtures/fake_site/sites/default/stage.txt
deleted file mode 100644
index 0087269e33..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/default/stage.txt
+++ /dev/null
@@ -1 +0,0 @@
-This file should be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-shm
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal b/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/db.sqlite-wal
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/files/ignore.txt b/package_manager/tests/fixtures/fake_site/sites/example.com/files/ignore.txt
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/files/ignore.txt
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/services.yml b/package_manager/tests/fixtures/fake_site/sites/example.com/services.yml
deleted file mode 100644
index f408d89e28..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/services.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-# This file should never be staged.
-key: "value"
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/settings.local.php b/package_manager/tests/fixtures/fake_site/sites/example.com/settings.local.php
deleted file mode 100644
index 15b43d2812..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/settings.local.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
diff --git a/package_manager/tests/fixtures/fake_site/sites/example.com/settings.php b/package_manager/tests/fixtures/fake_site/sites/example.com/settings.php
deleted file mode 100644
index 15b43d2812..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/example.com/settings.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
diff --git a/package_manager/tests/fixtures/fake_site/sites/simpletest/ignore.txt b/package_manager/tests/fixtures/fake_site/sites/simpletest/ignore.txt
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/sites/simpletest/ignore.txt
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/vendor/.htaccess b/package_manager/tests/fixtures/fake_site/vendor/.htaccess
deleted file mode 100644
index e11552b41d..0000000000
--- a/package_manager/tests/fixtures/fake_site/vendor/.htaccess
+++ /dev/null
@@ -1 +0,0 @@
-# This file should never be staged.
diff --git a/package_manager/tests/fixtures/fake_site/vendor/web.config b/package_manager/tests/fixtures/fake_site/vendor/web.config
deleted file mode 100644
index 08874eba8b..0000000000
--- a/package_manager/tests/fixtures/fake_site/vendor/web.config
+++ /dev/null
@@ -1 +0,0 @@
-This file should never be staged.
diff --git a/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php b/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
index ff09f97428..d964c09ba9 100644
--- a/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerSettingsValidatorTest.php
@@ -3,11 +3,8 @@
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\Component\Serialization\Json;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\package_manager\Exception\StageValidationException;
-use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\ValidationResult;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * @covers \Drupal\package_manager\EventSubscriber\ComposerSettingsValidator
@@ -16,22 +13,6 @@ use org\bovigo\vfs\vfsStream;
  */
 class ComposerSettingsValidatorTest extends PackageManagerKernelTestBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function disableValidators(ContainerBuilder $container): void {
-    parent::disableValidators($container);
-
-    // Disable the disk space validator, since it tries to inspect the file
-    // system in ways that vfsStream doesn't support, like calling stat() and
-    // disk_free_space().
-    $container->removeDefinition('package_manager.validator.disk_space');
-
-    // Disable the lock file validator, since the mock file system we create in
-    // this test doesn't have any lock files to validate.
-    $container->removeDefinition('package_manager.validator.lock_file');
-  }
-
   /**
    * Data provider for ::testSecureHttpValidation().
    *
@@ -78,16 +59,10 @@ class ComposerSettingsValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerSecureHttpValidation
    */
   public function testSecureHttpValidation(string $contents, array $expected_results): void {
-    $file = vfsStream::newFile('composer.json')->setContent($contents);
-    $this->vfsRoot->addChild($file);
-
-    $active_dir = $this->vfsRoot->url();
-    $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());
+    $this->createTestProject();
+    $active_dir = $this->container->get('package_manager.path_locator')
+      ->getActiveDirectory();
+    file_put_contents("$active_dir/composer.json", $contents);
 
     try {
       $this->createStage()->create();
diff --git a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
index 3195b51d3a..7b1f406859 100644
--- a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
+++ b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
@@ -2,9 +2,7 @@
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\EventSubscriber\DiskSpaceValidator;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Component\Utility\Bytes;
 
@@ -15,31 +13,6 @@ use Drupal\Component\Utility\Bytes;
  */
 class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function register(ContainerBuilder $container) {
-    parent::register($container);
-
-    // Replace the validator under test with a mocked version which can be
-    // rigged up to return specific values for various filesystem checks.
-    $container->getDefinition('package_manager.validator.disk_space')
-      ->setClass(TestDiskSpaceValidator::class);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function disableValidators(ContainerBuilder $container): void {
-    parent::disableValidators($container);
-
-    // Disable the lock file and Composer settings validators, since in this
-    // test we are validating an imaginary file system which doesn't have any
-    // Composer files.
-    $container->removeDefinition('package_manager.validator.lock_file');
-    $container->removeDefinition('package_manager.validator.composer_settings');
-  }
-
   /**
    * Data provider for ::testDiskSpaceValidation().
    *
@@ -47,17 +20,21 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
    *   Sets of arguments to pass to the test method.
    */
   public function providerDiskSpaceValidation(): array {
-    $root_insufficient = t('Drupal root filesystem "root" has insufficient space. There must be at least 1024 megabytes free.');
-    $vendor_insufficient = t('Vendor filesystem "vendor" has insufficient space. There must be at least 1024 megabytes free.');
-    $temp_insufficient = t('Directory "temp" has insufficient space. There must be at least 1024 megabytes free.');
+    // These will be defined by ::createTestProject().
+    $root = 'vfs://root/active';
+    $vendor = "$root/vendor";
+
+    $root_insufficient = "Drupal root filesystem \"$root\" has insufficient space. There must be at least 1024 megabytes free.";
+    $vendor_insufficient = "Vendor filesystem \"$vendor\" has insufficient space. There must be at least 1024 megabytes free.";
+    $temp_insufficient = 'Directory "temp" has insufficient space. There must be at least 1024 megabytes free.';
     $summary = t("There is not enough disk space to create a staging area.");
 
     return [
       'shared, vendor and temp sufficient, root insufficient' => [
         TRUE,
         [
-          'root' => '1M',
-          'vendor' => '2G',
+          $root => '1M',
+          $vendor => '2G',
           'temp' => '4G',
         ],
         [
@@ -67,8 +44,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'shared, root and vendor insufficient, temp sufficient' => [
         TRUE,
         [
-          'root' => '1M',
-          'vendor' => '2M',
+          $root => '1M',
+          $vendor => '2M',
           'temp' => '2G',
         ],
         [
@@ -78,8 +55,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'shared, vendor and root sufficient, temp insufficient' => [
         TRUE,
         [
-          'root' => '2G',
-          'vendor' => '4G',
+          $root => '2G',
+          $vendor => '4G',
           'temp' => '1M',
         ],
         [
@@ -89,8 +66,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'shared, root and temp insufficient, vendor sufficient' => [
         TRUE,
         [
-          'root' => '1M',
-          'vendor' => '2G',
+          $root => '1M',
+          $vendor => '2G',
           'temp' => '2M',
         ],
         [
@@ -103,8 +80,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'not shared, root insufficient, vendor and temp sufficient' => [
         FALSE,
         [
-          'root' => '5M',
-          'vendor' => '1G',
+          $root => '5M',
+          $vendor => '1G',
           'temp' => '4G',
         ],
         [
@@ -114,8 +91,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'not shared, vendor insufficient, root and temp sufficient' => [
         FALSE,
         [
-          'root' => '2G',
-          'vendor' => '10M',
+          $root => '2G',
+          $vendor => '10M',
           'temp' => '4G',
         ],
         [
@@ -125,8 +102,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'not shared, root and vendor sufficient, temp insufficient' => [
         FALSE,
         [
-          'root' => '1G',
-          'vendor' => '2G',
+          $root => '1G',
+          $vendor => '2G',
           'temp' => '3M',
         ],
         [
@@ -136,8 +113,8 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
       'not shared, root and vendor insufficient, temp sufficient' => [
         FALSE,
         [
-          'root' => '500M',
-          'vendor' => '75M',
+          $root => '500M',
+          $vendor => '75M',
           'temp' => '2G',
         ],
         [
@@ -156,22 +133,17 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
    * @param bool $shared_disk
    *   Whether the root and vendor directories are on the same logical disk.
    * @param array $free_space
-   *   The free space that should be reported for various locations. The keys
-   *   are the locations (only 'root', 'vendor', and 'temp' are supported), and
-   *   the values are the space that should be reported, in a format that can be
-   *   parsed by \Drupal\Component\Utility\Bytes::toNumber().
+   *   The free space that should be reported for various paths. The keys
+   *   are the paths, and the values are the free space that should be reported,
+   *   in a format that can be parsed by
+   *   \Drupal\Component\Utility\Bytes::toNumber().
    * @param \Drupal\package_manager\ValidationResult[] $expected_results
    *   The expected validation results.
    *
    * @dataProvider providerDiskSpaceValidation
    */
   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());
+    $this->createTestProject();
 
     /** @var \Drupal\Tests\package_manager\Kernel\TestDiskSpaceValidator $validator */
     $validator = $this->container->get('package_manager.validator.disk_space');
@@ -182,47 +154,3 @@ class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
   }
 
 }
-
-/**
- * A test version of the disk space validator.
- */
-class TestDiskSpaceValidator extends DiskSpaceValidator {
-
-  /**
-   * Whether the root and vendor directories are on the same logical disk.
-   *
-   * @var bool
-   */
-  public $sharedDisk;
-
-  /**
-   * The amount of free space, keyed by location.
-   *
-   * @var float[]
-   */
-  public $freeSpace = [];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function stat(string $path): array {
-    return [
-      'dev' => $this->sharedDisk ? 'disk' : uniqid(),
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function freeSpace(string $path): float {
-    return $this->freeSpace[$path];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function temporaryDirectory(): string {
-    return 'temp';
-  }
-
-}
diff --git a/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php b/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
deleted file mode 100644
index 0489ad4213..0000000000
--- a/package_manager/tests/src/Kernel/ExcludedPathsSubscriberTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-namespace Drupal\Tests\package_manager\Kernel;
-
-use Drupal\Core\Database\Connection;
-use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber;
-
-/**
- * @covers \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
- *
- * @group package_manager
- */
-class ExcludedPathsSubscriberTest extends PackageManagerKernelTestBase {
-
-  /**
-   * Data provider for ::testSqliteDatabaseExcluded().
-   *
-   * @return array[]
-   *   Sets of arguments to pass to the test method.
-   */
-  public function providerSqliteDatabaseExcluded(): array {
-    $drupal_root = $this->getDrupalRoot();
-
-    return [
-      'relative path, in site directory' => [
-        'sites/example.com/db.sqlite',
-        [
-          'sites/example.com/db.sqlite',
-          'sites/example.com/db.sqlite-shm',
-          'sites/example.com/db.sqlite-wal',
-        ],
-      ],
-      'relative path, at root' => [
-        'db.sqlite',
-        [
-          'db.sqlite',
-          'db.sqlite-shm',
-          'db.sqlite-wal',
-        ],
-      ],
-      'absolute path, in site directory' => [
-        $drupal_root . '/sites/example.com/db.sqlite',
-        [
-          'sites/example.com/db.sqlite',
-          'sites/example.com/db.sqlite-shm',
-          'sites/example.com/db.sqlite-wal',
-        ],
-      ],
-      'absolute path, at root' => [
-        $drupal_root . '/db.sqlite',
-        [
-          'db.sqlite',
-          'db.sqlite-shm',
-          'db.sqlite-wal',
-        ],
-      ],
-    ];
-  }
-
-  /**
-   * Tests that SQLite database paths are excluded from the staging area.
-   *
-   * The exclusion of SQLite databases from the staging area is functionally
-   * tested by \Drupal\Tests\package_manager\Functional\ExcludedPathsTest. The
-   * purpose of this test is to ensure that SQLite database paths are processed
-   * properly (e.g., converting an absolute path to a relative path) before
-   * being flagged for exclusion.
-   *
-   * @param string $database
-   *   The path of the SQLite database, as set in the database connection
-   *   options.
-   * @param string[] $expected_exclusions
-   *   The database paths which should be flagged for exclusion.
-   *
-   * @dataProvider providerSqliteDatabaseExcluded
-   *
-   * @see \Drupal\Tests\package_manager\Functional\ExcludedPathsTest
-   */
-  public function testSqliteDatabaseExcluded(string $database, array $expected_exclusions): void {
-    $connection = $this->prophesize(Connection::class);
-    $connection->driver()->willReturn('sqlite');
-    $connection->getConnectionOptions()->willReturn(['database' => $database]);
-
-    $subscriber = new ExcludedPathsSubscriber(
-      'sites/default',
-      $this->container->get('package_manager.symfony_file_system'),
-      $this->container->get('stream_wrapper_manager'),
-      $connection->reveal(),
-      $this->container->get('package_manager.path_locator')
-    );
-
-    $event = new PreCreateEvent($this->createStage());
-    $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/ExcludedPathsTest.php b/package_manager/tests/src/Kernel/ExcludedPathsTest.php
index b9a8eadbe3..776c0ff9a0 100644
--- a/package_manager/tests/src/Kernel/ExcludedPathsTest.php
+++ b/package_manager/tests/src/Kernel/ExcludedPathsTest.php
@@ -3,10 +3,8 @@
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\Core\Database\Connection;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber;
-use Drupal\package_manager\PathLocator;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * @covers \Drupal\package_manager\EventSubscriber\ExcludedPathsSubscriber
@@ -16,153 +14,56 @@ use org\bovigo\vfs\vfsStream;
 class ExcludedPathsTest extends PackageManagerKernelTestBase {
 
   /**
-   * {@inheritdoc}
+   * The mocked SQLite database connection.
+   *
+   * @var \Drupal\Core\Database\Connection|\Prophecy\Prophecy\ObjectProphecy
    */
-  protected function setUp(): void {
-    parent::setUp();
-
-    // Ensure that any staging directories created by TestStage are created
-    // in the virtual file system.
-    TestStage::$stagingRoot = $this->vfsRoot->url();
-
-    // We need to rebuild the container after setting a private file path, since
-    // the private stream wrapper is only registered if this setting is set.
-    // @see \Drupal\Core\CoreServiceProvider::register()
-    $this->setSetting('file_private_path', 'private');
-    $kernel = $this->container->get('kernel');
-    $kernel->rebuildContainer();
-    $this->container = $kernel->getContainer();
-  }
+  private $mockDatabase;
 
   /**
    * {@inheritdoc}
    */
-  public function register(ContainerBuilder $container) {
+  protected function setUp(): void {
+    parent::setUp();
+
     // Normally, package_manager_bypass will disable all the actual staging
     // operations. In this case, we want to perform them so that we can be sure
     // that files are staged as expected.
     $this->setSetting('package_manager_bypass_stager', FALSE);
+    // The private stream wrapper is only registered if this setting is set.
+    // @see \Drupal\Core\CoreServiceProvider::register()
+    $this->setSetting('file_private_path', 'private');
 
-    $container->getDefinition('package_manager.excluded_paths_subscriber')
-      ->setClass(TestExcludedPathsSubscriber::class);
-
-    parent::register($container);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function disableValidators(ContainerBuilder $container): void {
-    parent::disableValidators($container);
-
-    // Disable the disk space validator, since it tries to inspect the file
-    // system in ways that vfsStream doesn't support, like calling stat() and
-    // disk_free_space().
-    $container->removeDefinition('package_manager.validator.disk_space');
+    // Rebuild the container to make the new settings take effect.
+    $kernel = $this->container->get('kernel');
+    $kernel->rebuildContainer();
+    $this->container = $kernel->getContainer();
 
-    // Disable the lock file and Composer settings validators, since in this
-    // test we have an imaginary file system without any Composer files.
-    $container->removeDefinition('package_manager.validator.lock_file');
+    // Mock a SQLite database connection so we can test that the subscriber will
+    // exclude the database files.
+    $this->mockDatabase = $this->prophesize(Connection::class);
+    $this->mockDatabase->driver()->willReturn('sqlite');
   }
 
   /**
    * Tests that certain paths are excluded from staging operations.
    */
   public function testExcludedPaths(): void {
-    $site = [
-      'composer.json' => '{}',
-      'private' => [
-        'ignore.txt' => 'This file should never be staged.',
-      ],
-      'sites' => [
-        'default' => [
-          'services.yml' => <<<END
-# This file should never be staged.
-must_not_be: 'empty'
-END,
-          'settings.local.php' => <<<END
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
-END,
-          'settings.php' => <<<END
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
-END,
-          'stage.txt' => 'This file should be staged.',
-        ],
-        'example.com' => [
-          'files' => [
-            'ignore.txt' => 'This file should never be staged.',
-          ],
-          'db.sqlite' => 'This file should never be staged.',
-          'db.sqlite-shm' => 'This file should never be staged.',
-          'db.sqlite-wal' => 'This file should never be staged.',
-          'services.yml' => <<<END
-# This file should never be staged.
-key: "value"
-END,
-          'settings.local.php' => <<<END
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
-END,
-          'settings.php' => <<<END
-<?php
-
-/**
- * @file
- * This file should never be staged.
- */
-END,
-        ],
-        'simpletest' => [
-          'ignore.txt' => 'This file should never be staged.',
-        ],
-      ],
-      'vendor' => [
-        '.htaccess' => '# This file should never be staged.',
-        'web.config' => 'This file should never be staged.',
-      ],
-    ];
-    vfsStream::create(['active' => $site], $this->vfsRoot);
-
-    $active_dir = $this->vfsRoot->getChild('active')->url();
-
-    $path_locator = $this->prophesize(PathLocator::class);
-    $path_locator->getActiveDirectory()->willReturn($active_dir);
-    $path_locator->getProjectRoot()->willReturn($active_dir);
-    $path_locator->getWebRoot()->willReturn('');
-    $path_locator->getVendorDirectory()->willReturn("$active_dir/vendor");
-    $this->container->set('package_manager.path_locator', $path_locator->reveal());
+    $this->createTestProject();
+    $active_dir = $this->container->get('package_manager.path_locator')
+      ->getActiveDirectory();
 
     $site_path = 'sites/example.com';
     // Ensure that we are using directories within the fake site fixture for
     // public and private files.
     $this->setSetting('file_public_path', "$site_path/files");
 
-    /** @var \Drupal\Tests\package_manager\Kernel\TestExcludedPathsSubscriber $subscriber */
-    $subscriber = $this->container->get('package_manager.excluded_paths_subscriber');
-    $subscriber->sitePath = $site_path;
-
     // Mock a SQLite database connection to a file in the active directory. The
     // file should not be staged.
-    $database = $this->prophesize(Connection::class);
-    $database->driver()->willReturn('sqlite');
-    $database->getConnectionOptions()->willReturn([
+    $this->mockDatabase->getConnectionOptions()->willReturn([
       'database' => $site_path . '/db.sqlite',
     ]);
-    $subscriber->database = $database->reveal();
+    $this->setUpSubscriber($site_path);
 
     $stage = $this->createStage();
     $stage->create();
@@ -185,6 +86,9 @@ END,
       'sites/default/settings.php',
       'sites/default/settings.local.php',
       'sites/default/services.yml',
+      // No git directories should be staged.
+      '.git/ignore.txt',
+      'modules/example/.git/ignore.txt',
     ];
     foreach ($ignore as $path) {
       $this->assertFileExists("$active_dir/$path");
@@ -192,6 +96,10 @@ END,
     }
     // A non-excluded file in the default site directory should be staged.
     $this->assertFileExists("$stage_dir/sites/default/stage.txt");
+    // Regular module files should be staged.
+    $this->assertFileExists("$stage_dir/modules/example/example.info.yml");
+    // Files that start with .git, but aren't actually .git, should be staged.
+    $this->assertFileExists("$stage_dir/.gitignore");
 
     // A new file added to the staging area in an excluded directory, should not
     // be copied to the active directory.
@@ -207,21 +115,94 @@ END,
     }
   }
 
-}
-
-/**
- * A test-only implementation of the excluded path event subscriber.
- */
-class TestExcludedPathsSubscriber extends ExcludedPathsSubscriber {
+  /**
+   * Data provider for ::testSqliteDatabaseExcluded().
+   *
+   * @return array[]
+   *   Sets of arguments to pass to the test method.
+   */
+  public function providerSqliteDatabaseExcluded(): array {
+    $drupal_root = $this->getDrupalRoot();
+
+    return [
+      'relative path, in site directory' => [
+        'sites/example.com/db.sqlite',
+        [
+          'sites/example.com/db.sqlite',
+          'sites/example.com/db.sqlite-shm',
+          'sites/example.com/db.sqlite-wal',
+        ],
+      ],
+      'relative path, at root' => [
+        'db.sqlite',
+        [
+          'db.sqlite',
+          'db.sqlite-shm',
+          'db.sqlite-wal',
+        ],
+      ],
+      'absolute path, in site directory' => [
+        $drupal_root . '/sites/example.com/db.sqlite',
+        [
+          'sites/example.com/db.sqlite',
+          'sites/example.com/db.sqlite-shm',
+          'sites/example.com/db.sqlite-wal',
+        ],
+      ],
+      'absolute path, at root' => [
+        $drupal_root . '/db.sqlite',
+        [
+          'db.sqlite',
+          'db.sqlite-shm',
+          'db.sqlite-wal',
+        ],
+      ],
+    ];
+  }
 
   /**
-   * {@inheritdoc}
+   * Tests that SQLite database paths are excluded from the staging area.
+   *
+   * The exclusion of SQLite databases from the staging area is functionally
+   * tested by \Drupal\Tests\package_manager\Functional\ExcludedPathsTest. The
+   * purpose of this test is to ensure that SQLite database paths are processed
+   * properly (e.g., converting an absolute path to a relative path) before
+   * being flagged for exclusion.
+   *
+   * @param string $database
+   *   The path of the SQLite database, as set in the database connection
+   *   options.
+   * @param string[] $expected_exclusions
+   *   The database paths which should be flagged for exclusion.
+   *
+   * @dataProvider providerSqliteDatabaseExcluded
    */
-  public $sitePath;
+  public function testSqliteDatabaseExcluded(string $database, array $expected_exclusions): void {
+    $this->mockDatabase->getConnectionOptions()->willReturn([
+      'database' => $database,
+    ]);
+
+    $event = new PreCreateEvent($this->createStage());
+    $this->setUpSubscriber();
+    $this->container->get('package_manager.excluded_paths_subscriber')->ignoreCommonPaths($event);
+    // All of the expected exclusions should be flagged.
+    $this->assertEmpty(array_diff($expected_exclusions, $event->getExcludedPaths()));
+  }
 
   /**
-   * {@inheritdoc}
+   * Sets up the event subscriber with a mocked database and site path.
+   *
+   * @param string $site_path
+   *   (optional) The site path. Defaults to 'sites/default'.
    */
-  public $database;
+  private function setUpSubscriber(string $site_path = 'sites/default'): void {
+    $this->container->set('package_manager.excluded_paths_subscriber', new ExcludedPathsSubscriber(
+      $site_path,
+      $this->container->get('package_manager.symfony_file_system'),
+      $this->container->get('stream_wrapper_manager'),
+      $this->mockDatabase->reveal(),
+      $this->container->get('package_manager.path_locator')
+    ));
+  }
 
 }
diff --git a/package_manager/tests/src/Kernel/LockFileValidatorTest.php b/package_manager/tests/src/Kernel/LockFileValidatorTest.php
index c71a6d27e4..ed02f41c3a 100644
--- a/package_manager/tests/src/Kernel/LockFileValidatorTest.php
+++ b/package_manager/tests/src/Kernel/LockFileValidatorTest.php
@@ -2,14 +2,11 @@
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
 use Drupal\package_manager\EventSubscriber\LockFileValidator;
-use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\ValidationResult;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * @coversDefaultClass \Drupal\package_manager\EventSubscriber\LockFileValidator
@@ -19,36 +16,20 @@ use org\bovigo\vfs\vfsStream;
 class LockFileValidatorTest extends PackageManagerKernelTestBase {
 
   /**
-   * {@inheritdoc}
+   * The path of the active directory in the virtual file system.
+   *
+   * @var string
    */
-  protected function setUp(): void {
-    parent::setUp();
-
-    $vendor = vfsStream::newDirectory('vendor');
-    $this->vfsRoot->addChild($vendor);
-
-    $path_locator = $this->prophesize(PathLocator::class);
-    $path_locator->getActiveDirectory()->willReturn($this->vfsRoot->url());
-    $path_locator->getProjectRoot()->willReturn($this->vfsRoot->url());
-    $path_locator->getWebRoot()->willReturn('');
-    $path_locator->getVendorDirectory()->willReturn($vendor->url());
-    $this->container->set('package_manager.path_locator', $path_locator->reveal());
-  }
+  private $activeDir;
 
   /**
    * {@inheritdoc}
    */
-  protected function disableValidators(ContainerBuilder $container): void {
-    parent::disableValidators($container);
-
-    // Disable the disk space validator, since it tries to inspect the file
-    // system in ways that vfsStream doesn't support, like calling stat() and
-    // disk_free_space().
-    $container->removeDefinition('package_manager.validator.disk_space');
-
-    // Disable the Composer settings validator, since it tries to read Composer
-    // files that may not exist in this test.
-    $container->removeDefinition('package_manager.validator.composer_settings');
+  protected function setUp(): void {
+    parent::setUp();
+    $this->createTestProject();
+    $this->activeDir = $this->container->get('package_manager.path_locator')
+      ->getActiveDirectory();
   }
 
   /**
@@ -57,6 +38,8 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
    * @covers ::storeHash
    */
   public function testCreateWithNoLock(): void {
+    unlink($this->activeDir . '/composer.lock');
+
     $no_lock = ValidationResult::createError(['Could not hash the active lock file.']);
     $this->assertResults([$no_lock], PreCreateEvent::class);
   }
@@ -68,12 +51,11 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
    * @covers ::deleteHash
    */
   public function testCreateWithLock(): void {
-    $this->createActiveLockFile();
     $this->assertResults([]);
 
     // Change the lock file to ensure the stored hash of the previous version
     // has been deleted.
-    $this->vfsRoot->getChild('composer.lock')->setContent('"changed"');
+    file_put_contents($this->activeDir . '/composer.lock', 'changed');
     $this->assertResults([]);
   }
 
@@ -83,14 +65,12 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerValidateStageEvents
    */
   public function testLockFileChanged(string $event_class): void {
-    $this->createActiveLockFile();
-
     // Add a listener with an extremely high priority to the same event that
     // should raise the validation error. Because the validator uses the default
     // priority of 0, this listener changes lock file before the validator
     // runs.
     $this->addListener($event_class, function () {
-      $this->vfsRoot->getChild('composer.lock')->setContent('"changed"');
+      file_put_contents($this->activeDir . '/composer.lock', 'changed');
     });
     $result = ValidationResult::createError([
       'Stored lock file hash does not match the active lock file.',
@@ -104,14 +84,12 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerValidateStageEvents
    */
   public function testLockFileDeleted(string $event_class): void {
-    $this->createActiveLockFile();
-
     // Add a listener with an extremely high priority to the same event that
     // should raise the validation error. Because the validator uses the default
     // priority of 0, this listener deletes lock file before the validator
     // runs.
     $this->addListener($event_class, function () {
-      $this->vfsRoot->removeChild('composer.lock');
+      unlink($this->activeDir . '/composer.lock');
     });
     $result = ValidationResult::createError([
       'Could not hash the active lock file.',
@@ -125,8 +103,6 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerValidateStageEvents
    */
   public function testNoStoredHash(string $event_class): void {
-    $this->createActiveLockFile();
-
     $reflector = new \ReflectionClassConstant(LockFileValidator::class, 'STATE_KEY');
     $state_key = $reflector->getValue();
 
@@ -160,14 +136,6 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase {
     ];
   }
 
-  /**
-   * Creates a 'composer.lock' file in the active directory.
-   */
-  private function createActiveLockFile(): void {
-    $lock_file = vfsStream::newFile('composer.lock')->setContent('{}');
-    $this->vfsRoot->addChild($lock_file);
-  }
-
   /**
    * Adds an event listener with the highest possible priority.
    *
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 548535b528..aa26879c9f 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -5,10 +5,13 @@ namespace Drupal\Tests\package_manager\Kernel;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\Event\StageEvent;
+use Drupal\package_manager\EventSubscriber\DiskSpaceValidator;
 use Drupal\package_manager\Exception\StageException;
 use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\Stage;
 use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
+use org\bovigo\vfs\vfsStream;
 
 /**
  * Base class for kernel tests of Package Manager's functionality.
@@ -119,6 +122,129 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
       ->set('existing_updates', $updates);
   }
 
+  /**
+   * Creates a test project in a virtual file system.
+   *
+   * This will create two directories at the root of the virtual file system:
+   * 'active', which is the active directory containing a fake Drupal code base,
+   * and 'stage', which is the root directory used to stage changes. The path
+   * locator service will also be mocked so that it points to the test project.
+   */
+  protected function createTestProject(): void {
+    $tree = [
+      'active' => [
+        'composer.json' => '{}',
+        'composer.lock' => '{}',
+        '.git' => [
+          'ignore.txt' => 'This file should never be staged.',
+        ],
+        '.gitignore' => 'This file should be staged.',
+        'private' => [
+          'ignore.txt' => 'This file should never be staged.',
+        ],
+        'modules' => [
+          'example' => [
+            'example.info.yml' => 'This file should be staged.',
+            '.git' => [
+              'ignore.txt' => 'This file should never be staged.',
+            ],
+          ],
+        ],
+        'sites' => [
+          'default' => [
+            'services.yml' => <<<END
+# This file should never be staged.
+must_not_be: 'empty'
+END,
+            'settings.local.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+            'settings.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+            'stage.txt' => 'This file should be staged.',
+          ],
+          'example.com' => [
+            'files' => [
+              'ignore.txt' => 'This file should never be staged.',
+            ],
+            'db.sqlite' => 'This file should never be staged.',
+            'db.sqlite-shm' => 'This file should never be staged.',
+            'db.sqlite-wal' => 'This file should never be staged.',
+            'services.yml' => <<<END
+# This file should never be staged.
+key: "value"
+END,
+            'settings.local.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+            'settings.php' => <<<END
+<?php
+
+/**
+ * @file
+ * This file should never be staged.
+ */
+END,
+          ],
+          'simpletest' => [
+            'ignore.txt' => 'This file should never be staged.',
+          ],
+        ],
+        'vendor' => [
+          '.htaccess' => '# This file should never be staged.',
+          'web.config' => 'This file should never be staged.',
+        ],
+      ],
+      'stage' => [],
+    ];
+    $root = vfsStream::create($tree, $this->vfsRoot)->url();
+    $active_dir = "$root/active";
+    TestStage::$stagingRoot = "$root/stage";
+
+    $path_locator = $this->prophesize(PathLocator::class);
+    $path_locator->getActiveDirectory()->willReturn($active_dir);
+    $path_locator->getProjectRoot()->willReturn($active_dir);
+    $path_locator->getWebRoot()->willReturn('');
+    $path_locator->getVendorDirectory()->willReturn("$active_dir/vendor");
+
+    // We won't need the prophet anymore.
+    $path_locator = $path_locator->reveal();
+    $this->container->set('package_manager.path_locator', $path_locator);
+
+    // Since the path locator now points to a virtual file system, we need to
+    // replace the disk space validator with a test-only version that bypasses
+    // system calls, like disk_free_space() and stat(), which aren't supported
+    // by vfsStream.
+    $validator = new TestDiskSpaceValidator(
+      $this->container->get('package_manager.path_locator'),
+      $this->container->get('string_translation')
+    );
+    // By default, the validator should report that the root, vendor, and
+    // temporary directories have basically infinite free space.
+    $validator->freeSpace = [
+      $path_locator->getActiveDirectory() => PHP_INT_MAX,
+      $path_locator->getVendorDirectory() => PHP_INT_MAX,
+      $validator->temporaryDirectory() => PHP_INT_MAX,
+    ];
+    $this->container->set('package_manager.validator.disk_space', $validator);
+  }
+
 }
 
 /**
@@ -156,3 +282,47 @@ class TestStage extends Stage {
   }
 
 }
+
+/**
+ * A test version of the disk space validator to bypass system-level functions.
+ */
+class TestDiskSpaceValidator extends DiskSpaceValidator {
+
+  /**
+   * Whether the root and vendor directories are on the same logical disk.
+   *
+   * @var bool
+   */
+  public $sharedDisk = TRUE;
+
+  /**
+   * The amount of free space, keyed by path.
+   *
+   * @var float[]
+   */
+  public $freeSpace = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function stat(string $path): array {
+    return [
+      'dev' => $this->sharedDisk ? 'disk' : uniqid(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function freeSpace(string $path): float {
+    return $this->freeSpace[$path];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function temporaryDirectory(): string {
+    return 'temp';
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/StageEventsTest.php b/package_manager/tests/src/Kernel/StageEventsTest.php
index eb5f46c1b4..8321d259ea 100644
--- a/package_manager/tests/src/Kernel/StageEventsTest.php
+++ b/package_manager/tests/src/Kernel/StageEventsTest.php
@@ -13,7 +13,6 @@ use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
 use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\Exception\StageValidationException;
-use Drupal\package_manager\Stage;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -44,16 +43,7 @@ class StageEventsTest extends PackageManagerKernelTestBase implements EventSubsc
    */
   protected function setUp(): void {
     parent::setUp();
-
-    $this->stage = new Stage(
-      $this->container->get('package_manager.path_locator'),
-      $this->container->get('package_manager.beginner'),
-      $this->container->get('package_manager.stager'),
-      $this->container->get('package_manager.committer'),
-      $this->container->get('file_system'),
-      $this->container->get('event_dispatcher'),
-      $this->container->get('tempstore.shared')
-    );
+    $this->stage = $this->createStage();
   }
 
   /**
diff --git a/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php b/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
index 871d42c4aa..a2dc72efd2 100644
--- a/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
+++ b/package_manager/tests/src/Kernel/WritableFileSystemValidatorTest.php
@@ -6,8 +6,6 @@ use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\EventSubscriber\WritableFileSystemValidator;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\package_manager\PathLocator;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * Unit tests the file system permissions validator.
@@ -39,16 +37,8 @@ class WritableFileSystemValidatorTest extends PackageManagerKernelTestBase {
    * {@inheritdoc}
    */
   protected function disableValidators(ContainerBuilder $container): void {
-    // Disable the disk space validator, since it tries to inspect the file
-    // system in ways that vfsStream doesn't support, like calling stat() and
-    // disk_free_space().
-    $container->removeDefinition('package_manager.validator.disk_space');
-
-    // Disable the lock file and Composer settings validators, since in this
-    // test we are validating an imaginary file system which doesn't have any
-    // Composer files.
-    $container->removeDefinition('package_manager.validator.lock_file');
-    $container->removeDefinition('package_manager.validator.composer_settings');
+    // The parent method disables the validator we're testing, so we don't want
+    // to do anything here.
   }
 
   /**
@@ -58,8 +48,9 @@ class WritableFileSystemValidatorTest extends PackageManagerKernelTestBase {
    *   Sets of arguments to pass to the test method.
    */
   public function providerWritable(): array {
-    $root_error = t('The Drupal directory "vfs://root" is not writable.');
-    $vendor_error = t('The vendor directory "vfs://root/vendor" is not writable.');
+    // The root and vendor paths are defined by ::createTestProject().
+    $root_error = 'The Drupal directory "vfs://root/active" is not writable.';
+    $vendor_error = 'The vendor directory "vfs://root/active/vendor" is not writable.';
     $summary = t('The file system is not writable.');
     $writable_permission = 0777;
     $non_writable_permission = 0444;
@@ -107,20 +98,16 @@ class WritableFileSystemValidatorTest extends PackageManagerKernelTestBase {
    * @dataProvider providerWritable
    */
   public function testWritable(int $root_permissions, int $vendor_permissions, array $expected_results): void {
-    $root = vfsStream::setup('root', $root_permissions);
-    $vendor = vfsStream::newDirectory('vendor', $vendor_permissions);
-    $root->addChild($vendor);
-
-    $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());
+    $this->createTestProject();
+    // For reasons unclear, the built-in chmod() function doesn't seem to work
+    // when changing vendor permissions, so just call vfsStream's API directly.
+    $active_dir = $this->vfsRoot->getChild('active');
+    $active_dir->chmod($root_permissions);
+    $active_dir->getChild('vendor')->chmod($vendor_permissions);
 
     /** @var \Drupal\Tests\package_manager\Kernel\TestWritableFileSystemValidator $validator */
     $validator = $this->container->get('package_manager.validator.file_system');
-    $validator->appRoot = $root->url();
+    $validator->appRoot = $active_dir->url();
 
     $this->assertResults($expected_results, PreCreateEvent::class);
   }
diff --git a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
index 6a2dc5aebf..27f1cb965c 100644
--- a/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/StagedProjectsValidatorTest.php
@@ -118,9 +118,15 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     copy("$fixture/composer.lock", 'public://composer.lock');
 
     $event_dispatcher = $this->container->get('event_dispatcher');
-    // Disable the disk space validator, since it doesn't work with vfsStream.
-    $disk_space_validator = $this->container->get('package_manager.validator.disk_space');
-    $event_dispatcher->removeSubscriber($disk_space_validator);
+    // Disable the disk space validator, since it doesn't work with vfsStream,
+    // and the excluded paths subscriber, since it won't deal with this tiny
+    // virtual file system correctly.
+    $disable_subscribers = array_map([$this->container, 'get'], [
+      'package_manager.validator.disk_space',
+      'package_manager.excluded_paths_subscriber',
+    ]);
+    array_walk($disable_subscribers, [$event_dispatcher, 'removeSubscriber']);
+
     // Just before the staged changes are applied, delete the composer.json file
     // to trigger an error. This uses the highest possible priority to guarantee
     // it runs before any other subscribers.
-- 
GitLab