ComposerProjectTemplatesTest.php

Same filename in other branches
  1. 9 core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
  2. 10 core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
  3. 11.x core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php

Namespace

Drupal\BuildTests\Composer\Template

File

core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php

View source
<?php

namespace Drupal\BuildTests\Composer\Template;

use Composer\Json\JsonFile;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Composer\Composer;
use Symfony\Component\Finder\Finder;

/**
 * Demonstrate that Composer project templates are buildable as patched.
 *
 * We have to use the packages.json fixture so that Composer will use the
 * in-codebase version of the project template.
 *
 * We also have to add path repositories to the in-codebase project template or
 * else Composer will try to use packagist to resolve dependencies we'd prefer
 * it to find locally.
 *
 * This is because Composer only uses the packages.json file to resolve the
 * project template and not any other dependencies.
 *
 * @group #slow
 * @group Template
 *
 * @requires externalCommand composer
 */
class ComposerProjectTemplatesTest extends BuildTestBase {
    
    /**
     * Get Composer items that we want to be path repos, from within a directory.
     *
     * @param string $workspace_directory
     *   The full path to the workspace directory.
     * @param string $subdir
     *   The subdirectory to search under composer/.
     *
     * @return string[]
     *   Array of paths, indexed by package name.
     */
    public function getPathReposForType($workspace_directory, $subdir) {
        // Find the Composer items that we want to be path repos.
        $path_repos = Finder::create()->files()
            ->name('composer.json')
            ->in($workspace_directory . '/composer/' . $subdir);
        $data = [];
        
        /* @var $path_repo \SplFileInfo */
        foreach ($path_repos as $path_repo) {
            $json_file = new JsonFile($path_repo->getPathname());
            $json = $json_file->read();
            $data[$json['name']] = $path_repo->getPath();
        }
        return $data;
    }
    public function provideTemplateCreateProject() {
        return [
            'recommended-project' => [
                'drupal/recommended-project',
                'composer/Template/RecommendedProject',
                '/web',
            ],
            'legacy-project' => [
                'drupal/legacy-project',
                'composer/Template/LegacyProject',
                '',
            ],
        ];
    }
    
    /**
     * Make sure we've accounted for all the templates.
     */
    public function testVerifyTemplateTestProviderIsAccurate() {
        $root = $this->getDrupalRoot();
        $data = $this->provideTemplateCreateProject($root);
        // Find all the templates.
        $template_files = Finder::create()->files()
            ->name('composer.json')
            ->in($root . '/composer/Template');
        $this->assertEquals(count($template_files), count($data));
        // We could have the same number of templates but different names.
        $template_data = [];
        foreach ($data as $data_name => $data_value) {
            $template_data[$data_value[0]] = $data_name;
        }
        
        /* @var $file \SplFileInfo */
        foreach ($template_files as $file) {
            $json_file = new JsonFile($file->getPathname());
            $json = $json_file->read();
            $this->assertArrayHasKey('name', $json);
            // Does provideTemplateCreateProject() give us this template name?
            $this->assertArrayHasKey($json['name'], $template_data);
        }
    }
    
