ModuleInstallerTest.php

Same filename and directory in other branches
  1. 9 core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
  2. 8.9.x core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
  3. 10 core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php

Namespace

Drupal\KernelTests\Core\Extension

File

core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php

View source
<?php

declare (strict_types=1);
namespace Drupal\KernelTests\Core\Extension;

use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Extension\Exception\ObsoleteExtensionException;
use Drupal\Core\Extension\ModuleInstaller;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\KernelTests\KernelTestBase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;

/**
 * Tests the ModuleInstaller class.
 *
 * @coversDefaultClass \Drupal\Core\Extension\ModuleInstaller
 *
 * @group Extension
 */
class ModuleInstallerTest extends KernelTestBase implements LoggerInterface {
  use RfcLoggerTrait;
  
  /**
   * Tests that routes are rebuilt during install and uninstall of modules.
   *
   * @covers ::install
   * @covers ::uninstall
   */
  public function testRouteRebuild() : void {
    // Remove the routing table manually to ensure it can be created lazily
    // properly.
    Database::getConnection()->schema()
      ->dropTable('router');
    $this->container
      ->get('module_installer')
      ->install([
      'router_test',
    ]);
    $route = $this->container
      ->get('router.route_provider')
      ->getRouteByName('router_test.1');
    $this->assertEquals('/router_test/test1', $route->getPath());
    $this->container
      ->get('module_installer')
      ->uninstall([
      'router_test',
    ]);
    $this->expectException(RouteNotFoundException::class);
    $this->container
      ->get('router.route_provider')
      ->getRouteByName('router_test.1');
  }
  
  /**
   * Tests config changes by hook_install() are saved for dependent modules.
   *
   * @covers ::install
   */
  public function testConfigChangeOnInstall() : void {
    // Install the child module so the parent is installed automatically.
    $this->container
      ->get('module_installer')
      ->install([
      'module_handler_test_multiple_child',
    ]);
    $modules = $this->config('core.extension')
      ->get('module');
    $this->assertArrayHasKey('module_handler_test_multiple', $modules, 'Module module_handler_test_multiple is installed');
    $this->assertArrayHasKey('module_handler_test_multiple_child', $modules, 'Module module_handler_test_multiple_child is installed');
    $this->assertEquals(1, $modules['module_handler_test_multiple'], 'Weight of module_handler_test_multiple is set.');
    $this->assertEquals(1, $modules['module_handler_test_multiple_child'], 'Weight of module_handler_test_multiple_child is set.');
  }
  
  /**
   * Tests cache bins defined by modules are removed when uninstalled.
   *
   * @covers ::removeCacheBins
   */
  public function testCacheBinCleanup() : void {
    $schema = $this->container
      ->get('database')
      ->schema();
    $table = 'cache_module_cache_bin';
    $module_installer = $this->container
      ->get('module_installer');
    $module_installer->install([
      'module_cache_bin',
    ]);
    // Prime the bin.
    /** @var \Drupal\Core\Cache\CacheBackendInterface $cache_bin */
    $cache_bin = $this->container
      ->get('module_cache_bin.cache_bin');
    $cache_bin->set('foo', 'bar');
    // A database backend is used so there is a convenient way check whether the
    // backend is uninstalled.
    $this->assertTrue($schema->tableExists($table));
    $module_installer->uninstall([
      'module_cache_bin',
    ]);
    $this->assertFalse($schema->tableExists($table));
  }
  
  /**
   * Ensure that rebuilding the container in hook_install() works.
   */
  public function testKernelRebuildDuringHookInstall() : void {
    \Drupal::state()->set('module_test_install:rebuild_container', TRUE);
    $module_installer = $this->container
      ->get('module_installer');
    $this->assertTrue($module_installer->install([
      'module_test',
    ]));
  }
  
  /**
   * Ensure that hooks reacting to install or uninstall are invoked.
   */
  public function testInvokingRespondentHooks() : void {
    $module_installer = $this->container
      ->get('module_installer');
    $this->assertTrue($module_installer->install([
      'respond_install_uninstall_hook_test',
    ]));
    $this->assertTrue($module_installer->install([
      'cache_test',
    ]));
    $this->assertTrue(isset($GLOBALS['hook_module_preinstall']));
    $this->assertTrue(isset($GLOBALS['hook_modules_installed']));
    $module_installer->uninstall([
      'cache_test',
    ]);
    $this->assertTrue(isset($GLOBALS['hook_module_preuninstall']));
    $this->assertTrue(isset($GLOBALS['hook_modules_uninstalled']));
    $this->assertTrue(isset($GLOBALS['hook_cache_flush']));
  }
  
