diff --git a/core/core.services.yml b/core/core.services.yml index 6310c21..2925d99 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -298,7 +298,7 @@ services: - { name: event_subscriber } config.installer: class: Drupal\Core\Config\ConfigInstaller - arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] + arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '@profile_handler'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -506,6 +506,9 @@ services: - { name: module_install.uninstall_validator } arguments: ['@string_translation'] lazy: true + profile_handler: + class: Drupal\Core\Extension\ProfileHandler + arguments: ['@app.root', '@info_parser'] theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 1019270..e2b804a 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -439,6 +439,12 @@ function install_begin_request($class_loader, &$install_state) { if (isset($install_state['profile_info']['distribution']['install']['theme'])) { $install_state['theme'] = $install_state['profile_info']['distribution']['install']['theme']; } + // Ensure all profile directories are registered. + $profiles = \Drupal::service('profile_handler')->getProfiles($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $listing->setProfileDirectories($profile_directories); } // Use the language from the profile configuration, if available, to override @@ -1191,6 +1197,8 @@ function install_select_profile(&$install_state) { * - For non-interactive installations via install_drupal() settings. * - A discovered profile that is a distribution. If multiple profiles are * distributions, then the first discovered profile will be selected. + * If an inherited profile is detected that is a distribution, it will be + * chosen over its base profile. * - Only one visible profile is available. * * @param array $install_state @@ -1213,11 +1221,8 @@ function _install_select_profile(&$install_state) { } } // Check for a distribution profile. - foreach ($install_state['profiles'] as $profile) { - $profile_info = install_profile_info($profile->getName()); - if (!empty($profile_info['distribution'])) { - return $profile->getName(); - } + if ($distribution = \Drupal::service('profile_handler')->selectDistribution(array_keys($install_state['profiles']))) { + return $distribution; } // Get all visible (not hidden) profiles. @@ -1550,7 +1555,10 @@ function install_profile_themes(&$install_state) { * An array of information about the current installation state. */ function install_install_profile(&$install_state) { - \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE); + $profiles = \Drupal::service('profile_handler')->getProfiles(); + + // Install all the profiles. + \Drupal::service('module_installer')->install(array_keys($profiles), FALSE); // Install all available optional config. During installation the module order // is determined by dependencies. If there are no dependencies between modules // then the order in which they are installed is dependent on random factors diff --git a/core/includes/install.inc b/core/includes/install.inc index 3a9c2bc..2d21030 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1029,6 +1029,14 @@ function drupal_check_module($module) { * - install: Optional parameters to override the installer: * - theme: The machine name of a theme to use in the installer instead of * Drupal's default installer theme. + * - base profile: Existence of this key denotes that the installation profile + * depends on a parent installation profile. + * - name: The shortname of the base installation profile. + * - excluded_dependencies: An array of shortnames of other modules that have + * to be excluded from the base profile requirements. This allows e.g. to + * disable a demo module that would be installed by the base profile. + * If there are no excluded_dependencies, a shortcut of "base profile: name" + * can be used. * * Note that this function does an expensive file system scan to get info file * information for dependencies. If you only need information from the info @@ -1054,18 +1062,7 @@ function install_profile_info($profile, $langcode = 'en') { $cache = &drupal_static(__FUNCTION__, array()); if (!isset($cache[$profile][$langcode])) { - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'themes' => array('stark'), - 'description' => '', - 'version' => NULL, - 'hidden' => FALSE, - 'php' => DRUPAL_MINIMUM_PHP, - ); - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); - $info += $defaults; + $info = \Drupal::service('profile_handler')->getProfileInfo($profile); // drupal_required_modules() includes the current profile as a dependency. // Remove that dependency, since a module cannot depend on itself. diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 97c3688..ad29260 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -7,6 +7,7 @@ use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; use Drupal\Core\Site\Settings; +use Drupal\Core\Extension\ProfileHandlerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -54,6 +55,13 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $sourceStorage; /** + * The profile handler. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Is configuration being created as part of a configuration sync. * * @var bool @@ -73,13 +81,16 @@ class ConfigInstaller implements ConfigInstallerInterface { * The configuration manager. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * The profile handler. */ - public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) { + public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, ProfileHandlerInterface $profile_handler) { $this->configFactory = $config_factory; $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; + $this->profileHandler = $profile_handler; } /** @@ -462,7 +473,8 @@ public function checkConfigurationToInstall($type, $name) { // Install profiles can not have config clashes. Configuration that // has the same name as a module's configuration will be used instead. - if ($name != $this->drupalGetProfile()) { + $profiles = $this->profileHandler->getProfiles(); + if (!isset($profiles[$name])) { // Throw an exception if the module being installed contains configuration // that already exists. Additionally, can not continue installing more // modules because those may depend on the current module being installed. diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 14e80dd..a400ae3 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -78,7 +78,6 @@ protected function getAllFolders() { $this->folders += $this->getCoreNames(); $install_profile = Settings::get('install_profile'); - $profile = drupal_get_profile(); $extensions = $this->configStorage->read('core.extension'); // @todo Remove this scan as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); @@ -86,14 +85,6 @@ protected function getAllFolders() { $modules = $extensions['module']; // Remove the install profile as this is handled later. unset($modules[$install_profile]); - $profile_list = $listing->scan('profile'); - if ($profile && isset($profile_list[$profile])) { - // Prime the drupal_get_filename() static cache with the profile info - // file location so we can use drupal_get_path() on the active profile - // during the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname()); - } $module_list_scan = $listing->scan('module'); $module_list = array(); foreach (array_keys($modules) as $module) { @@ -114,18 +105,11 @@ protected function getAllFolders() { } if ($this->includeProfile) { - // The install profile can override module default configuration. We do - // this by replacing the config file path from the module/theme with the - // install profile version if there are any duplicates. - if (isset($profile)) { - if (!isset($profile_list)) { - $profile_list = $listing->scan('profile'); - } - if (isset($profile_list[$profile])) { - $profile_folders = $this->getComponentNames(array($profile_list[$profile])); - $this->folders = $profile_folders + $this->folders; - } - } + // The install profile (and any parent profiles) can override module + // default configuration. We do this by replacing the config file path + // from the module/theme with the install profile version if there are + // any duplicates. + $this->folders += $this->getComponentNames(\Drupal::service('profile_handler')->getProfiles($install_profile)); } } return $this->folders; diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index bd9bf30..80d370d 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -151,21 +151,12 @@ protected function getAllFolders() { if (!isset($this->folders)) { $this->folders = array(); $this->folders += $this->getCoreNames(); + // Get dependent profiles and add the extension components. + $this->folders += $this->getComponentNames(\Drupal::service('profile_handler')->getProfiles()); // Perform an ExtensionDiscovery scan as we cannot use drupal_get_path() // yet because the system module may not yet be enabled during install. // @todo Remove as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); - if ($profile = drupal_get_profile()) { - $profile_list = $listing->scan('profile'); - if (isset($profile_list[$profile])) { - // Prime the drupal_get_filename() static cache with the profile info - // file location so we can use drupal_get_path() on the active profile - // during the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname()); - $this->folders += $this->getComponentNames(array($profile_list[$profile])); - } - } // @todo Remove as part of https://www.drupal.org/node/2186491 $this->folders += $this->getComponentNames($listing->scan('module')); $this->folders += $this->getComponentNames($listing->scan('theme')); diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 0d9283c..b27d567 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -99,6 +99,15 @@ class ExtensionDiscovery { protected $sitePath; /** + * The profile handler. + * + * It is used to determine the directories we want to scan modules for. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root @@ -109,12 +118,22 @@ class ExtensionDiscovery { * The available profile directories * @param string $site_path * The path to the site. + * @param Drupal\Core\Extension\ProfileHandlerInterface + * The Profile handler. */ - public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL, $profile_handler = NULL) { $this->root = $root; $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; $this->profileDirectories = $profile_directories; $this->sitePath = $site_path; + // ExtensionDiscovery can be called without a service container. + // (@drupalKernel::moduleData) so check if profile_handler is available. + if (!isset($profile_handler) && \Drupal::hasService('profile_handler')) { + $this->profileHandler = \Drupal::service('profile_handler'); + } + else { + $this->profileHandler = $profile_handler; + } } /** @@ -248,8 +267,18 @@ public function setProfileDirectoriesFromSettings() { // In case both profile directories contain the same extension, the actual // profile always has precedence. if ($profile) { - $this->profileDirectories[] = drupal_get_path('profile', $profile); + if (isset($this->profileHandler)) { + $profiles = $this->profileHandler->getProfiles($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $this->profileDirectories = array_unique(array_merge($profile_directories, $this->profileDirectories)); + } + else { + $this->profileDirectories[] = drupal_get_path('profile', $profile); + } } + return $this; } diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php new file mode 100644 index 0000000..43030a9 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -0,0 +1,302 @@ +root = $root; + $this->infoParser = $info_parser; + $this->extensionDiscovery = $extension_discovery; + } + + /** + * Returns an extension discovery object. + * + * @return \Drupal\Core\Extension\ExtensionDiscovery + * The extension discovery object. + */ + protected function getExtensionDiscovery() { + if (!isset($this->extensionDiscovery)) { + $this->extensionDiscovery = new ExtensionDiscovery($this->root); + } + return $this->extensionDiscovery; + } + + /** + * Return the full path to a profile. + * + * Wrapper around drupal_get_path. If profile path is not available yet we + * call scan('profile') and prime the cache. + * + * @param string $profile + * The name of the profile. + * + * @return string + * The full path to the profile. + */ + protected function getProfilePath($profile) { + // Check to see if system_rebuild_module_data cache is primed. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $modules_cache = &drupal_static('system_rebuild_module_data'); + if (!$this->scanCache && !isset($modules_cache)) { + $listing = $this->getExtensionDiscovery(); + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan requires knowing what the active profile is. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $profiles = $listing->scan('profile'); + foreach ($profiles as $profile_name => $extension) { + // Prime the drupal_get_filename() static cache with the profile info file + // location so we can use drupal_get_path() on the active profile during + // the module scan. + // @todo Remove as part of https://www.drupal.org/node/2186491. + drupal_get_filename('profile', $profile_name, $extension->getPathname()); + } + $this->scanCache = TRUE; + } + return drupal_get_path('profile', $profile); + } + + /** + * {@inheritdoc} + */ + public function getProfileInfo($profile) { + // Even though info_parser caches the info array, we need to also cache + // this since it is recursive. + if (!isset($this->infoCache[$profile])) { + + // Set defaults for profile info. + $defaults = [ + 'dependencies' => [], + 'themes' => ['stark'], + 'description' => '', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + 'base profile' => [ + 'name' => '', + 'excluded_dependencies' => [] + ], + ]; + + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $info = $this->infoParser->parse($profile_file); + $info += $defaults; + + // Normalize any base profile info. + if (is_string($info['base profile'])) { + $info['base profile'] = [ + 'name' => $info['base profile'], + 'excluded_dependencies' => [], + ]; + } + + $profile_list = []; + // Get the base profile dependencies. + if ($base_profile_name = $info['base profile']['name']) { + $base_info = $this->getProfileInfo($base_profile_name); + $profile_list += $base_info['profile_list']; + + // Ensure all dependencies are cleanly merged. + $info['dependencies'] = array_merge($info['dependencies'], $base_info['dependencies']); + // Apply excluded dependencies. + $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']); + // Ensure there's no circular dependency. + $info['dependencies'] = array_diff($info['dependencies'], [$profile]); + } + $profile_list[$profile] = $profile; + $info['profile_list'] = $profile_list; + + // Ensure the same dependency notation as in modules can be used. + array_walk($info['dependencies'], function(&$dependency) { + $dependency = ModuleHandler::parseDependency($dependency)['name']; + }); + + // Installation profiles are hidden by default, unless explicitly specified + // otherwise in the .info.yml file. + $info['hidden'] = isset($info['hidden']) ? $info['hidden'] : TRUE; + + $this->infoCache[$profile] = $info; + } + return $this->infoCache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function setProfileInfo($profile, $info) { + $this->infoCache[$profile] = $info; + // Also unset the cached profile extension so the updated info will + // be picked up. + unset($this->cache[$profile]); + } + + /** + * {@inheritdoc} + */ + public function clearProfileCache() { + $this->cache = []; + $this->infoCache = []; + } + + /** + * Create an Extension object for a profile. + * + * @param string $profile + * The name of the profile. + * + * @return \Drupal\Core\Extension\Extension + * The extension object for the profile + * Properties added to extension: + * info: The parsed info.yml data. + * origin: The directory origin as used in ExtensionDiscovery. + */ + protected function getProfileExtension($profile) { + $profile_info = $this->getProfileInfo($profile); + + $type = $profile_info['type']; + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL; + $extension = new Extension($this->root, $type, $profile_file, $filename); + + $extension->info = $profile_info; + $extension->origin = ''; + + return $extension; + } + + /** + * Get a list of dependent profile names. + * + * @param string $profile + * Name of profile. + * + * @return string[] + * An associative array of profile names, keyed by profile name + * in descending order of their dependencies. + * (parent profiles first, main profile last) + */ + protected function getProfileList($profile) { + $profile_info = $this->getProfileInfo($profile); + return $profile_info['profile_list']; + } + + /** + * {@inheritdoc} + */ + public function getProfiles($profile = NULL) { + if (empty($profile)) { + $profile = drupal_get_profile(); + } + if (!isset($this->cache[$profile])) { + $profiles = []; + // Check if a valid profile name was given. + if (!empty($profile)) { + $list = $this->getProfileList($profile); + + // Starting weight for profiles ensures their hooks run last. + $weight = 1000; + + // Loop through profile list and create Extension objects. + $profiles = []; + foreach ($list as $profile_name) { + $extension = $this->getProfileExtension($profile_name); + $extension->weight = $weight; + $weight++; + $profiles[$profile_name] = $extension; + } + } + $this->cache[$profile] = $profiles; + } + return $this->cache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function selectDistribution($profile_list) { + // First, find all profiles marked as distributions + $distributions = []; + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if (!empty($profile_info['distribution'])) { + $distributions[$profile_name] = $profile_name; + } + } + // Remove any base profiles. + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if ($base_profile = $profile_info['base profile']['name']) { + unset($distributions[$base_profile]); + } + } + return !empty($distributions) ? current($distributions) : NULL; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php new file mode 100644 index 0000000..938e3eb --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -0,0 +1,75 @@ +scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { - // Prime the drupal_get_filename() static cache with the profile info file - // location so we can use drupal_get_path() on the active profile during - // the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491. - drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname()); - } - // Find modules. $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. - if ($profile) { - $modules[$profile] = $profiles[$profile]; - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; - } + + // Find profiles. + /** @var \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler */ + $profile_handler = \Drupal::service('profile_handler'); + $modules = array_merge($modules, $profile_handler->getProfiles()); // Set defaults for module info. $defaults = array( @@ -993,7 +979,14 @@ function _system_rebuild_module_data() { // Read info files for each module. foreach ($modules as $key => $module) { // Look for the info file. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + // @todo each extension handler should be responsible for loading the + // info data. For now we just have a ProfileHandler. + if ($module->getType() == 'profile') { + $module->info = $profile_handler->getProfileInfo($module->getName()); + } + else { + $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + } // Add the info file modification time, so it becomes available for // contributed modules to use for ordering module lists. @@ -1002,12 +995,6 @@ function _system_rebuild_module_data() { // Merge in defaults and save. $modules[$key]->info = $module->info + $defaults; - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { - $modules[$key]->info['hidden'] = TRUE; - } - // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info.yml files if necessary. // @todo Remove $type argument, obsolete with $module->getType(). @@ -1021,7 +1008,8 @@ function _system_rebuild_module_data() { _system_rebuild_module_data_ensure_required($module, $modules); } - + // This must be done after _system_rebuild_module_data_ensure_required(). + $profile = drupal_get_profile(); if ($profile && isset($modules[$profile])) { // The installation profile is required, if it's a valid module. $modules[$profile]->info['required'] = TRUE; diff --git a/core/profiles/testing_inherited/testing_inherited.info.yml b/core/profiles/testing_inherited/testing_inherited.info.yml new file mode 100644 index 0000000..3ca5802 --- /dev/null +++ b/core/profiles/testing_inherited/testing_inherited.info.yml @@ -0,0 +1,14 @@ +name: Testing Inherited +type: profile +description: 'Profile for testing base profile inheritance.' +version: VERSION +core: 8.x +hidden: true + +base profile: + name: minimal + excluded_dependencies: + - dblog + +dependencies: + - config diff --git a/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php new file mode 100644 index 0000000..4592f76 --- /dev/null +++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php @@ -0,0 +1,33 @@ +drupalGet(''); + // Check the login block is present. + $this->assertSession()->linkExists(t('Create new account')); + $this->assertSession()->statusCodeEquals(200); + + // Check the excluded_dependencies flag on installation profiles. + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config')); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('dblog')); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php new file mode 100644 index 0000000..987e930 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php @@ -0,0 +1,107 @@ +container->get('profile_handler'); + $info = $profile_handler->getProfileInfo('testing_inherited'); + $this->assertNotEmpty($info); + $this->assertEquals($info['name'], 'Testing Inherited'); + $this->assertEquals($info['base profile']['name'], 'minimal'); + $this->assertEquals($info['base profile']['excluded_dependencies'], ['dblog']); + $this->assertTrue(in_array('config', $info['dependencies'], 'config should be found in dependencies')); + $this->assertFalse(in_array('dblog', $info['dependencies'], 'dblog should not be found in dependencies')); + $this->assertTrue($info['hidden'], 'Profiles should be hidden'); + $this->assertNotEmpty($info['profile_list']); + $profile_list = $info['profile_list']; + // Testing order of profile list. + $this->assertEquals($profile_list, [ + 'minimal' => 'minimal', + 'testing_inherited' => 'testing_inherited' + ]); + + // Test that profiles without any base return normalized info. + $info = $profile_handler->getProfileInfo('minimal'); + $this->assertEquals($info['base profile'], ['name' => '', 'excluded_dependencies' => []]); + } + + /** + * Tests getting profile dependency list. + * + * @covers ::getProfiles + */ + public function testGetProfiles() { + $profile_handler = $this->container->get('profile_handler'); + $profiles = $profile_handler->getProfiles('testing_inherited'); + $this->assertCount(2, $profiles); + + $first_profile = current($profiles); + $this->assertEquals(get_class($first_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($first_profile->getName(), 'minimal'); + $this->assertEquals($first_profile->weight, 1000); + $this->assertObjectHasAttribute('origin', $first_profile); + + $second_profile = next($profiles); + $this->assertEquals(get_class($second_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($second_profile->getName(), 'testing_inherited'); + $this->assertEquals($second_profile->weight, 1001); + $this->assertObjectHasAttribute('origin', $second_profile); + } + + /** + * @covers ::selectDistribution + * @covers ::setProfileInfo + */ + public function testSelectDistribution() { + /** @var \Drupal\Core\Extension\ProfileHandler $profile_handler */ + $profile_handler = $this->container->get('profile_handler'); + $profiles = ['minimal', 'testing_inherited']; + $base_info = $profile_handler->getProfileInfo('minimal'); + $profile_info = $profile_handler->getProfileInfo('testing_inherited'); + + // Neither profile has distribution set + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'No distribution should be selected'); + + // Set base profile distribution + $base_info['distribution']['name'] = 'Minimal'; + $profile_handler->setProfileInfo('minimal', $base_info); + // Base profile distribution should not be selected + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'Base profile distribution should not be selected'); + + // Set main profile distribution + $profile_info['distribution']['name'] = 'Testing Inherited'; + $profile_handler->setProfileInfo('testing_inherited', $profile_info); + // Main profile distribution should be selected + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEquals($distribution, 'testing_inherited'); + } + +}