class UnpackRecipeTest

Tests recipe unpacking.

@group Unpack

Hierarchy

Expanded class hierarchy of UnpackRecipeTest

File

core/tests/Drupal/Tests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php, line 18

Namespace

Drupal\Tests\Composer\Plugin\Unpack\Functional
View source
class UnpackRecipeTest extends BuildTestBase {
  use ExecTrait;
  
  /**
   * Directory to perform the tests in.
   */
  protected string $fixturesDir;
  
  /**
   * The Symfony FileSystem component.
   *
   * @var \Composer\Util\Filesystem
   */
  protected Filesystem $fileSystem;
  
  /**
   * The Fixtures object.
   *
   * @var \Drupal\Tests\Composer\Plugin\Unpack\Fixtures
   */
  protected Fixtures $fixtures;
  
  /**
   * {@inheritdoc}
   */
  protected function setUp() : void {
    parent::setUp();
    $this->fileSystem = new Filesystem();
    $this->fixtures = new Fixtures();
    $this->fixtures
      ->createIsolatedComposerCacheDir();
    $this->fixturesDir = $this->fixtures
      ->tmpDir($this->name());
    $replacements = [
      'PROJECT_ROOT' => $this->fixtures
        ->projectRoot(),
      'COMPOSER_INSTALLERS' => InstalledVersions::getInstallPath('composer/installers'),
    ];
    $this->fixtures
      ->cloneFixtureProjects($this->fixturesDir, $replacements);
  }
  
  /**
   * {@inheritdoc}
   */
  protected function tearDown() : void {
    // Remove any temporary directories that were created.
    $this->fixtures
      ->tearDown();
    parent::tearDown();
  }
  
