Initial Drupal 11 with DDEV setup
This commit is contained in:
		@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer\Component;
 | 
			
		||||
 | 
			
		||||
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
 | 
			
		||||
use Drupal\Composer\Composer;
 | 
			
		||||
use Symfony\Component\Finder\Finder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Try to install dependencies per component, using Composer.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Composer
 | 
			
		||||
 * @group Component
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Provides an array with relative paths to the component paths.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   An array with relative paths to the component paths.
 | 
			
		||||
   */
 | 
			
		||||
  public static function provideComponentPaths(): array {
 | 
			
		||||
    $data = [];
 | 
			
		||||
    // During the dataProvider phase, there is not a workspace directory yet.
 | 
			
		||||
    // So we will find relative paths and assemble them with the workspace
 | 
			
		||||
    // path later.
 | 
			
		||||
    $drupal_root = self::getDrupalRootStatic();
 | 
			
		||||
    $composer_json_finder = self::getComponentPathsFinder($drupal_root);
 | 
			
		||||
 | 
			
		||||
    /** @var \Symfony\Component\Finder\SplFileInfo $path */
 | 
			
		||||
    foreach ($composer_json_finder->getIterator() as $path) {
 | 
			
		||||
      $data[$path->getRelativePath()] = ['/' . $path->getRelativePath()];
 | 
			
		||||
    }
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test whether components' composer.json can be installed in isolation.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider provideComponentPaths
 | 
			
		||||
   */
 | 
			
		||||
  public function testComponentComposerJson(string $component_path): void {
 | 
			
		||||
    // Only copy the components. Copy all of them because some of them depend on
 | 
			
		||||
    // each other.
 | 
			
		||||
    $finder = new Finder();
 | 
			
		||||
    $finder->files()
 | 
			
		||||
      ->ignoreUnreadableDirs()
 | 
			
		||||
      ->in($this->getDrupalRoot() . static::$componentsPath)
 | 
			
		||||
      ->ignoreDotFiles(FALSE)
 | 
			
		||||
      ->ignoreVCS(FALSE);
 | 
			
		||||
    $this->copyCodebase($finder->getIterator());
 | 
			
		||||
 | 
			
		||||
    $working_dir = $this->getWorkingPath() . static::$componentsPath . $component_path;
 | 
			
		||||
 | 
			
		||||
    // We add path repositories so we can wire internal dependencies together.
 | 
			
		||||
    $this->addExpectedRepositories($working_dir);
 | 
			
		||||
 | 
			
		||||
    // Perform the installation.
 | 
			
		||||
    $this->executeCommand("composer install --working-dir=$working_dir --no-interaction --no-progress");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds expected repositories as path repositories to package under test.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $working_dir
 | 
			
		||||
   *   The working directory.
 | 
			
		||||
   */
 | 
			
		||||
  protected function addExpectedRepositories(string $working_dir): void {
 | 
			
		||||
    foreach ($this->provideComponentPaths() as $path) {
 | 
			
		||||
      $path = $path[0];
 | 
			
		||||
      $package_name = 'drupal/core' . strtolower(preg_replace('/[A-Z]/', '-$0', substr($path, 1)));
 | 
			
		||||
      $path_repo = $this->getWorkingPath() . static::$componentsPath . $path;
 | 
			
		||||
      $repo_name = strtolower($path);
 | 
			
		||||
      // Add path repositories with the current version number to the current
 | 
			
		||||
      // package under test.
 | 
			
		||||
      $drupal_version = Composer::drupalVersionBranch();
 | 
			
		||||
      $this->executeCommand("composer config repositories.$repo_name " .
 | 
			
		||||
        "'{\"type\": \"path\",\"url\": \"$path_repo\",\"options\": {\"versions\": {\"$package_name\": \"$drupal_version\"}}}' --working-dir=$working_dir");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,77 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer\Component;
 | 
			
		||||
 | 
			
		||||
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
 | 
			
		||||
use Drupal\Composer\Composer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Demonstrate that the Component generator responds to release tagging.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Composer
 | 
			
		||||
 * @group Component
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Highly arbitrary version and constraint expectations.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   - First element is the tag that should be applied to \Drupal::version.
 | 
			
		||||
   *   - Second element is the resulting constraint which should be present in
 | 
			
		||||
   *     the component core dependencies.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerVersionConstraint(): array {
 | 
			
		||||
    return [
 | 
			
		||||
      // [Tag, constraint]
 | 
			
		||||
      '1.0.x-dev' => ['1.0.x-dev', '1.0.x-dev'],
 | 
			
		||||
      '1.0.0-beta1' => ['1.0.0-beta1', '1.0.0-beta1'],
 | 
			
		||||
      '1.0.0-rc1' => ['1.0.0-rc1', '1.0.0-rc1'],
 | 
			
		||||
      '1.0.0' => ['1.0.0', '^1.0'],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Validate release tagging and regeneration of dependencies.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerVersionConstraint
 | 
			
		||||
   */
 | 
			
		||||
  public function testReleaseTagging(string $tag, string $constraint): void {
 | 
			
		||||
    $this->copyCodebase();
 | 
			
		||||
    $drupal_root = $this->getWorkspaceDirectory();
 | 
			
		||||
 | 
			
		||||
    // Set the core version.
 | 
			
		||||
    Composer::setDrupalVersion($drupal_root, $tag);
 | 
			
		||||
    $this->assertDrupalVersion($tag, $drupal_root);
 | 
			
		||||
 | 
			
		||||
    // Emulate the release script.
 | 
			
		||||
    // @see https://github.com/xjm/drupal_core_release/blob/main/tag.sh
 | 
			
		||||
    $this->executeCommand("COMPOSER_ROOT_VERSION=\"$tag\" composer update drupal/core*");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    $this->assertErrorOutputContains('generateComponentPackages');
 | 
			
		||||
 | 
			
		||||
    // Find all the components.
 | 
			
		||||
    $component_finder = $this->getComponentPathsFinder($drupal_root);
 | 
			
		||||
 | 
			
		||||
    // Loop through all the component packages.
 | 
			
		||||
    /** @var \Symfony\Component\Finder\SplFileInfo $composer_json */
 | 
			
		||||
    foreach ($component_finder->getIterator() as $composer_json) {
 | 
			
		||||
      $composer_json_data = json_decode(file_get_contents($composer_json->getPathname()), TRUE);
 | 
			
		||||
      $requires = array_merge(
 | 
			
		||||
        $composer_json_data['require'] ?? [],
 | 
			
		||||
        $composer_json_data['require-dev'] ?? []
 | 
			
		||||
      );
 | 
			
		||||
      // Required packages from drupal/core-* should have our constraint.
 | 
			
		||||
      foreach ($requires as $package => $req_constraint) {
 | 
			
		||||
        if (str_contains($package, 'drupal/core-')) {
 | 
			
		||||
          $this->assertEquals($constraint, $req_constraint);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,64 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer;
 | 
			
		||||
 | 
			
		||||
use Drupal\BuildTests\Framework\BuildTestBase;
 | 
			
		||||
use Symfony\Component\Finder\Finder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for Composer build tests.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
abstract class ComposerBuildTestBase extends BuildTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Relative path from Drupal root to the Components directory.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected static $componentsPath = '/core/lib/Drupal/Component';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert that the VERSION constant in Drupal.php is the expected value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $expectedVersion
 | 
			
		||||
   *   The expected version.
 | 
			
		||||
   * @param string $dir
 | 
			
		||||
   *   The path to the site root.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertDrupalVersion(string $expectedVersion, string $dir): void {
 | 
			
		||||
    $drupal_php_path = $dir . '/core/lib/Drupal.php';
 | 
			
		||||
    $this->assertFileExists($drupal_php_path);
 | 
			
		||||
 | 
			
		||||
    // Read back the Drupal version that was set and assert it matches
 | 
			
		||||
    // expectations
 | 
			
		||||
    $this->executeCommand("php -r 'include \"$drupal_php_path\"; print \Drupal::VERSION;'");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    $this->assertCommandOutputContains($expectedVersion);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find all the composer.json files for components.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $drupal_root
 | 
			
		||||
   *   The Drupal root directory.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Symfony\Component\Finder\Finder
 | 
			
		||||
   *   A Finder object with all the composer.json files for components.
 | 
			
		||||
   */
 | 
			
		||||
  protected static function getComponentPathsFinder(string $drupal_root): Finder {
 | 
			
		||||
    $finder = new Finder();
 | 
			
		||||
    $finder->name('composer.json')
 | 
			
		||||
      ->in($drupal_root . static::$componentsPath)
 | 
			
		||||
      ->ignoreUnreadableDirs()
 | 
			
		||||
      ->depth(1);
 | 
			
		||||
 | 
			
		||||
    return $finder;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer;
 | 
			
		||||
 | 
			
		||||
use Drupal\BuildTests\Framework\BuildTestBase;
 | 
			
		||||
use Drupal\Tests\Composer\ComposerIntegrationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @group Composer
 | 
			
		||||
 */
 | 
			
		||||
class ComposerValidateTest extends BuildTestBase {
 | 
			
		||||
 | 
			
		||||
  use ComposerIntegrationTrait;
 | 
			
		||||
 | 
			
		||||
  public static function provideComposerJson() {
 | 
			
		||||
    $data = [];
 | 
			
		||||
    $composer_json_finder = self::getComposerJsonFinder(self::getDrupalRootStatic());
 | 
			
		||||
    foreach ($composer_json_finder->getIterator() as $composer_json) {
 | 
			
		||||
      $data[] = [$composer_json->getPathname()];
 | 
			
		||||
    }
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @dataProvider provideComposerJson
 | 
			
		||||
   */
 | 
			
		||||
  public function testValidateComposer($path): void {
 | 
			
		||||
    $this->executeCommand('composer validate --strict --no-check-all ' . $path);
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,683 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer\Plugin\Unpack\Functional;
 | 
			
		||||
 | 
			
		||||
use Composer\InstalledVersions;
 | 
			
		||||
use Composer\Util\Filesystem;
 | 
			
		||||
use Drupal\Tests\Composer\Plugin\Unpack\Fixtures;
 | 
			
		||||
use Drupal\BuildTests\Framework\BuildTestBase;
 | 
			
		||||
use Drupal\Tests\Composer\Plugin\ExecTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests recipe unpacking.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Unpack
 | 
			
		||||
 */
 | 
			
		||||
class UnpackRecipeTest extends BuildTestBase {
 | 
			
		||||
 | 
			
		||||
  use ExecTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Directory to perform the tests in.
 | 
			
		||||
   */
 | 
			
		||||
  protected string $fixturesDir;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The Symfony FileSystem component.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Composer\Util\Filesystem
 | 
			
		||||
   */
 | 
			
		||||
  protected Filesystem $fileSystem;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The Fixtures object.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Tests\Composer\Plugin\Unpack\Fixtures
 | 
			
		||||
   */
 | 
			
		||||
  protected Fixtures $fixtures;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->fileSystem = new Filesystem();
 | 
			
		||||
    $this->fixtures = new Fixtures();
 | 
			
		||||
    $this->fixtures->createIsolatedComposerCacheDir();
 | 
			
		||||
    $this->fixturesDir = $this->fixtures->tmpDir($this->name());
 | 
			
		||||
    $replacements = [
 | 
			
		||||
      'PROJECT_ROOT' => $this->fixtures->projectRoot(),
 | 
			
		||||
      'COMPOSER_INSTALLERS' => InstalledVersions::getInstallPath('composer/installers'),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function tearDown(): void {
 | 
			
		||||
    // Remove any temporary directories that were created.
 | 
			
		||||
    $this->fixtures->tearDown();
 | 
			
		||||
    parent::tearDown();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the dependencies unpack on install.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAutomaticUnpack(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    copy($root_project_path . '/composer.json', $root_project_path . '/composer.json.original');
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a module in require-dev that should be moved to require
 | 
			
		||||
    // by the unpacker.
 | 
			
		||||
    $this->runComposer('require --dev fixtures/module-a:^1.0');
 | 
			
		||||
    // Ensure we have added the dependency to require-dev.
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and unpack it.
 | 
			
		||||
    $stdout = $this->runComposer('require fixtures/recipe-a');
 | 
			
		||||
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    // The more specific constraint should have been used.
 | 
			
		||||
    $this->assertSame("^1.0", $root_composer_json['require']['fixtures/module-a']);
 | 
			
		||||
 | 
			
		||||
    // Copy old composer.json back over and require recipe again to ensure it
 | 
			
		||||
    // is still unpacked. This tests that unpacking does not rely on composer
 | 
			
		||||
    // package events.
 | 
			
		||||
    unlink($root_project_path . '/composer.json');
 | 
			
		||||
    copy($root_project_path . '/composer.json.original', $root_project_path . '/composer.json');
 | 
			
		||||
    $stdout = $this->runComposer('require fixtures/recipe-a');
 | 
			
		||||
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests recursive unpacking.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRecursiveUnpacking(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('config --merge --json sort-packages true');
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
    $stdOut = $this->runComposer('require fixtures/recipe-c fixtures/recipe-a');
 | 
			
		||||
    $this->assertSame("fixtures/recipe-c unpacked.\nfixtures/recipe-a unpacked.\nfixtures/recipe-b unpacked.\n", $stdOut);
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'fixtures/module-a',
 | 
			
		||||
      'fixtures/module-b',
 | 
			
		||||
      'fixtures/theme-a',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
    // Ensure the resulting composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
    // Ensure the recipes exist.
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-c/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // Ensure the complex constraint has been written correctly.
 | 
			
		||||
    $this->assertSame('>=2.0.1.0-dev, <3.0.0.0-dev', $root_composer_json['require']['fixtures/module-b']);
 | 
			
		||||
 | 
			
		||||
    // Ensure composer.lock is ordered correctly.
 | 
			
		||||
    $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'fixtures/module-a',
 | 
			
		||||
      'fixtures/module-b',
 | 
			
		||||
      'fixtures/theme-a',
 | 
			
		||||
    ], array_column($root_composer_lock['packages'], 'name'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the dev dependencies do not unpack on install.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNoAutomaticDevUnpack(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a module in require.
 | 
			
		||||
    $this->runComposer('require fixtures/module-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    // Install a recipe as a dev dependency.
 | 
			
		||||
    $stdout = $this->runComposer('require --dev fixtures/recipe-a');
 | 
			
		||||
    $this->assertStringContainsString("Recipes required as a development dependency are not automatically unpacked.", $stdout);
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // Assert the state of the root composer.json as no unpacking has occurred.
 | 
			
		||||
    $this->assertSame(['fixtures/recipe-a'], array_keys($root_composer_json['require-dev']));
 | 
			
		||||
    $this->assertSame(['composer/installers', 'drupal/core-recipe-unpack', 'ext-json', 'fixtures/module-a'], array_keys($root_composer_json['require']));
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests dependency unpacking using drupal:recipe-unpack.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnpackCommand(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking as it is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    // Install a module in require-dev.
 | 
			
		||||
    $this->runComposer('require --dev fixtures/module-a');
 | 
			
		||||
    // Install a module in require.
 | 
			
		||||
    $this->runComposer('require fixtures/module-b:*');
 | 
			
		||||
 | 
			
		||||
    // Ensure we have added the dependencies.
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is not unpacked.
 | 
			
		||||
    $stdout = $this->runComposer('require fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // When the package is unpacked, the unpacked dependencies should be logged
 | 
			
		||||
    // in the stdout.
 | 
			
		||||
    $this->assertStringNotContainsString("unpacked.", $stdout);
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
 | 
			
		||||
    // The package dependencies should not be in the root composer.json.
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    // Try unpacking a recipe that in not in the root composer.json.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->runComposer('drupal:recipe-unpack fixtures/recipe-b');
 | 
			
		||||
      $this->fail('Unpacking a non-existent dependency should fail');
 | 
			
		||||
    }
 | 
			
		||||
    catch (\RuntimeException $e) {
 | 
			
		||||
      $this->assertStringContainsString('fixtures/recipe-b not found in the root composer.json.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The dev dependency has not moved.
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
 | 
			
		||||
 | 
			
		||||
    $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
 | 
			
		||||
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    // The more specific constraints has been used.
 | 
			
		||||
    $this->assertSame("^2.0", $root_composer_json['require']['fixtures/module-b']);
 | 
			
		||||
 | 
			
		||||
    // Try unpacking something that is not a recipe.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->runComposer('drupal:recipe-unpack fixtures/module-a');
 | 
			
		||||
      $this->fail('Unpacking a module should fail');
 | 
			
		||||
    }
 | 
			
		||||
    catch (\RuntimeException $e) {
 | 
			
		||||
      $this->assertStringContainsString('fixtures/module-a is not a recipe.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try unpacking something that in not in the root composer.json.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->runComposer('drupal:recipe-unpack fixtures/module-c');
 | 
			
		||||
      $this->fail('Unpacking a non-existent dependency should fail');
 | 
			
		||||
    }
 | 
			
		||||
    catch (\RuntimeException $e) {
 | 
			
		||||
      $this->assertStringContainsString('fixtures/module-c not found in the root composer.json.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests dependency unpacking using drupal:recipe-unpack with multiple args.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnpackCommandWithMultipleRecipes(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking as it is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is not unpacked.
 | 
			
		||||
    $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // When the package is unpacked, the unpacked dependencies should be logged
 | 
			
		||||
    // in the stdout.
 | 
			
		||||
    $this->assertStringNotContainsString("unpacked.", $stdOut);
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    $stdOut = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a fixtures/recipe-d');
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
 | 
			
		||||
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests dependency unpacking using drupal:recipe-unpack with no arguments.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnpackCommandWithoutRecipesArgument(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Tests unpack command with no arguments and no recipes in the root
 | 
			
		||||
    // composer package.
 | 
			
		||||
    $stdOut = $this->runComposer('drupal:recipe-unpack');
 | 
			
		||||
    $this->assertSame("No recipes to unpack.\n", $stdOut);
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking as it is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is not unpacked.
 | 
			
		||||
    $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // When the package is unpacked, the unpacked dependencies should be logged
 | 
			
		||||
    // in the stdout.
 | 
			
		||||
    $this->assertStringNotContainsString("unpacked.", $stdOut);
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    $stdOut = $this->runComposer('drupal:recipe-unpack');
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
 | 
			
		||||
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests unpacking a recipe in require-dev using drupal:recipe-unpack.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnpackCommandOnDevRecipe(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking, which is the default behavior.
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('require fixtures/recipe-b');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is not unpacked.
 | 
			
		||||
    $this->runComposer('require --dev fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require-dev']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    $error_output = '';
 | 
			
		||||
    $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a', error_output: $error_output);
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-a is present in the require-dev key. Unpacking will move the recipe's dependencies to the require key.", $error_output);
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe A's dependencies are moved to require.
 | 
			
		||||
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe B's dependencies are in require and the recipe has been
 | 
			
		||||
    // unpacked.
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    // Ensure installed.json and installed.php are correct.
 | 
			
		||||
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
 | 
			
		||||
    $installed_packages = array_column($installed_json['packages'], 'name');
 | 
			
		||||
    $this->assertContains('fixtures/module-b', $installed_packages);
 | 
			
		||||
    $this->assertNotContains('fixtures/recipe-a', $installed_packages);
 | 
			
		||||
    $this->assertSame([], $installed_json['dev-package-names']);
 | 
			
		||||
    $installed_php = include_once $root_project_path . '/vendor/composer/installed.php';
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
 | 
			
		||||
    $this->assertFalse($installed_php['versions']['fixtures/module-b']['dev_requirement']);
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-a', $installed_php['versions']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the unpacking a recipe that is an indirect dev dependency.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnpackCommandOnIndirectDevDependencyRecipe(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Run composer install and confirm the composer.lock was created.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
    // Disable automatic unpacking as it is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('require --dev fixtures/recipe-b');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and ensure it is not unpacked.
 | 
			
		||||
    $this->runComposer('require fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require-dev']);
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe A's dependencies are in require.
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe B is still in require-dev even though all it's dependencies
 | 
			
		||||
    // have been unpacked to require due to unpacking recipe A.
 | 
			
		||||
    $this->assertSame(['fixtures/recipe-b'], array_keys($root_composer_json['require-dev']));
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe B is still list in installed.json.
 | 
			
		||||
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
 | 
			
		||||
    $installed_packages = array_column($installed_json['packages'], 'name');
 | 
			
		||||
    $this->assertContains('fixtures/recipe-b', $installed_packages);
 | 
			
		||||
    $this->assertContains('fixtures/recipe-b', $installed_json['dev-package-names']);
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a recipe can be removed and the unpack plugin does not interfere.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRemoveRecipe(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking, which is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and ensure it is not unpacked.
 | 
			
		||||
    $this->runComposer('require fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'fixtures/recipe-a',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
 | 
			
		||||
    // Removing the recipe should work as normal.
 | 
			
		||||
    $this->runComposer('remove fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'ext-json',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a recipe can be ignored and not unpacked.
 | 
			
		||||
   */
 | 
			
		||||
  public function testIgnoreRecipe(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking as it is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-a"]\'');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and ensure it does not get unpacked.
 | 
			
		||||
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame("fixtures/recipe-a not unpacked because it is ignored.", trim($stdOut));
 | 
			
		||||
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'fixtures/recipe-a',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
 | 
			
		||||
    // Try using the unpack command on an ignored recipe.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
 | 
			
		||||
      $this->fail('Ignored recipes should not be unpacked.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (\RuntimeException $e) {
 | 
			
		||||
      $this->assertStringContainsString('fixtures/recipe-a is in the extra.drupal-recipe-unpack.ignore list.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a dependent recipe can be ignored and not unpacked.
 | 
			
		||||
   */
 | 
			
		||||
  public function testIgnoreDependentRecipe(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Disable automatic unpacking, which is the default behavior,
 | 
			
		||||
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-b"]\'');
 | 
			
		||||
    $this->runComposer('config sort-packages true');
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is not packed but not removed.
 | 
			
		||||
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-b not unpacked because it is ignored.", $stdOut);
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
 | 
			
		||||
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'fixtures/module-b',
 | 
			
		||||
      'fixtures/recipe-b',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that recipes stick around after being unpacked.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRecipeIsPhysicallyPresentAfterUnpack(): void {
 | 
			
		||||
    $root_project_dir = 'composer-root';
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/' . $root_project_dir;
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe, which should unpack it.
 | 
			
		||||
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-b');
 | 
			
		||||
    $this->assertStringContainsString("fixtures/recipe-b unpacked.", $stdOut);
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // Require another dependency.
 | 
			
		||||
    $this->runComposer('require --verbose fixtures/module-b');
 | 
			
		||||
 | 
			
		||||
    // The recipe should still be physically installed...
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // ...but it should NOT be in installed.json or installed.php.
 | 
			
		||||
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
 | 
			
		||||
    $installed_packages = array_column($installed_json['packages'], 'name');
 | 
			
		||||
    $this->assertContains('fixtures/module-b', $installed_packages);
 | 
			
		||||
    $this->assertNotContains('fixtures/recipe-b', $installed_packages);
 | 
			
		||||
    $installed_php = include_once $root_project_path . '/vendor/composer/installed.php';
 | 
			
		||||
    $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
 | 
			
		||||
    $this->assertArrayNotHasKey('fixtures/recipe-b', $installed_php['versions']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a recipe can be required using --no-install and installed later.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRecipeNotUnpackedIfInstallIsDeferred(): void {
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
 | 
			
		||||
    // Install a recipe and check it is in `composer.json` but not unpacked or
 | 
			
		||||
    // physically installed.
 | 
			
		||||
    $stdOut = $this->runComposer('require --verbose --no-install fixtures/recipe-a');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame("Recipes are not unpacked when the --no-install option is used.", trim($stdOut));
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'fixtures/recipe-a',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
    $this->assertFileDoesNotExist($root_project_path . '/recipes/recipe-a/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // After installing dependencies, the recipe should be installed, but still
 | 
			
		||||
    // not unpacked.
 | 
			
		||||
    $this->runComposer('install');
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'ext-json',
 | 
			
		||||
      'fixtures/recipe-a',
 | 
			
		||||
    ], array_keys($root_composer_json['require']));
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that recipes are unpacked when using `composer create-project`.
 | 
			
		||||
   */
 | 
			
		||||
  public function testComposerCreateProject(): void {
 | 
			
		||||
    // Prepare the project to use for create-project.
 | 
			
		||||
    $root_project_path = $this->fixturesDir . '/composer-root';
 | 
			
		||||
    $this->runComposer('require --verbose --no-install fixtures/recipe-a');
 | 
			
		||||
 | 
			
		||||
    $stdOut = $this->runComposer('create-project --repository=\'{"type": "path","url": "' . $root_project_path . '","options": {"symlink": false}}\' fixtures/root composer-root2 -s dev', $this->fixturesDir);
 | 
			
		||||
    // The recipes depended upon by the project, even indirectly, should all
 | 
			
		||||
    // have been unpacked.
 | 
			
		||||
    $this->assertSame("fixtures/recipe-b unpacked.\nfixtures/recipe-a unpacked.\n", $stdOut);
 | 
			
		||||
    $this->doTestRecipeAUnpacked($this->fixturesDir . '/composer-root2', $stdOut);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests Recipe A is unpacked correctly.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $root_project_path
 | 
			
		||||
   *   Path to the composer project under test.
 | 
			
		||||
   * @param string $stdout
 | 
			
		||||
   *   The standard out from the composer command unpacks the recipe.
 | 
			
		||||
   */
 | 
			
		||||
  private function doTestRecipeAUnpacked(string $root_project_path, string $stdout): void {
 | 
			
		||||
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
 | 
			
		||||
 | 
			
		||||
    // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json
 | 
			
		||||
    // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json
 | 
			
		||||
    $expected_unpacked = [
 | 
			
		||||
      'fixtures/recipe-a' => [
 | 
			
		||||
        'fixtures/module-b',
 | 
			
		||||
      ],
 | 
			
		||||
      'fixtures/recipe-b' => [
 | 
			
		||||
        'fixtures/module-a',
 | 
			
		||||
        'fixtures/theme-a',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($expected_unpacked as $package => $dependencies) {
 | 
			
		||||
      // When the package is unpacked, the unpacked dependencies should be logged
 | 
			
		||||
      // in the stdout.
 | 
			
		||||
      $this->assertStringContainsString("$package unpacked.", $stdout);
 | 
			
		||||
 | 
			
		||||
      // After being unpacked, the package should be removed from the root
 | 
			
		||||
      // composer.json and composer.lock.
 | 
			
		||||
      $this->assertArrayNotHasKey($package, $root_composer_json['require']);
 | 
			
		||||
 | 
			
		||||
      foreach ($dependencies as $dependency) {
 | 
			
		||||
        // The package dependencies should be in the root composer.json.
 | 
			
		||||
        $this->assertArrayHasKey($dependency, $root_composer_json['require']);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure the resulting Composer files are valid.
 | 
			
		||||
    $this->runComposer('validate', $root_project_path);
 | 
			
		||||
 | 
			
		||||
    // The dev dependency has moved.
 | 
			
		||||
    $this->assertArrayNotHasKey('require-dev', $root_composer_json);
 | 
			
		||||
 | 
			
		||||
    // Ensure recipe files exist.
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
 | 
			
		||||
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
 | 
			
		||||
 | 
			
		||||
    // Ensure composer.lock is ordered correctly.
 | 
			
		||||
    $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
 | 
			
		||||
    $this->assertSame([
 | 
			
		||||
      'composer/installers',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'fixtures/module-a',
 | 
			
		||||
      'fixtures/module-b',
 | 
			
		||||
      'fixtures/theme-a',
 | 
			
		||||
    ], array_column($root_composer_lock['packages'], 'name'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executes a Composer command with standard options.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $command
 | 
			
		||||
   *   The composer command to execute.
 | 
			
		||||
   * @param string $cwd
 | 
			
		||||
   *   The current working directory to run the command from.
 | 
			
		||||
   * @param string $error_output
 | 
			
		||||
   *   Passed by reference to allow error output to be tested.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   Standard output from the command.
 | 
			
		||||
   */
 | 
			
		||||
  private function runComposer(string $command, ?string $cwd = NULL, string &$error_output = ''): string {
 | 
			
		||||
    $cwd ??= $this->fixturesDir . '/composer-root';
 | 
			
		||||
 | 
			
		||||
    // Always add --no-interaction and --no-ansi to Composer commands.
 | 
			
		||||
    $output = $this->mustExec("composer $command --no-interaction --no-ansi", $cwd, [], $error_output);
 | 
			
		||||
    if ($command === 'install') {
 | 
			
		||||
      $this->assertFileExists($cwd . '/composer.lock');
 | 
			
		||||
    }
 | 
			
		||||
    return $output;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the contents of a file as an array.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $path
 | 
			
		||||
   *   The path to the file.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   The contents of the file as an array.
 | 
			
		||||
   */
 | 
			
		||||
  private function getFileContents(string $path): array {
 | 
			
		||||
    $file = file_get_contents($path);
 | 
			
		||||
    return json_decode($file, TRUE, flags: JSON_THROW_ON_ERROR);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,469 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\BuildTests\Composer\Template;
 | 
			
		||||
 | 
			
		||||
use Composer\Json\JsonFile;
 | 
			
		||||
use Composer\Semver\VersionParser;
 | 
			
		||||
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
 | 
			
		||||
use Drupal\Composer\Composer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Demonstrate that Composer project templates can be built as patched.
 | 
			
		||||
 *
 | 
			
		||||
 * We have to use the packages.json fixture so that Composer will use the
 | 
			
		||||
 * in-codebase version of the project template.
 | 
			
		||||
 *
 | 
			
		||||
 * We also have to add path repositories to the in-codebase project template or
 | 
			
		||||
 * else Composer will try to use packagist to resolve dependencies we'd prefer
 | 
			
		||||
 * it to find locally.
 | 
			
		||||
 *
 | 
			
		||||
 * This is because Composer only uses the packages.json file to resolve the
 | 
			
		||||
 * project template and not any other dependencies.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Template
 | 
			
		||||
 */
 | 
			
		||||
class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The minimum stability requirement for dependencies.
 | 
			
		||||
   *
 | 
			
		||||
   * @see https://getcomposer.org/doc/04-schema.md#minimum-stability
 | 
			
		||||
   */
 | 
			
		||||
  protected const MINIMUM_STABILITY = 'stable';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The order of stability strings from least stable to most stable.
 | 
			
		||||
   *
 | 
			
		||||
   * This only includes normalized stability strings: i.e., ones that are
 | 
			
		||||
   * returned by \Composer\Semver\VersionParser::parseStability().
 | 
			
		||||
   */
 | 
			
		||||
  protected const STABILITY_ORDER = ['dev', 'alpha', 'beta', 'RC', 'stable'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get Composer items that we want to be path repos, from within a directory.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $workspace_directory
 | 
			
		||||
   *   The full path to the workspace directory.
 | 
			
		||||
   * @param string $subdir
 | 
			
		||||
   *   The subdirectory to search under composer/.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string[]
 | 
			
		||||
   *   Array of paths, indexed by package name.
 | 
			
		||||
   */
 | 
			
		||||
  public function getPathReposForType($workspace_directory, $subdir) {
 | 
			
		||||
    // Find the Composer items that we want to be path repos.
 | 
			
		||||
    /** @var \SplFileInfo[] $path_repos */
 | 
			
		||||
    $path_repos = Composer::composerSubprojectPaths($workspace_directory, $subdir);
 | 
			
		||||
 | 
			
		||||
    $data = [];
 | 
			
		||||
    foreach ($path_repos as $path_repo) {
 | 
			
		||||
      $json_file = new JsonFile($path_repo->getPathname());
 | 
			
		||||
      $json = $json_file->read();
 | 
			
		||||
      $data[$json['name']] = $path_repo->getPath();
 | 
			
		||||
    }
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static function provideTemplateCreateProject() {
 | 
			
		||||
    return [
 | 
			
		||||
      'recommended-project' => [
 | 
			
		||||
        'drupal/recommended-project',
 | 
			
		||||
        'composer/Template/RecommendedProject',
 | 
			
		||||
        '/web',
 | 
			
		||||
      ],
 | 
			
		||||
      'legacy-project' => [
 | 
			
		||||
        'drupal/legacy-project',
 | 
			
		||||
        'composer/Template/LegacyProject',
 | 
			
		||||
        '',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Make sure that static::MINIMUM_STABILITY is sufficiently strict.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMinimumStabilityStrictness(): void {
 | 
			
		||||
    // Ensure that static::MINIMUM_STABILITY is not less stable than the
 | 
			
		||||
    // current core stability. For example, if we've already released a beta on
 | 
			
		||||
    // the branch, ensure that we no longer allow alpha dependencies.
 | 
			
		||||
    $this->assertGreaterThanOrEqual(array_search($this->getCoreStability(), static::STABILITY_ORDER), array_search(static::MINIMUM_STABILITY, static::STABILITY_ORDER));
 | 
			
		||||
 | 
			
		||||
    // Ensure that static::MINIMUM_STABILITY is the same as the least stable
 | 
			
		||||
    // dependency.
 | 
			
		||||
    // - We can't set it stricter than our least stable dependency.
 | 
			
		||||
    // - We don't want to set it looser than we need to, because we don't want
 | 
			
		||||
    //   to in the future accidentally commit a dependency that regresses our
 | 
			
		||||
    //   actual stability requirement without us explicitly changing this
 | 
			
		||||
    //   constant.
 | 
			
		||||
    $root = $this->getDrupalRoot();
 | 
			
		||||
    $process = $this->executeCommand("composer --working-dir=$root info --format=json");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    $installed = json_decode($process->getOutput(), TRUE);
 | 
			
		||||
 | 
			
		||||
    // A lookup of the numerical position of each of the stability terms.
 | 
			
		||||
    $stability_order_indexes = array_flip(static::STABILITY_ORDER);
 | 
			
		||||
 | 
			
		||||
    $minimum_stability_order_index = $stability_order_indexes[static::MINIMUM_STABILITY];
 | 
			
		||||
 | 
			
		||||
    $exclude = [
 | 
			
		||||
      'drupal/core',
 | 
			
		||||
      'drupal/core-recipe-unpack',
 | 
			
		||||
      'drupal/core-project-message',
 | 
			
		||||
      'drupal/core-vendor-hardening',
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($installed['installed'] as $project) {
 | 
			
		||||
      // Exclude dependencies that are required with "self.version", since
 | 
			
		||||
      // those stabilities will automatically match the corresponding Drupal
 | 
			
		||||
      // release.
 | 
			
		||||
      if (in_array($project['name'], $exclude, TRUE)) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // VersionParser::parseStability doesn't play nice with (mostly dev-)
 | 
			
		||||
      // versions ending with the first seven characters of the commit ID as
 | 
			
		||||
      // returned by "composer info". Let's strip those suffixes here.
 | 
			
		||||
      $version = preg_replace('/ [0-9a-f]{7}$/i', '', $project['version']);
 | 
			
		||||
 | 
			
		||||
      $project_stability = VersionParser::parseStability($version);
 | 
			
		||||
      $project_stability_order_index = $stability_order_indexes[$project_stability];
 | 
			
		||||
 | 
			
		||||
      $project_stabilities[$project['name']] = $project_stability;
 | 
			
		||||
 | 
			
		||||
      $this->assertGreaterThanOrEqual($minimum_stability_order_index, $project_stability_order_index, sprintf(
 | 
			
		||||
        "Dependency %s with stability %s does not meet minimum stability %s.",
 | 
			
		||||
        $project['name'],
 | 
			
		||||
        $project_stability,
 | 
			
		||||
        static::MINIMUM_STABILITY,
 | 
			
		||||
      ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // At least one project should be at the minimum stability.
 | 
			
		||||
    $this->assertContains(static::MINIMUM_STABILITY, $project_stabilities);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Make sure we've accounted for all the templates.
 | 
			
		||||
   */
 | 
			
		||||
  public function testVerifyTemplateTestProviderIsAccurate(): void {
 | 
			
		||||
    $root = $this->getDrupalRoot();
 | 
			
		||||
    $data = $this->provideTemplateCreateProject();
 | 
			
		||||
 | 
			
		||||
    // Find all the templates.
 | 
			
		||||
    $template_files = Composer::composerSubprojectPaths($root, 'Template');
 | 
			
		||||
 | 
			
		||||
    $this->assertSameSize($template_files, $data);
 | 
			
		||||
 | 
			
		||||
    // We could have the same number of templates but different names.
 | 
			
		||||
    $template_data = [];
 | 
			
		||||
    foreach ($data as $data_name => $data_value) {
 | 
			
		||||
      $template_data[$data_value[0]] = $data_name;
 | 
			
		||||
    }
 | 
			
		||||
    /** @var \SplFileInfo $file */
 | 
			
		||||
    foreach ($template_files as $file) {
 | 
			
		||||
      $json_file = new JsonFile($file->getPathname());
 | 
			
		||||
      $json = $json_file->read();
 | 
			
		||||
      $this->assertArrayHasKey('name', $json);
 | 
			
		||||
      // Assert that the template name is in the project created
 | 
			
		||||
      // from the template.
 | 
			
		||||
      $this->assertArrayHasKey($json['name'], $template_data);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @dataProvider provideTemplateCreateProject
 | 
			
		||||
   */
 | 
			
		||||
  public function testTemplateCreateProject($project, $package_dir, $docroot_dir): void {
 | 
			
		||||
    // Make a working COMPOSER_HOME directory for setting global composer config
 | 
			
		||||
    $composer_home = $this->getWorkspaceDirectory() . '/composer-home';
 | 
			
		||||
    mkdir($composer_home);
 | 
			
		||||
    // Create an empty global composer.json file, just to avoid warnings.
 | 
			
		||||
    file_put_contents("$composer_home/composer.json", '{}');
 | 
			
		||||
 | 
			
		||||
    // Disable packagist globally (but only in our own custom COMPOSER_HOME).
 | 
			
		||||
    // It is necessary to do this globally rather than in our SUT composer.json
 | 
			
		||||
    // in order to ensure that Packagist is disabled during the
 | 
			
		||||
    // `composer create-project` command.
 | 
			
		||||
    $this->executeCommand("COMPOSER_HOME=$composer_home composer config --no-interaction --global repo.packagist false");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
 | 
			
		||||
    // Create a "Composer"-type repository containing one entry for every
 | 
			
		||||
    // package in the vendor directory.
 | 
			
		||||
    $vendor_packages_path = $this->getWorkspaceDirectory() . '/vendor_packages/packages.json';
 | 
			
		||||
    $this->makeVendorPackage($vendor_packages_path);
 | 
			
		||||
 | 
			
		||||
    // Make a copy of the code to alter in the workspace directory.
 | 
			
		||||
    $this->copyCodebase();
 | 
			
		||||
 | 
			
		||||
    // Tests are typically run on "-dev" versions, but we want to simulate
 | 
			
		||||
    // running them on a tagged release at the same stability as specified in
 | 
			
		||||
    // static::MINIMUM_STABILITY, in order to verify that everything will work
 | 
			
		||||
    // if/when we make such a release.
 | 
			
		||||
    $simulated_core_version = \Drupal::VERSION;
 | 
			
		||||
    $simulated_core_version_suffix = (static::MINIMUM_STABILITY === 'stable' ? '' : '-' . static::MINIMUM_STABILITY . '99');
 | 
			
		||||
    $simulated_core_version = str_replace('-dev', $simulated_core_version_suffix, $simulated_core_version);
 | 
			
		||||
    Composer::setDrupalVersion($this->getWorkspaceDirectory(), $simulated_core_version);
 | 
			
		||||
    $this->assertDrupalVersion($simulated_core_version, $this->getWorkspaceDirectory());
 | 
			
		||||
 | 
			
		||||
    // Remove the packages.drupal.org entry (and any other custom repository)
 | 
			
		||||
    // from the SUT's repositories section. There is no way to do this via
 | 
			
		||||
    // `composer config --unset`, so we read and rewrite composer.json.
 | 
			
		||||
    $composer_json_path = $this->getWorkspaceDirectory() . "/$package_dir/composer.json";
 | 
			
		||||
    $composer_json = json_decode(file_get_contents($composer_json_path), TRUE);
 | 
			
		||||
    unset($composer_json['repositories']);
 | 
			
		||||
    $json = json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
 | 
			
		||||
    file_put_contents($composer_json_path, $json);
 | 
			
		||||
 | 
			
		||||
    // Set up the template to use our path repos. Inclusion of metapackages is
 | 
			
		||||
    // reported differently, so we load up a separate set for them.
 | 
			
		||||
    $metapackage_path_repos = $this->getPathReposForType($this->getWorkspaceDirectory(), 'Metapackage');
 | 
			
		||||
    $this->assertArrayHasKey('drupal/core-recommended', $metapackage_path_repos);
 | 
			
		||||
    $path_repos = array_merge($metapackage_path_repos, $this->getPathReposForType($this->getWorkspaceDirectory(), 'Plugin'));
 | 
			
		||||
    // Always add drupal/core as a path repo.
 | 
			
		||||
    $path_repos['drupal/core'] = $this->getWorkspaceDirectory() . '/core';
 | 
			
		||||
    foreach ($path_repos as $name => $path) {
 | 
			
		||||
      $this->executeCommand("composer config --no-interaction repositories.$name path $path", $package_dir);
 | 
			
		||||
      $this->assertCommandSuccessful();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Change drupal/core-recommended to require the simulated version of
 | 
			
		||||
    // drupal/core.
 | 
			
		||||
    $core_recommended_dir = 'composer/Metapackage/CoreRecommended';
 | 
			
		||||
    $this->executeCommand("composer remove --no-interaction drupal/core --no-update", $core_recommended_dir);
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    $this->executeCommand("composer require --no-interaction drupal/core:^$simulated_core_version --no-update", $core_recommended_dir);
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
 | 
			
		||||
    // Add our vendor package repository to our SUT's repositories section.
 | 
			
		||||
    // Call it "local" (although the name does not matter).
 | 
			
		||||
    $this->executeCommand("composer config --no-interaction repositories.local composer file://" . $vendor_packages_path, $package_dir);
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
 | 
			
		||||
    $repository_path = $this->getWorkspaceDirectory() . '/test_repository/packages.json';
 | 
			
		||||
    $this->makeTestPackage($repository_path, $simulated_core_version);
 | 
			
		||||
 | 
			
		||||
    $installed_composer_json = $this->getWorkspaceDirectory() . '/test_project/composer.json';
 | 
			
		||||
    $autoloader = $this->getWorkspaceDirectory() . '/test_project' . $docroot_dir . '/autoload.php';
 | 
			
		||||
    $recipes_dir = $this->getWorkspaceDirectory() . '/test_project/recipes';
 | 
			
		||||
    $this->assertFileDoesNotExist($autoloader);
 | 
			
		||||
    $this->assertDirectoryDoesNotExist($recipes_dir);
 | 
			
		||||
 | 
			
		||||
    $this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$simulated_core_version composer create-project --no-ansi $project test_project $simulated_core_version -vvv --repository $repository_path");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    // Check the output of the project creation for the absence of warnings
 | 
			
		||||
    // about any non-allowed composer plugins.
 | 
			
		||||
    // Note: There are different warnings for disallowed composer plugins
 | 
			
		||||
    // depending on running in non-interactive mode or not. It seems the Drupal
 | 
			
		||||
    // CI environment always forces composer commands to run in the
 | 
			
		||||
    // non-interactive mode. The only thing these messages have in common is the
 | 
			
		||||
    // following string.
 | 
			
		||||
    $this->assertErrorOutputNotContains('See https://getcomposer.org/allow-plugins');
 | 
			
		||||
 | 
			
		||||
    // Ensure we used the project from our codebase.
 | 
			
		||||
    $this->assertErrorOutputContains("Installing $project ($simulated_core_version): Symlinking from $package_dir");
 | 
			
		||||
    // Ensure that we used drupal/core from our codebase. This probably means
 | 
			
		||||
    // that drupal/core-recommended was added successfully by the project.
 | 
			
		||||
    $this->assertErrorOutputContains("Installing drupal/core ($simulated_core_version): Symlinking from");
 | 
			
		||||
    // Verify that there is an autoloader. This is written by the scaffold
 | 
			
		||||
    // plugin, so its existence assures us that scaffolding happened.
 | 
			
		||||
    $this->assertFileExists($autoloader);
 | 
			
		||||
    // Verify recipes directory exists.
 | 
			
		||||
    $this->assertDirectoryExists($recipes_dir);
 | 
			
		||||
 | 
			
		||||
    // Verify that the minimum stability in the installed composer.json file
 | 
			
		||||
    // matches the stability of the simulated core version.
 | 
			
		||||
    $this->assertFileExists($installed_composer_json);
 | 
			
		||||
    $composer_json_contents = file_get_contents($installed_composer_json);
 | 
			
		||||
    $this->assertStringContainsString('"minimum-stability": "' . static::MINIMUM_STABILITY . '"', $composer_json_contents);
 | 
			
		||||
 | 
			
		||||
    // In order to verify that Composer used the path repos for our project, we
 | 
			
		||||
    // have to get the requirements from the project composer.json so we can
 | 
			
		||||
    // reconcile our expectations.
 | 
			
		||||
    $template_json_file = $this->getWorkspaceDirectory() . '/' . $package_dir . '/composer.json';
 | 
			
		||||
    $this->assertFileExists($template_json_file);
 | 
			
		||||
    $json_file = new JsonFile($template_json_file);
 | 
			
		||||
    $template_json = $json_file->read();
 | 
			
		||||
    // Get the require and require-dev information, and ensure that our
 | 
			
		||||
    // requirements are not erroneously empty.
 | 
			
		||||
    $this->assertNotEmpty(
 | 
			
		||||
      $require = array_merge($template_json['require'] ?? [], $template_json['require-dev'] ?? [])
 | 
			
		||||
    );
 | 
			
		||||
    // Verify that path repo packages were installed.
 | 
			
		||||
    $path_repos = array_keys($path_repos);
 | 
			
		||||
    foreach (array_keys($require) as $package_name) {
 | 
			
		||||
      if (in_array($package_name, $path_repos)) {
 | 
			
		||||
        // Metapackages do not report that they were installed as symlinks, but
 | 
			
		||||
        // we still must check that their installed version matches
 | 
			
		||||
        // COMPOSER_CORE_VERSION.
 | 
			
		||||
        if (array_key_exists($package_name, $metapackage_path_repos)) {
 | 
			
		||||
          $this->assertErrorOutputContains("Installing $package_name ($simulated_core_version)");
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          $this->assertErrorOutputContains("Installing $package_name ($simulated_core_version): Symlinking from");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a test package that points to the templates.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $repository_path
 | 
			
		||||
   *   The path where to create the test package.
 | 
			
		||||
   * @param string $version
 | 
			
		||||
   *   The version under test.
 | 
			
		||||
   */
 | 
			
		||||
  protected function makeTestPackage($repository_path, $version): void {
 | 
			
		||||
    $json = <<<JSON
 | 
			
		||||
{
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "drupal/recommended-project": {
 | 
			
		||||
      "$version": {
 | 
			
		||||
        "name": "drupal/recommended-project",
 | 
			
		||||
        "dist": {
 | 
			
		||||
          "type": "path",
 | 
			
		||||
          "url": "composer/Template/RecommendedProject"
 | 
			
		||||
        },
 | 
			
		||||
        "type": "project",
 | 
			
		||||
        "version": "$version"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "drupal/legacy-project": {
 | 
			
		||||
      "$version": {
 | 
			
		||||
        "name": "drupal/legacy-project",
 | 
			
		||||
        "dist": {
 | 
			
		||||
          "type": "path",
 | 
			
		||||
          "url": "composer/Template/LegacyProject"
 | 
			
		||||
        },
 | 
			
		||||
        "type": "project",
 | 
			
		||||
        "version": "$version"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
JSON;
 | 
			
		||||
    mkdir(dirname($repository_path));
 | 
			
		||||
    file_put_contents($repository_path, $json);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a test package that points to all the projects in vendor.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $repository_path
 | 
			
		||||
   *   The path where to create the test package.
 | 
			
		||||
   */
 | 
			
		||||
  protected function makeVendorPackage($repository_path): void {
 | 
			
		||||
    $root = $this->getDrupalRoot();
 | 
			
		||||
    $process = $this->executeCommand("composer --working-dir=$root info --format=json");
 | 
			
		||||
    $this->assertCommandSuccessful();
 | 
			
		||||
    $installed = json_decode($process->getOutput(), TRUE);
 | 
			
		||||
 | 
			
		||||
    // Build out package definitions for everything installed in
 | 
			
		||||
    // the vendor directory.
 | 
			
		||||
    $packages = [];
 | 
			
		||||
    foreach ($installed['installed'] as $project) {
 | 
			
		||||
      $name = $project['name'];
 | 
			
		||||
      $version = $project['version'];
 | 
			
		||||
      $path = "vendor/$name";
 | 
			
		||||
      $full_path = "$root/$path";
 | 
			
		||||
      // We are building a set of path repositories to projects in the vendor
 | 
			
		||||
      // directory, so we will skip any project that does not exist in vendor.
 | 
			
		||||
      // Also skip the projects that are symlinked in vendor. These are in our
 | 
			
		||||
      // metapackage. They will be represented as path repositories in the test
 | 
			
		||||
      // project's composer.json.
 | 
			
		||||
      if (is_dir($full_path) && !is_link($full_path)) {
 | 
			
		||||
        $packages['packages'][$name] = [
 | 
			
		||||
          $version => [
 | 
			
		||||
            "name" => $name,
 | 
			
		||||
            "dist" => [
 | 
			
		||||
              "type" => "path",
 | 
			
		||||
              "url" => $full_path,
 | 
			
		||||
            ],
 | 
			
		||||
            "version" => $version,
 | 
			
		||||
          ],
 | 
			
		||||
        ];
 | 
			
		||||
        // Ensure composer plugins are registered correctly.
 | 
			
		||||
        $package_json = json_decode(file_get_contents($full_path . '/composer.json'), TRUE);
 | 
			
		||||
        if (isset($package_json['type']) && $package_json['type'] === 'composer-plugin') {
 | 
			
		||||
          $packages['packages'][$name][$version]['type'] = $package_json['type'];
 | 
			
		||||
          $packages['packages'][$name][$version]['require'] = $package_json['require'];
 | 
			
		||||
          $packages['packages'][$name][$version]['extra'] = $package_json['extra'];
 | 
			
		||||
          if (isset($package_json['autoload'])) {
 | 
			
		||||
            $packages['packages'][$name][$version]['autoload'] = $package_json['autoload'];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $json = json_encode($packages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
 | 
			
		||||
    mkdir(dirname($repository_path));
 | 
			
		||||
    file_put_contents($repository_path, $json);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the stability of the current core version.
 | 
			
		||||
   *
 | 
			
		||||
   * If the current core version is a tagged release (not a "-dev" version),
 | 
			
		||||
   * this returns the stability of that version.
 | 
			
		||||
   *
 | 
			
		||||
   * If the current core version is a "-dev" version, but not a "x.y.0-dev"
 | 
			
		||||
   * version, this returns "stable", because it means that the corresponding
 | 
			
		||||
   * "x.y.0" has already been released, and only stable changes are now
 | 
			
		||||
   * permitted on the branch.
 | 
			
		||||
   *
 | 
			
		||||
   * If the current core version is a "x.y.0-dev" version, then this returns
 | 
			
		||||
   * the stability of the latest tag that matches "x.y.0-*". For example, if
 | 
			
		||||
   * we've already released "x.y.0-alpha1" but have not yet released
 | 
			
		||||
   * "x.y.0-beta1", then the current stability is "alpha". If there aren't any
 | 
			
		||||
   * matching tags, this returns "dev", because it means that an "alpha1" has
 | 
			
		||||
   * not yet been released.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   One of: "dev", "alpha", "beta", "RC", "stable".
 | 
			
		||||
   */
 | 
			
		||||
  protected function getCoreStability() {
 | 
			
		||||
    $version = \Drupal::VERSION;
 | 
			
		||||
    // If the current version is x.y-dev then this is the equivalent of the main
 | 
			
		||||
    // branch and should be treated as a dev release.
 | 
			
		||||
    if (preg_match('/^(\d)+\.(\d)+-dev$/', $version)) {
 | 
			
		||||
      return 'dev';
 | 
			
		||||
    }
 | 
			
		||||
    $stability = VersionParser::parseStability($version);
 | 
			
		||||
    if ($stability === 'dev') {
 | 
			
		||||
      // Strip off "-dev";
 | 
			
		||||
      $version_towards = substr($version, 0, -4);
 | 
			
		||||
 | 
			
		||||
      if (!str_ends_with($version_towards, '.0')) {
 | 
			
		||||
        // If the current version is developing towards an x.y.z release where
 | 
			
		||||
        // z is not 0, it means that the x.y.0 has already been released, and
 | 
			
		||||
        // only stable changes are permitted on the branch.
 | 
			
		||||
        $stability = 'stable';
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // If the current version is developing towards an x.y.0 release, there
 | 
			
		||||
        // might be tagged pre-releases. "git describe" identifies the latest
 | 
			
		||||
        // one.
 | 
			
		||||
        $root = $this->getDrupalRoot();
 | 
			
		||||
        $process = $this->executeCommand("git -C \"$root\" describe --abbrev=0 --match=\"$version_towards-*\"");
 | 
			
		||||
 | 
			
		||||
        // If there aren't any tagged pre-releases for this version yet, return
 | 
			
		||||
        // 'dev'. Ensure that any other error from "git describe" causes a test
 | 
			
		||||
        // failure.
 | 
			
		||||
        if (!$process->isSuccessful()) {
 | 
			
		||||
          $this->assertErrorOutputContains('No names found, cannot describe anything.');
 | 
			
		||||
          return 'dev';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We expect a pre-release, because:
 | 
			
		||||
        // - A tag should not be of "dev" stability.
 | 
			
		||||
        // - After a "stable" release is made, \Drupal::VERSION is incremented,
 | 
			
		||||
        //   so there should not be a stable release on that new version.
 | 
			
		||||
        $stability = VersionParser::parseStability(trim($process->getOutput()));
 | 
			
		||||
        $this->assertContains($stability, ['alpha', 'beta', 'RC']);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $stability;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user