Loading core/.phpstan-baseline.php +2 −2 Original line number Diff line number Diff line Loading @@ -34082,13 +34082,13 @@ 'message' => '#^Calling md5\\(\\) is forbidden, use hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/ComposerIntegrationTest.php', 'path' => __DIR__ . '/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Calling sha1\\(\\) is forbidden, use sha256\\(\\) or hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/ComposerIntegrationTest.php', 'path' => __DIR__ . '/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\Core\\\\Access\\\\TestAccessCheckInterface\\:\\:access\\(\\) has no return type specified\\.$#', core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php +4 −1 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Core\Recipe\Recipe; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; Loading @@ -16,6 +17,8 @@ */ trait RecipeTestTrait { use ComposerIntegrationTrait; /** * Creates a recipe in a temporary directory. * Loading Loading @@ -100,7 +103,7 @@ protected function runDrupalCommand(array $arguments, int $timeout = 500): Proce // `dr` must be run from the project root. ['install_path' => $project_root] = InstalledVersions::getRootPackage(); array_unshift($arguments, (new PhpExecutableFinder())->find(), 'vendor/bin/dr'); array_unshift($arguments, (new PhpExecutableFinder())->find(), static::binDir() . '/dr'); $process = (new Process($arguments)) ->setWorkingDirectory($project_root) Loading core/tests/Drupal/KernelTests/DrupalApplicationTesterTrait.php +4 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ namespace Drupal\KernelTests; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Symfony\Component\Console\Tester\ApplicationTester; /** Loading @@ -11,11 +12,13 @@ */ trait DrupalApplicationTesterTrait { use ComposerIntegrationTrait; /** * Build our ApplicationTester. */ private function applicationTester(array $context = []): ApplicationTester { $application = include __DIR__ . '/../../../../vendor/bin/dr'; $application = include static::binDir() . '/dr'; $context['kernel.environment'] = 'testing'; $context['kernel.allow_dumping'] = FALSE; $application = $application($context); Loading core/tests/Drupal/Tests/ComposerIntegrationTest.php→core/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php +153 −3 Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ declare(strict_types=1); namespace Drupal\Tests; namespace Drupal\Tests\Composer; use Drupal\Composer\Plugin\VendorHardening\Config; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Finder\Finder; Loading @@ -14,10 +16,158 @@ * Tests Composer integration. */ #[Group('Composer')] #[CoversTrait(ComposerIntegrationTrait::class)] class ComposerIntegrationTest extends UnitTestCase { use ComposerIntegrationTrait; /** * The original COMPOSER_BIN_DIR value, if any. */ protected string|false $originalBinDirEnv; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->originalBinDirEnv = getenv('COMPOSER_BIN_DIR'); // Ensure a clean environment for every test. putenv('COMPOSER_BIN_DIR'); } /** * {@inheritdoc} */ protected function tearDown(): void { if ($this->originalBinDirEnv === FALSE) { putenv('COMPOSER_BIN_DIR'); } else { putenv('COMPOSER_BIN_DIR=' . $this->originalBinDirEnv); } parent::tearDown(); } /** * Creates a virtual project root with an optional composer.json. * * @param array|null $composer * The decoded composer.json contents, or NULL to omit the file. * * @return string * The URL of the virtual project root. */ protected function createProjectRoot(?array $composer): string { $structure = []; if ($composer !== NULL) { $structure['composer.json'] = json_encode($composer); } return vfsStream::setup('project', NULL, $structure)->url(); } /** * Tests bin dir resolution against composer.json contents. * * @param array|null $composer * The decoded composer.json contents, or NULL to omit the file. * @param string $expected * The expected bin dir, relative to the project root. */ #[DataProvider('providerBinDir')] public function testBinDir(?array $composer, string $expected): void { $root = $this->createProjectRoot($composer); $this->assertSame($root . '/' . $expected, self::binDir($root)); } /** * Data provider for testBinDir(). */ public static function providerBinDir(): array { return [ 'no composer.json' => [ NULL, 'vendor/bin', ], 'no config section' => [ ['name' => 'test/project'], 'vendor/bin', ], 'empty config section' => [ ['config' => []], 'vendor/bin', ], 'bin-dir set' => [ ['config' => ['bin-dir' => 'tools']], 'tools', ], 'bin-dir nested' => [ ['config' => ['bin-dir' => 'build/bin']], 'build/bin', ], 'vendor-dir set without bin-dir' => [ ['config' => ['vendor-dir' => 'deps']], 'deps/bin', ], 'bin-dir wins over vendor-dir' => [ ['config' => ['vendor-dir' => 'deps', 'bin-dir' => 'tools']], 'tools', ], ]; } /** * Tests that an absolute bin-dir is returned unchanged. */ public function testAbsoluteBinDir(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => '/opt/project/bin'], ]); $this->assertSame('/opt/project/bin', self::binDir($root)); } /** * Tests that a Windows-style absolute bin-dir is returned unchanged. */ public function testWindowsAbsoluteBinDir(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'C:\\project\\bin'], ]); $this->assertSame('C:\\project\\bin', self::binDir($root)); } /** * Tests that the COMPOSER_BIN_DIR environment variable takes precedence. */ public function testEnvironmentVariableOverride(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'tools'], ]); putenv('COMPOSER_BIN_DIR=env-bin'); $this->assertSame($root . '/env-bin', self::binDir($root)); } /** * Tests that an empty COMPOSER_BIN_DIR environment variable is ignored. */ public function testEmptyEnvironmentVariableIgnored(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'tools'], ]); putenv('COMPOSER_BIN_DIR='); $this->assertSame($root . '/tools', self::binDir($root)); } /** * Tests that malformed composer.json falls back to the default. */ public function testMalformedComposerJson(): void { $root = vfsStream::setup('project', NULL, [ 'composer.json' => '{not valid json', ])->url(); $this->assertSame($root . '/vendor/bin', self::binDir($root)); } /** * Tests composer.lock content-hash. * Loading Loading @@ -81,7 +231,7 @@ public function testComposerTilde(string $path): void { */ public static function providerTestComposerJson(): array { $data = []; $composer_json_finder = self::getComposerJsonFinder(realpath(__DIR__ . '/../../../../')); $composer_json_finder = self::getComposerJsonFinder(realpath(__DIR__ . '/../../../../../')); foreach ($composer_json_finder->getIterator() as $composer_json) { $data[$composer_json->getPathname()] = [$composer_json->getPathname()]; } Loading core/tests/Drupal/Tests/Composer/ComposerIntegrationTrait.php +48 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ namespace Drupal\Tests\Composer; use Composer\InstalledVersions; use Symfony\Component\Finder\Finder; /** Loading Loading @@ -35,4 +36,51 @@ public static function getComposerJsonFinder(string $drupal_root): Finder { return $composer_json_finder; } /** * Gets the absolute path to the Composer bin directory. * * Resolution order follows Composer itself: the COMPOSER_BIN_DIR * environment variable, the "bin-dir" setting in composer.json, the * "vendor-dir" setting plus "/bin", and finally "vendor/bin". * * @param string|null $project_root * The directory containing the root composer.json. If NULL, it is * detected from the Composer runtime API. * * @return string * The absolute path to the bin directory. The directory is not * guaranteed to exist. */ public static function binDir(?string $project_root = NULL): string { if ($project_root === NULL) { $project_root = realpath(InstalledVersions::getRootPackage()['install_path']); } $bin_dir = getenv('COMPOSER_BIN_DIR'); if ($bin_dir === FALSE || $bin_dir === '') { $config = []; $file = $project_root . '/composer.json'; if (is_readable($file)) { $json = json_decode((string) file_get_contents($file), TRUE); $config = $json['config'] ?? []; } if (!empty($config['bin-dir'])) { $bin_dir = $config['bin-dir']; } else { $vendor_dir = !empty($config['vendor-dir']) ? $config['vendor-dir'] : 'vendor'; $bin_dir = $vendor_dir . '/bin'; } } // Relative paths are resolved against the composer.json location. if (!preg_match('{^(?:/|[a-zA-Z]:[\\\\/])}', $bin_dir)) { $bin_dir = $project_root . '/' . $bin_dir; } return $bin_dir; } } Loading
core/.phpstan-baseline.php +2 −2 Original line number Diff line number Diff line Loading @@ -34082,13 +34082,13 @@ 'message' => '#^Calling md5\\(\\) is forbidden, use hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/ComposerIntegrationTest.php', 'path' => __DIR__ . '/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Calling sha1\\(\\) is forbidden, use sha256\\(\\) or hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/ComposerIntegrationTest.php', 'path' => __DIR__ . '/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\Core\\\\Access\\\\TestAccessCheckInterface\\:\\:access\\(\\) has no return type specified\\.$#',
core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php +4 −1 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Core\Recipe\Recipe; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; Loading @@ -16,6 +17,8 @@ */ trait RecipeTestTrait { use ComposerIntegrationTrait; /** * Creates a recipe in a temporary directory. * Loading Loading @@ -100,7 +103,7 @@ protected function runDrupalCommand(array $arguments, int $timeout = 500): Proce // `dr` must be run from the project root. ['install_path' => $project_root] = InstalledVersions::getRootPackage(); array_unshift($arguments, (new PhpExecutableFinder())->find(), 'vendor/bin/dr'); array_unshift($arguments, (new PhpExecutableFinder())->find(), static::binDir() . '/dr'); $process = (new Process($arguments)) ->setWorkingDirectory($project_root) Loading
core/tests/Drupal/KernelTests/DrupalApplicationTesterTrait.php +4 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ namespace Drupal\KernelTests; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Symfony\Component\Console\Tester\ApplicationTester; /** Loading @@ -11,11 +12,13 @@ */ trait DrupalApplicationTesterTrait { use ComposerIntegrationTrait; /** * Build our ApplicationTester. */ private function applicationTester(array $context = []): ApplicationTester { $application = include __DIR__ . '/../../../../vendor/bin/dr'; $application = include static::binDir() . '/dr'; $context['kernel.environment'] = 'testing'; $context['kernel.allow_dumping'] = FALSE; $application = $application($context); Loading
core/tests/Drupal/Tests/ComposerIntegrationTest.php→core/tests/Drupal/Tests/Composer/ComposerIntegrationTest.php +153 −3 Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ declare(strict_types=1); namespace Drupal\Tests; namespace Drupal\Tests\Composer; use Drupal\Composer\Plugin\VendorHardening\Config; use Drupal\Tests\Composer\ComposerIntegrationTrait; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Finder\Finder; Loading @@ -14,10 +16,158 @@ * Tests Composer integration. */ #[Group('Composer')] #[CoversTrait(ComposerIntegrationTrait::class)] class ComposerIntegrationTest extends UnitTestCase { use ComposerIntegrationTrait; /** * The original COMPOSER_BIN_DIR value, if any. */ protected string|false $originalBinDirEnv; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->originalBinDirEnv = getenv('COMPOSER_BIN_DIR'); // Ensure a clean environment for every test. putenv('COMPOSER_BIN_DIR'); } /** * {@inheritdoc} */ protected function tearDown(): void { if ($this->originalBinDirEnv === FALSE) { putenv('COMPOSER_BIN_DIR'); } else { putenv('COMPOSER_BIN_DIR=' . $this->originalBinDirEnv); } parent::tearDown(); } /** * Creates a virtual project root with an optional composer.json. * * @param array|null $composer * The decoded composer.json contents, or NULL to omit the file. * * @return string * The URL of the virtual project root. */ protected function createProjectRoot(?array $composer): string { $structure = []; if ($composer !== NULL) { $structure['composer.json'] = json_encode($composer); } return vfsStream::setup('project', NULL, $structure)->url(); } /** * Tests bin dir resolution against composer.json contents. * * @param array|null $composer * The decoded composer.json contents, or NULL to omit the file. * @param string $expected * The expected bin dir, relative to the project root. */ #[DataProvider('providerBinDir')] public function testBinDir(?array $composer, string $expected): void { $root = $this->createProjectRoot($composer); $this->assertSame($root . '/' . $expected, self::binDir($root)); } /** * Data provider for testBinDir(). */ public static function providerBinDir(): array { return [ 'no composer.json' => [ NULL, 'vendor/bin', ], 'no config section' => [ ['name' => 'test/project'], 'vendor/bin', ], 'empty config section' => [ ['config' => []], 'vendor/bin', ], 'bin-dir set' => [ ['config' => ['bin-dir' => 'tools']], 'tools', ], 'bin-dir nested' => [ ['config' => ['bin-dir' => 'build/bin']], 'build/bin', ], 'vendor-dir set without bin-dir' => [ ['config' => ['vendor-dir' => 'deps']], 'deps/bin', ], 'bin-dir wins over vendor-dir' => [ ['config' => ['vendor-dir' => 'deps', 'bin-dir' => 'tools']], 'tools', ], ]; } /** * Tests that an absolute bin-dir is returned unchanged. */ public function testAbsoluteBinDir(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => '/opt/project/bin'], ]); $this->assertSame('/opt/project/bin', self::binDir($root)); } /** * Tests that a Windows-style absolute bin-dir is returned unchanged. */ public function testWindowsAbsoluteBinDir(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'C:\\project\\bin'], ]); $this->assertSame('C:\\project\\bin', self::binDir($root)); } /** * Tests that the COMPOSER_BIN_DIR environment variable takes precedence. */ public function testEnvironmentVariableOverride(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'tools'], ]); putenv('COMPOSER_BIN_DIR=env-bin'); $this->assertSame($root . '/env-bin', self::binDir($root)); } /** * Tests that an empty COMPOSER_BIN_DIR environment variable is ignored. */ public function testEmptyEnvironmentVariableIgnored(): void { $root = $this->createProjectRoot([ 'config' => ['bin-dir' => 'tools'], ]); putenv('COMPOSER_BIN_DIR='); $this->assertSame($root . '/tools', self::binDir($root)); } /** * Tests that malformed composer.json falls back to the default. */ public function testMalformedComposerJson(): void { $root = vfsStream::setup('project', NULL, [ 'composer.json' => '{not valid json', ])->url(); $this->assertSame($root . '/vendor/bin', self::binDir($root)); } /** * Tests composer.lock content-hash. * Loading Loading @@ -81,7 +231,7 @@ public function testComposerTilde(string $path): void { */ public static function providerTestComposerJson(): array { $data = []; $composer_json_finder = self::getComposerJsonFinder(realpath(__DIR__ . '/../../../../')); $composer_json_finder = self::getComposerJsonFinder(realpath(__DIR__ . '/../../../../../')); foreach ($composer_json_finder->getIterator() as $composer_json) { $data[$composer_json->getPathname()] = [$composer_json->getPathname()]; } Loading
core/tests/Drupal/Tests/Composer/ComposerIntegrationTrait.php +48 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ namespace Drupal\Tests\Composer; use Composer\InstalledVersions; use Symfony\Component\Finder\Finder; /** Loading Loading @@ -35,4 +36,51 @@ public static function getComposerJsonFinder(string $drupal_root): Finder { return $composer_json_finder; } /** * Gets the absolute path to the Composer bin directory. * * Resolution order follows Composer itself: the COMPOSER_BIN_DIR * environment variable, the "bin-dir" setting in composer.json, the * "vendor-dir" setting plus "/bin", and finally "vendor/bin". * * @param string|null $project_root * The directory containing the root composer.json. If NULL, it is * detected from the Composer runtime API. * * @return string * The absolute path to the bin directory. The directory is not * guaranteed to exist. */ public static function binDir(?string $project_root = NULL): string { if ($project_root === NULL) { $project_root = realpath(InstalledVersions::getRootPackage()['install_path']); } $bin_dir = getenv('COMPOSER_BIN_DIR'); if ($bin_dir === FALSE || $bin_dir === '') { $config = []; $file = $project_root . '/composer.json'; if (is_readable($file)) { $json = json_decode((string) file_get_contents($file), TRUE); $config = $json['config'] ?? []; } if (!empty($config['bin-dir'])) { $bin_dir = $config['bin-dir']; } else { $vendor_dir = !empty($config['vendor-dir']) ? $config['vendor-dir'] : 'vendor'; $bin_dir = $vendor_dir . '/bin'; } } // Relative paths are resolved against the composer.json location. if (!preg_match('{^(?:/|[a-zA-Z]:[\\\\/])}', $bin_dir)) { $bin_dir = $project_root . '/' . $bin_dir; } return $bin_dir; } }