  /**
   * Tests install with a module with an invalid core version constraint.
   *
   * @dataProvider providerTestInvalidCoreInstall
   * @covers ::install
   */
  public function testInvalidCoreInstall($module_name, $install_dependencies) : void {
    $this->expectException(MissingDependencyException::class);
    $this->expectExceptionMessage("Unable to install modules: module '{$module_name}' is incompatible with this version of Drupal core.");
    $this->container
      ->get('module_installer')
      ->install([
      $module_name,
    ], $install_dependencies);
  }
  
  /**
   * Data provider for testInvalidCoreInstall().
   */
  public static function providerTestInvalidCoreInstall() {
    return [
      'no dependencies system_core_incompatible_semver_test' => [
        'system_core_incompatible_semver_test',
        FALSE,
      ],
      'install_dependencies system_core_incompatible_semver_test' => [
        'system_core_incompatible_semver_test',
        TRUE,
      ],
    ];
  }
  
  /**
   * Tests install with a dependency with an invalid core version constraint.
   *
   * @covers ::install
   */
  public function testDependencyInvalidCoreInstall() : void {
    $this->expectException(MissingDependencyException::class);
    $this->expectExceptionMessage("Unable to install modules: module 'system_incompatible_core_version_dependencies_test'. Its dependency module 'system_core_incompatible_semver_test' is incompatible with this version of Drupal core.");
    $this->container
      ->get('module_installer')
      ->install([
      'system_incompatible_core_version_dependencies_test',
    ]);
  }
  
  /**
   * Tests no dependencies install with a dependency with invalid core.
   *
   * @covers ::install
   */
  public function testDependencyInvalidCoreInstallNoDependencies() : void {
    $this->assertTrue($this->container
      ->get('module_installer')
      ->install([
      'system_incompatible_core_version_dependencies_test',
    ], FALSE));
  }
  
  /**
   * Tests trying to install an obsolete module.
   *
   * @covers ::install
   */
  public function testObsoleteInstall() : void {
    $this->expectException(ObsoleteExtensionException::class);
    $this->expectExceptionMessage("Unable to install modules: module 'system_status_obsolete_test' is obsolete.");
    $this->container
      ->get('module_installer')
      ->install([
      'system_status_obsolete_test',
    ]);
  }
  
  /**
   * Tests container rebuilding due to the container_rebuild_required info key.
   *
   * @param array $modules
   *   The modules to install.
   * @param int $count
   *   The number of times the container should have been rebuilt.
   *
   * @covers ::install
   * @dataProvider containerRebuildRequiredProvider
   */
  public function testContainerRebuildRequired(array $modules, int $count) : void {
    $this->container
      ->get('module_installer')
      ->install([
      'module_test',
    ]);
    $GLOBALS['container_rebuilt'] = 0;
    $this->container
      ->get('module_installer')
      ->install($modules);
    $this->assertSame($count, $GLOBALS['container_rebuilt']);
  }
  
  /**
   * Data provider for ::testContainerRebuildRequired().
   */
  public static function containerRebuildRequiredProvider() : array {
    return [
      [
        [
          'container_rebuild_required_true',
        ],
        1,
      ],
      [
        [
          'container_rebuild_required_false',
        ],
        1,
      ],
      [
        [
          'container_rebuild_required_false',
          'container_rebuild_required_false_2',
        ],
        1,
      ],
      [
        [
          'container_rebuild_required_false',
          'container_rebuild_required_false_2',
          'container_rebuild_required_true',
        ],
        2,
      ],
      [
        [
          'container_rebuild_required_false',
          'container_rebuild_required_false_2',
          'container_rebuild_required_true',
          'container_rebuild_required_true_2',
        ],
        3,
      ],
      [
        [
          'container_rebuild_required_true',
          'container_rebuild_required_false',
          'container_rebuild_required_false_2',
        ],
        2,
      ],
      [
        [
          'container_rebuild_required_false',
          'container_rebuild_required_true',
          'container_rebuild_required_false_2',
        ],
        3,
      ],
      [
        [
          'container_rebuild_required_false',
          'container_rebuild_required_true',
          'container_rebuild_required_false_2',
          'container_rebuild_required_true_2',
        ],
        4,
      ],
      [
        [
          'container_rebuild_required_true',
          'container_rebuild_required_false',
          'container_rebuild_required_true_2',
          'container_rebuild_required_false_2',
        ],
        4,
      ],
      [
        [
          'container_rebuild_required_false_2',
          'container_rebuild_required_dependency_false',
        ],
        3,
      ],
      [
        [
          'container_rebuild_required_false_2',
          'container_rebuild_required_dependency_false',
          'container_rebuild_required_true',
        ],
        3,
      ],
    ];
  }
  
