Verified Commit b265e035 authored by Dave Long's avatar Dave Long
Browse files

test: #3609757 Composer bin-dir setting is ignored

By: fjgarlin
By: jonathan1055
By: longwave
(cherry picked from commit a8ea302a)
parent 06b9973e
Loading
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -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;

@@ -16,6 +17,8 @@
 */
trait RecipeTestTrait {

  use ComposerIntegrationTrait;

  /**
   * Creates a recipe in a temporary directory.
   *
@@ -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)
+4 −1
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

namespace Drupal\KernelTests;

use Drupal\Tests\Composer\ComposerIntegrationTrait;
use Symfony\Component\Console\Tester\ApplicationTester;

/**
@@ -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);
+153 −3
Original line number Diff line number Diff line
@@ -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;
@@ -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.
   *
@@ -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()];
    }
+48 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

namespace Drupal\Tests\Composer;

use Composer\InstalledVersions;
use Symfony\Component\Finder\Finder;

/**
@@ -35,4 +36,51 @@ public static function getComposerJsonFinder($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;
  }

}