From 5ef2211afcbf49b6e9d6659c703afe7e4519cb11 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Sat, 25 Nov 2023 11:33:10 +0000
Subject: [PATCH] Issue #3391776 by alexpott, smustgrave, catch: InfoParser
 returns an empty array if passed a non-existing file

---
 .../Core/Extension/InfoParserDynamic.php      | 108 +++++++++---------
 .../Core/Test/FunctionalTestSetupTrait.php    |   2 -
 .../Core/Theme/BaseThemeMissingTest.php       |  78 ++-----------
 .../Drupal/KernelTests/KernelTestBase.php     |   3 -
 .../Commands/TestSiteReleaseLocksCommand.php  |   2 +
 .../Commands/TestSiteTearDownCommand.php      |   2 +
 .../Tests/Core/Asset/CssOptimizerUnitTest.php |   5 -
 .../Tests/Core/Command/QuickStartTest.php     |   1 -
 .../Tests/Core/Database/UrlConversionTest.php |  10 --
 .../Core/Extension/InfoParserUnitTest.php     |   5 +-
 .../Tests/Scripts/TestSiteApplicationTest.php |   1 -
 core/tests/bootstrap.php                      |   4 +
 12 files changed, 74 insertions(+), 147 deletions(-)

diff --git a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
index 8298223a374c..fc6e8a56265e 100644
--- a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
+++ b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
@@ -37,68 +37,68 @@ public function __construct(string $app_root = NULL) {
    */
   public function parse($filename) {
     if (!file_exists($filename)) {
-      $parsed_info = [];
+      throw new InfoParserException("Unable to parse $filename as it does not exist");
     }
-    else {
-      try {
-        $parsed_info = Yaml::decode(file_get_contents($filename));
-      }
-      catch (InvalidDataTypeException $e) {
-        throw new InfoParserException("Unable to parse $filename " . $e->getMessage());
-      }
-      $missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
-      if (!empty($missing_keys)) {
-        throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
-      }
-      if (!isset($parsed_info['core_version_requirement'])) {
-        if (str_starts_with($filename, 'core/') || str_starts_with($filename, $this->root . '/core/')) {
-          // Core extensions do not need to specify core compatibility: they are
-          // by definition compatible so a sensible default is used. Core
-          // modules are allowed to provide these for testing purposes.
-          $parsed_info['core_version_requirement'] = \Drupal::VERSION;
-        }
-        elseif (isset($parsed_info['package']) && $parsed_info['package'] === 'Testing') {
-          // Modules in the testing package are exempt as well. This makes it
-          // easier for contrib to use test modules.
-          $parsed_info['core_version_requirement'] = \Drupal::VERSION;
-        }
-        else {
-          // Non-core extensions must specify core compatibility.
-          throw new InfoParserException("The 'core_version_requirement' key must be present in " . $filename);
-        }
-      }
 
-      // Determine if the extension is compatible with the current version of
-      // Drupal core.
-      try {
-        $parsed_info['core_incompatible'] = !Semver::satisfies(\Drupal::VERSION, $parsed_info['core_version_requirement']);
+    try {
+      $parsed_info = Yaml::decode(file_get_contents($filename));
+    }
+    catch (InvalidDataTypeException $e) {
+      throw new InfoParserException("Unable to parse $filename " . $e->getMessage());
+    }
+    $missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
+    if (!empty($missing_keys)) {
+      throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
+    }
+    if (!isset($parsed_info['core_version_requirement'])) {
+      if (str_starts_with($filename, 'core/') || str_starts_with($filename, $this->root . '/core/')) {
+        // Core extensions do not need to specify core compatibility: they are
+        // by definition compatible so a sensible default is used. Core
+        // modules are allowed to provide these for testing purposes.
+        $parsed_info['core_version_requirement'] = \Drupal::VERSION;
       }
-      catch (\UnexpectedValueException $exception) {
-        throw new InfoParserException("The 'core_version_requirement' constraint ({$parsed_info['core_version_requirement']}) is not a valid value in $filename");
+      elseif (isset($parsed_info['package']) && $parsed_info['package'] === 'Testing') {
+        // Modules in the testing package are exempt as well. This makes it
+        // easier for contrib to use test modules.
+        $parsed_info['core_version_requirement'] = \Drupal::VERSION;
       }
-      if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
-        $parsed_info['version'] = \Drupal::VERSION;
+      else {
+        // Non-core extensions must specify core compatibility.
+        throw new InfoParserException("The 'core_version_requirement' key must be present in " . $filename);
       }
-      $parsed_info += [ExtensionLifecycle::LIFECYCLE_IDENTIFIER => ExtensionLifecycle::STABLE];
-      $lifecycle = $parsed_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
-      if (!ExtensionLifecycle::isValid($lifecycle)) {
-        $valid_values = [
-          ExtensionLifecycle::EXPERIMENTAL,
-          ExtensionLifecycle::STABLE,
-          ExtensionLifecycle::DEPRECATED,
-          ExtensionLifecycle::OBSOLETE,
-        ];
-        throw new InfoParserException("'lifecycle: {$lifecycle}' is not valid in $filename. Valid values are: '" . implode("', '", $valid_values) . "'.");
+    }
+
+    // Determine if the extension is compatible with the current version of
+    // Drupal core.
+    try {
+      $parsed_info['core_incompatible'] = !Semver::satisfies(\Drupal::VERSION, $parsed_info['core_version_requirement']);
+    }
+    catch (\UnexpectedValueException $exception) {
+      throw new InfoParserException("The 'core_version_requirement' constraint ({$parsed_info['core_version_requirement']}) is not a valid value in $filename");
+    }
+    if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
+      $parsed_info['version'] = \Drupal::VERSION;
+    }
+    $parsed_info += [ExtensionLifecycle::LIFECYCLE_IDENTIFIER => ExtensionLifecycle::STABLE];
+    $lifecycle = $parsed_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
+    if (!ExtensionLifecycle::isValid($lifecycle)) {
+      $valid_values = [
+        ExtensionLifecycle::EXPERIMENTAL,
+        ExtensionLifecycle::STABLE,
+        ExtensionLifecycle::DEPRECATED,
+        ExtensionLifecycle::OBSOLETE,
+      ];
+      throw new InfoParserException("'lifecycle: {$lifecycle}' is not valid in $filename. Valid values are: '" . implode("', '", $valid_values) . "'.");
+    }
+    if (in_array($lifecycle, [ExtensionLifecycle::DEPRECATED, ExtensionLifecycle::OBSOLETE], TRUE)) {
+      if (empty($parsed_info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER])) {
+        throw new InfoParserException(sprintf("Extension %s (%s) has 'lifecycle: %s' but is missing a '%s' entry.", $parsed_info['name'], $filename, $lifecycle, ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER));
       }
-      if (in_array($lifecycle, [ExtensionLifecycle::DEPRECATED, ExtensionLifecycle::OBSOLETE], TRUE)) {
-        if (empty($parsed_info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER])) {
-          throw new InfoParserException(sprintf("Extension %s (%s) has 'lifecycle: %s' but is missing a '%s' entry.", $parsed_info['name'], $filename, $lifecycle, ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER));
-        }
-        if (!filter_var($parsed_info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER], FILTER_VALIDATE_URL)) {
-          throw new InfoParserException(sprintf("Extension %s (%s) has a '%s' entry that is not a valid URL.", $parsed_info['name'], $filename, ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER));
-        }
+      if (!filter_var($parsed_info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER], FILTER_VALIDATE_URL)) {
+        throw new InfoParserException(sprintf("Extension %s (%s) has a '%s' entry that is not a valid URL.", $parsed_info['name'], $filename, ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER));
       }
     }
+
     return $parsed_info;
   }
 
diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
index 48c28243b024..2e7083ff86f0 100644
--- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
+++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
@@ -633,8 +633,6 @@ protected function prepareEnvironment() {
     $this->classLoader = require __DIR__ . '/../../../../../autoload.php';
     $request = Request::createFromGlobals();
     $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);
-    // TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
-    chdir(DRUPAL_ROOT);
     $kernel->boot();
     $kernel->preHandle($request);
     $this->prepareDatabasePrefix();
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/BaseThemeMissingTest.php b/core/tests/Drupal/KernelTests/Core/Theme/BaseThemeMissingTest.php
index 334bde6d1bac..44f7c5638cb5 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/BaseThemeMissingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/BaseThemeMissingTest.php
@@ -2,12 +2,9 @@
 
 namespace Drupal\KernelTests\Core\Theme;
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Extension\InfoParserException;
-use Drupal\Core\Extension\ThemeExtensionList;
+use Drupal\Core\Site\Settings;
 use Drupal\KernelTests\KernelTestBase;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * Tests the behavior of a theme when base_theme info key is missing.
@@ -34,80 +31,23 @@ class BaseThemeMissingTest extends KernelTestBase {
   protected function setUp(): void {
     parent::setUp();
 
-    $this->themeInstaller = $this->container->get('theme_installer');
-  }
+    // Add a directory to extension discovery to find the theme with a missing
+    // base class.
+    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
+    $settings = Settings::getAll();
+    $settings['test_parent_site'] = 'core/tests/fixtures/test_missing_base_theme';
+    new Settings($settings);
 
-  /**
-   * {@inheritdoc}
-   */
-  public function register(ContainerBuilder $container) {
-    parent::register($container);
-
-    $container->getDefinition('extension.list.theme')
-      ->setClass(VfsThemeExtensionList::class);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUpFilesystem() {
-    parent::setUpFilesystem();
-
-    $vfs_root = vfsStream::setup('core');
-    vfsStream::create([
-      'themes' => [
-        'test_missing_base_theme' => [
-          'test_missing_base_theme.info.yml' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_missing_base_theme/test_missing_base_theme.info.yml'),
-          'test_missing_base_theme.theme' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_missing_base_theme/test_missing_base_theme.theme'),
-        ],
-      ],
-    ], $vfs_root);
+    $this->themeInstaller = $this->container->get('theme_installer');
   }
 
   /**
    * Tests exception is thrown.
    */
   public function testMissingBaseThemeException() {
-    $this->container->get('extension.list.theme')
-      ->setExtensionDiscovery(new ExtensionDiscovery('vfs://core'));
-
     $this->expectException(InfoParserException::class);
-    $this->expectExceptionMessage('Missing required key ("base theme") in themes/test_missing_base_theme/test_missing_base_theme.info.yml, see https://www.drupal.org/node/3066038');
+    $this->expectExceptionMessage('Missing required key ("base theme") in core/tests/fixtures/test_missing_base_theme/test_missing_base_theme.info.yml, see https://www.drupal.org/node/3066038');
     $this->themeInstaller->install(['test_missing_base_theme']);
   }
 
 }
-
-/**
- * Test theme extension list class.
- */
-class VfsThemeExtensionList extends ThemeExtensionList {
-
-  /**
-   * The extension discovery for this extension list.
-   *
-   * @var \Drupal\Core\Extension\ExtensionDiscovery
-   */
-  protected $extensionDiscovery;
-
-  /**
-   * Sets the extension discovery.
-   *
-   * @param \Drupal\Core\Extension\ExtensionDiscovery $discovery
-   *   The extension discovery.
-   *
-   * @return self
-   */
-  public function setExtensionDiscovery(ExtensionDiscovery $discovery) {
-    $this->extensionDiscovery = $discovery;
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getExtensionDiscovery() {
-    return $this->extensionDiscovery;
-  }
-
-}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 3acf47a5eccb..46b4d8801899 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -241,9 +241,6 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
   public static function setUpBeforeClass(): void {
     parent::setUpBeforeClass();
     VarDumper::setHandler(TestVarDumper::class . '::cliHandler');
-
-    // Change the current dir to DRUPAL_ROOT.
-    chdir(static::getDrupalRoot());
   }
 
   /**
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLocksCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLocksCommand.php
index c88abb84ab18..f2b606aec75d 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLocksCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteReleaseLocksCommand.php
@@ -30,6 +30,8 @@ protected function configure() {
    * {@inheritdoc}
    */
   protected function execute(InputInterface $input, OutputInterface $output): int {
+    $root = dirname(__DIR__, 5);
+    chdir($root);
     TestDatabase::releaseAllTestLocks();
     $output->writeln('<info>Successfully released all the test database locks</info>');
     return 0;
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
index a2b5f54e3203..efde0539d947 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
@@ -38,6 +38,8 @@ protected function configure() {
    * {@inheritdoc}
    */
   protected function execute(InputInterface $input, OutputInterface $output): int {
+    $root = dirname(__DIR__, 5);
+    chdir($root);
     $db_prefix = $input->getArgument('db-prefix');
     // Validate the db_prefix argument.
     try {
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
index 347759527f1a..e4833ec41a2b 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
@@ -247,11 +247,6 @@ public function testOptimize($css_asset, $expected) {
     $original_base_path = $base_path;
     $base_path = '/';
 
-    // \Drupal\Core\Asset\CssOptimizer::loadFile() relies on the current working
-    // directory being the one that is used when index.php is the entry point.
-    // Note: PHPUnit automatically restores the original working directory.
-    chdir(realpath(__DIR__ . '/../../../../../../'));
-
     $this->assertEquals($expected, $this->optimizer->optimize($css_asset), 'Group of file CSS assets optimized correctly.');
 
     $base_path = $original_base_path;
diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
index 536a67398f1d..ff6c3c5462ac 100644
--- a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
+++ b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
@@ -54,7 +54,6 @@ protected function setUp(): void {
     $php_executable_finder = new PhpExecutableFinder();
     $this->php = $php_executable_finder->find();
     $this->root = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)), 2);
-    chdir($this->root);
     if (!is_writable("{$this->root}/sites/simpletest")) {
       $this->markTestSkipped('This test requires a writable sites/simpletest directory');
     }
diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
index c3f75582bace..9a93304e7841 100644
--- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
@@ -21,16 +21,6 @@
  */
 class UrlConversionTest extends UnitTestCase {
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp(): void {
-    parent::setUp();
-    $this->root = dirname(__FILE__, 7);
-    // This unit test relies on reading files relative to Drupal root.
-    chdir($this->root);
-  }
-
   /**
    * @covers ::convertDbUrlToConnectionInfo
    *
diff --git a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
index 8b51ed22c32d..c1cf06d64057 100644
--- a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
@@ -45,8 +45,9 @@ protected function setUp(): void {
    */
   public function testInfoParserNonExisting() {
     vfsStream::setup('modules');
-    $info = $this->infoParser->parse(vfsStream::url('modules') . '/does_not_exist.info.txt');
-    $this->assertEmpty($info, 'Non existing info.yml returns empty array.');
+    $this->expectException('\Drupal\Core\Extension\InfoParserException');
+    $this->expectExceptionMessage('Unable to parse vfs://modules/does_not_exist.info.txt as it does not exist');
+    $this->infoParser->parse(vfsStream::url('modules') . '/does_not_exist.info.txt');
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php
index 490e49242ad7..d15ef7946e83 100644
--- a/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php
+++ b/core/tests/Drupal/Tests/Scripts/TestSiteApplicationTest.php
@@ -42,7 +42,6 @@ protected function setUp(): void {
     parent::setUp();
     $php_executable_finder = new PhpExecutableFinder();
     $this->php = $php_executable_finder->find();
-    $this->root = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)), 2);
   }
 
   /**
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index 0ea13b0f3937..a5718d049a46 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -179,3 +179,7 @@ class_alias('\Drupal\Tests\DocumentElement', '\Behat\Mink\Element\DocumentElemen
   $deprecation_ignore_filename = realpath(__DIR__ . "/../.deprecation-ignore.txt");
   putenv("SYMFONY_DEPRECATIONS_HELPER=ignoreFile=$deprecation_ignore_filename");
 }
+
+// Drupal expects to be run from its root directory. This ensures all test types
+// are consistent.
+chdir(dirname(__DIR__, 2));
-- 
GitLab