  /**
   * Tests trying to install a deprecated module.
   *
   * @covers ::install
   *
   * @group legacy
   */
  public function testDeprecatedInstall() : void {
    $this->expectDeprecation("The module 'deprecated_module' is deprecated. See http://example.com/deprecated");
    \Drupal::service('module_installer')->install([
      'deprecated_module',
    ]);
    $this->assertTrue(\Drupal::service('module_handler')->moduleExists('deprecated_module'));
  }
  
  /**
   * Tests the BC layer for uninstall validators.
   *
   * @covers ::__construct
   * @covers ::addUninstallValidator
   *
   * @group legacy
   */
  public function testUninstallValidatorsBC() : void {
    $this->expectDeprecation('The "module_installer.uninstall_validators" service is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Inject "!tagged_iterator module_install.uninstall_validator" instead. See https://www.drupal.org/node/3432595');
    $module_installer = new ModuleInstaller($this->container
      ->getParameter('app.root'), $this->container
      ->get('module_handler'), $this->container
      ->get('kernel'), $this->container
      ->get('database'), $this->container
      ->get('update.update_hook_registry'), $this->container
      ->get('logger.channel.default'));
    $this->expectDeprecation('Drupal\\Core\\Extension\\ModuleInstaller::addUninstallValidator is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Inject the uninstall validators into the constructor instead. See https://www.drupal.org/node/3432595');
    $module_installer->addUninstallValidator($this->createMock(ModuleUninstallValidatorInterface::class));
  }
  
  /**
   * Tests field storage definitions are installed only if entity types exist.
   */
  public function testFieldStorageEntityTypeDependencies() : void {
    $profile = 'minimal';
    $this->setInstallProfile($profile);
    // Install a module that will make workspaces a dependency of taxonomy.
    \Drupal::service('module_installer')->install([
      'field_storage_entity_type_dependency_test',
    ]);
    // Installing taxonomy will install workspaces first. During installation of
    // workspaces, the storage for 'workspace' field should not be attempted
    // before the taxonomy term entity storage has been created, so there
    // should not be a EntityStorageException logged.
    \Drupal::service('module_installer')->install([
      'taxonomy',
    ]);
    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('workspaces'));
    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('taxonomy'));
    $this->assertArrayHasKey('workspace', \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('taxonomy_term'));
  }
  
  /**
   * Tests that entity storage tables are installed before simple config.
   *
   * When multiple modules are installed together in one batch, entity storage
   * for entity types in all the modules should exist before simple config from
   * any module is installed.
   */
  public function testEntityStorageInstalledBeforeSimpleConfig() : void {
    \Drupal::service('module_installer')->install([
      'node',
      'module_installer_config_subscriber',
    ]);
    // The module_installer_config_subscriber module has an config save event
    // subscriber for its own simple config. When that config is saved during
    // module installed, it checks that the node storage table exists.
    $this->assertNotTrue(\Drupal::keyValue('module_installer_config_subscriber')->get('node_tables_missing'));
  }
  
  /**
   * {@inheritdoc}
   */
  public function register(ContainerBuilder $container) : void {
    parent::register($container);
    $container->register(__CLASS__, __CLASS__)
      ->setSynthetic(TRUE)
      ->addTag('logger');
    $container->set(__CLASS__, $this);
  }
  
  /**
   * {@inheritdoc}
   */
  public function log($level, \Stringable|string $message, array $context = []) : void {
    if ($level > RfcLogLevel::ERROR) {
      return;
    }
    // Fails the test if an error or more severe message is logged.
    $message = (string) $message;
    $placeholders = \Drupal::service('logger.log_message_parser')->parseMessagePlaceholders($message, $context);
    $message = empty($placeholders) ? $message : strtr($message, $placeholders);
    $this->fail($message);
  }

}

Classes

Title Deprecated Summary
ModuleInstallerTest Tests the ModuleInstaller class.

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