  /**
   * Tests the dependencies unpack on install.
   */
  public function testAutomaticUnpack() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    copy($root_project_path . '/composer.json', $root_project_path . '/composer.json.original');
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('install');
    // Install a module in require-dev that should be moved to require
    // by the unpacker.
    $this->runComposer('require --dev fixtures/module-a:^1.0');
    // Ensure we have added the dependency to require-dev.
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
    // Install a recipe and unpack it.
    $stdout = $this->runComposer('require fixtures/recipe-a');
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // The more specific constraint should have been used.
    $this->assertSame("^1.0", $root_composer_json['require']['fixtures/module-a']);
    // Copy old composer.json back over and require recipe again to ensure it
    // is still unpacked. This tests that unpacking does not rely on composer
    // package events.
    unlink($root_project_path . '/composer.json');
    copy($root_project_path . '/composer.json.original', $root_project_path . '/composer.json');
    $stdout = $this->runComposer('require fixtures/recipe-a');
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
  }
  
  /**
   * Tests recursive unpacking.
   */
  public function testRecursiveUnpacking() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('config --merge --json sort-packages true');
    $this->runComposer('install');
    $stdOut = $this->runComposer('require fixtures/recipe-c fixtures/recipe-a');
    $this->assertSame("fixtures/recipe-c unpacked.\nfixtures/recipe-a unpacked.\nfixtures/recipe-b unpacked.\n", $stdOut);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/module-a',
      'fixtures/module-b',
      'fixtures/theme-a',
    ], array_keys($root_composer_json['require']));
    // Ensure the resulting composer files are valid.
    $this->runComposer('validate');
    // Ensure the recipes exist.
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
    $this->assertFileExists($root_project_path . '/recipes/recipe-c/recipe.yml');
    // Ensure the complex constraint has been written correctly.
    $this->assertSame('>=2.0.1.0-dev, <3.0.0.0-dev', $root_composer_json['require']['fixtures/module-b']);
    // Ensure composer.lock is ordered correctly.
    $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/module-a',
      'fixtures/module-b',
      'fixtures/theme-a',
    ], array_column($root_composer_lock['packages'], 'name'));
  }
  
  /**
   * Tests the dev dependencies do not unpack on install.
   */
  public function testNoAutomaticDevUnpack() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('install');
    // Install a module in require.
    $this->runComposer('require fixtures/module-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
    // Install a recipe as a dev dependency.
    $stdout = $this->runComposer('require --dev fixtures/recipe-a');
    $this->assertStringContainsString("Recipes required as a development dependency are not automatically unpacked.", $stdout);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // Assert the state of the root composer.json as no unpacking has occurred.
    $this->assertSame([
      'fixtures/recipe-a',
    ], array_keys($root_composer_json['require-dev']));
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/module-a',
    ], array_keys($root_composer_json['require']));
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests dependency unpacking using drupal:recipe-unpack.
   */
  public function testUnpackCommand() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('install');
    // Disable automatic unpacking as it is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    // Install a module in require-dev.
    $this->runComposer('require --dev fixtures/module-a');
    // Install a module in require.
    $this->runComposer('require fixtures/module-b:*');
    // Ensure we have added the dependencies.
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
    // Install a recipe and check it is not unpacked.
    $stdout = $this->runComposer('require fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // When the package is unpacked, the unpacked dependencies should be logged
    // in the stdout.
    $this->assertStringNotContainsString("unpacked.", $stdout);
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
    // The package dependencies should not be in the root composer.json.
    $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
    // Try unpacking a recipe that in not in the root composer.json.
    try {
      $this->runComposer('drupal:recipe-unpack fixtures/recipe-b');
      $this->fail('Unpacking a non-existent dependency should fail');
    } catch (\RuntimeException $e) {
      $this->assertStringContainsString('fixtures/recipe-b not found in the root composer.json.', $e->getMessage());
    }
    // The dev dependency has not moved.
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
    $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // The more specific constraints has been used.
    $this->assertSame("^2.0", $root_composer_json['require']['fixtures/module-b']);
    // Try unpacking something that is not a recipe.
    try {
      $this->runComposer('drupal:recipe-unpack fixtures/module-a');
      $this->fail('Unpacking a module should fail');
    } catch (\RuntimeException $e) {
      $this->assertStringContainsString('fixtures/module-a is not a recipe.', $e->getMessage());
    }
    // Try unpacking something that in not in the root composer.json.
    try {
      $this->runComposer('drupal:recipe-unpack fixtures/module-c');
      $this->fail('Unpacking a non-existent dependency should fail');
    } catch (\RuntimeException $e) {
      $this->assertStringContainsString('fixtures/module-c not found in the root composer.json.', $e->getMessage());
    }
  }
  
  /**
   * Tests dependency unpacking using drupal:recipe-unpack with multiple args.
   */
  public function testUnpackCommandWithMultipleRecipes() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    $this->runComposer('install');
    // Disable automatic unpacking as it is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    // Install a recipe and check it is not unpacked.
    $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // When the package is unpacked, the unpacked dependencies should be logged
    // in the stdout.
    $this->assertStringNotContainsString("unpacked.", $stdOut);
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
    $stdOut = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a fixtures/recipe-d');
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
    $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests dependency unpacking using drupal:recipe-unpack with no arguments.
   */
  public function testUnpackCommandWithoutRecipesArgument() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    $this->runComposer('install');
    // Tests unpack command with no arguments and no recipes in the root
    // composer package.
    $stdOut = $this->runComposer('drupal:recipe-unpack');
    $this->assertSame("No recipes to unpack.\n", $stdOut);
    // Disable automatic unpacking as it is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    // Install a recipe and check it is not unpacked.
    $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // When the package is unpacked, the unpacked dependencies should be logged
    // in the stdout.
    $this->assertStringNotContainsString("unpacked.", $stdOut);
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
    $stdOut = $this->runComposer('drupal:recipe-unpack');
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
    $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests unpacking a recipe in require-dev using drupal:recipe-unpack.
   */
  public function testUnpackCommandOnDevRecipe() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('install');
    // Disable automatic unpacking, which is the default behavior.
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    $this->runComposer('require fixtures/recipe-b');
    // Install a recipe and check it is not unpacked.
    $this->runComposer('require --dev fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require-dev']);
    $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require']);
    $error_output = '';
    $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a', error_output: $error_output);
    $this->assertStringContainsString("fixtures/recipe-a is present in the require-dev key. Unpacking will move the recipe's dependencies to the require key.", $error_output);
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // Ensure recipe A's dependencies are moved to require.
    $this->doTestRecipeAUnpacked($root_project_path, $stdout);
    // Ensure recipe B's dependencies are in require and the recipe has been
    // unpacked.
    $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
    // Ensure installed.json and installed.php are correct.
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
    $installed_packages = array_column($installed_json['packages'], 'name');
    $this->assertContains('fixtures/module-b', $installed_packages);
    $this->assertNotContains('fixtures/recipe-a', $installed_packages);
    $this->assertSame([], $installed_json['dev-package-names']);
    $installed_php = (include_once $root_project_path . '/vendor/composer/installed.php');
    $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
    $this->assertFalse($installed_php['versions']['fixtures/module-b']['dev_requirement']);
    $this->assertArrayNotHasKey('fixtures/recipe-a', $installed_php['versions']);
  }
  
  /**
   * Tests the unpacking a recipe that is an indirect dev dependency.
   */
  public function testUnpackCommandOnIndirectDevDependencyRecipe() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Run composer install and confirm the composer.lock was created.
    $this->runComposer('install');
    // Disable automatic unpacking as it is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    $this->runComposer('require --dev fixtures/recipe-b');
    // Install a recipe and ensure it is not unpacked.
    $this->runComposer('require fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require-dev']);
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
    $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // Ensure recipe A's dependencies are in require.
    $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
    $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
    // Ensure recipe B is still in require-dev even though all it's dependencies
    // have been unpacked to require due to unpacking recipe A.
    $this->assertSame([
      'fixtures/recipe-b',
    ], array_keys($root_composer_json['require-dev']));
    // Ensure recipe B is still list in installed.json.
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
    $installed_packages = array_column($installed_json['packages'], 'name');
    $this->assertContains('fixtures/recipe-b', $installed_packages);
    $this->assertContains('fixtures/recipe-b', $installed_json['dev-package-names']);
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests a recipe can be removed and the unpack plugin does not interfere.
   */
  public function testRemoveRecipe() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Disable automatic unpacking, which is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
    $this->runComposer('install');
    // Install a recipe and ensure it is not unpacked.
    $this->runComposer('require fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/recipe-a',
    ], array_keys($root_composer_json['require']));
    // Removing the recipe should work as normal.
    $this->runComposer('remove fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
    ], array_keys($root_composer_json['require']));
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests a recipe can be ignored and not unpacked.
   */
  public function testIgnoreRecipe() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Disable automatic unpacking as it is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-a"]\'');
    $this->runComposer('install');
    // Install a recipe and ensure it does not get unpacked.
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame("fixtures/recipe-a not unpacked because it is ignored.", trim($stdOut));
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/recipe-a',
    ], array_keys($root_composer_json['require']));
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
    // Try using the unpack command on an ignored recipe.
    try {
      $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
      $this->fail('Ignored recipes should not be unpacked.');
    } catch (\RuntimeException $e) {
      $this->assertStringContainsString('fixtures/recipe-a is in the extra.drupal-recipe-unpack.ignore list.', $e->getMessage());
    }
  }
  
  /**
   * Tests a dependent recipe can be ignored and not unpacked.
   */
  public function testIgnoreDependentRecipe() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    // Disable automatic unpacking, which is the default behavior,
    $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-b"]\'');
    $this->runComposer('config sort-packages true');
    $this->runComposer('install');
    // Install a recipe and check it is not packed but not removed.
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertStringContainsString("fixtures/recipe-b not unpacked because it is ignored.", $stdOut);
    $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/module-b',
      'fixtures/recipe-b',
    ], array_keys($root_composer_json['require']));
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests that recipes stick around after being unpacked.
   */
  public function testRecipeIsPhysicallyPresentAfterUnpack() : void {
    $root_project_dir = 'composer-root';
    $root_project_path = $this->fixturesDir . '/' . $root_project_dir;
    $this->runComposer('install');
    // Install a recipe, which should unpack it.
    $stdOut = $this->runComposer('require --verbose fixtures/recipe-b');
    $this->assertStringContainsString("fixtures/recipe-b unpacked.", $stdOut);
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
    // Require another dependency.
    $this->runComposer('require --verbose fixtures/module-b');
    // The recipe should still be physically installed...
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
    // ...but it should NOT be in installed.json or installed.php.
    $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
    $installed_packages = array_column($installed_json['packages'], 'name');
    $this->assertContains('fixtures/module-b', $installed_packages);
    $this->assertNotContains('fixtures/recipe-b', $installed_packages);
    $installed_php = (include_once $root_project_path . '/vendor/composer/installed.php');
    $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
    $this->assertArrayNotHasKey('fixtures/recipe-b', $installed_php['versions']);
  }
  
  /**
   * Tests a recipe can be required using --no-install and installed later.
   */
  public function testRecipeNotUnpackedIfInstallIsDeferred() : void {
    $root_project_path = $this->fixturesDir . '/composer-root';
    $this->runComposer('install');
    // Install a recipe and check it is in `composer.json` but not unpacked or
    // physically installed.
    $stdOut = $this->runComposer('require --verbose --no-install fixtures/recipe-a');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame("Recipes are not unpacked when the --no-install option is used.", trim($stdOut));
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/recipe-a',
    ], array_keys($root_composer_json['require']));
    $this->assertFileDoesNotExist($root_project_path . '/recipes/recipe-a/recipe.yml');
    // After installing dependencies, the recipe should be installed, but still
    // not unpacked.
    $this->runComposer('install');
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/recipe-a',
    ], array_keys($root_composer_json['require']));
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate');
  }
  
  /**
   * Tests that recipes are unpacked when using `composer create-project`.
   */
  public function testComposerCreateProject() : void {
    // Prepare the project to use for create-project.
    $root_project_path = $this->fixturesDir . '/composer-root';
    $this->runComposer('require --verbose --no-install fixtures/recipe-a');
    $stdOut = $this->runComposer('create-project --repository=\'{"type": "path","url": "' . $root_project_path . '","options": {"symlink": false}}\' fixtures/root composer-root2 -s dev', $this->fixturesDir);
    // The recipes depended upon by the project, even indirectly, should all
    // have been unpacked.
    $this->assertSame("fixtures/recipe-b unpacked.\nfixtures/recipe-a unpacked.\n", $stdOut);
    $this->doTestRecipeAUnpacked($this->fixturesDir . '/composer-root2', $stdOut);
  }
  
  /**
   * Tests Recipe A is unpacked correctly.
   *
   * @param string $root_project_path
   *   Path to the composer project under test.
   * @param string $stdout
   *   The standard out from the composer command unpacks the recipe.
   */
  private function doTestRecipeAUnpacked(string $root_project_path, string $stdout) : void {
    $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
    // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json
    // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json
    $expected_unpacked = [
      'fixtures/recipe-a' => [
        'fixtures/module-b',
      ],
      'fixtures/recipe-b' => [
        'fixtures/module-a',
        'fixtures/theme-a',
      ],
    ];
    foreach ($expected_unpacked as $package => $dependencies) {
      // When the package is unpacked, the unpacked dependencies should be logged
      // in the stdout.
      $this->assertStringContainsString("{$package} unpacked.", $stdout);
      // After being unpacked, the package should be removed from the root
      // composer.json and composer.lock.
      $this->assertArrayNotHasKey($package, $root_composer_json['require']);
      foreach ($dependencies as $dependency) {
        // The package dependencies should be in the root composer.json.
        $this->assertArrayHasKey($dependency, $root_composer_json['require']);
      }
    }
    // Ensure the resulting Composer files are valid.
    $this->runComposer('validate', $root_project_path);
    // The dev dependency has moved.
    $this->assertArrayNotHasKey('require-dev', $root_composer_json);
    // Ensure recipe files exist.
    $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
    $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
    // Ensure composer.lock is ordered correctly.
    $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
    $this->assertSame([
      'composer/installers',
      'drupal/core-recipe-unpack',
      'fixtures/module-a',
      'fixtures/module-b',
      'fixtures/theme-a',
    ], array_column($root_composer_lock['packages'], 'name'));
  }
  
  /**
   * Executes a Composer command with standard options.
   *
   * @param string $command
   *   The composer command to execute.
   * @param string $cwd
   *   The current working directory to run the command from.
   * @param string $error_output
   *   Passed by reference to allow error output to be tested.
   *
   * @return string
   *   Standard output from the command.
   */
  private function runComposer(string $command, ?string $cwd = NULL, string &$error_output = '') : string {
    $cwd ??= $this->fixturesDir . '/composer-root';
    // Always add --no-interaction and --no-ansi to Composer commands.
    $output = $this->mustExec("composer {$command} --no-interaction --no-ansi", $cwd, [], $error_output);
    if ($command === 'install') {
      $this->assertFileExists($cwd . '/composer.lock');
    }
    return $output;
  }
  
  /**
   * Gets the contents of a file as an array.
   *
   * @param string $path
   *   The path to the file.
   *
   * @return array
   *   The contents of the file as an array.
   */
  private function getFileContents(string $path) : array {
    $file = file_get_contents($path);
    return json_decode($file, TRUE, flags: JSON_THROW_ON_ERROR);
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
BuildTestBase::$commandProcess private property The most recent command process.
BuildTestBase::$destroyBuild protected property Default to destroying build artifacts after a test finishes.
BuildTestBase::$hostName private static property Our native host name, used by PHP when it starts up the server.
BuildTestBase::$hostPort private property Port that will be tested.
BuildTestBase::$mink private property The Mink session manager.
BuildTestBase::$phpFinder private property The PHP executable finder.
BuildTestBase::$portLocks private property A list of ports used by the test.
BuildTestBase::$serverDocroot private property The docroot for the server process.
BuildTestBase::$serverProcess private property The process that&#039;s running the HTTP server.
BuildTestBase::$workspaceDir private property The working directory where this test will manipulate files.
BuildTestBase::assertCommandExitCode public function Asserts that the last command returned the specified exit code.
BuildTestBase::assertCommandOutputContains public function Assert that text is present in the output of the most recent command.
BuildTestBase::assertCommandSuccessful public function Asserts that the last command ran without error.
BuildTestBase::assertDrupalVisit public function Helper function to assert that the last visit was a Drupal site.
BuildTestBase::assertErrorOutputContains public function Assert that text is present in the error output of the most recent command.
BuildTestBase::assertErrorOutputNotContains public function Assert text is not present in the error output of the most recent command.
BuildTestBase::checkPortIsAvailable protected function Checks whether a port is available.
BuildTestBase::copyCodebase public function Copy the current working codebase into a workspace. 1
BuildTestBase::executeCommand public function Run a command.
BuildTestBase::findAvailablePort protected function Discover an available port number.
BuildTestBase::getCodebaseFinder public function Get a default Finder object for a Drupal codebase.
BuildTestBase::getComposerRoot public function Gets the path to the Composer root directory.
BuildTestBase::getDrupalRoot public function Get the root path of this Drupal codebase.
BuildTestBase::getDrupalRootStatic public static function Get the root path of this Drupal codebase.
BuildTestBase::getMink public function Get the Mink instance.
BuildTestBase::getPortNumber protected function Get the port number for requests.
BuildTestBase::getWorkingPath protected function Get the working directory within the workspace, creating if necessary.
BuildTestBase::getWorkingPathDrupalRoot public function Gets the working path for Drupal core.
BuildTestBase::getWorkspaceDirectory public function Full path to the workspace where this test can build.
BuildTestBase::getWorkspaceDrupalRoot public function Gets the path to Drupal root in the workspace directory.
BuildTestBase::initMink protected function Set up the Mink session manager.
BuildTestBase::instantiateServer protected function Do the work of making a server process. 1
BuildTestBase::standUpServer protected function Makes a local test server using PHP&#039;s internal HTTP server.
BuildTestBase::stopServer protected function Stop the HTTP server, zero out all necessary variables.
BuildTestBase::visit public function Visit a URI on the HTTP server. 1
ExecTrait::mustExec protected function Runs an arbitrary command.
RequiresComposerTrait::requiresComposer public static function
UnpackRecipeTest::$fileSystem protected property The Symfony FileSystem component.
UnpackRecipeTest::$fixtures protected property The Fixtures object.
UnpackRecipeTest::$fixturesDir protected property Directory to perform the tests in.
UnpackRecipeTest::doTestRecipeAUnpacked private function Tests Recipe A is unpacked correctly.
UnpackRecipeTest::getFileContents private function Gets the contents of a file as an array.
UnpackRecipeTest::runComposer private function Executes a Composer command with standard options.
UnpackRecipeTest::setUp protected function Overrides BuildTestBase::setUp
UnpackRecipeTest::tearDown protected function Overrides BuildTestBase::tearDown
UnpackRecipeTest::testAutomaticUnpack public function Tests the dependencies unpack on install.
UnpackRecipeTest::testComposerCreateProject public function Tests that recipes are unpacked when using `composer create-project`.
UnpackRecipeTest::testIgnoreDependentRecipe public function Tests a dependent recipe can be ignored and not unpacked.
UnpackRecipeTest::testIgnoreRecipe public function Tests a recipe can be ignored and not unpacked.
UnpackRecipeTest::testNoAutomaticDevUnpack public function Tests the dev dependencies do not unpack on install.
UnpackRecipeTest::testRecipeIsPhysicallyPresentAfterUnpack public function Tests that recipes stick around after being unpacked.
UnpackRecipeTest::testRecipeNotUnpackedIfInstallIsDeferred public function Tests a recipe can be required using --no-install and installed later.
UnpackRecipeTest::testRecursiveUnpacking public function Tests recursive unpacking.
UnpackRecipeTest::testRemoveRecipe public function Tests a recipe can be removed and the unpack plugin does not interfere.
UnpackRecipeTest::testUnpackCommand public function Tests dependency unpacking using drupal:recipe-unpack.
UnpackRecipeTest::testUnpackCommandOnDevRecipe public function Tests unpacking a recipe in require-dev using drupal:recipe-unpack.
UnpackRecipeTest::testUnpackCommandOnIndirectDevDependencyRecipe public function Tests the unpacking a recipe that is an indirect dev dependency.
UnpackRecipeTest::testUnpackCommandWithMultipleRecipes public function Tests dependency unpacking using drupal:recipe-unpack with multiple args.
UnpackRecipeTest::testUnpackCommandWithoutRecipesArgument public function Tests dependency unpacking using drupal:recipe-unpack with no arguments.

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