diff --git a/src/Capability/BaseResolverProvider.php b/src/Capability/BaseResolverProvider.php new file mode 100644 index 00000000..cf8c539e --- /dev/null +++ b/src/Capability/BaseResolverProvider.php @@ -0,0 +1,46 @@ +composer = $args['composer']; + $this->io = $args['io']; + $this->plugin = $args['plugin']; + } + + /** + * {@inheritDoc} + */ + abstract public function getResolvers(); +} diff --git a/src/Capability/CoreResolverProvider.php b/src/Capability/CoreResolverProvider.php new file mode 100644 index 00000000..c6ce3aa2 --- /dev/null +++ b/src/Capability/CoreResolverProvider.php @@ -0,0 +1,22 @@ +composer, $this->io), + new PatchesFile($this->composer, $this->io), + new DependencyPatches($this->composer, $this->io), + ]; + } +} diff --git a/src/Capability/ResolverProvider.php b/src/Capability/ResolverProvider.php new file mode 100644 index 00000000..189e24be --- /dev/null +++ b/src/Capability/ResolverProvider.php @@ -0,0 +1,22 @@ +executor = new ProcessExecutor($this->io); $this->patches = array(); $this->installedPatches = array(); + $this->patchesResolved = false; + $this->patchCollection = new PatchCollection(); $this->configuration = [ 'exit-on-patch-failure' => [ @@ -84,9 +102,9 @@ public function activate(Composer $composer, IOInterface $io) 'type' => 'bool', 'default' => false, ], - 'disable-patching-from-dependencies' => [ - 'type' => 'bool', - 'default' => false, + 'disable-resolvers' => [ + 'type' => 'list', + 'default' => [], ], 'disable-patch-reports' => [ 'type' => 'bool', @@ -110,10 +128,10 @@ public function activate(Composer $composer, IOInterface $io) public static function getSubscribedEvents() { return array( - ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'), - ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'), - PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'), - PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'), +// ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'), +// ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'), + PackageEvents::PRE_PACKAGE_INSTALL => array('resolvePatches'), + PackageEvents::PRE_PACKAGE_UPDATE => array('resolvePatches'), // The following is a higher weight for compatibility with // https://github.com/AydinHassan/magento-core-composer-installer and // more generally for compatibility with any Composer plugin which @@ -126,6 +144,87 @@ public static function getSubscribedEvents() ); } + /** + * Return a list of plugin capabilities. + * + * @return array + */ + public function getCapabilities() + { + return [ + 'cweagans\Composer\Capability\ResolverProvider' => 'cweagans\Composer\Capability\CoreResolverProvider', + ]; + } + + /** + * Gather a list of all patch resolvers from all enabled Composer plugins. + * + * @return ResolverBase[] + * A list of PatchResolvers to be run. + */ + public function getPatchResolvers() + { + $resolvers = []; + $plugin_manager = $this->composer->getPluginManager(); + foreach ($plugin_manager->getPluginCapabilities( + 'cweagans\Composer\Capability\ResolverProvider', + ['composer' => $this->composer, 'io' => $this->io] + ) as $capability) { + /** @var ResolverProvider $capability */ + $newResolvers = $capability->getResolvers(); + if (!is_array($newResolvers)) { + throw new \UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' failed to return an array from getResolvers().' + ); + } + foreach ($newResolvers as $resolver) { + if (!$resolver instanceof ResolverBase) { + throw new \UnexpectedValueException( + 'Plugin capability ' . get_class($capability) . ' returned an invalid value.' + ); + } + } + $resolvers = array_merge($resolvers, $newResolvers); + } + + return $resolvers; + } + + /** + * Gather patches that need to be applied to the current set of packages. + * + * Note that this work is done unconditionally if this plugin is enabled, + * even if patching is disabled in any way. The point where patches are applied + * is where the work will be skipped. It's done this way to ensure that + * patching can be disabled temporarily in a way that doesn't affect the + * contents of composer.lock. + * + * @param PackageEvent $event + * The PackageEvent passed by Composer + */ + public function resolvePatches(PackageEvent $event) + { + // No need to resolve patches more than once. + if ($this->patchesResolved) { + return; + } + + // Let each resolver discover patches and add them to the PatchCollection. + /** @var ResolverInterface $resolver */ + foreach ($this->getPatchResolvers() as $resolver) { + if (!in_array(get_class($resolver), $this->getConfig('disable-resolvers'))) { + $resolver->resolve($this->patchCollection, $event); + } else { + if ($this->io->isVerbose()) { + $this->io->write(' - Skipping resolver ' . get_class($resolver) . ''); + } + } + } + + // Make sure we only do this once. + $this->patchesResolved = true; + } + /** * Before running composer install, * @param Event $event @@ -186,163 +285,6 @@ public function checkPatches(Event $event) } } - /** - * Gather patches from dependencies and store them for later use. - * - * @param PackageEvent $event - */ - public function gatherPatches(PackageEvent $event) - { - // If we've already done this, then don't do it again. - if (isset($this->patches['_patchesGathered'])) { - $this->io->write('Patches already gathered. Skipping', true, IOInterface::VERBOSE); - return; - } // If patching has been disabled, bail out here. - elseif (!$this->isPatchingEnabled()) { - $this->io->write('Patching is disabled. Skipping.', true, IOInterface::VERBOSE); - return; - } - - $this->patches = $this->grabPatches(); - if (empty($this->patches)) { - $this->io->write('No patches supplied.'); - } - - $extra = $this->composer->getPackage()->getExtra(); - $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); - - // Now add all the patches from dependencies that will be installed. - if (!$this->getConfig('disable-patching-from-dependencies')) { - $operations = $event->getOperations(); - $this->io->write('Gathering patches for dependencies. This might take a minute.'); - foreach ($operations as $operation) { - if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') { - $package = $this->getPackageFromOperation($operation); - $extra = $package->getExtra(); - if (isset($extra['patches'])) { - if (isset($patches_ignore[$package->getName()])) { - foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { - if (isset($extra['patches'][$package_name])) { - $extra['patches'][$package_name] = array_diff( - $extra['patches'][$package_name], - $patches - ); - } - } - } - $this->patches = Util::arrayMergeRecursiveDistinct($this->patches, $extra['patches']); - } - // Unset installed patches for this package - if (isset($this->installedPatches[$package->getName()])) { - unset($this->installedPatches[$package->getName()]); - } - } - } - } - - // Merge installed patches from dependencies that did not receive an update. - foreach ($this->installedPatches as $patches) { -// $this->patches = Util::arrayMergeRecursiveDistinct($this->patches, $patches); - } - - // If we're in verbose mode, list the projects we're going to patch. - if ($this->io->isVerbose()) { - foreach ($this->patches as $package => $patches) { - $number = count($patches); - $this->io->write('Found ' . $number . ' patches for ' . $package . '.'); - } - } - - // Make sure we don't gather patches again. Extra keys in $this->patches - // won't hurt anything, so we'll just stash it there. - $this->patches['_patchesGathered'] = true; - } - - /** - * Get the patches from root composer or external file - * @return Patches - * @throws \Exception - */ - public function grabPatches() - { - // First, try to get the patches from the root composer.json. - $patches = []; - $extra = $this->composer->getPackage()->getExtra(); - if (isset($extra['patches'])) { - $this->io->write('Gathering patches for root package.'); - $patches = $extra['patches']; - } // If it's not specified there, look for a patches-file definition. - elseif ($this->getConfig('patches-file') != '') { - $this->io->write('Gathering patches from patch file.'); - $patches = file_get_contents($this->getConfig('patches-file')); - $patches = json_decode($patches, true); - $error = json_last_error(); - if ($error != 0) { - switch ($error) { - case JSON_ERROR_DEPTH: - $msg = ' - Maximum stack depth exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $msg = ' - Underflow or the modes mismatch'; - break; - case JSON_ERROR_CTRL_CHAR: - $msg = ' - Unexpected control character found'; - break; - case JSON_ERROR_SYNTAX: - $msg = ' - Syntax error, malformed JSON'; - break; - case JSON_ERROR_UTF8: - $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - default: - $msg = ' - Unknown error'; - break; - } - throw new \Exception('There was an error in the supplied patches file:' . $msg); - } - if (isset($patches['patches'])) { - $patches = $patches['patches']; - } elseif (!$patches) { - throw new \Exception('There was an error in the supplied patch file'); - } - } else { - return array(); - } - - // Now that we have a populated $patches list, populate patch objects for everything. - foreach ($patches as $package => $patch_defs) { - if (isset($patch_defs[0]) && is_array($patch_defs[0])) { - $this->io->write("Using expanded definition format for package {$package}"); - - foreach ($patch_defs as $index => $def) { - $patch = new Patch(); - $patch->package = $package; - $patch->url = $def["url"]; - $patch->description = $def["description"]; - - $patches[$package][$index] = $patch; - } - } else { - $this->io->write("Using compact definition format for package {$package}"); - - $temporary_patch_list = []; - - foreach ($patch_defs as $description => $url) { - $patch = new Patch(); - $patch->package = $package; - $patch->url = $url; - $patch->description = $description; - - $temporary_patch_list[] = $patch; - } - - $patches[$package] = $temporary_patch_list; - } - } - - return $patches; - } - /** * @param PackageEvent $event * @throws \Exception @@ -355,7 +297,7 @@ public function postInstall(PackageEvent $event) $package = $this->getPackageFromOperation($operation); $package_name = $package->getName(); - if (!isset($this->patches[$package_name])) { + if (empty($this->patchCollection->getPatchesForPackage($package_name))) { if ($this->io->isVerbose()) { $this->io->write('No patches found for ' . $package_name . '.'); } @@ -376,7 +318,7 @@ public function postInstall(PackageEvent $event) $extra = $localPackage->getExtra(); $extra['patches_applied'] = array(); - foreach ($this->patches[$package_name] as $index => $patch) { + foreach ($this->patchCollection->getPatchesForPackage($package_name) as $patch) { /** @var Patch $patch */ $this->io->write(' ' . $patch->url . ' (' . $patch->description . ')'); try { @@ -404,7 +346,7 @@ public function postInstall(PackageEvent $event) // $localPackage->setExtra($extra); $this->io->write(''); - $this->writePatchReport($this->patches[$package_name], $install_path); + $this->writePatchReport($this->patchCollection->getPatchesForPackage($package_name), $install_path); } /** diff --git a/src/Resolvers/DependencyPatches.php b/src/Resolvers/DependencyPatches.php new file mode 100644 index 00000000..9ee63496 --- /dev/null +++ b/src/Resolvers/DependencyPatches.php @@ -0,0 +1,68 @@ +io->write(' - Gathering patches from dependencies.'); + + $operations = $event->getOperations(); + foreach ($operations as $operation) { + if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') { + // @TODO handle exception. + $package = $this->getPackageFromOperation($operation); + /** @var PackageInterface $extra */ + $extra = $package->getExtra(); + if (isset($extra['patches'])) { + $patches = $this->findPatchesInJson($extra['patches']); + foreach ($patches as $package => $patch_list) { + foreach ($patch_list as $patch) { + $collection->addPatch($patch); + } + } + } + } + } + } + + /** + * Get a Package object from an OperationInterface object. + * + * @param OperationInterface $operation + * @return PackageInterface + * @throws \Exception + * + * @todo Will this method ever get something other than an InstallOperation or UpdateOperation? + */ + protected function getPackageFromOperation(OperationInterface $operation) + { + if ($operation instanceof InstallOperation) { + $package = $operation->getPackage(); + } elseif ($operation instanceof UpdateOperation) { + $package = $operation->getTargetPackage(); + } else { + throw new \Exception('Unknown operation: ' . get_class($operation)); + } + + return $package; + } +} diff --git a/src/Resolvers/PatchesFile.php b/src/Resolvers/PatchesFile.php new file mode 100644 index 00000000..4e4de17d --- /dev/null +++ b/src/Resolvers/PatchesFile.php @@ -0,0 +1,72 @@ +io->write(' - Gathering patches from patches file.'); + + $extra = $this->composer->getPackage()->getExtra(); + $valid_patches_file = array_key_exists('patches-file', $extra) && + file_exists(realpath($extra['patches-file'])) && + is_readable(realpath($extra['patches-file'])); + + // If we don't have a valid patches file, exit early. + if (!$valid_patches_file) { + return; + } + + $patches_file = $this->readPatchesFile($extra['patches-file']); + + foreach ($this->findPatchesInJson($patches_file) as $package => $patches) { + foreach ($patches as $patch) { + /** @var Patch $patch */ + $collection->addPatch($patch); + } + } + } + + /** + * Read a patches file. + * + * @param $patches_file + * A URI to a file. Can be anything accepted by file_get_contents(). + * @return array + * A list of patches. + * @throws \InvalidArgumentException + */ + protected function readPatchesFile($patches_file) + { + $patches = file_get_contents($patches_file); + $patches = json_decode($patches, true); + + // First, check for JSON syntax issues. + $json_error = json_last_error_msg(); + if ($json_error != "No error") { + throw new \InvalidArgumentException($json_error); + } + + // Next, make sure there is a patches key in the file. + if (!array_key_exists('patches', $patches)) { + throw new \InvalidArgumentException('No patches found.'); + } + + // If nothing is wrong at this point, we can return the list of patches. + return $patches['patches']; + } +} diff --git a/src/Resolvers/ResolverBase.php b/src/Resolvers/ResolverBase.php new file mode 100644 index 00000000..8462cb00 --- /dev/null +++ b/src/Resolvers/ResolverBase.php @@ -0,0 +1,93 @@ +composer = $composer; + $this->io = $io; + } + + /** + * {@inheritdoc} + */ + abstract public function resolve(PatchCollection $collection, PackageEvent $event); + + /** + * Handles the different patch definition formats and returns a list of Patches. + * + * @param array $patches + * An array of patch defs from composer.json or a patches file. + * + * @return array $patches + * An array of Patch objects grouped by package name. + */ + public function findPatchesInJson($patches) + { + // Given an array of patch data (pulled directly from the root composer.json + // or a patches file), figure out what patch format each package is using and + // marshall everything into Patch objects. + foreach ($patches as $package => $patch_defs) { + if (isset($patch_defs[0]) && is_array($patch_defs[0])) { + $this->io->write("Using expanded definition format for package {$package}"); + + foreach ($patch_defs as $index => $def) { + $patch = new Patch(); + $patch->package = $package; + $patch->url = $def["url"]; + $patch->description = $def["description"]; + + $patches[$package][$index] = $patch; + } + } else { + $this->io->write("Using compact definition format for package {$package}"); + + $temporary_patch_list = []; + + foreach ($patch_defs as $description => $url) { + $patch = new Patch(); + $patch->package = $package; + $patch->url = $url; + $patch->description = $description; + + $temporary_patch_list[] = $patch; + } + + $patches[$package] = $temporary_patch_list; + } + } + + return $patches; + } +} diff --git a/src/Resolvers/ResolverInterface.php b/src/Resolvers/ResolverInterface.php new file mode 100644 index 00000000..99d73cef --- /dev/null +++ b/src/Resolvers/ResolverInterface.php @@ -0,0 +1,42 @@ +io->write(' - Gathering patches from root package'); + + $extra = $this->composer->getPackage()->getExtra(); + + if (!isset($extra['patches'])) { + return; + } + + foreach ($this->findPatchesInJson($extra['patches']) as $package => $patches) { + foreach ($patches as $patch) { + /** @var Patch $patch */ + $collection->addPatch($patch); + } + } + } +} diff --git a/tests/_data/dep-test-package/composer.json b/tests/_data/dep-test-package/composer.json new file mode 100644 index 00000000..c1ad968a --- /dev/null +++ b/tests/_data/dep-test-package/composer.json @@ -0,0 +1,16 @@ +{ + "name": "cweagans/dep-test-package", + "description": "Project for use in cweagans/composer-patches acceptance tests.", + "type": "project", + "license": "BSD-2-Clause", + "require": { + "drupal/drupal": "8.2.0" + }, + "extra": { + "patches": { + "drupal/drupal": { + "Add a startup config for the PHP web server": "https://www.drupal.org/files/issues/add_a_startup-1543858-58.patch" + } + } + } +} diff --git a/tests/_support/Helper/Acceptance.php b/tests/_support/Helper/Acceptance.php index 2c811994..589f4ee0 100644 --- a/tests/_support/Helper/Acceptance.php +++ b/tests/_support/Helper/Acceptance.php @@ -20,8 +20,8 @@ public function _beforeSuite($settings = []) $this->_afterSuite(); } $filesystem->mkdir($this->_getPluginDir()); - $filesystem->mirror($this->_getProjectRoot() . '/src', $this->_getPluginDir() . '/src'); - $filesystem->copy($this->_getProjectRoot() . '/composer.json', $this->_getPluginDir() . '/composer.json'); + $filesystem->symlink($this->_getProjectRoot() . '/src', $this->_getPluginDir() . '/src'); + $filesystem->symlink($this->_getProjectRoot() . '/composer.json', $this->_getPluginDir() . '/composer.json'); } public function _afterSuite() diff --git a/tests/acceptance/ApplyPatchFromDependencyCept.php b/tests/acceptance/ApplyPatchFromDependencyCept.php new file mode 100644 index 00000000..bda7cfb4 --- /dev/null +++ b/tests/acceptance/ApplyPatchFromDependencyCept.php @@ -0,0 +1,6 @@ +wantTo('apply a patch defined in a dependency'); +$I->amInPath(realpath(__DIR__ . '/fixtures/apply-patch-from-dependency')); +$I->runShellCommand('composer install'); +$I->canSeeFileFound('./vendor/drupal/drupal/.ht.router.php'); diff --git a/tests/acceptance/DontApplyPatchFromDependencyCept.php b/tests/acceptance/DontApplyPatchFromDependencyCept.php new file mode 100644 index 00000000..0c9278da --- /dev/null +++ b/tests/acceptance/DontApplyPatchFromDependencyCept.php @@ -0,0 +1,6 @@ +wantTo('dont apply a patch defined in a dependency if the dependency patch resolver is disabled'); +$I->amInPath(realpath(__DIR__ . '/fixtures/dont-apply-patch-from-dependency')); +$I->runShellCommand('composer install'); +$I->cantSeeFileFound('./vendor/drupal/drupal/.ht.router.php'); diff --git a/tests/acceptance/fixtures/apply-patch-from-dependency/composer.json b/tests/acceptance/fixtures/apply-patch-from-dependency/composer.json new file mode 100644 index 00000000..cf161c75 --- /dev/null +++ b/tests/acceptance/fixtures/apply-patch-from-dependency/composer.json @@ -0,0 +1,20 @@ +{ + "name": "cweagans/composer-patches-test-project", + "description": "Project for use in cweagans/composer-patches acceptance tests.", + "type": "project", + "license": "BSD-2-Clause", + "repositories": [ + { + "type": "path", + "url": "../composer-patches" + }, + { + "type": "path", + "url": "../../../_data/dep-test-package" + } + ], + "require": { + "cweagans/composer-patches": "*@dev", + "cweagans/dep-test-package": "*@dev" + } +} diff --git a/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.json b/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.json new file mode 100644 index 00000000..5cf8ff4c --- /dev/null +++ b/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.json @@ -0,0 +1,25 @@ +{ + "name": "cweagans/composer-patches-test-project", + "description": "Project for use in cweagans/composer-patches acceptance tests.", + "type": "project", + "license": "BSD-2-Clause", + "repositories": [ + { + "type": "path", + "url": "../composer-patches" + }, + { + "type": "path", + "url": "../../../_data/dep-test-package" + } + ], + "require": { + "cweagans/composer-patches": "*@dev", + "cweagans/dep-test-package": "*@dev" + }, + "extra": { + "composer-patches": { + "disable-resolvers": ["cweagans\\Composer\\Resolvers\\DependencyPatches"] + } + } +} diff --git a/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.phar b/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.phar new file mode 100644 index 00000000..4055d874 Binary files /dev/null and b/tests/acceptance/fixtures/dont-apply-patch-from-dependency/composer.phar differ diff --git a/tests/unit/CoreResolverProviderTest.php b/tests/unit/CoreResolverProviderTest.php new file mode 100644 index 00000000..3ad9a328 --- /dev/null +++ b/tests/unit/CoreResolverProviderTest.php @@ -0,0 +1,26 @@ + Stub::make(\Composer\Composer::class), + 'io' => new \Composer\IO\NullIO(), + 'plugin' => Stub::makeEmpty(\Composer\Plugin\PluginInterface::class), + ]); + + $resolvers = $resolverProvider->getResolvers(); + + $this->assertCount(3, $resolvers); + $this->assertInstanceOf(\cweagans\Composer\Resolvers\RootComposer::class, $resolvers[0]); + $this->assertInstanceOf(\cweagans\Composer\Resolvers\PatchesFile::class, $resolvers[1]); + $this->assertInstanceOf(\cweagans\Composer\Resolvers\DependencyPatches::class, $resolvers[2]); + } +} diff --git a/tests/unit/DependencyPatchesResolverTest.php b/tests/unit/DependencyPatchesResolverTest.php new file mode 100644 index 00000000..13b122ca --- /dev/null +++ b/tests/unit/DependencyPatchesResolverTest.php @@ -0,0 +1,42 @@ +setExtra(['patches' => []]); + $composer = new Composer(); + $composer->setPackage($root_package); + $io = new NullIO(); + $event = Stub::make(PackageEvent::class, [ + 'getOperations' => function () { + return []; + }, + ]); + + // Empty patch list. + $resolver = new DependencyPatches($composer, $io); + $resolver->resolve($patch_collection, $event); + $this->assertCount(0, $patch_collection->getPatchesForPackage('test/package')); + + // @TODO: Add operations to the event and test that the resolver finds patches appropriately. + } +} diff --git a/tests/unit/PatchesFileResolverTest.php b/tests/unit/PatchesFileResolverTest.php new file mode 100644 index 00000000..83d77e2d --- /dev/null +++ b/tests/unit/PatchesFileResolverTest.php @@ -0,0 +1,63 @@ +setExtra([ + 'patches-file' => __DIR__ . '/../_data/dummyPatches.json', + ]); + $composer->setPackage($package); + $io = new NullIO(); + $event = Stub::make(PackageEvent::class, []); + + $collection = new PatchCollection(); + $resolver = new PatchesFile($composer, $io); + $resolver->resolve($collection, $event); + $this->assertCount(2, $collection->getPatchesForPackage('test/package')); + $this->assertCount(2, $collection->getPatchesForPackage('test/package2')); + + // Empty patches. + try { + $package = new RootPackage('test/package', '1.0.0.0', '1.0.0'); + $package->setExtra([ + 'patches-file' => __DIR__ . '/../_data/dummyPatchesEmpty.json', + ]); + $composer->setPackage($package); + $collection = new PatchCollection(); + $resolver = new PatchesFile($composer, $io); + $resolver->resolve($collection, $event); + } catch (\InvalidArgumentException $e) { + $this->assertEquals('No patches found.', $e->getMessage()); + } + + // Invalid JSON. + try { + $package = new RootPackage('test/package', '1.0.0.0', '1.0.0'); + $package->setExtra([ + 'patches-file' => __DIR__ . '/../_data/dummyPatchesInvalid.json', + ]); + $composer->setPackage($package); + $collection = new PatchCollection(); + $resolver = new PatchesFile($composer, $io); + $resolver->resolve($collection, $event); + } catch (\InvalidArgumentException $e) { + $this->assertEquals('Syntax error', $e->getMessage()); + } + } +} diff --git a/tests/unit/RootComposerResolverTest.php b/tests/unit/RootComposerResolverTest.php new file mode 100644 index 00000000..14b32991 --- /dev/null +++ b/tests/unit/RootComposerResolverTest.php @@ -0,0 +1,52 @@ +setExtra(['patches' => []]); + $composer = new Composer(); + $composer->setPackage($root_package); + $io = new NullIO(); + $event = Stub::make(PackageEvent::class, []); + + // Empty patch list. + $resolver = new RootComposer($composer, $io); + $resolver->resolve($patch_collection, $event); + $this->assertCount(0, $patch_collection->getPatchesForPackage('test/package')); + + // One patch. + $patch = new \stdClass(); + $patch->url = 'http://drupal.org'; + $patch->description = 'Test patch'; + $root_package->setExtra([ + 'patches' => [ + 'test/package' => [ + 0 => $patch, + ] + ] + ]); + + $composer->setPackage($root_package); + $resolver = new RootComposer($composer, $io); + $resolver->resolve($patch_collection, $event); + $this->assertCount(1, $patch_collection->getPatchesForPackage('test/package')); + } +}