first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer;
use Symfony\Component\Finder\Finder;
/**
* Some utility functions for testing the Composer integration.
*/
trait ComposerIntegrationTrait {
/**
* Get a Finder object to traverse all of the composer.json files in core.
*
* @param string $drupal_root
* Absolute path to the root of the Drupal installation.
*
* @return \Symfony\Component\Finder\Finder
* A Finder object able to iterate all the composer.json files in core.
*/
public static function getComposerJsonFinder($drupal_root) {
$composer_json_finder = new Finder();
$composer_json_finder->name('composer.json')
->in([
// Only find composer.json files within composer/ and core/ directories
// so we don't inadvertently test contrib in local dev environments.
$drupal_root . '/composer',
$drupal_root . '/core',
])
->ignoreUnreadableDirs()
->notPath('#^vendor#')
->notPath('#/fixture#');
return $composer_json_finder;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer;
use Drupal\Composer\Composer;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Composer\Composer
* @group Composer
*/
class ComposerTest extends UnitTestCase {
/**
* Verify that Composer::ensureComposerVersion() doesn't break.
*
* @covers ::ensureComposerVersion
*/
public function testEnsureComposerVersion(): void {
try {
$this->assertNull(Composer::ensureComposerVersion());
}
catch (\RuntimeException $e) {
$this->assertMatchesRegularExpression('/Drupal core development requires Composer 2.3.5, but Composer /', $e->getMessage());
}
}
/**
* Ensure that the configured php version matches the minimum php version.
*
* Also ensure that the minimum php version in the root-level composer.json
* file exactly matches \Drupal::MINIMUM_PHP.
*/
public function testEnsurePhpConfiguredVersion(): void {
$composer_json = json_decode(file_get_contents($this->root . '/composer.json'), TRUE);
$composer_core_json = json_decode(file_get_contents($this->root . '/core/composer.json'), TRUE);
$this->assertEquals(\Drupal::MINIMUM_PHP, $composer_json['config']['platform']['php'], 'The \Drupal::MINIMUM_PHP constant should always be exactly the same as the config.platform.php in the root composer.json.');
$this->assertEquals($composer_core_json['require']['php'], '>=' . $composer_json['config']['platform']['php'], 'The config.platform.php configured version in the root composer.json file should always be exactly the same as the minimum php version configured in core/composer.json.');
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Generator;
use Drupal\Composer\Generator\Builder\DrupalCoreRecommendedBuilder;
use Drupal\Composer\Generator\Builder\DrupalDevDependenciesBuilder;
use Drupal\Composer\Generator\Builder\DrupalPinnedDevDependenciesBuilder;
use PHPUnit\Framework\TestCase;
use Drupal\Composer\Composer;
/**
* Test DrupalCoreRecommendedBuilder.
*
* @group Metapackage
*/
class BuilderTest extends TestCase {
/**
* Provides test data for testBuilder.
*/
public static function builderTestData() {
return [
[
DrupalCoreRecommendedBuilder::class,
[
'name' => 'drupal/core-recommended',
'type' => 'metapackage',
'description' => 'Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.',
'license' => 'GPL-2.0-or-later',
'require' =>
[
'drupal/core' => Composer::drupalVersionBranch(),
'symfony/polyfill-ctype' => '~v1.12.0',
'symfony/yaml' => '~v3.4.32',
],
'conflict' =>
[
'webflo/drupal-core-strict' => '*',
],
],
],
[
DrupalDevDependenciesBuilder::class,
[
'name' => 'drupal/core-dev',
'type' => 'metapackage',
'description' => 'require-dev dependencies from drupal/drupal; use in addition to drupal/core-recommended to run tests from drupal/core.',
'license' => 'GPL-2.0-or-later',
'require' =>
[
'behat/mink' => '^1.8',
],
'conflict' =>
[
'webflo/drupal-core-require-dev' => '*',
],
],
],
[
DrupalPinnedDevDependenciesBuilder::class,
[
'name' => 'drupal/core-dev-pinned',
'type' => 'metapackage',
'description' => 'Pinned require-dev dependencies from drupal/drupal; use in addition to drupal/core-recommended to run tests from drupal/core.',
'license' => 'GPL-2.0-or-later',
'require' =>
[
'drupal/core' => Composer::drupalVersionBranch(),
'behat/mink' => 'v1.8.0',
'symfony/css-selector' => 'v4.3.5',
],
'conflict' =>
[
'webflo/drupal-core-require-dev' => '*',
],
],
],
];
}
/**
* Tests all of the various kinds of builders.
*
* @dataProvider builderTestData
*/
public function testBuilder($builderClass, $expected): void {
$fixtures = new Fixtures();
$drupalCoreInfo = $fixtures->drupalCoreComposerFixture();
$builder = new $builderClass($drupalCoreInfo);
$generatedJson = $builder->getPackage();
$this->assertEquals($expected, $generatedJson);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Generator;
use Drupal\Composer\Generator\Util\DrupalCoreComposer;
/**
* Convenience class for creating fixtures.
*/
class Fixtures {
/**
* Generate a suitable DrupalCoreComposer fixture for testing.
*
* @return \Drupal\Composer\Generator\Util\DrupalCoreComposer
* DrupalCoreComposer fixture.
*/
public function drupalCoreComposerFixture() {
return new DrupalCoreComposer($this->composerJson(), $this->composerLock());
}
/**
* Data for a composer.json fixture.
*
* @return array
* composer.json fixture data.
*/
protected function composerJson() {
return [
'name' => 'drupal/project-fixture',
'description' => 'A fixture for testing the metapackage generator.',
'type' => 'project',
'license' => 'GPL-2.0-or-later',
'require' =>
[
'composer/installers' => '^1.9',
'php' => '>=7.0.8',
'symfony/yaml' => '~3.4.5',
],
'require-dev' =>
[
'behat/mink' => '^1.8',
],
];
}
/**
* Data for a composer.lock fixture.
*
* @return array
* composer.lock fixture data.
*/
protected function composerLock() {
return [
'_readme' =>
[
'This is a composer.lock fixture. It contains only a subset of a',
'typical composer.lock file (just what is needed for testing).',
],
'content-hash' => 'da9910627bab73a256b39ceda83d7167',
'packages' =>
[
[
'name' => "composer/installers",
'version' => 'v1.9.0',
'source' => [
'type' => 'git',
'url' => 'https://github.com/composer/installers.git',
'reference' => 'b93bcf0fa1fccb0b7d176b0967d969691cd74cca',
],
],
[
'name' => 'symfony/polyfill-ctype',
'version' => 'v1.12.0',
'source' =>
[
'type' => 'git',
'url' => 'https://github.com/symfony/polyfill-ctype.git',
'reference' => '550ebaac289296ce228a706d0867afc34687e3f4',
],
],
[
'name' => 'symfony/yaml',
'version' => 'v3.4.32',
'source' =>
[
'type' => 'git',
'url' => 'https://github.com/symfony/yaml.git',
'reference' => '768f817446da74a776a31eea335540f9dcb53942',
],
],
],
'packages-dev' =>
[
[
'name' => 'behat/mink',
'version' => 'v1.8.0',
'source' =>
[
'type' => 'git',
'url' => 'https://github.com/minkphp/Mink.git',
'reference' => 'e1772aabb6b654464264a6cc72158c8b3409d8bc',
],
],
[
'name' => 'symfony/css-selector',
'version' => 'v4.3.5',
'source' =>
[
'type' => 'git',
'url' => 'https://github.com/symfony/css-selector.git',
'reference' => 'f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9',
],
],
],
];
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Generator;
use Drupal\Composer\Generator\Builder\DrupalCoreRecommendedBuilder;
use Drupal\Composer\Generator\Builder\DrupalDevDependenciesBuilder;
use Drupal\Composer\Generator\Builder\DrupalPinnedDevDependenciesBuilder;
use Drupal\Composer\Generator\PackageGenerator;
use Drupal\Composer\Generator\Util\DrupalCoreComposer;
use PHPUnit\Framework\TestCase;
/**
* Test to see if the metapackages are up-to-date with the root composer.lock.
*
* @group Metapackage
*/
class MetapackageUpdateTest extends TestCase {
/**
* Provides test data for testUpdated.
*/
public static function updatedTestData() {
return [
[
DrupalCoreRecommendedBuilder::class,
'composer/Metapackage/CoreRecommended',
],
[
DrupalDevDependenciesBuilder::class,
'composer/Metapackage/DevDependencies',
],
[
DrupalPinnedDevDependenciesBuilder::class,
'composer/Metapackage/PinnedDevDependencies',
],
];
}
/**
* Tests to see if the generated metapackages are in sync with composer.lock.
*
* Note that this is not a test of code correctness, but rather it merely
* confirms if the package builder was used on the most recent set of
* metapackages.
*
* See BuilderTest.php for a test that checks for code correctness.
*
* @param string $builderClass
* The metapackage builder to test.
* @param string $path
* The relative path to the metapackage
*
* @dataProvider updatedTestData
*/
public function testUpdated($builderClass, $path): void {
// Create a DrupalCoreComposer for the System Under Test (current repo)
$repositoryRoot = dirname(__DIR__, 6);
$drupalCoreInfo = DrupalCoreComposer::createFromPath($repositoryRoot);
// Rebuild the metapackage for the composer.json / composer.lock of
// the current repo.
$builder = new $builderClass($drupalCoreInfo);
$generatedJson = $builder->getPackage();
$generatedJson = PackageGenerator::encode($generatedJson);
// Also load the most-recently-generated version of the metapackage.
$loadedJson = file_get_contents("$repositoryRoot/$path/composer.json");
// The generated json is the "expected", what we think the loaded
// json would contain, if the current patch is generated correctly
// (metapackages updated when composer.lock is updated).
$version = str_replace('.0-dev', '.x-dev', \Drupal::VERSION);
$message = <<< __EOT__
The rebuilt version of $path does not match what is in the source tree.
To fix, run:
COMPOSER_ROOT_VERSION=$version composer update --lock
__EOT__;
$this->assertEquals($generatedJson, $loadedJson, $message);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Generator;
use PHPUnit\Framework\TestCase;
/**
* Tests DrupalCoreRecommendedBuilder.
*
* @group Metapackage
*/
class OverlapWithTopLevelDependenciesTest extends TestCase {
/**
* Provides data for testOverlapWithTemplateProject().
*/
public static function templateProjectPathProvider() {
return [
[
'composer/Template/RecommendedProject',
],
[
'composer/Template/LegacyProject',
],
];
}
/**
* Tests top level and core-recommended dependencies do not overlap.
*
* @dataProvider templateProjectPathProvider
*
* @param string $template_project_path
* The path of the project template to test.
*/
public function testOverlapWithTemplateProject($template_project_path): void {
$root = dirname(__DIR__, 6);
// Read template project composer.json.
$top_level_composer_json = json_decode(file_get_contents("$root/$template_project_path/composer.json"), TRUE);
// Read drupal/core-recommended composer.json.
$core_recommended_composer_json = json_decode(file_get_contents("$root/composer/Metapackage/CoreRecommended/composer.json"), TRUE);
// Fail if any required project in the require section of the template
// project also exists in core/recommended.
foreach ($top_level_composer_json['require'] as $project => $version_constraint) {
$this->assertArrayNotHasKey($project, $core_recommended_composer_json['require'], "Pinned project $project is also a top-level dependency of $template_project_path. This can expose a Composer bug. See https://www.drupal.org/project/drupal/issues/3134648 and https://github.com/composer/composer/issues/8882");
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\ProjectMessage;
use Composer\Package\RootPackageInterface;
use Drupal\Composer\Plugin\ProjectMessage\Message;
use PHPUnit\Framework\TestCase;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass Drupal\Composer\Plugin\ProjectMessage\Message
* @group ProjectMessage
*/
class ConfigTest extends TestCase {
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
vfsStream::setup('config_test', NULL, [
'bespoke' => [
'special_file.txt' => "Special\nFile",
],
]);
}
public static function provideGetMessageText() {
return [
[[], []],
[
['Special', 'File'],
[
'drupal-core-project-message' => [
'event-name-file' => vfsStream::url('config_test/bespoke/special_file.txt'),
],
],
],
[
['I am the message.'],
[
'drupal-core-project-message' => [
'event-name-message' => ['I am the message.'],
],
],
],
[
['This message overrides file.'],
[
'drupal-core-project-message' => [
'event-name-message' => ['This message overrides file.'],
'event-name-file' => vfsStream::url('config_test/bespoke/special_file.txt'),
],
],
],
];
}
/**
* @dataProvider provideGetMessageText
* @covers ::getText
*/
public function testGetMessageText($expected, $config): void {
// Root package has our config.
$root = $this->createMock(RootPackageInterface::class);
$root->expects($this->once())
->method('getExtra')
->willReturn($config);
$message = new Message($root, 'event-name');
$this->assertSame($expected, $message->getText());
}
/**
* @covers ::getText
*/
public function testDefaultFile(): void {
// Root package has no extra field.
$root = $this->createMock(RootPackageInterface::class);
$root->expects($this->once())
->method('getExtra')
->willReturn([]);
// The default is to try to read from event-name-message.txt, so we expect
// config to try that.
$message = $this->getMockBuilder(Message::class)
->setConstructorArgs([$root, 'event-name'])
->onlyMethods(['getMessageFromFile'])
->getMock();
$message->expects($this->once())
->method('getMessageFromFile')
->with('event-name-message.txt')
->willReturn([]);
$this->assertSame([], $message->getText());
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold;
use Drupal\Tests\Traits\PhpUnitWarnings;
/**
* Convenience class for creating fixtures.
*/
trait AssertUtilsTrait {
use PhpUnitWarnings;
/**
* Asserts that a given file exists and is/is not a symlink.
*
* @param string $path
* The path to check exists.
* @param bool $is_link
* Checks if the file should be a symlink or not.
* @param string $contents_contains
* Regex to check the file contents.
*/
protected function assertScaffoldedFile($path, $is_link, $contents_contains) {
$this->assertFileExists($path);
$contents = file_get_contents($path);
$this->assertStringContainsString($contents_contains, basename($path) . ': ' . $contents);
$this->assertSame($is_link, is_link($path));
}
/**
* Asserts that a file does not exist or exists and does not contain a value.
*
* @param string $path
* The path to check exists.
* @param string $contents_not_contains
* A string that is expected should NOT occur in the file contents.
*/
protected function assertScaffoldedFileDoesNotContain($path, $contents_not_contains) {
// If the file does not exist at all, we'll count that as a pass.
if (!file_exists($path)) {
return;
}
$contents = file_get_contents($path);
$this->assertStringNotContainsString($contents_not_contains, $contents, basename($path) . ' contains unexpected contents:');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold;
use Symfony\Component\Process\Process;
/**
* Convenience class for creating fixtures.
*/
trait ExecTrait {
/**
* Runs an arbitrary command.
*
* @param string $cmd
* The command to execute (escaped as required)
* @param string $cwd
* The current working directory to run the command from.
* @param array $env
* Environment variables to define for the subprocess.
*
* @return string
* Standard output from the command
*/
protected function mustExec($cmd, $cwd, array $env = []) {
$process = Process::fromShellCommandline($cmd, $cwd, $env + ['PATH' => getenv('PATH'), 'HOME' => getenv('HOME')]);
$process->setTimeout(300)->setIdleTimeout(300)->run();
$exitCode = $process->getExitCode();
if (0 != $exitCode) {
throw new \RuntimeException("Exit code: {$exitCode}\n\n" . $process->getErrorOutput() . "\n\n" . $process->getOutput());
}
return $process->getOutput();
}
}

View File

@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold;
use Composer\Console\Application;
use Composer\Factory;
use Composer\IO\BufferIO;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\Handler;
use Drupal\Composer\Plugin\Scaffold\Interpolator;
use Drupal\Composer\Plugin\Scaffold\Operations\AppendOp;
use Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
/**
* Convenience class for creating fixtures.
*/
class Fixtures {
/**
* Keep a persistent prefix to help group our tmp directories together.
*
* @var string
*/
protected static $randomPrefix = '';
/**
* Directories to delete when we are done.
*
* @var string[]
*/
protected $tmpDirs = [];
/**
* A Composer IOInterface to write to.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The composer object.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Gets an IO fixture.
*
* @return \Composer\IO\IOInterface
* A Composer IOInterface to write to; output may be retrieved via
* Fixtures::getOutput().
*/
public function io() {
if (!$this->io) {
$this->io = new BufferIO();
}
return $this->io;
}
/**
* Gets the Composer object.
*
* @return \Composer\Composer
* The main Composer object, needed by the scaffold Handler, etc.
*/
public function getComposer() {
if (!$this->composer) {
$this->composer = Factory::create($this->io(), NULL, TRUE);
}
return $this->composer;
}
/**
* Gets the output from the io() fixture.
*
* @return string
* Output captured from tests that write to Fixtures::io().
*/
public function getOutput() {
return $this->io()->getOutput();
}
/**
* Gets the path to Scaffold component.
*
* Used to inject the component into composer.json files.
*
* @return string
* Path to the root of this project.
*/
public function projectRoot() {
return realpath(__DIR__) . '/../../../../../../../composer/Plugin/Scaffold';
}
/**
* Gets the path to the project fixtures.
*
* @return string
* Path to project fixtures
*/
public function allFixturesDir() {
return realpath(__DIR__ . '/fixtures');
}
/**
* Gets the path to one particular project fixture.
*
* @param string $project_name
* The project name to get the path for.
*
* @return string
* Path to project fixture.
*/
public function projectFixtureDir($project_name) {
$dir = $this->allFixturesDir() . '/' . $project_name;
if (!is_dir($dir)) {
throw new \RuntimeException("Requested fixture project {$project_name} that does not exist.");
}
return $dir;
}
/**
* Gets the path to one particular bin path.
*
* @param string $bin_name
* The bin name to get the path for.
*
* @return string
* Path to project fixture.
*/
public function binFixtureDir($bin_name) {
$dir = $this->allFixturesDir() . '/scripts/' . $bin_name;
if (!is_dir($dir)) {
throw new \RuntimeException("Requested fixture bin dir {$bin_name} that does not exist.");
}
return $dir;
}
/**
* Gets a path to a source scaffold fixture.
*
* Use in place of ScaffoldFilePath::sourcePath().
*
* @param string $project_name
* The name of the project to fetch; $package_name is
* "fixtures/$project_name".
* @param string $source
* The name of the asset; path is "assets/$source".
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* The full and relative path to the desired asset
*
* @see \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath::sourcePath()
*/
public function sourcePath($project_name, $source) {
$package_name = "fixtures/{$project_name}";
$source_rel_path = "assets/{$source}";
$package_path = $this->projectFixtureDir($project_name);
return ScaffoldFilePath::sourcePath($package_name, $package_path, 'unknown', $source_rel_path);
}
/**
* Gets an Interpolator with 'web-root' and 'package-name' set.
*
* Use in place of ManageOptions::getLocationReplacements().
*
* @return \Drupal\Composer\Plugin\Scaffold\Interpolator
* An interpolator with location replacements, including 'web-root'.
*
* @see \Drupal\Composer\Plugin\Scaffold\ManageOptions::getLocationReplacements()
*/
public function getLocationReplacements() {
$destinationTmpDir = $this->mkTmpDir('location-replacements');
$interpolator = new Interpolator();
$interpolator->setData(['web-root' => $destinationTmpDir, 'package-name' => 'fixtures/tmp-destination']);
return $interpolator;
}
/**
* Creates a ReplaceOp fixture.
*
* @param string $project_name
* The name of the project to fetch; $package_name is
* "fixtures/$project_name".
* @param string $source
* The name of the asset; path is "assets/$source".
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp
* A replace operation object.
*/
public function replaceOp($project_name, $source) {
$source_path = $this->sourcePath($project_name, $source);
return new ReplaceOp($source_path, TRUE);
}
/**
* Creates an AppendOp fixture.
*
* @param string $project_name
* The name of the project to fetch; $package_name is
* "fixtures/$project_name".
* @param string $source
* The name of the asset; path is "assets/$source".
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\AppendOp
* An append operation object.
*/
public function appendOp($project_name, $source) {
$source_path = $this->sourcePath($project_name, $source);
return new AppendOp(NULL, $source_path);
}
/**
* Gets a destination path in a tmp dir.
*
* Use in place of ScaffoldFilePath::destinationPath().
*
* @param string $destination
* Destination path; should be in the form '[web-root]/robots.txt', where
* '[web-root]' is always literally '[web-root]', with any arbitrarily
* desired filename following.
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $interpolator
* Location replacements. Obtain via Fixtures::getLocationReplacements()
* when creating multiple scaffold destinations.
* @param string $package_name
* (optional) The name of the fixture package that this path came from.
* Taken from interpolator if not provided.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* A destination scaffold file backed by temporary storage.
*
* @see \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath::destinationPath()
*/
public function destinationPath($destination, ?Interpolator $interpolator = NULL, $package_name = NULL) {
$interpolator = $interpolator ?: $this->getLocationReplacements();
$package_name = $package_name ?: $interpolator->interpolate('[package-name]');
return ScaffoldFilePath::destinationPath($package_name, $destination, $interpolator);
}
/**
* Generates a path to a temporary location, but do not create the directory.
*
* @param string $prefix
* A prefix for the temporary directory name.
*
* @return string
* Path to temporary directory
*/
public function tmpDir($prefix) {
$prefix .= static::persistentPrefix();
$tmpDir = sys_get_temp_dir() . '/scaffold-' . $prefix . uniqid(md5($prefix . microtime()), TRUE);
$this->tmpDirs[] = $tmpDir;
return $tmpDir;
}
/**
* Generates a persistent prefix to use with all of our temporary directories.
*
* The presumption is that this should reduce collisions in highly-parallel
* tests. We prepend the process id to play nicely with phpunit process
* isolation.
*
* @return string
* A random string that will remain the same for the entire process run.
*/
protected static function persistentPrefix() {
if (empty(static::$randomPrefix)) {
static::$randomPrefix = getmypid() . md5(microtime());
}
return static::$randomPrefix;
}
/**
* Creates a temporary directory.
*
* @param string $prefix
* A prefix for the temporary directory name.
*
* @return string
* Path to temporary directory
*/
public function mkTmpDir($prefix) {
$tmpDir = $this->tmpDir($prefix);
$filesystem = new Filesystem();
$filesystem->ensureDirectoryExists($tmpDir);
return $tmpDir;
}
/**
* Create an isolated cache directory for Composer.
*/
public function createIsolatedComposerCacheDir() {
$cacheDir = $this->mkTmpDir('composer-cache');
putenv("COMPOSER_CACHE_DIR=$cacheDir");
}
/**
* Calls 'tearDown' in any test that copies fixtures to transient locations.
*/
public function tearDown() {
// Remove any temporary directories that were created.
$filesystem = new Filesystem();
foreach ($this->tmpDirs as $dir) {
$filesystem->remove($dir);
}
// Clear out variables from the previous pass.
$this->tmpDirs = [];
$this->io = NULL;
// Clear the composer cache dir, if it was set
putenv('COMPOSER_CACHE_DIR=');
}
/**
* Creates a temporary copy of all of the fixtures projects into a temp dir.
*
* The fixtures remain dirty if they already exist. Individual tests should
* first delete any fixture directory that needs to remain pristine. Since all
* temporary directories are removed in tearDown, this is only an issue when
* a) the FIXTURE_DIR environment variable has been set, or b) tests are
* calling cloneFixtureProjects more than once per test method.
*
* @param string $fixturesDir
* The directory to place fixtures in.
* @param array $replacements
* Key : value mappings for placeholders to replace in composer.json
* templates.
*/
public function cloneFixtureProjects($fixturesDir, array $replacements = []) {
$filesystem = new Filesystem();
// We will replace 'SYMLINK' with the string 'true' in our composer.json
// fixture.
$replacements += ['SYMLINK' => 'true'];
$interpolator = new Interpolator('__', '__');
$interpolator->setData($replacements);
$filesystem->copy($this->allFixturesDir(), $fixturesDir);
$composer_json_templates = glob($fixturesDir . "/*/composer.json.tmpl");
foreach ($composer_json_templates as $composer_json_tmpl) {
// Inject replacements into composer.json.
if (file_exists($composer_json_tmpl)) {
$composer_json_contents = file_get_contents($composer_json_tmpl);
$composer_json_contents = $interpolator->interpolate($composer_json_contents, [], FALSE);
file_put_contents(dirname($composer_json_tmpl) . "/composer.json", $composer_json_contents);
@unlink($composer_json_tmpl);
}
}
}
/**
* Runs the scaffold operation.
*
* This is equivalent to running `composer composer-scaffold`, but we do the
* equivalent operation by instantiating a Handler object in order to continue
* running in the same process, so that coverage may be calculated for the
* code executed by these tests.
*
* @param string $cwd
* The working directory to run the scaffold command in.
*
* @return string
* Output captured from tests that write to Fixtures::io().
*/
public function runScaffold($cwd) {
chdir($cwd);
$handler = new Handler($this->getComposer(), $this->io());
$handler->scaffold();
return $this->getOutput();
}
/**
* Runs a `composer` command.
*
* @param string $cmd
* The Composer command to execute (escaped as required)
* @param string $cwd
* The current working directory to run the command from.
*
* @return string
* Standard output and standard error from the command.
*/
public function runComposer($cmd, $cwd) {
chdir($cwd);
$input = new StringInput($cmd);
$output = new BufferedOutput();
$application = new Application();
$application->setAutoExit(FALSE);
$exitCode = $application->run($input, $output);
$output = $output->fetch();
if ($exitCode != 0) {
throw new \Exception("Fixtures::runComposer failed to set up fixtures.\n\nCommand: '{$cmd}'\nExit code: {$exitCode}\nOutput: \n\n$output");
}
return $output;
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
/**
* Tests Composer Hooks that run scaffold operations.
*
* The purpose of this test file is to exercise all of the different Composer
* commands that invoke scaffold operations, and ensure that files are
* scaffolded when they should be.
*
* Note that this test file uses `exec` to run Composer for a pure functional
* test. Other functional test files invoke Composer commands directly via the
* Composer Application object, in order to get more accurate test coverage
* information.
*
* @group Scaffold
*/
class ComposerHookTest extends BuildTestBase {
use ExecTrait;
use AssertUtilsTrait;
/**
* Directory to perform the tests in.
*
* @var string
*/
protected $fixturesDir;
/**
* The Symfony FileSystem component.
*
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $fileSystem;
/**
* The Fixtures object.
*
* @var \Drupal\Tests\Composer\Plugin\Scaffold\Fixtures
*/
protected $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 = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->fixtures->projectRoot()];
$this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// Remove any temporary directories et. al. that were created.
$this->fixtures->tearDown();
parent::tearDown();
}
/**
* Tests to see if scaffold operation runs at the correct times.
*/
public function testComposerHooks(): void {
$topLevelProjectDir = 'composer-hooks-fixture';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
// First test: run composer install. This is the same as composer update
// since there is no lock file. Ensure that scaffold operation ran.
$this->mustExec("composer install --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
// Run composer required to add in the scaffold-override-fixture. This
// project is "allowed" in our main fixture project, but not required.
// We expect that requiring this library should re-scaffold, resulting
// in a changed default.settings.php file.
$stdout = $this->mustExec("composer require --no-ansi --no-interaction fixtures/drupal-assets-fixture:dev-main fixtures/scaffold-override-fixture:dev-main", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
// Make sure that the appropriate notice informing us that scaffolding
// is allowed was printed.
$this->assertStringContainsString('Package fixtures/scaffold-override-fixture has scaffold operations, and is already allowed in the root-level composer.json file.', $stdout);
// Delete one scaffold file, just for test purposes, then run
// 'composer update' and see if the scaffold file is replaced.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileDoesNotExist($sut . '/sites/default/default.settings.php');
$this->mustExec("composer update --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
// Delete the same test scaffold file again, then run
// 'composer drupal:scaffold' and see if the scaffold file is
// re-scaffolded.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileDoesNotExist($sut . '/sites/default/default.settings.php');
$this->mustExec("composer install --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
// Delete the same test scaffold file yet again, then run
// 'composer install' and see if the scaffold file is re-scaffolded.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileDoesNotExist($sut . '/sites/default/default.settings.php');
$this->mustExec("composer drupal:scaffold --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
// Run 'composer create-project' to create a new test project called
// 'create-project-test', which is a copy of 'fixtures/drupal-drupal'.
$sut = $this->fixturesDir . '/create-project-test';
$filesystem = new Filesystem();
$filesystem->remove($sut);
$stdout = $this->mustExec("composer create-project --repository=packages.json fixtures/drupal-drupal {$sut}", $this->fixturesDir, ['COMPOSER_MIRROR_PATH_REPOS' => 1]);
$this->assertDirectoryExists($sut);
$this->assertStringContainsString('Scaffolding files for fixtures/drupal-drupal', $stdout);
$this->assertScaffoldedFile($sut . '/index.php', FALSE, 'Test version of index.php from drupal/core');
}
/**
* Tests to see if scaffold messages are omitted when running scaffold twice.
*/
public function testScaffoldMessagesDoNotPrintTwice(): void {
$topLevelProjectDir = 'drupal-drupal';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
// First test: run composer install. This is the same as composer update
// since there is no lock file. Ensure that scaffold operation ran.
$stdout = $this->mustExec("composer install --no-ansi", $sut);
$this->assertStringContainsString('- Copy [web-root]/index.php from assets/index.php', $stdout);
$this->assertStringContainsString('- Copy [web-root]/update.php from assets/update.php', $stdout);
// Run scaffold operation again. It should not print anything.
$stdout = $this->mustExec("composer scaffold --no-ansi", $sut);
$this->assertEquals('', $stdout);
// Delete a file and run it again. It should re-scaffold the removed file.
unlink("$sut/index.php");
$stdout = $this->mustExec("composer scaffold --no-ansi", $sut);
$this->assertStringContainsString('- Copy [web-root]/index.php from assets/index.php', $stdout);
$this->assertStringNotContainsString('- Copy [web-root]/update.php from assets/update.php', $stdout);
}
/**
* Tests to see if scaffold events are dispatched and picked up by the plugin.
*/
public function testScaffoldEvents(): void {
$topLevelProjectDir = 'scaffold-events-fixture';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
$output = $this->mustExec("composer install --no-ansi", $sut);
$this->assertStringContainsString('Hello preDrupalScaffoldCmd', $output);
$this->assertStringContainsString('Hello postDrupalScaffoldCmd', $output);
}
}

View File

@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
use Drupal\Tests\PhpUnitCompatibilityTrait;
use PHPUnit\Framework\TestCase;
/**
* Tests to see whether .gitignore files are correctly managed.
*
* The purpose of this test file is to run a scaffold operation and
* confirm that the files that were scaffolded are added to the
* repository's .gitignore file.
*
* @group Scaffold
*/
class ManageGitIgnoreTest extends TestCase {
use ExecTrait;
use AssertUtilsTrait;
use PhpUnitCompatibilityTrait;
/**
* The root of this project.
*
* Used to substitute this project's base directory into composer.json files
* so Composer can find it.
*
* @var string
*/
protected $projectRoot;
/**
* Directory to perform the tests in.
*
* @var string
*/
protected $fixturesDir;
/**
* The Symfony FileSystem component.
*
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $fileSystem;
/**
* The Fixtures object.
*
* @var \Drupal\Tests\Composer\Plugin\Scaffold\Fixtures
*/
protected $fixtures;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->fileSystem = new Filesystem();
$this->fixtures = new Fixtures();
$this->fixtures->createIsolatedComposerCacheDir();
$this->projectRoot = $this->fixtures->projectRoot();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// Remove any temporary directories et. al. that were created.
$this->fixtures->tearDown();
}
/**
* Creates a system-under-test and initialize a git repository for it.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
*
* @return string
* The path to the fixture directory.
*/
protected function createSutWithGit($fixture_name) {
$this->fixturesDir = $this->fixtures->tmpDir($this->name());
$sut = $this->fixturesDir . '/' . $fixture_name;
$replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->projectRoot];
$this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
// .gitignore files will not be managed unless there is a git repository.
$this->mustExec('git init', $sut);
// Add some user info so git does not complain.
$this->mustExec('git config user.email "test@example.com"', $sut);
$this->mustExec('git config user.name "Test User"', $sut);
$this->mustExec('git add .', $sut);
$this->mustExec('git commit -m "Initial commit."', $sut);
// Run composer install, but suppress scaffolding.
$this->fixtures->runComposer("install --no-ansi --no-scripts --no-plugins", $sut);
return $sut;
}
/**
* Tests scaffold command correctly manages the .gitignore file.
*/
public function testManageGitIgnore(): void {
// Note that the drupal-composer-drupal-project fixture does not
// have any configuration settings related to .gitignore management.
$sut = $this->createSutWithGit('drupal-composer-drupal-project');
$this->assertFileDoesNotExist($sut . '/docroot/autoload.php');
$this->assertFileDoesNotExist($sut . '/docroot/index.php');
$this->assertFileDoesNotExist($sut . '/docroot/sites/.gitignore');
// Run the scaffold command.
$this->fixtures->runScaffold($sut);
$this->assertFileExists($sut . '/docroot/autoload.php');
$this->assertFileExists($sut . '/docroot/index.php');
$expected = <<<EOT
/build
/.csslintrc
/.editorconfig
/.eslintignore
/.eslintrc.json
/.gitattributes
/.ht.router.php
/autoload.php
/index.php
/robots.txt
/update.php
/web.config
EOT;
// At this point we should have a .gitignore file, because although we did
// not explicitly ask for .gitignore tracking, the vendor directory is not
// tracked, so the default in that instance is to manage .gitignore files.
$this->assertScaffoldedFile($sut . '/docroot/.gitignore', FALSE, $expected);
$this->assertScaffoldedFile($sut . '/docroot/sites/.gitignore', FALSE, 'example.settings.local.php');
$this->assertScaffoldedFile($sut . '/docroot/sites/default/.gitignore', FALSE, 'default.services.yml');
$expected = <<<EOT
M docroot/.gitignore
?? docroot/sites/.gitignore
?? docroot/sites/default/.gitignore
EOT;
// Check to see whether there are any untracked files. We expect that
// only the .gitignore files themselves should be untracked.
$stdout = $this->mustExec('git status --porcelain', $sut);
$this->assertEquals(trim($expected), trim($stdout));
}
/**
* Tests scaffold command does not manage the .gitignore file when disabled.
*/
public function testUnmanagedGitIgnoreWhenDisabled(): void {
// Note that the drupal-drupal fixture has a configuration setting
// `"gitignore": false,` which disables .gitignore file handling.
$sut = $this->createSutWithGit('drupal-drupal');
$this->assertFileDoesNotExist($sut . '/docroot/autoload.php');
$this->assertFileDoesNotExist($sut . '/docroot/index.php');
// Run the scaffold command.
$this->fixtures->runScaffold($sut);
$this->assertFileExists($sut . '/autoload.php');
$this->assertFileExists($sut . '/index.php');
$this->assertFileDoesNotExist($sut . '/.gitignore');
$this->assertFileDoesNotExist($sut . '/docroot/sites/default/.gitignore');
}
/**
* Tests appending to an unmanaged file, and confirm it is not .gitignored.
*
* If we append to an unmanaged (not scaffolded) file, and we are managing
* .gitignore files, then we expect that the unmanaged file should not be
* added to the .gitignore file, because unmanaged files should be committed.
*/
public function testAppendToEmptySettingsIsUnmanaged(): void {
$sut = $this->createSutWithGit('drupal-drupal-append-settings');
$this->assertFileDoesNotExist($sut . '/autoload.php');
$this->assertFileDoesNotExist($sut . '/index.php');
$this->assertFileDoesNotExist($sut . '/sites/.gitignore');
// Run the scaffold command.
$this->fixtures->runScaffold($sut);
$this->assertFileExists($sut . '/autoload.php');
$this->assertFileExists($sut . '/index.php');
$this->assertScaffoldedFile($sut . '/sites/.gitignore', FALSE, 'example.sites.php');
$this->assertScaffoldedFileDoesNotContain($sut . '/sites/.gitignore', 'settings.php');
}
/**
* Tests scaffold command disables .gitignore management when git not present.
*
* The scaffold operation should still succeed if there is no 'git'
* executable.
*/
public function testUnmanagedGitIgnoreWhenGitNotAvailable(): void {
// Note that the drupal-composer-drupal-project fixture does not have any
// configuration settings related to .gitignore management.
$sut = $this->createSutWithGit('drupal-composer-drupal-project');
$this->assertFileDoesNotExist($sut . '/docroot/sites/default/.gitignore');
$this->assertFileDoesNotExist($sut . '/docroot/index.php');
$this->assertFileDoesNotExist($sut . '/docroot/sites/.gitignore');
// Confirm that 'git' is available (n.b. if it were not, createSutWithGit()
// would fail).
$output = [];
exec('git --help', $output, $status);
$this->assertEquals(0, $status);
// Modify our $PATH so that it begins with a path that contains an
// executable script named 'git' that always exits with 127, as if git were
// not found. Note that we run our tests using process isolation, so we do
// not need to restore the PATH when we are done.
$unavailableGitPath = $sut . '/bin';
mkdir($unavailableGitPath);
$bash = <<<SH
#!/bin/bash
exit 127
SH;
file_put_contents($unavailableGitPath . '/git', $bash);
chmod($unavailableGitPath . '/git', 0755);
$oldPath = getenv('PATH');
putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH'));
// Confirm that 'git' is no longer available.
$output = [];
exec('git --help', $output, $status);
$this->assertEquals(127, $status);
// Run the scaffold command.
$output = $this->mustExec('composer drupal:scaffold 2>&1', NULL);
putenv('PATH=' . $oldPath . ':' . getenv('PATH'));
$expected = <<<EOT
Scaffolding files for fixtures/drupal-assets-fixture:
- Copy [web-root]/.csslintrc from assets/.csslintrc
- Copy [web-root]/.editorconfig from assets/.editorconfig
- Copy [web-root]/.eslintignore from assets/.eslintignore
- Copy [web-root]/.eslintrc.json from assets/.eslintrc.json
- Copy [web-root]/.gitattributes from assets/.gitattributes
- Copy [web-root]/.ht.router.php from assets/.ht.router.php
- Skip [web-root]/.htaccess: overridden in fixtures/drupal-composer-drupal-project
- Copy [web-root]/sites/default/default.services.yml from assets/default.services.yml
- Skip [web-root]/sites/default/default.settings.php: overridden in fixtures/scaffold-override-fixture
- Copy [web-root]/sites/example.settings.local.php from assets/example.settings.local.php
- Copy [web-root]/sites/example.sites.php from assets/example.sites.php
- Copy [web-root]/index.php from assets/index.php
- Skip [web-root]/robots.txt: overridden in fixtures/drupal-composer-drupal-project
- Copy [web-root]/update.php from assets/update.php
- Copy [web-root]/web.config from assets/web.config
Scaffolding files for fixtures/scaffold-override-fixture:
- Copy [web-root]/sites/default/default.settings.php from assets/override-settings.php
Scaffolding files for fixtures/drupal-composer-drupal-project:
- Skip [web-root]/.htaccess: disabled
- Copy [web-root]/robots.txt from assets/robots-default.txt
EOT;
$this->assertStringContainsString($expected, $output);
$this->assertFileExists($sut . '/docroot/index.php');
$this->assertFileDoesNotExist($sut . '/docroot/sites/default/.gitignore');
}
}

View File

@@ -0,0 +1,439 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Composer\Plugin\Scaffold\ScaffoldTestResult;
use Drupal\Tests\PhpUnitCompatibilityTrait;
use PHPUnit\Framework\TestCase;
/**
* Tests Composer Scaffold.
*
* The purpose of this test file is to exercise all of the different kinds of
* scaffold operations: copy, symlinks, skips and so on.
*
* @group Scaffold
*/
class ScaffoldTest extends TestCase {
use AssertUtilsTrait;
use PhpUnitCompatibilityTrait;
/**
* The root of this project.
*
* Used to substitute this project's base directory into composer.json files
* so Composer can find it.
*
* @var string
*/
protected $projectRoot;
/**
* Directory to perform the tests in.
*
* @var string
*/
protected $fixturesDir;
/**
* The Symfony FileSystem component.
*
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $fileSystem;
/**
* The Fixtures object.
*
* @var \Drupal\Tests\Composer\Plugin\Scaffold\Fixtures
*/
protected $fixtures;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->fileSystem = new Filesystem();
$this->fixtures = new Fixtures();
$this->fixtures->createIsolatedComposerCacheDir();
$this->projectRoot = $this->fixtures->projectRoot();
// The directory used for creating composer projects to test can be
// configured using the SCAFFOLD_FIXTURE_DIR environment variable. Otherwise
// a directory will be created in the system's temporary directory.
$this->fixturesDir = getenv('SCAFFOLD_FIXTURE_DIR');
if (!$this->fixturesDir) {
$this->fixturesDir = $this->fixtures->tmpDir($this->name());
}
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// Remove any temporary directories et. al. that were created.
$this->fixtures->tearDown();
}
/**
* Creates the System-Under-Test.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
* @param array $replacements
* Key : value mappings for placeholders to replace in composer.json
* templates.
*
* @return string
* The path to the created System-Under-Test.
*/
protected function createSut($fixture_name, array $replacements = []) {
$sut = $this->fixturesDir . '/' . $fixture_name;
// Erase just our sut, to ensure it is clean. Recopy all of the fixtures.
$this->fileSystem->remove($sut);
$replacements += ['PROJECT_ROOT' => $this->projectRoot];
$this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
return $sut;
}
/**
* Creates the system-under-test and runs a scaffold operation on it.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
* @param bool $is_link
* Whether to use symlinks for 'replace' operations.
* @param bool $relocated_docroot
* Whether the named fixture has a relocated document root.
*/
public function scaffoldSut($fixture_name, $is_link = FALSE, $relocated_docroot = TRUE) {
$sut = $this->createSut($fixture_name, ['SYMLINK' => $is_link ? 'true' : 'false']);
// Run composer install to get the dependencies we need to test.
$this->fixtures->runComposer("install --no-ansi --no-scripts --no-plugins", $sut);
// Test drupal:scaffold.
$scaffoldOutput = $this->fixtures->runScaffold($sut);
// Calculate the docroot directory and assert that our fixture layout
// matches what was stipulated in $relocated_docroot. Fail fast if
// the caller provided the wrong value.
$docroot = $sut;
if ($relocated_docroot) {
$docroot .= '/docroot';
$this->assertFileExists($docroot);
}
else {
$this->assertFileDoesNotExist($sut . '/docroot');
}
return new ScaffoldTestResult($docroot, $scaffoldOutput);
}
/**
* Data provider for testScaffoldWithExpectedException.
*/
public static function scaffoldExpectedExceptionTestValues() {
return [
[
'drupal-drupal-missing-scaffold-file',
'Scaffold file assets/missing-robots-default.txt not found in package fixtures/drupal-drupal-missing-scaffold-file.',
TRUE,
],
[
'project-with-empty-scaffold-path',
'No scaffold file path given for [web-root]/my-error in package fixtures/project-with-empty-scaffold-path',
FALSE,
],
[
'project-with-illegal-dir-scaffold',
'Scaffold file assets in package fixtures/project-with-illegal-dir-scaffold is a directory; only files may be scaffolded',
FALSE,
],
];
}
/**
* Tests that scaffold files throw when they have bad values.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
* @param string $expected_exception_message
* The expected exception message.
* @param bool $is_link
* Whether or not symlinking should be used.
*
* @dataProvider scaffoldExpectedExceptionTestValues
*/
public function testScaffoldWithExpectedException($fixture_name, $expected_exception_message, $is_link): void {
// Test scaffold. Expect an error.
$this->expectException(\Exception::class);
$this->expectExceptionMessage($expected_exception_message);
$this->scaffoldSut($fixture_name, $is_link);
}
/**
* Try to scaffold a project that does not scaffold anything.
*/
public function testEmptyProject(): void {
$fixture_name = 'empty-fixture';
$result = $this->scaffoldSut($fixture_name, FALSE, FALSE);
$this->assertStringContainsString('Nothing scaffolded because no packages are allowed in the top-level composer.json file', $result->scaffoldOutput());
}
/**
* Try to scaffold a project that allows a project with no scaffold files.
*/
public function testProjectThatScaffoldsEmptyProject(): void {
$fixture_name = 'project-allowing-empty-fixture';
$result = $this->scaffoldSut($fixture_name, FALSE, FALSE);
$this->assertStringContainsString('The allowed package fixtures/empty-fixture does not provide a file mapping for Composer Scaffold', $result->scaffoldOutput());
$this->assertCommonDrupalAssetsWereScaffolded($result->docroot(), FALSE);
$this->assertAutoloadFileCorrect($result->docroot());
}
public static function scaffoldOverridingSettingsExcludingHtaccessValues() {
return [
[
'drupal-composer-drupal-project',
TRUE,
TRUE,
],
[
'drupal-drupal',
FALSE,
FALSE,
],
];
}
/**
* Asserts that the drupal/assets scaffold files correct for sut.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
* @param bool $is_link
* Whether to use symlinks for 'replace' operations.
* @param bool $relocated_docroot
* Whether the named fixture has a relocated document root.
*
* @dataProvider scaffoldOverridingSettingsExcludingHtaccessValues
*/
public function testScaffoldOverridingSettingsExcludingHtaccess($fixture_name, $is_link, $relocated_docroot): void {
$result = $this->scaffoldSut($fixture_name, $is_link, $relocated_docroot);
$this->assertCommonDrupalAssetsWereScaffolded($result->docroot(), $is_link);
$this->assertAutoloadFileCorrect($result->docroot(), $relocated_docroot);
$this->assertDefaultSettingsFromScaffoldOverride($result->docroot(), $is_link);
$this->assertHtaccessExcluded($result->docroot());
}
/**
* Asserts that the appropriate file was replaced.
*
* Check the drupal/drupal-based project to confirm that the expected file was
* replaced, and that files that were not supposed to be replaced remain
* unchanged.
*/
public function testDrupalDrupalFileWasReplaced(): void {
$fixture_name = 'drupal-drupal-test-overwrite';
$result = $this->scaffoldSut($fixture_name, FALSE, FALSE);
$this->assertScaffoldedFile($result->docroot() . '/replace-me.txt', FALSE, 'from assets that replaces file');
$this->assertScaffoldedFile($result->docroot() . '/keep-me.txt', FALSE, 'File in drupal-drupal-test-overwrite that is not replaced');
$this->assertScaffoldedFile($result->docroot() . '/make-me.txt', FALSE, 'from assets that replaces file');
$this->assertCommonDrupalAssetsWereScaffolded($result->docroot(), FALSE);
$this->assertAutoloadFileCorrect($result->docroot());
$this->assertScaffoldedFile($result->docroot() . '/robots.txt', FALSE, $fixture_name);
}
/**
* Provides test values for testDrupalDrupalFileWasAppended.
*/
public static function scaffoldAppendTestValues(): array {
return array_merge(
static::scaffoldAppendTestValuesToPermute(FALSE),
static::scaffoldAppendTestValuesToPermute(TRUE),
[
[
'drupal-drupal-append-settings',
FALSE,
'sites/default/settings.php',
'<?php
// Default settings.php contents
include __DIR__ . "/settings-custom-additions.php";',
'NOTICE Creating a new file at [web-root]/sites/default/settings.php. Examine the contents and ensure that it came out correctly.',
],
]
);
}
/**
* Tests values to run both with $is_link FALSE and $is_link TRUE.
*
* @param bool $is_link
* Whether or not symlinking should be used.
*/
protected static function scaffoldAppendTestValuesToPermute($is_link) {
return [
[
'drupal-drupal-test-append',
$is_link,
'robots.txt',
'# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
# This content is prepended to the top of the existing robots.txt fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# Test version of robots.txt from drupal/core.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
',
'Prepend to [web-root]/robots.txt from assets/prepend-to-robots.txt',
],
[
'drupal-drupal-append-to-append',
$is_link,
'robots.txt',
'# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture.
# This content is prepended to the top of the existing robots.txt fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# Test version of robots.txt from drupal/core.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in profile-with-append composer.json fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture.',
'Append to [web-root]/robots.txt from assets/append-to-robots.txt',
],
];
}
/**
* Tests a fixture where the robots.txt file is prepended / appended to.
*
* @param string $fixture_name
* The name of the fixture to use from
* core/tests/Drupal/Tests/Component/Scaffold/fixtures.
* @param bool $is_link
* Whether or not symlinking should be used.
* @param string $scaffold_file_path
* Relative path to the scaffold file target we are testing.
* @param string $scaffold_file_contents
* A string expected to be contained inside the scaffold file we are testing.
* @param string $scaffoldOutputContains
* A string expected to be contained in the scaffold command output.
*
* @dataProvider scaffoldAppendTestValues
*/
public function testDrupalDrupalFileWasAppended(string $fixture_name, bool $is_link, string $scaffold_file_path, string $scaffold_file_contents, string $scaffoldOutputContains): void {
$result = $this->scaffoldSut($fixture_name, $is_link, FALSE);
$this->assertStringContainsString($scaffoldOutputContains, $result->scaffoldOutput());
$this->assertScaffoldedFile($result->docroot() . '/' . $scaffold_file_path, FALSE, $scaffold_file_contents);
$this->assertCommonDrupalAssetsWereScaffolded($result->docroot(), $is_link);
$this->assertAutoloadFileCorrect($result->docroot());
}
/**
* Asserts that the default settings file was overridden by the test.
*
* @param string $docroot
* The path to the System-under-Test's docroot.
* @param bool $is_link
* Whether or not symlinking is used.
*
* @internal
*/
protected function assertDefaultSettingsFromScaffoldOverride(string $docroot, bool $is_link): void {
$this->assertScaffoldedFile($docroot . '/sites/default/default.settings.php', $is_link, 'scaffolded from the scaffold-override-fixture');
}
/**
* Asserts that the .htaccess file was excluded by the test.
*
* @param string $docroot
* The path to the System-under-Test's docroot.
*
* @internal
*/
protected function assertHtaccessExcluded(string $docroot): void {
// Ensure that the .htaccess.txt file was not written, as our
// top-level composer.json excludes it from the files to scaffold.
$this->assertFileDoesNotExist($docroot . '/.htaccess');
}
/**
* Asserts that the scaffold files from drupal/assets are placed as expected.
*
* This tests that all assets from drupal/assets were scaffolded, save
* for .htaccess, robots.txt and default.settings.php, which are scaffolded
* in different ways in different tests.
*
* @param string $docroot
* The path to the System-under-Test's docroot.
* @param bool $is_link
* Whether or not symlinking is used.
*
* @internal
*/
protected function assertCommonDrupalAssetsWereScaffolded(string $docroot, bool $is_link): void {
// Assert scaffold files are written in the correct locations.
$this->assertScaffoldedFile($docroot . '/.csslintrc', $is_link, 'Test version of .csslintrc from drupal/core.');
$this->assertScaffoldedFile($docroot . '/.editorconfig', $is_link, 'Test version of .editorconfig from drupal/core.');
$this->assertScaffoldedFile($docroot . '/.eslintignore', $is_link, 'Test version of .eslintignore from drupal/core.');
$this->assertScaffoldedFile($docroot . '/.eslintrc.json', $is_link, 'Test version of .eslintrc.json from drupal/core.');
$this->assertScaffoldedFile($docroot . '/.gitattributes', $is_link, 'Test version of .gitattributes from drupal/core.');
$this->assertScaffoldedFile($docroot . '/.ht.router.php', $is_link, 'Test version of .ht.router.php from drupal/core.');
$this->assertScaffoldedFile($docroot . '/sites/default/default.services.yml', $is_link, 'Test version of default.services.yml from drupal/core.');
$this->assertScaffoldedFile($docroot . '/sites/example.settings.local.php', $is_link, 'Test version of example.settings.local.php from drupal/core.');
$this->assertScaffoldedFile($docroot . '/sites/example.sites.php', $is_link, 'Test version of example.sites.php from drupal/core.');
$this->assertScaffoldedFile($docroot . '/index.php', $is_link, 'Test version of index.php from drupal/core.');
$this->assertScaffoldedFile($docroot . '/update.php', $is_link, 'Test version of update.php from drupal/core.');
$this->assertScaffoldedFile($docroot . '/web.config', $is_link, 'Test version of web.config from drupal/core.');
}
/**
* Assert that the autoload file was scaffolded and contains correct path.
*
* @param string $docroot
* Location of the doc root, where autoload.php should be written.
* @param bool $relocated_docroot
* Whether the document root is relocated or now.
*
* @internal
*/
protected function assertAutoloadFileCorrect(string $docroot, bool $relocated_docroot = FALSE): void {
$autoload_path = $docroot . '/autoload.php';
// Ensure that the autoload.php file was written.
$this->assertFileExists($autoload_path);
$contents = file_get_contents($autoload_path);
$expected = "return require __DIR__ . '/vendor/autoload.php';";
if ($relocated_docroot) {
$expected = "return require __DIR__ . '/../vendor/autoload.php';";
}
$this->assertStringContainsString($expected, $contents);
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\PhpUnitCompatibilityTrait;
use PHPUnit\Framework\TestCase;
/**
* Tests Upgrading the Composer Scaffold plugin.
*
* Upgrading a Composer plugin can be a dangerous operation. If the plugin
* instantiates any classes during the activate method, and the plugin code
* is subsequently modified by a `composer update` operation, then any
* post-update hook (& etc.) may run with inconsistent code, leading to
* runtime errors. This test ensures that it is possible to upgrade from the
* last available stable 8.8.x tag to the current Scaffold plugin code (e.g. in
* the current patch-under-test).
*
* @group Scaffold
*/
class ScaffoldUpgradeTest extends TestCase {
use AssertUtilsTrait;
use ExecTrait;
use PhpUnitCompatibilityTrait;
/**
* The Fixtures object.
*
* @var \Drupal\Tests\Composer\Plugin\Scaffold\Fixtures
*/
protected $fixtures;
/**
* The Fixtures directory.
*
* @var string
*/
protected string $fixturesDir;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->fixtures = new Fixtures();
$this->fixtures->createIsolatedComposerCacheDir();
}
/**
* Tests upgrading the Composer Scaffold plugin.
*/
public function testScaffoldUpgrade(): void {
$composerVersionLine = exec('composer --version');
if (str_contains($composerVersionLine, 'Composer version 2')) {
$this->markTestSkipped('We cannot run the scaffold upgrade test with Composer 2 until we have a stable version of drupal/core-composer-scaffold to start from that we can install with Composer 2.x.');
}
$this->fixturesDir = $this->fixtures->tmpDir($this->name());
$replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->fixtures->projectRoot()];
$this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
$topLevelProjectDir = 'drupal-drupal';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
// First step: set up the Scaffold plug in. Ensure that scaffold operation
// ran. This is more of a control than a test.
$this->mustExec("composer install --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'A settings.php fixture file scaffolded from the scaffold-override-fixture');
// Next, bring back packagist.org and install core-composer-scaffold:8.8.0.
// Packagist is disabled in the fixture; we bring it back by removing the
// line that disables it.
$this->mustExec("composer config --unset repositories.packagist.org", $sut);
$stdout = $this->mustExec("composer require --no-ansi drupal/core-composer-scaffold:8.8.0 --no-plugins 2>&1", $sut);
$this->assertStringContainsString(" - Installing drupal/core-composer-scaffold (8.8.0):", $stdout);
// We can't force the path repo to re-install over the stable version
// without removing it, and removing it masks the bugs we are testing for.
// We will therefore make a git repo so that we can tag an explicit version
// to require.
$testVersion = '99.99.99';
$scaffoldPluginTmpRepo = $this->createTmpRepo($this->fixtures->projectRoot(), $this->fixturesDir, $testVersion);
// Disable packagist.org and upgrade back to the Scaffold plugin under test.
// This puts the `"packagist.org": false` config line back in composer.json
// so that Packagist will no longer be used.
$this->mustExec("composer config repositories.packagist.org false", $sut);
$this->mustExec("composer config repositories.composer-scaffold vcs 'file:///$scaffoldPluginTmpRepo'", $sut);
// Using 'mustExec' was giving a strange binary string here.
$output = $this->mustExec("composer require --no-ansi drupal/core-composer-scaffold:$testVersion 2>&1", $sut);
$this->assertStringContainsString("Installing drupal/core-composer-scaffold ($testVersion)", $output);
// Remove a scaffold file and run the scaffold command again to prove that
// scaffolding is still working.
unlink("$sut/index.php");
$stdout = $this->mustExec("composer scaffold", $sut);
$this->assertStringContainsString("Scaffolding files for", $stdout);
$this->assertFileExists("$sut/index.php");
}
/**
* Copy the provided source directory and create a temporary git repository.
*
* @param string $source
* Path to directory to copy.
* @param string $destParent
* Path to location to create git repository.
* @param string $version
* Version to tag the repository with.
*
* @return string
* Path to temporary git repository.
*/
protected function createTmpRepo($source, $destParent, $version) {
$target = $destParent . '/' . basename($source);
$filesystem = new Filesystem();
$filesystem->copy($source, $target);
$this->mustExec("git init", $target);
$this->mustExec('git config user.email "scaffoldtest@example.com"', $target);
$this->mustExec('git config user.name "Scaffold Test"', $target);
$this->mustExec("git add .", $target);
$this->mustExec("git commit -m 'Initial commit'", $target);
$this->mustExec("git tag $version", $target);
return $target;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Integration;
use Drupal\Composer\Plugin\Scaffold\Operations\AppendOp;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Composer\Plugin\Scaffold\Operations\AppendOp
*
* @group Scaffold
*/
class AppendOpTest extends TestCase {
use PhpUnitWarnings;
/**
* @covers ::process
*/
public function testProcess(): void {
$fixtures = new Fixtures();
$destination = $fixtures->destinationPath('[web-root]/robots.txt');
$options = ScaffoldOptions::create([]);
// Assert that there is no target file before we run our test.
$this->assertFileDoesNotExist($destination->fullPath());
// Create a file.
file_put_contents($destination->fullPath(), "# This is a test\n");
$prepend = $fixtures->sourcePath('drupal-drupal-test-append', 'prepend-to-robots.txt');
$append = $fixtures->sourcePath('drupal-drupal-test-append', 'append-to-robots.txt');
$sut = new AppendOp($prepend, $append, TRUE);
$sut->scaffoldAtNewLocation($destination);
$expected = <<<EOT
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
# This content is prepended to the top of the existing robots.txt fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This is a test
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
EOT;
$pre_calculated_contents = $sut->contents();
$this->assertEquals(trim($expected), trim($pre_calculated_contents));
// Test the system under test.
$sut->process($destination, $fixtures->io(), $options);
// Assert that the target file was created.
$this->assertFileExists($destination->fullPath());
// Assert the target contained the contents from the correct scaffold files.
$contents = trim(file_get_contents($destination->fullPath()));
$this->assertEquals(trim($expected), $contents);
// Confirm that expected output was written to our io fixture.
$output = $fixtures->getOutput();
$this->assertStringContainsString('Prepend to [web-root]/robots.txt from assets/prepend-to-robots.txt', $output);
$this->assertStringContainsString('Append to [web-root]/robots.txt from assets/append-to-robots.txt', $output);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Integration;
use Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp
*
* @group Scaffold
*/
class ReplaceOpTest extends TestCase {
use PhpUnitWarnings;
/**
* @covers ::process
*/
public function testProcess(): void {
$fixtures = new Fixtures();
$destination = $fixtures->destinationPath('[web-root]/robots.txt');
$source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt');
$options = ScaffoldOptions::create([]);
$sut = new ReplaceOp($source, TRUE);
// Assert that there is no target file before we run our test.
$this->assertFileDoesNotExist($destination->fullPath());
// Test the system under test.
$sut->process($destination, $fixtures->io(), $options);
// Assert that the target file was created.
$this->assertFileExists($destination->fullPath());
// Assert the target contained the contents from the correct scaffold file.
$contents = trim(file_get_contents($destination->fullPath()));
$this->assertEquals('# Test version of robots.txt from drupal/core.', $contents);
// Confirm that expected output was written to our io fixture.
$output = $fixtures->getOutput();
$this->assertStringContainsString('Copy [web-root]/robots.txt from assets/robots.txt', $output);
}
/**
* @covers ::process
*/
public function testEmptyFile(): void {
$fixtures = new Fixtures();
$destination = $fixtures->destinationPath('[web-root]/empty_file.txt');
$source = $fixtures->sourcePath('empty-file', 'empty_file.txt');
$options = ScaffoldOptions::create([]);
$sut = new ReplaceOp($source, TRUE);
// Assert that there is no target file before we run our test.
$this->assertFileDoesNotExist($destination->fullPath());
// Test the system under test.
$sut->process($destination, $fixtures->io(), $options);
// Assert that the target file was created.
$this->assertFileExists($destination->fullPath());
// Assert the target contained the contents from the correct scaffold file.
$this->assertSame('', file_get_contents($destination->fullPath()));
// Confirm that expected output was written to our io fixture.
$output = $fixtures->getOutput();
$this->assertStringContainsString('Copy [web-root]/empty_file.txt from assets/empty_file.txt', $output);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Integration;
use PHPUnit\Framework\TestCase;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Composer\Plugin\Scaffold\Operations\AppendOp;
use Drupal\Composer\Plugin\Scaffold\Operations\SkipOp;
use Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection;
/**
* @coversDefaultClass \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection
*
* @group Scaffold
*/
class ScaffoldFileCollectionTest extends TestCase {
/**
* @covers ::__construct
*/
public function testCreate(): void {
$fixtures = new Fixtures();
$locationReplacements = $fixtures->getLocationReplacements();
$scaffold_file_fixtures = [
'fixtures/drupal-assets-fixture' => [
'[web-root]/index.php' => $fixtures->replaceOp('drupal-assets-fixture', 'index.php'),
'[web-root]/.htaccess' => $fixtures->replaceOp('drupal-assets-fixture', '.htaccess'),
'[web-root]/robots.txt' => $fixtures->replaceOp('drupal-assets-fixture', 'robots.txt'),
'[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-assets-fixture', 'default.services.yml'),
],
'fixtures/drupal-profile' => [
'[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-profile', 'profile.default.services.yml'),
],
'fixtures/drupal-drupal' => [
'[web-root]/.htaccess' => new SkipOp(),
'[web-root]/robots.txt' => $fixtures->appendOp('drupal-drupal-test-append', 'append-to-robots.txt'),
],
];
$sut = new ScaffoldFileCollection($scaffold_file_fixtures, $locationReplacements);
$resolved_file_mappings = iterator_to_array($sut);
// Confirm that the keys of the output are the same as the keys of the
// input.
$this->assertEquals(array_keys($scaffold_file_fixtures), array_keys($resolved_file_mappings));
// '[web-root]/robots.txt' is now a SkipOp, as it is now part of an
// append operation.
$this->assertEquals([
'[web-root]/index.php',
'[web-root]/.htaccess',
'[web-root]/robots.txt',
'[web-root]/sites/default/default.services.yml',
], array_keys($resolved_file_mappings['fixtures/drupal-assets-fixture']));
$this->assertInstanceOf(SkipOp::class, $resolved_file_mappings['fixtures/drupal-assets-fixture']['[web-root]/robots.txt']->op());
$this->assertEquals([
'[web-root]/sites/default/default.services.yml',
], array_keys($resolved_file_mappings['fixtures/drupal-profile']));
$this->assertEquals([
'[web-root]/.htaccess',
'[web-root]/robots.txt',
], array_keys($resolved_file_mappings['fixtures/drupal-drupal']));
// Test that .htaccess is skipped.
$this->assertInstanceOf(SkipOp::class, $resolved_file_mappings['fixtures/drupal-assets-fixture']['[web-root]/.htaccess']->op());
// Test that the expected append operation exists.
$this->assertInstanceOf(AppendOp::class, $resolved_file_mappings['fixtures/drupal-drupal']['[web-root]/robots.txt']->op());
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold\Integration;
use Drupal\Composer\Plugin\Scaffold\Operations\SkipOp;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass \Drupal\Composer\Plugin\Scaffold\Operations\SkipOp
*
* @group Scaffold
*/
class SkipOpTest extends TestCase {
use PhpUnitWarnings;
/**
* @covers ::process
*/
public function testProcess(): void {
$fixtures = new Fixtures();
$destination = $fixtures->destinationPath('[web-root]/robots.txt');
$options = ScaffoldOptions::create([]);
$sut = new SkipOp();
// Assert that there is no target file before we run our test.
$this->assertFileDoesNotExist($destination->fullPath());
// Test the system under test.
$sut->process($destination, $fixtures->io(), $options);
// Assert that the target file was not created.
$this->assertFileDoesNotExist($destination->fullPath());
// Confirm that expected output was written to our io fixture.
$output = $fixtures->getOutput();
$this->assertStringContainsString('Skip [web-root]/robots.txt: disabled', $output);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold;
/**
* Holds result of a scaffold test.
*/
class ScaffoldTestResult {
protected $docroot;
protected $scaffoldOutput;
/**
* Holds the location of the scaffold fixture and the stdout from the test.
*
* @param string $docroot
* The location of the scaffold fixture.
* @param string $scaffoldOutput
* The stdout from the test.
*/
public function __construct($docroot, $scaffoldOutput) {
$this->docroot = $docroot;
$this->scaffoldOutput = $scaffoldOutput;
}
/**
* Returns the location of the docroot from the scaffold test.
*
* @return string
*/
public function docroot() {
return $this->docroot;
}
/**
* Returns the standard output from the scaffold test.
*
* @return string
*/
public function scaffoldOutput() {
return $this->scaffoldOutput;
}
}

View File

@@ -0,0 +1,38 @@
# Fixtures README
These fixtures are automatically copied to a temporary directory during test
runs. After the test run, the fixtures are automatically deleted.
Set the SCAFFOLD_FIXTURE_DIR environment variable to place the fixtures in a
specific location rather than a temporary directory. If this is done, then the
fixtures will not be deleted after the test run. This is useful for ad-hoc
testing.
Example:
$ SCAFFOLD_FIXTURE_DIR=$HOME/tmp/scaffold-fixtures composer unit
$ cd $HOME/tmp/scaffold-fixtures
$ cd drupal-drupal
$ composer drupal:scaffold
Scaffolding files for fixtures/drupal-assets-fixture:
- Link [web-root]/.csslintrc from assets/.csslintrc
- Link [web-root]/.editorconfig from assets/.editorconfig
- Link [web-root]/.eslintignore from assets/.eslintignore
- Link [web-root]/.eslintrc.json from assets/.eslintrc.json
- Link [web-root]/.gitattributes from assets/.gitattributes
- Link [web-root]/.ht.router.php from assets/.ht.router.php
- Skip [web-root]/.htaccess: overridden in my/project
- Link [web-root]/sites/default/default.services.yml from assets/default.services.yml
- Skip [web-root]/sites/default/default.settings.php: overridden in fixtures/scaffold-override-fixture
- Link [web-root]/sites/example.settings.local.php from assets/example.settings.local.php
- Link [web-root]/sites/example.sites.php from assets/example.sites.php
- Link [web-root]/index.php from assets/index.php
- Skip [web-root]/robots.txt: overridden in my/project
- Link [web-root]/update.php from assets/update.php
- Link [web-root]/web.config from assets/web.config
Scaffolding files for fixtures/scaffold-override-fixture:
- Link [web-root]/sites/default/default.settings.php from assets/override-settings.php
Scaffolding files for my/project:
- Skip [web-root]/.htaccess: disabled
- Link [web-root]/robots.txt from assets/robots-default.txt

View File

@@ -0,0 +1 @@
# robots.txt fixture scaffolded from "file-mappings" in composer-hooks-fixture composer.json fixture.

View File

@@ -0,0 +1,73 @@
{
"name": "fixtures/drupal-drupal",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/scaffold-override-fixture"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/robots-default.txt"
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
},
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "fixtures/composer-plugin-implements-scaffold-events",
"type": "composer-plugin",
"require": {
"composer-plugin-api": "^2",
"drupal/core-composer-scaffold": "*"
},
"autoload": {
"psr-4": {
"Drupal\\Tests\\fixture\\Composer\\Plugin\\": "src"
}
},
"extra": {
"class": "Drupal\\Tests\\fixture\\Composer\\Plugin\\ComposerPluginImplementsScaffoldEvents"
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types = 1);
namespace Drupal\Tests\fixture\Composer\Plugin;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Plugin\PluginInterface;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Script\Event;
use Drupal\Composer\Plugin\Scaffold\Handler;
/**
* A fixture composer plugin implement Drupal scaffold events.
*/
class ComposerPluginImplementsScaffoldEvents implements PluginInterface, EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
Handler::PRE_DRUPAL_SCAFFOLD_CMD => 'preDrupalScaffoldCmd',
Handler::POST_DRUPAL_SCAFFOLD_CMD => 'postDrupalScaffoldCmd',
];
}
/**
* Implements pre Drupal scaffold cmd.
*/
public static function preDrupalScaffoldCmd(Event $event): void {
$event->getIO()->write('Hello preDrupalScaffoldCmd');
}
/**
* Implements post Drupal scaffold cmd.
*/
public static function postDrupalScaffoldCmd(Event $event): void {
$event->getIO()->write('Hello postDrupalScaffoldCmd');
}
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io): void {
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io): void {
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io): void {
}
}

View File

@@ -0,0 +1 @@
# Test version of .csslintrc from drupal/core.

View File

@@ -0,0 +1 @@
# Test version of .editorconfig from drupal/core.

View File

@@ -0,0 +1 @@
# Test version of .eslintignore from drupal/core.

View File

@@ -0,0 +1,2 @@
// Test version of .eslintrc.json from drupal/core.
{}

View File

@@ -0,0 +1 @@
# Test version of .gitattributes from drupal/core.

View File

@@ -0,0 +1,2 @@
<?php
// Test version of .ht.router.php from drupal/core.

View File

@@ -0,0 +1 @@
# Test version of .htaccess from drupal/core.

View File

@@ -0,0 +1 @@
# Test version of default.services.yml from drupal/core.

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* Test version of default.settings.php from drupal/core.
*/

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* Test version of example.settings.local.php from drupal/core.
*/

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* Test version of example.sites.php from drupal/core.
*/

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* Test version of index.php from drupal/core.
*/

View File

@@ -0,0 +1 @@
# Test version of robots.txt from drupal/core.

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* Test version of update.php from drupal/core.
*/

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Test version of web.config from drupal/core. -->

View File

@@ -0,0 +1,24 @@
{
"name": "fixtures/drupal-assets-fixture",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/.csslintrc": "assets/.csslintrc",
"[web-root]/.editorconfig": "assets/.editorconfig",
"[web-root]/.eslintignore": "assets/.eslintignore",
"[web-root]/.eslintrc.json": "assets/.eslintrc.json",
"[web-root]/.gitattributes": "assets/.gitattributes",
"[web-root]/.ht.router.php": "assets/.ht.router.php",
"[web-root]/.htaccess": "assets/.htaccess",
"[web-root]/sites/default/default.services.yml": "assets/default.services.yml",
"[web-root]/sites/default/default.settings.php": "assets/default.settings.php",
"[web-root]/sites/example.settings.local.php": "assets/example.settings.local.php",
"[web-root]/sites/example.sites.php": "assets/example.sites.php",
"[web-root]/index.php": "assets/index.php",
"[web-root]/robots.txt": "assets/robots.txt",
"[web-root]/update.php": "assets/update.php",
"[web-root]/web.config": "assets/web.config"
}
}
}
}

View File

@@ -0,0 +1,2 @@
composer.lock
vendor

View File

@@ -0,0 +1 @@
# robots.txt fixture scaffolded from "file-mappings" in drupal-composer-drupal-project composer.json fixture.

View File

@@ -0,0 +1,74 @@
{
"name": "fixtures/drupal-composer-drupal-project",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*",
"fixtures/scaffold-override-fixture": "*"
},
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true
}
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/scaffold-override-fixture"
],
"locations": {
"web-root": "./docroot"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/robots-default.txt"
}
},
"installer-paths": {
"docroot/core": ["type:drupal-core"],
"docroot/modules/contrib/{$name}": ["type:drupal-module"],
"docroot/modules/custom/{$name}": ["type:drupal-custom-module"],
"docroot/profiles/contrib/{$name}": ["type:drupal-profile"],
"docroot/profiles/custom/{$name}": ["type:drupal-custom-profile"],
"docroot/themes/contrib/{$name}": ["type:drupal-theme"],
"docroot/themes/custom/{$name}": ["type:drupal-custom-theme"],
"docroot/libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "fixtures/drupal-core-fixture",
"require": {
"fixtures/drupal-assets-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-assets-fixture"
]
}
}
}

View File

@@ -0,0 +1 @@
include __DIR__ . "/settings-custom-additions.php";

View File

@@ -0,0 +1,3 @@
<?php
// Default settings.php contents

View File

@@ -0,0 +1,64 @@
{
"name": "fixtures/drupal-drupal-append-settings",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture"
],
"gitignore": true,
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/sites/default/settings.php": {
"default": "assets/default-settings.txt",
"append": "assets/append-to-settings.txt"
}
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1,3 @@
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture.

View File

@@ -0,0 +1,3 @@
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-append-to-append composer.json fixture.
# This content is prepended to the top of the existing robots.txt fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

View File

@@ -0,0 +1,72 @@
{
"name": "fixtures/drupal-drupal-test-append",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"profile-with-append": {
"type": "path",
"url": "../profile-with-append",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/profile-with-append": "*",
"fixtures/drupal-core-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/profile-with-append"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": {
"prepend": "assets/prepend-to-robots.txt",
"append": "assets/append-to-robots.txt"
}
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1,69 @@
{
"name": "fixtures/drupal-drupal-missing-scaffold-file",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*",
"fixtures/scaffold-override-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/scaffold-override-fixture"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/missing-robots-default.txt"
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1,3 @@
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.

View File

@@ -0,0 +1,3 @@
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
# This content is prepended to the top of the existing robots.txt fixture.
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

View File

@@ -0,0 +1,63 @@
{
"name": "fixtures/drupal-drupal-test-append",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": {
"prepend": "assets/prepend-to-robots.txt",
"append": "assets/append-to-robots.txt"
}
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1 @@
# File from assets that replaces file in web root.

View File

@@ -0,0 +1 @@
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-overwrite composer.json fixture.

View File

@@ -0,0 +1,81 @@
{
"name": "fixtures/drupal-drupal-test-overwrite",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*",
"fixtures/scaffold-override-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/scaffold-override-fixture"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/robots-default.txt",
"make-me.txt": {
"path": "assets/replacement.txt",
"overwrite": false
},
"keep-me.txt": {
"path": "assets/replacement.txt",
"overwrite": false
},
"replace-me.txt": {
"path": "assets/replacement.txt",
"overwrite": true
}
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1 @@
# File in drupal-drupal-test-overwrite that is not replaced by a scaffold file.

View File

@@ -0,0 +1 @@
# File in drupal-drupal-test-overwrite that is replaced by a scaffold file.

View File

@@ -0,0 +1 @@
# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal composer.json fixture.

View File

@@ -0,0 +1,76 @@
{
"name": "fixtures/drupal-drupal",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*",
"fixtures/scaffold-override-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/scaffold-override-fixture"
],
"gitignore": false,
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": {
"mode": "replace",
"path": "assets/robots-default.txt",
"overwrite": true
}
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
},
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true
}
}
}

View File

@@ -0,0 +1 @@
# default.services.yml fixture scaffolded from "file-mappings" in drupal-project composer.json fixture.

View File

@@ -0,0 +1,10 @@
{
"name": "fixtures/drupal-profile",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/.htaccess": false
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "fixtures/empty-file",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/empty_file.txt": "assets/empty_file.txt"
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "fixtures/empty-fixture-allowing-core",
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture"
],
"locations": {
"web-root": "./"
}
}
}
}

View File

@@ -0,0 +1,3 @@
{
"name": "fixtures/empty-fixture"
}

View File

@@ -0,0 +1,14 @@
{
"packages": {
"fixtures/drupal-drupal": {
"dev-master": {
"name": "fixtures/drupal-drupal",
"version": "1.0.0",
"dist": {
"url": "./drupal-drupal",
"type": "path"
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# This content is appended to the bottom of the existing robots.txt fixture.
# robots.txt fixture scaffolded from "file-mappings" in profile-with-append composer.json fixture.

View File

@@ -0,0 +1,12 @@
{
"name": "fixtures/profile-with-append",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": {
"append": "assets/append-to-robots.txt"
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
{
"name": "fixtures/project-allowing-empty-fixture",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"drupal-core-fixture": {
"type": "path",
"url": "../drupal-core-fixture",
"options": {
"symlink": true
}
},
"drupal-assets-fixture": {
"type": "path",
"url": "../drupal-assets-fixture",
"options": {
"symlink": true
}
},
"empty-fixture": {
"type": "path",
"url": "../empty-fixture",
"options": {
"symlink": true
}
},
"scaffold-override-fixture": {
"type": "path",
"url": "../scaffold-override-fixture",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/drupal-core-fixture": "*",
"fixtures/empty-fixture": "*",
"fixtures/scaffold-override-fixture": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/drupal-core-fixture",
"fixtures/empty-fixture",
"fixtures/scaffold-override-fixture"
],
"locations": {
"web-root": "./"
},
"gitignore": false,
"symlink": __SYMLINK__,
"file-mapping": {
"[web-root]/.htaccess": false
}
},
"installer-paths": {
"core": ["type:drupal-core"],
"modules/contrib/{$name}": ["type:drupal-module"],
"modules/custom/{$name}": ["type:drupal-custom-module"],
"profiles/contrib/{$name}": ["type:drupal-profile"],
"profiles/custom/{$name}": ["type:drupal-custom-profile"],
"themes/contrib/{$name}": ["type:drupal-theme"],
"themes/custom/{$name}": ["type:drupal-custom-theme"],
"libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"]
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "fixtures/project-with-empty-scaffold-path",
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./"
},
"file-mapping": {
"[web-root]/my-error": {
"path": ""
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "fixtures/project-with-illegal-dir-scaffold",
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./"
},
"file-mapping": {
"[web-root]/assets": {
"path": "assets"
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
{
"name": "fixtures/drupal-drupal",
"type": "project",
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"packagist.org": false,
"composer-scaffold": {
"type": "path",
"url": "__PROJECT_ROOT__",
"options": {
"symlink": true
}
},
"fixtures/composer-plugin-implements-scaffold-events": {
"type": "path",
"url": "../composer-plugin-implements-scaffold-events",
"options": {
"symlink": true
}
}
},
"require": {
"drupal/core-composer-scaffold": "*",
"fixtures/composer-plugin-implements-scaffold-events": "*"
},
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"fixtures/composer-plugin-implements-scaffold-events"
],
"locations": {
"web-root": "./"
},
"symlink": __SYMLINK__
}
},
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true,
"fixtures/composer-plugin-implements-scaffold-events": true
}
}
}

View File

@@ -0,0 +1,6 @@
<?php
/**
* @file
* A settings.php fixture file scaffolded from the scaffold-override-fixture.
*/

View File

@@ -0,0 +1,10 @@
{
"name": "fixtures/scaffold-override-fixture",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/sites/default/default.settings.php": "assets/override-settings.php"
}
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\VendorHardening;
use Composer\Package\RootPackageInterface;
use Drupal\Composer\Plugin\VendorHardening\Config;
use Drupal\Tests\Traits\PhpUnitWarnings;
use PHPUnit\Framework\TestCase;
/**
* @coversDefaultClass Drupal\Composer\Plugin\VendorHardening\Config
* @group VendorHardening
*/
class ConfigTest extends TestCase {
use PhpUnitWarnings;
/**
* @covers ::getPathsForPackage
*/
public function testGetPathsForPackageMixedCase(): void {
$config = $this->getMockBuilder(Config::class)
->onlyMethods(['getAllCleanupPaths'])
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('getAllCleanupPaths')
->willReturn(['package' => ['path']]);
$this->assertSame(['path'], $config->getPathsForPackage('pACKage'));
}
/**
* @covers ::getAllCleanupPaths
*/
public function testNoRootMergeConfig(): void {
// Root package has no extra field.
$root = $this->createMock(RootPackageInterface::class);
$root->expects($this->once())
->method('getExtra')
->willReturn([]);
$config = new Config($root);
$ref_default = new \ReflectionProperty($config, 'defaultConfig');
$ref_plugin_config = new \ReflectionMethod($config, 'getAllCleanupPaths');
$this->assertEquals(
$ref_default->getValue($config), $ref_plugin_config->invoke($config)
);
}
/**
* @covers ::getAllCleanupPaths
*/
public function testRootMergeConfig(): void {
// Root package has configuration in extra.
$root = $this->createMock(RootPackageInterface::class);
$root->expects($this->once())
->method('getExtra')
->willReturn([
'drupal-core-vendor-hardening' => [
'isa/string' => 'test_dir',
'an/array' => ['test_dir', 'doc_dir'],
],
]);
$config = new Config($root);
$ref_plugin_config = new \ReflectionMethod($config, 'getAllCleanupPaths');
$plugin_config = $ref_plugin_config->invoke($config);
$this->assertSame(['test_dir'], $plugin_config['isa/string']);
$this->assertSame(['test_dir', 'doc_dir'], $plugin_config['an/array']);
}
/**
* @covers ::getAllCleanupPaths
*
* @runInSeparateProcess
*/
public function testMixedCaseConfigCleanupPackages(): void {
// Root package has configuration in extra.
$root = $this->createMock(RootPackageInterface::class);
$root->expects($this->once())
->method('getExtra')
->willReturn([
'drupal-core-vendor-hardening' => [
'NotMikey179/vfsStream' => ['src/test'],
],
]);
$config = new Config($root);
$ref_plugin_config = new \ReflectionMethod($config, 'getAllCleanupPaths');
// Put some mixed-case in the defaults.
$ref_default = new \ReflectionProperty($config, 'defaultConfig');
$ref_default->setValue($config, [
'BeHatted/Mank' => ['tests'],
'SymFunic/HTTPFoundational' => ['src'],
]);
$plugin_config = $ref_plugin_config->invoke($config);
foreach (array_keys($plugin_config) as $package_name) {
$this->assertDoesNotMatchRegularExpression('/[A-Z]/', $package_name);
}
}
}

View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\VendorHardening;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Drupal\Composer\Plugin\VendorHardening\Config;
use Drupal\Composer\Plugin\VendorHardening\VendorHardeningPlugin;
use Drupal\Tests\Traits\PhpUnitWarnings;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @coversDefaultClass \Drupal\Composer\Plugin\VendorHardening\VendorHardeningPlugin
* @group VendorHardening
*/
class VendorHardeningPluginTest extends TestCase {
use PhpUnitWarnings;
use ProphecyTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
vfsStream::setup('vendor', NULL, [
'drupal' => [
'package' => [
'tests' => [
'SomeTest.php' => '<?php',
],
'SomeFile.php' => '<?php',
],
],
]);
}
/**
* @covers ::cleanPackage
*/
public function testCleanPackage(): void {
$config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('getPathsForPackage')
->willReturn(['tests']);
$plugin = $this->getMockBuilder(VendorHardeningPlugin::class)
->onlyMethods(['getInstallPathForPackage'])
->getMock();
$plugin->expects($this->once())
->method('getInstallPathForPackage')
->willReturn(vfsStream::url('vendor/drupal/package'));
$ref_config = new \ReflectionProperty($plugin, 'config');
$ref_config->setValue($plugin, $config);
$io = $this->prophesize(IOInterface::class);
$ref_io = new \ReflectionProperty($plugin, 'io');
$ref_io->setValue($plugin, $io->reveal());
$this->assertFileExists(vfsStream::url('vendor/drupal/package/tests/SomeTest.php'));
$package = $this->prophesize(PackageInterface::class);
$package->getName()->willReturn('drupal/package');
$plugin->cleanPackage($package->reveal());
$this->assertFileDoesNotExist(vfsStream::url('vendor/drupal/package/tests'));
}
/**
* @covers ::cleanPathsForPackage
*/
public function testCleanPathsForPackage(): void {
$plugin = $this->getMockBuilder(VendorHardeningPlugin::class)
->onlyMethods(['getInstallPathForPackage'])
->getMock();
$plugin->expects($this->once())
->method('getInstallPathForPackage')
->willReturn(vfsStream::url('vendor/drupal/package'));
$io = $this->prophesize(IOInterface::class);
$ref_io = new \ReflectionProperty($plugin, 'io');
$ref_io->setValue($plugin, $io->reveal());
$this->assertFileExists(vfsStream::url('vendor/drupal/package/tests/SomeTest.php'));
$this->assertFileExists(vfsStream::url('vendor/drupal/package/SomeFile.php'));
$package = $this->prophesize(PackageInterface::class);
$package->getName()->willReturn('drupal/package');
$ref_clean = new \ReflectionMethod($plugin, 'cleanPathsForPackage');
$ref_clean->invokeArgs($plugin, [$package->reveal(), ['tests', 'SomeFile.php']]);
$this->assertFileDoesNotExist(vfsStream::url('vendor/drupal/package/tests'));
$this->assertFileDoesNotExist(vfsStream::url('vendor/drupal/package/SomeFile.php'));
}
/**
* @covers ::cleanAllPackages
*/
public function testCleanAllPackages(): void {
$config = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
$config->expects($this->once())
->method('getAllCleanupPaths')
->willReturn(['drupal/package' => ['tests']]);
$package = $this->createMock(PackageInterface::class);
$package->expects($this->any())
->method('getName')
->willReturn('drupal/package');
$plugin = $this->getMockBuilder(VendorHardeningPlugin::class)
->onlyMethods(['getInstalledPackages', 'getInstallPathForPackage'])
->getMock();
$plugin->expects($this->once())
->method('getInstalledPackages')
->willReturn([$package]);
$plugin->expects($this->once())
->method('getInstallPathForPackage')
->willReturn(vfsStream::url('vendor/drupal/package'));
$io = $this->prophesize(IOInterface::class);
$ref_io = new \ReflectionProperty($plugin, 'io');
$ref_io->setValue($plugin, $io->reveal());
$ref_config = new \ReflectionProperty($plugin, 'config');
$ref_config->setValue($plugin, $config);
$this->assertFileExists(vfsStream::url('vendor/drupal/package/tests/SomeTest.php'));
$plugin->cleanAllPackages();
$this->assertFileDoesNotExist(vfsStream::url('vendor/drupal/package/tests'));
}
/**
* @covers ::writeAccessRestrictionFiles
*/
public function testWriteAccessRestrictionFiles(): void {
$dir = vfsStream::url('vendor');
// Set up mocks so that writeAccessRestrictionFiles() can eventually use
// the IOInterface object.
$composer = $this->getMockBuilder(Composer::class)
->onlyMethods(['getPackage'])
->getMock();
$composer->expects($this->once())
->method('getPackage')
->willReturn($this->prophesize(RootPackageInterface::class)->reveal());
$plugin = new VendorHardeningPlugin();
$plugin->activate($composer, $this->prophesize(IOInterface::class)->reveal());
$this->assertDirectoryExists($dir);
$this->assertFileDoesNotExist($dir . '/.htaccess');
$this->assertFileDoesNotExist($dir . '/web.config');
$plugin->writeAccessRestrictionFiles($dir);
$this->assertFileExists($dir . '/.htaccess');
$this->assertFileExists($dir . '/web.config');
}
public static function providerFindBinOverlap() {
return [
[
[],
['bin/script'],
['tests'],
],
[
['bin/composer' => 'bin/composer'],
['bin/composer'],
['bin', 'tests'],
],
[
['bin/composer' => 'bin/composer'],
['bin/composer'],
['bin/composer'],
],
[
[],
['bin/composer'],
['bin/something_else'],
],
[
[],
['test/script'],
['test/longer'],
],
[
['bin/very/long/path/script' => 'bin/very/long/path/script'],
['bin/very/long/path/script'],
['bin'],
],
[
['bin/bin/bin' => 'bin/bin/bin'],
['bin/bin/bin'],
['bin/bin'],
],
[
[],
['bin/bin'],
['bin/bin/bin'],
],
];
}
/**
* @covers ::findBinOverlap
* @dataProvider providerFindBinOverlap
*/
public function testFindBinOverlap($expected, $binaries, $clean_paths): void {
$plugin = $this->getMockBuilder(VendorHardeningPlugin::class)
->disableOriginalConstructor()
->getMock();
$ref_find_bin_overlap = new \ReflectionMethod($plugin, 'findBinOverlap');
$this->assertSame($expected, $ref_find_bin_overlap->invokeArgs($plugin, [$binaries, $clean_paths]));
}
}

View File

@@ -0,0 +1,17 @@
{
"_readme": [
"This file is a fixture used to test Drupal."
],
"content-hash": "da9910627bab73a256b39ceda83d7167",
"packages-dev": [
{
"name": "lullabot/mink-selenium2-driver",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/Lullabot/MinkSelenium2Driver.git",
"reference": "228004452354c1945fec2d273de2586da8c29213"
}
}
]
}