    /**
     * @dataProvider provideTemplateCreateProject
     */
    public function testTemplateCreateProject($project, $package_dir, $docroot_dir) {
        $composer_version_line = exec('composer --version');
        if (strpos($composer_version_line, 'Composer version 2') !== FALSE) {
            // @todo Remove in https://www.drupal.org/project/drupal/issues/3128631
            $this->markTestSkipped("Composer 2 not supported for this test yet.");
        }
        // Make a working COMPOSER_HOME directory for setting global composer config
        $composer_home = $this->getWorkspaceDirectory() . '/composer-home';
        mkdir($composer_home);
        // Disable packagist globally (but only in our own custom COMPOSER_HOME).
        // It is necessary to do this globally rather than in our SUT composer.json
        // in order to ensure that Packagist is disabled during the
        // `composer create-project` command.
        $this->executeCommand("COMPOSER_HOME={$composer_home} composer config --no-interaction --global repo.packagist false");
        $this->assertCommandSuccessful();
        // Get the Drupal core version branch. For instance, this should be
        // 8.9.x-dev for the 8.9.x branch.
        $core_version = Composer::drupalVersionBranch();
        // Create a "Composer"-type repository containing one entry for every
        // package in the vendor directory.
        $vendor_packages_path = $this->getWorkspaceDirectory() . '/vendor_packages/packages.json';
        $this->makeVendorPackage($vendor_packages_path);
        // Make a copy of the code to alter.
        $this->copyCodebase();
        // Remove the packages.drupal.org entry (and any other custom repository)
        // from the SUT's repositories section. There is no way to do this via
        // `composer config --unset`, so we read and rewrite composer.json.
        $composer_json_path = $this->getWorkspaceDirectory() . "/{$package_dir}/composer.json";
        $composer_json = json_decode(file_get_contents($composer_json_path), TRUE);
        unset($composer_json['repositories']);
        $json = json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        file_put_contents($composer_json_path, $json);
        // Set up the template to use our path repos. Inclusion of metapackages is
        // reported differently, so we load up a separate set for them.
        $metapackage_path_repos = $this->getPathReposForType($this->getWorkspaceDirectory(), 'Metapackage');
        $path_repos = array_merge($metapackage_path_repos, $this->getPathReposForType($this->getWorkspaceDirectory(), 'Plugin'));
        // Always add drupal/core as a path repo.
        $path_repos['drupal/core'] = $this->getWorkspaceDirectory() . '/core';
        foreach ($path_repos as $name => $path) {
            $this->executeCommand("composer config --no-interaction repositories.{$name} path {$path}", $package_dir);
            $this->assertCommandSuccessful();
        }
        $this->executeCommand("composer config --no-interaction repositories.local composer file://" . $vendor_packages_path, $package_dir);
        $this->assertCommandSuccessful();
        $repository_path = $this->getWorkspaceDirectory() . '/test_repository/packages.json';
        $this->makeTestPackage($repository_path, $core_version);
        $autoloader = $this->getWorkspaceDirectory() . '/testproject' . $docroot_dir . '/autoload.php';
        $this->assertFileNotExists($autoloader);
        $this->executeCommand("COMPOSER_HOME={$composer_home} COMPOSER_ROOT_VERSION={$core_version} composer create-project --no-ansi {$project} testproject {$core_version} -s dev -vv --repository {$repository_path}");
        $this->assertCommandSuccessful();
        // Ensure we used the project from our codebase.
        $this->assertErrorOutputContains("Installing {$project} ({$core_version}): Symlinking from {$package_dir}");
        // Ensure that we used drupal/core from our codebase. This probably means
        // that drupal/core-recommended was added successfully by the project.
        $this->assertErrorOutputContains("Installing drupal/core ({$core_version}): Symlinking from");
        // Verify that there is an autoloader. This is written by the scaffold
        // plugin, so its existence assures us that scaffolding happened.
        $this->assertFileExists($autoloader);
        // In order to verify that Composer used the path repos for our project, we
        // have to get the requirements from the project composer.json so we can
        // reconcile our expectations.
        $template_json_file = $this->getWorkspaceDirectory() . '/' . $package_dir . '/composer.json';
        $this->assertFileExists($template_json_file);
        $json_file = new JsonFile($template_json_file);
        $template_json = $json_file->read();
        // Get the require and require-dev information, and ensure that our
        // requirements are not erroneously empty.
        $this->assertNotEmpty($require = array_merge($template_json['require'] ?? [], $template_json['require-dev'] ?? []));
        // Verify that path repo packages were installed.
        $path_repos = array_keys($path_repos);
        foreach (array_keys($require) as $package_name) {
            if (in_array($package_name, $path_repos)) {
                // Metapackages do not report that they were installed as symlinks, but
                // we still must check that their installed version matches
                // COMPOSER_CORE_VERSION.
                if (array_key_exists($package_name, $metapackage_path_repos)) {
                    $this->assertErrorOutputContains("Installing {$package_name} ({$core_version})");
                }
                else {
                    $this->assertErrorOutputContains("Installing {$package_name} ({$core_version}): Symlinking from");
                }
            }
        }
    }
    
    /**
     * Creates a test package that points to the templates.
     *
     * @param string $repository_path
     *   The path where to create the test package.
     * @param string $version
     *   The version under test.
     */
    protected function makeTestPackage($repository_path, $version) {
        $json = <<<JSON
{
  "packages": {
    "drupal/recommended-project": {
      "{<span class="php-variable">$version</span>}": {
        "name": "drupal/recommended-project",
        "dist": {
          "type": "path",
          "url": "composer/Template/RecommendedProject"
        },
        "type": "project",
        "version": "{<span class="php-variable">$version</span>}"
      }
    },
    "drupal/legacy-project": {
      "{<span class="php-variable">$version</span>}": {
        "name": "drupal/legacy-project",
        "dist": {
          "type": "path",
          "url": "composer/Template/LegacyProject"
        },
        "type": "project",
        "version": "{<span class="php-variable">$version</span>}"
      }
    }
  }
}
JSON;
        mkdir(dirname($repository_path));
        file_put_contents($repository_path, $json);
    }
    
    /**
     * Creates a test package that points to all the projects in vendor.
     *
     * @param string $repository_path
     *   The path where to create the test package.
     */
    protected function makeVendorPackage($repository_path) {
        $root = $this->getDrupalRoot();
        $process = $this->executeCommand("composer --working-dir={$root} info --format=json");
        $this->assertCommandSuccessful();
        $installed = json_decode($process->getOutput(), TRUE);
        // Build out package definitions for everything installed in
        // the vendor directory.
        $packages = [];
        foreach ($installed['installed'] as $project) {
            $name = $project['name'];
            $version = $project['version'];
            $path = "vendor/{$name}";
            $full_path = "{$root}/{$path}";
            // We are building a set of path repositories to projects in the vendor
            // directory, so we will skip any project that does not exist in vendor.
            if (is_dir($full_path)) {
                $packages['packages'][$name] = [
                    $version => [
                        "name" => $name,
                        "dist" => [
                            "type" => "path",
                            "url" => $path,
                        ],
                        "version" => $version,
                    ],
                ];
            }
        }
        $json = json_encode($packages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        mkdir(dirname($repository_path));
        file_put_contents($repository_path, $json);
    }

}

Classes

Title Deprecated Summary
ComposerProjectTemplatesTest Demonstrate that Composer project templates are buildable as patched.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.