Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -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");
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}